From df3ab7657c1f32d105424ffdabbf5f917678f123 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 16 Jul 2025 17:10:40 -0400 Subject: [PATCH 01/91] program: make lp shares reduce only --- programs/drift/src/instructions/user.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 27a2cf18d0..3717db74cc 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2937,6 +2937,9 @@ pub fn handle_add_perp_lp_shares<'c: 'info, 'info>( let clock = Clock::get()?; let now = clock.unix_timestamp; + msg!("add_perp_lp_shares is disabled"); + return Err(ErrorCode::DefaultError.into()); + let AccountMaps { perp_market_map, spot_market_map, @@ -3054,9 +3057,10 @@ pub fn handle_remove_perp_lp_shares_in_expiring_market<'c: 'info, 'info>( // additional validate { + let signer_is_admin = ctx.accounts.signer.key() == admin_hot_wallet::id(); let market = perp_market_map.get_ref(&market_index)?; validate!( - market.is_reduce_only()?, + market.is_reduce_only()? || signer_is_admin, ErrorCode::PerpMarketNotInReduceOnly, "Can only permissionless burn when market is in reduce only" )?; @@ -4630,6 +4634,7 @@ pub struct RemoveLiquidityInExpiredMarket<'info> { pub state: Box>, #[account(mut)] pub user: AccountLoader<'info, User>, + pub signer: Signer<'info>, } #[derive(Accounts)] From fed9dc6c8a3abe194b4179ee65524d57acc38111 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 11:01:16 -0400 Subject: [PATCH 02/91] init --- programs/drift/src/controller/liquidation.rs | 63 +- programs/drift/src/controller/lp.rs | 401 ---- programs/drift/src/controller/lp/tests.rs | 1818 ----------------- programs/drift/src/controller/mod.rs | 1 - programs/drift/src/controller/orders.rs | 152 +- programs/drift/src/controller/pnl.rs | 54 +- programs/drift/src/controller/position.rs | 108 +- .../drift/src/controller/position/tests.rs | 605 +----- programs/drift/src/instructions/admin.rs | 33 - programs/drift/src/instructions/keeper.rs | 31 - programs/drift/src/instructions/user.rs | 226 +- programs/drift/src/lib.rs | 38 - programs/drift/src/math/bankruptcy.rs | 1 - programs/drift/src/math/cp_curve/tests.rs | 177 +- programs/drift/src/math/lp.rs | 191 -- programs/drift/src/math/lp/tests.rs | 451 ---- programs/drift/src/math/margin.rs | 14 +- programs/drift/src/math/margin/tests.rs | 148 -- programs/drift/src/math/mod.rs | 1 - programs/drift/src/math/orders.rs | 1 - programs/drift/src/math/position.rs | 68 +- programs/drift/src/state/perp_market.rs | 23 - programs/drift/src/state/user.rs | 118 +- programs/drift/src/validation/position.rs | 8 - 24 files changed, 46 insertions(+), 4685 deletions(-) delete mode 100644 programs/drift/src/controller/lp.rs delete mode 100644 programs/drift/src/controller/lp/tests.rs delete mode 100644 programs/drift/src/math/lp.rs delete mode 100644 programs/drift/src/math/lp/tests.rs diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index d470c4757b..137bc1d055 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -5,7 +5,6 @@ use anchor_lang::prelude::*; use crate::controller::amm::get_fee_pool_tokens; use crate::controller::funding::settle_funding_payment; -use crate::controller::lp::burn_lp_shares; use crate::controller::orders; use crate::controller::orders::{cancel_order, fill_perp_order, place_perp_order}; use crate::controller::position::{ @@ -181,8 +180,7 @@ pub fn liquidate_perp( let position_index = get_position_index(&user.perp_positions, market_index)?; validate!( user.perp_positions[position_index].is_open_position() - || user.perp_positions[position_index].has_open_order() - || user.perp_positions[position_index].is_lp(), + || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; @@ -222,27 +220,7 @@ pub fn liquidate_perp( drop(market); // burning lp shares = removing open bids/asks - let lp_shares = user.perp_positions[position_index].lp_shares; - if lp_shares > 0 { - let (position_delta, pnl) = burn_lp_shares( - &mut user.perp_positions[position_index], - perp_market_map.get_ref_mut(&market_index)?.deref_mut(), - lp_shares, - oracle_price, - )?; - - // emit LP record for shares removed - emit_stack::<_, { LPRecord::SIZE }>(LPRecord { - ts: now, - action: LPAction::RemoveLiquidity, - user: *user_key, - n_shares: lp_shares, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - })?; - } + let lp_shares = 0; // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { @@ -824,8 +802,7 @@ pub fn liquidate_perp_with_fill( let position_index = get_position_index(&user.perp_positions, market_index)?; validate!( user.perp_positions[position_index].is_open_position() - || user.perp_positions[position_index].has_open_order() - || user.perp_positions[position_index].is_lp(), + || user.perp_positions[position_index].has_open_order(), ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; @@ -865,27 +842,7 @@ pub fn liquidate_perp_with_fill( drop(market); // burning lp shares = removing open bids/asks - let lp_shares = user.perp_positions[position_index].lp_shares; - if lp_shares > 0 { - let (position_delta, pnl) = burn_lp_shares( - &mut user.perp_positions[position_index], - perp_market_map.get_ref_mut(&market_index)?.deref_mut(), - lp_shares, - oracle_price, - )?; - - // emit LP record for shares removed - emit_stack::<_, { LPRecord::SIZE }>(LPRecord { - ts: now, - action: LPAction::RemoveLiquidity, - user: *user_key, - n_shares: lp_shares, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - })?; - } + let lp_shares = 0; // check if user exited liquidation territory let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { @@ -2435,12 +2392,6 @@ pub fn liquidate_borrow_for_perp_pnl( base_asset_amount )?; - validate!( - !user_position.is_lp(), - ErrorCode::InvalidPerpPositionToLiquidate, - "user is an lp. must call liquidate_perp first" - )?; - let pnl = user_position.quote_asset_amount.cast::()?; validate!( @@ -2970,12 +2921,6 @@ pub fn liquidate_perp_pnl_for_deposit( base_asset_amount )?; - validate!( - !user_position.is_lp(), - ErrorCode::InvalidPerpPositionToLiquidate, - "user is an lp. must call liquidate_perp first" - )?; - let unsettled_pnl = user_position.quote_asset_amount.cast::()?; validate!( diff --git a/programs/drift/src/controller/lp.rs b/programs/drift/src/controller/lp.rs deleted file mode 100644 index e9e3f37e35..0000000000 --- a/programs/drift/src/controller/lp.rs +++ /dev/null @@ -1,401 +0,0 @@ -use anchor_lang::prelude::{msg, Pubkey}; - -use crate::bn::U192; -use crate::controller; -use crate::controller::position::update_position_and_market; -use crate::controller::position::{get_position_index, PositionDelta}; -use crate::emit; -use crate::error::{DriftResult, ErrorCode}; -use crate::get_struct_values; -use crate::math::casting::Cast; -use crate::math::cp_curve::{get_update_k_result, update_k}; -use crate::math::lp::calculate_settle_lp_metrics; -use crate::math::position::calculate_base_asset_value_with_oracle_price; -use crate::math::safe_math::SafeMath; - -use crate::state::events::{LPAction, LPRecord}; -use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::PerpMarket; -use crate::state::perp_market_map::PerpMarketMap; -use crate::state::state::State; -use crate::state::user::PerpPosition; -use crate::state::user::User; -use crate::validate; -use anchor_lang::prelude::Account; - -#[cfg(test)] -mod tests; - -pub fn apply_lp_rebase_to_perp_market( - perp_market: &mut PerpMarket, - expo_diff: i8, -) -> DriftResult<()> { - // target_base_asset_amount_per_lp is the only one that it doesnt get applied - // thus changing the base of lp and without changing target_base_asset_amount_per_lp - // causes an implied change - - validate!(expo_diff != 0, ErrorCode::DefaultError, "expo_diff = 0")?; - - perp_market.amm.per_lp_base = perp_market.amm.per_lp_base.safe_add(expo_diff)?; - let rebase_divisor: i128 = 10_i128.pow(expo_diff.abs().cast()?); - - if expo_diff > 0 { - perp_market.amm.base_asset_amount_per_lp = perp_market - .amm - .base_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - - perp_market.amm.quote_asset_amount_per_lp = perp_market - .amm - .quote_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - - perp_market.amm.total_fee_earned_per_lp = perp_market - .amm - .total_fee_earned_per_lp - .safe_mul(rebase_divisor.cast()?)?; - } else { - perp_market.amm.base_asset_amount_per_lp = perp_market - .amm - .base_asset_amount_per_lp - .safe_div(rebase_divisor)?; - - perp_market.amm.quote_asset_amount_per_lp = perp_market - .amm - .quote_asset_amount_per_lp - .safe_div(rebase_divisor)?; - - perp_market.amm.total_fee_earned_per_lp = perp_market - .amm - .total_fee_earned_per_lp - .safe_div(rebase_divisor.cast()?)?; - } - - msg!( - "rebasing perp market_index={} per_lp_base expo_diff={}", - perp_market.market_index, - expo_diff, - ); - - crate::validation::perp_market::validate_perp_market(perp_market)?; - - Ok(()) -} - -pub fn apply_lp_rebase_to_perp_position( - perp_market: &PerpMarket, - perp_position: &mut PerpPosition, -) -> DriftResult<()> { - let expo_diff = perp_market - .amm - .per_lp_base - .safe_sub(perp_position.per_lp_base)?; - - if expo_diff > 0 { - let rebase_divisor: i64 = 10_i64.pow(expo_diff.cast()?); - - perp_position.last_base_asset_amount_per_lp = perp_position - .last_base_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - perp_position.last_quote_asset_amount_per_lp = perp_position - .last_quote_asset_amount_per_lp - .safe_mul(rebase_divisor)?; - - msg!( - "rebasing perp position for market_index={} per_lp_base by expo_diff={}", - perp_market.market_index, - expo_diff, - ); - } else if expo_diff < 0 { - let rebase_divisor: i64 = 10_i64.pow(expo_diff.abs().cast()?); - - perp_position.last_base_asset_amount_per_lp = perp_position - .last_base_asset_amount_per_lp - .safe_div(rebase_divisor)?; - perp_position.last_quote_asset_amount_per_lp = perp_position - .last_quote_asset_amount_per_lp - .safe_div(rebase_divisor)?; - - msg!( - "rebasing perp position for market_index={} per_lp_base by expo_diff={}", - perp_market.market_index, - expo_diff, - ); - } - - perp_position.per_lp_base = perp_position.per_lp_base.safe_add(expo_diff)?; - - Ok(()) -} - -pub fn mint_lp_shares( - position: &mut PerpPosition, - market: &mut PerpMarket, - n_shares: u64, -) -> DriftResult<()> { - let amm = market.amm; - - let (sqrt_k,) = get_struct_values!(amm, sqrt_k); - - if position.lp_shares > 0 { - settle_lp_position(position, market)?; - } else { - position.last_base_asset_amount_per_lp = amm.base_asset_amount_per_lp.cast()?; - position.last_quote_asset_amount_per_lp = amm.quote_asset_amount_per_lp.cast()?; - position.per_lp_base = amm.per_lp_base; - } - - // add share balance - position.lp_shares = position.lp_shares.safe_add(n_shares)?; - - // update market state - let new_sqrt_k = sqrt_k.safe_add(n_shares.cast()?)?; - let new_sqrt_k_u192 = U192::from(new_sqrt_k); - - let update_k_result = get_update_k_result(market, new_sqrt_k_u192, true)?; - update_k(market, &update_k_result)?; - - market.amm.user_lp_shares = market.amm.user_lp_shares.safe_add(n_shares.cast()?)?; - - crate::validation::perp_market::validate_perp_market(market)?; - crate::validation::position::validate_perp_position_with_perp_market(position, market)?; - - Ok(()) -} - -pub fn settle_lp_position( - position: &mut PerpPosition, - market: &mut PerpMarket, -) -> DriftResult<(PositionDelta, i64)> { - if position.base_asset_amount > 0 { - validate!( - position.last_cumulative_funding_rate.cast::()? - == market.amm.cumulative_funding_rate_long, - ErrorCode::InvalidPerpPositionDetected - )?; - } else if position.base_asset_amount < 0 { - validate!( - position.last_cumulative_funding_rate.cast::()? - == market.amm.cumulative_funding_rate_short, - ErrorCode::InvalidPerpPositionDetected - )?; - } - - apply_lp_rebase_to_perp_position(market, position)?; - - let lp_metrics: crate::math::lp::LPMetrics = - calculate_settle_lp_metrics(&market.amm, position)?; - - let position_delta = PositionDelta { - base_asset_amount: lp_metrics.base_asset_amount.cast()?, - quote_asset_amount: lp_metrics.quote_asset_amount.cast()?, - remainder_base_asset_amount: Some(lp_metrics.remainder_base_asset_amount.cast::()?), - }; - - let pnl: i64 = update_position_and_market(position, market, &position_delta)?; - - position.last_base_asset_amount_per_lp = market.amm.base_asset_amount_per_lp.cast()?; - position.last_quote_asset_amount_per_lp = market.amm.quote_asset_amount_per_lp.cast()?; - - crate::validation::perp_market::validate_perp_market(market)?; - crate::validation::position::validate_perp_position_with_perp_market(position, market)?; - - Ok((position_delta, pnl)) -} - -pub fn settle_lp( - user: &mut User, - user_key: &Pubkey, - market: &mut PerpMarket, - now: i64, -) -> DriftResult { - if let Ok(position) = user.get_perp_position_mut(market.market_index) { - if position.lp_shares > 0 { - let (position_delta, pnl) = settle_lp_position(position, market)?; - - if position_delta.base_asset_amount != 0 || position_delta.quote_asset_amount != 0 { - crate::emit!(LPRecord { - ts: now, - action: LPAction::SettleLiquidity, - user: *user_key, - market_index: market.market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - n_shares: 0 - }); - } - } - } - - Ok(()) -} - -// note: must settle funding before settling the lp bc -// settling the lp can take on a new position which requires funding -// to be up-to-date -pub fn settle_funding_payment_then_lp( - user: &mut User, - user_key: &Pubkey, - market: &mut PerpMarket, - now: i64, -) -> DriftResult { - crate::controller::funding::settle_funding_payment(user, user_key, market, now)?; - settle_lp(user, user_key, market, now) -} - -pub fn burn_lp_shares( - position: &mut PerpPosition, - market: &mut PerpMarket, - shares_to_burn: u64, - oracle_price: i64, -) -> DriftResult<(PositionDelta, i64)> { - // settle - let (mut position_delta, mut pnl) = settle_lp_position(position, market)?; - - // clean up - let unsettled_remainder = market - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(position.remainder_base_asset_amount.cast()?)?; - if shares_to_burn as u128 == market.amm.user_lp_shares && unsettled_remainder != 0 { - crate::validate!( - unsettled_remainder.unsigned_abs() <= market.amm.order_step_size as u128, - ErrorCode::UnableToBurnLPTokens, - "unsettled baa on final burn too big rel to stepsize {}: {} (remainder:{})", - market.amm.order_step_size, - market.amm.base_asset_amount_with_unsettled_lp, - position.remainder_base_asset_amount - )?; - - // sub bc lps take the opposite side of the user - position.remainder_base_asset_amount = position - .remainder_base_asset_amount - .safe_sub(unsettled_remainder.cast()?)?; - } - - // update stats - if position.remainder_base_asset_amount != 0 { - let base_asset_amount = position.remainder_base_asset_amount as i128; - - // user closes the dust - market.amm.base_asset_amount_with_amm = market - .amm - .base_asset_amount_with_amm - .safe_sub(base_asset_amount)?; - - market.amm.base_asset_amount_with_unsettled_lp = market - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(base_asset_amount)?; - - let dust_base_asset_value = calculate_base_asset_value_with_oracle_price(base_asset_amount, oracle_price)? - .safe_add(1) // round up - ?; - - let dust_burn_position_delta = PositionDelta { - base_asset_amount: 0, - quote_asset_amount: -dust_base_asset_value.cast()?, - remainder_base_asset_amount: Some(-position.remainder_base_asset_amount.cast()?), - }; - - update_position_and_market(position, market, &dust_burn_position_delta)?; - - msg!( - "perp {} remainder_base_asset_amount burn fee= {}", - position.market_index, - dust_base_asset_value - ); - - position_delta.quote_asset_amount = position_delta - .quote_asset_amount - .safe_sub(dust_base_asset_value.cast()?)?; - pnl = pnl.safe_sub(dust_base_asset_value.cast()?)?; - } - - // update last_ metrics - position.last_base_asset_amount_per_lp = market.amm.base_asset_amount_per_lp.cast()?; - position.last_quote_asset_amount_per_lp = market.amm.quote_asset_amount_per_lp.cast()?; - - // burn shares - position.lp_shares = position.lp_shares.safe_sub(shares_to_burn)?; - - market.amm.user_lp_shares = market.amm.user_lp_shares.safe_sub(shares_to_burn.cast()?)?; - - // update market state - let new_sqrt_k = market.amm.sqrt_k.safe_sub(shares_to_burn.cast()?)?; - let new_sqrt_k_u192 = U192::from(new_sqrt_k); - - let update_k_result = get_update_k_result(market, new_sqrt_k_u192, false)?; - update_k(market, &update_k_result)?; - - crate::validation::perp_market::validate_perp_market(market)?; - crate::validation::position::validate_perp_position_with_perp_market(position, market)?; - - Ok((position_delta, pnl)) -} - -pub fn remove_perp_lp_shares( - perp_market_map: PerpMarketMap, - oracle_map: &mut OracleMap, - state: &Account, - user: &mut std::cell::RefMut, - user_key: Pubkey, - shares_to_burn: u64, - market_index: u16, - now: i64, -) -> DriftResult<()> { - let position_index = get_position_index(&user.perp_positions, market_index)?; - - // standardize n shares to burn - // account for issue where lp shares are smaller than step size - let shares_to_burn = if user.perp_positions[position_index].lp_shares == shares_to_burn { - shares_to_burn - } else { - let market = perp_market_map.get_ref(&market_index)?; - crate::math::orders::standardize_base_asset_amount( - shares_to_burn.cast()?, - market.amm.order_step_size, - )? - .cast()? - }; - - if shares_to_burn == 0 { - return Ok(()); - } - - let mut market = perp_market_map.get_ref_mut(&market_index)?; - - let time_since_last_add_liquidity = now.safe_sub(user.last_add_perp_lp_shares_ts)?; - - validate!( - time_since_last_add_liquidity >= state.lp_cooldown_time.cast()?, - ErrorCode::TryingToRemoveLiquidityTooFast - )?; - - controller::funding::settle_funding_payment(user, &user_key, &mut market, now)?; - - let position = &mut user.perp_positions[position_index]; - - validate!( - position.lp_shares >= shares_to_burn, - ErrorCode::InsufficientLPTokens - )?; - - let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; - let (position_delta, pnl) = - burn_lp_shares(position, &mut market, shares_to_burn, oracle_price)?; - - emit!(LPRecord { - ts: now, - action: LPAction::RemoveLiquidity, - user: user_key, - n_shares: shares_to_burn, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - }); - - Ok(()) -} diff --git a/programs/drift/src/controller/lp/tests.rs b/programs/drift/src/controller/lp/tests.rs deleted file mode 100644 index 2ab426bc76..0000000000 --- a/programs/drift/src/controller/lp/tests.rs +++ /dev/null @@ -1,1818 +0,0 @@ -use crate::controller::lp::*; -use crate::controller::pnl::settle_pnl; -use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; -use crate::PRICE_PRECISION; -use crate::{SettlePnlMode, BASE_PRECISION_I64}; -use std::str::FromStr; - -use anchor_lang::Owner; -use solana_program::pubkey::Pubkey; - -use crate::create_account_info; -use crate::create_anchor_account_info; -use crate::math::casting::Cast; -use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_U64, LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, QUOTE_PRECISION_I128, QUOTE_SPOT_MARKET_INDEX, 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, - calculate_perp_position_value_and_pnl, meets_maintenance_margin_requirement, - MarginRequirementType, -}; -use crate::math::position::{ - // get_new_position_amounts, - get_position_update_type, - PositionUpdateType, -}; -use crate::state::margin_calculation::{MarginCalculation, MarginContext}; -use crate::state::oracle::{HistoricalOracleData, OracleSource}; -use crate::state::oracle::{OraclePriceData, StrictOraclePrice}; -use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::{MarketStatus, PerpMarket, PoolBalance}; -use crate::state::perp_market_map::PerpMarketMap; -use crate::state::spot_market::{SpotBalanceType, SpotMarket}; -use crate::state::spot_market_map::SpotMarketMap; -use crate::state::state::{OracleGuardRails, State, ValidityGuardRails}; -use crate::state::user::{SpotPosition, User}; -use crate::test_utils::*; -use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; -use anchor_lang::prelude::Clock; - -#[test] -fn test_lp_wont_collect_improper_funding() { - let mut position = PerpPosition { - base_asset_amount: 1, - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 1, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_short = -10; - market.amm.cumulative_funding_rate_long = -10; - market.amm.cumulative_funding_rate_long = -10; - - let result = settle_lp_position(&mut position, &mut market); - assert_eq!(result, Err(ErrorCode::InvalidPerpPositionDetected)); -} - -#[test] -fn test_full_long_settle() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 1, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_short = -10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 10); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 0); - // net baa doesnt change - assert_eq!( - og_market.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_amm - ); - - // burn - let lp_shares = position.lp_shares; - burn_lp_shares(&mut position, &mut market, lp_shares, 0).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); -} - -#[test] -fn test_full_short_settle() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - peg_multiplier: 1, - user_lp_shares: 100 * AMM_RESERVE_PRECISION, - order_step_size: 1, - ..AMM::default_test() - }; - - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - mint_lp_shares(&mut position, &mut market, 100 * BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = -10; - market.amm.quote_asset_amount_per_lp = 10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, -10); - assert_eq!(position.last_quote_asset_amount_per_lp, 10); - assert_eq!(position.base_asset_amount, -10 * 100); - assert_eq!(position.quote_asset_amount, 10 * 100); -} - -#[test] -fn test_partial_short_settle() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 3, - ..AMM::default_test() - }; - - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = -10; - market.amm.quote_asset_amount_per_lp = 10; - - market.amm.base_asset_amount_with_unsettled_lp = 9; - market.amm.base_asset_amount_long = 9; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.base_asset_amount, -9); - assert_eq!(position.quote_asset_amount, 10); - assert_eq!(position.remainder_base_asset_amount, -1); - assert_eq!(position.last_base_asset_amount_per_lp, -10); - assert_eq!(position.last_quote_asset_amount_per_lp, 10); - - // burn - let _position = position; - let lp_shares = position.lp_shares; - burn_lp_shares(&mut position, &mut market, lp_shares, 0).unwrap(); - assert_eq!(position.lp_shares, 0); -} - -#[test] -fn test_partial_long_settle() { - let mut position = PerpPosition { - lp_shares: BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: -10, - quote_asset_amount_per_lp: 10, - order_step_size: 3, - ..AMM::default_test() - }; - - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.base_asset_amount, -9); - assert_eq!(position.quote_asset_amount, 10); - assert_eq!(position.remainder_base_asset_amount, -1); - assert_eq!(position.last_base_asset_amount_per_lp, -10); - assert_eq!(position.last_quote_asset_amount_per_lp, 10); -} - -#[test] -fn test_remainder_long_settle_too_large_order_step_size() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 5 * BASE_PRECISION_U64, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm = 10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(position.remainder_base_asset_amount, 10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -10); - // net baa doesnt change after settle_lp_position - assert_eq!(market.amm.base_asset_amount_with_amm, 10); - - // burn - let lp_shares = position.lp_shares; - assert_eq!(lp_shares, BASE_PRECISION_U64); - burn_lp_shares(&mut position, &mut market, lp_shares, 22).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); - assert_eq!(position.quote_asset_amount, -11); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 0); -} - -#[test] -fn test_remainder_overflows_too_large_order_step_size() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 5 * BASE_PRECISION_U64, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm = 10; - market.amm.base_asset_amount_short = 0; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(position.remainder_base_asset_amount, 10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -10); - // net baa doesnt change after settle_lp_position - assert_eq!(market.amm.base_asset_amount_with_amm, 10); - - market.amm.base_asset_amount_per_lp += BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp += -16900000000; - market.amm.base_asset_amount_with_unsettled_lp += -(BASE_PRECISION_I128 + 1); - // market.amm.base_asset_amount_short ; - market.amm.base_asset_amount_with_amm += BASE_PRECISION_I128 + 1; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 1000000011); - assert_eq!(position.last_quote_asset_amount_per_lp, -16900000010); - assert_eq!(position.quote_asset_amount, -16900000010); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 1000000011); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // might break i32 limit - market.amm.base_asset_amount_per_lp = 3 * BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp = -(3 * 16900000000); - market.amm.base_asset_amount_with_unsettled_lp = -(3 * BASE_PRECISION_I128 + 1); - market.amm.base_asset_amount_short = -(3 * BASE_PRECISION_I128 + 1); - - // not allowed to settle when remainder is above i32 but below order size - assert!(settle_lp_position(&mut position, &mut market).is_err()); - - // assert_eq!(position.last_base_asset_amount_per_lp, 1000000001); - // assert_eq!(position.last_quote_asset_amount_per_lp, -16900000000); - assert_eq!(position.quote_asset_amount, -16900000010); - assert_eq!(position.base_asset_amount, 0); - // assert_eq!(position.remainder_base_asset_amount, 1000000001); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // past order_step_size on market - market.amm.base_asset_amount_per_lp = 5 * BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp = -116900000000; - market.amm.base_asset_amount_with_unsettled_lp = -(5 * BASE_PRECISION_I128 + 1); - market.amm.base_asset_amount_short = -(5 * BASE_PRECISION_I128); - market.amm.base_asset_amount_with_amm = 1; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -1); - assert_eq!(market.amm.base_asset_amount_short, -5000000000); - assert_eq!(market.amm.base_asset_amount_long, 5 * BASE_PRECISION_I128); - assert_eq!(market.amm.base_asset_amount_with_amm, 1); - - assert_eq!(position.last_base_asset_amount_per_lp, 5000000001); - assert_eq!(position.last_quote_asset_amount_per_lp, -116900000000); - assert_eq!(position.quote_asset_amount, -116900000000); - assert_eq!(position.base_asset_amount, 5000000000); - assert_eq!(position.remainder_base_asset_amount, 1); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // burn - let lp_shares = position.lp_shares; - assert_eq!(lp_shares, BASE_PRECISION_U64); - burn_lp_shares(&mut position, &mut market, lp_shares, 22).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); - assert_eq!(position.quote_asset_amount, -116900000001); - assert_eq!(position.base_asset_amount, 5000000000); - assert_eq!(position.remainder_base_asset_amount, 0); - - assert_eq!(market.amm.base_asset_amount_with_amm, 0); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 0); - assert_eq!(market.amm.base_asset_amount_short, -5000000000); - assert_eq!(market.amm.base_asset_amount_long, 5000000000); -} - -#[test] -fn test_remainder_burn_large_order_step_size() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: 2 * BASE_PRECISION_U64, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - let og_market = market; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm += 10; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 10); - assert_eq!(position.last_quote_asset_amount_per_lp, -10); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -10); - assert_eq!(position.remainder_base_asset_amount, 10); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, -10); - // net baa doesnt change after settle_lp_position - assert_eq!(market.amm.base_asset_amount_with_amm, 10); - - market.amm.base_asset_amount_per_lp = BASE_PRECISION_I128 + 1; - market.amm.quote_asset_amount_per_lp = -16900000000; - market.amm.base_asset_amount_with_unsettled_lp += -(BASE_PRECISION_I128 + 1); - market.amm.base_asset_amount_with_amm += BASE_PRECISION_I128 + 1; - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.last_base_asset_amount_per_lp, 1000000001); - assert_eq!(position.last_quote_asset_amount_per_lp, -16900000000); - assert_eq!(position.quote_asset_amount, -16900000000); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 1000000001); - assert_eq!( - (position.remainder_base_asset_amount as u64) < market.amm.order_step_size, - true - ); - - // burn with overflowed remainder - let lp_shares = position.lp_shares; - assert_eq!(lp_shares, BASE_PRECISION_U64); - burn_lp_shares(&mut position, &mut market, lp_shares, 22).unwrap(); - assert_eq!(position.lp_shares, 0); - assert_eq!(og_market.amm.sqrt_k, market.amm.sqrt_k); - assert_eq!(position.quote_asset_amount, -16900000023); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, 0); -} - -#[test] -pub fn test_lp_settle_pnl() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - position.last_cumulative_funding_rate = 1337; - - 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 clock = Clock { - slot: 0, - epoch_start_timestamp: 0, - epoch: 0, - leader_schedule_epoch: 0, - unix_timestamp: 0, - }; - let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); - - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - 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: 2 * BASE_PRECISION_U64 / 100, - quote_asset_amount: -150 * QUOTE_PRECISION_I128, - base_asset_amount_with_amm: BASE_PRECISION_I128, - base_asset_amount_long: BASE_PRECISION_I128, - oracle: oracle_price_key, - concentration_coef: 1000001, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: oracle_price.agg.price, - last_oracle_price_twap_5min: oracle_price.agg.price, - last_oracle_price_twap: oracle_price.agg.price, - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - number_of_users_with_base: 1, - number_of_users: 1, - status: MarketStatus::Active, - liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, - pnl_pool: PoolBalance { - scaled_balance: (50 * SPOT_BALANCE_PRECISION), - market_index: QUOTE_SPOT_MARKET_INDEX, - ..PoolBalance::default() - }, - unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION.cast().unwrap(), - ..PerpMarket::default() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 10; - market.amm.quote_asset_amount_per_lp = -10; - market.amm.base_asset_amount_with_unsettled_lp = -10; - market.amm.base_asset_amount_with_amm += 10; - market.amm.cumulative_funding_rate_long = 169; - market.amm.cumulative_funding_rate_short = 169; - - settle_lp_position(&mut position, &mut market).unwrap(); - create_anchor_account_info!(market, PerpMarket, market_account_info); - let 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, - deposit_balance: 100 * SPOT_BALANCE_PRECISION, - ..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 user_key = Pubkey::default(); - let authority = Pubkey::default(); - - let mut user = User { - perp_positions: get_positions(position), - 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 state = State { - oracle_guard_rails: OracleGuardRails { - validity: ValidityGuardRails { - slots_before_stale_for_amm: 10, // 5s - slots_before_stale_for_margin: 120, // 60s - confidence_interval_max_size: 1000, - too_volatile_ratio: 5, - }, - ..OracleGuardRails::default() - }, - ..State::default() - }; - - let MarginCalculation { - total_collateral: total_collateral1, - margin_requirement: margin_requirement1, - .. - } = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user, - &market_map, - &spot_market_map, - &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial), - ) - .unwrap(); - - assert_eq!(total_collateral1, 49999988); - assert_eq!(margin_requirement1, 2099020); // $2+ for margin req - - let result = settle_pnl( - 0, - &mut user, - &authority, - &user_key, - &market_map, - &spot_market_map, - &mut oracle_map, - &clock, - &state, - None, - SettlePnlMode::MustSettle, - ); - - assert_eq!(result, Ok(())); - // assert_eq!(result, Err(ErrorCode::InsufficientCollateralForSettlingPNL)) -} - -#[test] -fn test_lp_margin_calc() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - position.last_cumulative_funding_rate = 1337; - - 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 slot = 0; - 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: 2 * BASE_PRECISION_U64 / 100, - quote_asset_amount: -150 * QUOTE_PRECISION_I128, - base_asset_amount_with_amm: BASE_PRECISION_I128, - base_asset_amount_long: BASE_PRECISION_I128, - oracle: oracle_price_key, - concentration_coef: 1000001, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: oracle_price.agg.price, - last_oracle_price_twap_5min: oracle_price.agg.price, - last_oracle_price_twap: oracle_price.agg.price, - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - number_of_users_with_base: 1, - number_of_users: 1, - status: MarketStatus::Active, - liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, - pnl_pool: PoolBalance { - scaled_balance: (50 * SPOT_BALANCE_PRECISION), - market_index: QUOTE_SPOT_MARKET_INDEX, - ..PoolBalance::default() - }, - unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION.cast().unwrap(), - ..PerpMarket::default() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 100 * BASE_PRECISION_I128; - market.amm.quote_asset_amount_per_lp = -BASE_PRECISION_I128; - market.amm.base_asset_amount_with_unsettled_lp = -100 * BASE_PRECISION_I128; - market.amm.base_asset_amount_short = -100 * BASE_PRECISION_I128; - market.amm.cumulative_funding_rate_long = 169 * 100000000; - market.amm.cumulative_funding_rate_short = 169 * 100000000; - - settle_lp_position(&mut position, &mut market).unwrap(); - create_anchor_account_info!(market, PerpMarket, market_account_info); - let 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, - deposit_balance: 100 * SPOT_BALANCE_PRECISION, - ..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(position), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 5000 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - user.perp_positions[0].base_asset_amount = BASE_PRECISION_I128 as i64; - - // user has lp shares + long and last cumulative funding doesnt match - assert_eq!(user.perp_positions[0].lp_shares, 1000000000); - assert_eq!( - user.perp_positions[0].base_asset_amount, - BASE_PRECISION_I128 as i64 - ); - assert!( - user.perp_positions[0].last_cumulative_funding_rate != market.amm.last_funding_rate_long - ); - - let result = - meets_maintenance_margin_requirement(&user, &market_map, &spot_market_map, &mut oracle_map); - - assert_eq!(result.unwrap(), true); - - // add move lower - let oracle_price_data = OraclePriceData { - price: oracle_price.agg.price, - confidence: 100000, - delay: 1, - has_sufficient_number_of_data_points: true, - }; - - assert_eq!(market.amm.base_asset_amount_per_lp, 100000000000); - assert_eq!(market.amm.quote_asset_amount_per_lp, -1000000000); - assert_eq!(market.amm.cumulative_funding_rate_long, 16900000000); - assert_eq!(market.amm.cumulative_funding_rate_short, 16900000000); - - assert_eq!(user.perp_positions[0].lp_shares, 1000000000); - assert_eq!(user.perp_positions[0].base_asset_amount, 1000000000); - assert_eq!( - user.perp_positions[0].last_base_asset_amount_per_lp, - 100000000000 - ); - assert_eq!( - user.perp_positions[0].last_quote_asset_amount_per_lp, - -1000000000 - ); - assert_eq!( - user.perp_positions[0].last_cumulative_funding_rate, - 16900000000 - ); - - // increase markets so user has to settle lp - market.amm.base_asset_amount_per_lp *= 2; - market.amm.quote_asset_amount_per_lp *= 20; - - // update funding so user has unsettled funding - market.amm.cumulative_funding_rate_long *= 2; - market.amm.cumulative_funding_rate_short *= 2; - - apply_lp_rebase_to_perp_market(&mut market, 1).unwrap(); - - let sim_user_pos = user.perp_positions[0] - .simulate_settled_lp_position(&market, oracle_price_data.price) - .unwrap(); - assert_ne!( - sim_user_pos.base_asset_amount, - user.perp_positions[0].base_asset_amount - ); - assert_eq!(sim_user_pos.base_asset_amount, 101000000000); - assert_eq!(sim_user_pos.quote_asset_amount, -20000000000); - assert_eq!(sim_user_pos.last_cumulative_funding_rate, 16900000000); - - let strict_quote_price = StrictOraclePrice::test(1000000); - // ensure margin calc doesnt incorrectly count funding rate (funding pnl MUST come before settling lp) - let ( - margin_requirement, - weighted_unrealized_pnl, - worse_case_base_asset_value, - _open_order_fraction, - _base_asset_value, - ) = calculate_perp_position_value_and_pnl( - &user.perp_positions[0], - &market, - &oracle_price_data, - &strict_quote_price, - crate::math::margin::MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - assert_eq!(margin_requirement, 1012000000); // $1010 + $2 mr for lp_shares - assert_eq!(weighted_unrealized_pnl, -9916900000); // $-9900000000 upnl (+ -16900000 from old funding) - assert_eq!(worse_case_base_asset_value, 10100000000); //$10100 -} - -#[test] -fn test_lp_has_correct_entry_be_price() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 100000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 101000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - - market.amm.base_asset_amount_per_lp = BASE_PRECISION_I128; - market.amm.quote_asset_amount_per_lp = -99_999_821; - market.amm.base_asset_amount_with_unsettled_lp = BASE_PRECISION_I128; - market.amm.base_asset_amount_long = BASE_PRECISION_I128; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(position.get_entry_price().unwrap(), 99999821); - - assert_eq!(position.quote_entry_amount, -99999821); - assert_eq!(position.quote_break_even_amount, -99999821); - assert_eq!(position.quote_asset_amount, -99999821); - - market.amm.base_asset_amount_per_lp -= BASE_PRECISION_I128 / 2; - market.amm.quote_asset_amount_per_lp += 97_999_821; - market.amm.base_asset_amount_with_unsettled_lp = BASE_PRECISION_I128 / 2; - market.amm.base_asset_amount_long = BASE_PRECISION_I128 / 2; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(position.get_entry_price().unwrap(), 99999822); - - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.quote_entry_amount, -49999911); - assert_eq!(position.quote_break_even_amount, -49999911); - assert_eq!(position.quote_asset_amount, -2000000); - assert_eq!(position.base_asset_amount, 500_000_000); - - let base_delta = -BASE_PRECISION_I128 / 4; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp += 98_999_821 / 4; - let (update_base_delta, _) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - base_delta, - market.amm.order_step_size as u128, - ) - .unwrap(); - - market.amm.base_asset_amount_with_unsettled_lp += update_base_delta; - market.amm.base_asset_amount_long += update_base_delta; - - settle_lp_position(&mut position, &mut market).unwrap(); - assert_eq!(position.get_entry_price().unwrap(), 99999824); - assert_eq!(position.get_cost_basis().unwrap(), -75833183); - - assert_eq!(position.base_asset_amount, 300000000); - assert_eq!(position.remainder_base_asset_amount, -50000000); - assert_eq!(position.quote_entry_amount, -24999956); - assert_eq!(position.quote_break_even_amount, -24999956); - assert_eq!(position.quote_asset_amount, 22749955); -} - -#[test] -fn test_lp_has_correct_entry_be_price_sim_no_remainders() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 2000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 0); - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.base_asset_amount, 0); - let mut num_position_flips = 0; - let mut flip_indexes: Vec = Vec::new(); - - for i in 0..3000 { - if i % 3 == 0 { - let px = 100_000_000 - i; - let multi = i % 19 + 1; - let divisor = 10; - let base_delta = -BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp += px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += base_delta; - market.amm.base_asset_amount_short += base_delta; - } else { - // buy - let px = 99_199_821 + i; - let multi = i % 5 + 1; - let divisor = 5; - let base_delta = BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp -= px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += base_delta; - market.amm.base_asset_amount_long += base_delta; - } - - let position_base_before = position.base_asset_amount; - - settle_lp_position(&mut position, &mut market).unwrap(); - - if position_base_before.signum() != position.base_asset_amount.signum() { - num_position_flips += 1; - flip_indexes.push(i); - } - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - let iii = position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount as i64) - .unwrap(); - msg!( - "{}: entry: {}, be: {} cb:{} ({}/{})", - i, - entry, - be, - cb, - iii, - position.base_asset_amount, - ); - assert_eq!(position.remainder_base_asset_amount, 0); - - if position.get_base_asset_amount_with_remainder_abs().unwrap() != 0 { - assert!(entry <= 100 * PRICE_PRECISION as i128); - assert!(entry >= 99 * PRICE_PRECISION as i128); - } - } - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - assert_eq!(position.base_asset_amount, 200500000000); - assert_eq!(entry, 99202392); - assert_eq!(be, 99202392); - assert_eq!(cb, 95227357); - assert_eq!(num_position_flips, 4); - assert_eq!(flip_indexes, [0, 1, 18, 19]); -} - -#[test] -fn test_lp_remainder_position_updates() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(880), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-881), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.remainder_base_asset_amount, -1); - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: -199 * 1000000, - base_asset_amount: BASE_PRECISION_I64, - remainder_base_asset_amount: Some(-BASE_PRECISION_I64 / 22), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - assert_eq!(position.base_asset_amount, 1000000000); - assert_eq!(position.remainder_base_asset_amount, -45454546); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: 199 * 1000000, - base_asset_amount: -BASE_PRECISION_I64 * 2, - remainder_base_asset_amount: Some(BASE_PRECISION_I64 / 23), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, -101912122); - assert_eq!(position.base_asset_amount, -1000000000); - assert_eq!(position.remainder_base_asset_amount, -1976286); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); -} - -#[test] -fn test_lp_remainder_position_updates_2() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 300000000, - remainder_base_asset_amount: Some(33333333), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 500000000, - remainder_base_asset_amount: Some(0), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 0); - assert_eq!(position.base_asset_amount, 800000000); - assert_eq!(position.remainder_base_asset_amount, 33333333); - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - let position_delta = PositionDelta { - quote_asset_amount: 199 * 10000, - base_asset_amount: -300000000, - remainder_base_asset_amount: Some(-63636363), - }; - - let pnl: i64 = update_position_and_market(&mut position, &mut market, &position_delta).unwrap(); - assert_eq!(pnl, 1990000); - assert_eq!(position.base_asset_amount, 500000000); - assert_eq!(position.remainder_base_asset_amount, -30303030); - assert_eq!(market.amm.base_asset_amount_long, 500000000); - assert_eq!(market.amm.base_asset_amount_short, 0); - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 500000000); - assert_eq!(market.amm.base_asset_amount_with_amm, 0); - - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); -} - -#[test] -fn test_lp_has_correct_entry_be_price_sim() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 2000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 0); - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.base_asset_amount, 0); - let mut num_position_flips = 0; - let mut flip_indexes: Vec = Vec::new(); - - let mut total_remainder = 0; - for i in 0..3000 { - if i % 3 == 0 { - let px = 100_000_000 - i; - let multi = i % 19 + 1; - let divisor = 11; - let base_delta = -BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp += px * multi / divisor; - - let (update_base_delta, rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - base_delta, - market.amm.order_step_size as u128, - ) - .unwrap(); - total_remainder += rr; - - let (total_remainder_f, _rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - total_remainder, - market.amm.order_step_size as u128, - ) - .unwrap(); - if total_remainder_f != 0 { - total_remainder -= total_remainder_f; - msg!("total_remainder update {}", total_remainder); - } - - market.amm.base_asset_amount_with_unsettled_lp += update_base_delta; - market.amm.base_asset_amount_long += update_base_delta; - } else { - // buy - let px = 99_199_821 + i; - let multi = i % 5 + 1; - let divisor = 6; - let base_delta = BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp -= px * multi / divisor; - - let (update_base_delta, rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - base_delta, - market.amm.order_step_size as u128, - ) - .unwrap(); - total_remainder += rr; - - let (total_remainder_f, _rr) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - total_remainder, - market.amm.order_step_size as u128, - ) - .unwrap(); - if total_remainder_f != 0 { - total_remainder -= total_remainder_f; - } - - market.amm.base_asset_amount_with_unsettled_lp += update_base_delta; - market.amm.base_asset_amount_short += update_base_delta; - } - - let position_base_before = position.base_asset_amount; - crate::validation::perp_market::validate_perp_market(&market).unwrap(); - crate::validation::position::validate_perp_position_with_perp_market(&position, &market) - .unwrap(); - - settle_lp_position(&mut position, &mut market).unwrap(); - - if position_base_before.signum() != position.base_asset_amount.signum() { - num_position_flips += 1; - flip_indexes.push(i); - } - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - let iii = position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount as i64) - .unwrap(); - msg!( - "{}: entry: {}, be: {} cb:{} ({}/{})", - i, - entry, - be, - cb, - iii, - position.base_asset_amount, - ); - // assert_ne!(position.remainder_base_asset_amount, 0); - - if position.get_base_asset_amount_with_remainder_abs().unwrap() != 0 { - assert!(entry <= 100 * PRICE_PRECISION as i128); - assert!(entry >= 99 * PRICE_PRECISION as i128); - } - } - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - assert_eq!(entry, 99202570); - assert_eq!(be, 99202570); - assert_eq!(cb, 91336780); - assert_eq!(num_position_flips, 5); - assert_eq!(flip_indexes, [1, 18, 19, 36, 37]); - assert_eq!(position.base_asset_amount, 91300000000); -} - -#[test] -fn test_lp_has_correct_entry_be_price_sim_more_flips() { - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - let amm = AMM { - order_step_size: BASE_PRECISION_U64 / 10, - sqrt_k: BASE_PRECISION_U64 as u128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - assert_eq!(market.amm.user_lp_shares, 0); - assert_eq!(market.amm.sqrt_k, 1000000000); - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - assert_eq!(market.amm.user_lp_shares, 1000000000); - assert_eq!(market.amm.sqrt_k, 2000000000); - assert_eq!(position.get_entry_price().unwrap(), 0); - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 0); - assert_eq!(position.remainder_base_asset_amount, 0); - assert_eq!(position.base_asset_amount, 0); - let mut num_position_flips = 0; - let mut flip_indexes: Vec = Vec::new(); - - for i in 0..3000 { - if i % 2 == 0 { - let px = 99_800_000 - i * i % 4; - let multi = i % 7 + 1 + i; - let divisor = 10; - let amt2 = -BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += amt2; - market.amm.quote_asset_amount_per_lp += px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += amt2; - market.amm.base_asset_amount_short += amt2; - } else { - // buy - let px = 99_199_821 + i * i % 4; - let multi = i % 7 + 1 + i; - let divisor = 10; - let base_delta = BASE_PRECISION_I128 * multi / divisor; - market.amm.base_asset_amount_per_lp += base_delta; - market.amm.quote_asset_amount_per_lp -= px * multi / divisor; - market.amm.base_asset_amount_with_unsettled_lp += base_delta; - market.amm.base_asset_amount_long += base_delta; - } - - let position_base_before = position.base_asset_amount; - - settle_lp_position(&mut position, &mut market).unwrap(); - - if position_base_before.signum() != position.base_asset_amount.signum() { - num_position_flips += 1; - flip_indexes.push(i); - } - assert_eq!(position.remainder_base_asset_amount, 0); - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - let iii = position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount as i64) - .unwrap(); - msg!( - "{}: entry: {}, be: {} cb:{} ({}/{})", - i, - entry, - be, - cb, - iii, - position.base_asset_amount, - ); - - if position.get_base_asset_amount_with_remainder_abs().unwrap() != 0 { - assert!(entry <= 99_800_000_i128); - assert!(entry >= 99_199_820_i128); - } - } - - assert_eq!(num_position_flips, 3000); - // assert_eq!(flip_indexes, [0, 1, 18, 19]); - - let entry = position.get_entry_price().unwrap(); - let be = position.get_breakeven_price().unwrap(); - let cb = position.get_cost_basis().unwrap(); - - assert_eq!(position.base_asset_amount, 150200000000); - assert_eq!(position.remainder_base_asset_amount, 0); - - assert_eq!(entry, 99199822); - assert_eq!(be, 99199822); - assert_eq!(cb, -801664962); -} - -#[test] -fn test_get_position_update_type_lp_opens() { - // position is empty, every inc must be open - let position = PerpPosition { - ..PerpPosition::default() - }; - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let position = PerpPosition { - ..PerpPosition::default() - }; - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Open - ); -} - -#[test] -fn test_get_position_update_type_lp_negative_position() { - // $119 short - let position = PerpPosition { - base_asset_amount: -1000000000 * 2, - quote_asset_amount: 119000000 * 2, - quote_entry_amount: 119000000 * 2, - quote_break_even_amount: 119000000 * 2, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 119000000); - assert_eq!(position.get_breakeven_price().unwrap(), 119000000); - assert_eq!(position.get_entry_price().unwrap(), 119000000); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); // more negative - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81, - remainder_base_asset_amount: Some(-81000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81000, - remainder_base_asset_amount: Some(-81), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); -} - -#[test] -fn test_get_position_update_type_lp_positive_position() { - // $119 long - let position = PerpPosition { - base_asset_amount: 1000000000 * 2, - quote_asset_amount: -119000000 * 2, - quote_entry_amount: -119000000 * 2, - quote_break_even_amount: -119000000 * 2, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 119000000); - assert_eq!(position.get_breakeven_price().unwrap(), 119000000); - assert_eq!(position.get_entry_price().unwrap(), 119000000); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // no base/remainder is reduce (should be skipped earlier) - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81, - remainder_base_asset_amount: Some(-81000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81000, - remainder_base_asset_amount: Some(-81), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); -} - -#[test] -fn test_get_position_update_type_lp_positive_position_with_positive_remainder() { - // $119 long - let position = PerpPosition { - base_asset_amount: 1000000000 * 2, - remainder_base_asset_amount: 7809809, - quote_asset_amount: -119000000 * 2, - quote_entry_amount: -119000000 * 2, - quote_break_even_amount: -119000000 * 2, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 119000000); - assert_eq!(position.get_breakeven_price().unwrap(), 118537123); - assert_eq!(position.get_entry_price().unwrap(), 118537123); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000001 * 2, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-7809809), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: Some(-7809809 - 1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: -898989, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 10000, - base_asset_amount: 0, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // no base/remainder is reduce (should be skipped earlier) - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - let delta = PositionDelta { - quote_asset_amount: 1899990, - base_asset_amount: -8989898, - remainder_base_asset_amount: Some(-88881000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81, - remainder_base_asset_amount: Some(-81000), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); - - // opposite sign remainder/base - let delta = PositionDelta { - quote_asset_amount: -88888, - base_asset_amount: 81000, - remainder_base_asset_amount: Some(-81), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); -} - -#[test] -fn test_get_position_update_type_positive_remainder() { - // $119 long (only a remainder size) - let position = PerpPosition { - base_asset_amount: 0, - remainder_base_asset_amount: 7809809, - quote_asset_amount: -119000000 * 7809809 / BASE_PRECISION_I64, - quote_entry_amount: -119000000 * 7809809 / BASE_PRECISION_I64, - quote_break_even_amount: -119000000 * 7809809 / BASE_PRECISION_I64, - ..PerpPosition::default() - }; - - assert_eq!(position.get_cost_basis().unwrap(), 0); - assert_eq!(position.get_breakeven_price().unwrap(), 118999965); - assert_eq!(position.get_entry_price().unwrap(), 118999965); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000001 * 2, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Increase - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-8791), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Reduce - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-7809809), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Close - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: 0, - remainder_base_asset_amount: Some(-7809809 - 1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: Some(-7809809 - 1), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: Some(0), - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller - - let delta = PositionDelta { - quote_asset_amount: 0, - base_asset_amount: -1000000000 * 2, - remainder_base_asset_amount: None, - }; - assert_eq!( - get_position_update_type(&position, &delta).unwrap(), - PositionUpdateType::Flip - ); // different signum but smaller -} diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index ecd9a3a6ab..1565eb1174 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -2,7 +2,6 @@ pub mod amm; pub mod funding; pub mod insurance; pub mod liquidation; -pub mod lp; pub mod orders; pub mod pda; pub mod pnl; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 5d9818172b..8884c1695e 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -8,11 +8,10 @@ use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use anchor_lang::prelude::*; use crate::controller::funding::settle_funding_payment; -use crate::controller::lp::burn_lp_shares; 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_lp_market_position, update_position_and_market, update_quote_asset_amount, + update_position_and_market, update_quote_asset_amount, PositionDirection, }; use crate::controller::spot_balance::{ @@ -37,7 +36,6 @@ use crate::math::fulfillment::{ determine_perp_fulfillment_methods, determine_spot_fulfillment_methods, }; use crate::math::liquidation::validate_user_not_being_liquidated; -use crate::math::lp::calculate_lp_shares_to_burn_for_risk_reduction; use crate::math::matching::{ are_orders_same_market_but_different_sides, calculate_fill_for_matched_orders, calculate_filler_multiplier_for_matched_orders, do_orders_cross, is_maker_for_taker, @@ -995,7 +993,7 @@ pub fn fill_perp_order( // settle lp position so its tradeable let mut market = perp_market_map.get_ref_mut(&market_index)?; - controller::lp::settle_funding_payment_then_lp(user, &user_key, &mut market, now)?; + settle_funding_payment(user, &user_key, &mut market, now)?; validate!( matches!( @@ -2238,29 +2236,6 @@ pub fn fulfill_perp_order_with_amm( let user_position_delta = get_position_delta_for_fill(base_asset_amount, quote_asset_amount, order_direction)?; - if liquidity_split != AMMLiquiditySplit::ProtocolOwned { - update_lp_market_position( - market, - &user_position_delta, - fee_to_market_for_lp.cast()?, - liquidity_split, - )?; - } - - if market.amm.user_lp_shares > 0 { - let (new_terminal_quote_reserve, new_terminal_base_reserve) = - crate::math::amm::calculate_terminal_reserves(&market.amm)?; - market.amm.terminal_quote_asset_reserve = new_terminal_quote_reserve; - - let (min_base_asset_reserve, max_base_asset_reserve) = - crate::math::amm::calculate_bid_ask_bounds( - market.amm.concentration_coef, - new_terminal_base_reserve, - )?; - market.amm.min_base_asset_reserve = min_base_asset_reserve; - market.amm.max_base_asset_reserve = max_base_asset_reserve; - } - // Increment the protocol's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; market.amm.total_exchange_fee = market.amm.total_exchange_fee.safe_add(user_fee.cast()?)?; @@ -3293,129 +3268,6 @@ pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index: } } -pub fn attempt_burn_user_lp_shares_for_risk_reduction( - state: &State, - user: &mut User, - user_key: Pubkey, - margin_calc: MarginCalculation, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - clock: &Clock, - market_index: u16, -) -> DriftResult { - let now = clock.unix_timestamp; - let time_since_last_liquidity_change: i64 = now.safe_sub(user.last_add_perp_lp_shares_ts)?; - // avoid spamming update if orders have already been set - if time_since_last_liquidity_change >= state.lp_cooldown_time.cast()? { - burn_user_lp_shares_for_risk_reduction( - state, - user, - user_key, - market_index, - margin_calc, - perp_market_map, - spot_market_map, - oracle_map, - clock, - )?; - user.last_add_perp_lp_shares_ts = now; - } - - Ok(()) -} - -pub fn burn_user_lp_shares_for_risk_reduction( - state: &State, - user: &mut User, - user_key: Pubkey, - market_index: u16, - margin_calc: MarginCalculation, - perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, - oracle_map: &mut OracleMap, - clock: &Clock, -) -> DriftResult { - let position_index = get_position_index(&user.perp_positions, market_index)?; - let is_lp = user.perp_positions[position_index].is_lp(); - if !is_lp { - return Ok(()); - } - - let mut market = perp_market_map.get_ref_mut(&market_index)?; - - let quote_oracle_id = spot_market_map - .get_ref(&market.quote_spot_market_index)? - .oracle_id(); - let quote_oracle_price = oracle_map.get_price_data("e_oracle_id)?.price; - - let oracle_price_data = oracle_map.get_price_data(&market.oracle_id())?; - - let oracle_price = if market.status == MarketStatus::Settlement { - market.expiry_price - } else { - oracle_price_data.price - }; - - let user_custom_margin_ratio = user.max_margin_ratio; - let (lp_shares_to_burn, base_asset_amount_to_close) = - calculate_lp_shares_to_burn_for_risk_reduction( - &user.perp_positions[position_index], - &market, - oracle_price, - quote_oracle_price, - margin_calc.margin_shortage()?, - user_custom_margin_ratio, - user.is_high_leverage_mode(), - )?; - - let (position_delta, pnl) = burn_lp_shares( - &mut user.perp_positions[position_index], - &mut market, - lp_shares_to_burn, - oracle_price, - )?; - - // emit LP record for shares removed - emit_stack::<_, { LPRecord::SIZE }>(LPRecord { - ts: clock.unix_timestamp, - action: LPAction::RemoveLiquidityDerisk, - user: user_key, - n_shares: lp_shares_to_burn, - market_index, - delta_base_asset_amount: position_delta.base_asset_amount, - delta_quote_asset_amount: position_delta.quote_asset_amount, - pnl, - })?; - - let direction_to_close = user.perp_positions[position_index].get_direction_to_close(); - - let params = OrderParams::get_close_perp_params( - &market, - direction_to_close, - base_asset_amount_to_close, - )?; - - drop(market); - - if user.has_room_for_new_order() { - controller::orders::place_perp_order( - state, - user, - user_key, - perp_market_map, - spot_market_map, - oracle_map, - &None, - clock, - params, - PlaceOrderOptions::default().explanation(OrderActionExplanation::DeriskLp), - )?; - } - - Ok(()) -} - pub fn pay_keeper_flat_reward_for_perps( user: &mut User, filler: Option<&mut User>, diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 13c29ada7d..46cf1e9779 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -1,7 +1,7 @@ use crate::controller::amm::{update_pnl_pool_and_user_balance, update_pool_balances}; use crate::controller::funding::settle_funding_payment; use crate::controller::orders::{ - attempt_burn_user_lp_shares_for_risk_reduction, cancel_orders, + cancel_orders, validate_market_within_price_band, }; use crate::controller::position::{ @@ -74,7 +74,7 @@ pub fn settle_pnl( validate_market_within_price_band(&market, state, oracle_price)?; - crate::controller::lp::settle_funding_payment_then_lp(user, user_key, &mut market, now)?; + settle_funding_payment(user, user_key, &mut market, now)?; drop(market); @@ -82,48 +82,7 @@ pub fn settle_pnl( let unrealized_pnl = user.perp_positions[position_index].get_unrealized_pnl(oracle_price)?; // cannot settle negative pnl this way on a user who is in liquidation territory - if user.perp_positions[position_index].is_lp() && !user.is_advanced_lp() { - let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( - user, - perp_market_map, - spot_market_map, - oracle_map, - MarginContext::standard(MarginRequirementType::Initial) - .margin_buffer(state.liquidation_margin_buffer_ratio), - )?; - - if !margin_calc.meets_margin_requirement() { - msg!("market={} lp does not meet initial margin requirement, attempting to burn shares for risk reduction", - market_index); - attempt_burn_user_lp_shares_for_risk_reduction( - state, - user, - *user_key, - margin_calc, - perp_market_map, - spot_market_map, - oracle_map, - clock, - market_index, - )?; - - // if the unrealized pnl is negative, return early after trying to burn shares - if unrealized_pnl < 0 - && !(meets_settle_pnl_maintenance_margin_requirement( - user, - perp_market_map, - spot_market_map, - oracle_map, - )?) - { - msg!( - "Unable to settle market={} negative pnl as user is in liquidation territory", - market_index - ); - return Ok(()); - } - } - } else if unrealized_pnl < 0 { + if unrealized_pnl < 0 { // may already be cached let meets_margin_requirement = match meets_margin_requirement { Some(meets_margin_requirement) => meets_margin_requirement, @@ -436,12 +395,6 @@ pub fn settle_expired_position( "User must first cancel open orders for expired market" )?; - validate!( - user.perp_positions[position_index].lp_shares == 0, - ErrorCode::PerpMarketSettlementUserHasActiveLP, - "User must first burn lp shares for expired market" - )?; - let base_asset_value = calculate_base_asset_value_with_expiry_price( &user.perp_positions[position_index], perp_market.expiry_price, @@ -453,7 +406,6 @@ pub fn settle_expired_position( let position_delta = PositionDelta { quote_asset_amount: base_asset_value, base_asset_amount: -user.perp_positions[position_index].base_asset_amount, - remainder_base_asset_amount: None, }; update_position_and_market( diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index a84e4c951a..fea9ef135a 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -74,21 +74,11 @@ pub fn get_position_index(user_positions: &PerpPositions, market_index: u16) -> pub struct PositionDelta { pub quote_asset_amount: i64, pub base_asset_amount: i64, - pub remainder_base_asset_amount: Option, } impl PositionDelta { - pub fn get_delta_base_with_remainder_abs(&self) -> DriftResult { - let delta_base_i128 = - if let Some(remainder_base_asset_amount) = self.remainder_base_asset_amount { - self.base_asset_amount - .safe_add(remainder_base_asset_amount.cast()?)? - .abs() - .cast::()? - } else { - self.base_asset_amount.abs().cast::()? - }; - Ok(delta_base_i128) + pub fn get_delta_base_abs(&self) -> DriftResult { + self.base_asset_amount.abs().cast::() } } @@ -97,7 +87,7 @@ pub fn update_position_and_market( market: &mut PerpMarket, delta: &PositionDelta, ) -> DriftResult { - if delta.base_asset_amount == 0 && delta.remainder_base_asset_amount.unwrap_or(0) == 0 { + if delta.base_asset_amount == 0 { update_quote_asset_amount(position, market, delta.quote_asset_amount)?; return Ok(delta.quote_asset_amount); } @@ -107,12 +97,8 @@ pub fn update_position_and_market( // Update User let ( new_base_asset_amount, - new_settled_base_asset_amount, new_quote_asset_amount, - new_remainder_base_asset_amount, - ) = get_new_position_amounts(position, delta, market)?; - - market.update_market_with_counterparty(delta, new_settled_base_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 => { @@ -127,8 +113,8 @@ pub fn update_position_and_market( (new_quote_entry_amount, new_quote_break_even_amount, 0_i64) } PositionUpdateType::Reduce | PositionUpdateType::Close => { - let current_base_i128 = position.get_base_asset_amount_with_remainder_abs()?; - let delta_base_i128 = delta.get_delta_base_with_remainder_abs()?; + let current_base_i128 = position.get_base_asset_amount_abs()?; + let delta_base_i128 = delta.get_delta_base_abs()?; let new_quote_entry_amount = position.quote_entry_amount.safe_sub( position @@ -156,8 +142,8 @@ pub fn update_position_and_market( (new_quote_entry_amount, new_quote_break_even_amount, pnl) } PositionUpdateType::Flip => { - let current_base_i128 = position.get_base_asset_amount_with_remainder_abs()?; - let delta_base_i128 = delta.get_delta_base_with_remainder_abs()?; + let current_base_i128 = position.get_base_asset_amount_abs()?; + let delta_base_i128 = delta.get_delta_base_abs()?; // same calculation for new_quote_entry_amount let new_quote_break_even_amount = delta.quote_asset_amount.safe_sub( @@ -209,7 +195,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_long = market .amm .base_asset_amount_long - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_long = market .amm .quote_entry_amount_long @@ -223,7 +209,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_short = market .amm .base_asset_amount_short - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_short = market .amm .quote_entry_amount_short @@ -239,7 +225,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_long = market .amm .base_asset_amount_long - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_long = market.amm.quote_entry_amount_long.safe_sub( position .quote_entry_amount @@ -257,7 +243,7 @@ pub fn update_position_and_market( market.amm.base_asset_amount_short = market .amm .base_asset_amount_short - .safe_add(new_settled_base_asset_amount.cast()?)?; + .safe_add(delta.base_asset_amount.cast()?)?; market.amm.quote_entry_amount_short = market.amm.quote_entry_amount_short.safe_sub( position @@ -358,9 +344,6 @@ pub fn update_position_and_market( _ => {} } - let new_position_base_with_remainder = - new_base_asset_amount.safe_add(new_remainder_base_asset_amount)?; - // Update user position if let PositionUpdateType::Close = update_type { position.last_cumulative_funding_rate = 0; @@ -368,7 +351,7 @@ pub fn update_position_and_market( update_type, PositionUpdateType::Open | PositionUpdateType::Increase | PositionUpdateType::Flip ) { - if new_position_base_with_remainder > 0 { + if new_base_asset_amount > 0 { position.last_cumulative_funding_rate = market.amm.cumulative_funding_rate_long.cast()?; } else { @@ -388,7 +371,6 @@ pub fn update_position_and_market( new_base_asset_amount )?; - position.remainder_base_asset_amount = new_remainder_base_asset_amount.cast::()?; position.base_asset_amount = new_base_asset_amount; position.quote_asset_amount = new_quote_asset_amount; @@ -398,68 +380,6 @@ pub fn update_position_and_market( Ok(pnl) } -pub fn update_lp_market_position( - market: &mut PerpMarket, - delta: &PositionDelta, - fee_to_market: i128, - liquidity_split: AMMLiquiditySplit, -) -> DriftResult { - if market.amm.user_lp_shares == 0 || liquidity_split == AMMLiquiditySplit::ProtocolOwned { - return Ok(0); // no need to split with LP - } - - let base_unit: i128 = market.amm.get_per_lp_base_unit()?; - - let (per_lp_delta_base, per_lp_delta_quote, per_lp_fee) = - market - .amm - .calculate_per_lp_delta(delta, fee_to_market, liquidity_split, base_unit)?; - - market.amm.base_asset_amount_per_lp = market - .amm - .base_asset_amount_per_lp - .safe_add(-per_lp_delta_base)?; - - market.amm.quote_asset_amount_per_lp = market - .amm - .quote_asset_amount_per_lp - .safe_add(-per_lp_delta_quote)?; - - // track total fee earned by lps (to attribute breakdown of IL) - market.amm.total_fee_earned_per_lp = market - .amm - .total_fee_earned_per_lp - .saturating_add(per_lp_fee.cast()?); - - // update per lp position - market.amm.quote_asset_amount_per_lp = - market.amm.quote_asset_amount_per_lp.safe_add(per_lp_fee)?; - - let lp_delta_base = market - .amm - .calculate_lp_base_delta(per_lp_delta_base, base_unit)?; - let lp_delta_quote = market - .amm - .calculate_lp_base_delta(per_lp_delta_quote, base_unit)?; - - market.amm.base_asset_amount_with_amm = market - .amm - .base_asset_amount_with_amm - .safe_sub(lp_delta_base)?; - - market.amm.base_asset_amount_with_unsettled_lp = market - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(lp_delta_base)?; - - market.amm.quote_asset_amount_with_unsettled_lp = market - .amm - .quote_asset_amount_with_unsettled_lp - .safe_add(lp_delta_quote.cast()?)?; - - Ok(lp_delta_base) -} - pub fn update_position_with_base_asset_amount( base_asset_amount: u64, direction: PositionDirection, @@ -557,7 +477,6 @@ pub fn update_quote_asset_amount( if position.quote_asset_amount == 0 && position.base_asset_amount == 0 - && position.remainder_base_asset_amount == 0 { market.number_of_users = market.number_of_users.safe_add(1)?; } @@ -568,7 +487,6 @@ pub fn update_quote_asset_amount( if position.quote_asset_amount == 0 && position.base_asset_amount == 0 - && position.remainder_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 7145c7dabd..bfe693fe44 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -1,9 +1,8 @@ use crate::controller::amm::{ calculate_base_swap_output_with_spread, move_price, recenter_perp_market_amm, swap_base_asset, }; -use crate::controller::lp::{apply_lp_rebase_to_perp_market, settle_lp_position}; use crate::controller::position::{ - update_lp_market_position, update_position_and_market, PositionDelta, + update_position_and_market, PositionDelta, }; use crate::controller::repeg::_update_amm; @@ -13,7 +12,6 @@ use crate::math::constants::{ PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; -use crate::math::lp::calculate_settle_lp_metrics; use crate::math::position::swap_direction_to_close_position; use crate::math::repeg; use crate::state::oracle::{OraclePriceData, PrelaunchOracle}; @@ -43,33 +41,6 @@ use anchor_lang::Owner; use solana_program::pubkey::Pubkey; use std::str::FromStr; -#[test] -fn full_amm_split() { - let delta = PositionDelta { - base_asset_amount: 10 * BASE_PRECISION_I64, - quote_asset_amount: -10 * BASE_PRECISION_I64, - remainder_base_asset_amount: None, - }; - - let amm = AMM { - user_lp_shares: 0, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: 10 * AMM_RESERVE_PRECISION_I128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - update_lp_market_position(&mut market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(market.amm.base_asset_amount_with_unsettled_lp, 0); - assert_eq!( - market.amm.base_asset_amount_with_amm, - 10 * AMM_RESERVE_PRECISION_I128 - ); -} #[test] fn amm_pool_balance_liq_fees_example() { @@ -707,566 +678,12 @@ fn amm_perp_ref_offset() { crate::validation::perp_market::validate_perp_market(&perp_market).unwrap(); } -#[test] -fn amm_split_large_k() { - let perp_market_str = String::from("Ct8MLGv1N/dvAH3EF67yBqaUQerctpm4yqpK+QNSrXCQz76p+B+ka+8Ni2/aLOukHaFdQJXR2jkqDS+O0MbHvA9M+sjCgLVtQwhkAQAAAAAAAAAAAAAAAAIAAAAAAAAAkI1kAQAAAAB6XWQBAAAAAO8yzWQAAAAAnJ7I3f///////////////2dHvwAAAAAAAAAAAAAAAABGiVjX6roAAAAAAAAAAAAAAAAAAAAAAAB1tO47J+xiAAAAAAAAAAAAGD03Fis3mgAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAABxqRCIGRxiAAAAAAAAAAAAEy8wZfK9YwAAAAAAAAAAAGZeZCE+g3sAAAAAAAAAAAAKYeQAAAAAAAAAAAAAAAAAlIvoyyc3mgAAAAAAAAAAAADQdQKjbgAAAAAAAAAAAAAAwu8g05H/////////////E6tNHAIAAAAAAAAAAAAAAO3mFwd0AAAAAAAAAAAAAAAAgPQg5rUAAAAAAAAAAAAAGkDtXR4AAAAAAAAAAAAAAEv0WeZW/f////////////9kUidaqAIAAAAAAAAAAAAA0ZMEr1H9/////////////w5/U3uqAgAAAAAAAAAAAAAANfbqfCd3AAAAAAAAAAAAIhABAAAAAAAiEAEAAAAAACIQAQAAAAAAY1QBAAAAAAA5f3WMVAAAAAAAAAAAAAAAFhkiihsAAAAAAAAAAAAAAO2EfWc5AAAAAAAAAAAAAACM/5CAQgAAAAAAAAAAAAAAvenX0SsAAAAAAAAAAAAAALgPUogZAAAAAAAAAAAAAAC01x97AAAAAAAAAAAAAAAAOXzVbgAAAAAAAAAAAAAAAMG4+QwBAAAAAAAAAAAAAABwHI3fLeJiAAAAAAAAAAAABvigOblGmgAAAAAAAAAAALeRnZsi9mIAAAAAAAAAAAAqgs3ynCeaAAAAAAAAAAAAQwhkAQAAAAAAAAAAAAAAAJOMZAEAAAAAFKJkAQAAAABTl2QBAAAAALFuZAEAAAAAgrx7DAAAAAAUAwAAAAAAAAN1TAYAAAAAuC7NZAAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAA4fUFAAAAAAAAAAAAAAAAn2HvyMABAADGV6rZFwAAAE5Qg2oPAAAA8zHNZAAAAAAdYAAAAAAAAE2FAAAAAAAA6zLNZAAAAAD6AAAAaEIAABQDAAAUAwAAAAAAANcBAABkADIAZGQAAcDIUt4AAAAA0QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI9qQbynsAAAAAAAAAAAAAAAAAAAAAAAAFNPTC1QRVJQICAgICAgICAgICAgICAgICAgICAgICAghuS1//////8A4fUFAAAAAAB0O6QLAAAAR7PdeQMAAAD+Mc1kAAAAAADKmjsAAAAAAAAAAAAAAAAAAAAAAAAAAOULDwAAAAAAUBkAAAAAAADtAQAAAAAAAMgAAAAAAAAAECcAAKhhAADoAwAA9AEAAAAAAAAQJwAAZAIAAGQCAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655); - - let og_baapl = perp_market.amm.base_asset_amount_per_lp; - let og_qaapl = perp_market.amm.quote_asset_amount_per_lp; - - // msg!("perp_market: {:?}", perp_market); - - // min long order for $2.3 - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64 / 10, - quote_asset_amount: -2300000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054758); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655); - - // min short order for $2.3 - let delta = PositionDelta { - base_asset_amount: -BASE_PRECISION_I64 / 10, - quote_asset_amount: 2300000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535654); - - let mut existing_position = PerpPosition { - market_index: 0, - base_asset_amount: 0, - quote_asset_amount: 0, - lp_shares: perp_market.amm.user_lp_shares as u64, - last_base_asset_amount_per_lp: og_baapl as i64, - last_quote_asset_amount_per_lp: og_qaapl as i64, - per_lp_base: 0, - ..PerpPosition::default() - }; - - settle_lp_position(&mut existing_position, &mut perp_market).unwrap(); - - assert_eq!(existing_position.base_asset_amount, 0); - assert_eq!(existing_position.remainder_base_asset_amount, 0); - assert_eq!(existing_position.quote_asset_amount, -33538939); // out of favor rounding - - assert_eq!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - // long order for $230 - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64 * 10, - quote_asset_amount: -230000000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574055043); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535660); - - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_baapl - perp_market.amm.base_asset_amount_per_lp) - / AMM_RESERVE_PRECISION_I128, - 9977763076 - ); - // assert_eq!((perp_market.amm.sqrt_k as i128) * (og_baapl-perp_market.amm.base_asset_amount_per_lp) / AMM_RESERVE_PRECISION_I128, 104297175); - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_qaapl - perp_market.amm.quote_asset_amount_per_lp) - / QUOTE_PRECISION_I128, - -173828625034 - ); - assert_eq!( - (perp_market.amm.sqrt_k as i128) - * (og_qaapl - perp_market.amm.quote_asset_amount_per_lp - 1) - / QUOTE_PRECISION_I128, - -208594350041 - ); - // assert_eq!(243360075047/9977763076 < 23, true); // ensure rounding in favor - - // long order for $230 - let delta = PositionDelta { - base_asset_amount: -BASE_PRECISION_I64 * 10, - quote_asset_amount: 230000000, - remainder_base_asset_amount: None, - }; - - let og_baapl = perp_market.amm.base_asset_amount_per_lp; - let og_qaapl = perp_market.amm.quote_asset_amount_per_lp; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535653); - - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_baapl - perp_market.amm.base_asset_amount_per_lp) - / AMM_RESERVE_PRECISION_I128, - -9977763076 - ); - // assert_eq!((perp_market.amm.sqrt_k as i128) * (og_baapl-perp_market.amm.base_asset_amount_per_lp) / AMM_RESERVE_PRECISION_I128, 104297175); - assert_eq!( - (perp_market.amm.sqrt_k as i128) * (og_qaapl - perp_market.amm.quote_asset_amount_per_lp) - / QUOTE_PRECISION_I128, - 243360075047 - ); - // assert_eq!(243360075047/9977763076 < 23, true); // ensure rounding in favor -} - -#[test] -fn test_quote_unsettled_lp() { - let perp_market_str = String::from("Ct8MLGv1N/dvAH3EF67yBqaUQerctpm4yqpK+QNSrXCQz76p+B+ka+8Ni2/aLOukHaFdQJXR2jkqDS+O0MbHvA9M+sjCgLVtzjkqCQAAAAAAAAAAAAAAAAIAAAAAAAAAl44wCQAAAAD54C0JAAAAAGJ4JmYAAAAAyqMxdXz//////////////wV1ZyH9//////////////8Uy592jFYPAAAAAAAAAAAAAAAAAAAAAAD6zIP0/dAIAAAAAAAAAAAA+srqThjtHwAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAAByWgjyVb4IAAAAAAAAAAAAOpuf9pLjCAAAAAAAAAAAAMRfA6LzxhAAAAAAAAAAAABs6IcCAAAAAAAAAAAAAAAAeXyo6oHtHwAAAAAAAAAAAABngilYXAEAAAAAAAAAAAAAZMIneaP+////////////GeN71uL//////////////+fnyHru//////////////8AIA8MEgUDAAAAAAAAAAAAv1P8g/EBAAAAAAAAAAAAACNQgLCty/////////////+KMQ7JGjMAAAAAAAAAAAAA4DK7xH3K/////////////2grSsB0NQAAAAAAAAAAAACsBC7WWDkCAAAAAAAAAAAAsis3AAAAAACyKzcAAAAAALIrNwAAAAAATGc8AAAAAADH51Hn/wYAAAAAAAAAAAAANXNbBAgCAAAAAAAAAAAAAPNHO0UKBQAAAAAAAAAAAABiEweaqQUAAAAAAAAAAAAAg16F138BAAAAAAAAAAAAAFBZFMk0AQAAAAAAAAAAAACoA6JpBwAAAAAAAAAAAAAALahXXQcAAAAAAAAAAAAAAMG4+QwBAAAAAAAAAAAAAADr9qfqkdAIAAAAAAAAAAAAlBk2nZ/uHwAAAAAAAAAAAHPdcUR+0QgAAAAAAAAAAAAF+03DR+sfAAAAAAAAAAAAzjkqCQAAAAAAAAAAAAAAAJXnMAkAAAAAT9IxCQAAAADyXDEJAAAAAKlJLgkAAAAAyg2YDwAAAABfBwAAAAAAANVPrUEAAAAAZW0mZgAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAA4fUFAAAAAAAAAAAAAAAAj0W2KSYpAABzqJhf6gAAAOD5o985AQAAS3gmZgAAAADxKQYAAAAAAMlUBgAAAAAAS3gmZgAAAADuAgAA7CwAAHcBAAC9AQAAAAAAAH0AAADECTIAZMgAAcDIUt4DAAAAFJMfEQAAAADBogAAAAAAAIneROQcpf//AAAAAAAAAAAAAAAAAAAAAFe4ynNxUwoAAAAAAAAAAAAAAAAAAAAAAFNPTC1QRVJQICAgICAgICAgICAgICAgICAgICAgICAgAJsy4v////8AZc0dAAAAAP8PpdToAAAANOVq3RYAAAB7cyZmAAAAAADh9QUAAAAAAAAAAAAAAAAAAAAAAAAAAEyBWwAAAAAA2DEAAAAAAABzBQAAAAAAAMgAAAAAAAAATB0AANQwAADoAwAA9AEAAAAAAAAQJwAAASoAACtgAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); - perp_market.amm.quote_asset_amount_with_unsettled_lp = 0; - - let mut existing_position: PerpPosition = PerpPosition::default(); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, -12324473595); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -564969495606); - - existing_position.last_quote_asset_amount_per_lp = - perp_market.amm.quote_asset_amount_per_lp as i64; - existing_position.last_base_asset_amount_per_lp = - perp_market.amm.base_asset_amount_per_lp as i64; - - let pos_delta = PositionDelta { - quote_asset_amount: QUOTE_PRECISION_I64 * 150, - base_asset_amount: -BASE_PRECISION_I64, - remainder_base_asset_amount: Some(-881), - }; - assert_eq!(perp_market.amm.quote_asset_amount_with_unsettled_lp, 0); - let fee_to_market = 1000000; // uno doll - let liq_split = AMMLiquiditySplit::Shared; - let base_unit: i128 = perp_market.amm.get_per_lp_base_unit().unwrap(); - assert_eq!(base_unit, 1_000_000_000_000); // 10^4 * base_precision - - let (per_lp_delta_base, per_lp_delta_quote, per_lp_fee) = perp_market - .amm - .calculate_per_lp_delta(&pos_delta, fee_to_market, liq_split, base_unit) - .unwrap(); - - assert_eq!(per_lp_delta_base, -211759); - assert_eq!(per_lp_delta_quote, 31764); - assert_eq!(per_lp_fee, 169); - - let pos_delta2 = PositionDelta { - quote_asset_amount: -QUOTE_PRECISION_I64 * 150, - base_asset_amount: BASE_PRECISION_I64, - remainder_base_asset_amount: Some(0), - }; - let (per_lp_delta_base, per_lp_delta_quote, per_lp_fee) = perp_market - .amm - .calculate_per_lp_delta(&pos_delta2, fee_to_market, liq_split, base_unit) - .unwrap(); - - assert_eq!(per_lp_delta_base, 211759); - assert_eq!(per_lp_delta_quote, -31763); - assert_eq!(per_lp_fee, 169); - - let expected_base_asset_amount_with_unsettled_lp = -75249424409; - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - // 0 - expected_base_asset_amount_with_unsettled_lp // ~-75 - ); - // let lp_delta_quote = perp_market - // .amm - // .calculate_lp_base_delta(per_lp_delta_quote, QUOTE_PRECISION_I128) - // .unwrap(); - // assert_eq!(lp_delta_quote, -19883754464333); - - let delta_base = - update_lp_market_position(&mut perp_market, &pos_delta, fee_to_market, liq_split).unwrap(); - assert_eq!( - perp_market.amm.user_lp_shares * 1000000 / perp_market.amm.sqrt_k, - 132561 - ); // 13.2 % of amm - assert_eq!( - perp_market.amm.quote_asset_amount_with_unsettled_lp, - 19884380 - ); // 19.884380/.132 ~= 150 (+ fee) - assert_eq!(delta_base, -132_561_910); // ~13% - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - // 0 - -75381986319 // ~-75 - ); - - // settle lp and quote with unsettled should go back to zero - existing_position.lp_shares = perp_market.amm.user_lp_shares as u64; - existing_position.per_lp_base = 3; - - let lp_metrics: crate::math::lp::LPMetrics = - calculate_settle_lp_metrics(&perp_market.amm, &existing_position).unwrap(); - - let position_delta = PositionDelta { - base_asset_amount: lp_metrics.base_asset_amount as i64, - quote_asset_amount: lp_metrics.quote_asset_amount as i64, - remainder_base_asset_amount: Some(lp_metrics.remainder_base_asset_amount as i64), - }; - - assert_eq!(position_delta.base_asset_amount, 100000000); - - assert_eq!( - position_delta.remainder_base_asset_amount.unwrap_or(0), - 32561910 - ); - - assert_eq!(position_delta.quote_asset_amount, -19778585); - - let pnl = update_position_and_market(&mut existing_position, &mut perp_market, &position_delta) - .unwrap(); - - //.132561*1e6*.8 = 106048.8 - assert_eq!(perp_market.amm.quote_asset_amount_with_unsettled_lp, 105795); //? - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - expected_base_asset_amount_with_unsettled_lp - 32561910 - ); - - assert_eq!(pnl, 0); -} - -#[test] -fn amm_split_large_k_with_rebase() { - let perp_market_str = String::from("Ct8MLGv1N/dvAH3EF67yBqaUQerctpm4yqpK+QNSrXCQz76p+B+ka+8Ni2/aLOukHaFdQJXR2jkqDS+O0MbHvA9M+sjCgLVtQwhkAQAAAAAAAAAAAAAAAAIAAAAAAAAAkI1kAQAAAAB6XWQBAAAAAO8yzWQAAAAAnJ7I3f///////////////2dHvwAAAAAAAAAAAAAAAABGiVjX6roAAAAAAAAAAAAAAAAAAAAAAAB1tO47J+xiAAAAAAAAAAAAGD03Fis3mgAAAAAAAAAAAJxiDwAAAAAAAAAAAAAAAABxqRCIGRxiAAAAAAAAAAAAEy8wZfK9YwAAAAAAAAAAAGZeZCE+g3sAAAAAAAAAAAAKYeQAAAAAAAAAAAAAAAAAlIvoyyc3mgAAAAAAAAAAAADQdQKjbgAAAAAAAAAAAAAAwu8g05H/////////////E6tNHAIAAAAAAAAAAAAAAO3mFwd0AAAAAAAAAAAAAAAAgPQg5rUAAAAAAAAAAAAAGkDtXR4AAAAAAAAAAAAAAEv0WeZW/f////////////9kUidaqAIAAAAAAAAAAAAA0ZMEr1H9/////////////w5/U3uqAgAAAAAAAAAAAAAANfbqfCd3AAAAAAAAAAAAIhABAAAAAAAiEAEAAAAAACIQAQAAAAAAY1QBAAAAAAA5f3WMVAAAAAAAAAAAAAAAFhkiihsAAAAAAAAAAAAAAO2EfWc5AAAAAAAAAAAAAACM/5CAQgAAAAAAAAAAAAAAvenX0SsAAAAAAAAAAAAAALgPUogZAAAAAAAAAAAAAAC01x97AAAAAAAAAAAAAAAAOXzVbgAAAAAAAAAAAAAAAMG4+QwBAAAAAAAAAAAAAABwHI3fLeJiAAAAAAAAAAAABvigOblGmgAAAAAAAAAAALeRnZsi9mIAAAAAAAAAAAAqgs3ynCeaAAAAAAAAAAAAQwhkAQAAAAAAAAAAAAAAAJOMZAEAAAAAFKJkAQAAAABTl2QBAAAAALFuZAEAAAAAgrx7DAAAAAAUAwAAAAAAAAN1TAYAAAAAuC7NZAAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAA4fUFAAAAAAAAAAAAAAAAn2HvyMABAADGV6rZFwAAAE5Qg2oPAAAA8zHNZAAAAAAdYAAAAAAAAE2FAAAAAAAA6zLNZAAAAAD6AAAAaEIAABQDAAAUAwAAAAAAANcBAABkADIAZGQAAcDIUt4AAAAA0QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI9qQbynsAAAAAAAAAAAAAAAAAAAAAAAAFNPTC1QRVJQICAgICAgICAgICAgICAgICAgICAgICAghuS1//////8A4fUFAAAAAAB0O6QLAAAAR7PdeQMAAAD+Mc1kAAAAAADKmjsAAAAAAAAAAAAAAAAAAAAAAAAAAOULDwAAAAAAUBkAAAAAAADtAQAAAAAAAMgAAAAAAAAAECcAAKhhAADoAwAA9AEAAAAAAAAQJwAAZAIAAGQCAAAAAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let mut perp_market = perp_market_loader.load_mut().unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498335213293 - ); - - let og_baawul = perp_market.amm.base_asset_amount_with_unsettled_lp; - let og_baapl = perp_market.amm.base_asset_amount_per_lp; - let og_qaapl = perp_market.amm.quote_asset_amount_per_lp; - - // update base - let base_change = 5; - apply_lp_rebase_to_perp_market(&mut perp_market, base_change).unwrap(); - - // noop delta - let delta = PositionDelta { - base_asset_amount: 0, - quote_asset_amount: 0, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, og_qaapl * 100000); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, og_baapl * 100000); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - og_baawul - ); - - // min long order for $2.3 - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64 / 10, - quote_asset_amount: -2300000, - remainder_base_asset_amount: None, - }; - - let u1 = - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - assert_eq!(u1, 96471070); - - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498431684363 - ); - - assert_eq!( - perp_market.amm.base_asset_amount_per_lp - og_baapl * 100000, - -287639 - ); - assert_eq!( - perp_market.amm.quote_asset_amount_per_lp - og_qaapl * 100000, - 6615 - ); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp - og_baawul, - 96471070 - ); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -57405475887639); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 1253565506615); - - let num = perp_market.amm.quote_asset_amount_per_lp - (og_qaapl * 100000); - let denom = perp_market.amm.base_asset_amount_per_lp - (og_baapl * 100000); - assert_eq!(-num * 1000000 / denom, 22997); // $22.997 cost basis for short (vs $23 actual) - - // min short order for $2.3 - let delta = PositionDelta { - base_asset_amount: -BASE_PRECISION_I64 / 10, - quote_asset_amount: 2300000, - remainder_base_asset_amount: None, - }; - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -57405475600000); - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 1253565499999); - assert_eq!( - (og_qaapl * 100000) - perp_market.amm.quote_asset_amount_per_lp, - 1 - ); - - let mut existing_position = PerpPosition { - market_index: 0, - base_asset_amount: 0, - quote_asset_amount: 0, - lp_shares: perp_market.amm.user_lp_shares as u64, - last_base_asset_amount_per_lp: og_baapl as i64, - last_quote_asset_amount_per_lp: og_qaapl as i64, - per_lp_base: 0, - ..PerpPosition::default() - }; - - settle_lp_position(&mut existing_position, &mut perp_market).unwrap(); - - assert_eq!(existing_position.base_asset_amount, 0); - assert_eq!(existing_position.remainder_base_asset_amount, 0); - assert_eq!(existing_position.quote_asset_amount, -335); // out of favor rounding... :/ - - assert_eq!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - assert_eq!( - perp_market - .amm - .imbalanced_base_asset_amount_with_lp() - .unwrap(), - -303686915482213 - ); - - assert_eq!(perp_market.amm.target_base_asset_amount_per_lp, -565000000); - - // update base back - let base_change = -2; - apply_lp_rebase_to_perp_market(&mut perp_market, base_change).unwrap(); - // noop delta - let delta = PositionDelta { - base_asset_amount: 0, - quote_asset_amount: 0, - remainder_base_asset_amount: None, - }; - - // unchanged with rebase (even when target!=0) - assert_eq!( - perp_market - .amm - .imbalanced_base_asset_amount_with_lp() - .unwrap(), - -303686915482213 - ); - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!( - perp_market.amm.quote_asset_amount_per_lp, - og_qaapl * 1000 - 1 - ); // down only rounding - assert_eq!(perp_market.amm.base_asset_amount_per_lp, og_baapl * 1000); - - // 1 long order for $23 before lp position does rebasing - let delta = PositionDelta { - base_asset_amount: BASE_PRECISION_I64, - quote_asset_amount: -23000000, - remainder_base_asset_amount: None, - }; - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054756000); - - update_lp_market_position(&mut perp_market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - let now = 110; - let clock_slot = 111; - let state = State::default(); - let oracle_price_data = OraclePriceData { - price: 23 * PRICE_PRECISION_I64, - confidence: PRICE_PRECISION_U64 / 100, - delay: 14, - has_sufficient_number_of_data_points: true, - }; - - let cost = _update_amm( - &mut perp_market, - &oracle_price_data, - &state, - now, - clock_slot, - ) - .unwrap(); - assert_eq!(cost, -3017938); - - assert_eq!(perp_market.amm.quote_asset_amount_per_lp, 12535655660); - assert_eq!(perp_market.amm.base_asset_amount_per_lp, -574054784763); - assert_eq!( - existing_position.last_base_asset_amount_per_lp, - -57405475600000 - ); - assert_eq!(existing_position.per_lp_base, 5); - assert_ne!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - assert_eq!(perp_market.amm.base_asset_amount_long, 121646400000000); - assert_eq!(perp_market.amm.base_asset_amount_short, -121139000000000); - assert_eq!(perp_market.amm.base_asset_amount_with_amm, 8100106185); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 499299893815 - ); - let prev_with_unsettled_lp = perp_market.amm.base_asset_amount_with_unsettled_lp; - settle_lp_position(&mut existing_position, &mut perp_market).unwrap(); - - assert_eq!(perp_market.amm.base_asset_amount_long, 121646400000000); - assert_eq!(perp_market.amm.base_asset_amount_short, -121139900000000); - assert_eq!(perp_market.amm.base_asset_amount_with_amm, 8100106185); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498399893815 - ); - assert_eq!( - perp_market.amm.base_asset_amount_with_unsettled_lp, - 498399893815 - ); - assert!(perp_market.amm.base_asset_amount_with_unsettled_lp < prev_with_unsettled_lp); - - // 96.47% owned - assert_eq!(perp_market.amm.user_lp_shares, 33538939700000000); - assert_eq!(perp_market.amm.sqrt_k, 34765725006847590); - - assert_eq!(existing_position.per_lp_base, perp_market.amm.per_lp_base); - - assert_eq!(existing_position.base_asset_amount, -900000000); - assert_eq!(existing_position.remainder_base_asset_amount, -64680522); - assert_eq!(existing_position.quote_asset_amount, 22168904); // out of favor rounding... :/ - assert_eq!( - existing_position.last_base_asset_amount_per_lp, - perp_market.amm.base_asset_amount_per_lp as i64 - ); // out of favor rounding... :/ - assert_eq!( - existing_position.last_quote_asset_amount_per_lp, - perp_market.amm.quote_asset_amount_per_lp as i64 - ); // out of favor rounding... :/ -} - -#[test] -fn full_lp_split() { - let delta = PositionDelta { - base_asset_amount: 10 * BASE_PRECISION_I64, - quote_asset_amount: -10 * BASE_PRECISION_I64, - remainder_base_asset_amount: None, - }; - - let amm = AMM { - user_lp_shares: 100 * AMM_RESERVE_PRECISION, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: 10 * AMM_RESERVE_PRECISION_I128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - update_lp_market_position(&mut market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!( - market.amm.base_asset_amount_per_lp as i64, - -10 * BASE_PRECISION_I64 / 100 - ); - assert_eq!( - market.amm.quote_asset_amount_per_lp as i64, - 10 * BASE_PRECISION_I64 / 100 - ); - assert_eq!(market.amm.base_asset_amount_with_amm, 0); - assert_eq!( - market.amm.base_asset_amount_with_unsettled_lp, - 10 * AMM_RESERVE_PRECISION_I128 - ); -} - -#[test] -fn half_half_amm_lp_split() { - let delta = PositionDelta { - base_asset_amount: 10 * BASE_PRECISION_I64, - quote_asset_amount: -10 * BASE_PRECISION_I64, - remainder_base_asset_amount: None, - }; - - let amm = AMM { - user_lp_shares: 100 * AMM_RESERVE_PRECISION, - sqrt_k: 200 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: 10 * AMM_RESERVE_PRECISION_I128, - ..AMM::default_test() - }; - let mut market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - update_lp_market_position(&mut market, &delta, 0, AMMLiquiditySplit::Shared).unwrap(); - - assert_eq!( - market.amm.base_asset_amount_with_amm, - 5 * AMM_RESERVE_PRECISION_I128 - ); - assert_eq!( - market.amm.base_asset_amount_with_unsettled_lp, - 5 * AMM_RESERVE_PRECISION_I128 - ); -} - #[test] fn test_position_entry_sim() { let mut existing_position: PerpPosition = PerpPosition::default(); let position_delta = PositionDelta { base_asset_amount: BASE_PRECISION_I64 / 2, quote_asset_amount: -99_345_000 / 2, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1288,7 +705,6 @@ fn test_position_entry_sim() { let position_delta_to_reduce = PositionDelta { base_asset_amount: -BASE_PRECISION_I64 / 5, quote_asset_amount: 99_245_000 / 5, - remainder_base_asset_amount: None, }; let pnl = update_position_and_market( @@ -1306,7 +722,6 @@ fn test_position_entry_sim() { let position_delta_to_flip = PositionDelta { base_asset_amount: -BASE_PRECISION_I64, quote_asset_amount: 99_345_000, - remainder_base_asset_amount: None, }; let pnl = @@ -1325,7 +740,6 @@ fn increase_long_from_no_position() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1365,7 +779,6 @@ fn increase_short_from_no_position() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1409,7 +822,6 @@ fn increase_long() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1461,7 +873,6 @@ fn increase_short() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 1, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1510,7 +921,6 @@ fn reduce_long_profitable() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1561,7 +971,6 @@ fn reduce_long_unprofitable() { let position_delta = PositionDelta { base_asset_amount: -1, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1612,7 +1021,6 @@ fn flip_long_to_short_profitable() { let position_delta = PositionDelta { base_asset_amount: -11, quote_asset_amount: 22, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1664,7 +1072,6 @@ fn flip_long_to_short_unprofitable() { let position_delta = PositionDelta { base_asset_amount: -11, quote_asset_amount: 10, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1717,7 +1124,6 @@ fn reduce_short_profitable() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1766,7 +1172,6 @@ fn decrease_short_unprofitable() { let position_delta = PositionDelta { base_asset_amount: 1, quote_asset_amount: -15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1815,7 +1220,6 @@ fn flip_short_to_long_profitable() { let position_delta = PositionDelta { base_asset_amount: 11, quote_asset_amount: -60, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1867,7 +1271,6 @@ fn flip_short_to_long_unprofitable() { let position_delta = PositionDelta { base_asset_amount: 11, quote_asset_amount: -120, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1919,7 +1322,6 @@ fn close_long_profitable() { let position_delta = PositionDelta { base_asset_amount: -10, quote_asset_amount: 15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -1970,7 +1372,6 @@ fn close_long_unprofitable() { let position_delta = PositionDelta { base_asset_amount: -10, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2020,7 +1421,6 @@ fn close_short_profitable() { let position_delta = PositionDelta { base_asset_amount: 10, quote_asset_amount: -5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2068,7 +1468,6 @@ fn close_short_unprofitable() { let position_delta = PositionDelta { base_asset_amount: 10, quote_asset_amount: -15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2116,7 +1515,6 @@ fn close_long_with_quote_break_even_amount_less_than_quote_asset_amount() { let position_delta = PositionDelta { base_asset_amount: -10, quote_asset_amount: 5, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { @@ -2167,7 +1565,6 @@ fn close_short_with_quote_break_even_amount_more_than_quote_asset_amount() { let position_delta = PositionDelta { base_asset_amount: 10, quote_asset_amount: -15, - remainder_base_asset_amount: None, }; let mut market = PerpMarket { amm: AMM { diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index b5a8acb8f4..2510948d5d 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3422,39 +3422,6 @@ pub fn handle_update_perp_market_target_base_asset_amount_per_lp( Ok(()) } -pub fn handle_update_perp_market_per_lp_base( - ctx: Context, - per_lp_base: i8, -) -> Result<()> { - let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; - msg!("perp market {}", perp_market.market_index); - - let old_per_lp_base = perp_market.amm.per_lp_base; - msg!( - "updated perp_market per_lp_base {} -> {}", - old_per_lp_base, - per_lp_base - ); - - let expo_diff = per_lp_base.safe_sub(old_per_lp_base)?; - - validate!( - expo_diff.abs() == 1, - ErrorCode::DefaultError, - "invalid expo update (must be 1)", - )?; - - validate!( - per_lp_base.abs() <= 9, - ErrorCode::DefaultError, - "only consider lp_base within range of AMM_RESERVE_PRECISION", - )?; - - controller::lp::apply_lp_rebase_to_perp_market(perp_market, expo_diff)?; - - Ok(()) -} - pub fn handle_update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 4b00898041..222ec30755 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1077,37 +1077,6 @@ pub fn handle_settle_funding_payment<'c: 'info, 'info>( Ok(()) } -#[access_control( - amm_not_paused(&ctx.accounts.state) -)] -pub fn handle_settle_lp<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, SettleLP>, - market_index: u16, -) -> 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 AccountMaps { - perp_market_map, .. - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - let market = &mut perp_market_map.get_ref_mut(&market_index)?; - controller::lp::settle_funding_payment_then_lp(user, &user_key, market, now)?; - user.update_last_active_slot(clock.slot); - - Ok(()) -} - #[access_control( liq_not_paused(&ctx.accounts.state) )] diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3717db74cc..80c3d7c013 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -11,6 +11,7 @@ use anchor_spl::{ use solana_program::program::invoke; use solana_program::system_instruction::transfer; +use crate::controller::funding::settle_funding_payment; use crate::controller::orders::{cancel_orders, ModifyOrderId}; use crate::controller::position::update_position_and_market; use crate::controller::position::PositionDirection; @@ -1611,14 +1612,14 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( &clock, )?; - controller::lp::settle_funding_payment_then_lp( + settle_funding_payment( &mut from_user, &from_user_key, perp_market_map.get_ref_mut(&market_index)?.deref_mut(), now, )?; - controller::lp::settle_funding_payment_then_lp( + settle_funding_payment( &mut to_user, &to_user_key, perp_market_map.get_ref_mut(&market_index)?.deref_mut(), @@ -2923,208 +2924,6 @@ pub fn handle_place_and_make_spot_order<'c: 'info, 'info>( Ok(()) } -#[access_control( - amm_not_paused(&ctx.accounts.state) -)] -pub fn handle_add_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - n_shares: u64, - market_index: u16, -) -> 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; - - msg!("add_perp_lp_shares is disabled"); - return Err(ErrorCode::DefaultError.into()); - - let AccountMaps { - perp_market_map, - spot_market_map, - mut oracle_map, - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; - math::liquidation::validate_user_not_being_liquidated( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - state.liquidation_margin_buffer_ratio, - )?; - - { - let mut market = perp_market_map.get_ref_mut(&market_index)?; - - validate!( - matches!(market.status, MarketStatus::Active), - ErrorCode::MarketStatusInvalidForNewLP, - "Market Status doesn't allow for new LP liquidity" - )?; - - validate!( - !matches!(market.contract_type, ContractType::Prediction), - ErrorCode::MarketStatusInvalidForNewLP, - "Contract Type doesn't allow for LP liquidity" - )?; - - validate!( - !market.is_operation_paused(PerpOperation::AmmFill), - ErrorCode::MarketStatusInvalidForNewLP, - "Market amm fills paused" - )?; - - validate!( - n_shares >= market.amm.order_step_size, - ErrorCode::NewLPSizeTooSmall, - "minting {} shares is less than step size {}", - n_shares, - market.amm.order_step_size, - )?; - - controller::funding::settle_funding_payment(user, &user_key, &mut market, now)?; - - // standardize n shares to mint - let n_shares = crate::math::orders::standardize_base_asset_amount( - n_shares.cast()?, - market.amm.order_step_size, - )? - .cast::()?; - - controller::lp::mint_lp_shares( - user.force_get_perp_position_mut(market_index)?, - &mut market, - n_shares, - )?; - - user.last_add_perp_lp_shares_ts = now; - } - - // check margin requirements - meets_place_order_margin_requirement( - user, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - true, - )?; - - user.update_last_active_slot(clock.slot); - - emit!(LPRecord { - ts: now, - action: LPAction::AddLiquidity, - user: user_key, - n_shares, - market_index, - ..LPRecord::default() - }); - - Ok(()) -} - -pub fn handle_remove_perp_lp_shares_in_expiring_market<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, RemoveLiquidityInExpiredMarket<'info>>, - shares_to_burn: u64, - market_index: u16, -) -> 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 AccountMaps { - perp_market_map, - mut oracle_map, - .. - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - // additional validate - { - let signer_is_admin = ctx.accounts.signer.key() == admin_hot_wallet::id(); - let market = perp_market_map.get_ref(&market_index)?; - validate!( - market.is_reduce_only()? || signer_is_admin, - ErrorCode::PerpMarketNotInReduceOnly, - "Can only permissionless burn when market is in reduce only" - )?; - } - - controller::lp::remove_perp_lp_shares( - perp_market_map, - &mut oracle_map, - state, - user, - user_key, - shares_to_burn, - market_index, - now, - )?; - - user.update_last_active_slot(clock.slot); - - Ok(()) -} - -#[access_control( - amm_not_paused(&ctx.accounts.state) -)] -pub fn handle_remove_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - shares_to_burn: u64, - market_index: u16, -) -> 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 AccountMaps { - perp_market_map, - mut oracle_map, - .. - } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), - &get_writable_perp_market_set(market_index), - &MarketSet::new(), - clock.slot, - Some(state.oracle_guard_rails), - )?; - - controller::lp::remove_perp_lp_shares( - perp_market_map, - &mut oracle_map, - state, - user, - user_key, - shares_to_burn, - market_index, - now, - )?; - - user.update_last_active_slot(clock.slot); - - Ok(()) -} - pub fn handle_update_user_name( ctx: Context, _sub_account_id: u16, @@ -4618,25 +4417,6 @@ pub struct PlaceAndMatchRFQOrders<'info> { pub ix_sysvar: AccountInfo<'info>, } -#[derive(Accounts)] -pub struct AddRemoveLiquidity<'info> { - pub state: Box>, - #[account( - mut, - constraint = can_sign_for_user(&user, &authority)?, - )] - pub user: AccountLoader<'info, User>, - pub authority: Signer<'info>, -} - -#[derive(Accounts)] -pub struct RemoveLiquidityInExpiredMarket<'info> { - pub state: Box>, - #[account(mut)] - pub user: AccountLoader<'info, User>, - pub signer: Signer<'info>, -} - #[derive(Accounts)] #[instruction( sub_account_id: u16, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 9deb3ceca9..92981feac5 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -324,30 +324,6 @@ pub mod drift { ) } - pub fn add_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - n_shares: u64, - market_index: u16, - ) -> Result<()> { - handle_add_perp_lp_shares(ctx, n_shares, market_index) - } - - pub fn remove_perp_lp_shares<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, AddRemoveLiquidity<'info>>, - shares_to_burn: u64, - market_index: u16, - ) -> Result<()> { - handle_remove_perp_lp_shares(ctx, shares_to_burn, market_index) - } - - pub fn remove_perp_lp_shares_in_expiring_market<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, RemoveLiquidityInExpiredMarket<'info>>, - shares_to_burn: u64, - market_index: u16, - ) -> Result<()> { - handle_remove_perp_lp_shares_in_expiring_market(ctx, shares_to_burn, market_index) - } - pub fn update_user_name( ctx: Context, _sub_account_id: u16, @@ -539,13 +515,6 @@ pub mod drift { handle_settle_funding_payment(ctx) } - pub fn settle_lp<'c: 'info, 'info>( - ctx: Context<'_, '_, 'c, 'info, SettleLP>, - market_index: u16, - ) -> Result<()> { - handle_settle_lp(ctx, market_index) - } - pub fn settle_expired_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AdminUpdatePerpMarket<'info>>, market_index: u16, @@ -1372,13 +1341,6 @@ pub mod drift { ) } - pub fn update_perp_market_per_lp_base( - ctx: Context, - per_lp_base: i8, - ) -> Result<()> { - handle_update_perp_market_per_lp_base(ctx, per_lp_base) - } - pub fn update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index 400c6504e8..287b103060 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -22,7 +22,6 @@ pub fn is_user_bankrupt(user: &User) -> bool { if perp_position.base_asset_amount != 0 || perp_position.quote_asset_amount > 0 || perp_position.has_open_order() - || perp_position.is_lp() { return false; } diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index 99039b67d0..2ae3d2506b 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -1,7 +1,4 @@ use crate::controller::amm::update_spreads; -use crate::controller::lp::burn_lp_shares; -use crate::controller::lp::mint_lp_shares; -use crate::controller::lp::settle_lp_position; use crate::controller::position::PositionDirection; use crate::math::amm::calculate_bid_ask_bounds; use crate::math::constants::BASE_PRECISION; @@ -338,7 +335,7 @@ fn amm_spread_adj_logic() { ..PerpPosition::default() }; - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); + // todo fix this market.amm.base_asset_amount_per_lp = 1; market.amm.quote_asset_amount_per_lp = -QUOTE_PRECISION_I64 as i128; @@ -368,174 +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); -} - -#[test] -fn calculate_k_with_lps_tests() { - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - terminal_quote_asset_reserve: 999900009999000 * AMM_RESERVE_PRECISION, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 50_000_000_000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 10) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 10) as i128, - order_step_size: 5, - max_spread: 1000, - ..AMM::default_test() - }, - margin_ratio_initial: 1000, - ..PerpMarket::default() - }; - // let (t_price, _t_qar, _t_bar) = calculate_terminal_price_and_reserves(&market.amm).unwrap(); - // market.amm.terminal_quote_asset_reserve = _t_qar; - - let mut position = PerpPosition { - ..PerpPosition::default() - }; - - mint_lp_shares(&mut position, &mut market, BASE_PRECISION_U64).unwrap(); - - market.amm.base_asset_amount_per_lp = 1; - market.amm.quote_asset_amount_per_lp = -QUOTE_PRECISION_I64 as i128; - - let reserve_price = market.amm.reserve_price().unwrap(); - update_spreads(&mut market, reserve_price).unwrap(); - - settle_lp_position(&mut position, &mut market).unwrap(); - - assert_eq!(position.base_asset_amount, 0); - assert_eq!(position.quote_asset_amount, -QUOTE_PRECISION_I64); - assert_eq!(position.last_base_asset_amount_per_lp, 1); - assert_eq!( - position.last_quote_asset_amount_per_lp, - -QUOTE_PRECISION_I64 - ); - - // increase k by 1% - let update_k_up = - get_update_k_result(&market, bn::U192::from(102 * AMM_RESERVE_PRECISION), false).unwrap(); - let (t_price, _t_qar, _t_bar) = - amm::calculate_terminal_price_and_reserves(&market.amm).unwrap(); - - // new terminal reserves are balanced, terminal price = peg) - // assert_eq!(t_qar, 999900009999000); - // assert_eq!(t_bar, 1000100000000000); - assert_eq!(t_price, 49901136949); // - // assert_eq!(update_k_up.sqrt_k, 101 * AMM_RESERVE_PRECISION); - - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - ); - assert_eq!(cost, 49400); //0.05 - - // lp whale adds - let lp_whale_amount = 1000 * BASE_PRECISION_U64; - mint_lp_shares(&mut position, &mut market, lp_whale_amount).unwrap(); - - // ensure same cost - let update_k_up = - get_update_k_result(&market, bn::U192::from(1102 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - ); - assert_eq!(cost, 49450); //0.05 - - let update_k_down = - get_update_k_result(&market, bn::U192::from(1001 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_down).unwrap(); - assert_eq!(cost, -4995004950); //amm rug - - // lp whale removes - burn_lp_shares(&mut position, &mut market, lp_whale_amount, 0).unwrap(); - - // ensure same cost - let update_k_up = - get_update_k_result(&market, bn::U192::from(102 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - 1 - ); - assert_eq!(cost, 49450); //0.05 - - let update_k_down = - get_update_k_result(&market, bn::U192::from(79 * AMM_RESERVE_PRECISION), false).unwrap(); - let cost = adjust_k_cost(&mut market, &update_k_down).unwrap(); - assert_eq!(cost, -1407000); //0.05 - - // lp owns 50% of vAMM, same k - position.lp_shares = 50 * BASE_PRECISION_U64; - market.amm.user_lp_shares = 50 * AMM_RESERVE_PRECISION; - // cost to increase k is always positive when imbalanced - let cost = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert_eq!( - market.amm.base_asset_amount_with_amm, - (AMM_RESERVE_PRECISION / 10) as i128 - 1 - ); - assert_eq!(cost, 187800); //0.19 - - // lp owns 99% of vAMM, same k - position.lp_shares = 99 * BASE_PRECISION_U64; - market.amm.user_lp_shares = 99 * AMM_RESERVE_PRECISION; - let cost2 = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert!(cost2 > cost); - assert_eq!(cost2, 76804900); //216.45 - - // lp owns 100% of vAMM, same k - position.lp_shares = 100 * BASE_PRECISION_U64; - market.amm.user_lp_shares = 100 * AMM_RESERVE_PRECISION; - let cost3 = adjust_k_cost(&mut market, &update_k_up).unwrap(); - assert!(cost3 > cost); - assert!(cost3 > cost2); - assert_eq!(cost3, 216450200); - - // // todo: support this - // market.amm.base_asset_amount_with_amm = -(AMM_RESERVE_PRECISION as i128); - // let cost2 = adjust_k_cost(&mut market, &update_k_up).unwrap(); - // assert!(cost2 > cost); - // assert_eq!(cost2, 249999999999850000000001); -} - -#[test] -fn calculate_bid_ask_per_lp_token() { - let (bound1_s, bound2_s) = - calculate_bid_ask_bounds(MAX_CONCENTRATION_COEFFICIENT, 24704615072091).unwrap(); - - assert_eq!(bound1_s, 17468968372288); - assert_eq!(bound2_s, 34937266634951); - - let (bound1, bound2) = calculate_bid_ask_bounds( - MAX_CONCENTRATION_COEFFICIENT, - 24704615072091 + BASE_PRECISION, - ) - .unwrap(); - - assert_eq!(bound1 - bound1_s, 707113563); - assert_eq!(bound2 - bound2_s, 1414200000); - - let more_conc = - CONCENTRATION_PRECISION + (MAX_CONCENTRATION_COEFFICIENT - CONCENTRATION_PRECISION) / 20; - - let (bound1_s, bound2_s) = calculate_bid_ask_bounds(more_conc, 24704615072091).unwrap(); - - assert_eq!(bound1_s, 24203363415750); - assert_eq!(bound2_s, 25216247650234); - - let (bound1, bound2) = - calculate_bid_ask_bounds(more_conc, 24704615072091 + BASE_PRECISION).unwrap(); - - assert_eq!(bound1 - bound1_s, 979710202); - assert_eq!(bound2 - bound2_s, 1020710000); - - let (bound1_3, bound2_3) = - calculate_bid_ask_bounds(more_conc, 24704615072091 + 2 * BASE_PRECISION).unwrap(); - - assert_eq!(bound1_3 - bound1_s, 979710202 * 2); - assert_eq!(bound2_3 - bound2_s, 1020710000 * 2); -} +} \ No newline at end of file diff --git a/programs/drift/src/math/lp.rs b/programs/drift/src/math/lp.rs deleted file mode 100644 index a65ae33adb..0000000000 --- a/programs/drift/src/math/lp.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::error::{DriftResult, ErrorCode}; -use crate::msg; -use crate::{ - validate, MARGIN_PRECISION_U128, PRICE_PRECISION, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, -}; -use std::u64; - -use crate::math::amm::calculate_market_open_bids_asks; -use crate::math::casting::Cast; -use crate::math::helpers; -use crate::math::margin::MarginRequirementType; -use crate::math::orders::{ - standardize_base_asset_amount, standardize_base_asset_amount_ceil, - standardize_base_asset_amount_with_remainder_i128, -}; -use crate::math::safe_math::SafeMath; - -use crate::state::perp_market::PerpMarket; -use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; - -#[cfg(test)] -mod tests; - -#[derive(Debug)] -pub struct LPMetrics { - pub base_asset_amount: i128, - pub quote_asset_amount: i128, - pub remainder_base_asset_amount: i128, -} - -pub fn calculate_settle_lp_metrics(amm: &AMM, position: &PerpPosition) -> DriftResult { - let (base_asset_amount, quote_asset_amount) = calculate_settled_lp_base_quote(amm, position)?; - - // stepsize it - let (standardized_base_asset_amount, remainder_base_asset_amount) = - standardize_base_asset_amount_with_remainder_i128( - base_asset_amount, - amm.order_step_size.cast()?, - )?; - - let lp_metrics = LPMetrics { - base_asset_amount: standardized_base_asset_amount, - quote_asset_amount, - remainder_base_asset_amount: remainder_base_asset_amount.cast()?, - }; - - Ok(lp_metrics) -} - -pub fn calculate_settled_lp_base_quote( - amm: &AMM, - position: &PerpPosition, -) -> DriftResult<(i128, i128)> { - let n_shares = position.lp_shares; - let base_unit: i128 = amm.get_per_lp_base_unit()?; - - validate!( - amm.per_lp_base == position.per_lp_base, - ErrorCode::InvalidPerpPositionDetected, - "calculate_settled_lp_base_quote :: position/market per_lp_base unequal {} != {}", - position.per_lp_base, - amm.per_lp_base - )?; - - let n_shares_i128 = n_shares.cast::()?; - - // give them slice of the damm market position - let amm_net_base_asset_amount_per_lp = amm - .base_asset_amount_per_lp - .safe_sub(position.last_base_asset_amount_per_lp.cast()?)?; - - let base_asset_amount = amm_net_base_asset_amount_per_lp - .cast::()? - .safe_mul(n_shares_i128)? - .safe_div(base_unit)?; - - let amm_net_quote_asset_amount_per_lp = amm - .quote_asset_amount_per_lp - .safe_sub(position.last_quote_asset_amount_per_lp.cast()?)?; - - let quote_asset_amount = amm_net_quote_asset_amount_per_lp - .cast::()? - .safe_mul(n_shares_i128)? - .safe_div(base_unit)?; - - Ok((base_asset_amount, quote_asset_amount)) -} - -pub fn calculate_lp_open_bids_asks( - market_position: &PerpPosition, - market: &PerpMarket, -) -> DriftResult<(i64, i64)> { - let total_lp_shares = market.amm.sqrt_k; - let lp_shares = market_position.lp_shares; - - let (max_bids, max_asks) = calculate_market_open_bids_asks(&market.amm)?; - let open_asks = helpers::get_proportion_i128(max_asks, lp_shares.cast()?, total_lp_shares)?; - let open_bids = helpers::get_proportion_i128(max_bids, lp_shares.cast()?, total_lp_shares)?; - - Ok((open_bids.cast()?, open_asks.cast()?)) -} - -pub fn calculate_lp_shares_to_burn_for_risk_reduction( - perp_position: &PerpPosition, - market: &PerpMarket, - oracle_price: i64, - quote_oracle_price: i64, - margin_shortage: u128, - user_custom_margin_ratio: u32, - user_high_leverage_mode: bool, -) -> DriftResult<(u64, u64)> { - let settled_lp_position = perp_position.simulate_settled_lp_position(market, oracle_price)?; - - let worse_case_base_asset_amount = - settled_lp_position.worst_case_base_asset_amount(oracle_price, market.contract_type)?; - - let open_orders_from_lp_shares = if worse_case_base_asset_amount >= 0 { - worse_case_base_asset_amount.safe_sub( - settled_lp_position - .base_asset_amount - .safe_add(perp_position.open_bids)? - .cast()?, - )? - } else { - worse_case_base_asset_amount.safe_sub( - settled_lp_position - .base_asset_amount - .safe_add(perp_position.open_asks)? - .cast()?, - )? - }; - - let margin_ratio = market - .get_margin_ratio( - worse_case_base_asset_amount.unsigned_abs(), - MarginRequirementType::Initial, - user_high_leverage_mode, - )? - .max(user_custom_margin_ratio); - - let base_asset_amount_to_cover = margin_shortage - .safe_mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)? - .safe_div( - oracle_price - .cast::()? - .safe_mul(quote_oracle_price.cast()?)? - .safe_div(PRICE_PRECISION)? - .safe_mul(margin_ratio.cast()?)? - .safe_div(MARGIN_PRECISION_U128)?, - )? - .cast::()?; - - let current_base_asset_amount = settled_lp_position.base_asset_amount.unsigned_abs(); - - // if closing position is enough to cover margin shortage, then only a small % of lp shares need to be burned - if base_asset_amount_to_cover < current_base_asset_amount { - let base_asset_amount_to_close = standardize_base_asset_amount_ceil( - base_asset_amount_to_cover, - market.amm.order_step_size, - )? - .min(current_base_asset_amount); - let lp_shares_to_burn = standardize_base_asset_amount( - settled_lp_position.lp_shares / 10, - market.amm.order_step_size, - )? - .max(market.amm.order_step_size); - return Ok((lp_shares_to_burn, base_asset_amount_to_close)); - } - - let base_asset_amount_to_cover = - base_asset_amount_to_cover.safe_sub(current_base_asset_amount)?; - - let percent_to_burn = base_asset_amount_to_cover - .cast::()? - .safe_mul(100)? - .safe_div_ceil(open_orders_from_lp_shares.unsigned_abs())?; - - let lp_shares_to_burn = settled_lp_position - .lp_shares - .cast::()? - .safe_mul(percent_to_burn.cast()?)? - .safe_div_ceil(100)? - .cast::()?; - - let standardized_lp_shares_to_burn = - standardize_base_asset_amount_ceil(lp_shares_to_burn, market.amm.order_step_size)? - .clamp(market.amm.order_step_size, settled_lp_position.lp_shares); - - Ok((standardized_lp_shares_to_burn, current_base_asset_amount)) -} diff --git a/programs/drift/src/math/lp/tests.rs b/programs/drift/src/math/lp/tests.rs deleted file mode 100644 index c55253e0a5..0000000000 --- a/programs/drift/src/math/lp/tests.rs +++ /dev/null @@ -1,451 +0,0 @@ -use crate::math::constants::AMM_RESERVE_PRECISION; -use crate::math::lp::*; -use crate::state::user::PerpPosition; - -mod calculate_get_proportion_u128 { - use crate::math::helpers::get_proportion_u128; - - use super::*; - - pub fn get_proportion_u128_safe( - value: u128, - numerator: u128, - denominator: u128, - ) -> DriftResult { - if numerator == 0 { - return Ok(0); - } - - let proportional_value = if numerator <= denominator { - let ratio = denominator.safe_mul(10000)?.safe_div(numerator)?; - value.safe_mul(10000)?.safe_div(ratio)? - } else { - value.safe_mul(numerator)?.safe_div(denominator)? - }; - - Ok(proportional_value) - } - - #[test] - fn test_safe() { - let sqrt_k = AMM_RESERVE_PRECISION * 10_123; - let max_reserve = sqrt_k * 14121 / 10000; - let max_asks = max_reserve - sqrt_k; - - // let ans1 = get_proportion_u128_safe(max_asks, sqrt_k - sqrt_k / 100, sqrt_k).unwrap(); - // let ans2 = get_proportion_u128(max_asks, sqrt_k - sqrt_k / 100, sqrt_k).unwrap(); - // assert_eq!(ans1, ans2); //fails - - let ans1 = get_proportion_u128_safe(max_asks, sqrt_k / 2, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, sqrt_k / 2, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - let ans1 = get_proportion_u128_safe(max_asks, AMM_RESERVE_PRECISION, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, AMM_RESERVE_PRECISION, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - let ans1 = get_proportion_u128_safe(max_asks, 0, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, 0, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - let ans1 = get_proportion_u128_safe(max_asks, 1325324, sqrt_k).unwrap(); - let ans2 = get_proportion_u128(max_asks, 1325324, sqrt_k).unwrap(); - assert_eq!(ans1, ans2); - - // let ans1 = get_proportion_u128(max_asks, sqrt_k, sqrt_k).unwrap(); - // assert_eq!(ans1, max_asks); - } -} - -mod calculate_lp_open_bids_asks { - use super::*; - - #[test] - fn test_simple_lp_bid_ask() { - let position = PerpPosition { - lp_shares: 100, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_reserve: 10, - max_base_asset_reserve: 100, - min_base_asset_reserve: 0, - sqrt_k: 200, - ..AMM::default_test() - }; - let market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - let (open_bids, open_asks) = calculate_lp_open_bids_asks(&position, &market).unwrap(); - - assert_eq!(open_bids, 10 * 100 / 200); - assert_eq!(open_asks, -90 * 100 / 200); - } - - #[test] - fn test_max_ask() { - let position = PerpPosition { - lp_shares: 100, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_reserve: 0, - max_base_asset_reserve: 100, - min_base_asset_reserve: 0, - sqrt_k: 200, - ..AMM::default_test() - }; - let market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - let (open_bids, open_asks) = calculate_lp_open_bids_asks(&position, &market).unwrap(); - - assert_eq!(open_bids, 0); // wont go anymore short - assert_eq!(open_asks, -100 * 100 / 200); - } - - #[test] - fn test_max_bid() { - let position = PerpPosition { - lp_shares: 100, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_reserve: 10, - max_base_asset_reserve: 10, - min_base_asset_reserve: 0, - sqrt_k: 200, - ..AMM::default_test() - }; - let market = PerpMarket { - amm, - ..PerpMarket::default_test() - }; - - let (open_bids, open_asks) = calculate_lp_open_bids_asks(&position, &market).unwrap(); - - assert_eq!(open_bids, 10 * 100 / 200); - assert_eq!(open_asks, 0); // no more long - } -} - -mod calculate_settled_lp_base_quote { - use crate::math::constants::BASE_PRECISION_U64; - - use super::*; - - #[test] - fn test_long_settle() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - ..AMM::default_test() - }; - - let (baa, qaa) = calculate_settled_lp_base_quote(&amm, &position).unwrap(); - - assert_eq!(baa, 10 * 100); - assert_eq!(qaa, -10 * 100); - } - - #[test] - fn test_short_settle() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: -10, - quote_asset_amount_per_lp: 10, - ..AMM::default_test() - }; - - let (baa, qaa) = calculate_settled_lp_base_quote(&amm, &position).unwrap(); - - assert_eq!(baa, -10 * 100); - assert_eq!(qaa, 10 * 100); - } -} - -mod calculate_settle_lp_metrics { - use crate::math::constants::BASE_PRECISION_U64; - - use super::*; - - #[test] - fn test_long_settle() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - order_step_size: 1, - ..AMM::default_test() - }; - - let lp_metrics = calculate_settle_lp_metrics(&amm, &position).unwrap(); - - assert_eq!(lp_metrics.base_asset_amount, 10 * 100); - assert_eq!(lp_metrics.quote_asset_amount, -10 * 100); - assert_eq!(lp_metrics.remainder_base_asset_amount, 0); - } - - #[test] - fn test_all_remainder() { - let position = PerpPosition { - lp_shares: 100 * BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - order_step_size: 50 * 100, - ..AMM::default_test() - }; - - let lp_metrics = calculate_settle_lp_metrics(&amm, &position).unwrap(); - - assert_eq!(lp_metrics.base_asset_amount, 0); - assert_eq!(lp_metrics.quote_asset_amount, -10 * 100); - assert_eq!(lp_metrics.remainder_base_asset_amount, 10 * 100); - } - - #[test] - fn test_portion_remainder() { - let position = PerpPosition { - lp_shares: BASE_PRECISION_U64, - ..PerpPosition::default() - }; - - let amm = AMM { - base_asset_amount_per_lp: 10, - quote_asset_amount_per_lp: -10, - order_step_size: 3, - ..AMM::default_test() - }; - - let lp_metrics = calculate_settle_lp_metrics(&amm, &position).unwrap(); - - assert_eq!(lp_metrics.base_asset_amount, 9); - assert_eq!(lp_metrics.quote_asset_amount, -10); - assert_eq!(lp_metrics.remainder_base_asset_amount, 1); - } -} - -mod calculate_lp_shares_to_burn_for_risk_reduction { - use crate::math::lp::calculate_lp_shares_to_burn_for_risk_reduction; - use crate::state::perp_market::PerpMarket; - use crate::state::user::User; - use crate::test_utils::create_account_info; - use crate::{PRICE_PRECISION_I64, QUOTE_PRECISION}; - use anchor_lang::prelude::AccountLoader; - use solana_program::pubkey::Pubkey; - use std::str::FromStr; - - #[test] - fn test() { - let user_str = String::from("n3Vf4++XOuwuqzjlmLoHfrMxu0bx1zK4CI3jhlcn84aSUBauaSLU4gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARHJpZnQgTGlxdWlkaXR5IFByb3ZpZGVyICAgICAgICAbACHcCQAAAAAAAAAAAAAAAAAAAAAAAACcpMgCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2ssqwBAAAAAATnHP3///+9awAIAAAAAKnr8AcAAAAAqufxBwAAAAAAAAAAAAAAAAAAAAAAAAAAuITI//////8AeTlTJwAAANxGF1tu/P//abUakBEAAACBFNL6BAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+hUCAAAAAAAAAAAAAAAAACC8EHuk9f//1uYrCQMAAAAAAAAACQAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANv7p2UAAAAAnKTIAgAAAAAAAAAAAAAAAAAAAAAAAAAAsprK//////8AAAAAAAAAAPeGAgAAAAAAAAAAAAAAAAAzkaIOAAAAAA8AAACIEwAAAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - let mut decoded_bytes = base64::decode(user_str).unwrap(); - let user_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let user_account_info = create_account_info(&key, true, &mut lamports, user_bytes, &owner); - - let user_loader: AccountLoader = AccountLoader::try_from(&user_account_info).unwrap(); - let mut user = user_loader.load_mut().unwrap(); - let position = &mut user.perp_positions[0]; - - let perp_market_str = String::from("Ct8MLGv1N/cU6tVVkVpIHdjrXil5+Blo7M7no01SEzFkvCN2nSnel3KwISF8o/5okioZqvmQEJy52E6a0AS00gJa1vUpMUQZgG2jAAAAAAAAAAAAAAAAAAMAAAAAAAAAiKOiAAAAAAATRqMAAAAAAEr2u2UAAAAA3EYXW278/////////////2m1GpARAAAAAAAAAAAAAACRgrV0qi0BAAAAAAAAAAAAAAAAAAAAAABFREBhQ1YEAAAAAAAAAAAA9sh+SuuHBwAAAAAAAAAAACaTDwAAAAAAAAAAAAAAAADvHx32D0IEAAAAAAAAAAAA67nFJa5vBAAAAAAAAAAAAHMxOUELtwUAAAAAAAAAAACqHV4AAAAAAAAAAAAAAAAApw4iE86DBwAAAAAAAAAAAADzSoISXwAAAAAAAAAAAAAAHtBmbKP/////////////CreY1F8CAAAAAAAAAAAAAPZZghQfAAAAAAAAAAAAAAAAQGNSv8YBAAAAAAAAAAAAUdkndDAAAAAAAAAAAAAAAEEeAcSS/v/////////////0bAXnbQEAAAAAAAAAAAAAPuj0I3f+/////////////6felr+KAQAAAAAAAAAAAABX2/mMhMQCAAAAAAAAAAAALukbAAAAAAAu6RsAAAAAAC7pGwAAAAAAqPUJAAAAAADkPmeWogAAAAAAAAAAAAAAsD8vhpIAAAAAAAAAAAAAACibCEwQAAAAAAAAAAAAAAAr/d/xbQAAAAAAAAAAAAAAwY+XFgAAAAAAAAAAAAAAAMyF/KFFAAAAAAAAAAAAAAA9rLKsAQAAAAAAAAAAAAAAPayyrAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+6JzGf04EAAAAAAAAAAAAtqLk+X6VBwAAAAAAAAAAAPDUDdGDVwQAAAAAAAAAAABeb5d+v4UHAAAAAAAAAAAAgG2jAAAAAAAAAAAAAAAAACJ6ogAAAAAAE0qkAAAAAAAaYqMAAAAAAIF1pAAAAAAArJmiDgAAAAAlBwAAAAAAAN5ukP7/////veq7ZQAAAAAQDgAAAAAAAADh9QUAAAAAZAAAAAAAAAAAZc0dAAAAAAAAAAAAAAAAiuqcc0QAAAA8R6NuAQAAAIyqSgkAAAAAt+27ZQAAAAATCAEAAAAAAPjJAAAAAAAASva7ZQAAAACUEQAAoIYBALQ2AADKCAAASQEAAH0AAAD0ATIAZMgEAQAAAAAEAAAAfRuiDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhv4EJ8hQEAAAAAAAAAAAAAAAAAAAAAADFNQk9OSy1QRVJQICAgICAgICAgICAgICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8VAwAAAAAA+x4AAAAAAACFAwAAAAAAACYCAADuAgAAqGEAAFDDAADECQAA3AUAAAAAAAAQJwAABwQAAA0GAAAEAAEAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market = perp_market_loader.load_mut().unwrap(); - - let oracle_price = 10 * PRICE_PRECISION_I64; - let quote_oracle_price = PRICE_PRECISION_I64; - - let margin_shortage = 40 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 168900000000); - assert_eq!(base_asset_amount, 12400000000); - - let margin_shortage = 20 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 8000000000); - - let margin_shortage = 5 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 2000000000); - - // flip existing position the other direction - position.base_asset_amount = -position.base_asset_amount; - - let margin_shortage = 40 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 168900000000); - assert_eq!(base_asset_amount, 12400000000); - - let margin_shortage = 20 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 8000000000); - - let margin_shortage = 5 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - 0, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 16800000000); - assert_eq!(base_asset_amount, 2000000000); - } - - #[test] - fn custom_margin_ratio() { - let user_str = String::from("n3Vf4++XOuwIrD1jL22rz6RZlEfmZHqxneDBS0Mflxjd93h2f2ldQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdGl0c29jY2VyICAgICAgICAgICAgICAgICAgICAgICDnqurCZBgAAAAAAAAAAAAAAAAAAAAAAADOM8akAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgO0UfBAAAAPeaGv//////SM9HIAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyRaK3v////8AAAAAAAAAAPeaGv//////SM9HIAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGq0wmUAAAAATspepQEAAACAlpgAAAAAAAAAAAAAAAAAGn3imQQAAAAAAAAAAAAAACMV2Pf/////AAAAAAAAAAB7Ro0QAAAAACYAAAAQJwAAAQADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - let mut decoded_bytes = base64::decode(user_str).unwrap(); - let user_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let user_account_info = create_account_info(&key, true, &mut lamports, user_bytes, &owner); - - let user_loader: AccountLoader = AccountLoader::try_from(&user_account_info).unwrap(); - let mut user = user_loader.load_mut().unwrap(); - let user_custom_margin_ratio = user.max_margin_ratio; - let position = &mut user.perp_positions[0]; - - let perp_market_str = String::from("Ct8MLGv1N/cV6vWLwJY+18dY2GsrmrNldgnISB7pmbcf7cn9S4FZ4PnAFyuhDfpNGQiNlPW/YdO1TVvXSDoyKpguE3PujqMbEGDwDQoAAAAAAAAAAAAAAAIAAAAAAAAAC7rzDQoAAABst/MNCgAAACK5wmUAAAAAceZN/////////////////3v2pRcAAAAAAAAAAAAAAADlXWMfRwMAAAAAAAAAAAAAAAAAAAAAAABkI6UNRQAAAAAAAAAAAAAAqfLzd0UAAAAAAAAAAAAAACaTDwAAAAAAAAAAAAAAAAAPPu6ERAAAAAAAAAAAAAAA9NX/YkcAAAAAAAAAAAAAAI0luEJFAAAAAAAAAAAAAAD9eI3+CQAAAAAAAAAAAAAAIZTqlkQAAAAAAAAAAAAAAIBENlMCAAAAAAAAAAAAAADgfcyL/v//////////////meiO4gAAAAAAAAAAAAAAAMfZc/z///////////////8AoHJOGAkAAAAAAAAAAAAAnURpyQAAAAAAAAAAAAAAAOYK1g3F//////////////9I7emQMwAAAAAAAAAAAAAAKoCi9MT//////////////3cTS98zAAAAAAAAAAAAAAAgO0UfBAAAAAAAAAAAAAAAGV4SEgAAAAAZXhISAAAAABleEhIAAAAAA0itQgAAAAAhNkO9CQAAAAAAAAAAAAAAMTlM9gcAAAAAAAAAAAAAAGJujdoBAAAAAAAAAAAAAADvEtDaAgAAAAAAAAAAAAAAOLU20gIAAAAAAAAAAAAAALu3wLsCAAAAAAAAAAAAAABdkcJWBgUAAAAAAAAAAAAANhdFnLwEAAAAAAAAAAAAAEDj5IkCAAAAAAAAAAAAAACCV2gFRQAAAAAAAAAAAAAAPWo+gEUAAAAAAAAAAAAAAIIDPw5FAAAAAAAAAAAAAAAAJ1l3RQAAAAAAAAAAAAAAEGDwDQoAAAAAAAAAAAAAAE05yg0KAAAAUbrzDQoAAADP+d4NCgAAAC3a3g0KAAAAGVONEAAAAAACAQAAAAAAAGDUKbz/////G7nCZQAAAAAQDgAAAAAAAKCGAQAAAAAAoIYBAAAAAABAQg8AAAAAAAAAAAAAAAAA3+0VvzsAAAAAAAAAAAAAACbWIwUKAAAAIbnCZQAAAABNyBIAAAAAAPtZAwAAAAAAIbnCZQAAAAAKAAAA6AMAAKQDAABEAAAAAAAAAP0pAABkADIAZMgAAQDKmjsAAAAA1jQGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ezvzkkgAAAAAAAAAAAAAAAAAAAAAAAEJUQy1QRVJQICAgICAgICAgICAgICAgICAgICAgICAg6AMAAAAAAADoAwAAAAAAAOgDAAAAAAAA6AMAAAAAAAAiucJlAAAAABAnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJsXAwAAAAAAIhAAAAAAAACHAQAAAAAAABAnAAAQJwAAECcAABAnAAD0AQAAkAEAAAAAAAABAAAAFwAAABsAAAABAAEAAgAAALX/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - let mut decoded_bytes = base64::decode(perp_market_str).unwrap(); - let perp_market_bytes = decoded_bytes.as_mut_slice(); - - let key = Pubkey::default(); - let owner = Pubkey::from_str("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH").unwrap(); - let mut lamports = 0; - let perp_market_account_info = - create_account_info(&key, true, &mut lamports, perp_market_bytes, &owner); - - let perp_market_loader: AccountLoader = - AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market = perp_market_loader.load_mut().unwrap(); - - let oracle_price = 43174 * PRICE_PRECISION_I64; - let quote_oracle_price = PRICE_PRECISION_I64; - - let margin_shortage = 2077 * QUOTE_PRECISION; - - let (lp_shares_to_burn, base_asset_amount) = - calculate_lp_shares_to_burn_for_risk_reduction( - position, - &perp_market, - oracle_price, - quote_oracle_price, - margin_shortage, - user_custom_margin_ratio, - false, - ) - .unwrap(); - - assert_eq!(lp_shares_to_burn, 1770400000); - assert_eq!(base_asset_amount, 48200000); - assert_eq!(position.lp_shares, 17704500000); - } -} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 73415cb58b..cc6af96add 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -119,8 +119,6 @@ pub fn calculate_perp_position_value_and_pnl( market_position, )?; - let market_position = market_position.simulate_settled_lp_position(market, valuation_price)?; - let (base_asset_value, unrealized_pnl) = calculate_base_asset_value_and_pnl_with_oracle_price(&market_position, valuation_price)?; @@ -150,11 +148,7 @@ 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()?)? - .safe_add( - market_position - .margin_requirement_for_lp_shares(market.amm.order_step_size, valuation_price)?, - )?; + .safe_add(market_position.margin_requirement_for_open_orders()?)?; let unrealized_asset_weight = market.get_unrealized_asset_weight(total_unrealized_pnl, margin_requirement_type)?; @@ -584,8 +578,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( let has_perp_liability = market_position.base_asset_amount != 0 || market_position.quote_asset_amount < 0 - || market_position.has_open_order() - || market_position.is_lp(); + || market_position.has_open_order(); if has_perp_liability { calculation.add_perp_liability()?; @@ -978,9 +971,6 @@ pub fn calculate_user_equity( market_position, )?; - let market_position = - market_position.simulate_settled_lp_position(market, valuation_price)?; - let (_, unrealized_pnl) = calculate_base_asset_value_and_pnl_with_oracle_price( &market_position, valuation_price, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 33763abd05..7a256b65db 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -394,154 +394,6 @@ mod test { let ans = (0).nth_root(2); assert_eq!(ans, 0); } - - #[test] - fn test_lp_user_short() { - let mut market = PerpMarket { - market_index: 0, - amm: AMM { - base_asset_reserve: 5 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 5 * AMM_RESERVE_PRECISION, - sqrt_k: 5 * AMM_RESERVE_PRECISION, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, - max_base_asset_reserve: 10 * AMM_RESERVE_PRECISION, - ..AMM::default_test() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - imf_factor: 1000, // 1_000/1_000_000 = .001 - unrealized_pnl_initial_asset_weight: 10000, - unrealized_pnl_maintenance_asset_weight: 10000, - ..PerpMarket::default() - }; - - let position = PerpPosition { - lp_shares: market.amm.user_lp_shares as u64, - ..PerpPosition::default() - }; - - let oracle_price_data = OraclePriceData { - price: (2 * PRICE_PRECISION) as i64, - confidence: 0, - delay: 2, - has_sufficient_number_of_data_points: true, - }; - - let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // make the market unbalanced - - let trade_size = 3 * AMM_RESERVE_PRECISION; - let (new_qar, new_bar) = calculate_swap_output( - trade_size, - market.amm.base_asset_reserve, - SwapDirection::Add, // user shorts - market.amm.sqrt_k, - ) - .unwrap(); - market.amm.quote_asset_reserve = new_qar; - market.amm.base_asset_reserve = new_bar; - - let (pmr2, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // larger margin req in more unbalanced market - assert!(pmr2 > pmr) - } - - #[test] - fn test_lp_user_long() { - let mut market = PerpMarket { - market_index: 0, - amm: AMM { - base_asset_reserve: 5 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 5 * AMM_RESERVE_PRECISION, - sqrt_k: 5 * AMM_RESERVE_PRECISION, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, - max_base_asset_reserve: 10 * AMM_RESERVE_PRECISION, - ..AMM::default_test() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - imf_factor: 1000, // 1_000/1_000_000 = .001 - unrealized_pnl_initial_asset_weight: 10000, - unrealized_pnl_maintenance_asset_weight: 10000, - ..PerpMarket::default() - }; - - let position = PerpPosition { - lp_shares: market.amm.user_lp_shares as u64, - ..PerpPosition::default() - }; - - let oracle_price_data = OraclePriceData { - price: (2 * PRICE_PRECISION) as i64, - confidence: 0, - delay: 2, - has_sufficient_number_of_data_points: true, - }; - - let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // make the market unbalanced - let trade_size = 3 * AMM_RESERVE_PRECISION; - let (new_qar, new_bar) = calculate_swap_output( - trade_size, - market.amm.base_asset_reserve, - SwapDirection::Remove, // user longs - market.amm.sqrt_k, - ) - .unwrap(); - market.amm.quote_asset_reserve = new_qar; - market.amm.base_asset_reserve = new_bar; - - let strict_oracle_price = StrictOraclePrice::test(QUOTE_PRECISION_I64); - let (pmr2, _, _, _, _) = calculate_perp_position_value_and_pnl( - &position, - &market, - &oracle_price_data, - &strict_oracle_price, - MarginRequirementType::Initial, - 0, - false, - false, - ) - .unwrap(); - - // larger margin req in more unbalanced market - assert!(pmr2 > pmr) - } } #[cfg(test)] diff --git a/programs/drift/src/math/mod.rs b/programs/drift/src/math/mod.rs index aa7ec7f196..89edbdafc5 100644 --- a/programs/drift/src/math/mod.rs +++ b/programs/drift/src/math/mod.rs @@ -16,7 +16,6 @@ pub mod funding; pub mod helpers; pub mod insurance; pub mod liquidation; -pub mod lp; pub mod margin; pub mod matching; pub mod oracle; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 749b2efada..ec146bd1f7 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -353,7 +353,6 @@ pub fn get_position_delta_for_fill( PositionDirection::Long => base_asset_amount.cast()?, PositionDirection::Short => -base_asset_amount.cast()?, }, - remainder_base_asset_amount: None, }) } diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index b16c7c1c34..b07f7c91c7 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -174,32 +174,19 @@ pub fn get_position_update_type( position: &PerpPosition, delta: &PositionDelta, ) -> DriftResult { - if position.base_asset_amount == 0 && position.remainder_base_asset_amount == 0 { + if position.base_asset_amount == 0 { return Ok(PositionUpdateType::Open); } - let position_base_with_remainder = if position.remainder_base_asset_amount != 0 { - position - .base_asset_amount - .safe_add(position.remainder_base_asset_amount.cast::()?)? - } else { - position.base_asset_amount - }; + let position_base = position.base_asset_amount; - let delta_base_with_remainder = - if let Some(remainder_base_asset_amount) = delta.remainder_base_asset_amount { - delta - .base_asset_amount - .safe_add(remainder_base_asset_amount.cast()?)? - } else { - delta.base_asset_amount - }; + let delta_base = delta.base_asset_amount; - if position_base_with_remainder.signum() == delta_base_with_remainder.signum() { + if position_base.signum() == delta_base.signum() { Ok(PositionUpdateType::Increase) - } else if position_base_with_remainder.abs() > delta_base_with_remainder.abs() { + } else if position_base.abs() > delta_base.abs() { Ok(PositionUpdateType::Reduce) - } else if position_base_with_remainder.abs() == delta_base_with_remainder.abs() { + } else if position_base.abs() == delta_base.abs() { Ok(PositionUpdateType::Close) } else { Ok(PositionUpdateType::Flip) @@ -209,8 +196,7 @@ pub fn get_position_update_type( pub fn get_new_position_amounts( position: &PerpPosition, delta: &PositionDelta, - market: &PerpMarket, -) -> DriftResult<(i64, i64, i64, i64)> { +) -> DriftResult<(i64, i64)> { let new_quote_asset_amount = position .quote_asset_amount .safe_add(delta.quote_asset_amount)?; @@ -219,48 +205,8 @@ pub fn get_new_position_amounts( .base_asset_amount .safe_add(delta.base_asset_amount)?; - let mut new_remainder_base_asset_amount = position - .remainder_base_asset_amount - .cast::()? - .safe_add( - delta - .remainder_base_asset_amount - .unwrap_or(0) - .cast::()?, - )?; - let mut new_settled_base_asset_amount = delta.base_asset_amount; - - if delta.remainder_base_asset_amount.is_some() { - if new_remainder_base_asset_amount.unsigned_abs() >= market.amm.order_step_size { - let (standardized_remainder_base_asset_amount, remainder_base_asset_amount) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - new_remainder_base_asset_amount.cast()?, - market.amm.order_step_size.cast()?, - )?; - - new_base_asset_amount = - new_base_asset_amount.safe_add(standardized_remainder_base_asset_amount.cast()?)?; - - new_settled_base_asset_amount = new_settled_base_asset_amount - .safe_add(standardized_remainder_base_asset_amount.cast()?)?; - - new_remainder_base_asset_amount = remainder_base_asset_amount.cast()?; - } else { - new_remainder_base_asset_amount = new_remainder_base_asset_amount.cast()?; - } - - validate!( - new_remainder_base_asset_amount.abs() <= i32::MAX as i64, - ErrorCode::InvalidPositionDelta, - "new_remainder_base_asset_amount={} > i32 max", - new_remainder_base_asset_amount - )?; - } - Ok(( new_base_asset_amount, - new_settled_base_asset_amount, new_quote_asset_amount, - new_remainder_base_asset_amount, )) } diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 8983162b7b..b4f18c1e2c 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -589,29 +589,6 @@ impl PerpMarket { Ok(depth) } - pub fn update_market_with_counterparty( - &mut self, - delta: &PositionDelta, - new_settled_base_asset_amount: i64, - ) -> DriftResult { - // indicates that position delta is settling lp counterparty - if delta.remainder_base_asset_amount.is_some() { - // todo: name for this is confusing, but adding is correct as is - // definition: net position of users in the market that has the LP as a counterparty (which have NOT settled) - self.amm.base_asset_amount_with_unsettled_lp = self - .amm - .base_asset_amount_with_unsettled_lp - .safe_add(new_settled_base_asset_amount.cast()?)?; - - self.amm.quote_asset_amount_with_unsettled_lp = self - .amm - .quote_asset_amount_with_unsettled_lp - .safe_add(delta.quote_asset_amount.cast()?)?; - } - - Ok(()) - } - pub fn is_price_divergence_ok_for_settle_pnl(&self, oracle_price: i64) -> DriftResult { let oracle_divergence = oracle_price .safe_sub(self.amm.historical_oracle_data.last_oracle_price_twap_5min)? diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 2f49d86889..b32292bdbc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1,4 +1,3 @@ -use crate::controller::lp::apply_lp_rebase_to_perp_position; use crate::controller::position::{add_new_position, get_position_index, PositionDirection}; use crate::error::{DriftResult, ErrorCode}; use crate::math::auction::{calculate_auction_price, is_auction_complete}; @@ -7,14 +6,13 @@ use crate::math::constants::{ EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, OPEN_ORDER_MARGIN_REQUIREMENT, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, THIRTY_DAY, }; -use crate::math::lp::{calculate_lp_open_bids_asks, calculate_settle_lp_metrics}; use crate::math::margin::MarginRequirementType; 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_base_asset_value_with_oracle_price, calculate_perp_liability_value, + calculate_perp_liability_value, }; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{ @@ -23,10 +21,10 @@ 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, PerpMarket}; +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, PERCENTAGE_PRECISION_I64, QUOTE_PRECISION_U64}; +use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; use crate::{math_error, SPOT_WEIGHT_PRECISION_I128}; use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; @@ -979,7 +977,6 @@ impl PerpPosition { !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() - && !self.is_lp() } pub fn is_open_position(&self) -> bool { @@ -990,103 +987,12 @@ impl PerpPosition { self.open_orders != 0 || self.open_bids != 0 || self.open_asks != 0 } - pub fn margin_requirement_for_lp_shares( - &self, - order_step_size: u64, - valuation_price: i64, - ) -> DriftResult { - if !self.is_lp() { - return Ok(0); - } - Ok(QUOTE_PRECISION.max( - order_step_size - .cast::()? - .safe_mul(valuation_price.cast()?)? - .safe_div(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO)?, - )) - } - pub fn margin_requirement_for_open_orders(&self) -> DriftResult { self.open_orders .cast::()? .safe_mul(OPEN_ORDER_MARGIN_REQUIREMENT) } - pub fn is_lp(&self) -> bool { - self.lp_shares > 0 - } - - pub fn simulate_settled_lp_position( - &self, - market: &PerpMarket, - valuation_price: i64, - ) -> DriftResult { - let mut settled_position = *self; - - if !settled_position.is_lp() { - return Ok(settled_position); - } - - apply_lp_rebase_to_perp_position(market, &mut settled_position)?; - - // compute lp metrics - let mut lp_metrics = calculate_settle_lp_metrics(&market.amm, &settled_position)?; - - // compute settled position - let base_asset_amount = settled_position - .base_asset_amount - .safe_add(lp_metrics.base_asset_amount.cast()?)?; - - let mut quote_asset_amount = settled_position - .quote_asset_amount - .safe_add(lp_metrics.quote_asset_amount.cast()?)?; - - let mut new_remainder_base_asset_amount = settled_position - .remainder_base_asset_amount - .cast::()? - .safe_add(lp_metrics.remainder_base_asset_amount.cast()?)?; - - if new_remainder_base_asset_amount.unsigned_abs() >= market.amm.order_step_size { - let (standardized_remainder_base_asset_amount, remainder_base_asset_amount) = - crate::math::orders::standardize_base_asset_amount_with_remainder_i128( - new_remainder_base_asset_amount.cast()?, - market.amm.order_step_size.cast()?, - )?; - - lp_metrics.base_asset_amount = lp_metrics - .base_asset_amount - .safe_add(standardized_remainder_base_asset_amount)?; - - new_remainder_base_asset_amount = remainder_base_asset_amount.cast()?; - } else { - new_remainder_base_asset_amount = new_remainder_base_asset_amount.cast()?; - } - - // dust position in baa/qaa - if new_remainder_base_asset_amount != 0 { - let dust_base_asset_value = calculate_base_asset_value_with_oracle_price( - new_remainder_base_asset_amount.cast()?, - valuation_price, - )? - .safe_add(1)?; - - quote_asset_amount = quote_asset_amount.safe_sub(dust_base_asset_value.cast()?)?; - } - - let (lp_bids, lp_asks) = calculate_lp_open_bids_asks(&settled_position, market)?; - - let open_bids = settled_position.open_bids.safe_add(lp_bids)?; - - let open_asks = settled_position.open_asks.safe_add(lp_asks)?; - - settled_position.base_asset_amount = base_asset_amount; - settled_position.quote_asset_amount = quote_asset_amount; - settled_position.open_bids = open_bids; - settled_position.open_asks = open_asks; - - Ok(settled_position) - } - pub fn has_unsettled_pnl(&self) -> bool { self.base_asset_amount == 0 && self.quote_asset_amount != 0 } @@ -1162,18 +1068,12 @@ impl PerpPosition { Ok(unrealized_pnl) } - pub fn get_base_asset_amount_with_remainder(&self) -> DriftResult { - if self.remainder_base_asset_amount != 0 { - self.base_asset_amount - .cast::()? - .safe_add(self.remainder_base_asset_amount.cast::()?) - } else { - self.base_asset_amount.cast::() - } + pub fn get_base_asset_amount(&self) -> DriftResult { + self.base_asset_amount.cast::() } - pub fn get_base_asset_amount_with_remainder_abs(&self) -> DriftResult { - Ok(self.get_base_asset_amount_with_remainder()?.abs()) + pub fn get_base_asset_amount_abs(&self) -> DriftResult { + Ok(self.get_base_asset_amount()?.abs()) } pub fn get_claimable_pnl(&self, oracle_price: i64, pnl_pool_excess: i128) -> DriftResult { @@ -1231,7 +1131,7 @@ use super::protected_maker_mode_config::ProtectedMakerParams; #[cfg(test)] impl PerpPosition { pub fn get_breakeven_price(&self) -> DriftResult { - let base_with_remainder = self.get_base_asset_amount_with_remainder()?; + let base_with_remainder = self.get_base_asset_amount()?; if base_with_remainder == 0 { return Ok(0); } @@ -1243,7 +1143,7 @@ impl PerpPosition { } pub fn get_entry_price(&self) -> DriftResult { - let base_with_remainder = self.get_base_asset_amount_with_remainder()?; + let base_with_remainder = self.get_base_asset_amount()?; if base_with_remainder == 0 { return Ok(0); } diff --git a/programs/drift/src/validation/position.rs b/programs/drift/src/validation/position.rs index e8f527de63..3d36698583 100644 --- a/programs/drift/src/validation/position.rs +++ b/programs/drift/src/validation/position.rs @@ -11,14 +11,6 @@ pub fn validate_perp_position_with_perp_market( position: &PerpPosition, market: &PerpMarket, ) -> DriftResult { - if position.lp_shares != 0 { - validate!( - position.per_lp_base == market.amm.per_lp_base, - ErrorCode::InvalidPerpPositionDetected, - "position/market per_lp_base unequal" - )?; - } - validate!( position.market_index == market.market_index, ErrorCode::InvalidPerpPositionDetected, From e99ffa7e9ecdccc6ada82e3d0fab8f5785d45c35 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 16:50:46 -0400 Subject: [PATCH 03/91] rm more fields --- programs/drift/src/controller/amm.rs | 3 +- programs/drift/src/controller/amm/tests.rs | 55 ------- programs/drift/src/controller/orders.rs | 4 +- .../src/controller/orders/amm_lp_jit_tests.rs | 136 ---------------- programs/drift/src/controller/repeg.rs | 2 +- programs/drift/src/instructions/admin.rs | 28 ---- programs/drift/src/lib.rs | 10 -- programs/drift/src/math/amm_jit.rs | 47 +----- programs/drift/src/math/funding.rs | 3 +- programs/drift/src/math/position.rs | 12 +- programs/drift/src/state/perp_market.rs | 146 +----------------- programs/drift/src/validation/perp_market.rs | 32 +--- 12 files changed, 19 insertions(+), 459 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 9aa33e087e..5e4871b8a3 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -360,8 +360,7 @@ pub fn formulaic_update_k( let new_sqrt_k = bn::U192::from(market.amm.sqrt_k) .safe_mul(bn::U192::from(k_scale_numerator))? - .safe_div(bn::U192::from(k_scale_denominator))? - .max(bn::U192::from(market.amm.user_lp_shares.safe_add(1)?)); + .safe_div(bn::U192::from(k_scale_denominator))?; let update_k_result = get_update_k_result(market, new_sqrt_k, true)?; diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index c62e2959d5..5031a75bbc 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -255,61 +255,6 @@ fn iterative_no_bounds_formualic_k_tests() { assert_eq!(market.amm.total_fee_minus_distributions, 985625029); } -#[test] -fn decrease_k_up_to_user_lp_shares() { - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 512295081967, - quote_asset_reserve: 488 * AMM_RESERVE_PRECISION, - sqrt_k: 500 * AMM_RESERVE_PRECISION, - user_lp_shares: 150 * AMM_RESERVE_PRECISION, - peg_multiplier: 50000000, - concentration_coef: MAX_CONCENTRATION_COEFFICIENT, - base_asset_amount_with_amm: -12295081967, - total_fee_minus_distributions: -100 * QUOTE_PRECISION as i128, - total_fee_withdrawn: 100 * QUOTE_PRECISION, - curve_update_intensity: 100, - ..AMM::default() - }, - ..PerpMarket::default() - }; - // let prev_sqrt_k = market.amm.sqrt_k; - let (new_terminal_quote_reserve, new_terminal_base_reserve) = - amm::calculate_terminal_reserves(&market.amm).unwrap(); - market.amm.terminal_quote_asset_reserve = new_terminal_quote_reserve; - let (min_base_asset_reserve, max_base_asset_reserve) = - amm::calculate_bid_ask_bounds(market.amm.concentration_coef, new_terminal_base_reserve) - .unwrap(); - market.amm.min_base_asset_reserve = min_base_asset_reserve; - market.amm.max_base_asset_reserve = max_base_asset_reserve; - - // let reserve_price = market.amm.reserve_price().unwrap(); - let now = 10000; - let oracle_price_data = OraclePriceData { - price: 50 * PRICE_PRECISION_I64, - confidence: 0, - delay: 2, - has_sufficient_number_of_data_points: true, - }; - - // negative funding cost - let mut count = 0; - let mut prev_k = market.amm.sqrt_k; - let mut new_k = 0; - while prev_k != new_k && count < 100000 { - let funding_cost = (QUOTE_PRECISION * 100000) as i128; - prev_k = market.amm.sqrt_k; - formulaic_update_k(&mut market, &oracle_price_data, funding_cost, now).unwrap(); - new_k = market.amm.sqrt_k; - msg!("quote_asset_reserve:{}", market.amm.quote_asset_reserve); - msg!("new_k:{}", new_k); - count += 1 - } - - assert_eq!(market.amm.base_asset_amount_with_amm, -12295081967); - assert_eq!(market.amm.sqrt_k, 162234889619); - assert_eq!(market.amm.total_fee_minus_distributions, 29796232175); -} #[test] fn update_pool_balances_test_high_util_borrow() { diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 8884c1695e..4502129845 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1092,6 +1092,7 @@ pub fn fill_perp_order( amm_lp_allowed_to_jit_make = market .amm .amm_lp_allowed_to_jit_make(amm_wants_to_jit_make)?; + // TODO what do do here? amm_can_skip_duration = market.can_skip_auction_duration(&state, amm_lp_allowed_to_jit_make)?; @@ -1862,7 +1863,6 @@ fn fulfill_perp_order( fee_structure, oracle_map, fill_mode.is_liquidation(), - None, )?; if maker_fill_base_asset_amount != 0 { @@ -2462,7 +2462,6 @@ pub fn fulfill_perp_order_with_match( fee_structure: &FeeStructure, oracle_map: &mut OracleMap, is_liquidation: bool, - amm_lp_allowed_to_jit_make: Option, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2549,7 +2548,6 @@ pub fn fulfill_perp_order_with_match( taker_base_asset_amount, maker_base_asset_amount, taker.orders[taker_order_index].has_limit_price(slot)?, - amm_lp_allowed_to_jit_make, )?; if jit_base_asset_amount > 0 { diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs index 2ef6a02bb7..d4f40df218 100644 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs @@ -326,146 +326,10 @@ pub mod amm_lp_jit { BASE_PRECISION_U64, BASE_PRECISION_U64, false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::ProtocolOwned); - assert_eq!(jit_base_asset_amount, 500000000); - } - - #[test] - fn amm_lp_jit_amm_lp_same_side_imbalanced() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, // lps are long vs target, wants shorts - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), // amm is too long vs target, wants shorts - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - // lp needs nearly 5 base to get to target - assert_eq!( - market.amm.imbalanced_base_asset_amount_with_lp().unwrap(), - 4_941_986_570 - ); - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - let amm_inventory_pct = calculate_inventory_liquidity_ratio( - market.amm.base_asset_amount_with_amm, - market.amm.base_asset_reserve, - market.amm.min_base_asset_reserve, - market.amm.max_base_asset_reserve, - ) - .unwrap(); - assert_eq!(amm_inventory_pct, PERCENTAGE_PRECISION_I128 / 200); // .5% of amm inventory is in position - - // maker order satisfies taker, vAMM doing match - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Long, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::Shared); - assert_eq!(jit_base_asset_amount, 500000000); - - // taker order is heading to vAMM - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Long, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64 * 2, - BASE_PRECISION_U64, - false, - None, ) .unwrap(); assert_eq!(amm_liquidity_split, AMMLiquiditySplit::ProtocolOwned); - assert_eq!(jit_base_asset_amount, 0); // its coming anyways - - // no jit for additional long (more shorts for amm) - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Long, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64 * 100, - BASE_PRECISION_U64 * 100, - false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::Shared); assert_eq!(jit_base_asset_amount, 500000000); - - // wrong direction (increases lp and vamm inventory) - let (jit_base_asset_amount, amm_liquidity_split) = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Short, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - None, - ) - .unwrap(); - assert_eq!(amm_liquidity_split, AMMLiquiditySplit::ProtocolOwned); - assert_eq!(jit_base_asset_amount, 0); } #[test] diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index a9f55fe107..ea3af12285 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -333,7 +333,7 @@ pub fn settle_expired_market( )?; validate!( - market.amm.base_asset_amount_with_unsettled_lp == 0 && market.amm.user_lp_shares == 0, + market.amm.base_asset_amount_with_unsettled_lp == 0, ErrorCode::MarketSettlementRequiresSettledLP, "Outstanding LP in market" )?; diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 2510948d5d..669a6d95b2 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -2321,14 +2321,6 @@ pub fn handle_update_k(ctx: Context, sqrt_k: u128) -> Result<()> { perp_market.amm.sqrt_k )?; - validate!( - perp_market.amm.sqrt_k > perp_market.amm.user_lp_shares, - ErrorCode::InvalidUpdateK, - "cannot decrease sqrt_k={} below user_lp_shares={}", - perp_market.amm.sqrt_k, - perp_market.amm.user_lp_shares - )?; - perp_market.amm.total_fee_minus_distributions = perp_market .amm .total_fee_minus_distributions @@ -3402,26 +3394,6 @@ pub fn handle_update_perp_market_curve_update_intensity( Ok(()) } -#[access_control( - perp_market_valid(&ctx.accounts.perp_market) -)] -pub fn handle_update_perp_market_target_base_asset_amount_per_lp( - ctx: Context, - target_base_asset_amount_per_lp: i32, -) -> Result<()> { - let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; - msg!("perp market {}", perp_market.market_index); - - msg!( - "perp_market.amm.target_base_asset_amount_per_lp: {} -> {}", - perp_market.amm.target_base_asset_amount_per_lp, - target_base_asset_amount_per_lp - ); - - perp_market.amm.target_base_asset_amount_per_lp = target_base_asset_amount_per_lp; - Ok(()) -} - pub fn handle_update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 92981feac5..cedcbfbfeb 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1331,16 +1331,6 @@ pub mod drift { handle_update_perp_market_curve_update_intensity(ctx, curve_update_intensity) } - pub fn update_perp_market_target_base_asset_amount_per_lp( - ctx: Context, - target_base_asset_amount_per_lp: i32, - ) -> Result<()> { - handle_update_perp_market_target_base_asset_amount_per_lp( - ctx, - target_base_asset_amount_per_lp, - ) - } - pub fn update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, diff --git a/programs/drift/src/math/amm_jit.rs b/programs/drift/src/math/amm_jit.rs index 17179b24b3..8614402ab9 100644 --- a/programs/drift/src/math/amm_jit.rs +++ b/programs/drift/src/math/amm_jit.rs @@ -133,19 +133,11 @@ pub fn calculate_clamped_jit_base_asset_amount( .cast::()?; // bound it; dont flip the net_baa - let max_amm_base_asset_amount = if liquidity_split != AMMLiquiditySplit::LPOwned { - market - .amm - .base_asset_amount_with_amm - .unsigned_abs() - .cast::()? - } else { - market - .amm - .imbalanced_base_asset_amount_with_lp()? - .unsigned_abs() - .cast::()? - }; + let max_amm_base_asset_amount = market + .amm + .base_asset_amount_with_amm + .unsigned_abs() + .cast::()?; let jit_base_asset_amount = jit_base_asset_amount.min(max_amm_base_asset_amount); @@ -161,10 +153,9 @@ pub fn calculate_amm_jit_liquidity( taker_base_asset_amount: u64, maker_base_asset_amount: u64, taker_has_limit_price: bool, - amm_lp_allowed_to_jit_make: Option, ) -> DriftResult<(u64, AMMLiquiditySplit)> { let mut jit_base_asset_amount: u64 = 0; - let mut liquidity_split: AMMLiquiditySplit = AMMLiquiditySplit::ProtocolOwned; + let liquidity_split: AMMLiquiditySplit = AMMLiquiditySplit::ProtocolOwned; // taker has_limit_price = false means (limit price = 0 AND auction is complete) so // market order will always land and fill on amm next round @@ -177,33 +168,7 @@ pub fn calculate_amm_jit_liquidity( } let amm_wants_to_jit_make = market.amm.amm_wants_to_jit_make(taker_direction)?; - let amm_lp_wants_to_jit_make = market.amm.amm_lp_wants_to_jit_make(taker_direction)?; - let amm_lp_allowed_to_jit_make = match amm_lp_allowed_to_jit_make { - Some(allowed) => allowed, - None => market - .amm - .amm_lp_allowed_to_jit_make(amm_wants_to_jit_make)?, - }; - let split_with_lps = amm_lp_allowed_to_jit_make && amm_lp_wants_to_jit_make; - if amm_wants_to_jit_make { - liquidity_split = if split_with_lps { - AMMLiquiditySplit::Shared - } else { - AMMLiquiditySplit::ProtocolOwned - }; - - jit_base_asset_amount = calculate_jit_base_asset_amount( - market, - base_asset_amount, - maker_price, - valid_oracle_price, - taker_direction, - liquidity_split, - )?; - } else if split_with_lps { - liquidity_split = AMMLiquiditySplit::LPOwned; - jit_base_asset_amount = calculate_jit_base_asset_amount( market, base_asset_amount, diff --git a/programs/drift/src/math/funding.rs b/programs/drift/src/math/funding.rs index 5fc9d78a3a..683f62c2cc 100644 --- a/programs/drift/src/math/funding.rs +++ b/programs/drift/src/math/funding.rs @@ -29,8 +29,7 @@ pub fn calculate_funding_rate_long_short( // If the net market position owes funding payment, the protocol receives payment let settled_net_market_position = market .amm - .base_asset_amount_with_amm - .safe_add(market.amm.base_asset_amount_with_unsettled_lp)?; + .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/position.rs b/programs/drift/src/math/position.rs index b07f7c91c7..8f008c0f35 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -43,23 +43,17 @@ pub fn calculate_base_asset_value(base_asset_amount: i128, amm: &AMM) -> DriftRe let (base_asset_reserve, quote_asset_reserve) = (amm.base_asset_reserve, amm.quote_asset_reserve); - let amm_lp_shares = amm.sqrt_k.safe_sub(amm.user_lp_shares)?; - - let base_asset_reserve_proportion = - get_proportion_u128(base_asset_reserve, amm_lp_shares, amm.sqrt_k)?; - - let quote_asset_reserve_proportion = - get_proportion_u128(quote_asset_reserve, amm_lp_shares, amm.sqrt_k)?; + let amm_lp_shares = amm.sqrt_k; let (new_quote_asset_reserve, _new_base_asset_reserve) = amm::calculate_swap_output( base_asset_amount.unsigned_abs(), - base_asset_reserve_proportion, + base_asset_reserve, swap_direction, amm_lp_shares, )?; let base_asset_value = calculate_quote_asset_amount_swapped( - quote_asset_reserve_proportion, + quote_asset_reserve, new_quote_asset_reserve, swap_direction, amm.peg_multiplier, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index b4f18c1e2c..1adf030388 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1185,18 +1185,11 @@ impl AMM { } pub fn get_lower_bound_sqrt_k(self) -> DriftResult { - Ok(self.sqrt_k.min( - self.user_lp_shares - .safe_add(self.user_lp_shares.safe_div(1000)?)? - .max(self.min_order_size.cast()?) - .max(self.base_asset_amount_with_amm.unsigned_abs().cast()?), - )) + Ok(self.sqrt_k) } pub fn get_protocol_owned_position(self) -> DriftResult { - self.base_asset_amount_with_amm - .safe_add(self.base_asset_amount_with_unsettled_lp)? - .cast::() + self.base_asset_amount_with_amm.cast::() } pub fn get_max_reference_price_offset(self) -> DriftResult { @@ -1215,109 +1208,6 @@ impl AMM { Ok(max_offset) } - pub fn get_per_lp_base_unit(self) -> DriftResult { - let scalar: i128 = 10_i128.pow(self.per_lp_base.abs().cast()?); - - if self.per_lp_base > 0 { - AMM_RESERVE_PRECISION_I128.safe_mul(scalar) - } else { - AMM_RESERVE_PRECISION_I128.safe_div(scalar) - } - } - - pub fn calculate_lp_base_delta( - &self, - per_lp_delta_base: i128, - base_unit: i128, - ) -> DriftResult { - // calculate dedicated for user lp shares - let lp_delta_base = - get_proportion_i128(per_lp_delta_base, self.user_lp_shares, base_unit.cast()?)?; - - Ok(lp_delta_base) - } - - pub fn calculate_per_lp_delta( - &self, - delta: &PositionDelta, - fee_to_market: i128, - liquidity_split: AMMLiquiditySplit, - base_unit: i128, - ) -> DriftResult<(i128, i128, i128)> { - let total_lp_shares = if liquidity_split == AMMLiquiditySplit::LPOwned { - self.user_lp_shares - } else { - self.sqrt_k - }; - - // update Market per lp position - let per_lp_delta_base = get_proportion_i128( - delta.base_asset_amount.cast()?, - base_unit.cast()?, - total_lp_shares, //.safe_div_ceil(rebase_divisor.cast()?)?, - )?; - - let mut per_lp_delta_quote = get_proportion_i128( - delta.quote_asset_amount.cast()?, - base_unit.cast()?, - total_lp_shares, //.safe_div_ceil(rebase_divisor.cast()?)?, - )?; - - // user position delta is short => lp position delta is long - if per_lp_delta_base < 0 { - // add one => lp subtract 1 - per_lp_delta_quote = per_lp_delta_quote.safe_add(1)?; - } - - // 1/5 of fee auto goes to market - // the rest goes to lps/market proportional - let per_lp_fee: i128 = if fee_to_market > 0 { - get_proportion_i128( - fee_to_market, - LP_FEE_SLICE_NUMERATOR, - LP_FEE_SLICE_DENOMINATOR, - )? - .safe_mul(base_unit)? - .safe_div(total_lp_shares.cast::()?)? - } else { - 0 - }; - - Ok((per_lp_delta_base, per_lp_delta_quote, per_lp_fee)) - } - - pub fn get_target_base_asset_amount_per_lp(&self) -> DriftResult { - if self.target_base_asset_amount_per_lp == 0 { - return Ok(0_i128); - } - - let target_base_asset_amount_per_lp: i128 = if self.per_lp_base > 0 { - let rebase_divisor = 10_i128.pow(self.per_lp_base.abs().cast()?); - self.target_base_asset_amount_per_lp - .cast::()? - .safe_mul(rebase_divisor)? - } else if self.per_lp_base < 0 { - let rebase_divisor = 10_i128.pow(self.per_lp_base.abs().cast()?); - self.target_base_asset_amount_per_lp - .cast::()? - .safe_div(rebase_divisor)? - } else { - self.target_base_asset_amount_per_lp.cast::()? - }; - - Ok(target_base_asset_amount_per_lp) - } - - pub fn imbalanced_base_asset_amount_with_lp(&self) -> DriftResult { - let target_lp_gap = self - .base_asset_amount_per_lp - .safe_sub(self.get_target_base_asset_amount_per_lp()?)?; - - let base_unit = self.get_per_lp_base_unit()?.cast()?; - - get_proportion_i128(target_lp_gap, self.user_lp_shares, base_unit) - } - pub fn amm_wants_to_jit_make(&self, taker_direction: PositionDirection) -> DriftResult { let amm_wants_to_jit_make = match taker_direction { PositionDirection::Long => { @@ -1330,25 +1220,6 @@ impl AMM { Ok(amm_wants_to_jit_make && self.amm_jit_is_active()) } - pub fn amm_lp_wants_to_jit_make( - &self, - taker_direction: PositionDirection, - ) -> DriftResult { - if self.user_lp_shares == 0 { - return Ok(false); - } - - let amm_lp_wants_to_jit_make = match taker_direction { - PositionDirection::Long => { - self.base_asset_amount_per_lp > self.get_target_base_asset_amount_per_lp()? - } - PositionDirection::Short => { - self.base_asset_amount_per_lp < self.get_target_base_asset_amount_per_lp()? - } - }; - Ok(amm_lp_wants_to_jit_make && self.amm_lp_jit_is_active()) - } - pub fn amm_lp_allowed_to_jit_make(&self, amm_wants_to_jit_make: bool) -> DriftResult { // only allow lps to make when the amm inventory is below a certain level of available liquidity // i.e. 10% @@ -1360,12 +1231,7 @@ impl AMM { self.max_base_asset_reserve, )?; - let min_side_liquidity = max_bids.min(max_asks.abs()); - let protocol_owned_min_side_liquidity = get_proportion_i128( - min_side_liquidity, - self.sqrt_k.safe_sub(self.user_lp_shares)?, - self.sqrt_k, - )?; + let protocol_owned_min_side_liquidity = max_bids.min(max_asks.abs()); Ok(self.base_asset_amount_with_amm.abs() < protocol_owned_min_side_liquidity.safe_div(10)?) @@ -1378,10 +1244,6 @@ impl AMM { self.amm_jit_intensity > 0 } - pub fn amm_lp_jit_is_active(&self) -> bool { - self.amm_jit_intensity > 100 - } - pub fn reserve_price(&self) -> DriftResult { amm::calculate_price( self.quote_asset_reserve, @@ -1448,7 +1310,7 @@ impl AMM { .base_asset_amount_with_amm .unsigned_abs() .max(min_order_size_u128) - < self.sqrt_k.safe_sub(self.user_lp_shares)?) + < self.sqrt_k) && (min_order_size_u128 < max_bids.unsigned_abs().max(max_asks.unsigned_abs())); Ok(can_lower) diff --git a/programs/drift/src/validation/perp_market.rs b/programs/drift/src/validation/perp_market.rs index f562dd3f5f..3d7fb73e14 100644 --- a/programs/drift/src/validation/perp_market.rs +++ b/programs/drift/src/validation/perp_market.rs @@ -32,19 +32,16 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { )?; validate!( (market.amm.base_asset_amount_long + market.amm.base_asset_amount_short) - == market.amm.base_asset_amount_with_amm - + market.amm.base_asset_amount_with_unsettled_lp, + == market.amm.base_asset_amount_with_amm, ErrorCode::InvalidAmmDetected, "Market NET_BAA Error: market.amm.base_asset_amount_long={}, + market.amm.base_asset_amount_short={} != - market.amm.base_asset_amount_with_amm={} - + market.amm.base_asset_amount_with_unsettled_lp={}", + market.amm.base_asset_amount_with_amm={}", market.amm.base_asset_amount_long, market.amm.base_asset_amount_short, market.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_unsettled_lp, )?; validate!( @@ -84,15 +81,6 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.quote_asset_reserve )?; - validate!( - market.amm.sqrt_k >= market.amm.user_lp_shares, - ErrorCode::InvalidAmmDetected, - "market {} market.amm.sqrt_k < market.amm.user_lp_shares: {} < {}", - market.market_index, - market.amm.sqrt_k, - market.amm.user_lp_shares, - )?; - let invariant_sqrt_u192 = crate::bn::U192::from(market.amm.sqrt_k); let invariant = invariant_sqrt_u192.safe_mul(invariant_sqrt_u192)?; let quote_asset_reserve = invariant @@ -229,22 +217,6 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.insurance_claim.revenue_withdraw_since_last_settle.unsigned_abs() )?; - validate!( - market.amm.base_asset_amount_per_lp < MAX_BASE_ASSET_AMOUNT_WITH_AMM as i128, - ErrorCode::InvalidAmmDetected, - "{} market.amm.base_asset_amount_per_lp too large: {}", - market.market_index, - market.amm.base_asset_amount_per_lp - )?; - - validate!( - market.amm.quote_asset_amount_per_lp < MAX_BASE_ASSET_AMOUNT_WITH_AMM as i128, - ErrorCode::InvalidAmmDetected, - "{} market.amm.quote_asset_amount_per_lp too large: {}", - market.market_index, - market.amm.quote_asset_amount_per_lp - )?; - Ok(()) } From 25ab531dafc0de16ba83d6810238baad7cbec2ea Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 16:56:55 -0400 Subject: [PATCH 04/91] make tests build --- programs/drift/src/controller/orders/tests.rs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 2f752066b8..eac2be1cba 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -486,7 +486,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -610,7 +609,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -734,7 +732,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -858,7 +855,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -981,7 +977,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1071,7 +1066,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1162,7 +1156,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1253,7 +1246,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1344,7 +1336,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1455,7 +1446,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1571,7 +1561,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1692,7 +1681,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1814,7 +1802,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -1960,7 +1947,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -2081,7 +2067,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -2212,7 +2197,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2364,7 +2348,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2514,7 +2497,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2665,7 +2647,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, - None, ) .unwrap(); @@ -2797,7 +2778,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); @@ -2928,7 +2908,6 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, - None, ) .unwrap(); From eba3f1123fb89bcee51677e90646a6ff2fbbdfe1 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 19 Jul 2025 17:10:16 -0400 Subject: [PATCH 05/91] start sdk changes --- sdk/src/math/bankruptcy.ts | 3 +- sdk/src/user.ts | 358 +----- test-scripts/run-anchor-tests.sh | 4 - tests/liquidityProvider.ts | 1912 ------------------------------ tests/perpLpJit.ts | 1250 ------------------- tests/perpLpRiskMitigation.ts | 537 --------- tests/tradingLP.ts | 281 ----- 7 files changed, 25 insertions(+), 4320 deletions(-) delete mode 100644 tests/liquidityProvider.ts delete mode 100644 tests/perpLpJit.ts delete mode 100644 tests/perpLpRiskMitigation.ts delete mode 100644 tests/tradingLP.ts diff --git a/sdk/src/math/bankruptcy.ts b/sdk/src/math/bankruptcy.ts index 23054ccdaa..92172d59f4 100644 --- a/sdk/src/math/bankruptcy.ts +++ b/sdk/src/math/bankruptcy.ts @@ -19,8 +19,7 @@ export function isUserBankrupt(user: User): boolean { if ( !position.baseAssetAmount.eq(ZERO) || position.quoteAssetAmount.gt(ZERO) || - hasOpenOrders(position) || - position.lpShares.gt(ZERO) + hasOpenOrders(position) ) { return false; } diff --git a/sdk/src/user.ts b/sdk/src/user.ts index c57245ade6..59e9e96c43 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -211,6 +211,14 @@ export class User { return this.getPerpPositionForUserAccount(userAccount, marketIndex); } + public getPerpPositionOrEmpty(marketIndex: number): PerpPosition { + const userAccount = this.getUserAccount(); + return ( + this.getPerpPositionForUserAccount(userAccount, marketIndex) ?? + this.getEmptyPosition(marketIndex) + ); + } + public getPerpPositionAndSlot( marketIndex: number ): DataAndSlot { @@ -414,243 +422,12 @@ export class User { public getPerpBidAsks(marketIndex: number): [BN, BN] { const position = this.getPerpPosition(marketIndex); - const [lpOpenBids, lpOpenAsks] = this.getLPBidAsks(marketIndex); - - const totalOpenBids = lpOpenBids.add(position.openBids); - const totalOpenAsks = lpOpenAsks.add(position.openAsks); + const totalOpenBids = position.openBids; + const totalOpenAsks = position.openAsks; return [totalOpenBids, totalOpenAsks]; } - /** - * calculates the open bids and asks for an lp - * optionally pass in lpShares to see what bid/asks a user *would* take on - * @returns : lp open bids - * @returns : lp open asks - */ - public getLPBidAsks(marketIndex: number, lpShares?: BN): [BN, BN] { - const position = this.getPerpPosition(marketIndex); - - const lpSharesToCalc = lpShares ?? position?.lpShares; - - if (!lpSharesToCalc || lpSharesToCalc.eq(ZERO)) { - return [ZERO, ZERO]; - } - - const market = this.driftClient.getPerpMarketAccount(marketIndex); - const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk( - market.amm.baseAssetReserve, - market.amm.minBaseAssetReserve, - market.amm.maxBaseAssetReserve, - market.amm.orderStepSize - ); - - const lpOpenBids = marketOpenBids.mul(lpSharesToCalc).div(market.amm.sqrtK); - const lpOpenAsks = marketOpenAsks.mul(lpSharesToCalc).div(market.amm.sqrtK); - - return [lpOpenBids, lpOpenAsks]; - } - - /** - * calculates the market position if the lp position was settled - * @returns : the settled userPosition - * @returns : the dust base asset amount (ie, < stepsize) - * @returns : pnl from settle - */ - public getPerpPositionWithLPSettle( - marketIndex: number, - originalPosition?: PerpPosition, - burnLpShares = false, - includeRemainderInBaseAmount = false - ): [PerpPosition, BN, BN] { - originalPosition = - originalPosition ?? - this.getPerpPosition(marketIndex) ?? - this.getEmptyPosition(marketIndex); - - if (originalPosition.lpShares.eq(ZERO)) { - return [originalPosition, ZERO, ZERO]; - } - - const position = this.getClonedPosition(originalPosition); - const market = this.driftClient.getPerpMarketAccount(position.marketIndex); - - if (market.amm.perLpBase != position.perLpBase) { - // perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares - const expoDiff = market.amm.perLpBase - position.perLpBase; - const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff)); - - if (expoDiff > 0) { - position.lastBaseAssetAmountPerLp = - position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar); - position.lastQuoteAssetAmountPerLp = - position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar); - } else { - position.lastBaseAssetAmountPerLp = - position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar); - position.lastQuoteAssetAmountPerLp = - position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar); - } - - position.perLpBase = position.perLpBase + expoDiff; - } - - const nShares = position.lpShares; - - // incorp unsettled funding on pre settled position - const quoteFundingPnl = calculateUnsettledFundingPnl(market, position); - - let baseUnit = AMM_RESERVE_PRECISION; - if (market.amm.perLpBase == position.perLpBase) { - if ( - position.perLpBase >= 0 && - position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber() - ) { - const marketPerLpRebase = new BN(10 ** market.amm.perLpBase); - baseUnit = baseUnit.mul(marketPerLpRebase); - } else if ( - position.perLpBase < 0 && - position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber() - ) { - const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase)); - baseUnit = baseUnit.div(marketPerLpRebase); - } else { - throw 'cannot calc'; - } - } else { - throw 'market.amm.perLpBase != position.perLpBase'; - } - - const deltaBaa = market.amm.baseAssetAmountPerLp - .sub(position.lastBaseAssetAmountPerLp) - .mul(nShares) - .div(baseUnit); - const deltaQaa = market.amm.quoteAssetAmountPerLp - .sub(position.lastQuoteAssetAmountPerLp) - .mul(nShares) - .div(baseUnit); - - function sign(v: BN) { - return v.isNeg() ? new BN(-1) : new BN(1); - } - - function standardize(amount: BN, stepSize: BN) { - const remainder = amount.abs().mod(stepSize).mul(sign(amount)); - const standardizedAmount = amount.sub(remainder); - return [standardizedAmount, remainder]; - } - - const [standardizedBaa, remainderBaa] = standardize( - deltaBaa, - market.amm.orderStepSize - ); - - position.remainderBaseAssetAmount += remainderBaa.toNumber(); - - if ( - Math.abs(position.remainderBaseAssetAmount) > - market.amm.orderStepSize.toNumber() - ) { - const [newStandardizedBaa, newRemainderBaa] = standardize( - new BN(position.remainderBaseAssetAmount), - market.amm.orderStepSize - ); - position.baseAssetAmount = - position.baseAssetAmount.add(newStandardizedBaa); - position.remainderBaseAssetAmount = newRemainderBaa.toNumber(); - } - - let dustBaseAssetValue = ZERO; - if (burnLpShares && position.remainderBaseAssetAmount != 0) { - const oraclePriceData = this.driftClient.getOracleDataForPerpMarket( - position.marketIndex - ); - dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount)) - .mul(oraclePriceData.price) - .div(AMM_RESERVE_PRECISION) - .add(ONE); - } - - let updateType; - if (position.baseAssetAmount.eq(ZERO)) { - updateType = 'open'; - } else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) { - updateType = 'increase'; - } else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) { - updateType = 'reduce'; - } else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) { - updateType = 'close'; - } else { - updateType = 'flip'; - } - - let newQuoteEntry; - let pnl; - if (updateType == 'open' || updateType == 'increase') { - newQuoteEntry = position.quoteEntryAmount.add(deltaQaa); - pnl = ZERO; - } else if (updateType == 'reduce' || updateType == 'close') { - newQuoteEntry = position.quoteEntryAmount.sub( - position.quoteEntryAmount - .mul(deltaBaa.abs()) - .div(position.baseAssetAmount.abs()) - ); - pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa); - } else { - newQuoteEntry = deltaQaa.sub( - deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs()) - ); - pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry)); - } - position.quoteEntryAmount = newQuoteEntry; - position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa); - position.quoteAssetAmount = position.quoteAssetAmount - .add(deltaQaa) - .add(quoteFundingPnl) - .sub(dustBaseAssetValue); - position.quoteBreakEvenAmount = position.quoteBreakEvenAmount - .add(deltaQaa) - .add(quoteFundingPnl) - .sub(dustBaseAssetValue); - - // update open bids/asks - const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk( - market.amm.baseAssetReserve, - market.amm.minBaseAssetReserve, - market.amm.maxBaseAssetReserve, - market.amm.orderStepSize - ); - const lpOpenBids = marketOpenBids - .mul(position.lpShares) - .div(market.amm.sqrtK); - const lpOpenAsks = marketOpenAsks - .mul(position.lpShares) - .div(market.amm.sqrtK); - position.openBids = lpOpenBids.add(position.openBids); - position.openAsks = lpOpenAsks.add(position.openAsks); - - // eliminate counting funding on settled position - if (position.baseAssetAmount.gt(ZERO)) { - position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong; - } else if (position.baseAssetAmount.lt(ZERO)) { - position.lastCumulativeFundingRate = - market.amm.cumulativeFundingRateShort; - } else { - position.lastCumulativeFundingRate = ZERO; - } - - const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount); - - if (includeRemainderInBaseAmount) { - position.baseAssetAmount = position.baseAssetAmount.add( - remainderBeforeRemoval - ); - position.remainderBaseAssetAmount = 0; - } - - return [position, remainderBeforeRemoval, pnl]; - } - /** * calculates Buying Power = free collateral / initial margin ratio * @returns : Precision QUOTE_PRECISION @@ -660,11 +437,7 @@ export class User { collateralBuffer = ZERO, enterHighLeverageMode = false ): BN { - const perpPosition = this.getPerpPositionWithLPSettle( - marketIndex, - undefined, - true - )[0]; + const perpPosition = this.getPerpPositionOrEmpty(marketIndex); const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex); const oraclePriceData = this.getOracleDataForPerpMarket(marketIndex); @@ -777,8 +550,7 @@ export class User { (pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || - !(pos.openOrders == 0) || - !pos.lpShares.eq(ZERO) + !(pos.openOrders == 0) ); } @@ -850,14 +622,6 @@ export class User { market.quoteSpotMarketIndex ); - if (perpPosition.lpShares.gt(ZERO)) { - perpPosition = this.getPerpPositionWithLPSettle( - perpPosition.marketIndex, - undefined, - !!withWeightMarginCategory - )[0]; - } - let positionUnrealizedPnl = calculatePositionPNL( market, perpPosition, @@ -1434,15 +1198,6 @@ export class User { perpPosition.marketIndex ); - if (perpPosition.lpShares.gt(ZERO)) { - // is an lp, clone so we dont mutate the position - perpPosition = this.getPerpPositionWithLPSettle( - market.marketIndex, - this.getClonedPosition(perpPosition), - !!marginCategory - )[0]; - } - let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; @@ -1517,19 +1272,6 @@ export class User { liabilityValue = liabilityValue.add( new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) ); - - if (perpPosition.lpShares.gt(ZERO)) { - liabilityValue = liabilityValue.add( - BN.max( - QUOTE_PRECISION, - valuationPrice - .mul(market.amm.orderStepSize) - .mul(QUOTE_PRECISION) - .div(AMM_RESERVE_PRECISION) - .div(PRICE_PRECISION) - ) - ); - } } } @@ -1593,13 +1335,7 @@ export class User { oraclePriceData: OraclePriceData, includeOpenOrders = false ): BN { - const userPosition = - this.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - )[0] || this.getEmptyPosition(marketIndex); + const userPosition = this.getPerpPositionOrEmpty(marketIndex); const market = this.driftClient.getPerpMarketAccount( userPosition.marketIndex ); @@ -1620,13 +1356,7 @@ export class User { oraclePriceData: OraclePriceData, includeOpenOrders = false ): BN { - const userPosition = - this.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - )[0] || this.getEmptyPosition(marketIndex); + const userPosition = this.getPerpPositionOrEmpty(marketIndex); const market = this.driftClient.getPerpMarketAccount( userPosition.marketIndex ); @@ -2173,11 +1903,9 @@ export class User { const oraclePrice = this.driftClient.getOracleDataForSpotMarket(marketIndex).price; if (perpMarketWithSameOracle) { - const perpPosition = this.getPerpPositionWithLPSettle( - perpMarketWithSameOracle.marketIndex, - undefined, - true - )[0]; + const perpPosition = this.getPerpPositionOrEmpty( + perpMarketWithSameOracle.marketIndex + ); if (perpPosition) { let freeCollateralDeltaForPerp = this.calculateFreeCollateralDeltaForPerp( @@ -2263,9 +1991,7 @@ export class User { this.driftClient.getOracleDataForPerpMarket(marketIndex).price; const market = this.driftClient.getPerpMarketAccount(marketIndex); - const currentPerpPosition = - this.getPerpPositionWithLPSettle(marketIndex, undefined, true)[0] || - this.getEmptyPosition(marketIndex); + const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); positionBaseSizeChange = standardizeBaseAssetAmount( positionBaseSizeChange, @@ -2556,12 +2282,7 @@ export class User { closeQuoteAmount: BN, estimatedEntryPrice: BN = ZERO ): BN { - const currentPosition = - this.getPerpPositionWithLPSettle( - positionMarketIndex, - undefined, - true - )[0] || this.getEmptyPosition(positionMarketIndex); + const currentPosition = this.getPerpPositionOrEmpty(positionMarketIndex); const closeBaseAmount = currentPosition.baseAssetAmount .mul(closeQuoteAmount) @@ -2627,9 +2348,7 @@ export class User { ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; - const currentPosition = - this.getPerpPositionWithLPSettle(targetMarketIndex, undefined, true)[0] || - this.getEmptyPosition(targetMarketIndex); + const currentPosition = this.getPerpPositionOrEmpty(targetMarketIndex); const targetSide = isVariant(tradeSide, 'short') ? 'short' : 'long'; @@ -3402,9 +3121,7 @@ export class User { return newLeverage; } - const currentPosition = - this.getPerpPositionWithLPSettle(targetMarketIndex)[0] || - this.getEmptyPosition(targetMarketIndex); + const currentPosition = this.getPerpPositionOrEmpty(targetMarketIndex); const perpMarket = this.driftClient.getPerpMarketAccount(targetMarketIndex); const oracleData = this.getOracleDataForPerpMarket(targetMarketIndex); @@ -3799,10 +3516,6 @@ export class User { oraclePriceData?: OraclePriceData; quoteOraclePriceData?: OraclePriceData; }): HealthComponent { - const settledLpPosition = this.getPerpPositionWithLPSettle( - perpPosition.marketIndex, - perpPosition - )[0]; const perpMarket = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); @@ -3814,7 +3527,7 @@ export class User { worstCaseBaseAssetAmount: worstCaseBaseAmount, worstCaseLiabilityValue, } = calculateWorstCasePerpLiabilityValue( - settledLpPosition, + perpPosition, perpMarket, oraclePrice ); @@ -3843,19 +3556,6 @@ export class User { new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) ); - if (perpPosition.lpShares.gt(ZERO)) { - marginRequirement = marginRequirement.add( - BN.max( - QUOTE_PRECISION, - oraclePrice - .mul(perpMarket.amm.orderStepSize) - .mul(QUOTE_PRECISION) - .div(AMM_RESERVE_PRECISION) - .div(PRICE_PRECISION) - ) - ); - } - return { marketIndex: perpMarket.marketIndex, size: worstCaseBaseAmount, @@ -3903,14 +3603,9 @@ export class User { perpMarket.quoteSpotMarketIndex ); - const settledPerpPosition = this.getPerpPositionWithLPSettle( - perpPosition.marketIndex, - perpPosition - )[0]; - const positionUnrealizedPnl = calculatePositionPNL( perpMarket, - settledPerpPosition, + perpPosition, true, oraclePriceData ); @@ -4062,12 +3757,7 @@ export class User { liquidationBuffer?: BN, includeOpenOrders?: boolean ): BN { - const currentPerpPosition = - this.getPerpPositionWithLPSettle( - marketToIgnore, - undefined, - !!marginCategory - )[0] || this.getEmptyPosition(marketToIgnore); + const currentPerpPosition = this.getPerpPositionOrEmpty(marketToIgnore); const oracleData = this.getOracleDataForPerpMarket(marketToIgnore); diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 2b10bd1bf7..a0e7494a33 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -44,7 +44,6 @@ test_files=( liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts - liquidityProvider.ts marketOrder.ts marketOrderBaseAssetAmount.ts maxDeposit.ts @@ -60,8 +59,6 @@ test_files=( ordersWithSpread.ts pauseExchange.ts pauseDepositWithdraw.ts - perpLpJit.ts - perpLpRiskMitigation.ts phoenixTest.ts placeAndMakePerp.ts placeAndMakeSignedMsgBankrun.ts @@ -85,7 +82,6 @@ test_files=( surgePricing.ts switchboardTxCus.ts switchOracle.ts - tradingLP.ts triggerOrders.ts triggerSpotOrder.ts transferPerpPosition.ts diff --git a/tests/liquidityProvider.ts b/tests/liquidityProvider.ts deleted file mode 100644 index 4bfc4df65d..0000000000 --- a/tests/liquidityProvider.ts +++ /dev/null @@ -1,1912 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { assert } from 'chai'; - -import { Program } from '@coral-xyz/anchor'; - -import * as web3 from '@solana/web3.js'; - -import { - TestClient, - QUOTE_PRECISION, - AMM_RESERVE_PRECISION, - EventSubscriber, - PRICE_PRECISION, - PositionDirection, - ZERO, - BN, - calculateAmmReservesAfterSwap, - calculatePrice, - User, - OracleSource, - SwapDirection, - Wallet, - isVariant, - LPRecord, - BASE_PRECISION, - getLimitOrderParams, - OracleGuardRails, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, - setFeedPriceNoProgram, - sleep, -} from './testHelpers'; -import { PerpPosition } from '../sdk'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -async function adjustOraclePostSwap(baa, swapDirection, market, context) { - const price = calculatePrice( - market.amm.baseAssetReserve, - market.amm.quoteAssetReserve, - market.amm.pegMultiplier - ); - - const [newQaa, newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - baa.abs(), - swapDirection - ); - - const newPrice = calculatePrice(newBaa, newQaa, market.amm.pegMultiplier); - const _newPrice = newPrice.toNumber() / PRICE_PRECISION.toNumber(); - await setFeedPriceNoProgram(context, _newPrice, market.amm.oracle); - - console.log('price => new price', price.toString(), newPrice.toString()); - - return _newPrice; -} - -async function createNewUser( - program, - provider, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -): Promise<[TestClient, User]> { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await provider.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - provider, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: provider.connection, - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: bulkAccountLoader - ? { - type: 'polling', - accountLoader: bulkAccountLoader, - } - : { - type: 'websocket', - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -async function fullClosePosition(driftClient, userPosition) { - console.log('=> closing:', userPosition.baseAssetAmount.toString()); - let position = (await driftClient.getUserAccount()).perpPositions[0]; - let sig; - let flag = true; - while (flag) { - sig = await driftClient.closePosition(0); - await driftClient.fetchAccounts(); - position = (await driftClient.getUserAccount()).perpPositions[0]; - if (position.baseAssetAmount.eq(ZERO)) { - flag = false; - } - } - - return sig; -} - -describe('liquidity providing', () => { - const chProgram = anchor.workspace.Drift as Program; - - let bankrunContextWrapper: BankrunContextWrapper; - - let bulkAccountLoader: TestBulkAccountLoader; - - async function _viewLogs(txsig) { - const tx = await bankrunContextWrapper.connection.getTransaction(txsig, { - commitment: 'confirmed', - }); - console.log('tx logs', tx.meta.logMessages); - } - async function delay(time) { - await new Promise((resolve) => setTimeout(resolve, time)); - } - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(300).mul(BASE_PRECISION); - const ammInitialQuoteAssetReserve = new BN(300).mul(BASE_PRECISION); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - const stableAmmInitialBaseAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - - const usdcAmount = new BN(1_000_000_000 * 1e6); - - let driftClient: TestClient; - let eventSubscriber: EventSubscriber; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let poorDriftClient: TestClient; - let poorDriftClientUser: User; - - let solusdc; - let solusdc2; - - before(async () => { - const context = await startAnchor('', [], []); - - bankrunContextWrapper = new BankrunContextWrapper(context); - - bulkAccountLoader = new TestBulkAccountLoader( - bankrunContextWrapper.connection, - 'processed', - 1 - ); - - usdcMint = await mockUSDCMint(bankrunContextWrapper); - - eventSubscriber = new EventSubscriber( - bankrunContextWrapper.connection, - chProgram - ); - await eventSubscriber.subscribe(); - - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - ]; - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // await driftClient.updateMarketBaseAssetAmountStepSize( - // new BN(0), - // new BN(1) - // ); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - [poorDriftClient, poorDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - QUOTE_PRECISION, - oracleInfos, - undefined, - bulkAccountLoader - ); - }); - - after(async () => { - await eventSubscriber.unsubscribe(); - - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - - await poorDriftClient.unsubscribe(); - await poorDriftClientUser.unsubscribe(); - }); - - const lpCooldown = 1; - - it('burn with standardized baa', async () => { - console.log('adding liquidity...'); - const initMarginReq = driftClientUser.getInitialMarginRequirement(); - assert(initMarginReq.eq(ZERO)); - - let market = driftClient.getPerpMarketAccount(0); - const lpAmount = new BN(100 * BASE_PRECISION.toNumber()); // 100 / (100 + 300) = 1/4 - const _sig = await driftClient.addPerpLpShares( - lpAmount, - market.marketIndex - ); - - await driftClient.fetchAccounts(); - - const addLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(addLiquidityRecord.action, 'addLiquidity')); - assert(addLiquidityRecord.nShares.eq(lpAmount)); - assert(addLiquidityRecord.marketIndex === 0); - assert( - addLiquidityRecord.user.equals( - await driftClient.getUserAccountPublicKey() - ) - ); - - const [bids, asks] = driftClientUser.getLPBidAsks(0); - console.log( - 'bar, min_bar, max_bar:', - market.amm.baseAssetReserve.toString(), - market.amm.minBaseAssetReserve.toString(), - market.amm.maxBaseAssetReserve.toString() - ); - console.log('LP open bids/asks:', bids.toString(), asks.toString()); - assert(bids.eq(new BN(41419999989))); - assert(asks.eq(new BN(-29288643749))); - - await driftClient.placePerpOrder( - getLimitOrderParams({ - baseAssetAmount: BASE_PRECISION, - marketIndex: 0, - direction: PositionDirection.LONG, // ++ bids - price: PRICE_PRECISION, - }) - ); - await driftClient.placePerpOrder( - getLimitOrderParams({ - baseAssetAmount: BASE_PRECISION, - marketIndex: 0, - direction: PositionDirection.SHORT, // ++ asks - price: PRICE_PRECISION.mul(new BN(100)), - }) - ); - - await driftClient.fetchAccounts(); - const [bids2, asks2] = driftClientUser.getPerpBidAsks(0); - assert(bids2.eq(bids.add(BASE_PRECISION))); - assert(asks2.eq(asks.sub(BASE_PRECISION))); - - await driftClient.cancelOrders(); - - await driftClient.fetchAccounts(); - const position3 = driftClientUser.getPerpPosition(0); - assert(position3.openOrders == 0); - assert(position3.openAsks.eq(ZERO)); - assert(position3.openBids.eq(ZERO)); - - const newInitMarginReq = driftClientUser.getInitialMarginRequirement(); - console.log(initMarginReq.toString(), '->', newInitMarginReq.toString()); - assert(newInitMarginReq.eq(new BN(9284008))); // 8284008 + $1 - - // ensure margin calcs didnt modify user position - const _position = driftClientUser.getPerpPosition(0); - assert(_position.openAsks.eq(ZERO)); - assert(_position.openBids.eq(ZERO)); - - const stepSize = new BN(1 * BASE_PRECISION.toNumber()); - await driftClient.updatePerpMarketStepSizeAndTickSize( - 0, - stepSize, - driftClient.getPerpMarketAccount(0).amm.orderTickSize - ); - - let user = await driftClientUser.getUserAccount(); - console.log('lpUser lpShares:', user.perpPositions[0].lpShares.toString()); - console.log( - 'lpUser baa:', - user.perpPositions[0].baseAssetAmount.toString() - ); - - assert(user.perpPositions[0].lpShares.eq(new BN('100000000000'))); - assert(user.perpPositions[0].baseAssetAmount.eq(ZERO)); - // some user goes long (lp should get a short) - console.log('user trading...'); - - market = driftClient.getPerpMarketAccount(0); - assert(market.amm.sqrtK.eq(new BN('400000000000'))); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - - const [newQaa, _newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - tradeSize.abs(), - SwapDirection.ADD - ); - const quoteAmount = newQaa.sub(market.amm.quoteAssetReserve); - const lpQuoteAmount = quoteAmount.mul(lpAmount).div(market.amm.sqrtK); - console.log( - lpQuoteAmount.mul(QUOTE_PRECISION).div(AMM_RESERVE_PRECISION).toString() - ); - - const newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - const sig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex, - new BN((newPrice * PRICE_PRECISION.toNumber() * 99) / 100) - ); - await _viewLogs(sig); - - // amm gets 33 (3/4 * 50 = 37.5) - // lp gets stepSize (1/4 * 50 = 12.5 => 10 with remainder 2.5) - // 2.5 / 12.5 = 0.2 - - await traderDriftClient.fetchAccounts(); - const traderUserAccount = await traderDriftClient.getUserAccount(); - const position = traderUserAccount.perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - - assert(position.baseAssetAmount.eq(new BN('-5000000000'))); - - await driftClient.fetchAccounts(); - const marketNetBaa = - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm; - - console.log('removing liquidity...'); - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - await _viewLogs(_txSig); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(settleLiquidityRecord.action, 'settleLiquidity')); - assert(settleLiquidityRecord.marketIndex === 0); - assert( - settleLiquidityRecord.user.equals( - await driftClient.getUserAccountPublicKey() - ) - ); - - // net baa doesnt change on settle - await driftClient.fetchAccounts(); - assert( - driftClient - .getPerpMarketAccount(0) - .amm.baseAssetAmountWithAmm.eq(marketNetBaa) - ); - - const marketAfter = driftClient.getPerpMarketAccount(0); - assert( - marketAfter.amm.baseAssetAmountWithUnsettledLp.eq(new BN('-250000000')) - ); - assert(marketAfter.amm.baseAssetAmountWithAmm.eq(new BN('-3750000000'))); - - user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - - assert( - settleLiquidityRecord.deltaBaseAssetAmount.eq(lpPosition.baseAssetAmount) - ); - assert( - settleLiquidityRecord.deltaQuoteAssetAmount.eq( - lpPosition.quoteAssetAmount - ) - ); - - console.log( - 'lp tokens, baa, qaa:', - lpPosition.lpShares.toString(), - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString(), - // lpPosition.unsettledPnl.toString(), - lpPosition.lastBaseAssetAmountPerLp.toString(), - lpPosition.lastQuoteAssetAmountPerLp.toString() - ); - - // assert(lpPosition.lpShares.eq(new BN(0))); - await driftClient.fetchAccounts(); - assert(user.perpPositions[0].baseAssetAmount.eq(new BN(1000000000))); // lp is long - console.log( - '=> net baa:', - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.toString() - ); - assert(user.perpPositions[0].quoteAssetAmount.eq(new BN(-1233700))); - // assert(user.perpPositions[0].unsettledPnl.eq(new BN(900))); - // remainder goes into the last - assert(user.perpPositions[0].lastBaseAssetAmountPerLp.eq(new BN(12500000))); - assert(user.perpPositions[0].lastQuoteAssetAmountPerLp.eq(new BN(-12337))); - - market = await driftClient.getPerpMarketAccount(0); - console.log( - market.amm.quoteAssetAmountPerLp.toString(), - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN(12500000))); - assert(market.amm.quoteAssetAmountPerLp.eq(new BN(-12337))); - console.log(user.perpPositions[0].remainderBaseAssetAmount.toString()); // lp remainder - assert(user.perpPositions[0].remainderBaseAssetAmount != 0); // lp remainder - assert(user.perpPositions[0].remainderBaseAssetAmount == 250000000); // lp remainder - - // remove - console.log('removing liquidity...'); - await driftClient.removePerpLpShares(0); - - await driftClient.fetchAccounts(); - - const removeLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(removeLiquidityRecord.action, 'removeLiquidity')); - assert(removeLiquidityRecord.nShares.eq(lpAmount)); - assert(removeLiquidityRecord.marketIndex === 0); - assert( - removeLiquidityRecord.user.equals( - await driftClient.getUserAccountPublicKey() - ) - ); - console.log( - 'removeLiquidityRecord.deltaQuoteAssetAmount', - removeLiquidityRecord.deltaQuoteAssetAmount.toString() - ); - assert(removeLiquidityRecord.deltaBaseAssetAmount.eq(ZERO)); - assert(removeLiquidityRecord.deltaQuoteAssetAmount.eq(new BN('-243866'))); // show pnl from burn in record - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - await fullClosePosition( - traderDriftClient, - traderDriftClient.getUserAccount().perpPositions[0] - ); - const traderUserAccount2 = - traderDriftClient.getUserAccount().perpPositions[0]; - - console.log( - traderUserAccount2.lpShares.toString(), - traderUserAccount2.baseAssetAmount.toString(), - traderUserAccount2.quoteAssetAmount.toString() - ); - - console.log('closing lp ...'); - console.log( - user.perpPositions[0].baseAssetAmount - .div(new BN(BASE_PRECISION.toNumber())) - .toString() - ); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - - const _ttxsig = await fullClosePosition(driftClient, user.perpPositions[0]); - // await _viewLogs(ttxsig); - - await driftClient.updatePerpMarketStepSizeAndTickSize( - 0, - new BN(1), - market.amm.orderTickSize - ); - - const user2 = await driftClientUser.getUserAccount(); - const position2 = user2.perpPositions[0]; - console.log( - position2.lpShares.toString(), - position2.baseAssetAmount.toString(), - position2.quoteAssetAmount.toString() - ); - - await driftClient.fetchAccounts(); - console.log( - '=> net baa:', - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.toString() - ); - assert( - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.eq(ZERO) - ); - - console.log('done!'); - }); - - it('settles lp', async () => { - console.log('adding liquidity...'); - - const market = driftClient.getPerpMarketAccount(0); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - - let user = await driftClientUser.getUserAccount(); - console.log(user.perpPositions[0].lpShares.toString()); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - const [settledLPPosition, _, sdkPnl] = - driftClientUser.getPerpPositionWithLPSettle(0); - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await await driftClientUser.getUserAccount(); - const position = user.perpPositions[0]; - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - console.log( - 'deltaBaseAssetAmount:', - settleLiquidityRecord.deltaBaseAssetAmount.toString() - ); - console.log( - 'deltaQuoteAssetAmount:', - settleLiquidityRecord.deltaQuoteAssetAmount.toString() - ); - - assert(settleLiquidityRecord.pnl.toString() === sdkPnl.toString()); - - // gets a short on settle - console.log( - 'simulated settle position:', - settledLPPosition.baseAssetAmount.toString(), - settledLPPosition.quoteAssetAmount.toString(), - settledLPPosition.quoteEntryAmount.toString() - ); - - // gets a short on settle - console.log( - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString(), - position.quoteEntryAmount.toString(), - position.remainderBaseAssetAmount.toString() - ); - - assert(settledLPPosition.baseAssetAmount.eq(position.baseAssetAmount)); - assert(settledLPPosition.quoteAssetAmount.eq(position.quoteAssetAmount)); - assert(settledLPPosition.quoteEntryAmount.eq(position.quoteEntryAmount)); - assert( - settledLPPosition.remainderBaseAssetAmount === - position.remainderBaseAssetAmount - ); - - console.log( - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - assert(position.baseAssetAmount.lt(ZERO)); - assert(position.quoteAssetAmount.gt(ZERO)); - assert(position.lpShares.gt(ZERO)); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await _viewLogs(_txSig); - - user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - const lpTokenAmount = lpPosition.lpShares; - assert(lpTokenAmount.eq(ZERO)); - - console.log( - 'lp position:', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString() - ); - - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await fullClosePosition( - traderDriftClient, - trader.perpPositions[0] - ); - await _viewLogs(_txsig); - - const traderPosition = (await traderDriftClient.getUserAccount()) - .perpPositions[0]; - console.log( - 'trader position:', - traderPosition.baseAssetAmount.toString(), - traderPosition.quoteAssetAmount.toString() - ); - - console.log('closing lp ...'); - const market2 = driftClient.getPerpMarketAccount(0); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.ADD, - market2, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, user.perpPositions[0]); - - await driftClient.fetchAccounts(); - console.log( - '=> net baa:', - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.toString() - ); - assert( - driftClient.getPerpMarketAccount(0).amm.baseAssetAmountWithAmm.eq(ZERO) - ); - - console.log('done!'); - }); - - it('provides and removes liquidity', async () => { - let market = driftClient.getPerpMarketAccount(0); - const prevSqrtK = market.amm.sqrtK; - const prevbar = market.amm.baseAssetReserve; - const prevqar = market.amm.quoteAssetReserve; - const prevQaa = - driftClient.getUserAccount().perpPositions[0].quoteAssetAmount; - - console.log('adding liquidity...'); - try { - const _txsig = await driftClient.addPerpLpShares( - new BN(100 * AMM_RESERVE_PRECISION.toNumber()), - market.marketIndex - ); - } catch (e) { - console.error(e); - } - await delay(lpCooldown + 1000); - - market = driftClient.getPerpMarketAccount(0); - console.log( - 'sqrtK:', - prevSqrtK.toString(), - '->', - market.amm.sqrtK.toString() - ); - console.log( - 'baseAssetReserve:', - prevbar.toString(), - '->', - market.amm.baseAssetReserve.toString() - ); - console.log( - 'quoteAssetReserve:', - prevqar.toString(), - '->', - market.amm.quoteAssetReserve.toString() - ); - - // k increases = more liquidity - assert(prevSqrtK.lt(market.amm.sqrtK)); - assert(prevqar.lt(market.amm.quoteAssetReserve)); - assert(prevbar.lt(market.amm.baseAssetReserve)); - - const lpShares = (await driftClientUser.getUserAccount()).perpPositions[0] - .lpShares; - console.log('lpShares:', lpShares.toString()); - assert(lpShares.gt(ZERO)); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - const user = await driftClientUser.getUserAccount(); - const lpTokenAmount = user.perpPositions[0].lpShares; - console.log('lp token amount:', lpTokenAmount.toString()); - assert(lpTokenAmount.eq(ZERO)); - // dont round down for no change - assert(user.perpPositions[0].quoteAssetAmount.eq(prevQaa)); - - console.log('asset reserves:'); - console.log(prevSqrtK.toString(), market.amm.sqrtK.toString()); - console.log(prevbar.toString(), market.amm.baseAssetReserve.toString()); - console.log(prevqar.toString(), market.amm.quoteAssetReserve.toString()); - - const errThreshold = new BN(500); - assert(prevSqrtK.eq(market.amm.sqrtK)); - assert( - prevbar.sub(market.amm.baseAssetReserve).abs().lte(errThreshold), - prevbar.sub(market.amm.baseAssetReserve).abs().toString() - ); - assert( - prevqar.sub(market.amm.quoteAssetReserve).abs().lte(errThreshold), - prevqar.sub(market.amm.quoteAssetReserve).abs().toString() - ); - assert(prevSqrtK.eq(market.amm.sqrtK)); - }); - - it('mints too many lp tokens', async () => { - console.log('adding liquidity...'); - const market = driftClient.getPerpMarketAccount(0); - try { - const _sig = await poorDriftClient.addPerpLpShares( - market.amm.sqrtK.mul(new BN(5)), - market.marketIndex - ); - _viewLogs(_sig); - assert(false); - } catch (e) { - console.error(e.message); - assert(e.message.includes('0x1773')); // insufficient collateral - } - }); - - it('provides lp, users shorts, removes lp, lp has long', async () => { - await driftClient.fetchAccounts(); - await traderDriftClient.fetchAccounts(); - console.log('adding liquidity...'); - - const traderUserAccount3 = await driftClient.getUserAccount(); - const position3 = traderUserAccount3.perpPositions[0]; - console.log( - 'lp position:', - position3.baseAssetAmount.toString(), - position3.quoteAssetAmount.toString() - ); - - const traderUserAccount0 = await traderDriftClient.getUserAccount(); - const position0 = traderUserAccount0.perpPositions[0]; - console.log( - 'trader position:', - position0.baseAssetAmount.toString(), - position0.quoteAssetAmount.toString() - ); - assert(position0.baseAssetAmount.eq(new BN('0'))); - - const market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.netBaseAssetAmount:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('0'))); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - // await delay(lpCooldown + 1000); - - let user = await driftClientUser.getUserAccount(); - console.log('lpUser lpShares:', user.perpPositions[0].lpShares.toString()); - console.log( - 'lpUser baa:', - user.perpPositions[0].baseAssetAmount.toString() - ); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(40 * BASE_PRECISION.toNumber()); - const _newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - try { - const _txsig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - // new BN(newPrice * PRICE_PRECISION.toNumber()) - ); - } catch (e) { - console.error(e); - } - - await traderDriftClient.fetchAccounts(); - const market1 = driftClient.getPerpMarketAccount(0); - console.log( - 'market1.amm.netBaseAssetAmount:', - market1.amm.baseAssetAmountWithAmm.toString() - ); - const ammLpRatio = - market1.amm.userLpShares.toNumber() / market1.amm.sqrtK.toNumber(); - - console.log('amm ratio:', ammLpRatio, '(', 40 * ammLpRatio, ')'); - - assert(market1.amm.baseAssetAmountWithAmm.eq(new BN('-30000000000'))); - - const traderUserAccount = await traderDriftClient.getUserAccount(); - // console.log(traderUserAccount); - const position = traderUserAccount.perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await _viewLogs(_txSig); - - user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - const lpTokenAmount = lpPosition.lpShares; - - console.log( - 'lp tokens', - lpTokenAmount.toString(), - 'baa, qaa', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString() - // lpPosition.unsettledPnl.toString() - ); - - const removeLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - assert(isVariant(removeLiquidityRecord.action, 'removeLiquidity')); - assert( - removeLiquidityRecord.deltaBaseAssetAmount.eq( - lpPosition.baseAssetAmount.sub(position3.baseAssetAmount) - ) - ); - assert( - removeLiquidityRecord.deltaQuoteAssetAmount.eq( - lpPosition.quoteAssetAmount.sub(position3.quoteAssetAmount) - ) - ); - - assert(lpTokenAmount.eq(new BN(0))); - console.log(user.perpPositions[0].baseAssetAmount.toString()); - console.log(user.perpPositions[0].quoteAssetAmount.toString()); - assert(user.perpPositions[0].baseAssetAmount.eq(new BN('10000000000'))); // lp is long - assert(user.perpPositions[0].quoteAssetAmount.eq(new BN(-9550985))); - - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - await fullClosePosition( - traderDriftClient, - traderUserAccount.perpPositions[0] - ); - - console.log('closing lp ...'); - console.log( - user.perpPositions[0].baseAssetAmount - .div(new BN(BASE_PRECISION.toNumber())) - .toString() - ); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, user.perpPositions[0]); - - const user2 = await driftClientUser.getUserAccount(); - const position2 = user2.perpPositions[0]; - console.log( - position2.lpShares.toString(), - position2.baseAssetAmount.toString(), - position2.quoteAssetAmount.toString() - ); - - console.log('done!'); - }); - - it('provides lp, users longs, removes lp, lp has short', async () => { - const market = driftClient.getPerpMarketAccount(0); - - console.log('adding liquidity...'); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - // await delay(lpCooldown + 1000); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(40 * BASE_PRECISION.toNumber()); - const _newPrice0 = await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(newPrice0 * PRICE_PRECISION.toNumber()) - ); - - const position = (await traderDriftClient.getUserAccount()) - .perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - - console.log('removing liquidity...'); - const _txSig = await driftClient.removePerpLpShares(market.marketIndex); - await _viewLogs(_txSig); - - await driftClientUser.fetchAccounts(); - const user = await driftClientUser.getUserAccount(); - const lpPosition = user.perpPositions[0]; - const lpTokenAmount = lpPosition.lpShares; - - console.log('lp tokens', lpTokenAmount.toString()); - console.log( - 'baa, qaa, qea', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString(), - lpPosition.quoteEntryAmount.toString() - - // lpPosition.unsettledPnl.toString() - ); - - assert(lpTokenAmount.eq(ZERO)); - assert(user.perpPositions[0].baseAssetAmount.eq(new BN('-10000000000'))); // lp is short - assert(user.perpPositions[0].quoteAssetAmount.eq(new BN('11940540'))); - assert(user.perpPositions[0].quoteEntryAmount.eq(new BN('11139500'))); - - console.log('closing trader...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await fullClosePosition(traderDriftClient, position); - - console.log('closing lp ...'); - await adjustOraclePostSwap( - user.perpPositions[0].baseAssetAmount, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, lpPosition); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const user2 = await driftClientUser.getUserAccount(); - const lpPosition2 = user2.perpPositions[0]; - - console.log('lp tokens', lpPosition2.lpShares.toString()); - console.log( - 'lp position for market', - lpPosition2.marketIndex, - ':\n', - 'baa, qaa, qea', - lpPosition2.baseAssetAmount.toString(), - lpPosition2.quoteAssetAmount.toString(), - lpPosition2.quoteEntryAmount.toString() - ); - assert(lpPosition2.baseAssetAmount.eq(ZERO)); - - console.log('done!'); - }); - - it('lp burns a partial position', async () => { - const market = driftClient.getPerpMarketAccount(0); - - console.log('adding liquidity...'); - await driftClient.addPerpLpShares( - new BN(100).mul(AMM_RESERVE_PRECISION), - market.marketIndex - ); - // await delay(lpCooldown + 1000); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const user0 = await driftClient.getUserAccount(); - const position0 = user0.perpPositions[0]; - console.log( - 'assert LP has 0 position in market index', - market.marketIndex, - ':', - position0.baseAssetAmount.toString(), - position0.quoteAssetAmount.toString() - ); - console.log(position0.lpShares.toString()); - - const baa0 = position0.baseAssetAmount; - assert(baa0.eq(ZERO)); - - console.log('user trading...'); - const tradeSize = new BN(40 * BASE_PRECISION.toNumber()); - const _newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - // new BN(newPrice * PRICE_PRECISION.toNumber()) - ); - - console.log('removing liquidity...'); - let user = await driftClient.getUserAccount(); - let position = user.perpPositions[0]; - - const fullShares = position.lpShares; - const halfShares = position.lpShares.div(new BN(2)); - const otherHalfShares = fullShares.sub(halfShares); - - try { - const _txSig = await driftClient.removePerpLpShares( - market.marketIndex, - halfShares - ); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - user = await driftClient.getUserAccount(); - position = user.perpPositions[0]; - console.log( - 'lp first half burn:', - user.perpPositions[0].baseAssetAmount.toString(), - user.perpPositions[0].quoteAssetAmount.toString(), - user.perpPositions[0].lpShares.toString() - ); - - const baa = user.perpPositions[0].baseAssetAmount; - const qaa = user.perpPositions[0].quoteAssetAmount; - assert(baa.eq(new BN(10000000000))); - assert(qaa.eq(new BN(-6860662))); - - console.log('removing the other half of liquidity'); - await driftClient.removePerpLpShares(market.marketIndex, otherHalfShares); - - await driftClient.fetchAccounts(); - - user = await driftClient.getUserAccount(); - console.log( - 'lp second half burn:', - user.perpPositions[0].baseAssetAmount.toString(), - user.perpPositions[0].quoteAssetAmount.toString(), - user.perpPositions[0].lpShares.toString() - ); - // lp is already settled so full burn baa is already in baa - assert(user.perpPositions[0].lpShares.eq(ZERO)); - - console.log('closing trader ...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - // await traderDriftClient.closePosition(new BN(0)); - const trader = await traderDriftClient.getUserAccount(); - const _txsig = await fullClosePosition( - traderDriftClient, - trader.perpPositions[0] - ); - - console.log('closing lp ...'); - await adjustOraclePostSwap( - baa, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - await fullClosePosition(driftClient, user.perpPositions[0]); - }); - - it('settles lp with pnl', async () => { - console.log('adding liquidity...'); - - const market = driftClient.getPerpMarketAccount(0); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - - let user = await driftClientUser.getUserAccount(); - console.log(user.perpPositions[0].lpShares.toString()); - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - // some user goes long (lp should get a short + pnl for closing long on settle) - console.log('user trading...'); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await await driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - }); - - it('update per lp base (0->1)', async () => { - //ensure non-zero for test - - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp(0, 169); - - await driftClient.fetchAccounts(); - const marketBefore = driftClient.getPerpMarketAccount(0); - console.log( - 'marketBefore.amm.totalFeeEarnedPerLp', - marketBefore.amm.totalFeeEarnedPerLp.toString() - ); - assert(marketBefore.amm.totalFeeEarnedPerLp.eq(new BN('272'))); - - const txSig1 = await driftClient.updatePerpMarketPerLpBase(0, 1); - await _viewLogs(txSig1); - - await sleep(1400); // todo? - await driftClient.fetchAccounts(); - const marketAfter = driftClient.getPerpMarketAccount(0); - - assert( - marketAfter.amm.totalFeeEarnedPerLp.eq( - marketBefore.amm.totalFeeEarnedPerLp.mul(new BN(10)) - ) - ); - - assert( - marketAfter.amm.baseAssetAmountPerLp.eq( - marketBefore.amm.baseAssetAmountPerLp.mul(new BN(10)) - ) - ); - - assert( - marketAfter.amm.quoteAssetAmountPerLp.eq( - marketBefore.amm.quoteAssetAmountPerLp.mul(new BN(10)) - ) - ); - console.log(marketAfter.amm.targetBaseAssetAmountPerLp); - console.log(marketBefore.amm.targetBaseAssetAmountPerLp); - - assert( - marketAfter.amm.targetBaseAssetAmountPerLp == - marketBefore.amm.targetBaseAssetAmountPerLp - ); - assert(marketAfter.amm.totalFeeEarnedPerLp.eq(new BN('2720'))); - - assert(marketBefore.amm.perLpBase == 0); - console.log('marketAfter.amm.perLpBase:', marketAfter.amm.perLpBase); - assert(marketAfter.amm.perLpBase == 1); - }); - - it('settle lp position after perLpBase change', async () => { - // some user goes long (lp should get a short + pnl for closing long on settle) - - const market = driftClient.getPerpMarketAccount(0); - console.log( - 'baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(ZERO)); - // await delay(lpCooldown + 1000); - - const user = await driftClientUser.getUserAccount(); - console.log(user.perpPositions[0].lpShares.toString()); - console.log(user.perpPositions[0].perLpBase); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - - console.log('user trading...'); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - const marketAfter0 = driftClient.getPerpMarketAccount(0); - - console.log( - 'baseAssetAmountWithUnsettledLp:', - marketAfter0.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert( - marketAfter0.amm.baseAssetAmountWithUnsettledLp.eq(new BN('1250000000')) - ); - - const netValueBefore = await driftClient.getUser().getNetSpotMarketValue(); - const posBefore0: PerpPosition = await driftClient - .getUser() - .getPerpPosition(0); - assert(posBefore0.perLpBase == 0); - - const posBefore: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - // console.log(posBefore); - assert(posBefore.perLpBase == 1); // properly sets it - - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - await _viewLogs(_txSig); - await driftClient.fetchAccounts(); - const marketAfter1 = driftClient.getPerpMarketAccount(0); - - console.log( - 'baseAssetAmountWithUnsettledLp:', - marketAfter1.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert(marketAfter1.amm.baseAssetAmountWithUnsettledLp.eq(new BN('0'))); - - const posAfter0: PerpPosition = await driftClient - .getUser() - .getPerpPosition(0); - assert(posAfter0.perLpBase == 1); - - const posAfter: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - - assert(posAfter.perLpBase == 1); - assert( - posAfter0.lastBaseAssetAmountPerLp.gt(posBefore0.lastBaseAssetAmountPerLp) - ); - // console.log(posAfter.lastBaseAssetAmountPerLp.toString()); - // console.log(posBefore.lastBaseAssetAmountPerLp.toString()); - - assert(posAfter.lastBaseAssetAmountPerLp.eq(new BN('625000000'))); - assert(posBefore.lastBaseAssetAmountPerLp.eq(new BN('750000000'))); - - const netValueAfter = await driftClient.getUser().getNetSpotMarketValue(); - - assert(netValueBefore.eq(netValueAfter)); - - const marketAfter2 = driftClient.getPerpMarketAccount(0); - - console.log( - 'baseAssetAmountWithUnsettledLp:', - marketAfter2.amm.baseAssetAmountWithUnsettledLp.toString() - ); - assert(marketAfter2.amm.baseAssetAmountWithUnsettledLp.eq(new BN('0'))); - console.log( - 'marketBefore.amm.totalFeeEarnedPerLp', - marketAfter2.amm.totalFeeEarnedPerLp.toString() - ); - assert(marketAfter2.amm.totalFeeEarnedPerLp.eq(new BN('2826'))); - }); - - it('add back lp shares from 0, after rebase', async () => { - const leShares = driftClientUser.getPerpPosition(0).lpShares; - await driftClient.removePerpLpShares(0, leShares); - await driftClient.fetchAccounts(); - - await driftClient.updatePerpMarketPerLpBase(0, 2); // update from 1->2 - - const posBeforeReadd: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log(posBeforeReadd.baseAssetAmount.toString()); - console.log(posBeforeReadd.quoteAssetAmount.toString()); - console.log(posBeforeReadd.lastBaseAssetAmountPerLp.toString()); - console.log(posBeforeReadd.lastQuoteAssetAmountPerLp.toString()); - console.log( - posBeforeReadd.lpShares.toString(), - posBeforeReadd.perLpBase.toString() - ); - - await driftClient.addPerpLpShares(leShares, 0); // lmao why is this different param order - - const posBefore: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posBefore'); - console.log(posBefore.baseAssetAmount.toString()); - console.log(posBefore.quoteAssetAmount.toString()); - console.log(posBefore.lastBaseAssetAmountPerLp.toString()); - console.log(posBefore.lastQuoteAssetAmountPerLp.toString()); - console.log(posBefore.lpShares.toString(), posBefore.perLpBase.toString()); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - const market = driftClient.getPerpMarketAccount(0); - - console.log('user trading...'); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfter: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfter'); - console.log(posAfter.baseAssetAmount.toString()); - console.log(posAfter.quoteAssetAmount.toString()); - console.log(posAfter.lastBaseAssetAmountPerLp.toString()); - console.log(posAfter.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfter.perLpBase.toString()); - - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfterSettle: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfterSettle'); - console.log(posAfterSettle.baseAssetAmount.toString()); - console.log(posAfterSettle.quoteAssetAmount.toString()); - console.log(posAfterSettle.lastBaseAssetAmountPerLp.toString()); - console.log(posAfterSettle.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfterSettle.perLpBase.toString()); - - assert(posAfterSettle.baseAssetAmount.eq(posAfter.baseAssetAmount)); - assert(posAfterSettle.quoteAssetAmount.eq(posAfter.quoteAssetAmount)); - }); - - it('settled at negative rebase value', async () => { - await driftClient.updatePerpMarketPerLpBase(0, 1); - await driftClient.updatePerpMarketPerLpBase(0, 0); - await driftClient.updatePerpMarketPerLpBase(0, -1); - await driftClient.updatePerpMarketPerLpBase(0, -2); - await driftClient.updatePerpMarketPerLpBase(0, -3); - - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - const market = driftClient.getPerpMarketAccount(0); - - console.log('user trading...'); - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex - ); - await _viewLogs(_txsig); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfter: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfter'); - console.log(posAfter.baseAssetAmount.toString()); - console.log(posAfter.quoteAssetAmount.toString()); - console.log(posAfter.lastBaseAssetAmountPerLp.toString()); - console.log(posAfter.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfter.perLpBase.toString()); - - const _txSig = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - market.marketIndex - ); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const posAfterSettle: PerpPosition = await driftClient - .getUser() - .getPerpPositionWithLPSettle(0)[0]; - console.log('posAfterSettle'); - console.log(posAfterSettle.baseAssetAmount.toString()); - console.log(posAfterSettle.quoteAssetAmount.toString()); - console.log(posAfterSettle.lastBaseAssetAmountPerLp.toString()); - console.log(posAfterSettle.lastQuoteAssetAmountPerLp.toString()); - console.log(posAfterSettle.perLpBase.toString()); - - assert(posAfterSettle.baseAssetAmount.eq(posAfter.baseAssetAmount)); - assert(posAfterSettle.quoteAssetAmount.eq(posAfter.quoteAssetAmount)); - }); - - it('permissionless lp burn', async () => { - const lpAmount = new BN(1 * BASE_PRECISION.toNumber()); - const _sig = await driftClient.addPerpLpShares(lpAmount, 0); - - const time = bankrunContextWrapper.connection.getTime(); - const _2sig = await driftClient.updatePerpMarketExpiry(0, new BN(time + 5)); - - await sleep(5000); - - await driftClient.fetchAccounts(); - const market = driftClient.getPerpMarketAccount(0); - console.log(market.status); - - await traderDriftClient.removePerpLpSharesInExpiringMarket( - 0, - await driftClient.getUserAccountPublicKey() - ); - - await driftClientUser.fetchAccounts(); - const position = driftClientUser.getPerpPosition(0); - console.log(position); - // assert(position.lpShares.eq(ZERO)); - }); - - return; - - it('lp gets paid in funding (todo)', async () => { - const market = driftClient.getPerpMarketAccount(1); - const marketIndex = market.marketIndex; - - console.log('adding liquidity to market ', marketIndex, '...'); - try { - const _sig = await driftClient.addPerpLpShares( - new BN(100_000).mul(new BN(BASE_PRECISION.toNumber())), - marketIndex - ); - } catch (e) { - console.error(e); - } - await delay(lpCooldown + 1000); - - console.log('user trading...'); - // const trader0 = await traderDriftClient.getUserAccount(); - const tradeSize = new BN(100).mul(AMM_RESERVE_PRECISION); - - const newPrice = await adjustOraclePostSwap( - tradeSize, - SwapDirection.ADD, - market, - bankrunContextWrapper - ); - console.log('market', marketIndex, 'post trade price:', newPrice); - try { - const _txig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - marketIndex, - new BN(newPrice * PRICE_PRECISION.toNumber()) - ); - } catch (e) { - console.error(e); - } - - console.log('updating funding rates'); - const _txsig = await driftClient.updateFundingRate(marketIndex, solusdc2); - - console.log('removing liquidity...'); - try { - const _txSig = await driftClient.removePerpLpShares(marketIndex); - _viewLogs(_txSig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - - const user = driftClientUser.getUserAccount(); - // const feePayment = new BN(1300000); - // const fundingPayment = new BN(900000); - - // dont get paid in fees bc the sqrtk is so big that fees dont get given to the lps - // TODO - // assert(user.perpPositions[1].unsettledPnl.eq(fundingPayment.add(feePayment))); - const position1 = user.perpPositions[1]; - console.log( - 'lp position:', - position1.baseAssetAmount.toString(), - position1.quoteAssetAmount.toString(), - 'vs step size:', - market.amm.orderStepSize.toString() - ); - assert(user.perpPositions[1].baseAssetAmount.eq(ZERO)); // lp has no position - assert( - user.perpPositions[1].baseAssetAmount.abs().lt(market.amm.orderStepSize) - ); - // const trader = traderDriftClient.getUserAccount(); - // await adjustOraclePostSwap( - // trader.perpPositions[1].baseAssetAmount, - // SwapDirection.ADD, - // market - // ); - // await traderDriftClient.closePosition(market.marketIndex); // close lp position - - // console.log('closing lp ...'); - // console.log(user.perpPositions[1].baseAssetAmount.toString()); - // await adjustOraclePostSwap( - // user.perpPositions[1].baseAssetAmount, - // SwapDirection.REMOVE, - // market - // ); - }); - - // // TODO - // it('provides and removes liquidity too fast', async () => { - // const market = driftClient.getPerpMarketAccount(0); - - // const lpShares = new BN(100 * AMM_RESERVE_PRECISION); - // const addLpIx = await driftClient.getAddLiquidityIx( - // lpShares, - // market.marketIndex - // ); - // const removeLpIx = await driftClient.getRemoveLiquidityIx( - // market.marketIndex, - // lpShares - // ); - - // const tx = new web3.Transaction().add(addLpIx).add(removeLpIx); - // try { - // await provider.sendAll([{ tx }]); - // assert(false); - // } catch (e) { - // console.error(e); - // assert(e.message.includes('0x17ce')); - // } - // }); - - // it('removes liquidity when market position is small', async () => { - // console.log('adding liquidity...'); - // await driftClient.addLiquidity(usdcAmount, new BN(0)); - // - // console.log('user trading...'); - // await traderDriftClient.openPosition( - // PositionDirection.LONG, - // new BN(1 * 1e6), - // new BN(0) - // ); - // - // console.log('removing liquidity...'); - // await driftClient.removeLiquidity(new BN(0)); - // - // const user = driftClient.getUserAccount(); - // const position = user.perpPositions[0]; - // - // // small loss - // assert(position.unsettledPnl.lt(ZERO)); - // // no position - // assert(position.baseAssetAmount.eq(ZERO)); - // assert(position.quoteAssetAmount.eq(ZERO)); - // }); - // - // uncomment when settle fcn is ready - - /* it('adds additional liquidity to an already open lp', async () => { - console.log('adding liquidity...'); - const lp_amount = new BN(300 * 1e6); - const _txSig = await driftClient.addLiquidity(lp_amount, new BN(0)); - - console.log( - 'tx logs', - (await connection.getTransaction(txsig, { commitment: 'confirmed' })).meta - .logMessages - ); - - const init_user = driftClientUser.getUserAccount(); - await driftClient.addLiquidity(lp_amount, new BN(0)); - const user = driftClientUser.getUserAccount(); - - const init_tokens = init_user.perpPositions[0].lpTokens; - const tokens = user.perpPositions[0].lpTokens; - console.log(init_tokens.toString(), tokens.toString()); - assert(init_tokens.lt(tokens)); - - await driftClient.removeLiquidity(new BN(0)); - }); */ - - /* it('settles an lps position', async () => { - console.log('adding liquidity...'); - await driftClient.addLiquidity(usdcAmount, new BN(0)); - - let user = driftClient.getUserAccount(); - const baa = user.perpPositions[0].baseAssetAmount; - const qaa = user.perpPositions[0].quoteAssetAmount; - const upnl = user.perpPositions[0].unsettledPnl; - - console.log('user trading...'); - await traderDriftClient.openPosition( - PositionDirection.SHORT, - new BN(115 * 1e5), - new BN(0) - ); - - console.log('settling...'); - await traderDriftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - new BN(0) - ); - - user = driftClient.getUserAccount(); - const position = user.perpPositions[0]; - const post_baa = position.baseAssetAmount; - const post_qaa = position.quoteAssetAmount; - const post_upnl = position.unsettledPnl; - - // they got the market position + upnl - console.log(baa.toString(), post_baa.toString()); - console.log(qaa.toString(), post_qaa.toString()); - console.log(upnl.toString(), post_upnl.toString()); - assert(!post_baa.eq(baa)); - assert(post_qaa.gt(qaa)); - assert(!post_upnl.eq(upnl)); - - // other sht was updated - const market = driftClient.getPerpMarketAccount(new BN(0)); - assert(market.amm.netBaseAssetAmount.eq(position.lastNetBaseAssetAmount)); - assert( - market.amm.totalFeeMinusDistributions.eq( - position.lastTotalFeeMinusDistributions - ) - ); - - const _txSig = await driftClient.removeLiquidity(new BN(0)); - - console.log('done!'); - }); */ - - /* it('simulates a settle via sdk', async () => { - const userPosition2 = driftClient.getUserAccount().perpPositions[0]; - console.log( - userPosition2.baseAssetAmount.toString(), - userPosition2.quoteAssetAmount.toString(), - userPosition2.unsettledPnl.toString() - ); - - console.log('add lp ...'); - await driftClient.addLiquidity(usdcAmount, new BN(0)); - - console.log('user trading...'); - await traderDriftClient.openPosition( - PositionDirection.SHORT, - new BN(115 * 1e5), - new BN(0) - ); - - const [settledPosition, result, _] = driftClientUser.getPerpPositionWithLPSettle( - new BN(0) - ); - - console.log('settling...'); - const _txSig = await traderDriftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - new BN(0) - ); - console.log( - 'tx logs', - (await connection.getTransaction(txsig, { commitment: 'confirmed' })).meta - .logMessages - ); - const userPosition = driftClient.getUserAccount().perpPositions[0]; - - console.log( - userPosition.baseAssetAmount.toString(), - settledPosition.baseAssetAmount.toString(), - - userPosition.quoteAssetAmount.toString(), - settledPosition.quoteAssetAmount.toString(), - - userPosition.unsettledPnl.toString(), - settledPosition.unsettledPnl.toString() - ); - assert(result == SettleResult.RECIEVED_MARKET_POSITION); - assert(userPosition.baseAssetAmount.eq(settledPosition.baseAssetAmount)); - assert(userPosition.quoteAssetAmount.eq(settledPosition.quoteAssetAmount)); - assert(userPosition.unsettledPnl.eq(settledPosition.unsettledPnl)); - }); */ -}); diff --git a/tests/perpLpJit.ts b/tests/perpLpJit.ts deleted file mode 100644 index 12acebca50..0000000000 --- a/tests/perpLpJit.ts +++ /dev/null @@ -1,1250 +0,0 @@ -import * as web3 from '@solana/web3.js'; -import * as anchor from '@coral-xyz/anchor'; -import { Program } from '@coral-xyz/anchor'; -import { assert } from 'chai'; - -import { - TestClient, - QUOTE_PRECISION, - EventSubscriber, - PRICE_PRECISION, - PositionDirection, - ZERO, - BN, - calculateAmmReservesAfterSwap, - calculatePrice, - User, - OracleSource, - SwapDirection, - Wallet, - LPRecord, - BASE_PRECISION, - getLimitOrderParams, - OracleGuardRails, - PostOnlyParams, - isVariant, - calculateBidAskPrice, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, - setFeedPriceNoProgram, - sleep, - // sleep, -} from './testHelpers'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -let lastOrderRecordsLength = 0; - -async function adjustOraclePostSwap(baa, swapDirection, market, context) { - const price = calculatePrice( - market.amm.baseAssetReserve, - market.amm.quoteAssetReserve, - market.amm.pegMultiplier - ); - - const [newQaa, newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - baa.abs(), - swapDirection - ); - - const newPrice = calculatePrice(newBaa, newQaa, market.amm.pegMultiplier); - const _newPrice = newPrice.toNumber() / PRICE_PRECISION.toNumber(); - await setFeedPriceNoProgram(context, _newPrice, market.amm.oracle); - - console.log('price => new price', price.toString(), newPrice.toString()); - - return _newPrice; -} - -async function createNewUser( - program, - context, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -) { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await context.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - context, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: context.connection.toConnection(), - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1, 2, 3], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: bulkAccountLoader - ? { - type: 'polling', - accountLoader: bulkAccountLoader, - } - : { - type: 'websocket', - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -describe('lp jit', () => { - const chProgram = anchor.workspace.Drift as Program; - - async function _viewLogs(txsig) { - bankrunContextWrapper.printTxLogs(txsig); - } - async function delay(time) { - await new Promise((resolve) => setTimeout(resolve, time)); - } - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(300).mul(BASE_PRECISION); - const ammInitialQuoteAssetReserve = new BN(300).mul(BASE_PRECISION); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - const stableAmmInitialBaseAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - - const usdcAmount = new BN(1_000_000_000 * 1e6); // 1 milli - - let driftClient: TestClient; - let eventSubscriber: EventSubscriber; - - let bulkAccountLoader: TestBulkAccountLoader; - - let bankrunContextWrapper: BankrunContextWrapper; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let poorDriftClient: TestClient; - let poorDriftClientUser: User; - - let solusdc; - let solusdc2; - let solusdc3; - let btcusdc; - - before(async () => { - const context = await startAnchor('', [], []); - - bankrunContextWrapper = new BankrunContextWrapper(context); - - bulkAccountLoader = new TestBulkAccountLoader( - bankrunContextWrapper.connection, - 'processed', - 1 - ); - - eventSubscriber = new EventSubscriber( - bankrunContextWrapper.connection.toConnection(), - chProgram - ); - - await eventSubscriber.subscribe(); - - usdcMint = await mockUSDCMint(bankrunContextWrapper); - - solusdc3 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - btcusdc = await mockOracleNoProgram(bankrunContextWrapper, 26069, -7); - - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - { publicKey: solusdc3, source: OracleSource.PYTH }, - { publicKey: btcusdc, source: OracleSource.PYTH }, - ]; - - // @ts-ignore - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // await driftClient.updateMarketBaseAssetAmountStepSize( - // new BN(0), - // new BN(1) - // ); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - // third market - await driftClient.initializePerpMarket( - 2, - solusdc3, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - - // third market - await driftClient.initializePerpMarket( - 3, - btcusdc, - stableAmmInitialBaseAssetReserve.div(new BN(1000)), - stableAmmInitialQuoteAssetReserve.div(new BN(1000)), - new BN(0), - new BN(26690 * 1000) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - // @ts-ignore - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - - // @ts-ignore - [poorDriftClient, poorDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - QUOTE_PRECISION.mul(new BN(10000)), - oracleInfos, - undefined, - bulkAccountLoader - ); - }); - - after(async () => { - await eventSubscriber.unsubscribe(); - - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - - await poorDriftClient.unsubscribe(); - await poorDriftClientUser.unsubscribe(); - }); - - const lpCooldown = 1; - it('perp jit check (amm jit intensity = 0)', async () => { - const marketIndex = 0; - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - 0, - BASE_PRECISION.toNumber() - ); - sleep(1200); - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString(), - 'target:', - market.amm.targetBaseAssetAmountPerLp - ); - assert(market.amm.sqrtK.eq(new BN('300000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - // assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('400000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '100000000000'); // 10 * 1e9 - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-12500000'))); - - // some user goes long (lp should get a short + pnl for closing long on settle) - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-25000000'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('7500000000'))); - - // add jit maker going other way - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(0.9 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(0.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(0.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - const order = traderDriftClientUser.getOrderByUserOrderId(1); - assert(!order.postOnly); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(1.011 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - const txSig = await poorDriftClient.placeAndMakePerpOrder( - makerOrderParams, - { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - } - ); - await _viewLogs(txSig); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-12500000'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('3750000000'))); - console.log( - 'market.amm.baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('1250000000'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await await driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - }); - it('perp jit check (amm jit intensity = 100)', async () => { - const marketIndex = 1; - await driftClient.updateAmmJitIntensity(marketIndex, 100); - - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - marketIndex, - BASE_PRECISION.toNumber() - ); - await delay(lpCooldown + 1000); - - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1100000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - await driftClientUser.fetchAccounts(); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '100000000000'); // 10 * 1e9 - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-4545454'))); - - // some user goes long (lp should get a short + pnl for closing long on settle) - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-9090908'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('9090909200'))); - - // add jit maker going other way - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(0.9 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(0.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(0.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - const order = traderDriftClient.getUser().getOrderByUserOrderId(1); - assert(!order.postOnly); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(1.011 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - const txSig = await poorDriftClient.placeAndMakePerpOrder( - makerOrderParams, - { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - } - ); - await _viewLogs(txSig); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-5455090'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('5204991000'))); - console.log( - 'market.amm.baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('545509000'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - await driftClientUser.fetchAccounts(); - user = await driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - // assert(settleLiquidityRecord.pnl.eq(sdkPnl)); //TODO - }); - it('perp jit check (amm jit intensity = 200)', async () => { - const marketIndex = 2; - - await driftClient.updateAmmJitIntensity(marketIndex, 200); - - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - marketIndex, - BASE_PRECISION.toNumber() - ); - sleep(1200); - - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert( - market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber(), - `targetBaseAssetAmountPerLp: ${ - market.amm.targetBaseAssetAmountPerLp - } != ${BASE_PRECISION.toNumber()}` - ); - - const _sig = await driftClient.addPerpLpShares( - new BN(100 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1100000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - await driftClientUser.fetchAccounts(); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '100000000000'); // 10 * 1e9 - - // lp goes long - const tradeSize = new BN(5 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-4545454'))); - - // some user goes long (lp should get a short + pnl for closing long on settle) - // try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - // } catch (e) { - // console.log(e); - // } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-9090908'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('9090909200'))); - - // const trader = await traderDriftClient.getUserAccount(); - // console.log( - // 'trader size', - // trader.perpPositions[0].baseAssetAmount.toString() - // ); - - for (let i = 0; i < 10; i++) { - // add jit maker going other way - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(0.9 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(0.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(0.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - // console.log(takerOrderParams); - const order = traderDriftClient.getUser().getOrderByUserOrderId(1); - // console.log(order); - - assert(!order.postOnly); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(1.011 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - // console.log('maker:', makerOrderParams); - - const txSig = await poorDriftClient.placeAndMakePerpOrder( - makerOrderParams, - { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - } - ); - await _viewLogs(txSig); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - console.log( - 'market.amm.baseAssetAmountWithUnsettledLp:', - market.amm.baseAssetAmountWithUnsettledLp.toString() - ); - - if (i == 0) { - assert(market.amm.baseAssetAmountPerLp.eq(new BN('-5227727'))); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('5227727300'))); - assert( - market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('522772700')) - ); - } - } - market = driftClient.getPerpMarketAccount(marketIndex); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('12499904'))); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('90400'))); - assert(market.amm.baseAssetAmountWithUnsettledLp.eq(new BN('-1249990400'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const sdkPnl = driftClientUser.getPerpPositionWithLPSettle(0)[2]; - - console.log('settling...'); - try { - const _txsigg = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = await driftClientUser.getUserAccount(); - const orderRecords = eventSubscriber.getEventsArray('OrderActionRecord'); - - const matchOrderRecord = orderRecords[1]; - assert( - isVariant(matchOrderRecord.actionExplanation, 'orderFilledWithMatchJit') - ); - assert(matchOrderRecord.baseAssetAmountFilled.toString(), '3750000000'); - assert(matchOrderRecord.quoteAssetAmountFilled.toString(), '3791212'); - - const jitOrderRecord = orderRecords[2]; - assert(isVariant(jitOrderRecord.actionExplanation, 'orderFilledWithLpJit')); - assert(jitOrderRecord.baseAssetAmountFilled.toString(), '1250000000'); - assert(jitOrderRecord.quoteAssetAmountFilled.toString(), '1263738'); - - // console.log('len of orderRecords', orderRecords.length); - lastOrderRecordsLength = orderRecords.length; - - // Convert the array to a JSON string - // const fs = require('fs'); - // // Custom replacer function to convert BN values to numerical representation - // const replacer = (key, value) => { - // if (value instanceof BN) { - // return value.toString(10); // Convert BN to base-10 string - // } - // return value; - // }; - // const jsonOrderRecords = JSON.stringify(orderRecords, replacer); - - // // Write the JSON string to a file - // fs.writeFile('orderRecords.json', jsonOrderRecords, 'utf8', (err) => { - // if (err) { - // console.error('Error writing to JSON file:', err); - // return; - // } - // console.log('orderRecords successfully written to orderRecords.json'); - // }); - - // assert(orderRecords) - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - // assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - }); - it('perp jit check BTC inout (amm jit intensity = 200)', async () => { - const marketIndex = 3; - - await driftClient.updateAmmJitIntensity(marketIndex, 200); - await driftClient.updatePerpMarketCurveUpdateIntensity(marketIndex, 100); - await driftClient.updatePerpMarketMaxSpread(marketIndex, 100000); - await driftClient.updatePerpMarketBaseSpread(marketIndex, 10000); - sleep(1200); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('1000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == 0); - - console.log('adding liquidity...'); - const _sig = await driftClient.addPerpLpShares( - BASE_PRECISION, - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('2000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - let [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - let perpy = await driftClientUser.getPerpPosition(marketIndex); - - assert(perpy.lpShares.toString() == '1000000000'); // 1e9 - console.log( - 'user.perpPositions[0].baseAssetAmount:', - perpy.baseAssetAmount.toString() - ); - assert(perpy.baseAssetAmount.toString() == '0'); // no fills - - // trader goes long - const tradeSize = BASE_PRECISION.div(new BN(20)); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - perpy = await driftClientUser.getPerpPosition(marketIndex); - assert(perpy.baseAssetAmount.toString() == '0'); // unsettled - - await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - marketIndex - ); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - perpy = await driftClientUser.getPerpPosition(marketIndex); - console.log('perpy.baseAssetAmount:', perpy.baseAssetAmount.toString()); - assert(perpy.baseAssetAmount.toString() == '-10000000'); // settled - - [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - const takerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize, - price: new BN(26000 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(26400.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(26000.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams); - await traderDriftClient.fetchAccounts(); - // console.log(takerOrderParams); - // const order = traderDriftClientUser.getOrderByUserOrderId(1); - - const makerOrderParams = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize, - price: new BN(26488.88 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - await poorDriftClient.placeAndMakePerpOrder(makerOrderParams, { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - }); - - await driftClient.fetchAccounts(); - const marketAfter = driftClient.getPerpMarketAccount(marketIndex); - const orderRecords = eventSubscriber.getEventsArray('OrderActionRecord'); - - console.log('len of orderRecords', orderRecords.length); - assert(orderRecords.length - lastOrderRecordsLength == 7); - lastOrderRecordsLength = orderRecords.length; - // Convert the array to a JSON string - - // console.log(marketAfter); - console.log(marketAfter.amm.baseAssetAmountPerLp.toString()); - console.log(marketAfter.amm.quoteAssetAmountPerLp.toString()); - console.log(marketAfter.amm.baseAssetAmountWithUnsettledLp.toString()); - console.log(marketAfter.amm.baseAssetAmountWithAmm.toString()); - - assert(marketAfter.amm.baseAssetAmountPerLp.eq(new BN(-5000000))); - assert(marketAfter.amm.quoteAssetAmountPerLp.eq(new BN(144606790 - 1))); - assert(marketAfter.amm.baseAssetAmountWithUnsettledLp.eq(new BN(-5000000))); - assert(marketAfter.amm.baseAssetAmountWithAmm.eq(new BN(5000000))); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPos = driftClientUser.getPerpPosition(marketIndex); - console.log(perpPos.baseAssetAmount.toString()); - assert(perpPos.baseAssetAmount.toString() == '-10000000'); - - const [settledPos, dustPos, lpPnl] = - driftClientUser.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - ); - // console.log('settlePos:', settledPos); - console.log('dustPos:', dustPos.toString()); - console.log('lpPnl:', lpPnl.toString()); - - assert(dustPos.toString() == '0'); - assert(lpPnl.toString() == '6134171'); - - const _sig2 = await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - marketIndex - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPosAfter = driftClientUser.getPerpPosition(marketIndex); - console.log( - 'perpPosAfter.baseAssetAmount:', - perpPosAfter.baseAssetAmount.toString() - ); - assert(perpPosAfter.baseAssetAmount.toString() == '-5000000'); - assert(perpPosAfter.baseAssetAmount.eq(settledPos.baseAssetAmount)); - - const takerOrderParams2 = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.SHORT, - baseAssetAmount: tradeSize.mul(new BN(20)), - price: new BN(26000 * PRICE_PRECISION.toNumber()), - auctionStartPrice: new BN(26400.99 * PRICE_PRECISION.toNumber()), - auctionEndPrice: new BN(26000.929 * PRICE_PRECISION.toNumber()), - auctionDuration: 10, - userOrderId: 1, - postOnly: PostOnlyParams.NONE, - }); - await traderDriftClient.placePerpOrder(takerOrderParams2); - await traderDriftClient.fetchAccounts(); - // console.log(takerOrderParams); - // const order = traderDriftClientUser.getOrderByUserOrderId(1); - - const makerOrderParams2 = getLimitOrderParams({ - marketIndex, - direction: PositionDirection.LONG, - baseAssetAmount: tradeSize.mul(new BN(20)), - price: new BN(26488.88 * PRICE_PRECISION.toNumber()), - userOrderId: 1, - postOnly: PostOnlyParams.MUST_POST_ONLY, - bitFlags: true, - }); - - [bid, ask] = calculateBidAskPrice( - driftClient.getPerpMarketAccount(marketIndex).amm, - driftClient.getOracleDataForPerpMarket(marketIndex) - ); - console.log(bid.toString(), '/', ask.toString()); - console.log('bid:', bid.toString()); - console.log('ask:', ask.toString()); - - await poorDriftClient.placeAndMakePerpOrder(makerOrderParams2, { - taker: await traderDriftClient.getUserAccountPublicKey(), - order: traderDriftClient.getOrderByUserId(1), - takerUserAccount: traderDriftClient.getUserAccount(), - takerStats: traderDriftClient.getUserStatsAccountPublicKey(), - }); - const marketAfter2 = driftClient.getPerpMarketAccount(marketIndex); - - console.log(marketAfter2.amm.baseAssetAmountPerLp.toString()); - console.log(marketAfter2.amm.quoteAssetAmountPerLp.toString()); - console.log(marketAfter2.amm.baseAssetAmountWithUnsettledLp.toString()); - console.log(marketAfter2.amm.baseAssetAmountWithAmm.toString()); - - assert(marketAfter2.amm.baseAssetAmountPerLp.eq(new BN(-2500000))); - assert(marketAfter2.amm.quoteAssetAmountPerLp.eq(new BN(78437566))); - assert( - marketAfter2.amm.baseAssetAmountWithUnsettledLp.eq(new BN(-2500000)) - ); - assert(marketAfter2.amm.baseAssetAmountWithAmm.eq(new BN(2500000))); - - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPos2 = driftClientUser.getPerpPosition(marketIndex); - console.log(perpPos2.baseAssetAmount.toString()); - assert(perpPos2.baseAssetAmount.toString() == '-5000000'); - - const [settledPos2, dustPos2, lpPnl2] = - driftClientUser.getPerpPositionWithLPSettle( - marketIndex, - undefined, - false, - true - ); - // console.log('settlePos:', settledPos2); - console.log('dustPos:', dustPos2.toString()); - console.log('lpPnl:', lpPnl2.toString()); - - assert(dustPos2.toString() == '0'); - assert(lpPnl2.toString() == '3067086'); - - await driftClient.settleLP( - await driftClient.getUserAccountPublicKey(), - marketIndex - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - const perpPosAfter2 = driftClientUser.getPerpPosition(marketIndex); - console.log( - 'perpPosAfter2.baseAssetAmount:', - perpPosAfter2.baseAssetAmount.toString() - ); - assert(perpPosAfter2.baseAssetAmount.toString() == '-2500000'); - assert(perpPosAfter2.baseAssetAmount.eq(settledPos2.baseAssetAmount)); - - const orderRecords2 = eventSubscriber.getEventsArray('OrderActionRecord'); - console.log('len of orderRecords', orderRecords2.length); - // assert(orderRecords.length - lastOrderRecordsLength == 7); - lastOrderRecordsLength = orderRecords2.length; - - // const fs = require('fs'); - // // Custom replacer function to convert BN values to numerical representation - // const replacer = (key, value) => { - // if (value instanceof BN) { - // return value.toString(10); // Convert BN to base-10 string - // } - // return value; - // }; - // const jsonOrderRecords2 = JSON.stringify(orderRecords2, replacer); - - // // Write the JSON string to a file - // fs.writeFile('orderRecords.json', jsonOrderRecords2, 'utf8', (err) => { - // if (err) { - // console.error('Error writing to JSON file:', err); - // return; - // } - // console.log('orderRecords successfully written to orderRecords.json'); - // }); - }); -}); diff --git a/tests/perpLpRiskMitigation.ts b/tests/perpLpRiskMitigation.ts deleted file mode 100644 index 12b8037f31..0000000000 --- a/tests/perpLpRiskMitigation.ts +++ /dev/null @@ -1,537 +0,0 @@ -import * as web3 from '@solana/web3.js'; -import * as anchor from '@coral-xyz/anchor'; -import { Program } from '@coral-xyz/anchor'; -import { assert } from 'chai'; - -import { - TestClient, - QUOTE_PRECISION, - EventSubscriber, - PRICE_PRECISION, - PositionDirection, - ZERO, - BN, - calculateAmmReservesAfterSwap, - calculatePrice, - User, - OracleSource, - SwapDirection, - Wallet, - LPRecord, - BASE_PRECISION, - OracleGuardRails, - isVariant, - MARGIN_PRECISION, - SettlePnlMode, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, - setFeedPriceNoProgram, - sleep, - // sleep, -} from './testHelpers'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -async function adjustOraclePostSwap(baa, swapDirection, market, context) { - const price = calculatePrice( - market.amm.baseAssetReserve, - market.amm.quoteAssetReserve, - market.amm.pegMultiplier - ); - - const [newQaa, newBaa] = calculateAmmReservesAfterSwap( - market.amm, - 'base', - baa.abs(), - swapDirection - ); - - const newPrice = calculatePrice(newBaa, newQaa, market.amm.pegMultiplier); - const _newPrice = newPrice.toNumber() / PRICE_PRECISION.toNumber(); - await setFeedPriceNoProgram(context, _newPrice, market.amm.oracle); - - console.log('price => new price', price.toString(), newPrice.toString()); - - return _newPrice; -} - -async function createNewUser( - program, - context: BankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -): Promise<[TestClient, User]> { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await context.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - context, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: context.connection.toConnection(), - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1, 2, 3], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -describe('lp risk mitigation', () => { - const chProgram = anchor.workspace.Drift as Program; - - async function _viewLogs(txsig) { - bankrunContextWrapper.printTxLogs(txsig); - } - async function delay(time) { - await new Promise((resolve) => setTimeout(resolve, time)); - } - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(10000).mul(BASE_PRECISION); - const ammInitialQuoteAssetReserve = new BN(10000).mul(BASE_PRECISION); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - const stableAmmInitialBaseAssetReserve = - BASE_PRECISION.mul(mantissaSqrtScale); - - const usdcAmount = new BN(5000 * 1e6); // 2000 bucks - - let driftClient: TestClient; - let eventSubscriber: EventSubscriber; - - let bulkAccountLoader: TestBulkAccountLoader; - - let bankrunContextWrapper: BankrunContextWrapper; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let poorDriftClient: TestClient; - let poorDriftClientUser: User; - - let solusdc; - let solusdc2; - let solusdc3; - let btcusdc; - - before(async () => { - const context = await startAnchor('', [], []); - - bankrunContextWrapper = new BankrunContextWrapper(context); - - bulkAccountLoader = new TestBulkAccountLoader( - bankrunContextWrapper.connection, - 'processed', - 1 - ); - - eventSubscriber = new EventSubscriber( - bankrunContextWrapper.connection.toConnection(), - chProgram - ); - - await eventSubscriber.subscribe(); - - usdcMint = await mockUSDCMint(bankrunContextWrapper); - - solusdc3 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - btcusdc = await mockOracleNoProgram(bankrunContextWrapper, 26069, -7); - - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - { publicKey: solusdc3, source: OracleSource.PYTH }, - { publicKey: btcusdc, source: OracleSource.PYTH }, - ]; - - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // await driftClient.updateMarketBaseAssetAmountStepSize( - // new BN(0), - // new BN(1) - // ); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - // third market - await driftClient.initializePerpMarket( - 2, - solusdc3, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - - // third market - await driftClient.initializePerpMarket( - 3, - btcusdc, - stableAmmInitialBaseAssetReserve.div(new BN(1000)), - stableAmmInitialQuoteAssetReserve.div(new BN(1000)), - new BN(0), - new BN(26690 * 1000) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpAuctionDuration(new BN(0)); - - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - await traderDriftClient.updateUserAdvancedLp([ - { - advancedLp: true, - subAccountId: 0, - }, - ]); - [poorDriftClient, poorDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - QUOTE_PRECISION.mul(new BN(10000)), - oracleInfos, - undefined, - bulkAccountLoader - ); - await poorDriftClient.updateUserAdvancedLp([ - { - advancedLp: true, - subAccountId: 0, - }, - ]); - }); - - after(async () => { - await eventSubscriber.unsubscribe(); - - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - - await poorDriftClient.unsubscribe(); - await poorDriftClientUser.unsubscribe(); - }); - - const lpCooldown = 1; - it('perp risk mitigation', async () => { - const marketIndex = 0; - console.log('adding liquidity...'); - await driftClient.updatePerpMarketTargetBaseAssetAmountPerLp( - marketIndex, - BASE_PRECISION.toNumber() - ); - sleep(1200); - await driftClient.fetchAccounts(); - let market = driftClient.getPerpMarketAccount(marketIndex); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString(), - 'target:', - market.amm.targetBaseAssetAmountPerLp - ); - assert(market.amm.sqrtK.eq(new BN('10000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - // assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - const _sig = await driftClient.addPerpLpShares( - new BN(1000 * BASE_PRECISION.toNumber()), - market.marketIndex - ); - await delay(lpCooldown + 1000); - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.sqrtK:', - market.amm.userLpShares.toString(), - '/', - market.amm.sqrtK.toString() - ); - assert(market.amm.sqrtK.eq(new BN('11000000000000'))); - assert(market.amm.baseAssetAmountPerLp.eq(ZERO)); - assert(market.amm.targetBaseAssetAmountPerLp == BASE_PRECISION.toNumber()); - - let user = await driftClientUser.getUserAccount(); - assert(user.perpPositions[0].lpShares.toString() == '1000000000000'); // 1000 * 1e9 - - // lp goes short - const tradeSize = new BN(500 * BASE_PRECISION.toNumber()); - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await driftClient.openPosition( - PositionDirection.SHORT, - tradeSize, - market.marketIndex, - new BN(0.1 * PRICE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('45454545'))); - await driftClientUser.fetchAccounts(); - await driftClient.accountSubscriber.setSpotOracleMap(); - - console.log( - 'driftClientUser.getFreeCollateral()=', - driftClientUser.getFreeCollateral().toString() - ); - assert(driftClientUser.getFreeCollateral().eq(new BN('4761073360'))); - // some user goes long (lp should get more short) - try { - await adjustOraclePostSwap( - tradeSize, - SwapDirection.REMOVE, - market, - bankrunContextWrapper - ); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - // new BN(100 * BASE_PRECISION.toNumber()) - ); - await _viewLogs(_txsig); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - market = driftClient.getPerpMarketAccount(0); - console.log( - 'market.amm.baseAssetAmountPerLp:', - market.amm.baseAssetAmountPerLp.toString() - ); - assert(market.amm.baseAssetAmountPerLp.eq(new BN('0'))); - console.log( - 'market.amm.baseAssetAmountWithAmm:', - market.amm.baseAssetAmountWithAmm.toString() - ); - assert(market.amm.baseAssetAmountWithAmm.eq(new BN('0'))); - - const trader = await traderDriftClient.getUserAccount(); - console.log( - 'trader size', - trader.perpPositions[0].baseAssetAmount.toString() - ); - - await driftClientUser.fetchAccounts(); - const [userPos, dustBase, sdkPnl] = - driftClientUser.getPerpPositionWithLPSettle(0); - - console.log('baseAssetAmount:', userPos.baseAssetAmount.toString()); - console.log('dustBase:', dustBase.toString()); - - console.log('settling...'); - try { - const _txsigg = await driftClient.settlePNL( - await driftClient.getUserAccountPublicKey(), - await driftClient.getUserAccount(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - user = driftClientUser.getUserAccount(); - - const settleLiquidityRecord: LPRecord = - eventSubscriber.getEventsArray('LPRecord')[0]; - - console.log( - 'settle pnl vs sdk', - settleLiquidityRecord.pnl.toString(), - sdkPnl.toString() - ); - assert(settleLiquidityRecord.pnl.eq(sdkPnl)); - - const perpLiqPrice = driftClientUser.liquidationPrice(0); - console.log('perpLiqPrice:', perpLiqPrice.toString()); - - await setFeedPriceNoProgram(bankrunContextWrapper, 8, solusdc); - console.log('settling...'); - try { - const _txsigg = await driftClient.settlePNL( - await driftClient.getUserAccountPublicKey(), - await driftClient.getUserAccount(), - 0 - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - - await driftClient.updateUserCustomMarginRatio([ - { - marginRatio: MARGIN_PRECISION.toNumber(), - subAccountId: 0, - }, - ]); - - await sleep(1000); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - console.log( - 'driftClientUser.getUserAccount().openOrders=', - driftClientUser.getUserAccount().openOrders - ); - assert(driftClientUser.getUserAccount().openOrders == 0); - - console.log('settling after margin ratio update...'); - try { - const _txsigg = await driftClient.settleMultiplePNLs( - await driftClient.getUserAccountPublicKey(), - await driftClient.getUserAccount(), - [0], - SettlePnlMode.TRY_SETTLE - ); - await _viewLogs(_txsigg); - } catch (e) { - console.log(e); - } - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - const afterReduceOrdersAccount = driftClientUser.getUserAccount(); - assert(afterReduceOrdersAccount.openOrders == 1); - - const leOrder = afterReduceOrdersAccount.orders[0]; - console.log(leOrder); - assert(leOrder.auctionDuration == 80); - assert(leOrder.auctionStartPrice.lt(leOrder.auctionEndPrice)); - assert(leOrder.auctionEndPrice.gt(ZERO)); - assert(leOrder.reduceOnly); - assert(!leOrder.postOnly); - assert(leOrder.marketIndex == 0); - assert(leOrder.baseAssetAmount.eq(new BN('500000000000'))); - assert(isVariant(leOrder.direction, 'long')); - assert(isVariant(leOrder.existingPositionDirection, 'short')); - - const afterReduceShares = - afterReduceOrdersAccount.perpPositions[0].lpShares; - - console.log('afterReduceShares=', afterReduceShares.toString()); - assert(afterReduceShares.lt(new BN(1000 * BASE_PRECISION.toNumber()))); - assert(afterReduceShares.eq(new BN('400000000000'))); - }); -}); diff --git a/tests/tradingLP.ts b/tests/tradingLP.ts deleted file mode 100644 index 565178183e..0000000000 --- a/tests/tradingLP.ts +++ /dev/null @@ -1,281 +0,0 @@ -import * as anchor from '@coral-xyz/anchor'; -import { assert } from 'chai'; -import { BN, User, OracleSource, Wallet, MARGIN_PRECISION } from '../sdk'; - -import { Program } from '@coral-xyz/anchor'; - -import * as web3 from '@solana/web3.js'; - -import { - TestClient, - PRICE_PRECISION, - PositionDirection, - ZERO, - OracleGuardRails, -} from '../sdk/src'; - -import { - initializeQuoteSpotMarket, - mockOracleNoProgram, - mockUSDCMint, - mockUserUSDCAccount, -} from './testHelpers'; -import { startAnchor } from 'solana-bankrun'; -import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; -import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; - -async function createNewUser( - program, - context: BankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - wallet, - bulkAccountLoader -) { - let walletFlag = true; - if (wallet == undefined) { - const kp = new web3.Keypair(); - await context.fundKeypair(kp, 10 ** 9); - wallet = new Wallet(kp); - walletFlag = false; - } - - console.log('wallet:', walletFlag); - const usdcAta = await mockUserUSDCAccount( - usdcMint, - usdcAmount, - context, - wallet.publicKey - ); - - const driftClient = new TestClient({ - connection: context.connection.toConnection(), - wallet: wallet, - programID: program.programId, - opts: { - commitment: 'confirmed', - }, - activeSubAccountId: 0, - perpMarketIndexes: [0, 1], - spotMarketIndexes: [0], - subAccountIds: [], - oracleInfos, - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - - if (walletFlag) { - await driftClient.initialize(usdcMint.publicKey, true); - await driftClient.subscribe(); - await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); - } else { - await driftClient.subscribe(); - } - - await driftClient.initializeUserAccountAndDepositCollateral( - usdcAmount, - usdcAta.publicKey - ); - - const driftClientUser = new User({ - // @ts-ignore - driftClient, - userAccountPublicKey: await driftClient.getUserAccountPublicKey(), - accountSubscription: { - type: 'polling', - accountLoader: bulkAccountLoader, - }, - }); - driftClientUser.subscribe(); - - return [driftClient, driftClientUser]; -} - -describe('trading liquidity providing', () => { - const chProgram = anchor.workspace.Drift as Program; - - // ammInvariant == k == x * y - const ammInitialBaseAssetReserve = new BN(300).mul(new BN(1e13)); - const ammInitialQuoteAssetReserve = new BN(300).mul(new BN(1e13)); - - const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); - const stableAmmInitialQuoteAssetReserve = new anchor.BN(1 * 10 ** 13).mul( - mantissaSqrtScale - ); - const stableAmmInitialBaseAssetReserve = new anchor.BN(1 * 10 ** 13).mul( - mantissaSqrtScale - ); - - const usdcAmount = new BN(1_000_000_000 * 1e6); - - let driftClient: TestClient; - - let bulkAccountLoader: TestBulkAccountLoader; - - let bankrunContextWrapper: BankrunContextWrapper; - - let usdcMint: web3.Keypair; - - let driftClientUser: User; - let traderDriftClient: TestClient; - let traderDriftClientUser: User; - - let solusdc; - let solusdc2; - - before(async () => { - const context = await startAnchor('', [], []); - - bankrunContextWrapper = new BankrunContextWrapper(context); - - bulkAccountLoader = new TestBulkAccountLoader( - bankrunContextWrapper.connection, - 'processed', - 1 - ); - - usdcMint = await mockUSDCMint(bankrunContextWrapper); - - solusdc2 = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - solusdc = await mockOracleNoProgram(bankrunContextWrapper, 1, -7); // make invalid - const oracleInfos = [ - { publicKey: solusdc, source: OracleSource.PYTH }, - { publicKey: solusdc2, source: OracleSource.PYTH }, - ]; - [driftClient, driftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - bankrunContextWrapper.provider.wallet, - bulkAccountLoader - ); - // used for trading / taking on baa - await driftClient.initializePerpMarket( - 0, - solusdc, - ammInitialBaseAssetReserve, - ammInitialQuoteAssetReserve, - new BN(60 * 60) - ); - await driftClient.updateLpCooldownTime(new BN(0)); - await driftClient.updatePerpMarketMaxFillReserveFraction(0, 1); - await driftClient.updatePerpMarketStepSizeAndTickSize( - 0, - new BN(1), - new BN(1) - ); - const oracleGuardRails: OracleGuardRails = { - priceDivergence: { - markOraclePercentDivergence: new BN(1000000), - oracleTwap5MinPercentDivergence: new BN(1000000), - }, - validity: { - slotsBeforeStaleForAmm: new BN(10), - slotsBeforeStaleForMargin: new BN(10), - confidenceIntervalMaxSize: new BN(100), - tooVolatileRatio: new BN(100), - }, - }; - await driftClient.updateOracleGuardRails(oracleGuardRails); - - // second market -- used for funding .. - await driftClient.initializePerpMarket( - 1, - solusdc2, - stableAmmInitialBaseAssetReserve, - stableAmmInitialQuoteAssetReserve, - new BN(0) - ); - await driftClient.updatePerpAuctionDuration(new BN(0)); - await driftClient.updatePerpMarketMarginRatio( - 0, - MARGIN_PRECISION.toNumber() / 2, - MARGIN_PRECISION.toNumber() / 4 - ); - - [traderDriftClient, traderDriftClientUser] = await createNewUser( - chProgram, - bankrunContextWrapper, - usdcMint, - usdcAmount, - oracleInfos, - undefined, - bulkAccountLoader - ); - }); - - after(async () => { - await driftClient.unsubscribe(); - await driftClientUser.unsubscribe(); - - await traderDriftClient.unsubscribe(); - await traderDriftClientUser.unsubscribe(); - }); - - it('lp trades with short', async () => { - let market = driftClient.getPerpMarketAccount(0); - - console.log('adding liquidity...'); - const _sig = await driftClient.addPerpLpShares( - new BN(100 * 1e13), - market.marketIndex - ); - - // some user goes long (lp should get a short) - console.log('user trading...'); - const tradeSize = new BN(40 * 1e13); - const _txsig = await traderDriftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - ); - - await traderDriftClient.fetchAccounts(); - const position = traderDriftClient.getUserAccount().perpPositions[0]; - console.log( - 'trader position:', - position.baseAssetAmount.toString(), - position.quoteAssetAmount.toString() - ); - assert(position.baseAssetAmount.gt(ZERO)); - - // settle says the lp would take on a short - const lpPosition = driftClientUser.getPerpPositionWithLPSettle(0)[0]; - console.log( - 'sdk settled lp position:', - lpPosition.baseAssetAmount.toString(), - lpPosition.quoteAssetAmount.toString() - ); - assert(lpPosition.baseAssetAmount.lt(ZERO)); - assert(lpPosition.quoteAssetAmount.gt(ZERO)); - - // lp trades a big long - await driftClient.openPosition( - PositionDirection.LONG, - tradeSize, - market.marketIndex - ); - await driftClient.fetchAccounts(); - await driftClientUser.fetchAccounts(); - - // lp now has a long - const newLpPosition = driftClientUser.getUserAccount().perpPositions[0]; - console.log( - 'lp position:', - newLpPosition.baseAssetAmount.toString(), - newLpPosition.quoteAssetAmount.toString() - ); - assert(newLpPosition.baseAssetAmount.gt(ZERO)); - assert(newLpPosition.quoteAssetAmount.lt(ZERO)); - // is still an lp - assert(newLpPosition.lpShares.gt(ZERO)); - market = driftClient.getPerpMarketAccount(0); - - console.log('done!'); - }); -}); From b58cda0037f22098bc6c7c84e7d336e8c7c311aa Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 20 Jul 2025 18:16:59 -0400 Subject: [PATCH 06/91] 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 07/91] 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 08/91] 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 09/91] 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 10/91] 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 11/91] 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 12/91] 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 13/91] 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 14/91] 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 15/91] 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 16/91] 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 17/91] 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 18/91] 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 19/91] 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 20/91] 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 21/91] 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 22/91] 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 23/91] 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 24/91] 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 25/91] 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 26/91] 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 27/91] 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 28/91] 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 29/91] 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 30/91] 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 31/91] 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 32/91] 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 33/91] 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 34/91] 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 35/91] 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 36/91] 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); +} From 2821269940ea777112e687574120c2d894799d68 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 16:06:03 -0400 Subject: [PATCH 37/91] fix cancel orders --- programs/drift/src/controller/liquidation.rs | 12 ++++++------ programs/drift/src/instructions/user.rs | 2 +- programs/drift/src/state/liquidation_mode.rs | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index d02307ce86..7c4ed9bae8 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -194,7 +194,7 @@ pub fn liquidate_perp( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - let (cancel_order_market_type, cancel_order_market_index, cancel_order_skip_isolated_positions) = + let (cancel_order_market_type, cancel_order_market_index) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, @@ -209,7 +209,7 @@ pub fn liquidate_perp( cancel_order_market_type, cancel_order_market_index, None, - cancel_order_skip_isolated_positions, + true, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -832,7 +832,7 @@ pub fn liquidate_perp_with_fill( ErrorCode::PositionDoesntHaveOpenPositionOrOrders )?; - let (cancel_orders_market_type, cancel_orders_market_index, cancel_orders_is_isolated) = + let (cancel_orders_market_type, cancel_orders_market_index) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( &mut user, @@ -847,7 +847,7 @@ pub fn liquidate_perp_with_fill( cancel_orders_market_type, cancel_orders_market_index, None, - cancel_orders_is_isolated, + true, )?; let mut market = perp_market_map.get_ref_mut(&market_index)?; @@ -3007,7 +3007,7 @@ 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) = + let (cancel_orders_market_type, cancel_orders_market_index) = liquidation_mode.get_cancel_orders_params(); let canceled_order_ids = orders::cancel_orders( user, @@ -3022,7 +3022,7 @@ pub fn liquidate_perp_pnl_for_deposit( cancel_orders_market_type, cancel_orders_market_index, None, - cancel_orders_is_isolated, + true, )?; let (safest_tier_spot_liability, safest_tier_perp_liability) = liquidation_mode diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 82eee452b1..acc7b31f63 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2607,7 +2607,7 @@ pub fn handle_cancel_orders<'c: 'info, 'info>( market_type, market_index, direction, - true, + false, )?; Ok(()) diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 662db067ee..3889c5a1d1 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -37,7 +37,7 @@ pub trait LiquidatePerpMode { fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; - fn get_cancel_orders_params(&self) -> (Option, Option, bool); + fn get_cancel_orders_params(&self) -> (Option, Option); fn calculate_max_pct_to_liquidate( &self, @@ -129,8 +129,8 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(user.exit_cross_margin_liquidation()) } - fn get_cancel_orders_params(&self) -> (Option, Option, bool) { - (None, None, true) + fn get_cancel_orders_params(&self) -> (Option, Option) { + (None, None) } fn calculate_max_pct_to_liquidate( @@ -280,8 +280,8 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { 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 get_cancel_orders_params(&self) -> (Option, Option) { + (Some(MarketType::Perp), Some(self.market_index)) } fn calculate_max_pct_to_liquidate( From 424987fdc6d6620a4199e630080553f318683359 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 17:16:14 -0400 Subject: [PATCH 38/91] fix set liquidation status --- programs/drift/src/controller/liquidation.rs | 25 ++++---------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 7c4ed9bae8..67237af660 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3740,26 +3740,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::>(); - - // 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)?; - // } - - // } + for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_position_margin_calculation.iter() { + if !user.is_isolated_position_being_liquidated(*market_index)? && !isolated_position_margin_calculation.meets_margin_requirement() { + user.enter_isolated_position_liquidation(*market_index)?; + } + } Ok(()) } From b84daf15f9c7e692a595fdcc58321c1d13c7b81b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 18:14:39 -0400 Subject: [PATCH 39/91] more tweaks --- programs/drift/src/controller/liquidation.rs | 68 +++++++------------ programs/drift/src/state/liquidation_mode.rs | 12 +++- .../drift/src/state/margin_calculation.rs | 34 +++++----- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 67237af660..7f8af63d1e 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -107,7 +107,7 @@ pub fn liquidate_perp( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -184,7 +184,7 @@ pub fn liquidate_perp( e })?; - let liquidation_id = user.enter_cross_margin_liquidation(slot)?; + let liquidation_id = liquidation_mode.enter_liquidation(user, slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -264,7 +264,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -563,12 +563,12 @@ pub fn liquidate_perp( 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); + liquidation_mode.increment_free_margin(user, margin_freed_for_perp_position)?; if base_asset_amount >= base_asset_amount_to_cover_margin_shortage { liquidation_mode.exit_liquidation(user)?; } else if liquidation_mode.should_user_enter_bankruptcy(user)? { - liquidation_mode.enter_bankruptcy(user); + liquidation_mode.enter_bankruptcy(user)?; } let liquidator_meets_initial_margin_requirement = @@ -698,7 +698,7 @@ pub fn liquidate_perp( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -762,7 +762,7 @@ pub fn liquidate_perp_with_fill( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -822,7 +822,7 @@ pub fn liquidate_perp_with_fill( e })?; - let liquidation_id = user.enter_cross_margin_liquidation(slot)?; + let liquidation_id = liquidation_mode.enter_liquidation(&mut user, slot)?; let mut margin_freed = 0_u64; let position_index = get_position_index(&user.perp_positions, market_index)?; @@ -889,7 +889,7 @@ pub fn liquidate_perp_with_fill( margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) .cast::()?; - liquidation_mode.increment_free_margin(&mut user, margin_freed); + 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) = @@ -902,7 +902,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1132,7 +1132,7 @@ pub fn liquidate_perp_with_fill( )?; margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; - liquidation_mode.increment_free_margin(&mut 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(&mut user)?; @@ -1156,7 +1156,7 @@ pub fn liquidate_perp_with_fill( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp: LiquidatePerpRecord { @@ -1207,7 +1207,7 @@ pub fn liquidate_spot( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -1791,7 +1791,7 @@ pub fn liquidate_spot_with_swap_begin( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2343,7 +2343,7 @@ pub fn liquidate_borrow_for_perp_pnl( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -2827,7 +2827,7 @@ pub fn liquidate_perp_pnl_for_deposit( )?; validate!( - !liquidator.is_cross_margin_bankrupt(), + !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, "liquidator bankrupt", )?; @@ -3004,7 +3004,7 @@ pub fn liquidate_perp_pnl_for_deposit( return Ok(()); } - let liquidation_id = user.enter_cross_margin_liquidation(slot)?; + let liquidation_id = liquidation_mode.enter_liquidation(user, slot)?; let mut margin_freed = 0_u64; let (cancel_orders_market_type, cancel_orders_market_index) = @@ -3048,7 +3048,7 @@ pub fn liquidate_perp_pnl_for_deposit( 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)?; let exiting_liq_territory = liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)?; @@ -3067,7 +3067,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, canceled_order_ids, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { @@ -3244,7 +3244,7 @@ pub fn liquidate_perp_pnl_for_deposit( 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); + liquidation_mode.increment_free_margin(user, margin_freed_from_liability)?; if pnl_transfer >= pnl_transfer_to_cover_margin_shortage { liquidation_mode.exit_liquidation(user)?; @@ -3276,7 +3276,7 @@ pub fn liquidate_perp_pnl_for_deposit( liquidator: *liquidator_key, margin_requirement, total_collateral, - bankrupt: user.is_cross_margin_bankrupt(), + bankrupt: liquidation_mode.is_user_bankrupt(&user)?, margin_freed, liquidate_perp_pnl_for_deposit: LiquidatePerpPnlForDepositRecord { perp_market_index, @@ -3320,17 +3320,11 @@ pub fn resolve_perp_bankruptcy( )?; validate!( - !liquidator.is_cross_margin_being_liquidated(), + !liquidator.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; - validate!( - !liquidator.is_cross_margin_bankrupt(), - ErrorCode::UserBankrupt, - "liquidator bankrupt", - )?; - let market = perp_market_map.get_ref(&market_index)?; validate!( @@ -3544,17 +3538,11 @@ pub fn resolve_spot_bankruptcy( )?; validate!( - !liquidator.is_cross_margin_being_liquidated(), + !liquidator.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "liquidator being liquidated", )?; - validate!( - !liquidator.is_cross_margin_bankrupt(), - ErrorCode::UserBankrupt, - "liquidator bankrupt", - )?; - let market = spot_market_map.get_ref(&market_index)?; validate!( @@ -3715,13 +3703,7 @@ pub fn set_user_status_to_being_liquidated( state: &State, ) -> DriftResult { validate!( - !user.is_cross_margin_bankrupt(), - ErrorCode::UserBankrupt, - "user bankrupt", - )?; - - validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, "user is already being liquidated", )?; @@ -3740,7 +3722,7 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_position_margin_calculation.iter() { + for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { if !user.is_isolated_position_being_liquidated(*market_index)? && !isolated_position_margin_calculation.meets_margin_requirement() { user.enter_isolated_position_liquidation(*market_index)?; } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 3889c5a1d1..aa98aa99cc 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -33,6 +33,8 @@ pub trait LiquidatePerpMode { margin_calculation: &MarginCalculation, ) -> DriftResult; + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult; + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult; fn exit_liquidation(&self, user: &mut User) -> DriftResult<()>; @@ -121,6 +123,10 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { Ok(margin_calculation.cross_margin_meets_margin_requirement()) } + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { + user.enter_cross_margin_liquidation(slot) + } + fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { Ok(margin_calculation.cross_margin_can_exit_liquidation()?) } @@ -276,6 +282,10 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { margin_calculation.isolated_position_can_exit_liquidation(self.market_index) } + fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { + user.enter_isolated_position_liquidation(self.market_index) + } + fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { user.exit_isolated_position_liquidation(self.market_index) } @@ -320,7 +330,7 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { margin_calculation: &MarginCalculation, ) -> DriftResult<(u128, i128, u8)> { let isolated_position_margin_calculation = margin_calculation - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&self.market_index) .safe_unwrap()?; Ok(( diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index b55e11fed3..9491e52378 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -172,7 +172,7 @@ pub struct MarginCalculation { margin_requirement_plus_buffer: u128, #[cfg(test)] pub margin_requirement_plus_buffer: u128, - pub isolated_position_margin_calculation: BTreeMap, + pub isolated_margin_calculations: BTreeMap, pub num_spot_liabilities: u8, pub num_perp_liabilities: u8, pub all_deposit_oracles_valid: bool, @@ -190,14 +190,14 @@ pub struct MarginCalculation { } #[derive(Clone, Copy, Debug, Default)] -pub struct IsolatedPositionMarginCalculation { +pub struct IsolatedMarginCalculation { pub margin_requirement: u128, pub total_collateral: i128, pub total_collateral_buffer: i128, pub margin_requirement_plus_buffer: u128, } -impl IsolatedPositionMarginCalculation { +impl IsolatedMarginCalculation { pub fn get_total_collateral_plus_buffer(&self) -> i128 { self.total_collateral .saturating_add(self.total_collateral_buffer) @@ -228,7 +228,7 @@ impl MarginCalculation { total_collateral_buffer: 0, margin_requirement: 0, margin_requirement_plus_buffer: 0, - isolated_position_margin_calculation: BTreeMap::new(), + isolated_margin_calculations: BTreeMap::new(), num_spot_liabilities: 0, num_perp_liabilities: 0, all_deposit_oracles_valid: true, @@ -311,14 +311,14 @@ impl MarginCalculation { 0 }; - let isolated_position_margin_calculation = IsolatedPositionMarginCalculation { + let isolated_position_margin_calculation = IsolatedMarginCalculation { margin_requirement, total_collateral, total_collateral_buffer, margin_requirement_plus_buffer, }; - self.isolated_position_margin_calculation + self.isolated_margin_calculations .insert(market_index, isolated_position_margin_calculation); if let Some(market_to_track) = self.market_to_track_margin_requirement() { @@ -416,7 +416,7 @@ impl MarginCalculation { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations { if !isolated_position_margin_calculation.meets_margin_requirement() { return false; @@ -434,7 +434,7 @@ impl MarginCalculation { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_position_margin_calculation + for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations { if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { return false; @@ -460,7 +460,7 @@ impl MarginCalculation { market_index: u16, ) -> DriftResult { Ok(self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()? .meets_margin_requirement()) @@ -472,7 +472,7 @@ impl MarginCalculation { market_index: u16, ) -> DriftResult { Ok(self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()? .meets_margin_requirement_with_buffer()) @@ -494,7 +494,7 @@ impl MarginCalculation { } Ok(self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()? .meets_margin_requirement_with_buffer()) @@ -519,7 +519,7 @@ impl MarginCalculation { return Err(ErrorCode::InvalidMarginCalculation); } - self.isolated_position_margin_calculation + self.isolated_margin_calculations .get(&market_index) .safe_unwrap()? .margin_shortage() @@ -538,7 +538,7 @@ impl MarginCalculation { }; let margin_requirement = if market_type == MarketType::Perp { - match self.isolated_position_margin_calculation.get(&market_index) { + match self.isolated_margin_calculations.get(&market_index) { Some(isolated_position_margin_calculation) => { isolated_position_margin_calculation.margin_requirement } @@ -566,7 +566,7 @@ impl MarginCalculation { pub fn get_isolated_position_free_collateral(&self, market_index: u16) -> DriftResult { let isolated_position_margin_calculation = self - .isolated_position_margin_calculation + .isolated_margin_calculations .get(&market_index) .safe_unwrap()?; isolated_position_margin_calculation @@ -686,9 +686,9 @@ impl MarginCalculation { pub fn get_isolated_position_margin_calculation( &self, market_index: u16, - ) -> DriftResult<&IsolatedPositionMarginCalculation> { + ) -> DriftResult<&IsolatedMarginCalculation> { if let Some(isolated_position_margin_calculation) = - self.isolated_position_margin_calculation.get(&market_index) + self.isolated_margin_calculations.get(&market_index) { Ok(isolated_position_margin_calculation) } else { @@ -697,7 +697,7 @@ impl MarginCalculation { } pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { - self.isolated_position_margin_calculation + self.isolated_margin_calculations .contains_key(&market_index) } } From ea0984261451ecb452b9e5279f30c98a58acd000 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 19 Aug 2025 18:36:39 -0400 Subject: [PATCH 40/91] clean up naming --- programs/drift/src/controller/liquidation.rs | 32 ++++----- .../drift/src/controller/liquidation/tests.rs | 6 +- programs/drift/src/controller/orders.rs | 28 ++++---- programs/drift/src/controller/pnl.rs | 2 +- programs/drift/src/instructions/user.rs | 26 ++++---- programs/drift/src/math/bankruptcy.rs | 4 +- programs/drift/src/math/bankruptcy/tests.rs | 24 +++---- programs/drift/src/math/liquidation.rs | 15 +++-- programs/drift/src/math/margin.rs | 24 +++---- programs/drift/src/math/margin/tests.rs | 12 ++-- programs/drift/src/math/orders.rs | 4 +- programs/drift/src/state/liquidation_mode.rs | 46 ++++++------- .../drift/src/state/margin_calculation.rs | 66 +++++++++---------- programs/drift/src/state/user.rs | 32 ++++----- programs/drift/src/state/user/tests.rs | 8 +-- 15 files changed, 165 insertions(+), 164 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 7f8af63d1e..7ea7d3ceb7 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -21,7 +21,7 @@ use crate::controller::spot_balance::{ }; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; -use crate::math::bankruptcy::is_user_bankrupt; +use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, QUOTE_PRECISION, @@ -1416,7 +1416,7 @@ pub fn liquidate_spot( msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() - && margin_calculation.cross_margin_can_exit_liquidation()? + && margin_calculation.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); return Ok(()); @@ -1464,7 +1464,7 @@ pub fn liquidate_spot( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1710,7 +1710,7 @@ pub fn liquidate_spot( if liability_transfer >= liability_transfer_to_cover_margin_shortage { user.exit_cross_margin_liquidation(); - } else if is_user_bankrupt(user) { + } else if is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -1949,7 +1949,7 @@ pub fn liquidate_spot_with_swap_begin( msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() - && margin_calculation.cross_margin_can_exit_liquidation()? + && margin_calculation.can_exit_cross_margin_liquidation()? { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::InvalidLiquidation); @@ -2020,7 +2020,7 @@ pub fn liquidate_spot_with_swap_begin( }); // must throw error to stop swap - if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { return Err(ErrorCode::InvalidLiquidation); } @@ -2283,9 +2283,9 @@ 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.cross_margin_can_exit_liquidation()? { + if margin_calulcation_after.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); - } else if is_user_bankrupt(user) { + } else if is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -2530,7 +2530,7 @@ pub fn liquidate_borrow_for_perp_pnl( msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() - && margin_calculation.cross_margin_can_exit_liquidation()? + && margin_calculation.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); return Ok(()); @@ -2574,7 +2574,7 @@ pub fn liquidate_borrow_for_perp_pnl( .cast::()?; user.increment_margin_freed(margin_freed)?; - if intermediate_margin_calculation.cross_margin_can_exit_liquidation()? { + if intermediate_margin_calculation.can_exit_cross_margin_liquidation()? { let market = perp_market_map.get_ref(&perp_market_index)?; let market_oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; @@ -2753,7 +2753,7 @@ pub fn liquidate_borrow_for_perp_pnl( if liability_transfer >= liability_transfer_to_cover_margin_shortage { user.exit_cross_margin_liquidation(); - } else if is_user_bankrupt(user) { + } else if is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -3527,7 +3527,7 @@ pub fn resolve_spot_bankruptcy( now: i64, insurance_fund_vault_balance: u64, ) -> DriftResult { - if !user.is_cross_margin_bankrupt() && is_user_bankrupt(user) { + if !user.is_cross_margin_bankrupt() && is_cross_margin_bankrupt(user) { user.enter_cross_margin_bankruptcy(); } @@ -3636,7 +3636,7 @@ pub fn resolve_spot_bankruptcy( } // exit bankruptcy - if !is_user_bankrupt(user) { + if !is_cross_margin_bankrupt(user) { user.exit_cross_margin_bankruptcy(); } @@ -3722,9 +3722,9 @@ pub fn set_user_status_to_being_liquidated( user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_position_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { - if !user.is_isolated_position_being_liquidated(*market_index)? && !isolated_position_margin_calculation.meets_margin_requirement() { - user.enter_isolated_position_liquidation(*market_index)?; + for (market_index, isolated_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { + if !user.is_isolated_margin_being_liquidated(*market_index)? && !isolated_margin_calculation.meets_margin_requirement() { + user.enter_isolated_margin_liquidation(*market_index)?; } } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 4548cbbb33..57253feaac 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -9396,7 +9396,7 @@ pub mod liquidate_isolated_perp { .unwrap(); let isolated_margin_calculation = margin_calculation - .get_isolated_position_margin_calculation(0) + .get_isolated_margin_calculation(0) .unwrap(); let total_collateral = isolated_margin_calculation.total_collateral; let margin_requirement_plus_buffer = @@ -9558,7 +9558,7 @@ pub mod liquidate_isolated_perp { assert_eq!(user.perp_positions[0].base_asset_amount, 2000000000); assert_eq!( - user.perp_positions[0].is_isolated_position_being_liquidated(), + user.perp_positions[0].is_being_liquidated(), false ); } @@ -9807,7 +9807,7 @@ pub mod liquidate_isolated_perp { .unwrap(); let market_after = perp_market_map.get_ref(&0).unwrap(); - assert!(!user.is_isolated_position_being_liquidated(0).unwrap()); + assert!(!user.is_isolated_margin_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 f30722744e..a295ee40d5 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1952,13 +1952,13 @@ 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) + .has_isolated_margin_calculation(market_index) { - let isolated_position_margin_calculation = taker_margin_calculation - .get_isolated_position_margin_calculation(market_index)?; + let isolated_margin_calculation = taker_margin_calculation + .get_isolated_margin_calculation(market_index)?; ( - isolated_position_margin_calculation.margin_requirement, - isolated_position_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, ) } else { ( @@ -2028,13 +2028,13 @@ 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) + .has_isolated_margin_calculation(market_index) { - let isolated_position_margin_calculation = maker_margin_calculation - .get_isolated_position_margin_calculation(market_index)?; + let isolated_margin_calculation = maker_margin_calculation + .get_isolated_margin_calculation(market_index)?; ( - isolated_position_margin_calculation.margin_requirement, - isolated_position_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, ) } else { ( @@ -3227,7 +3227,7 @@ pub fn force_cancel_orders( )?; let cross_margin_meets_initial_margin_requirement = - margin_calc.cross_margin_meets_margin_requirement(); + margin_calc.meets_cross_margin_requirement(); let mut total_fee = 0_u64; @@ -3278,9 +3278,9 @@ pub fn force_cancel_orders( continue; } } else { - let isolated_position_meets_margin_requirement = - margin_calc.isolated_position_meets_margin_requirement(market_index)?; - if isolated_position_meets_margin_requirement { + let meets_isolated_margin_requirement = + margin_calc.meets_isolated_margin_requirement(market_index)?; + if meets_isolated_margin_requirement { continue; } } diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 14e4fbd865..92974b04e1 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -266,7 +266,7 @@ pub fn settle_pnl( 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)?; + let token_amount = perp_position.get_isolated_token_amount(spot_market)?; validate!( token_amount >= pnl_to_settle_with_user.unsigned_abs(), diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index acc7b31f63..b2380d685e 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -33,7 +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_isolated_margin_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; @@ -2002,9 +2002,9 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( drop(spot_market); - if user.is_isolated_position_being_liquidated(perp_market_index)? { + if user.is_isolated_margin_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( + let is_being_liquidated = is_isolated_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -2014,7 +2014,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_isolated_position_liquidation(perp_market_index)?; + user.exit_isolated_margin_liquidation(perp_market_index)?; } } @@ -2174,9 +2174,9 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( user.exit_cross_margin_liquidation(); } - if user.is_isolated_position_being_liquidated(perp_market_index)? { + if user.is_isolated_margin_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( + let is_being_liquidated = is_isolated_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -2186,7 +2186,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; if !is_being_liquidated { - user.exit_isolated_position_liquidation(perp_market_index)?; + user.exit_isolated_margin_liquidation(perp_market_index)?; } } } else { @@ -2194,7 +2194,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let isolated_perp_position_token_amount = user .force_get_isolated_perp_position_mut(perp_market_index)? - .get_isolated_position_token_amount(&spot_market)?; + .get_isolated_token_amount(&spot_market)?; validate!( amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, @@ -2231,8 +2231,8 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( perp_market_index, )?; - if user.is_isolated_position_being_liquidated(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; } if user.is_cross_margin_being_liquidated() { @@ -2326,7 +2326,7 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( 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)?; + isolated_perp_position.get_isolated_token_amount(spot_market)?; validate!( amount as u128 <= isolated_position_token_amount, @@ -2352,8 +2352,8 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( perp_market_index, )?; - if user.is_isolated_position_being_liquidated(perp_market_index)? { - user.exit_isolated_position_liquidation(perp_market_index)?; + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; } user.update_last_active_slot(slot); diff --git a/programs/drift/src/math/bankruptcy.rs b/programs/drift/src/math/bankruptcy.rs index f8963c61c4..b6d3f97453 100644 --- a/programs/drift/src/math/bankruptcy.rs +++ b/programs/drift/src/math/bankruptcy.rs @@ -5,7 +5,7 @@ use crate::state::user::User; #[cfg(test)] mod tests; -pub fn is_user_bankrupt(user: &User) -> bool { +pub fn is_cross_margin_bankrupt(user: &User) -> bool { // user is bankrupt iff they have spot liabilities, no spot assets, and no perp exposure let mut has_liability = false; @@ -35,7 +35,7 @@ pub fn is_user_bankrupt(user: &User) -> bool { has_liability } -pub fn is_user_isolated_position_bankrupt(user: &User, market_index: u16) -> DriftResult { +pub fn is_isolated_margin_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 { diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index 371ab80461..d604c38616 100644 --- a/programs/drift/src/math/bankruptcy/tests.rs +++ b/programs/drift/src/math/bankruptcy/tests.rs @@ -1,4 +1,4 @@ -use crate::math::bankruptcy::is_user_bankrupt; +use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::state::spot_market::SpotBalanceType; use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User}; use crate::test_utils::{get_positions, get_spot_positions}; @@ -13,7 +13,7 @@ fn user_has_position_with_base() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -27,7 +27,7 @@ fn user_has_position_with_positive_quote() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -42,7 +42,7 @@ fn user_with_deposit() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -56,7 +56,7 @@ fn user_has_position_with_negative_quote() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(is_bankrupt); } @@ -71,14 +71,14 @@ fn user_with_borrow() { ..User::default() }; - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(is_bankrupt); } #[test] fn user_with_empty_position_and_balances() { let user = User::default(); - let is_bankrupt = is_user_bankrupt(&user); + let is_bankrupt = is_cross_margin_bankrupt(&user); assert!(!is_bankrupt); } @@ -95,30 +95,30 @@ fn user_with_isolated_position() { 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); + let is_bankrupt = is_cross_margin_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); + let is_bankrupt = is_cross_margin_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); + let is_bankrupt = is_cross_margin_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); + let is_bankrupt = is_cross_margin_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); + let is_bankrupt = is_cross_margin_bankrupt(&user_with_negative_pnl); assert!(is_bankrupt); } diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 9f25648c4e..50f6a5aba2 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.cross_margin_can_exit_liquidation()?; + let is_being_liquidated = !margin_calculation.can_exit_cross_margin_liquidation()?; Ok(is_being_liquidated) } @@ -238,7 +238,7 @@ pub fn validate_user_not_being_liquidated( )?; if user.is_cross_margin_being_liquidated() { - if margin_calculation.cross_margin_can_exit_liquidation()? { + if margin_calculation.can_exit_cross_margin_liquidation()? { user.exit_cross_margin_liquidation(); } else { return Err(ErrorCode::UserIsBeingLiquidated); @@ -248,13 +248,14 @@ pub fn validate_user_not_being_liquidated( .perp_positions .iter() .filter(|position| { - position.is_isolated() && position.is_isolated_position_being_liquidated() + position.is_isolated() && position.is_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)?; + if margin_calculation.can_exit_isolated_margin_liquidation(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; } else { return Err(ErrorCode::UserIsBeingLiquidated); } @@ -265,7 +266,7 @@ pub fn validate_user_not_being_liquidated( } // todo check if this is corrects -pub fn is_isolated_position_being_liquidated( +pub fn is_isolated_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, @@ -282,7 +283,7 @@ pub fn is_isolated_position_being_liquidated( )?; let is_being_liquidated = - !margin_calculation.isolated_position_can_exit_liquidation(perp_market_index)?; + !margin_calculation.can_exit_isolated_margin_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 16f75868ae..c5781a810a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -311,7 +311,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( token_value = 0; } - calculation.add_total_collateral(token_value)?; + calculation.add_isolated_total_collateral(token_value)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -329,7 +329,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( token_value, token_value, MarketIdentifier::spot(0), @@ -382,7 +382,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )?; } - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( spot_position.margin_requirement_for_open_orders()?, 0, MarketIdentifier::spot(spot_market.market_index), @@ -399,7 +399,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( } calculation - .add_total_collateral(worst_case_weighted_token_value.cast::()?)?; + .add_isolated_total_collateral(worst_case_weighted_token_value.cast::()?)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -422,7 +422,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( worst_case_weighted_token_value.unsigned_abs(), worst_case_token_value.unsigned_abs(), MarketIdentifier::spot(spot_market.market_index), @@ -459,13 +459,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_orders_value = 0; } - calculation.add_total_collateral(worst_case_orders_value.cast::()?)?; + calculation.add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(worst_case_orders_value)?; } Ordering::Less => { - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( worst_case_orders_value.unsigned_abs(), worst_case_orders_value.unsigned_abs(), MarketIdentifier::spot(0), @@ -559,7 +559,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( &strict_quote_price, )?; - calculation.add_isolated_position_margin_calculation( + calculation.add_isolated_margin_calculation( market.market_index, quote_token_value, weighted_pnl, @@ -570,13 +570,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(quote_token_value)?; } else { - calculation.add_margin_requirement( + calculation.add_isolated_margin_requirement( perp_margin_requirement, worst_case_liability_value, MarketIdentifier::perp(market.market_index), )?; - calculation.add_total_collateral(weighted_pnl)?; + calculation.add_isolated_total_collateral(weighted_pnl)?; } #[cfg(feature = "drift-rs")] @@ -799,7 +799,7 @@ pub fn calculate_max_withdrawable_amount( return token_amount.cast(); } - let free_collateral = calculation.get_cross_margin_free_collateral()?; + let free_collateral = calculation.get_cross_free_collateral()?; let (numerator_scale, denominator_scale) = if spot_market.decimals > 6 { (10_u128.pow(spot_market.decimals - 6), 1) @@ -945,7 +945,7 @@ pub fn calculate_user_equity( if market_position.is_isolated() { let quote_token_amount = - market_position.get_isolated_position_token_amount("e_spot_market)?; + market_position.get_isolated_token_amount("e_spot_market)?; let token_value = get_token_value( quote_token_amount.cast()?, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 5a858c3123..a540357d48 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -2794,7 +2794,7 @@ mod calculate_margin_requirement_and_total_collateral_and_liability_info { assert_eq!(calculation.total_collateral, 0); assert_eq!( - calculation.get_total_collateral_plus_buffer(), + calculation.get_cross_total_collateral_plus_buffer(), -QUOTE_PRECISION_I128 ); } @@ -4454,7 +4454,7 @@ mod isolated_position { 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_calculation = margin_calculation.get_isolated_margin_calculation(0).unwrap(); let isolated_margin_requirement = isolated_margin_calculation.margin_requirement; let isolated_total_collateral = isolated_margin_calculation.total_collateral; @@ -4463,9 +4463,9 @@ mod isolated_position { 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!(margin_calculation.meets_cross_margin_requirement(), true); assert_eq!(isolated_margin_calculation.meets_margin_requirement(), false); - assert_eq!(margin_calculation.isolated_position_meets_margin_requirement(0).unwrap(), false); + assert_eq!(margin_calculation.meets_isolated_margin_requirement(0).unwrap(), false); let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &user, @@ -4477,9 +4477,9 @@ mod isolated_position { .unwrap(); let cross_margin_margin_requirement = margin_calculation.margin_requirement_plus_buffer; - let cross_total_collateral = margin_calculation.get_total_collateral_plus_buffer(); + let cross_total_collateral = margin_calculation.get_cross_total_collateral_plus_buffer(); - let isolated_margin_calculation = margin_calculation.get_isolated_position_margin_calculation(0).unwrap(); + let isolated_margin_calculation = margin_calculation.get_isolated_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(); diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 85e2928959..88a59175d0 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -861,11 +861,11 @@ 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)? + .get_isolated_free_collateral(market_index)? .cast::()? } else { margin_calculation - .get_cross_margin_free_collateral()? + .get_cross_free_collateral()? .cast::()? }; diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index aa98aa99cc..938b96bac9 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -7,7 +7,7 @@ use crate::{ }, error::{DriftResult, ErrorCode}, math::{ - bankruptcy::{is_user_bankrupt, is_user_isolated_position_bankrupt}, + bankruptcy::{is_cross_margin_bankrupt, is_isolated_margin_bankrupt}, liquidation::calculate_max_pct_to_liquidate, margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap, @@ -93,7 +93,7 @@ pub fn get_perp_liquidation_mode( ) -> DriftResult> { let perp_position = user.get_perp_position(market_index)?; let mode: Box = if perp_position.is_isolated() { - Box::new(IsolatedLiquidatePerpMode::new(market_index)) + Box::new(IsolatedMarginLiquidatePerpMode::new(market_index)) } else { Box::new(CrossMarginLiquidatePerpMode::new(market_index)) }; @@ -120,7 +120,7 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { &self, margin_calculation: &MarginCalculation, ) -> DriftResult { - Ok(margin_calculation.cross_margin_meets_margin_requirement()) + Ok(margin_calculation.meets_cross_margin_requirement()) } fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { @@ -128,7 +128,7 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { - Ok(margin_calculation.cross_margin_can_exit_liquidation()?) + Ok(margin_calculation.can_exit_cross_margin_liquidation()?) } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { @@ -161,11 +161,11 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } fn is_user_bankrupt(&self, user: &User) -> DriftResult { - Ok(is_user_bankrupt(user)) + Ok(user.is_cross_margin_bankrupt()) } fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { - Ok(is_user_bankrupt(user)) + Ok(is_cross_margin_bankrupt(user)) } fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { @@ -256,38 +256,38 @@ impl LiquidatePerpMode for CrossMarginLiquidatePerpMode { } } -pub struct IsolatedLiquidatePerpMode { +pub struct IsolatedMarginLiquidatePerpMode { pub market_index: u16, } -impl IsolatedLiquidatePerpMode { +impl IsolatedMarginLiquidatePerpMode { pub fn new(market_index: u16) -> Self { Self { market_index } } } -impl LiquidatePerpMode for IsolatedLiquidatePerpMode { +impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { fn user_is_being_liquidated(&self, user: &User) -> DriftResult { - user.is_isolated_position_being_liquidated(self.market_index) + user.is_isolated_margin_being_liquidated(self.market_index) } fn meets_margin_requirements( &self, margin_calculation: &MarginCalculation, ) -> DriftResult { - margin_calculation.isolated_position_meets_margin_requirement(self.market_index) + margin_calculation.meets_isolated_margin_requirement(self.market_index) } fn can_exit_liquidation(&self, margin_calculation: &MarginCalculation) -> DriftResult { - margin_calculation.isolated_position_can_exit_liquidation(self.market_index) + margin_calculation.can_exit_isolated_margin_liquidation(self.market_index) } fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { - user.enter_isolated_position_liquidation(self.market_index) + user.enter_isolated_margin_liquidation(self.market_index) } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { - user.exit_isolated_position_liquidation(self.market_index) + user.exit_isolated_margin_liquidation(self.market_index) } fn get_cancel_orders_params(&self) -> (Option, Option) { @@ -310,32 +310,32 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { } fn is_user_bankrupt(&self, user: &User) -> DriftResult { - user.is_isolated_position_bankrupt(self.market_index) + user.is_isolated_margin_bankrupt(self.market_index) } fn should_user_enter_bankruptcy(&self, user: &User) -> DriftResult { - is_user_isolated_position_bankrupt(user, self.market_index) + is_isolated_margin_bankrupt(user, self.market_index) } fn enter_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - user.enter_isolated_position_bankruptcy(self.market_index) + user.enter_isolated_margin_bankruptcy(self.market_index) } fn exit_bankruptcy(&self, user: &mut User) -> DriftResult<()> { - user.exit_isolated_position_bankruptcy(self.market_index) + user.exit_isolated_margin_bankruptcy(self.market_index) } fn get_event_fields( &self, margin_calculation: &MarginCalculation, ) -> DriftResult<(u128, i128, u8)> { - let isolated_position_margin_calculation = margin_calculation + let isolated_margin_calculation = margin_calculation .isolated_margin_calculations .get(&self.market_index) .safe_unwrap()?; Ok(( - isolated_position_margin_calculation.margin_requirement, - isolated_position_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, LiquidationBitFlag::IsolatedPosition as u8, )) } @@ -352,7 +352,7 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { 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)?; + isolated_perp_position.get_isolated_token_amount(spot_market)?; validate!( token_amount != 0, @@ -396,6 +396,6 @@ impl LiquidatePerpMode for IsolatedLiquidatePerpMode { } fn margin_shortage(&self, margin_calculation: &MarginCalculation) -> DriftResult { - margin_calculation.isolated_position_margin_shortage(self.market_index) + margin_calculation.isolated_margin_shortage(self.market_index) } } diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 9491e52378..778682c2f1 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -246,7 +246,7 @@ impl MarginCalculation { } } - pub fn add_total_collateral(&mut self, total_collateral: i128) -> DriftResult { + pub fn add_isolated_total_collateral(&mut self, total_collateral: i128) -> DriftResult { self.total_collateral = self.total_collateral.safe_add(total_collateral)?; if self.context.margin_buffer > 0 && total_collateral < 0 { @@ -259,7 +259,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_margin_requirement( + pub fn add_isolated_margin_requirement( &mut self, margin_requirement: u128, liability_value: u128, @@ -287,7 +287,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_isolated_position_margin_calculation( + pub fn add_isolated_margin_calculation( &mut self, market_index: u16, deposit_value: i128, @@ -311,7 +311,7 @@ impl MarginCalculation { 0 }; - let isolated_position_margin_calculation = IsolatedMarginCalculation { + let isolated_margin_calculation = IsolatedMarginCalculation { margin_requirement, total_collateral, total_collateral_buffer, @@ -319,7 +319,7 @@ impl MarginCalculation { }; self.isolated_margin_calculations - .insert(market_index, isolated_position_margin_calculation); + .insert(market_index, isolated_margin_calculation); if let Some(market_to_track) = self.market_to_track_margin_requirement() { if market_to_track == MarketIdentifier::perp(market_index) { @@ -404,21 +404,21 @@ impl MarginCalculation { } #[inline(always)] - pub fn get_total_collateral_plus_buffer(&self) -> i128 { + pub fn get_cross_total_collateral_plus_buffer(&self) -> i128 { self.total_collateral .saturating_add(self.total_collateral_buffer) } pub fn meets_margin_requirement(&self) -> bool { - let cross_margin_meets_margin_requirement = self.cross_margin_meets_margin_requirement(); + let cross_margin_meets_margin_requirement = self.meets_cross_margin_requirement(); if !cross_margin_meets_margin_requirement { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { - if !isolated_position_margin_calculation.meets_margin_requirement() { + if !isolated_margin_calculation.meets_margin_requirement() { return false; } } @@ -428,15 +428,15 @@ 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(); + self.meets_cross_margin_requirement_with_buffer(); if !cross_margin_meets_margin_requirement { return false; } - for (_, isolated_position_margin_calculation) in &self.isolated_margin_calculations + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { - if !isolated_position_margin_calculation.meets_margin_requirement_with_buffer() { + if !isolated_margin_calculation.meets_margin_requirement_with_buffer() { return false; } } @@ -445,17 +445,17 @@ impl MarginCalculation { } #[inline(always)] - pub fn cross_margin_meets_margin_requirement(&self) -> bool { + pub fn meets_cross_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 + pub fn meets_cross_margin_requirement_with_buffer(&self) -> bool { + self.get_cross_total_collateral_plus_buffer() >= self.margin_requirement_plus_buffer as i128 } #[inline(always)] - pub fn isolated_position_meets_margin_requirement( + pub fn meets_isolated_margin_requirement( &self, market_index: u16, ) -> DriftResult { @@ -467,7 +467,7 @@ impl MarginCalculation { } #[inline(always)] - pub fn isolated_position_meets_margin_requirement_with_buffer( + pub fn meets_isolated_margin_requirement_with_buffer( &self, market_index: u16, ) -> DriftResult { @@ -478,16 +478,16 @@ impl MarginCalculation { .meets_margin_requirement_with_buffer()) } - pub fn cross_margin_can_exit_liquidation(&self) -> DriftResult { + pub fn can_exit_cross_margin_liquidation(&self) -> DriftResult { if !self.is_liquidation_mode() { msg!("liquidation mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); } - Ok(self.cross_margin_meets_margin_requirement_with_buffer()) + Ok(self.meets_cross_margin_requirement_with_buffer()) } - pub fn isolated_position_can_exit_liquidation(&self, market_index: u16) -> DriftResult { + pub fn can_exit_isolated_margin_liquidation(&self, market_index: u16) -> DriftResult { if !self.is_liquidation_mode() { msg!("liquidation mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); @@ -509,11 +509,11 @@ impl MarginCalculation { Ok(self .margin_requirement_plus_buffer .cast::()? - .safe_sub(self.get_total_collateral_plus_buffer())? + .safe_sub(self.get_cross_total_collateral_plus_buffer())? .unsigned_abs()) } - pub fn isolated_position_margin_shortage(&self, market_index: u16) -> DriftResult { + pub fn isolated_margin_shortage(&self, market_index: u16) -> DriftResult { if self.context.margin_buffer == 0 { msg!("margin buffer mode not enabled"); return Err(ErrorCode::InvalidMarginCalculation); @@ -539,8 +539,8 @@ impl MarginCalculation { let margin_requirement = if market_type == MarketType::Perp { match self.isolated_margin_calculations.get(&market_index) { - Some(isolated_position_margin_calculation) => { - isolated_position_margin_calculation.margin_requirement + Some(isolated_margin_calculation) => { + isolated_margin_calculation.margin_requirement } None => self.margin_requirement, } @@ -557,22 +557,22 @@ impl MarginCalculation { .safe_div(margin_requirement) } - pub fn get_cross_margin_free_collateral(&self) -> DriftResult { + pub fn get_cross_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 + pub fn get_isolated_free_collateral(&self, market_index: u16) -> DriftResult { + let isolated_margin_calculation = self .isolated_margin_calculations .get(&market_index) .safe_unwrap()?; - isolated_position_margin_calculation + isolated_margin_calculation .total_collateral .safe_sub( - isolated_position_margin_calculation + isolated_margin_calculation .margin_requirement .cast::()?, )? @@ -683,20 +683,20 @@ impl MarginCalculation { Ok(()) } - pub fn get_isolated_position_margin_calculation( + pub fn get_isolated_margin_calculation( &self, market_index: u16, ) -> DriftResult<&IsolatedMarginCalculation> { - if let Some(isolated_position_margin_calculation) = + if let Some(isolated_margin_calculation) = self.isolated_margin_calculations.get(&market_index) { - Ok(isolated_position_margin_calculation) + Ok(isolated_margin_calculation) } else { Err(ErrorCode::InvalidMarginCalculation) } } - pub fn has_isolated_position_margin_calculation(&self, market_index: u16) -> bool { + pub fn has_isolated_margin_calculation(&self, market_index: u16) -> bool { self.isolated_margin_calculations .contains_key(&market_index) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 6fbef39b6e..d7f0a60f4b 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -135,7 +135,7 @@ pub struct User { impl User { pub fn is_being_liquidated(&self) -> bool { - self.is_cross_margin_being_liquidated() || self.has_isolated_position_being_liquidated() + self.is_cross_margin_being_liquidated() || self.has_isolated_margin_being_liquidated() } pub fn is_cross_margin_being_liquidated(&self) -> bool { @@ -382,7 +382,7 @@ impl User { self.liquidation_margin_freed = 0; self.last_active_slot = slot; - let liquidation_id = if self.has_isolated_position_being_liquidated() { + let liquidation_id = if self.has_isolated_margin_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { get_then_update_id!(self, next_liquidation_id) @@ -408,22 +408,22 @@ impl User { self.liquidation_margin_freed = 0; } - pub fn has_isolated_position_being_liquidated(&self) -> bool { + pub fn has_isolated_margin_being_liquidated(&self) -> bool { self.perp_positions.iter().any(|position| { - position.is_isolated() && position.is_isolated_position_being_liquidated() + position.is_isolated() && position.is_being_liquidated() }) } - pub fn enter_isolated_position_liquidation( + pub fn enter_isolated_margin_liquidation( &mut self, perp_market_index: u16, ) -> DriftResult { - if self.is_isolated_position_being_liquidated(perp_market_index)? { + if self.is_isolated_margin_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() + || self.has_isolated_margin_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { @@ -437,34 +437,34 @@ impl User { Ok(liquidation_id) } - pub fn exit_isolated_position_liquidation(&mut self, perp_market_index: u16) -> DriftResult { + pub fn exit_isolated_margin_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( + pub fn is_isolated_margin_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()) + Ok(perp_position.is_being_liquidated()) } - pub fn enter_isolated_position_bankruptcy(&mut self, perp_market_index: u16) -> DriftResult { + pub fn enter_isolated_margin_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 { + pub fn exit_isolated_margin_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 is_isolated_position_bankrupt(&self, perp_market_index: u16) -> DriftResult { + pub fn is_isolated_margin_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) } @@ -735,7 +735,7 @@ impl User { )?; let isolated_position_margin_calculation = calculation - .get_isolated_position_margin_calculation(isolated_perp_position_market_index)?; + .get_isolated_margin_calculation(isolated_perp_position_market_index)?; validate!( calculation.all_liability_oracles_valid, @@ -1268,7 +1268,7 @@ impl PerpPosition { == PositionFlag::IsolatedPosition as u8 } - pub fn get_isolated_position_token_amount( + pub fn get_isolated_token_amount( &self, spot_market: &SpotMarket, ) -> DriftResult { @@ -1279,7 +1279,7 @@ impl PerpPosition { ) } - pub fn is_isolated_position_being_liquidated(&self) -> bool { + pub fn is_being_liquidated(&self) -> bool { self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 64573306a0..477e3b17b6 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2335,17 +2335,17 @@ mod next_liquidation_id { 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(); + let liquidation_id = user.enter_isolated_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 1); - user.exit_isolated_position_liquidation(1).unwrap(); + user.exit_isolated_margin_liquidation(1).unwrap(); user.exit_cross_margin_liquidation(); - let liquidation_id = user.enter_isolated_position_liquidation(1).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(1).unwrap(); assert_eq!(liquidation_id, 2); - let liquidation_id = user.enter_isolated_position_liquidation(2).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(2).unwrap(); assert_eq!(liquidation_id, 2); let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); From cc397f0bc75a7f3727c1802ffbc099ec70a8f90c Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 20 Aug 2025 19:14:09 -0400 Subject: [PATCH 41/91] update last active slot for isolated position liq --- programs/drift/src/controller/liquidation.rs | 20 ++++++++++---------- programs/drift/src/state/liquidation_mode.rs | 2 +- programs/drift/src/state/user.rs | 12 +++++++----- programs/drift/src/state/user/tests.rs | 15 ++++++++++----- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 7ea7d3ceb7..a0842b7a90 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -109,7 +109,7 @@ pub fn liquidate_perp( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; validate!( @@ -255,7 +255,7 @@ pub fn liquidate_perp( if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = - liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -764,7 +764,7 @@ pub fn liquidate_perp_with_fill( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; let market = perp_market_map.get_ref(&market_index)?; @@ -893,7 +893,7 @@ pub fn liquidate_perp_with_fill( if liquidation_mode.can_exit_liquidation(&intermediate_margin_calculation)? { let (margin_requirement, total_collateral, bit_flags) = - liquidation_mode.get_event_fields(&intermediate_margin_calculation)?; + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -1209,7 +1209,7 @@ pub fn liquidate_spot( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -1793,7 +1793,7 @@ pub fn liquidate_spot_with_swap_begin( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -2345,7 +2345,7 @@ pub fn liquidate_borrow_for_perp_pnl( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; validate!( @@ -2829,7 +2829,7 @@ pub fn liquidate_perp_pnl_for_deposit( validate!( !liquidator.is_being_liquidated(), ErrorCode::UserBankrupt, - "liquidator bankrupt", + "liquidator being liquidated", )?; validate!( @@ -3058,7 +3058,7 @@ pub fn liquidate_perp_pnl_for_deposit( 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)?; + liquidation_mode.get_event_fields(&margin_calculation)?; emit!(LiquidationRecord { ts: now, liquidation_id, @@ -3724,7 +3724,7 @@ pub fn set_user_status_to_being_liquidated( for (market_index, isolated_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { if !user.is_isolated_margin_being_liquidated(*market_index)? && !isolated_margin_calculation.meets_margin_requirement() { - user.enter_isolated_margin_liquidation(*market_index)?; + user.enter_isolated_margin_liquidation(*market_index, slot)?; } } diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 938b96bac9..15394e96b2 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -283,7 +283,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { } fn enter_liquidation(&self, user: &mut User, slot: u64) -> DriftResult { - user.enter_isolated_margin_liquidation(self.market_index) + user.enter_isolated_margin_liquidation(self.market_index, slot) } fn exit_liquidation(&self, user: &mut User) -> DriftResult<()> { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d7f0a60f4b..31dea2295c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -380,11 +380,11 @@ impl User { self.add_user_status(UserStatus::BeingLiquidated); self.liquidation_margin_freed = 0; - self.last_active_slot = slot; let liquidation_id = if self.has_isolated_margin_being_liquidated() { self.next_liquidation_id.safe_sub(1)? } else { + self.last_active_slot = slot; get_then_update_id!(self, next_liquidation_id) }; @@ -417,6 +417,7 @@ impl User { pub fn enter_isolated_margin_liquidation( &mut self, perp_market_index: u16, + slot: u64, ) -> DriftResult { if self.is_isolated_margin_being_liquidated(perp_market_index)? { return self.next_liquidation_id.safe_sub(1); @@ -427,6 +428,7 @@ impl User { { self.next_liquidation_id.safe_sub(1)? } else { + self.last_active_slot = slot; get_then_update_id!(self, next_liquidation_id) }; @@ -734,7 +736,7 @@ impl User { context, )?; - let isolated_position_margin_calculation = calculation + let isolated_margin_calculation = calculation .get_isolated_margin_calculation(isolated_perp_position_market_index)?; validate!( @@ -744,11 +746,11 @@ impl User { )?; validate!( - isolated_position_margin_calculation.meets_margin_requirement(), + isolated_margin_calculation.meets_margin_requirement(), ErrorCode::InsufficientCollateral, "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - isolated_position_margin_calculation.total_collateral, - isolated_position_margin_calculation.margin_requirement + isolated_margin_calculation.total_collateral, + isolated_margin_calculation.margin_requirement )?; Ok(true) diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 477e3b17b6..d5dce7ffde 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2332,23 +2332,28 @@ mod next_liquidation_id { }; user.perp_positions[1] = isolated_position_2; - let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(2).unwrap(); assert_eq!(liquidation_id, 1); + assert_eq!(user.last_active_slot, 2); - let liquidation_id = user.enter_isolated_margin_liquidation(1).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(1, 3).unwrap(); assert_eq!(liquidation_id, 1); + assert_eq!(user.last_active_slot, 2); user.exit_isolated_margin_liquidation(1).unwrap(); user.exit_cross_margin_liquidation(); - let liquidation_id = user.enter_isolated_margin_liquidation(1).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(1, 4).unwrap(); assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); - let liquidation_id = user.enter_isolated_margin_liquidation(2).unwrap(); + let liquidation_id = user.enter_isolated_margin_liquidation(2, 5).unwrap(); assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); - let liquidation_id = user.enter_cross_margin_liquidation(1).unwrap(); + let liquidation_id = user.enter_cross_margin_liquidation(6).unwrap(); assert_eq!(liquidation_id, 2); + assert_eq!(user.last_active_slot, 4); } } From 9833303defc24db7ab9d3edbedec162d62577f9b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 10:18:24 -0400 Subject: [PATCH 42/91] another liquidation review --- programs/drift/src/controller/liquidation.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index a0842b7a90..a91cd263c5 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -342,7 +342,6 @@ 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()?, @@ -1134,7 +1133,7 @@ pub fn liquidate_perp_with_fill( margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; liquidation_mode.increment_free_margin(&mut user, margin_freed_for_perp_position)?; - if margin_calculation_after.meets_margin_requirement() { + if liquidation_mode.can_exit_liquidation(&margin_calculation_after)? { liquidation_mode.exit_liquidation(&mut user)?; } else if liquidation_mode.should_user_enter_bankruptcy(&user)? { liquidation_mode.enter_bankruptcy(&mut user)?; @@ -1412,7 +1411,7 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -1945,7 +1944,7 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -2526,7 +2525,7 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -3718,7 +3717,7 @@ pub fn set_user_status_to_being_liquidated( )?; // todo handle this - if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_margin_requirement() { + if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { user.enter_cross_margin_liquidation(slot)?; } From 9cb040a667f2c05151e82b5fc78e6fe735da645b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 10:28:58 -0400 Subject: [PATCH 43/91] add test --- programs/drift/src/controller/liquidation.rs | 2 +- programs/drift/src/state/user/tests.rs | 52 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index a91cd263c5..44cec26b34 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3728,4 +3728,4 @@ pub fn set_user_status_to_being_liquidated( } Ok(()) -} +} \ No newline at end of file diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index d5dce7ffde..04d1e0177a 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2357,3 +2357,55 @@ mod next_liquidation_id { assert_eq!(user.last_active_slot, 4); } } + +mod force_get_isolated_perp_position_mut { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + + 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_mut = user.force_get_isolated_perp_position_mut(1).unwrap(); + assert_eq!(isolated_position_mut.base_asset_amount, 1); + } + + { + let isolated_position = user.get_isolated_perp_position(1).unwrap(); + assert_eq!(isolated_position.base_asset_amount, 1); + } + + { + let isolated_position = user.get_isolated_perp_position(2); + assert_eq!(isolated_position.is_err(), true); + } + + { + let isolated_position_mut = user.force_get_isolated_perp_position_mut(2).unwrap(); + assert_eq!(isolated_position_mut.market_index, 2); + assert_eq!(isolated_position_mut.position_flag, PositionFlag::IsolatedPosition as u8); + } + + let isolated_position = PerpPosition { + market_index: 1, + base_asset_amount: 1, + ..PerpPosition::default() + }; + + user.perp_positions[0] = isolated_position; + + { + let isolated_position_mut = user.force_get_isolated_perp_position_mut(1); + assert_eq!(isolated_position_mut.is_err(), true); + } + } +} \ No newline at end of file From 81774968fec1da6e34d34e2fbf42d7a019b90468 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 10:29:40 -0400 Subject: [PATCH 44/91] cargo fmt -- --- programs/drift/src/controller/liquidation.rs | 26 +++++-- .../drift/src/controller/liquidation/tests.rs | 7 +- programs/drift/src/controller/orders.rs | 58 ++++++++------- programs/drift/src/math/bankruptcy/tests.rs | 3 +- programs/drift/src/math/liquidation.rs | 4 +- programs/drift/src/math/margin.rs | 8 ++- programs/drift/src/math/margin/tests.rs | 72 +++++++++++-------- programs/drift/src/state/liquidation_mode.rs | 3 +- .../drift/src/state/margin_calculation.rs | 15 ++-- programs/drift/src/state/user.rs | 20 ++---- programs/drift/src/state/user/tests.rs | 8 ++- 11 files changed, 118 insertions(+), 106 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 44cec26b34..987bd4a80c 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1411,7 +1411,9 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -1944,7 +1946,9 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -2525,7 +2529,9 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -3717,15 +3723,21 @@ pub fn set_user_status_to_being_liquidated( )?; // todo handle this - if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && !margin_calculation.meets_cross_margin_requirement() + { user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { - if !user.is_isolated_margin_being_liquidated(*market_index)? && !isolated_margin_calculation.meets_margin_requirement() { + for (market_index, isolated_margin_calculation) in + margin_calculation.isolated_margin_calculations.iter() + { + if !user.is_isolated_margin_being_liquidated(*market_index)? + && !isolated_margin_calculation.meets_margin_requirement() + { user.enter_isolated_margin_liquidation(*market_index, slot)?; } } Ok(()) -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 57253feaac..cf0789520c 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -9557,10 +9557,7 @@ pub mod liquidate_isolated_perp { .unwrap(); assert_eq!(user.perp_positions[0].base_asset_amount, 2000000000); - assert_eq!( - user.perp_positions[0].is_being_liquidated(), - false - ); + assert_eq!(user.perp_positions[0].is_being_liquidated(), false); } #[test] @@ -9810,4 +9807,4 @@ pub mod liquidate_isolated_perp { assert!(!user.is_isolated_margin_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 a295ee40d5..0f3f7ac047 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1951,21 +1951,20 @@ fn fulfill_perp_order( )?; if !taker_margin_calculation.meets_margin_requirement() { - let (margin_requirement, total_collateral) = if taker_margin_calculation - .has_isolated_margin_calculation(market_index) - { - let isolated_margin_calculation = taker_margin_calculation - .get_isolated_margin_calculation(market_index)?; - ( - isolated_margin_calculation.margin_requirement, - isolated_margin_calculation.total_collateral, - ) - } else { - ( - taker_margin_calculation.margin_requirement, - taker_margin_calculation.total_collateral, - ) - }; + let (margin_requirement, total_collateral) = + if taker_margin_calculation.has_isolated_margin_calculation(market_index) { + let isolated_margin_calculation = + taker_margin_calculation.get_isolated_margin_calculation(market_index)?; + ( + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, + ) + } else { + ( + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral, + ) + }; msg!( "taker breached fill requirements (margin requirement {}) (total_collateral {})", @@ -2027,21 +2026,20 @@ fn fulfill_perp_order( } if !maker_margin_calculation.meets_margin_requirement() { - let (margin_requirement, total_collateral) = if maker_margin_calculation - .has_isolated_margin_calculation(market_index) - { - let isolated_margin_calculation = maker_margin_calculation - .get_isolated_margin_calculation(market_index)?; - ( - isolated_margin_calculation.margin_requirement, - isolated_margin_calculation.total_collateral, - ) - } else { - ( - maker_margin_calculation.margin_requirement, - maker_margin_calculation.total_collateral, - ) - }; + let (margin_requirement, total_collateral) = + if maker_margin_calculation.has_isolated_margin_calculation(market_index) { + let isolated_margin_calculation = + maker_margin_calculation.get_isolated_margin_calculation(market_index)?; + ( + isolated_margin_calculation.margin_requirement, + isolated_margin_calculation.total_collateral, + ) + } else { + ( + maker_margin_calculation.margin_requirement, + maker_margin_calculation.total_collateral, + ) + }; msg!( "maker ({}) breached fill requirements (margin requirement {}) (total_collateral {})", diff --git a/programs/drift/src/math/bankruptcy/tests.rs b/programs/drift/src/math/bankruptcy/tests.rs index d604c38616..fbc745caf7 100644 --- a/programs/drift/src/math/bankruptcy/tests.rs +++ b/programs/drift/src/math/bankruptcy/tests.rs @@ -93,7 +93,8 @@ fn user_with_isolated_position() { }; let mut user_with_scaled_balance = user.clone(); - user_with_scaled_balance.perp_positions[0].isolated_position_scaled_balance = 1000000000000000000; + user_with_scaled_balance.perp_positions[0].isolated_position_scaled_balance = + 1000000000000000000; let is_bankrupt = is_cross_margin_bankrupt(&user_with_scaled_balance); assert!(!is_bankrupt); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 50f6a5aba2..09d5b2da20 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -247,9 +247,7 @@ pub fn validate_user_not_being_liquidated( let isolated_positions_being_liquidated = user .perp_positions .iter() - .filter(|position| { - position.is_isolated() && position.is_being_liquidated() - }) + .filter(|position| position.is_isolated() && position.is_being_liquidated()) .map(|position| position.market_index) .collect::>(); diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index c5781a810a..ece1767bc4 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -398,8 +398,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_weighted_token_value = 0; } - calculation - .add_isolated_total_collateral(worst_case_weighted_token_value.cast::()?)?; + calculation.add_isolated_total_collateral( + worst_case_weighted_token_value.cast::()?, + )?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -459,7 +460,8 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_orders_value = 0; } - calculation.add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; + calculation + .add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(worst_case_orders_value)?; diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index a540357d48..5a544b52d9 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4320,11 +4320,11 @@ mod isolated_position { use anchor_lang::Owner; use solana_program::pubkey::Pubkey; - use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; + use crate::create_account_info; 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, + 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, @@ -4339,7 +4339,7 @@ mod isolated_position { 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}; + use crate::{create_anchor_account_info, QUOTE_PRECISION_I64}; #[test] pub fn isolated_position_margin_requirement() { @@ -4442,19 +4442,22 @@ mod isolated_position { ..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 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_margin_calculation(0).unwrap(); + let isolated_margin_calculation = margin_calculation + .get_isolated_margin_calculation(0) + .unwrap(); let isolated_margin_requirement = isolated_margin_calculation.margin_requirement; let isolated_total_collateral = isolated_margin_calculation.total_collateral; @@ -4464,28 +4467,41 @@ mod isolated_position { assert_eq!(isolated_total_collateral, -900000000); assert_eq!(margin_calculation.meets_margin_requirement(), false); assert_eq!(margin_calculation.meets_cross_margin_requirement(), true); - assert_eq!(isolated_margin_calculation.meets_margin_requirement(), false); - assert_eq!(margin_calculation.meets_isolated_margin_requirement(0).unwrap(), false); + assert_eq!( + isolated_margin_calculation.meets_margin_requirement(), + false + ); + assert_eq!( + margin_calculation + .meets_isolated_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 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_cross_total_collateral_plus_buffer(); - let isolated_margin_calculation = margin_calculation.get_isolated_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(); + let isolated_margin_calculation = margin_calculation + .get_isolated_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 +} diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index 15394e96b2..a388e9c761 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -351,8 +351,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { 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_token_amount(spot_market)?; + let token_amount = isolated_perp_position.get_isolated_token_amount(spot_market)?; validate!( token_amount != 0, diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 778682c2f1..ac689ddd66 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -416,8 +416,7 @@ impl MarginCalculation { return false; } - for (_, isolated_margin_calculation) in &self.isolated_margin_calculations - { + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { if !isolated_margin_calculation.meets_margin_requirement() { return false; } @@ -434,8 +433,7 @@ impl MarginCalculation { return false; } - for (_, isolated_margin_calculation) in &self.isolated_margin_calculations - { + for (_, isolated_margin_calculation) in &self.isolated_margin_calculations { if !isolated_margin_calculation.meets_margin_requirement_with_buffer() { return false; } @@ -455,10 +453,7 @@ impl MarginCalculation { } #[inline(always)] - pub fn meets_isolated_margin_requirement( - &self, - market_index: u16, - ) -> DriftResult { + pub fn meets_isolated_margin_requirement(&self, market_index: u16) -> DriftResult { Ok(self .isolated_margin_calculations .get(&market_index) @@ -539,9 +534,7 @@ impl MarginCalculation { let margin_requirement = if market_type == MarketType::Perp { match self.isolated_margin_calculations.get(&market_index) { - Some(isolated_margin_calculation) => { - isolated_margin_calculation.margin_requirement - } + Some(isolated_margin_calculation) => isolated_margin_calculation.margin_requirement, None => self.margin_requirement, } } else { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 31dea2295c..6acf51b71f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -409,9 +409,9 @@ impl User { } pub fn has_isolated_margin_being_liquidated(&self) -> bool { - self.perp_positions.iter().any(|position| { - position.is_isolated() && position.is_being_liquidated() - }) + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_being_liquidated()) } pub fn enter_isolated_margin_liquidation( @@ -445,10 +445,7 @@ impl User { Ok(()) } - pub fn is_isolated_margin_being_liquidated( - &self, - perp_market_index: u16, - ) -> DriftResult { + pub fn is_isolated_margin_being_liquidated(&self, perp_market_index: u16) -> DriftResult { let perp_position = self.get_isolated_perp_position(perp_market_index)?; Ok(perp_position.is_being_liquidated()) } @@ -736,8 +733,8 @@ impl User { context, )?; - let isolated_margin_calculation = calculation - .get_isolated_margin_calculation(isolated_perp_position_market_index)?; + let isolated_margin_calculation = + calculation.get_isolated_margin_calculation(isolated_perp_position_market_index)?; validate!( calculation.all_liability_oracles_valid, @@ -1270,10 +1267,7 @@ impl PerpPosition { == PositionFlag::IsolatedPosition as u8 } - pub fn get_isolated_token_amount( - &self, - spot_market: &SpotMarket, - ) -> DriftResult { + pub fn get_isolated_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { get_token_amount( self.isolated_position_scaled_balance as u128, spot_market, diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 04d1e0177a..53bac03415 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2373,7 +2373,6 @@ mod force_get_isolated_perp_position_mut { }; user.perp_positions[0] = isolated_position; - { let isolated_position_mut = user.force_get_isolated_perp_position_mut(1).unwrap(); assert_eq!(isolated_position_mut.base_asset_amount, 1); @@ -2392,7 +2391,10 @@ mod force_get_isolated_perp_position_mut { { let isolated_position_mut = user.force_get_isolated_perp_position_mut(2).unwrap(); assert_eq!(isolated_position_mut.market_index, 2); - assert_eq!(isolated_position_mut.position_flag, PositionFlag::IsolatedPosition as u8); + assert_eq!( + isolated_position_mut.position_flag, + PositionFlag::IsolatedPosition as u8 + ); } let isolated_position = PerpPosition { @@ -2408,4 +2410,4 @@ mod force_get_isolated_perp_position_mut { assert_eq!(isolated_position_mut.is_err(), true); } } -} \ No newline at end of file +} From 9a8ec1a020ec50137bdb5e28b03261b355ef6d8d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 21 Aug 2025 11:55:24 -0400 Subject: [PATCH 45/91] tweak naming --- programs/drift/src/math/margin.rs | 18 +++++++++--------- programs/drift/src/state/margin_calculation.rs | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index ece1767bc4..642f3388f1 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -311,7 +311,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( token_value = 0; } - calculation.add_isolated_total_collateral(token_value)?; + calculation.add_cross_margin_total_collateral(token_value)?; calculation.update_all_deposit_oracles_valid(oracle_valid); @@ -329,7 +329,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( token_value, token_value, MarketIdentifier::spot(0), @@ -382,7 +382,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )?; } - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( spot_position.margin_requirement_for_open_orders()?, 0, MarketIdentifier::spot(spot_market.market_index), @@ -398,7 +398,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_weighted_token_value = 0; } - calculation.add_isolated_total_collateral( + calculation.add_cross_margin_total_collateral( worst_case_weighted_token_value.cast::()?, )?; @@ -423,7 +423,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.market_index, )?; - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( worst_case_weighted_token_value.unsigned_abs(), worst_case_token_value.unsigned_abs(), MarketIdentifier::spot(spot_market.market_index), @@ -461,13 +461,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( } calculation - .add_isolated_total_collateral(worst_case_orders_value.cast::()?)?; + .add_cross_margin_total_collateral(worst_case_orders_value.cast::()?)?; #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(worst_case_orders_value)?; } Ordering::Less => { - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( worst_case_orders_value.unsigned_abs(), worst_case_orders_value.unsigned_abs(), MarketIdentifier::spot(0), @@ -572,13 +572,13 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(quote_token_value)?; } else { - calculation.add_isolated_margin_requirement( + calculation.add_cross_margin_margin_requirement( perp_margin_requirement, worst_case_liability_value, MarketIdentifier::perp(market.market_index), )?; - calculation.add_isolated_total_collateral(weighted_pnl)?; + calculation.add_cross_margin_total_collateral(weighted_pnl)?; } #[cfg(feature = "drift-rs")] diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index ac689ddd66..2bf4f4abd3 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -246,7 +246,7 @@ impl MarginCalculation { } } - pub fn add_isolated_total_collateral(&mut self, total_collateral: i128) -> DriftResult { + pub fn add_cross_margin_total_collateral(&mut self, total_collateral: i128) -> DriftResult { self.total_collateral = self.total_collateral.safe_add(total_collateral)?; if self.context.margin_buffer > 0 && total_collateral < 0 { @@ -259,7 +259,7 @@ impl MarginCalculation { Ok(()) } - pub fn add_isolated_margin_requirement( + pub fn add_cross_margin_margin_requirement( &mut self, margin_requirement: u128, liability_value: u128, From 6ddbaf8f6435c95bcf10fd437aab026bfe46c1bf Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 25 Aug 2025 15:22:22 -0300 Subject: [PATCH 46/91] add test to make sure false liquidaiton wont be triggered --- .../drift/src/controller/liquidation/tests.rs | 222 +++++++++++++++++- programs/drift/src/math/margin.rs | 5 +- 2 files changed, 224 insertions(+), 3 deletions(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index cf0789520c..08f360d1f1 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -8914,12 +8914,13 @@ mod liquidate_dust_spot_market { pub mod liquidate_isolated_perp { use crate::math::constants::ONE_HOUR; use crate::state::state::State; + use std::collections::BTreeSet; use std::str::FromStr; use anchor_lang::Owner; use solana_program::pubkey::Pubkey; - use crate::controller::liquidation::liquidate_perp; + use crate::controller::liquidation::{liquidate_perp, liquidate_spot}; use crate::controller::position::PositionDirection; use crate::create_anchor_account_info; use crate::error::ErrorCode; @@ -9807,4 +9808,223 @@ pub mod liquidate_isolated_perp { assert!(!user.is_isolated_margin_being_liquidated(0).unwrap()); assert_eq!(market_after.amm.total_liquidation_fee, 41787043); } + + #[test] + pub fn unhealthy_isolated_perp_doesnt_cause_cross_margin_liquidation() { + 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 mut market2 = market.clone(); + market2.market_index = 1; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + initial_liability_weight: SPOT_WEIGHT_PRECISION, + maintenance_liability_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 mut spot_market2 = spot_market.clone(); + spot_market2.market_index = 1; + create_anchor_account_info!(spot_market2, SpotMarket, spot_market2_account_info); + + let spot_market_account_infos = vec![spot_market_account_info, spot_market2_account_info]; + let mut spot_market_set = BTreeSet::default(); + spot_market_set.insert(0); + spot_market_set.insert(1); + let spot_market_map = SpotMarketMap::load( + &spot_market_set, + &mut spot_market_account_infos.iter().peekable(), + ) + .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: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + user.spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 1 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + user.perp_positions[1] = PerpPosition { + market_index: 1, + 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, + ..PerpPosition::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() + }; + let result = liquidate_perp( + 1, + 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, + ); + + assert_eq!(result, Err(ErrorCode::SufficientCollateral)); + + let result = liquidate_spot( + 0, + 1, + 1, + None, + &mut user, + &user_key, + &mut user_stats, + &mut liquidator, + &liquidator_key, + &mut liquidator_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + &state, + ); + + assert_eq!(result, Err(ErrorCode::SufficientCollateral)); + + 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(); + + assert_eq!(margin_calculation.meets_cross_margin_requirement(), true); + + assert_eq!(margin_calculation.meets_margin_requirement(), false); + + assert_eq!( + margin_calculation + .meets_isolated_margin_requirement(0) + .unwrap(), + false + ); + } } diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 642f3388f1..dd9dfe499a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -460,8 +460,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( worst_case_orders_value = 0; } - calculation - .add_cross_margin_total_collateral(worst_case_orders_value.cast::()?)?; + calculation.add_cross_margin_total_collateral( + worst_case_orders_value.cast::()?, + )?; #[cfg(feature = "drift-rs")] calculation.add_spot_asset_value(worst_case_orders_value)?; From 2db290770fc89e37732e4a74e3c06752150b1b1f Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Mon, 25 Aug 2025 16:10:48 -0300 Subject: [PATCH 47/91] test meets withdraw --- programs/drift/src/instructions/user.rs | 14 +- programs/drift/src/state/user.rs | 49 +----- programs/drift/src/state/user/tests.rs | 207 ++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 49 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index b2380d685e..1f3f4070e5 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2223,12 +2223,15 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( drop(spot_market); - user.meets_withdraw_margin_requirement_for_isolated_perp_position( + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - perp_market_index, + 0, + 0, + user_stats, + now, )?; if user.is_isolated_margin_being_liquidated(perp_market_index)? { @@ -2344,12 +2347,15 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( )?; } - user.meets_withdraw_margin_requirement_for_isolated_perp_position( + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( &perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, - perp_market_index, + 0, + 0, + &mut user_stats, + now, )?; if user.is_isolated_margin_being_liquidated(perp_market_index)? { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 6acf51b71f..d7c766862c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -643,9 +643,8 @@ impl User { 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 + "margin calculation: {:?}", + calculation )?; user_stats.update_fuel_bonus( @@ -698,9 +697,8 @@ impl User { 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 + "margin calculation: {:?}", + calculation )?; user_stats.update_fuel_bonus( @@ -714,45 +712,6 @@ 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, - isolated_perp_position_market_index: u16, - ) -> DriftResult { - let strict = margin_requirement_type == MarginRequirementType::Initial; - let context = MarginContext::standard(margin_requirement_type).strict(strict); - - let calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - self, - perp_market_map, - spot_market_map, - oracle_map, - context, - )?; - - let isolated_margin_calculation = - calculation.get_isolated_margin_calculation(isolated_perp_position_market_index)?; - - validate!( - calculation.all_liability_oracles_valid, - ErrorCode::InvalidOracle, - "User attempting to withdraw with outstanding liabilities when an oracle is invalid" - )?; - - validate!( - isolated_margin_calculation.meets_margin_requirement(), - ErrorCode::InsufficientCollateral, - "User attempting to withdraw where total_collateral {} is below initial_margin_requirement {}", - isolated_margin_calculation.total_collateral, - isolated_margin_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); diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 53bac03415..03a98eb77c 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2411,3 +2411,210 @@ mod force_get_isolated_perp_position_mut { } } } + +pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { + use crate::math::constants::ONE_HOUR; + use crate::state::state::State; + use std::collections::BTreeSet; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::{liquidate_perp, liquidate_spot}; + 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 unhealthy_isolated_perp_blocks_withdraw() { + 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 mut market2 = market.clone(); + market2.market_index = 1; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + initial_liability_weight: SPOT_WEIGHT_PRECISION, + maintenance_liability_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 mut spot_market2 = spot_market.clone(); + spot_market2.market_index = 1; + create_anchor_account_info!(spot_market2, SpotMarket, spot_market2_account_info); + + let spot_market_account_infos = vec![spot_market_account_info, spot_market2_account_info]; + let mut spot_market_set = BTreeSet::default(); + spot_market_set.insert(0); + spot_market_set.insert(1); + let spot_market_map = SpotMarketMap::load( + &spot_market_set, + &mut spot_market_account_infos.iter().peekable(), + ) + .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: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + user.spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 1 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + user.perp_positions[1] = PerpPosition { + market_index: 1, + 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, + ..PerpPosition::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() + }; + + let result = user.meets_withdraw_margin_requirement_and_increment_fuel_bonus(&perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, 1, 0, &mut user_stats, now); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + + let result: Result = user.meets_withdraw_margin_requirement_and_increment_fuel_bonus_swap(&perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, 0, 0, 0, 0, &mut user_stats, now); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } +} \ No newline at end of file From 8314bbe5ad0efeef7ed8cee8cbd218cd0659fc8d Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 11:18:39 -0300 Subject: [PATCH 48/91] change is bankrupt --- programs/drift/src/state/user.rs | 15 +++++++++++++++ programs/drift/src/validation/user.rs | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d7c766862c..c91d46ccce 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -142,6 +142,10 @@ impl User { self.status & (UserStatus::BeingLiquidated as u8 | UserStatus::Bankrupt as u8) > 0 } + pub fn is_bankrupt(&self) -> bool { + self.is_cross_margin_bankrupt() || self.has_isolated_margin_bankrupt() + } + pub fn is_cross_margin_bankrupt(&self) -> bool { self.status & (UserStatus::Bankrupt as u8) > 0 } @@ -442,6 +446,7 @@ impl User { pub fn exit_isolated_margin_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); + perp_position.position_flag &= !(PositionFlag::Bankruptcy as u8); Ok(()) } @@ -450,6 +455,12 @@ impl User { Ok(perp_position.is_being_liquidated()) } + pub fn has_isolated_margin_bankrupt(&self) -> bool { + self.perp_positions + .iter() + .any(|position| position.is_isolated() && position.is_bankrupt()) + } + pub fn enter_isolated_margin_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); @@ -1238,6 +1249,10 @@ impl PerpPosition { self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) != 0 } + + pub fn is_bankrupt(&self) -> bool { + self.position_flag & PositionFlag::Bankruptcy as u8 == PositionFlag::Bankruptcy as u8 + } } impl SpotBalance for PerpPosition { diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index f19851b35f..3f527fed0f 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_cross_margin_bankrupt(), + !user.is_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_cross_margin_bankrupt(), + !user.is_bankrupt(), ErrorCode::UserNotInactive, "user bankrupt" )?; From 654683cc505e0431192ce69ffbc9bcb036ec4d9b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 11:33:25 -0300 Subject: [PATCH 49/91] more --- programs/drift/src/controller/liquidation.rs | 25 +++++--------------- programs/drift/src/instructions/user.rs | 24 +++++++++---------- programs/drift/src/state/user.rs | 2 +- 3 files changed, 19 insertions(+), 32 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 987bd4a80c..0f1fa8dcc0 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1411,9 +1411,7 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() - && margin_calculation.meets_cross_margin_requirement() - { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -1946,9 +1944,7 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() - && margin_calculation.meets_cross_margin_requirement() - { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -2529,9 +2525,7 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() - && margin_calculation.meets_cross_margin_requirement() - { + if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -3722,19 +3716,12 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - // todo handle this - if !user.is_cross_margin_being_liquidated() - && !margin_calculation.meets_cross_margin_requirement() - { + if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_margin_calculation) in - margin_calculation.isolated_margin_calculations.iter() - { - if !user.is_isolated_margin_being_liquidated(*market_index)? - && !isolated_margin_calculation.meets_margin_requirement() - { + for (market_index, isolated_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { + if !user.is_isolated_margin_being_liquidated(*market_index)? && !isolated_margin_calculation.meets_margin_requirement() { user.enter_isolated_margin_liquidation(*market_index, slot)?; } } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 1f3f4070e5..5a4a9cc3f3 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_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_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())?; @@ -712,7 +712,7 @@ pub fn handle_withdraw<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let spot_market_is_reduce_only = { let spot_market = &mut spot_market_map.get_ref_mut(&market_index)?; @@ -882,13 +882,13 @@ pub fn handle_transfer_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_cross_margin_bankrupt(), + !to_user.is_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_cross_margin_bankrupt(), + !from_user.is_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1104,12 +1104,12 @@ pub fn handle_transfer_pools<'c: 'info, 'info>( let clock = Clock::get()?; validate!( - !to_user.is_cross_margin_bankrupt(), + !to_user.is_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_cross_margin_bankrupt(), + !from_user.is_bankrupt(), ErrorCode::UserBankrupt, "from_user bankrupt" )?; @@ -1577,13 +1577,13 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !to_user.is_cross_margin_bankrupt(), + !to_user.is_bankrupt(), ErrorCode::UserBankrupt, "to_user bankrupt" )?; validate!( - !from_user.is_cross_margin_bankrupt(), + !from_user.is_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_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let perp_market = perp_market_map.get_ref(&perp_market_index)?; @@ -2089,7 +2089,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let now = clock.unix_timestamp; validate!( - !user.is_cross_margin_bankrupt(), + !user.is_bankrupt(), ErrorCode::UserBankrupt, "user bankrupt" )?; @@ -2297,7 +2297,7 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( let mint = get_token_mint(remaining_accounts_iter)?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; { let perp_market = &perp_market_map.get_ref(&perp_market_index)?; @@ -3796,7 +3796,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_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; math::liquidation::validate_user_not_being_liquidated( &mut user, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index c91d46ccce..0e9d24abbc 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -485,7 +485,7 @@ impl User { } pub fn update_last_active_slot(&mut self, slot: u64) { - if !self.is_cross_margin_being_liquidated() { + if !self.is_being_liquidated() { self.last_active_slot = slot; } self.idle = false; From 4cab732db19193be344b6210ec8dc20654d01962 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 11:40:46 -0300 Subject: [PATCH 50/91] update uses of exit isolated liquidaiton --- programs/drift/src/instructions/user.rs | 27 ++----------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 5a4a9cc3f3..b4da56aacb 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -2175,19 +2175,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( } if user.is_isolated_margin_being_liquidated(perp_market_index)? { - // try to update liquidation status if user is was already being liq'd - let is_being_liquidated = is_isolated_margin_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_margin_liquidation(perp_market_index)?; - } + user.exit_isolated_margin_liquidation(perp_market_index)?; } } else { let mut spot_market = spot_market_map.get_ref_mut(&spot_market_index)?; @@ -2239,18 +2227,7 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( } 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, - &perp_market_map, - &spot_market_map, - &mut oracle_map, - state.liquidation_margin_buffer_ratio, - )?; - - if !is_being_liquidated { - user.exit_cross_margin_liquidation(); - } + user.exit_cross_margin_liquidation(); } } From 6a6a150f7cd2bc2417c4569751b1d75afcd4cd33 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 15:30:32 -0300 Subject: [PATCH 51/91] moar --- programs/drift/src/controller/liquidation/tests.rs | 8 ++++---- programs/drift/src/controller/orders.rs | 14 +++++++------- programs/drift/src/controller/pnl.rs | 4 ++-- programs/drift/src/instructions/admin.rs | 2 +- programs/drift/src/instructions/user.rs | 10 +++++----- programs/drift/src/math/liquidation.rs | 2 +- programs/drift/src/state/user/tests.rs | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 08f360d1f1..cdc97f60c9 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -17,7 +17,7 @@ pub mod liquidate_perp { 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::liquidation::is_cross_margin_being_liquidated; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; @@ -904,7 +904,7 @@ pub mod liquidate_perp { ) .unwrap(); assert_eq!(margin_req, 140014010000); - assert!(!is_user_being_liquidated( + assert!(!is_cross_margin_being_liquidated( &user, &perp_market_map, &spot_market_map, @@ -930,7 +930,7 @@ pub mod liquidate_perp { ) .unwrap(); assert_eq!(margin_req2, 1040104010000); - assert!(is_user_being_liquidated( + assert!(is_cross_margin_being_liquidated( &user, &perp_market_map, &spot_market_map, @@ -8931,7 +8931,7 @@ pub mod liquidate_isolated_perp { 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::liquidation::is_cross_margin_being_liquidated; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 0f3f7ac047..fffa26e4f8 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -117,7 +117,7 @@ pub fn place_perp_order( )?; } - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; if params.is_update_high_leverage_mode() { if let Some(config) = high_leverage_mode_config { @@ -1034,7 +1034,7 @@ pub fn fill_perp_order( "Order must be triggered first" )?; - if user.is_cross_margin_bankrupt() { + if user.is_bankrupt() { msg!("user is bankrupt"); return Ok((0, 0)); } @@ -2989,7 +2989,7 @@ pub fn trigger_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_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( @@ -3207,7 +3207,7 @@ pub fn force_cancel_orders( ErrorCode::UserIsBeingLiquidated )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -3420,7 +3420,7 @@ pub fn place_spot_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; if options.try_expire_orders { expire_orders( @@ -3750,7 +3750,7 @@ pub fn fill_spot_order( "Order must be triggered first" )?; - if user.is_cross_margin_bankrupt() { + if user.is_bankrupt() { msg!("User is bankrupt"); return Ok(0); } @@ -5243,7 +5243,7 @@ pub fn trigger_spot_order( state.liquidation_margin_buffer_ratio, )?; - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_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 92974b04e1..0dbc3d67f1 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -56,7 +56,7 @@ pub fn settle_pnl( meets_margin_requirement: Option, mode: SettlePnlMode, ) -> DriftResult { - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; let now = clock.unix_timestamp; { let spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; @@ -343,7 +343,7 @@ pub fn settle_expired_position( clock: &Clock, state: &State, ) -> DriftResult { - validate!(!user.is_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_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/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 00e0e645fd..669a6d95b2 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_cross_margin_bankrupt(), ErrorCode::UserBankrupt)?; + validate!(!user.is_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 b4da56aacb..cb53da3dc4 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -34,7 +34,7 @@ use crate::instructions::optional_accounts::{ use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; use crate::math::liquidation::is_isolated_margin_being_liquidated; -use crate::math::liquidation::is_user_being_liquidated; +use crate::math::liquidation::is_cross_margin_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ @@ -616,7 +616,7 @@ pub fn handle_deposit<'c: 'info, 'info>( drop(spot_market); 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( + let is_being_liquidated = is_cross_margin_being_liquidated( user, &perp_market_map, &spot_market_map, @@ -3515,7 +3515,7 @@ pub fn handle_update_user_reduce_only( let mut user = load_mut!(ctx.accounts.user)?; validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::LiquidationsOngoing )?; @@ -3531,7 +3531,7 @@ pub fn handle_update_user_advanced_lp( let mut user = load_mut!(ctx.accounts.user)?; validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::LiquidationsOngoing )?; @@ -3547,7 +3547,7 @@ pub fn handle_update_user_protected_maker_orders( let mut user = load_mut!(ctx.accounts.user)?; validate!( - !user.is_cross_margin_being_liquidated(), + !user.is_being_liquidated(), ErrorCode::LiquidationsOngoing )?; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 09d5b2da20..f11709a30d 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -198,7 +198,7 @@ pub fn calculate_asset_transfer_for_liability_transfer( Ok(asset_transfer) } -pub fn is_user_being_liquidated( +pub fn is_cross_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 03a98eb77c..e01024d853 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2432,7 +2432,7 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { 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::liquidation::is_cross_margin_being_liquidated; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; From 7c461876d14822b44bac35b4a883aa33cbff7f75 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 16:23:15 -0300 Subject: [PATCH 52/91] moar --- programs/drift/src/controller/liquidation.rs | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 0f1fa8dcc0..67d93ede70 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -107,9 +107,9 @@ pub fn liquidate_perp( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; validate!( @@ -761,9 +761,9 @@ pub fn liquidate_perp_with_fill( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; let market = perp_market_map.get_ref(&market_index)?; @@ -1206,9 +1206,9 @@ pub fn liquidate_spot( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -1790,9 +1790,9 @@ pub fn liquidate_spot_with_swap_begin( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; let asset_spot_market = spot_market_map.get_ref(&asset_market_index)?; @@ -2342,9 +2342,9 @@ pub fn liquidate_borrow_for_perp_pnl( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; validate!( @@ -2826,9 +2826,9 @@ pub fn liquidate_perp_pnl_for_deposit( )?; validate!( - !liquidator.is_being_liquidated(), + !liquidator.is_bankrupt(), ErrorCode::UserBankrupt, - "liquidator being liquidated", + "liquidator bankrupt", )?; validate!( @@ -3319,9 +3319,9 @@ pub fn resolve_perp_bankruptcy( )?; validate!( - !liquidator.is_being_liquidated(), - ErrorCode::UserIsBeingLiquidated, - "liquidator being liquidated", + !liquidator.is_bankrupt(), + ErrorCode::UserBankrupt, + "liquidator bankrupt", )?; let market = perp_market_map.get_ref(&market_index)?; @@ -3537,9 +3537,9 @@ pub fn resolve_spot_bankruptcy( )?; validate!( - !liquidator.is_being_liquidated(), - ErrorCode::UserIsBeingLiquidated, - "liquidator being liquidated", + !liquidator.is_bankrupt(), + ErrorCode::UserBankrupt, + "liquidator bankrupt", )?; let market = spot_market_map.get_ref(&market_index)?; From 51ae2eb5b6943fd6a8e5195328ecbe7c384fe12b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 16:39:28 -0300 Subject: [PATCH 53/91] reduce diff --- programs/drift/src/controller/liquidation.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 67d93ede70..08c95ef478 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3324,6 +3324,12 @@ pub fn resolve_perp_bankruptcy( "liquidator bankrupt", )?; + validate!( + !liquidator.is_being_liquidated(), + ErrorCode::UserIsBeingLiquidated, + "liquidator being liquidated", + )?; + let market = perp_market_map.get_ref(&market_index)?; validate!( @@ -3542,6 +3548,12 @@ pub fn resolve_spot_bankruptcy( "liquidator bankrupt", )?; + validate!( + !liquidator.is_being_liquidated(), + ErrorCode::UserIsBeingLiquidated, + "liquidator being liquidated", + )?; + let market = spot_market_map.get_ref(&market_index)?; validate!( @@ -3701,6 +3713,12 @@ pub fn set_user_status_to_being_liquidated( slot: u64, state: &State, ) -> DriftResult { + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt", + )?; + validate!( !user.is_being_liquidated(), ErrorCode::UserIsBeingLiquidated, From bae1b6b28e39de05704641994e94d3050c6c52ce Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 17:27:47 -0300 Subject: [PATCH 54/91] moar --- programs/drift/src/controller/liquidation.rs | 24 ++++++++++++----- programs/drift/src/instructions/user.rs | 23 +++++++--------- programs/drift/src/math/liquidation.rs | 1 - programs/drift/src/state/liquidation_mode.rs | 20 +++++++------- .../drift/src/state/margin_calculation.rs | 2 ++ programs/drift/src/state/user.rs | 7 +++-- programs/drift/src/state/user/tests.rs | 27 ++++++++++++++++--- 7 files changed, 67 insertions(+), 37 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 08c95ef478..7c6854d87d 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -1411,7 +1411,9 @@ pub fn liquidate_spot( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -1944,7 +1946,9 @@ pub fn liquidate_spot_with_swap_begin( now, )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation: {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -2525,7 +2529,9 @@ pub fn liquidate_borrow_for_perp_pnl( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && margin_calculation.meets_cross_margin_requirement() + { msg!("margin calculation {:?}", margin_calculation); return Err(ErrorCode::SufficientCollateral); } else if user.is_cross_margin_being_liquidated() @@ -3734,12 +3740,18 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; - if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { + if !user.is_cross_margin_being_liquidated() + && !margin_calculation.meets_cross_margin_requirement() + { user.enter_cross_margin_liquidation(slot)?; } - for (market_index, isolated_margin_calculation) in margin_calculation.isolated_margin_calculations.iter() { - if !user.is_isolated_margin_being_liquidated(*market_index)? && !isolated_margin_calculation.meets_margin_requirement() { + for (market_index, isolated_margin_calculation) in + margin_calculation.isolated_margin_calculations.iter() + { + if !user.is_isolated_margin_being_liquidated(*market_index)? + && !isolated_margin_calculation.meets_margin_requirement() + { user.enter_isolated_margin_liquidation(*market_index, slot)?; } } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index cb53da3dc4..e901716634 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -33,8 +33,8 @@ use crate::instructions::optional_accounts::{ }; use crate::instructions::SpotFulfillmentType; use crate::math::casting::Cast; -use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::liquidation::is_cross_margin_being_liquidated; +use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ @@ -2060,6 +2060,12 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( }; emit!(deposit_record); + 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(()) @@ -3514,10 +3520,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_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_reduce_only_status(reduce_only)?; Ok(()) @@ -3530,10 +3533,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_being_liquidated(), ErrorCode::LiquidationsOngoing)?; user.update_advanced_lp_status(advanced_lp)?; Ok(()) @@ -3546,10 +3546,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_being_liquidated(), ErrorCode::LiquidationsOngoing)?; validate!( protected_maker_orders != user.is_protected_maker(), diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index f11709a30d..c1adc5ff2d 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -263,7 +263,6 @@ pub fn validate_user_not_being_liquidated( Ok(()) } -// todo check if this is corrects pub fn is_isolated_margin_being_liquidated( user: &User, market_map: &PerpMarketMap, diff --git a/programs/drift/src/state/liquidation_mode.rs b/programs/drift/src/state/liquidation_mode.rs index a388e9c761..d509973a0c 100644 --- a/programs/drift/src/state/liquidation_mode.rs +++ b/programs/drift/src/state/liquidation_mode.rs @@ -12,7 +12,7 @@ use crate::{ margin::calculate_user_safest_position_tiers, safe_unwrap::SafeUnwrap, }, - state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}, + state::margin_calculation::MarginCalculation, validate, LIQUIDATION_PCT_PRECISION, QUOTE_SPOT_MARKET_INDEX, }; @@ -296,11 +296,11 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { fn calculate_max_pct_to_liquidate( &self, - user: &User, - margin_shortage: u128, - slot: u64, - initial_pct_to_liquidate: u128, - liquidation_duration: u128, + _user: &User, + _margin_shortage: u128, + _slot: u64, + _initial_pct_to_liquidate: u128, + _liquidation_duration: u128, ) -> DriftResult { Ok(LIQUIDATION_PCT_PRECISION) } @@ -340,7 +340,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { )) } - fn validate_spot_position(&self, user: &User, asset_market_index: u16) -> DriftResult<()> { + fn validate_spot_position(&self, _user: &User, asset_market_index: u16) -> DriftResult<()> { validate!( asset_market_index == QUOTE_SPOT_MARKET_INDEX, ErrorCode::CouldNotFindSpotPosition, @@ -365,9 +365,9 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { fn calculate_user_safest_position_tiers( &self, - user: &User, + _user: &User, perp_market_map: &PerpMarketMap, - spot_market_map: &SpotMarketMap, + _spot_market_map: &SpotMarketMap, ) -> DriftResult<(AssetTier, ContractTier)> { let contract_tier = perp_market_map.get_ref(&self.market_index)?.contract_tier; @@ -379,7 +379,7 @@ impl LiquidatePerpMode for IsolatedMarginLiquidatePerpMode { user: &mut User, token_amount: u128, spot_market: &mut SpotMarket, - cumulative_deposit_delta: Option, + _cumulative_deposit_delta: Option, ) -> DriftResult<()> { let perp_position = user.force_get_isolated_perp_position_mut(self.market_index)?; diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 2bf4f4abd3..8003c6dadd 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -216,6 +216,7 @@ impl IsolatedMarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_total_collateral_plus_buffer())? + .min(0) .unsigned_abs()) } } @@ -505,6 +506,7 @@ impl MarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_cross_total_collateral_plus_buffer())? + .min(0) .unsigned_abs()) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 0e9d24abbc..d1e75f2623 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1233,8 +1233,7 @@ 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 > 0 } pub fn get_isolated_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { @@ -1247,11 +1246,11 @@ impl PerpPosition { pub fn is_being_liquidated(&self) -> bool { self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) - != 0 + > 0 } pub fn is_bankrupt(&self) -> bool { - self.position_flag & PositionFlag::Bankruptcy as u8 == PositionFlag::Bankruptcy as u8 + self.position_flag & PositionFlag::Bankruptcy as u8 > 0 } } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index e01024d853..2b9b8394ff 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2609,12 +2609,33 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { ..Default::default() }; - let result = user.meets_withdraw_margin_requirement_and_increment_fuel_bonus(&perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, 1, 0, &mut user_stats, now); + let result = user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + 1, + 0, + &mut user_stats, + now, + ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); - let result: Result = user.meets_withdraw_margin_requirement_and_increment_fuel_bonus_swap(&perp_market_map, &spot_market_map, &mut oracle_map, MarginRequirementType::Initial, 0, 0, 0, 0, &mut user_stats, now); + let result: Result = user + .meets_withdraw_margin_requirement_and_increment_fuel_bonus_swap( + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginRequirementType::Initial, + 0, + 0, + 0, + 0, + &mut user_stats, + now, + ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); } -} \ No newline at end of file +} From d2f08ea12448b5aa87dafc38f165ca2f927ed8fc Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 26 Aug 2025 18:25:58 -0300 Subject: [PATCH 55/91] modularize some for tests --- .../drift/src/controller/isolated_position.rs | 417 ++++++++++++++++++ programs/drift/src/controller/mod.rs | 1 + programs/drift/src/instructions/user.rs | 350 ++------------- 3 files changed, 451 insertions(+), 317 deletions(-) create mode 100644 programs/drift/src/controller/isolated_position.rs diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs new file mode 100644 index 0000000000..6159497e5d --- /dev/null +++ b/programs/drift/src/controller/isolated_position.rs @@ -0,0 +1,417 @@ +use std::cell::RefMut; + +use anchor_lang::prelude::*; +use crate::controller::spot_balance::update_spot_balances; +use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; +use crate::error::ErrorCode; +use crate::math::casting::Cast; +use crate::math::liquidation::is_isolated_margin_being_liquidated; +use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; +use crate::state::events::{ + DepositDirection, DepositExplanation, DepositRecord, +}; +use crate::state::perp_market::MarketStatus; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::oracle_map::OracleMap; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::spot_market::SpotBalanceType; +use crate::state::state::State; +use crate::state::user::{ + User,UserStats, +}; +use crate::validate; +use crate::controller; +use crate::get_then_update_id; + +pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( + user_key: Pubkey, + user: &mut RefMut, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + slot: u64, + now: i64, + state: &State, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> Result<()> { + validate!( + amount != 0, + ErrorCode::InsufficientDeposit, + "deposit amount cant be 0", + )?; + + 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); + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + // try to update liquidation status if user is was already being liq'd + let is_being_liquidated = is_isolated_margin_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + perp_market_index, + state.liquidation_margin_buffer_ratio, + )?; + + if !is_being_liquidated { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } + + user.update_last_active_slot(slot); + + let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + + 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); + + Ok(()) +} + +pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( + user: &mut RefMut, + user_stats: &mut RefMut, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + slot: u64, + now: i64, + spot_market_index: u16, + perp_market_index: u16, + amount: i64, +) -> Result<()> { + validate!( + amount != 0, + ErrorCode::DefaultError, + "transfer amount cant be 0", + )?; + + validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + + { + 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, + ErrorCode::InvalidIsolatedPerpMarket, + "perp market quote spot market index ({}) != spot market index ({})", + 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), + now, + )?; + } + + 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, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &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, + oracle_map, + MarginRequirementType::Initial, + spot_market_index, + amount as u128, + user_stats, + now, + )?; + + validate_spot_margin_trading(user, &perp_market_map, &spot_market_map, oracle_map)?; + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + } 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_token_amount(&spot_market)?; + + validate!( + amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + 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::Deposit, + &mut spot_market, + &mut user.spot_positions[spot_position_index], + false, + None, + )?; + + update_spot_balances( + amount as u128, + &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, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + if user.is_cross_margin_being_liquidated() { + user.exit_cross_margin_liquidation(); + } + } + + user.update_last_active_slot(slot); + + Ok(()) +} + +pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( + user_key: Pubkey, + user: &mut RefMut, + user_stats: &mut RefMut, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + slot: u64, + now: i64, + spot_market_index: u16, + perp_market_index: u16, + amount: u64, +) -> Result<()> { + validate!( + amount != 0, + ErrorCode::DefaultError, + "withdraw amount cant be 0", + )?; + + 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_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, + )?; + } + + user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + &perp_market_map, + &spot_market_map, + oracle_map, + MarginRequirementType::Initial, + 0, + 0, + user_stats, + now, + )?; + + if user.is_isolated_margin_being_liquidated(perp_market_index)? { + user.exit_isolated_margin_liquidation(perp_market_index)?; + } + + 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); + + Ok(()) +} \ No newline at end of file diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index 1565eb1174..0a099f7cde 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -1,6 +1,7 @@ pub mod amm; pub mod funding; pub mod insurance; +pub mod isolated_position; pub mod liquidation; pub mod orders; pub mod pda; diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index e901716634..0e1308762b 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1934,93 +1934,21 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( 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), + controller::isolated_position::deposit_into_isolated_perp_position( + user_key, + user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, now, - )?; - - user.increment_total_deposits( + state, + spot_market_index, + perp_market_index, 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); - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - // try to update liquidation status if user is was already being liq'd - let is_being_liquidated = is_isolated_margin_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_margin_liquidation(perp_market_index)?; - } - } - - user.update_last_active_slot(slot); - - let spot_market = &mut spot_market_map.get_ref_mut(&spot_market_index)?; + let spot_market = spot_market_map.get_ref(&spot_market_index)?; controller::token::receive( &ctx.accounts.token_program, @@ -2035,32 +1963,9 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( 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); ctx.accounts.spot_market_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( &spot_market, ctx.accounts.spot_market_vault.amount, @@ -2112,132 +2017,18 @@ pub fn handle_transfer_isolated_perp_position_deposit<'c: 'info, 'info>( Some(state.oracle_guard_rails), )?; - { - 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, - ErrorCode::InvalidIsolatedPerpMarket, - "perp market quote spot market index ({}) != spot market index ({})", - 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, - )?; - } - - 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, - &mut spot_market, - &mut user.spot_positions[spot_position_index], - false, - None, - )?; - - update_spot_balances( - amount as u128, - &SpotBalanceType::Deposit, - &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, - )?; - - 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(); - } - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - } 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_token_amount(&spot_market)?; - - validate!( - amount.unsigned_abs() as u128 <= isolated_perp_position_token_amount, - ErrorCode::InsufficientCollateral, - "user has insufficient deposit for market {}", - 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::Deposit, - &mut spot_market, - &mut user.spot_positions[spot_position_index], - false, - None, - )?; - - update_spot_balances( - amount as u128, - &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, - 0, - 0, - user_stats, - now, - )?; - - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - - if user.is_cross_margin_being_liquidated() { - user.exit_cross_margin_liquidation(); - } - } - - user.update_last_active_slot(slot); + controller::isolated_position::transfer_isolated_perp_position_deposit( + user, + user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + spot_market_index, + perp_market_index, + amount, + )?; let spot_market = spot_market_map.get_ref(&spot_market_index)?; math::spot_withdraw::validate_spot_market_vault_amount( @@ -2280,96 +2071,21 @@ 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)?; - - { - 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_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, - )?; - } - - user.meets_withdraw_margin_requirement_and_increment_fuel_bonus( + controller::isolated_position::withdraw_from_isolated_perp_position( + user_key, + user, + &mut user_stats, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginRequirementType::Initial, - 0, - 0, - &mut user_stats, + slot, now, + spot_market_index, + perp_market_index, + amount, )?; - if user.is_isolated_margin_being_liquidated(perp_market_index)? { - user.exit_isolated_margin_liquidation(perp_market_index)?; - } - - 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); + let spot_market = spot_market_map.get_ref(&spot_market_index)?; controller::token::send_from_program_vault( &ctx.accounts.token_program, From ba8866a26bbdd2f912fc5c3e7093d9fc826ec341 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 15:36:54 -0300 Subject: [PATCH 56/91] add tests for the pnl for deposit liquidation --- programs/drift/src/controller/amm.rs | 2 +- .../drift/src/controller/liquidation/tests.rs | 367 ++++++++++++++++++ .../drift/src/state/margin_calculation.rs | 4 +- programs/drift/src/state/user.rs | 14 +- 4 files changed, 377 insertions(+), 10 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 5e4871b8a3..b6ed94b8cd 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -15,7 +15,7 @@ use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ - CONCENTRATION_PRECISION, FEE_ADJUSTMENT_MAX, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, + CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, K_BPS_UPDATE_SCALE, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, }; use crate::math::cp_curve::get_update_k_result; diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index cdc97f60c9..81170f25fc 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -10028,3 +10028,370 @@ pub mod liquidate_isolated_perp { ); } } + +pub mod liquidate_isolated_perp_pnl_for_deposit { + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::{liquidate_perp_pnl_for_deposit, liquidate_spot}; + use crate::create_account_info; + use crate::create_anchor_account_info; + use crate::error::ErrorCode; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_PCT_PRECISION, MARGIN_PRECISION, PEG_PRECISION, PERCENTAGE_PRECISION, + PRICE_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, + 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; + use crate::state::margin_calculation::MarginContext; + use crate::state::oracle::HistoricalOracleData; + use crate::state::oracle::OracleSource; + use crate::state::oracle_map::OracleMap; + use crate::state::perp_market::{ContractTier, MarketStatus, PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{AssetTier, SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{PositionFlag, UserStats}; + use crate::state::user::{Order, PerpPosition, SpotPosition, User, UserStatus}; + use crate::test_utils::*; + use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; + use crate::controller::liquidation::resolve_perp_bankruptcy; + + #[test] + pub fn successful_liquidation_liquidator_max_pnl_transfer() { + let now = 0_i64; + 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, + 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: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + unrealized_pnl_initial_asset_weight: 9000, + unrealized_pnl_maintenance_asset_weight: 10000, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_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: 200 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: QUOTE_PRECISION_I64, + last_oracle_price_twap_5min: QUOTE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_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: 6, + 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, + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: 0, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (sol_oracle_price.agg.price * 99 / 100), + last_oracle_price_twap_5min: (sol_oracle_price.agg.price * 99 / 100), + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_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]; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + liquidate_perp_pnl_for_deposit( + 0, + 0, + 50 * 10_u128.pow(6), // .8 + None, + &mut user, + &user_key, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + 10, + PERCENTAGE_PRECISION, + 150, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 39494950000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -50000000); + + assert_eq!( + liquidator.spot_positions[1].balance_type, + SpotBalanceType::Deposit + ); + assert_eq!(liquidator.spot_positions[0].scaled_balance, 150505050000); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -50000000); + } + + #[test] + pub fn successful_liquidation_pnl_transfer_leaves_position_bankrupt() { + let now = 0_i64; + 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, + 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: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + unrealized_pnl_initial_asset_weight: 9000, + unrealized_pnl_maintenance_asset_weight: 10000, + 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 market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_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: 200 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: QUOTE_PRECISION_I64, + last_oracle_price_twap_5min: QUOTE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_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: 6, + 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, + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: 0, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (sol_oracle_price.agg.price * 99 / 100), + last_oracle_price_twap_5min: (sol_oracle_price.agg.price * 99 / 100), + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_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]; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: -91 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let mut liquidator = User { + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + liquidate_perp_pnl_for_deposit( + 0, + 0, + 200 * 10_u128.pow(6), // .8 + None, + &mut user, + &user_key, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + slot, + MARGIN_PRECISION / 50, + PERCENTAGE_PRECISION, + 150, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].quote_asset_amount, -1900000); + assert_eq!(user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, PositionFlag::Bankrupt as u8); + + assert_eq!(liquidator.spot_positions[0].scaled_balance, 190000000000); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -89100000); + + let calc = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(MARGIN_PRECISION / 50), + ) + .unwrap(); + + assert_eq!(calc.meets_margin_requirement(), false); + + let market_after = market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 0); + + resolve_perp_bankruptcy( + 0, + &mut user, + &user_key, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + 0, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].quote_asset_amount, 0); + assert_eq!(user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, 0); + assert_eq!(user.is_being_liquidated(), false); + } +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index 8003c6dadd..d4b19ad69f 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -216,7 +216,7 @@ impl IsolatedMarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_total_collateral_plus_buffer())? - .min(0) + .max(0) .unsigned_abs()) } } @@ -506,7 +506,7 @@ impl MarginCalculation { .margin_requirement_plus_buffer .cast::()? .safe_sub(self.get_cross_total_collateral_plus_buffer())? - .min(0) + .max(0) .unsigned_abs()) } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d1e75f2623..717bbc9bb4 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -446,7 +446,7 @@ impl User { pub fn exit_isolated_margin_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); - perp_position.position_flag &= !(PositionFlag::Bankruptcy as u8); + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); Ok(()) } @@ -464,19 +464,19 @@ impl User { pub fn enter_isolated_margin_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; + perp_position.position_flag |= PositionFlag::Bankrupt as u8; Ok(()) } pub fn exit_isolated_margin_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); + perp_position.position_flag &= !(PositionFlag::Bankrupt as u8); Ok(()) } pub fn is_isolated_margin_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) + Ok(perp_position.position_flag & (PositionFlag::Bankrupt as u8) != 0) } pub fn increment_margin_freed(&mut self, margin_free: u64) -> DriftResult { @@ -1245,12 +1245,12 @@ impl PerpPosition { } pub fn is_being_liquidated(&self) -> bool { - self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankruptcy as u8) + self.position_flag & (PositionFlag::BeingLiquidated as u8 | PositionFlag::Bankrupt as u8) > 0 } pub fn is_bankrupt(&self) -> bool { - self.position_flag & PositionFlag::Bankruptcy as u8 > 0 + self.position_flag & PositionFlag::Bankrupt as u8 > 0 } } @@ -1753,7 +1753,7 @@ pub enum OrderBitFlag { pub enum PositionFlag { IsolatedPosition = 0b00000001, BeingLiquidated = 0b00000010, - Bankruptcy = 0b00000100, + Bankrupt = 0b00000100, } #[account(zero_copy(unsafe))] From 9fa04fa33bc820f311da736762d420c26d93e582 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 18:01:41 -0300 Subject: [PATCH 57/91] tests for isolated position transfer --- .../drift/src/controller/isolated_position.rs | 25 +- .../src/controller/isolated_position/tests.rs | 1112 +++++++++++++++++ programs/drift/src/instructions/user.rs | 4 +- programs/drift/src/state/user.rs | 9 +- 4 files changed, 1134 insertions(+), 16 deletions(-) create mode 100644 programs/drift/src/controller/isolated_position/tests.rs diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index 6159497e5d..6545ac65ae 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -3,7 +3,7 @@ use std::cell::RefMut; use anchor_lang::prelude::*; use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; -use crate::error::ErrorCode; +use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; @@ -23,9 +23,12 @@ use crate::validate; use crate::controller; use crate::get_then_update_id; +#[cfg(test)] +mod tests; + pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( user_key: Pubkey, - user: &mut RefMut, + user: &mut User, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -35,7 +38,7 @@ pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( spot_market_index: u16, perp_market_index: u16, amount: u64, -) -> Result<()> { +) -> DriftResult<()> { validate!( amount != 0, ErrorCode::InsufficientDeposit, @@ -154,8 +157,8 @@ pub fn deposit_into_isolated_perp_position<'c: 'info, 'info>( } pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( - user: &mut RefMut, - user_stats: &mut RefMut, + user: &mut User, + user_stats: &mut UserStats, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -164,7 +167,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( spot_market_index: u16, perp_market_index: u16, amount: i64, -) -> Result<()> { +) -> DriftResult<()> { validate!( amount != 0, ErrorCode::DefaultError, @@ -260,7 +263,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( let spot_position_index = user.force_get_spot_position_index(spot_market.market_index)?; update_spot_balances_and_cumulative_deposits( - amount as u128, + amount.abs() as u128, &SpotBalanceType::Deposit, &mut spot_market, &mut user.spot_positions[spot_position_index], @@ -269,7 +272,7 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( )?; update_spot_balances( - amount as u128, + amount.abs() as u128, &SpotBalanceType::Borrow, &mut spot_market, user.force_get_isolated_perp_position_mut(perp_market_index)?, @@ -305,8 +308,8 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>( pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( user_key: Pubkey, - user: &mut RefMut, - user_stats: &mut RefMut, + user: &mut User, + user_stats: &mut UserStats, perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, @@ -315,7 +318,7 @@ pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( spot_market_index: u16, perp_market_index: u16, amount: u64, -) -> Result<()> { +) -> DriftResult<()> { validate!( amount != 0, ErrorCode::DefaultError, diff --git a/programs/drift/src/controller/isolated_position/tests.rs b/programs/drift/src/controller/isolated_position/tests.rs new file mode 100644 index 0000000000..af29a122d9 --- /dev/null +++ b/programs/drift/src/controller/isolated_position/tests.rs @@ -0,0 +1,1112 @@ +pub mod deposit_into_isolated_perp_position { + use crate::controller::isolated_position::deposit_into_isolated_perp_position; + use crate::error::ErrorCode; + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, + LIQUIDATION_FEE_PRECISION, + PEG_PRECISION, + QUOTE_PRECISION_U64, QUOTE_PRECISION_I128, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + 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::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{ + PerpPosition, PositionFlag, User + }; + use crate::{create_anchor_account_info, test_utils::*}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_deposit_into_isolated_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + 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::default(); + + let user_key = Pubkey::default(); + + let state = State::default(); + deposit_into_isolated_perp_position( + user_key, + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + 0, + 0, + QUOTE_PRECISION_U64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 1000000000); + assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + } + + #[test] + pub fn fail_to_deposit_into_existing_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + 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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let state = State::default(); + let result = deposit_into_isolated_perp_position( + user_key, + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + &state, + 0, + 0, + QUOTE_PRECISION_U64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + +} + +pub mod transfer_isolated_perp_position_deposit { + use crate::controller::isolated_position::transfer_isolated_perp_position_deposit; + use crate::error::ErrorCode; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, + LIQUIDATION_FEE_PRECISION, + PEG_PRECISION, + QUOTE_PRECISION_I128, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + 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::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{ + PerpPosition, PositionFlag, SpotPosition, User, UserStats + }; + use crate::{create_anchor_account_info, test_utils::*, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_transfer_to_isolated_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut user_stats = UserStats::default(); + + transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_I64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 1000000000); + assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + + assert_eq!(user.spot_positions[0].scaled_balance, 0); + } + + #[test] + pub fn fail_to_transfer_to_existing_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_to_transfer_due_to_insufficient_collateral() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 2* SPOT_BALANCE_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::default(); + user.spot_positions[0] = SpotPosition { + market_index: 0, + scaled_balance: SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + 2* QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } + + #[test] + pub fn successful_transfer_from_isolated_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + + assert_eq!(user.spot_positions[0].scaled_balance, SPOT_BALANCE_PRECISION_U64); + } + + #[test] + pub fn fail_transfer_from_non_isolated_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_transfer_from_isolated_perp_position_due_to_insufficient_collateral() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + base_asset_amount: 100000, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let mut user_stats = UserStats::default(); + + let result = transfer_isolated_perp_position_deposit( + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + -QUOTE_PRECISION_I64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } +} + +pub mod withdraw_from_isolated_perp_position { + use crate::controller::isolated_position::withdraw_from_isolated_perp_position; + use crate::error::ErrorCode; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, + LIQUIDATION_FEE_PRECISION, + PEG_PRECISION, + QUOTE_PRECISION_U64, QUOTE_PRECISION_I128, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + 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::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{ + PerpPosition, PositionFlag, User, UserStats + }; + use crate::{create_anchor_account_info, test_utils::*, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + use crate::test_utils::get_pyth_price; + use crate::{create_account_info, PRICE_PRECISION_I64}; + + #[test] + pub fn successful_withdraw_from_isolated_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + + withdraw_from_isolated_perp_position( + user_key, + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_U64, + ) + .unwrap(); + + assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); + assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + } + + #[test] + pub fn withdraw_from_isolated_perp_position_fail_not_isolated_perp_position() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + open_orders: 1, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + + let result = withdraw_from_isolated_perp_position( + user_key, + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_U64, + ); + + assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); + } + + #[test] + pub fn fail_withdraw_from_isolated_perp_position_due_to_insufficient_collateral() { + 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::Active, + 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 { + status: MarketStatus::Active, + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: SPOT_BALANCE_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::default(); + user.perp_positions[0] = PerpPosition { + market_index: 0, + base_asset_amount: 100000, + isolated_position_scaled_balance: SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let user_key = Pubkey::default(); + + let mut user_stats = UserStats::default(); + + let result = withdraw_from_isolated_perp_position( + user_key, + &mut user, + &mut user_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + slot, + now, + 0, + 0, + QUOTE_PRECISION_U64, + ); + + assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); + } + +} \ No newline at end of file diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 0e1308762b..e20c7e0e3c 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1912,7 +1912,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( amount: u64, ) -> Result<()> { let user_key = ctx.accounts.user.key(); - let user = &mut load_mut!(ctx.accounts.user)?; + let mut user = load_mut!(ctx.accounts.user)?; let state = &ctx.accounts.state; let clock = Clock::get()?; @@ -1936,7 +1936,7 @@ pub fn handle_deposit_into_isolated_perp_position<'c: 'info, 'info>( controller::isolated_position::deposit_into_isolated_perp_position( user_key, - user, + &mut user, &perp_market_map, &spot_market_map, &mut oracle_map, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 717bbc9bb4..03dc229e2c 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -451,8 +451,11 @@ impl User { } pub fn is_isolated_margin_being_liquidated(&self, perp_market_index: u16) -> DriftResult { - let perp_position = self.get_isolated_perp_position(perp_market_index)?; - Ok(perp_position.is_being_liquidated()) + if let Ok(perp_position) = self.get_isolated_perp_position(perp_market_index) { + Ok(perp_position.is_being_liquidated()) + } else { + Ok(false) + } } pub fn has_isolated_margin_bankrupt(&self) -> bool { @@ -1087,7 +1090,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() && self.isolated_position_scaled_balance == 0 } pub fn is_open_position(&self) -> bool { From a732348ad67c6841aac42230decfdbcd18209e93 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 21:09:39 -0300 Subject: [PATCH 58/91] test for update spot balance --- .../src/controller/spot_balance/tests.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 291e3d6516..725d5b5a87 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -8,6 +8,7 @@ use crate::controller::spot_balance::*; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits_with_limits; use crate::create_account_info; use crate::create_anchor_account_info; +use crate::error::ErrorCode; use crate::math::constants::{ AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION, QUOTE_PRECISION_I128, @@ -31,6 +32,7 @@ use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{InsuranceFund, SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::PositionFlag; use crate::state::user::{Order, PerpPosition, SpotPosition, User}; use crate::test_utils::*; use crate::test_utils::{get_pyth_price, get_spot_positions}; @@ -1948,3 +1950,65 @@ fn check_spot_market_min_borrow_rate() { assert_eq!(accum_interest.borrow_interest, 317107433); assert_eq!(accum_interest.deposit_interest, 3171074); } + +#[test] +fn isolated_perp_position() { + let now = 30_i64; + let _slot = 0_u64; + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 100_000_000 * SPOT_BALANCE_PRECISION, //$100M usdc + borrow_balance: 0, + deposit_token_twap: QUOTE_PRECISION_U64 / 2, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + status: MarketStatus::Active, + ..SpotMarket::default() + }; + + let mut perp_position = PerpPosition { + market_index: 0, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + + let amount = QUOTE_PRECISION; + + update_spot_balances( + amount, + &SpotBalanceType::Deposit, + &mut spot_market, + &mut perp_position, + false, + ) + .unwrap(); + + assert_eq!(perp_position.isolated_position_scaled_balance, 1000000000); + assert_eq!(perp_position.get_isolated_token_amount(&spot_market).unwrap(), amount); + + update_spot_balances( + amount, + &SpotBalanceType::Borrow, + &mut spot_market, + &mut perp_position, + false, + ).unwrap(); + + assert_eq!(perp_position.isolated_position_scaled_balance, 0); + + let result = update_spot_balances( + amount, + &SpotBalanceType::Borrow, + &mut spot_market, + &mut perp_position, + false, + ); + + assert_eq!(result, Err(ErrorCode::CantUpdateSpotBalanceType)); +} \ No newline at end of file From 91baee3a8d9a1c745b543f6a7866fcc53323f93e Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 27 Aug 2025 22:04:14 -0300 Subject: [PATCH 59/91] test for settle pnl --- programs/drift/src/controller/amm.rs | 6 +- programs/drift/src/controller/amm/tests.rs | 115 +++++--- programs/drift/src/controller/pnl.rs | 12 +- programs/drift/src/controller/pnl/tests.rs | 268 +++++++++++++++++- .../drift/src/controller/position/tests.rs | 3 +- 5 files changed, 358 insertions(+), 46 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index b6ed94b8cd..5f162994ee 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -516,7 +516,7 @@ fn calculate_revenue_pool_transfer( pub fn update_pool_balances( market: &mut PerpMarket, spot_market: &mut SpotMarket, - user_quote_position: &SpotPosition, + user_quote_token_amount: i128, user_unsettled_pnl: i128, now: i64, ) -> DriftResult { @@ -664,11 +664,9 @@ pub fn update_pool_balances( let pnl_to_settle_with_user = if user_unsettled_pnl > 0 { min(user_unsettled_pnl, pnl_pool_token_amount.cast::()?) } else { - let token_amount = user_quote_position.get_signed_token_amount(spot_market)?; - // dont settle negative pnl to spot borrows when utilization is high (> 80%) let max_withdraw_amount = - -get_max_withdraw_for_market_with_token_amount(spot_market, token_amount, false)? + -get_max_withdraw_for_market_with_token_amount(spot_market, user_quote_token_amount, false)? .cast::()?; max_withdraw_amount.max(user_unsettled_pnl) diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index a2a33fd6d5..cc78f6b39d 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -286,10 +286,11 @@ fn update_pool_balances_test_high_util_borrow() { let mut spot_position = SpotPosition::default(); let unsettled_pnl = -100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -299,10 +300,11 @@ fn update_pool_balances_test_high_util_borrow() { // util is low => neg settle ok spot_market.borrow_balance = 0; let unsettled_pnl = -100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -320,10 +322,12 @@ fn update_pool_balances_test_high_util_borrow() { false, ) .unwrap(); + + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -339,10 +343,12 @@ fn update_pool_balances_test_high_util_borrow() { false, ) .unwrap(); + + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -377,12 +383,14 @@ fn update_pool_balances_test() { let spot_position = SpotPosition::default(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, 100, now).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 100, now).unwrap(); assert_eq!(to_settle_with_user, 0); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, -100, now).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, -100, now).unwrap(); assert_eq!(to_settle_with_user, -100); assert!(market.amm.fee_pool.balance() > 0); @@ -401,8 +409,9 @@ fn update_pool_balances_test() { assert_eq!(pnl_pool_token_amount, 99); assert_eq!(amm_fee_pool_token_amount, 1); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, &spot_position, 100, now).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 100, now).unwrap(); assert_eq!(to_settle_with_user, 99); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), @@ -420,7 +429,8 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 1); market.amm.total_fee_minus_distributions = 0; - update_pool_balances(&mut market, &mut spot_market, &spot_position, -1, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, -1, now).unwrap(); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), &spot_market, @@ -437,10 +447,11 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 0); market.amm.total_fee_minus_distributions = 90_000 * QUOTE_PRECISION as i128; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, -(100_000 * QUOTE_PRECISION as i128), now, ) @@ -463,10 +474,11 @@ fn update_pool_balances_test() { // negative fee pool market.amm.total_fee_minus_distributions = -8_008_123_456; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, 1_000_987_789, now, ) @@ -561,7 +573,8 @@ fn update_pool_balances_fee_to_revenue_test() { ); let spot_position = SpotPosition::default(); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -576,7 +589,8 @@ fn update_pool_balances_fee_to_revenue_test() { let prev_fee_pool_2 = (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 5000000); @@ -588,12 +602,14 @@ fn update_pool_balances_fee_to_revenue_test() { assert!(spot_market.revenue_pool.scaled_balance > prev_rev_pool); market.insurance_claim.quote_max_insurance = 1; // add min insurance - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 5000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 5000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -672,7 +688,8 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { ); let spot_position = SpotPosition::default(); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -687,7 +704,8 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { let prev_fee_pool_2 = (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 1000000); @@ -701,14 +719,16 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.insurance_claim.quote_max_insurance = 1; // add min insurance market.amm.net_revenue_since_last_funding = 1; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 1000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 1000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance market.amm.net_revenue_since_last_funding = 100000000; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -804,7 +824,8 @@ fn update_pool_balances_revenue_to_fee_test() { 100 * SPOT_BALANCE_PRECISION ); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -835,7 +856,8 @@ fn update_pool_balances_revenue_to_fee_test() { ); assert_eq!(market.amm.total_fee_minus_distributions, -10000000000); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -860,7 +882,8 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market_vault_amount, 200000000); // total spot_market deposit balance unchanged during transfers // calling multiple times doesnt effect other than fee pool -> pnl pool - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -870,7 +893,8 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.amm.total_fee_withdrawn, 0); assert_eq!(spot_market.revenue_pool.scaled_balance, 0); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -886,7 +910,8 @@ fn update_pool_balances_revenue_to_fee_test() { let spot_market_backup = spot_market; let market_backup = market; - assert!(update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).is_err()); // assert is_err if any way has revenue pool above deposit balances + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).is_err()); // assert is_err if any way has revenue pool above deposit balances spot_market = spot_market_backup; market = market_backup; spot_market.deposit_balance += 9900000001000; @@ -899,7 +924,8 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market_vault_amount, 10100000001); - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market.revenue_pool.scaled_balance, 9800000001000); assert_eq!(market.amm.fee_pool.scaled_balance, 105000000000); @@ -913,7 +939,8 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, now); // calling again only does fee -> pnl pool - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -926,7 +953,8 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, now); // calling again does nothing - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -975,8 +1003,9 @@ fn update_pool_balances_revenue_to_fee_test() { spot_market.revenue_pool.scaled_balance = 9800000001000; let market_backup = market; let spot_market_backup = spot_market; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); assert!( - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).is_err() + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now + 3600).is_err() ); // assert is_err if any way has revenue pool above deposit balances market = market_backup; spot_market = spot_market_backup; @@ -993,8 +1022,9 @@ fn update_pool_balances_revenue_to_fee_test() { 33928060 + 3600 ); - assert!(update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).is_err()); // now timestamp passed is wrong - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now + 3600).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + assert!(update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).is_err()); // now timestamp passed is wrong + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now + 3600).unwrap(); assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, 33931660); assert_eq!(spot_market.insurance_fund.last_revenue_settle_ts, 33931660); @@ -1072,7 +1102,8 @@ fn update_pool_balances_revenue_to_fee_devnet_state_test() { let prev_rev_pool = spot_market.revenue_pool.scaled_balance; let prev_tfmd = market.amm.total_fee_minus_distributions; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 1821000000000); assert_eq!(market.pnl_pool.scaled_balance, 381047000000000); @@ -1163,7 +1194,8 @@ fn update_pool_balances_revenue_to_fee_new_market() { let prev_rev_pool = spot_market.revenue_pool.scaled_balance; // let prev_tfmd = market.amm.total_fee_minus_distributions; - update_pool_balances(&mut market, &mut spot_market, &spot_position, 0, now).unwrap(); + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); + update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000); // $50 @@ -1509,10 +1541,11 @@ mod revenue_pool_transfer_tests { let spot_position = SpotPosition::default(); let unsettled_pnl = -100; let now = 100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1524,10 +1557,11 @@ mod revenue_pool_transfer_tests { // revenue pool not yet settled let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1542,10 +1576,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = -169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1560,10 +1595,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = 169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1626,10 +1662,11 @@ mod revenue_pool_transfer_tests { let spot_position = SpotPosition::default(); let unsettled_pnl = -100; let now = 100; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1641,10 +1678,11 @@ mod revenue_pool_transfer_tests { // revenue pool not yet settled let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) @@ -1659,10 +1697,11 @@ mod revenue_pool_transfer_tests { market.amm.net_revenue_since_last_funding = -169; let now = 10000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 0dbc3d67f1..ba88506df6 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -227,10 +227,18 @@ pub fn settle_pnl( let user_unsettled_pnl: i128 = user.perp_positions[position_index].get_claimable_pnl(oracle_price, max_pnl_pool_excess)?; + let is_isolated_position = user.perp_positions[position_index].is_isolated(); + + let user_quote_token_amount = if is_isolated_position { + user.perp_positions[position_index].get_isolated_token_amount(spot_market)?.cast()? + } else { + user.get_quote_spot_position().get_signed_token_amount(spot_market)? + }; + let pnl_to_settle_with_user = update_pool_balances( perp_market, spot_market, - user.get_quote_spot_position(), + user_quote_token_amount, user_unsettled_pnl, now, )?; @@ -263,7 +271,7 @@ pub fn settle_pnl( ); } - if user.perp_positions[position_index].is_isolated() { + if is_isolated_position { let perp_position = &mut user.perp_positions[position_index]; if pnl_to_settle_with_user < 0 { let token_amount = perp_position.get_isolated_token_amount(spot_market)?; diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index 4a35df4e49..dcb3f02f3a 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -22,7 +22,7 @@ use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::{OracleGuardRails, State, ValidityGuardRails}; -use crate::state::user::{PerpPosition, SpotPosition, User}; +use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User}; use crate::test_utils::*; use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; use crate::{create_account_info, SettlePnlMode}; @@ -2113,3 +2113,269 @@ pub fn is_price_divergence_ok_on_invalid_oracle() { .is_price_divergence_ok_for_settle_pnl(oracle_price.agg.price) .unwrap()); } + +#[test] +pub fn isolated_perp_position_negative_pnl() { + let clock = Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let state = State { + oracle_guard_rails: OracleGuardRails { + validity: ValidityGuardRails { + slots_before_stale_for_amm: 10, // 5s + slots_before_stale_for_margin: 120, // 60s + confidence_interval_max_size: 1000, + too_volatile_ratio: 5, + }, + ..OracleGuardRails::default() + }, + ..State::default() + }; + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + 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, + base_asset_amount_long: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData { + last_oracle_price: oracle_price.agg.price, + last_oracle_price_twap_5min: oracle_price.agg.price, + last_oracle_price_twap: oracle_price.agg.price, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + pnl_pool: PoolBalance { + scaled_balance: (50 * SPOT_BALANCE_PRECISION), + market_index: QUOTE_SPOT_MARKET_INDEX, + ..PoolBalance::default() + }, + unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION.cast().unwrap(), + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let 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, + deposit_balance: 100 * SPOT_BALANCE_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 { + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: -50 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let authority = Pubkey::default(); + + let mut expected_user = user; + expected_user.perp_positions[0].quote_asset_amount = 0; + expected_user.settled_perp_pnl = -50 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].settled_pnl = -50 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].isolated_position_scaled_balance = 50 * SPOT_BALANCE_PRECISION_U64; + + let mut expected_market = market; + expected_market.pnl_pool.scaled_balance = 100 * SPOT_BALANCE_PRECISION; + expected_market.amm.quote_asset_amount = -100 * QUOTE_PRECISION_I128; + expected_market.number_of_users = 0; + + settle_pnl( + 0, + &mut user, + &authority, + &user_key, + &market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + None, + SettlePnlMode::MustSettle, + ) + .unwrap(); + + assert_eq!(expected_user, user); + assert_eq!(expected_market, *market_map.get_ref(&0).unwrap()); +} + +#[test] +pub fn isolated_perp_position_user_unsettled_positive_pnl_less_than_pool() { + let clock = Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let state = State { + oracle_guard_rails: OracleGuardRails { + validity: ValidityGuardRails { + slots_before_stale_for_amm: 10, // 5s + slots_before_stale_for_margin: 120, // 60s + confidence_interval_max_size: 1000, + too_volatile_ratio: 5, + }, + ..OracleGuardRails::default() + }, + ..State::default() + }; + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + 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, + base_asset_amount_long: BASE_PRECISION_I128, + oracle: oracle_price_key, + historical_oracle_data: HistoricalOracleData { + last_oracle_price: oracle_price.agg.price, + last_oracle_price_twap_5min: oracle_price.agg.price, + last_oracle_price_twap: oracle_price.agg.price, + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + number_of_users_with_base: 1, + number_of_users: 1, + status: MarketStatus::Active, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + pnl_pool: PoolBalance { + scaled_balance: (50 * SPOT_BALANCE_PRECISION), + market_index: QUOTE_SPOT_MARKET_INDEX, + ..PoolBalance::default() + }, + unrealized_pnl_maintenance_asset_weight: SPOT_WEIGHT_PRECISION.cast().unwrap(), + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + let 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, + deposit_balance: 100 * SPOT_BALANCE_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 { + perp_positions: get_positions(PerpPosition { + market_index: 0, + quote_asset_amount: 25 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }), + ..User::default() + }; + + let user_key = Pubkey::default(); + let authority = Pubkey::default(); + + let mut expected_user = user; + expected_user.perp_positions[0].quote_asset_amount = 0; + expected_user.settled_perp_pnl = 25 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].settled_pnl = 25 * QUOTE_PRECISION_I64; + expected_user.perp_positions[0].isolated_position_scaled_balance = 125 * SPOT_BALANCE_PRECISION_U64; + + let mut expected_market = market; + expected_market.pnl_pool.scaled_balance = 25 * SPOT_BALANCE_PRECISION; + expected_market.amm.quote_asset_amount = -175 * QUOTE_PRECISION_I128; + expected_market.number_of_users = 0; + + settle_pnl( + 0, + &mut user, + &authority, + &user_key, + &market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + None, + SettlePnlMode::MustSettle, + ) + .unwrap(); + + assert_eq!(expected_user, user); + assert_eq!(expected_market, *market_map.get_ref(&0).unwrap()); +} diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 7c41bf2591..01f36ef1e3 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -121,10 +121,11 @@ fn amm_pool_balance_liq_fees_example() { assert_eq!(new_total_fee_minus_distributions, 640881949608); let unsettled_pnl = -10_000_000; + let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); let to_settle_with_user = update_pool_balances( &mut perp_market, &mut spot_market, - &spot_position, + user_quote_token_amount, unsettled_pnl, now, ) From 101e31168b57cb70ecd6e05c480060f24d5fdd45 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 15:58:32 -0400 Subject: [PATCH 60/91] add perp position max margin --- programs/drift/src/instructions/keeper.rs | 15 ++++++++++++ programs/drift/src/instructions/user.rs | 29 +++++++++++++++++++++++ programs/drift/src/lib.rs | 9 +++++++ programs/drift/src/math/margin.rs | 8 ++++++- programs/drift/src/state/user.rs | 7 +++--- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 101ccfb84e..a94479e547 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,4 +1,5 @@ use std::cell::RefMut; +use std::collections::BTreeMap; use std::convert::TryFrom; use anchor_lang::prelude::*; @@ -2708,6 +2709,16 @@ 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 mut perp_position_max_margin_ratio_map = BTreeMap::new(); + for (index, position) in user.perp_positions.iter_mut().enumerate() { + if position.max_margin_ratio == 0 { + continue; + } + + perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); + position.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, @@ -2720,6 +2731,10 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); user.max_margin_ratio = custom_margin_ratio_before; + // loop through margin ratio map and set max margin ratio + for (index, position) in perp_position_max_margin_ratio_map.iter() { + user.perp_positions[*index].max_margin_ratio = *position; + } if margin_calc.num_perp_liabilities > 0 { for position in user.perp_positions.iter().filter(|p| !p.is_available()) { diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index e20c7e0e3c..9119f4c9bc 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -3163,6 +3163,20 @@ pub fn handle_update_user_custom_margin_ratio( Ok(()) } +pub fn handle_update_user_perp_position_custom_margin_ratio( + ctx: Context, + _sub_account_id: u16, + perp_market_index: u16, + margin_ratio: u16, +) -> Result<()> { + let mut user = load_mut!(ctx.accounts.user)?; + + let perp_position = user.force_get_perp_position_mut(perp_market_index)?; + perp_position.max_margin_ratio = margin_ratio; + Ok(()) +} + + pub fn handle_update_user_margin_trading_enabled<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, _sub_account_id: u16, @@ -4736,6 +4750,21 @@ pub struct UpdateUser<'info> { pub authority: Signer<'info>, } +#[derive(Accounts)] +#[instruction( + sub_account_id: u16, +)] +pub struct UpdateUserPerpPositionCustomMarginRatio<'info> { + #[account( + mut, + seeds = [b"user", authority.key.as_ref(), sub_account_id.to_le_bytes().as_ref()], + bump, + constraint = can_sign_for_user(&user, &authority)? + )] + pub user: AccountLoader<'info, User>, + pub authority: Signer<'info>, +} + #[derive(Accounts)] pub struct DeleteUser<'info> { #[account( diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 7edf12a991..c1b77fd047 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -382,6 +382,15 @@ pub mod drift { handle_update_user_custom_margin_ratio(ctx, _sub_account_id, margin_ratio) } + pub fn update_user_perp_position_custom_margin_ratio( + ctx: Context, + _sub_account_id: u16, + perp_market_index: u16, + margin_ratio: u16, + ) -> Result<()> { + handle_update_user_perp_position_custom_margin_ratio(ctx, _sub_account_id, perp_market_index, margin_ratio) + } + pub fn update_user_margin_trading_enabled<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, UpdateUser<'info>>, _sub_account_id: u16, diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index dd9dfe499a..9954a7ff5e 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -528,6 +528,12 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; + let perp_position_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { + market_position.max_margin_ratio as u32 + } else { + 0_u32 + }; + let (perp_margin_requirement, weighted_pnl, worst_case_liability_value, base_asset_value) = calculate_perp_position_value_and_pnl( market_position, @@ -535,7 +541,7 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( oracle_price_data, &strict_quote_price, context.margin_type, - user_custom_margin_ratio, + user_custom_margin_ratio.max(perp_position_custom_margin_ratio), user_high_leverage_mode, )?; diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 03dc229e2c..2866dcca6f 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1073,10 +1073,9 @@ pub struct PerpPosition { /// Used to settle the users lp position /// precision: QUOTE_PRECISION pub last_quote_asset_amount_per_lp: i64, - /// Settling LP position can lead to a small amount of base asset being left over smaller than step size - /// This records that remainder so it can be settled later on - /// precision: BASE_PRECISION - pub remainder_base_asset_amount: i32, + pub padding: [u8; 2], + // custom max margin ratio for perp market + pub max_margin_ratio: u16, /// The market index for the perp market pub market_index: u16, /// The number of open orders From 0bc613242d91c752fb73df6e25177fe881afd897 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 16:59:30 -0400 Subject: [PATCH 61/91] program: test for custom perp position margin ratio --- programs/drift/src/math/margin/tests.rs | 126 ++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 5a544b52d9..eab128ca39 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -1080,6 +1080,132 @@ mod calculate_margin_requirement_and_total_collateral { assert_eq!(total_collateral, 5000000000); // 100 * $100 * .5 } + #[test] + pub fn user_perp_positions_custom_margin_ratio() { + 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: 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, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let MarginCalculation { + margin_requirement, .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + assert_eq!(margin_requirement, 20000000000); + + let user = User { + max_margin_ratio: 4 * MARGIN_PRECISION, // 1x leverage + ..user + }; + + let MarginCalculation { + margin_requirement, .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial), + ) + .unwrap(); + + // user custom margin ratio should override perp position custom margin ratio + assert_eq!(margin_requirement, 40000000000); + } + #[test] pub fn user_dust_deposit() { let slot = 0_u64; From 608928fe5c3c279801010c53919c6a0fa2dec9b5 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 17:24:48 -0400 Subject: [PATCH 62/91] add test for margin calc for disable hlm --- programs/drift/src/instructions/keeper.rs | 26 +--- programs/drift/src/math/margin.rs | 38 ++++++ programs/drift/src/math/margin/tests.rs | 147 ++++++++++++++++++++++ 3 files changed, 188 insertions(+), 23 deletions(-) diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index a94479e547..2c10f97453 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -27,6 +27,7 @@ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; @@ -2706,36 +2707,15 @@ pub fn handle_disable_user_high_leverage_mode<'c: 'info, 'info>( user.margin_mode = MarginMode::Default; - let custom_margin_ratio_before = user.max_margin_ratio; - user.max_margin_ratio = 0; - - let mut perp_position_max_margin_ratio_map = BTreeMap::new(); - for (index, position) in user.perp_positions.iter_mut().enumerate() { - if position.max_margin_ratio == 0 { - continue; - } - - perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); - position.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, + let margin_calc = get_margin_calculation_for_disable_high_leverage_mode( + &mut user, &perp_market_map, &spot_market_map, &mut oracle_map, - MarginContext::standard(MarginRequirementType::Initial).margin_buffer(margin_buffer), )?; let meets_margin_calc = margin_calc.meets_margin_requirement_with_buffer(); - user.max_margin_ratio = custom_margin_ratio_before; - // loop through margin ratio map and set max margin ratio - for (index, position) in perp_position_max_margin_ratio_map.iter() { - user.perp_positions[*index].max_margin_ratio = *position; - } - 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)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 9954a7ff5e..cea5b464cf 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::MARGIN_PRECISION; use crate::{validate, PRICE_PRECISION_I128}; use crate::{validation, PRICE_PRECISION_I64}; @@ -27,6 +28,7 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{MarketType, OrderFillSimulation, PerpPosition, User}; use num_integer::Roots; use std::cmp::{max, min, Ordering}; +use std::collections::BTreeMap; use super::spot_balance::get_token_amount; @@ -895,6 +897,42 @@ pub fn validate_spot_margin_trading( Ok(()) } +pub fn get_margin_calculation_for_disable_high_leverage_mode( + user: &mut User, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, +) -> DriftResult { + let custom_margin_ratio_before = user.max_margin_ratio; + + + let mut perp_position_max_margin_ratio_map = BTreeMap::new(); + for (index, position) in user.perp_positions.iter_mut().enumerate() { + if position.max_margin_ratio == 0 { + continue; + } + + perp_position_max_margin_ratio_map.insert(index, position.max_margin_ratio); + position.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, + oracle_map, + MarginContext::standard(MarginRequirementType::Initial).margin_buffer(margin_buffer), + )?; + + user.max_margin_ratio = custom_margin_ratio_before; + for (index, position) in perp_position_max_margin_ratio_map.iter() { + user.perp_positions[*index].max_margin_ratio = *position; + } + + Ok(margin_calc) +} + pub fn calculate_user_equity( user: &User, perp_market_map: &PerpMarketMap, diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index eab128ca39..566628ad9d 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4631,3 +4631,150 @@ mod isolated_position { assert_eq!(isolated_total_collateral, -1000000000); } } + +#[cfg(test)] +mod get_margin_calculation_for_disable_high_leverage_mode { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::{create_account_info, MARGIN_PRECISION}; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; + 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, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::get_pyth_price; + use crate::create_anchor_account_info; + + #[test] + pub fn get_margin_calculation_for_disable_high_leverage_mode() { + 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 mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[1] = PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }; + perp_positions[7] = PerpPosition { + market_index: 1, + max_margin_ratio: 5 * MARGIN_PRECISION as u16, // .5x leverage + ..PerpPosition::default() + }; + + let mut user = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + max_margin_ratio: 2 * MARGIN_PRECISION as u32, // .5x leverage + ..User::default() + }; + + let user_before = user.clone(); + + get_margin_calculation_for_disable_high_leverage_mode( + &mut user, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + // should not change user + assert_eq!(user, user_before); + } +} \ No newline at end of file From fc6bebc3f3669d7b05d40de980b96df3e4cde838 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 17:41:36 -0400 Subject: [PATCH 63/91] update test name --- programs/drift/src/math/margin/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 566628ad9d..ad8bf242fb 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4658,7 +4658,7 @@ mod get_margin_calculation_for_disable_high_leverage_mode { use crate::create_anchor_account_info; #[test] - pub fn get_margin_calculation_for_disable_high_leverage_mode() { + pub fn check_user_not_changed() { let slot = 0_u64; let mut sol_oracle_price = get_pyth_price(100, 6); From 5f3b7d05cade655d5b866f9f475df5653ce8e3c8 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 18:07:37 -0400 Subject: [PATCH 64/91] make max margin ratio persist --- programs/drift/src/controller/position.rs | 11 ++++++++ programs/drift/src/state/user/tests.rs | 31 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/programs/drift/src/controller/position.rs b/programs/drift/src/controller/position.rs index 30150d3c6e..462d441f05 100644 --- a/programs/drift/src/controller/position.rs +++ b/programs/drift/src/controller/position.rs @@ -49,8 +49,19 @@ pub fn add_new_position( .position(|market_position| market_position.is_available()) .ok_or(ErrorCode::MaxNumberOfPositions)?; + let max_margin_ratio = { + let old_position = &user_positions[new_position_index]; + + if old_position.market_index == market_index { + old_position.max_margin_ratio + } else { + 0_u16 + } + }; + let new_market_position = PerpPosition { market_index, + max_margin_ratio, ..PerpPosition::default() }; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 2b9b8394ff..748e8443a7 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2639,3 +2639,34 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); } } + +mod force_get_user_perp_position_mut { + use crate::state::user::{PerpPosition, PositionFlag, User}; + + #[test] + fn test() { + let mut user = User::default(); + + let perp_position = PerpPosition { + market_index: 0, + max_margin_ratio: 1, + ..PerpPosition::default() + }; + user.perp_positions[0] = perp_position; + + // if next available slot is same market index and has max margin ratio, persist it + { + let perp_position_mut = user.force_get_perp_position_mut(0).unwrap(); + assert_eq!(perp_position_mut.max_margin_ratio, 1); + } + + // if next available slot is has max margin but different market index, dont persist it + { + let perp_position_mut = user.force_get_perp_position_mut(2).unwrap(); + assert_eq!(perp_position_mut.max_margin_ratio, 0); + } + + assert_eq!(user.perp_positions[0].market_index, 2); + assert_eq!(user.perp_positions[0].max_margin_ratio, 0); + } +} From 3c56869bc4e86692f2436bc7e5ba955080fef258 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 19:03:31 -0400 Subject: [PATCH 65/91] add liquidation mode test --- .../drift/src/controller/liquidation/tests.rs | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 81170f25fc..4ed30b318a 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -10395,3 +10395,220 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { assert_eq!(user.is_being_liquidated(), false); } } + +mod liquidation_mode { + use crate::state::liquidation_mode::{CrossMarginLiquidatePerpMode, IsolatedMarginLiquidatePerpMode, LiquidatePerpMode}; + use std::collections::BTreeSet; + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::create_account_info; + use crate::create_anchor_account_info; + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, + MARGIN_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, + 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; + use crate::state::margin_calculation::MarginContext; + use crate::state::oracle::HistoricalOracleData; + use crate::state::oracle::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::PositionFlag; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::*; + use crate::test_utils::get_pyth_price; + + #[test] + pub fn tests_meets_margin_requirements() { + let now = 0_i64; + 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, + 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: sol_oracle_price_key, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + unrealized_pnl_initial_asset_weight: 9000, + unrealized_pnl_maintenance_asset_weight: 10000, + number_of_users_with_base: 1, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + ..PerpMarket::default() + }; + create_anchor_account_info!(market, PerpMarket, market_account_info); + + let mut market2 = PerpMarket { + market_index: 1, + ..market + }; + create_anchor_account_info!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let market_map = PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + + let mut usdc_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: 200 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: QUOTE_PRECISION_I64, + last_oracle_price_twap_5min: QUOTE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_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: 6, + 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, + deposit_balance: SPOT_BALANCE_PRECISION, + borrow_balance: 0, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (sol_oracle_price.agg.price * 99 / 100), + last_oracle_price_twap_5min: (sol_oracle_price.agg.price * 99 / 100), + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_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: 200 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[0] = PerpPosition { + market_index: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + perp_positions[1] = PerpPosition { + market_index: 1, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + let user_isolated_position_being_liquidated = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + ..User::default() + }; + + let isolated_liquidation_mode = IsolatedMarginLiquidatePerpMode::new(0); + let cross_liquidation_mode = CrossMarginLiquidatePerpMode::new(0); + + let liquidation_margin_buffer_ratio = MARGIN_PRECISION / 50; + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user_isolated_position_being_liquidated, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + ).unwrap(); + + assert_eq!(cross_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), true); + assert_eq!(isolated_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), false); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 90 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[0] = PerpPosition { + market_index: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 200 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + perp_positions[1] = PerpPosition { + market_index: 1, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + ..PerpPosition::default() + }; + let user_cross_margin_being_liquidated = User { + orders: [Order::default(); 32], + perp_positions, + spot_positions, + ..User::default() + }; + + let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user_cross_margin_being_liquidated, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + ).unwrap(); + + assert_eq!(cross_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), false); + assert_eq!(isolated_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), true); + } + +} + \ No newline at end of file From bf2839e1c97ddc3b6be586a16c102ca61dbd3626 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 19:22:22 -0400 Subject: [PATCH 66/91] more tests to make sure liqudiations dont bleed over --- .../drift/src/controller/liquidation/tests.rs | 223 +++++++++++++++++- 1 file changed, 221 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 4ed30b318a..20997e4ce5 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -1,6 +1,7 @@ pub mod liquidate_perp { use crate::math::constants::ONE_HOUR; use crate::state::state::State; + use std::collections::BTreeSet; use std::str::FromStr; use anchor_lang::Owner; @@ -30,7 +31,7 @@ pub mod liquidate_perp { use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{ - MarginMode, Order, OrderStatus, OrderType, PerpPosition, SpotPosition, User, UserStats, + 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}; @@ -2375,6 +2376,196 @@ pub mod liquidate_perp { let market_after = perp_market_map.get_ref(&0).unwrap(); assert_eq!(market_after.amm.total_liquidation_fee, 750000) } + + #[test] + pub fn cross_margin_doesnt_affect_isolated_margin() { + 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 mut market2 = PerpMarket { + market_index: 1, + 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!(market2, PerpMarket, market2_account_info); + + let market_account_infos = vec![market_account_info, market2_account_info]; + let market_set = BTreeSet::default(); + let perp_market_map = PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).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 spot_positions = [SpotPosition::default(); 8]; + let mut perp_positions = [PerpPosition::default(); 8]; + perp_positions[0] = 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, + ..PerpPosition::default() + }; + perp_positions[1] = PerpPosition { + market_index: 1, + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -50 * QUOTE_PRECISION_I64, + quote_entry_amount: -50 * QUOTE_PRECISION_I64, + quote_break_even_amount: -150 * QUOTE_PRECISION_I64, + isolated_position_scaled_balance: 200 * SPOT_BALANCE_PRECISION_U64, + position_flag: PositionFlag::IsolatedPosition as u8, + ..PerpPosition::default() + }; + let mut user = User { + perp_positions, + spot_positions, + ..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() + }; + + let isolated_position_before = user.perp_positions[1].clone(); + + let result = liquidate_perp( + 1, + 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, + ); + + assert_eq!(result, Err(ErrorCode::SufficientCollateral)); + + 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 isolated_position_after = user.perp_positions[1].clone(); + + assert_eq!(isolated_position_before, isolated_position_after); + } + } pub mod liquidate_perp_with_fill { @@ -10026,6 +10217,35 @@ pub mod liquidate_isolated_perp { .unwrap(), false ); + + let spot_position_one_before = user.spot_positions[0].clone(); + let spot_position_two_before = user.spot_positions[1].clone(); + let perp_position_one_before = user.perp_positions[1].clone(); + 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 spot_position_one_after = user.spot_positions[0].clone(); + let spot_position_two_after = user.spot_positions[1].clone(); + let perp_position_one_after = user.perp_positions[1].clone(); + + assert_eq!(spot_position_one_before, spot_position_one_after); + assert_eq!(spot_position_two_before, spot_position_two_after); + assert_eq!(perp_position_one_before, perp_position_one_after); } } @@ -10429,7 +10649,6 @@ mod liquidation_mode { #[test] pub fn tests_meets_margin_requirements() { - let now = 0_i64; let slot = 0_u64; let mut sol_oracle_price = get_pyth_price(100, 6); From eb60940c806f210c9cccab68c0402a665abfd628 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 2 Sep 2025 19:25:44 -0400 Subject: [PATCH 67/91] change test name --- programs/drift/src/controller/liquidation/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 20997e4ce5..bbd15d1b2d 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2378,7 +2378,7 @@ pub mod liquidate_perp { } #[test] - pub fn cross_margin_doesnt_affect_isolated_margin() { + pub fn unhealthy_cross_margin_doesnt_cause_isolated_position_liquidation() { let now = 0_i64; let slot = 0_u64; From ba10482810ad506316ea679987325a03acabc2f5 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 09:52:00 -0400 Subject: [PATCH 68/91] fix broken cargo tests --- programs/drift/src/controller/liquidation.rs | 7 +++++++ programs/drift/src/controller/liquidation/tests.rs | 2 ++ programs/drift/src/state/user.rs | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 59ee55c86d..514a9cf97e 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3753,9 +3753,11 @@ pub fn set_user_status_to_being_liquidated( MarginContext::liquidation(liquidation_margin_buffer_ratio), )?; + let mut updated_liquidation_status = false; if !user.is_cross_margin_being_liquidated() && !margin_calculation.meets_cross_margin_requirement() { + updated_liquidation_status = true; user.enter_cross_margin_liquidation(slot)?; } @@ -3765,9 +3767,14 @@ pub fn set_user_status_to_being_liquidated( if !user.is_isolated_margin_being_liquidated(*market_index)? && !isolated_margin_calculation.meets_margin_requirement() { + updated_liquidation_status = true; user.enter_isolated_margin_liquidation(*market_index, slot)?; } } + if !updated_liquidation_status { + return Err(ErrorCode::SufficientCollateral); + } + Ok(()) } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index bbd15d1b2d..9ffe22c0b2 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -10465,6 +10465,7 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { order_step_size: 10000000, quote_asset_amount: 150 * QUOTE_PRECISION_I128, base_asset_amount_with_amm: BASE_PRECISION_I128, + base_asset_amount_long: BASE_PRECISION_I128, oracle: sol_oracle_price_key, ..AMM::default() }, @@ -10594,6 +10595,7 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { let market_after = market_map.get_ref(&0).unwrap(); assert_eq!(market_after.amm.total_liquidation_fee, 0); + drop(market_after); resolve_perp_bankruptcy( 0, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 0535b2ffad..69c2773ffd 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1112,7 +1112,7 @@ impl PerpPosition { } pub fn is_available(&self) -> bool { - !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() && self.isolated_position_scaled_balance == 0 + !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() && self.isolated_position_scaled_balance == 0 && !self.is_being_liquidated() } pub fn is_open_position(&self) -> bool { From 58df2ffc8df82ff3e8158330eceaa79693545bb9 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 09:56:30 -0400 Subject: [PATCH 69/91] cargo fmt -- --- programs/drift/src/controller/amm.rs | 13 +- programs/drift/src/controller/amm/tests.rs | 230 +++++++++++++++--- .../drift/src/controller/isolated_position.rs | 20 +- .../src/controller/isolated_position/tests.rs | 90 ++++--- .../drift/src/controller/liquidation/tests.rs | 112 ++++++--- programs/drift/src/controller/pnl.rs | 7 +- programs/drift/src/controller/pnl/tests.rs | 6 +- .../src/controller/spot_balance/tests.rs | 12 +- programs/drift/src/math/margin.rs | 11 +- programs/drift/src/math/margin/tests.rs | 2 +- programs/drift/src/state/user.rs | 6 +- 11 files changed, 370 insertions(+), 139 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 1d8ae06409..93a190a4c7 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -15,8 +15,8 @@ use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ - CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, - K_BPS_UPDATE_SCALE, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, + CONCENTRATION_PRECISION, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, K_BPS_UPDATE_SCALE, + MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, MAX_SQRT_K, }; use crate::math::cp_curve::get_update_k_result; use crate::math::repeg::get_total_fee_lower_bound; @@ -726,9 +726,12 @@ pub fn update_pool_balances( min(user_unsettled_pnl, pnl_pool_token_amount.cast::()?) } else { // dont settle negative pnl to spot borrows when utilization is high (> 80%) - let max_withdraw_amount = - -get_max_withdraw_for_market_with_token_amount(spot_market, user_quote_token_amount, false)? - .cast::()?; + let max_withdraw_amount = -get_max_withdraw_for_market_with_token_amount( + spot_market, + user_quote_token_amount, + false, + )? + .cast::()?; max_withdraw_amount.max(user_unsettled_pnl) }; diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index 4c58d86f1e..5147d85ef2 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -387,13 +387,25 @@ fn update_pool_balances_test() { let spot_position = SpotPosition::default(); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 100, now).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, 0); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, -100, now).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + -100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, -100); assert!(market.amm.fee_pool.balance() > 0); @@ -413,8 +425,14 @@ fn update_pool_balances_test() { assert_eq!(amm_fee_pool_token_amount, 1); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - let to_settle_with_user = - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 100, now).unwrap(); + let to_settle_with_user = update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 100, + now, + ) + .unwrap(); assert_eq!(to_settle_with_user, 99); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), @@ -433,7 +451,14 @@ fn update_pool_balances_test() { market.amm.total_fee_minus_distributions = 0; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, -1, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + -1, + now, + ) + .unwrap(); let amm_fee_pool_token_amount = get_token_amount( market.amm.fee_pool.balance(), &spot_market, @@ -577,7 +602,14 @@ fn update_pool_balances_fee_to_revenue_test() { let spot_position = SpotPosition::default(); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -593,7 +625,14 @@ fn update_pool_balances_fee_to_revenue_test() { (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 5000000); @@ -606,13 +645,27 @@ fn update_pool_balances_fee_to_revenue_test() { market.insurance_claim.quote_max_insurance = 1; // add min insurance let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 5000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 5000001000000000); market.insurance_claim.quote_max_insurance = 100000000; // add lots of insurance let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -692,7 +745,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { let spot_position = SpotPosition::default(); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000000000); // under FEE_POOL_TO_REVENUE_POOL_THRESHOLD assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); @@ -708,7 +768,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { (FEE_POOL_TO_REVENUE_POOL_THRESHOLD + 50 * QUOTE_PRECISION) * SPOT_BALANCE_PRECISION; market.amm.fee_pool.scaled_balance = prev_fee_pool_2; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.pnl_pool.scaled_balance, 50000000000000000); assert_eq!(market.amm.total_fee_withdrawn, 1000000); @@ -723,7 +790,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.amm.net_revenue_since_last_funding = 1; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 1000001); assert_eq!(spot_market.revenue_pool.scaled_balance, 1000001000000000); @@ -731,7 +805,14 @@ fn update_pool_balances_fee_to_revenue_low_amm_revenue_test() { market.amm.net_revenue_since_last_funding = 100000000; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.total_fee_withdrawn, 6000000); assert_eq!(spot_market.revenue_pool.scaled_balance, 6000000000000000); } @@ -828,7 +909,14 @@ fn update_pool_balances_revenue_to_fee_test() { ); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -860,7 +948,14 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(market.amm.total_fee_minus_distributions, -10000000000); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, @@ -886,7 +981,14 @@ fn update_pool_balances_revenue_to_fee_test() { // calling multiple times doesnt effect other than fee pool -> pnl pool let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -897,7 +999,14 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market.revenue_pool.scaled_balance, 0); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!( market.amm.fee_pool.scaled_balance, 5 * SPOT_BALANCE_PRECISION @@ -914,7 +1023,14 @@ fn update_pool_balances_revenue_to_fee_test() { let spot_market_backup = spot_market; let market_backup = market; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - assert!(update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).is_err()); // assert is_err if any way has revenue pool above deposit balances + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + ) + .is_err()); // assert is_err if any way has revenue pool above deposit balances spot_market = spot_market_backup; market = market_backup; spot_market.deposit_balance += 9900000001000; @@ -928,7 +1044,14 @@ fn update_pool_balances_revenue_to_fee_test() { assert_eq!(spot_market_vault_amount, 10100000001); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(spot_market.deposit_balance, 10100000001000); assert_eq!(spot_market.revenue_pool.scaled_balance, 9800000001000); assert_eq!(market.amm.fee_pool.scaled_balance, 105000000000); @@ -943,7 +1066,14 @@ fn update_pool_balances_revenue_to_fee_test() { // calling again only does fee -> pnl pool let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -957,7 +1087,14 @@ fn update_pool_balances_revenue_to_fee_test() { // calling again does nothing let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 5000000000); assert_eq!(market.pnl_pool.scaled_balance, 295000000000); assert_eq!(market.amm.total_fee_minus_distributions, -9800000000); @@ -1007,9 +1144,14 @@ fn update_pool_balances_revenue_to_fee_test() { let market_backup = market; let spot_market_backup = spot_market; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - assert!( - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now + 3600).is_err() - ); // assert is_err if any way has revenue pool above deposit balances + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + 3600 + ) + .is_err()); // assert is_err if any way has revenue pool above deposit balances market = market_backup; spot_market = spot_market_backup; spot_market.deposit_balance += 9800000000001; @@ -1026,8 +1168,22 @@ fn update_pool_balances_revenue_to_fee_test() { ); let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - assert!(update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).is_err()); // now timestamp passed is wrong - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now + 3600).unwrap(); + assert!(update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + ) + .is_err()); // now timestamp passed is wrong + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now + 3600, + ) + .unwrap(); assert_eq!(market.insurance_claim.last_revenue_withdraw_ts, 33931660); assert_eq!(spot_market.insurance_fund.last_revenue_settle_ts, 33931660); @@ -1106,7 +1262,14 @@ fn update_pool_balances_revenue_to_fee_devnet_state_test() { let prev_tfmd = market.amm.total_fee_minus_distributions; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 1821000000000); assert_eq!(market.pnl_pool.scaled_balance, 381047000000000); @@ -1198,7 +1361,14 @@ fn update_pool_balances_revenue_to_fee_new_market() { // let prev_tfmd = market.amm.total_fee_minus_distributions; let user_quote_token_amount = spot_position.get_signed_token_amount(&spot_market).unwrap(); - update_pool_balances(&mut market, &mut spot_market, user_quote_token_amount, 0, now).unwrap(); + update_pool_balances( + &mut market, + &mut spot_market, + user_quote_token_amount, + 0, + now, + ) + .unwrap(); assert_eq!(market.amm.fee_pool.scaled_balance, 50000000000); // $50 diff --git a/programs/drift/src/controller/isolated_position.rs b/programs/drift/src/controller/isolated_position.rs index 6545ac65ae..5bf940354b 100644 --- a/programs/drift/src/controller/isolated_position.rs +++ b/programs/drift/src/controller/isolated_position.rs @@ -1,27 +1,23 @@ use std::cell::RefMut; -use anchor_lang::prelude::*; +use crate::controller; use crate::controller::spot_balance::update_spot_balances; use crate::controller::spot_position::update_spot_balances_and_cumulative_deposits; use crate::error::{DriftResult, ErrorCode}; +use crate::get_then_update_id; use crate::math::casting::Cast; use crate::math::liquidation::is_isolated_margin_being_liquidated; use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType}; -use crate::state::events::{ - DepositDirection, DepositExplanation, DepositRecord, -}; +use crate::state::events::{DepositDirection, DepositExplanation, DepositRecord}; +use crate::state::oracle_map::OracleMap; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::PerpMarketMap; -use crate::state::oracle_map::OracleMap; -use crate::state::spot_market_map::SpotMarketMap; use crate::state::spot_market::SpotBalanceType; +use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; -use crate::state::user::{ - User,UserStats, -}; +use crate::state::user::{User, UserStats}; use crate::validate; -use crate::controller; -use crate::get_then_update_id; +use anchor_lang::prelude::*; #[cfg(test)] mod tests; @@ -417,4 +413,4 @@ pub fn withdraw_from_isolated_perp_position<'c: 'info, 'info>( emit!(deposit_record); Ok(()) -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/isolated_position/tests.rs b/programs/drift/src/controller/isolated_position/tests.rs index af29a122d9..2c2cac1660 100644 --- a/programs/drift/src/controller/isolated_position/tests.rs +++ b/programs/drift/src/controller/isolated_position/tests.rs @@ -8,11 +8,9 @@ pub mod deposit_into_isolated_perp_position { use solana_program::pubkey::Pubkey; use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, - LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, - QUOTE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, }; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::oracle_map::OracleMap; @@ -20,12 +18,10 @@ pub mod deposit_into_isolated_perp_position { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::SpotMarket; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{ - PerpPosition, PositionFlag, User - }; - use crate::{create_anchor_account_info, test_utils::*}; + use crate::state::user::{PerpPosition, PositionFlag, User}; use crate::test_utils::get_pyth_price; use crate::{create_account_info, PRICE_PRECISION_I64}; + use crate::{create_anchor_account_info, test_utils::*}; #[test] pub fn successful_deposit_into_isolated_perp_position() { @@ -111,8 +107,14 @@ pub mod deposit_into_isolated_perp_position { ) .unwrap(); - assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 1000000000); - assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 1000000000 + ); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); } #[test] @@ -205,7 +207,6 @@ pub mod deposit_into_isolated_perp_position { assert_eq!(result, Err(ErrorCode::InvalidPerpPosition)); } - } pub mod transfer_isolated_perp_position_deposit { @@ -217,11 +218,8 @@ pub mod transfer_isolated_perp_position_deposit { use solana_program::pubkey::Pubkey; use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, - LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, - QUOTE_PRECISION_I128, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::oracle_map::OracleMap; @@ -229,12 +227,13 @@ pub mod transfer_isolated_perp_position_deposit { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::SpotMarket; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{ - PerpPosition, PositionFlag, SpotPosition, User, UserStats - }; - use crate::{create_anchor_account_info, test_utils::*, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + use crate::state::user::{PerpPosition, PositionFlag, SpotPosition, User, UserStats}; use crate::test_utils::get_pyth_price; use crate::{create_account_info, PRICE_PRECISION_I64}; + use crate::{ + create_anchor_account_info, test_utils::*, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION, + SPOT_BALANCE_PRECISION_U64, + }; #[test] pub fn successful_transfer_to_isolated_perp_position() { @@ -324,8 +323,14 @@ pub mod transfer_isolated_perp_position_deposit { ) .unwrap(); - assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 1000000000); - assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 1000000000 + ); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); assert_eq!(user.spot_positions[0].scaled_balance, 0); } @@ -480,7 +485,7 @@ pub mod transfer_isolated_perp_position_deposit { cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, decimals: 6, initial_asset_weight: SPOT_WEIGHT_PRECISION, - deposit_balance: 2* SPOT_BALANCE_PRECISION, + deposit_balance: 2 * SPOT_BALANCE_PRECISION, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: PRICE_PRECISION_I64, last_oracle_price_twap_5min: PRICE_PRECISION_I64, @@ -510,7 +515,7 @@ pub mod transfer_isolated_perp_position_deposit { now, 0, 0, - 2* QUOTE_PRECISION_I64, + 2 * QUOTE_PRECISION_I64, ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -607,9 +612,15 @@ pub mod transfer_isolated_perp_position_deposit { .unwrap(); assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); - assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); - assert_eq!(user.spot_positions[0].scaled_balance, SPOT_BALANCE_PRECISION_U64); + assert_eq!( + user.spot_positions[0].scaled_balance, + SPOT_BALANCE_PRECISION_U64 + ); } #[test] @@ -806,11 +817,9 @@ pub mod withdraw_from_isolated_perp_position { use solana_program::pubkey::Pubkey; use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, - LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, - QUOTE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, PEG_PRECISION, + QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, }; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::oracle_map::OracleMap; @@ -818,12 +827,13 @@ pub mod withdraw_from_isolated_perp_position { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::SpotMarket; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{ - PerpPosition, PositionFlag, User, UserStats - }; - use crate::{create_anchor_account_info, test_utils::*, SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64}; + use crate::state::user::{PerpPosition, PositionFlag, User, UserStats}; use crate::test_utils::get_pyth_price; use crate::{create_account_info, PRICE_PRECISION_I64}; + use crate::{ + create_anchor_account_info, test_utils::*, SPOT_BALANCE_PRECISION, + SPOT_BALANCE_PRECISION_U64, + }; #[test] pub fn successful_withdraw_from_isolated_perp_position() { @@ -918,7 +928,10 @@ pub mod withdraw_from_isolated_perp_position { .unwrap(); assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); - assert_eq!(user.perp_positions[0].position_flag, PositionFlag::IsolatedPosition as u8); + assert_eq!( + user.perp_positions[0].position_flag, + PositionFlag::IsolatedPosition as u8 + ); } #[test] @@ -1108,5 +1121,4 @@ pub mod withdraw_from_isolated_perp_position { assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); } - -} \ No newline at end of file +} diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 9ffe22c0b2..aab1707765 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -31,7 +31,8 @@ pub mod liquidate_perp { 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 + 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}; @@ -2454,7 +2455,8 @@ pub mod liquidate_perp { let market_account_infos = vec![market_account_info, market2_account_info]; let market_set = BTreeSet::default(); - let perp_market_map = PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + let perp_market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); let mut spot_market = SpotMarket { market_index: 0, @@ -2565,7 +2567,6 @@ pub mod liquidate_perp { assert_eq!(isolated_position_before, isolated_position_after); } - } pub mod liquidate_perp_with_fill { @@ -10237,7 +10238,8 @@ pub mod liquidate_isolated_perp { slot, now, &state, - ).unwrap(); + ) + .unwrap(); let spot_position_one_after = user.spot_positions[0].clone(); let spot_position_two_after = user.spot_positions[1].clone(); @@ -10256,6 +10258,7 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { use anchor_lang::Owner; use solana_program::pubkey::Pubkey; + use crate::controller::liquidation::resolve_perp_bankruptcy; use crate::controller::liquidation::{liquidate_perp_pnl_for_deposit, liquidate_spot}; use crate::create_account_info; use crate::create_anchor_account_info; @@ -10276,12 +10279,11 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{AssetTier, SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{PositionFlag, UserStats}; use crate::state::user::{Order, PerpPosition, SpotPosition, User, UserStatus}; + use crate::state::user::{PositionFlag, UserStats}; use crate::test_utils::*; use crate::test_utils::{get_positions, get_pyth_price, get_spot_positions}; - use crate::controller::liquidation::resolve_perp_bankruptcy; - + #[test] pub fn successful_liquidation_liquidator_max_pnl_transfer() { let now = 0_i64; @@ -10422,7 +10424,10 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { ) .unwrap(); - assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 39494950000); + assert_eq!( + user.perp_positions[0].isolated_position_scaled_balance, + 39494950000 + ); assert_eq!(user.perp_positions[0].quote_asset_amount, -50000000); assert_eq!( @@ -10577,7 +10582,10 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); assert_eq!(user.perp_positions[0].quote_asset_amount, -1900000); - assert_eq!(user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, PositionFlag::Bankrupt as u8); + assert_eq!( + user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, + PositionFlag::Bankrupt as u8 + ); assert_eq!(liquidator.spot_positions[0].scaled_balance, 190000000000); assert_eq!(liquidator.perp_positions[0].quote_asset_amount, -89100000); @@ -10613,13 +10621,18 @@ pub mod liquidate_isolated_perp_pnl_for_deposit { assert_eq!(user.perp_positions[0].isolated_position_scaled_balance, 0); assert_eq!(user.perp_positions[0].quote_asset_amount, 0); - assert_eq!(user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, 0); + assert_eq!( + user.perp_positions[0].position_flag & PositionFlag::Bankrupt as u8, + 0 + ); assert_eq!(user.is_being_liquidated(), false); } } mod liquidation_mode { - use crate::state::liquidation_mode::{CrossMarginLiquidatePerpMode, IsolatedMarginLiquidatePerpMode, LiquidatePerpMode}; + use crate::state::liquidation_mode::{ + CrossMarginLiquidatePerpMode, IsolatedMarginLiquidatePerpMode, LiquidatePerpMode, + }; use std::collections::BTreeSet; use std::str::FromStr; @@ -10629,11 +10642,9 @@ mod liquidation_mode { use crate::create_account_info; use crate::create_anchor_account_info; use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, - MARGIN_PRECISION, PEG_PRECISION, - QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, - SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, - SPOT_WEIGHT_PRECISION, + AMM_RESERVE_PRECISION, BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION, MARGIN_PRECISION, + PEG_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, 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; use crate::state::margin_calculation::MarginContext; @@ -10646,9 +10657,9 @@ mod liquidation_mode { use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::PositionFlag; use crate::state::user::{Order, PerpPosition, SpotPosition, User}; - use crate::test_utils::*; use crate::test_utils::get_pyth_price; - + use crate::test_utils::*; + #[test] pub fn tests_meets_margin_requirements() { let slot = 0_u64; @@ -10702,7 +10713,8 @@ mod liquidation_mode { let market_account_infos = vec![market_account_info, market2_account_info]; let market_set = BTreeSet::default(); - let market_map = PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); + let market_map = + PerpMarketMap::load(&market_set, &mut market_account_infos.iter().peekable()).unwrap(); let mut usdc_market = SpotMarket { market_index: 0, @@ -10781,16 +10793,28 @@ mod liquidation_mode { let cross_liquidation_mode = CrossMarginLiquidatePerpMode::new(0); let liquidation_margin_buffer_ratio = MARGIN_PRECISION / 50; - let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user_isolated_position_being_liquidated, - &market_map, - &spot_market_map, - &mut oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), - ).unwrap(); + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user_isolated_position_being_liquidated, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + ) + .unwrap(); - assert_eq!(cross_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), true); - assert_eq!(isolated_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), false); + assert_eq!( + cross_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + true + ); + assert_eq!( + isolated_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + false + ); let mut spot_positions = [SpotPosition::default(); 8]; spot_positions[0] = SpotPosition { @@ -10819,17 +10843,27 @@ mod liquidation_mode { ..User::default() }; - let margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( - &user_cross_margin_being_liquidated, - &market_map, - &spot_market_map, - &mut oracle_map, - MarginContext::liquidation(liquidation_margin_buffer_ratio), - ).unwrap(); + let margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user_cross_margin_being_liquidated, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(liquidation_margin_buffer_ratio), + ) + .unwrap(); - assert_eq!(cross_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), false); - assert_eq!(isolated_liquidation_mode.meets_margin_requirements(&margin_calculation).unwrap(), true); + assert_eq!( + cross_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + false + ); + assert_eq!( + isolated_liquidation_mode + .meets_margin_requirements(&margin_calculation) + .unwrap(), + true + ); } - } - \ No newline at end of file diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index b3dd569c3d..8473ccc6cf 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -237,9 +237,12 @@ pub fn settle_pnl( let is_isolated_position = user.perp_positions[position_index].is_isolated(); let user_quote_token_amount = if is_isolated_position { - user.perp_positions[position_index].get_isolated_token_amount(spot_market)?.cast()? + user.perp_positions[position_index] + .get_isolated_token_amount(spot_market)? + .cast()? } else { - user.get_quote_spot_position().get_signed_token_amount(spot_market)? + user.get_quote_spot_position() + .get_signed_token_amount(spot_market)? }; let pnl_to_settle_with_user = update_pool_balances( diff --git a/programs/drift/src/controller/pnl/tests.rs b/programs/drift/src/controller/pnl/tests.rs index dcb3f02f3a..e2f10ac990 100644 --- a/programs/drift/src/controller/pnl/tests.rs +++ b/programs/drift/src/controller/pnl/tests.rs @@ -2221,7 +2221,8 @@ pub fn isolated_perp_position_negative_pnl() { expected_user.perp_positions[0].quote_asset_amount = 0; expected_user.settled_perp_pnl = -50 * QUOTE_PRECISION_I64; expected_user.perp_positions[0].settled_pnl = -50 * QUOTE_PRECISION_I64; - expected_user.perp_positions[0].isolated_position_scaled_balance = 50 * SPOT_BALANCE_PRECISION_U64; + expected_user.perp_positions[0].isolated_position_scaled_balance = + 50 * SPOT_BALANCE_PRECISION_U64; let mut expected_market = market; expected_market.pnl_pool.scaled_balance = 100 * SPOT_BALANCE_PRECISION; @@ -2354,7 +2355,8 @@ pub fn isolated_perp_position_user_unsettled_positive_pnl_less_than_pool() { expected_user.perp_positions[0].quote_asset_amount = 0; expected_user.settled_perp_pnl = 25 * QUOTE_PRECISION_I64; expected_user.perp_positions[0].settled_pnl = 25 * QUOTE_PRECISION_I64; - expected_user.perp_positions[0].isolated_position_scaled_balance = 125 * SPOT_BALANCE_PRECISION_U64; + expected_user.perp_positions[0].isolated_position_scaled_balance = + 125 * SPOT_BALANCE_PRECISION_U64; let mut expected_market = market; expected_market.pnl_pool.scaled_balance = 25 * SPOT_BALANCE_PRECISION; diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 71ab46c80c..a8158e5a73 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -1997,7 +1997,12 @@ fn isolated_perp_position() { .unwrap(); assert_eq!(perp_position.isolated_position_scaled_balance, 1000000000); - assert_eq!(perp_position.get_isolated_token_amount(&spot_market).unwrap(), amount); + assert_eq!( + perp_position + .get_isolated_token_amount(&spot_market) + .unwrap(), + amount + ); update_spot_balances( amount, @@ -2005,7 +2010,8 @@ fn isolated_perp_position() { &mut spot_market, &mut perp_position, false, - ).unwrap(); + ) + .unwrap(); assert_eq!(perp_position.isolated_position_scaled_balance, 0); @@ -2018,4 +2024,4 @@ fn isolated_perp_position() { ); assert_eq!(result, Err(ErrorCode::CantUpdateSpotBalanceType)); -} \ No newline at end of file +} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index aa4cc5d331..65863c4a8a 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -530,11 +530,12 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( 0, )?; - let perp_position_custom_margin_ratio = if context.margin_type == MarginRequirementType::Initial { - market_position.max_margin_ratio as u32 - } else { - 0_u32 - }; + let perp_position_custom_margin_ratio = + if context.margin_type == MarginRequirementType::Initial { + market_position.max_margin_ratio as u32 + } else { + 0_u32 + }; let (perp_margin_requirement, weighted_pnl, worst_case_liability_value, base_asset_value) = calculate_perp_position_value_and_pnl( diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index a288a57be1..9f997e11f3 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -4656,7 +4656,7 @@ mod get_margin_calculation_for_disable_high_leverage_mode { use crate::state::user::{Order, PerpPosition, SpotPosition, User}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; - use crate::{create_account_info, MARGIN_PRECISION, create_anchor_account_info}; + use crate::{create_account_info, create_anchor_account_info, MARGIN_PRECISION}; #[test] pub fn check_user_not_changed() { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 69c2773ffd..885f1001f3 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -1112,7 +1112,11 @@ impl PerpPosition { } pub fn is_available(&self) -> bool { - !self.is_open_position() && !self.has_open_order() && !self.has_unsettled_pnl() && self.isolated_position_scaled_balance == 0 && !self.is_being_liquidated() + !self.is_open_position() + && !self.has_open_order() + && !self.has_unsettled_pnl() + && self.isolated_position_scaled_balance == 0 + && !self.is_being_liquidated() } pub fn is_open_position(&self) -> bool { From 71fcdfa9dee7156b2105e7be3b9e3231362490be Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sat, 20 Sep 2025 12:29:45 -0400 Subject: [PATCH 70/91] first ts test --- sdk/src/driftClient.ts | 197 +++++++++- sdk/src/idl/drift.json | 217 ++++++++++- sdk/src/types.ts | 2 + sdk/src/user.ts | 19 +- test-scripts/run-anchor-tests.sh | 1 + tests/isolatedPositionDriftClient.ts | 547 +++++++++++++++++++++++++++ 6 files changed, 966 insertions(+), 17 deletions(-) create mode 100644 tests/isolatedPositionDriftClient.ts diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 01dfa60030..abff8433df 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -2204,6 +2204,15 @@ export class DriftClient { return this.getTokenAmount(QUOTE_SPOT_MARKET_INDEX); } + public getIsolatedPerpPositionTokenAmount( + perpMarketIndex: number, + subAccountId?: number + ): BN { + return this.getUser(subAccountId).getIsolatePerpPositionTokenAmount( + perpMarketIndex + ); + } + /** * Returns the token amount for a given market. The spot market precision is based on the token mint decimals. * Positive if it is a deposit, negative if it is a borrow. @@ -3772,6 +3781,191 @@ export class DriftClient { ); } + async depositIntoIsolatedPerpPosition( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getDepositIntoIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + async getDepositIntoIsolatedPerpPositionIx( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [spotMarketIndex], + readablePerpMarketIndex: [perpMarketIndex], + }); + + const tokenProgram = this.getTokenProgramForSpotMarket(spotMarketAccount); + return await this.program.instruction.depositIntoIsolatedPerpPosition( + spotMarketIndex, + perpMarketIndex, + amount, + { + accounts: { + state: await this.getStatePublicKey(), + spotMarketVault: spotMarketAccount.vault, + user: userAccountPublicKey, + userStats: this.getUserStatsAccountPublicKey(), + userTokenAccount: userTokenAccount, + authority: this.wallet.publicKey, + tokenProgram, + }, + remainingAccounts, + } + ); + } + + public async transferIsolatedPerpPositionDeposit( + amount: BN, + perpMarketIndex: number, + subAccountId?: number, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getTransferIsolatedPerpPositionDepositIx( + amount, + perpMarketIndex, + subAccountId + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getTransferIsolatedPerpPositionDepositIx( + amount: BN, + perpMarketIndex: number, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + const user = await this.getUserAccount(subAccountId); + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [user], + writableSpotMarketIndexes: [spotMarketIndex], + readablePerpMarketIndex: [perpMarketIndex], + }); + + return await this.program.instruction.transferIsolatedPerpPositionDeposit( + spotMarketIndex, + perpMarketIndex, + amount, + { + accounts: { + state: await this.getStatePublicKey(), + spotMarketVault: spotMarketAccount.vault, + user: userAccountPublicKey, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + }, + remainingAccounts, + } + ); + } + + public async withdrawFromIsolatedPerpPosition( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getWithdrawFromIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ), + txParams + ) + ); + return txSig; + } + + public async getWithdrawFromIsolatedPerpPositionIx( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + writableSpotMarketIndexes: [spotMarketIndex], + readablePerpMarketIndex: [perpMarketIndex], + }); + + return await this.program.instruction.withdrawFromIsolatedPerpPosition( + spotMarketIndex, + perpMarketIndex, + amount, + { + accounts: { + state: await this.getStatePublicKey(), + spotMarketVault: spotMarketAccount.vault, + user: userAccountPublicKey, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + userTokenAccount: userTokenAccount, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarketAccount), + driftSigner: this.getSignerPublicKey(), + }, + remainingAccounts, + } + ); + } + public async updateSpotMarketCumulativeInterest( marketIndex: number, txParams?: TxParams @@ -9240,8 +9434,7 @@ export class DriftClient { public async updateUserGovTokenInsuranceStake( authority: PublicKey, - txParams?: TxParams, - env: DriftEnv = 'mainnet-beta' + txParams?: TxParams ): Promise { const ix = await this.getUpdateUserGovTokenInsuranceStakeIx(authority); const tx = await this.buildTransaction(ix, txParams); diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 3fab52decf..e621bcb8df 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -652,6 +652,163 @@ } ] }, + { + "name": "depositIntoIsolatedPerpPosition", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "transferIsolatedPerpPositionDeposit", + "accounts": [ + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "i64" + } + ] + }, + { + "name": "withdrawFromIsolatedPerpPosition", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, { "name": "placePerpOrder", "accounts": [ @@ -11486,13 +11643,13 @@ "type": "u64" }, { - "name": "lastBaseAssetAmountPerLp", + "name": "isolatedPositionScaledBalance", "docs": [ "The last base asset amount per lp the amm had", "Used to settle the users lp position", - "precision: BASE_PRECISION" + "precision: SPOT_BALANCE_PRECISION" ], - "type": "i64" + "type": "u64" }, { "name": "lastQuoteAssetAmountPerLp", @@ -11531,8 +11688,8 @@ "type": "u8" }, { - "name": "perLpBase", - "type": "i8" + "name": "positionFlag", + "type": "u8" } ] } @@ -12170,6 +12327,17 @@ ] } }, + { + "name": "LiquidationBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + } + ] + } + }, { "name": "SettlePnlExplanation", "type": { @@ -12282,13 +12450,7 @@ "kind": "enum", "variants": [ { - "name": "Standard", - "fields": [ - { - "name": "trackOpenOrdersFraction", - "type": "bool" - } - ] + "name": "Standard" }, { "name": "Liquidation", @@ -12850,6 +13012,23 @@ ] } }, + { + "name": "PositionFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IsolatedPosition" + }, + { + "name": "BeingLiquidated" + }, + { + "name": "Bankrupt" + } + ] + } + }, { "name": "ReferrerStatus", "type": { @@ -13797,6 +13976,11 @@ "defined": "SpotBankruptcyRecord" }, "index": false + }, + { + "name": "bitFlags", + "type": "u8", + "index": false } ] }, @@ -15105,8 +15289,8 @@ }, { "code": 6094, - "name": "CantUpdatePoolBalanceType", - "msg": "CantUpdatePoolBalanceType" + "name": "CantUpdateSpotBalanceType", + "msg": "CantUpdateSpotBalanceType" }, { "code": 6095, @@ -16217,6 +16401,11 @@ "code": 6316, "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" + }, + { + "code": 6317, + "name": "InvalidIsolatedPerpMarket", + "msg": "Invalid Isolated Perp Market" } ], "metadata": { diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 2d49e61b58..f263e2b9cc 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1110,6 +1110,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; + isolatedPositionScaledBalance: BN; + positionFlag: number; }; export type UserStatsAccount = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 08494f71f8..7b78495e21 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -334,6 +334,22 @@ export class User { }; } + public getIsolatePerpPositionTokenAmount(perpMarketIndex: number): BN { + const perpPosition = this.getPerpPosition(perpMarketIndex); + const perpMarket = this.driftClient.getPerpMarketAccount(perpMarketIndex); + const spotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + if (perpPosition === undefined) { + return ZERO; + } + return getTokenAmount( + perpPosition.isolatedPositionScaledBalance, + spotMarket, + SpotBalanceType.DEPOSIT + ); + } + public getClonedPosition(position: PerpPosition): PerpPosition { const clonedPosition = Object.assign({}, position); return clonedPosition; @@ -570,7 +586,8 @@ export class User { (pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || - !(pos.openOrders == 0) + !(pos.openOrders == 0) || + pos.isolatedPositionScaledBalance.gt(ZERO) ); } diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index e8ef72b4ea..a352caf9ef 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -36,6 +36,7 @@ test_files=( highLeverageMode.ts ifRebalance.ts insuranceFundStake.ts + isolatedPositionDriftClient.ts liquidateBorrowForPerpPnl.ts liquidatePerp.ts liquidatePerpWithFill.ts diff --git a/tests/isolatedPositionDriftClient.ts b/tests/isolatedPositionDriftClient.ts new file mode 100644 index 0000000000..644ae91308 --- /dev/null +++ b/tests/isolatedPositionDriftClient.ts @@ -0,0 +1,547 @@ +import * as anchor from '@coral-xyz/anchor'; +import { assert } from 'chai'; +import { BN, OracleSource, ZERO } from '../sdk'; + +import { Program } from '@coral-xyz/anchor'; + +import { PublicKey } from '@solana/web3.js'; + +import { TestClient, PositionDirection, EventSubscriber } from '../sdk/src'; + +import { + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + initializeQuoteSpotMarket, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('drift client', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bankrunContextWrapper: BankrunContextWrapper; + + let bulkAccountLoader: TestBulkAccountLoader; + + let userAccountPublicKey: PublicKey; + + let usdcMint; + let userUSDCAccount; + + let solUsd; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(100000); + const ammInitialQuoteAssetAmount = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetAmount = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 1); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + userStats: true, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + + await driftClient.subscribe(); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + + const periodicity = new BN(60 * 60); // 1 HOUR + + await driftClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetAmount, + ammInitialQuoteAssetAmount, + periodicity + ); + + await driftClient.updatePerpMarketStepSizeAndTickSize( + 0, + new BN(1), + new BN(1) + ); + }); + + after(async () => { + await driftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('Initialize user account and deposit collateral', async () => { + await driftClient.initializeUserAccount(); + + userAccountPublicKey = await driftClient.getUserAccountPublicKey(); + + const txSig = await driftClient.depositIntoIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + const depositTokenAmount = + driftClient.getIsolatedPerpPositionTokenAmount(0); + console.log('depositTokenAmount', depositTokenAmount.toString()); + assert(depositTokenAmount.eq(usdcAmount)); + + // Check that drift collateral account has proper collateral + const quoteSpotVault = + await bankrunContextWrapper.connection.getTokenAccount( + driftClient.getQuoteSpotMarketAccount().vault + ); + + assert.ok(new BN(Number(quoteSpotVault.amount)).eq(usdcAmount)); + + await eventSubscriber.awaitTx(txSig); + const depositRecord = eventSubscriber.getEventsArray('DepositRecord')[0]; + + assert.ok( + depositRecord.userAuthority.equals( + bankrunContextWrapper.provider.wallet.publicKey + ) + ); + assert.ok(depositRecord.user.equals(userAccountPublicKey)); + + assert.ok( + JSON.stringify(depositRecord.direction) === + JSON.stringify({ deposit: {} }) + ); + assert.ok(depositRecord.amount.eq(new BN(10000000))); + }); + + it('Transfer isolated perp position deposit', async () => { + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount.neg(), 0); + + const quoteAssetTokenAmount = + driftClient.getIsolatedPerpPositionTokenAmount(0); + assert(quoteAssetTokenAmount.eq(ZERO)); + + const quoteTokenAmount = driftClient.getQuoteAssetTokenAmount(); + assert(quoteTokenAmount.eq(usdcAmount)); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + const quoteAssetTokenAmount2 = + driftClient.getIsolatedPerpPositionTokenAmount(0); + assert(quoteAssetTokenAmount2.eq(usdcAmount)); + + const quoteTokenAmoun2 = driftClient.getQuoteAssetTokenAmount(); + assert(quoteTokenAmoun2.eq(ZERO)); + }); + + it('Withdraw Collateral', async () => { + await driftClient.withdrawFromIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + await driftClient.fetchAccounts(); + assert(driftClient.getIsolatedPerpPositionTokenAmount(0).eq(ZERO)); + + // Check that drift collateral account has proper collateral] + const quoteSpotVault = + await bankrunContextWrapper.connection.getTokenAccount( + driftClient.getQuoteSpotMarketAccount().vault + ); + + assert.ok(new BN(Number(quoteSpotVault.amount)).eq(ZERO)); + + const userUSDCtoken = + await bankrunContextWrapper.connection.getTokenAccount( + userUSDCAccount.publicKey + ); + assert.ok(new BN(Number(userUSDCtoken.amount)).eq(usdcAmount)); + + const depositRecord = eventSubscriber.getEventsArray('DepositRecord')[0]; + + assert.ok( + depositRecord.userAuthority.equals( + bankrunContextWrapper.provider.wallet.publicKey + ) + ); + assert.ok(depositRecord.user.equals(userAccountPublicKey)); + + assert.ok( + JSON.stringify(depositRecord.direction) === + JSON.stringify({ withdraw: {} }) + ); + assert.ok(depositRecord.amount.eq(new BN(10000000))); + }); + + it('Long from 0 position', async () => { + // Re-Deposit USDC, assuming we have 0 balance here + await driftClient.depositIntoIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + + const marketIndex = 0; + const baseAssetAmount = new BN(48000000000); + const txSig = await driftClient.openPosition( + PositionDirection.LONG, + baseAssetAmount, + marketIndex + ); + bankrunContextWrapper.connection.printTxLogs(txSig); + + const marketData = driftClient.getPerpMarketAccount(0); + await setFeedPriceNoProgram( + bankrunContextWrapper, + 1.01, + marketData.amm.oracle + ); + + const orderR = eventSubscriber.getEventsArray('OrderActionRecord')[0]; + console.log(orderR.takerFee.toString()); + console.log(orderR.baseAssetAmountFilled.toString()); + + const user: any = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + + console.log( + 'getQuoteAssetTokenAmount:', + driftClient.getIsolatedPerpPositionTokenAmount(0).toString() + ); + assert( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(10000000)) + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(48001)) + ); + + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(-48000001))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(-48048002))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(48000000000))); + assert.ok(user.perpPositions[0].positionFlag === 1); + + const market = driftClient.getPerpMarketAccount(0); + console.log(market.amm.baseAssetAmountWithAmm.toNumber()); + console.log(market); + + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(48000000000))); + console.log(market.amm.totalFee.toString()); + assert.ok(market.amm.totalFee.eq(new BN(48001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(48001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(1))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(48000001))); + assert.ok(orderActionRecord.marketIndex === marketIndex); + + assert.ok(orderActionRecord.takerExistingQuoteEntryAmount === null); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + + assert(driftClient.getPerpMarketAccount(0).nextFillRecordId.eq(new BN(2))); + }); + + it('Withdraw fails due to insufficient collateral', async () => { + // lil hack to stop printing errors + const oldConsoleLog = console.log; + const oldConsoleError = console.error; + console.log = function () { + const _noop = ''; + }; + console.error = function () { + const _noop = ''; + }; + try { + await driftClient.withdrawFromIsolatedPerpPosition( + usdcAmount, + 0, + userUSDCAccount.publicKey + ); + assert(false, 'Withdrawal succeeded'); + } catch (e) { + assert(true); + } finally { + console.log = oldConsoleLog; + console.error = oldConsoleError; + } + }); + + it('Reduce long position', async () => { + const marketIndex = 0; + const baseAssetAmount = new BN(24000000000); + await driftClient.openPosition( + PositionDirection.SHORT, + baseAssetAmount, + marketIndex + ); + + await driftClient.fetchAccounts(); + + await driftClient.fetchAccounts(); + const user = driftClient.getUserAccount(); + console.log( + 'quoteAssetAmount:', + user.perpPositions[0].quoteAssetAmount.toNumber() + ); + console.log( + 'quoteBreakEvenAmount:', + user.perpPositions[0].quoteBreakEvenAmount.toNumber() + ); + + assert.ok(user.perpPositions[0].quoteAssetAmount.eq(new BN(-24072002))); + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(-24000001))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(-24048001))); + + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(24000000000))); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(10000000)) + ); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(72001)) + ); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(24000000000))); + assert.ok(market.amm.totalFee.eq(new BN(72001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(72001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(2))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(24000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(24000000))); + assert.ok(orderActionRecord.marketIndex === 0); + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000000)) + ); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + }); + + it('Reverse long position', async () => { + const marketData = driftClient.getPerpMarketAccount(0); + await setFeedPriceNoProgram( + bankrunContextWrapper, + 1.0, + marketData.amm.oracle + ); + + const baseAssetAmount = new BN(48000000000); + await driftClient.openPosition(PositionDirection.SHORT, baseAssetAmount, 0); + + await driftClient.fetchAccounts(); + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + await driftClient.fetchAccounts(); + const user = driftClient.getUserAccount(); + console.log( + 'quoteAssetAmount:', + user.perpPositions[0].quoteAssetAmount.toNumber() + ); + console.log( + 'quoteBreakEvenAmount:', + user.perpPositions[0].quoteBreakEvenAmount.toNumber() + ); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9879998)) + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(120001)) + ); + console.log(user.perpPositions[0].quoteBreakEvenAmount.toString()); + console.log(user.perpPositions[0].quoteAssetAmount.toString()); + + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(24000000))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(23952000))); + assert.ok(user.perpPositions[0].quoteAssetAmount.eq(new BN(24000000))); + console.log(user.perpPositions[0].baseAssetAmount.toString()); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(-24000000000))); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(-24000000000))); + assert.ok(market.amm.totalFee.eq(new BN(120001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(120001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(3))); + console.log(orderActionRecord.baseAssetAmountFilled.toNumber()); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(48000000))); + + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000001)) + ); + assert.ok( + orderActionRecord.takerExistingBaseAssetAmount.eq(new BN(24000000000)) + ); + + assert.ok(orderActionRecord.marketIndex === 0); + }); + + it('Close position', async () => { + const marketIndex = 0; + await driftClient.closePosition(marketIndex); + + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + marketIndex + ); + + const user: any = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(0))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(0))); + console.log(driftClient.getIsolatedPerpPositionTokenAmount(0).toString()); + assert.ok( + driftClient.getIsolatedPerpPositionTokenAmount(0).eq(new BN(9855998)) + ); + console.log( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.toString() + ); + assert( + driftClient + .getUserStats() + .getAccountAndSlot() + .data.fees.totalFeePaid.eq(new BN(144001)) + ); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(0))); + assert.ok(market.amm.totalFee.eq(new BN(144001))); + assert.ok(market.amm.totalFeeMinusDistributions.eq(new BN(144001))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(4))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(24000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(24000000))); + assert.ok(orderActionRecord.marketIndex === 0); + + assert.ok( + orderActionRecord.takerExistingQuoteEntryAmount.eq(new BN(24000000)) + ); + assert.ok(orderActionRecord.takerExistingBaseAssetAmount === null); + }); + + it('Open short position', async () => { + const baseAssetAmount = new BN(48000000000); + await driftClient.openPosition(PositionDirection.SHORT, baseAssetAmount, 0); + + await driftClient.settlePNL( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + const user = await driftClient.program.account.user.fetch( + userAccountPublicKey + ); + assert.ok(user.perpPositions[0].positionFlag === 1); + console.log(user.perpPositions[0].quoteBreakEvenAmount.toString()); + assert.ok(user.perpPositions[0].quoteEntryAmount.eq(new BN(47999999))); + assert.ok(user.perpPositions[0].quoteBreakEvenAmount.eq(new BN(47951999))); + assert.ok(user.perpPositions[0].baseAssetAmount.eq(new BN(-48000000000))); + + const market = driftClient.getPerpMarketAccount(0); + assert.ok(market.amm.baseAssetAmountWithAmm.eq(new BN(-48000000000))); + + const orderActionRecord = + eventSubscriber.getEventsArray('OrderActionRecord')[0]; + + assert.ok(orderActionRecord.taker.equals(userAccountPublicKey)); + assert.ok(orderActionRecord.fillRecordId.eq(new BN(5))); + assert.ok(orderActionRecord.baseAssetAmountFilled.eq(new BN(48000000000))); + assert.ok(orderActionRecord.quoteAssetAmountFilled.eq(new BN(47999999))); + assert.ok(orderActionRecord.marketIndex === 0); + }); +}); From 7af9f657af6c7423ed392476b1cba4e198f02d34 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 21 Sep 2025 18:48:13 -0400 Subject: [PATCH 71/91] isolatedPositionLiquidatePerp test --- tests/isolatedPositionLiquidatePerp.ts | 425 +++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 tests/isolatedPositionLiquidatePerp.ts diff --git a/tests/isolatedPositionLiquidatePerp.ts b/tests/isolatedPositionLiquidatePerp.ts new file mode 100644 index 0000000000..c97feadfe0 --- /dev/null +++ b/tests/isolatedPositionLiquidatePerp.ts @@ -0,0 +1,425 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Program } from '@coral-xyz/anchor'; +import { + BASE_PRECISION, + BN, + ContractTier, + EventSubscriber, + isVariant, + LIQUIDATION_PCT_PRECISION, + OracleGuardRails, + OracleSource, + PositionDirection, + PRICE_PRECISION, + QUOTE_PRECISION, + TestClient, + User, + Wallet, + ZERO, +} from '../sdk/src'; +import { assert } from 'chai'; + +import { Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js'; + +import { + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPriceNoProgram, +} from './testHelpers'; +import { PERCENTAGE_PRECISION } from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('liquidate perp (no open orders)', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let usdcMint; + let userUSDCAccount; + + const liquidatorKeyPair = new Keypair(); + let liquidatorUSDCAccount: Keypair; + let liquidatorDriftClient: TestClient; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + const oracle = await mockOracleNoProgram(bankrunContextWrapper, 1); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + await driftClient.subscribe(); + + await driftClient.updateInitialPctToLiquidate( + LIQUIDATION_PCT_PRECISION.toNumber() + ); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + const oracleGuardRails: OracleGuardRails = { + priceDivergence: { + markOraclePercentDivergence: PERCENTAGE_PRECISION, + oracleTwap5MinPercentDivergence: PERCENTAGE_PRECISION.muln(100), + }, + validity: { + slotsBeforeStaleForAmm: new BN(100), + slotsBeforeStaleForMargin: new BN(100), + confidenceIntervalMaxSize: new BN(100000), + tooVolatileRatio: new BN(11), // allow 11x change + }, + }; + + await driftClient.updateOracleGuardRails(oracleGuardRails); + + const periodicity = new BN(0); + + await driftClient.initializePerpMarket( + 0, + + oracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity + ); + + await driftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + await driftClient.openPosition( + PositionDirection.LONG, + new BN(175).mul(BASE_PRECISION).div(new BN(10)), // 17.5 SOL + 0, + new BN(0) + ); + + bankrunContextWrapper.fundKeypair(liquidatorKeyPair, LAMPORTS_PER_SOL); + liquidatorUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper, + liquidatorKeyPair.publicKey + ); + liquidatorDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new Wallet(liquidatorKeyPair), + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await liquidatorDriftClient.subscribe(); + + await liquidatorDriftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + liquidatorUSDCAccount.publicKey + ); + }); + + after(async () => { + await driftClient.unsubscribe(); + await liquidatorDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('liquidate', async () => { + const marketIndex = 0; + + const driftClientUser = new User({ + driftClient: driftClient, + userAccountPublicKey: await driftClient.getUserAccountPublicKey(), + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await driftClientUser.subscribe(); + + const oracle = driftClient.getPerpMarketAccount(0).amm.oracle; + await setFeedPriceNoProgram(bankrunContextWrapper, 0.9, oracle); + + await driftClient.settlePNL( + driftClientUser.userAccountPublicKey, + driftClientUser.getUserAccount(), + 0 + ); + + await setFeedPriceNoProgram(bankrunContextWrapper, 1.1, oracle); + + await driftClient.settlePNL( + driftClientUser.userAccountPublicKey, + driftClientUser.getUserAccount(), + 0 + ); + + await driftClientUser.unsubscribe(); + + await setFeedPriceNoProgram(bankrunContextWrapper, 0.1, oracle); + + const txSig1 = await liquidatorDriftClient.setUserStatusToBeingLiquidated( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount() + ); + console.log('setUserStatusToBeingLiquidated txSig:', txSig1); + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 3); + + const txSig = await liquidatorDriftClient.liquidatePerp( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + new BN(175).mul(BASE_PRECISION).div(new BN(10)) + ); + + bankrunContextWrapper.connection.printTxLogs(txSig); + + for (let i = 0; i < 32; i++) { + assert(!isVariant(driftClient.getUserAccount().orders[i].status, 'open')); + } + + assert( + liquidatorDriftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(17500000000)) + ); + + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 3); + + const liquidationRecord = + eventSubscriber.getEventsArray('LiquidationRecord')[0]; + assert(liquidationRecord.liquidationId === 1); + assert(isVariant(liquidationRecord.liquidationType, 'liquidatePerp')); + assert(liquidationRecord.liquidatePerp.marketIndex === 0); + assert(liquidationRecord.canceledOrderIds.length === 0); + assert( + liquidationRecord.liquidatePerp.oraclePrice.eq( + PRICE_PRECISION.div(new BN(10)) + ) + ); + assert( + liquidationRecord.liquidatePerp.baseAssetAmount.eq(new BN(-17500000000)) + ); + + assert( + liquidationRecord.liquidatePerp.quoteAssetAmount.eq(new BN(1750000)) + ); + assert(liquidationRecord.liquidatePerp.ifFee.eq(new BN(0))); + assert(liquidationRecord.liquidatePerp.liquidatorFee.eq(new BN(0))); + + const fillRecord = eventSubscriber.getEventsArray('OrderActionRecord')[0]; + assert(isVariant(fillRecord.action, 'fill')); + assert(fillRecord.marketIndex === 0); + assert(isVariant(fillRecord.marketType, 'perp')); + assert(fillRecord.baseAssetAmountFilled.eq(new BN(17500000000))); + assert(fillRecord.quoteAssetAmountFilled.eq(new BN(1750000))); + assert(fillRecord.takerOrderBaseAssetAmount.eq(new BN(17500000000))); + assert( + fillRecord.takerOrderCumulativeBaseAssetAmountFilled.eq( + new BN(17500000000) + ) + ); + assert(fillRecord.takerFee.eq(new BN(0))); + assert(isVariant(fillRecord.takerOrderDirection, 'short')); + assert(fillRecord.makerOrderBaseAssetAmount.eq(new BN(17500000000))); + assert( + fillRecord.makerOrderCumulativeBaseAssetAmountFilled.eq( + new BN(17500000000) + ) + ); + console.log(fillRecord.makerFee.toString()); + assert(fillRecord.makerFee.eq(new BN(ZERO))); + assert(isVariant(fillRecord.makerOrderDirection, 'long')); + + assert(fillRecord.takerExistingQuoteEntryAmount.eq(new BN(17500007))); + assert(fillRecord.takerExistingBaseAssetAmount === null); + assert(fillRecord.makerExistingQuoteEntryAmount === null); + assert(fillRecord.makerExistingBaseAssetAmount === null); + + const _sig2 = await liquidatorDriftClient.liquidatePerpPnlForDeposit( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + 0, + driftClient.getUserAccount().perpPositions[0].quoteAssetAmount + ); + + await driftClient.fetchAccounts(); + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 5); + console.log( + driftClient.getUserAccount().perpPositions[0].quoteAssetAmount.toString() + ); + assert( + driftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-5767653)) + ); + + await driftClient.updatePerpMarketContractTier(0, ContractTier.A); + const tx1 = await driftClient.updatePerpMarketMaxImbalances( + marketIndex, + new BN(40000).mul(QUOTE_PRECISION), + QUOTE_PRECISION, + QUOTE_PRECISION + ); + bankrunContextWrapper.connection.printTxLogs(tx1); + + await driftClient.fetchAccounts(); + const marketBeforeBankruptcy = + driftClient.getPerpMarketAccount(marketIndex); + assert( + marketBeforeBankruptcy.insuranceClaim.revenueWithdrawSinceLastSettle.eq( + ZERO + ) + ); + assert( + marketBeforeBankruptcy.insuranceClaim.quoteSettledInsurance.eq(ZERO) + ); + assert( + marketBeforeBankruptcy.insuranceClaim.quoteMaxInsurance.eq( + QUOTE_PRECISION + ) + ); + assert(marketBeforeBankruptcy.amm.totalSocialLoss.eq(ZERO)); + const _sig = await liquidatorDriftClient.resolvePerpBankruptcy( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + + await driftClient.fetchAccounts(); + // all social loss + const marketAfterBankruptcy = driftClient.getPerpMarketAccount(marketIndex); + assert( + marketAfterBankruptcy.insuranceClaim.revenueWithdrawSinceLastSettle.eq( + ZERO + ) + ); + assert(marketAfterBankruptcy.insuranceClaim.quoteSettledInsurance.eq(ZERO)); + assert( + marketAfterBankruptcy.insuranceClaim.quoteMaxInsurance.eq(QUOTE_PRECISION) + ); + assert(marketAfterBankruptcy.amm.feePool.scaledBalance.eq(ZERO)); + console.log( + 'marketAfterBankruptcy.amm.totalSocialLoss:', + marketAfterBankruptcy.amm.totalSocialLoss.toString() + ); + assert(marketAfterBankruptcy.amm.totalSocialLoss.eq(new BN(5750007))); + + // assert(!driftClient.getUserAccount().isBankrupt); + // assert(!driftClient.getUserAccount().isBeingLiquidated); + assert(driftClient.getUserAccount().perpPositions[0].positionFlag === 1); + + console.log(driftClient.getUserAccount()); + // assert( + // driftClient.getUserAccount().perpPositions[0].quoteAssetAmount.eq(ZERO) + // ); + // assert(driftClient.getUserAccount().perpPositions[0].lpShares.eq(ZERO)); + + const perpBankruptcyRecord = + eventSubscriber.getEventsArray('LiquidationRecord')[0]; + + assert(isVariant(perpBankruptcyRecord.liquidationType, 'perpBankruptcy')); + assert(perpBankruptcyRecord.perpBankruptcy.marketIndex === 0); + console.log(perpBankruptcyRecord.perpBankruptcy.pnl.toString()); + console.log( + perpBankruptcyRecord.perpBankruptcy.cumulativeFundingRateDelta.toString() + ); + assert(perpBankruptcyRecord.perpBankruptcy.pnl.eq(new BN(-5767653))); + console.log( + perpBankruptcyRecord.perpBankruptcy.cumulativeFundingRateDelta.toString() + ); + assert( + perpBankruptcyRecord.perpBankruptcy.cumulativeFundingRateDelta.eq( + new BN(328572000) + ) + ); + + const market = driftClient.getPerpMarketAccount(0); + console.log( + market.amm.cumulativeFundingRateLong.toString(), + market.amm.cumulativeFundingRateShort.toString() + ); + assert(market.amm.cumulativeFundingRateLong.eq(new BN(328580333))); + assert(market.amm.cumulativeFundingRateShort.eq(new BN(-328563667))); + }); +}); From e40563e3baabd19f0e339756480ec8eea5b33f1a Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Sun, 21 Sep 2025 18:54:26 -0400 Subject: [PATCH 72/91] isolatedPositionLiquidatePerpwithFill test --- .../isolatedPositionLiquidatePerpwithFill.ts | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 tests/isolatedPositionLiquidatePerpwithFill.ts diff --git a/tests/isolatedPositionLiquidatePerpwithFill.ts b/tests/isolatedPositionLiquidatePerpwithFill.ts new file mode 100644 index 0000000000..787b5d42f5 --- /dev/null +++ b/tests/isolatedPositionLiquidatePerpwithFill.ts @@ -0,0 +1,338 @@ +import * as anchor from '@coral-xyz/anchor'; +import { Program } from '@coral-xyz/anchor'; +import { + BASE_PRECISION, + BN, + EventSubscriber, + isVariant, + LIQUIDATION_PCT_PRECISION, + OracleGuardRails, + OracleSource, + PositionDirection, + PRICE_PRECISION, + QUOTE_PRECISION, + TestClient, + Wallet, +} from '../sdk/src'; +import { assert } from 'chai'; + +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + setFeedPriceNoProgram, +} from './testHelpers'; +import { OrderType, PERCENTAGE_PRECISION, PerpOperation } from '../sdk'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; + +describe('liquidate perp (no open orders)', () => { + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + let eventSubscriber: EventSubscriber; + + let bulkAccountLoader: TestBulkAccountLoader; + + let bankrunContextWrapper: BankrunContextWrapper; + + let usdcMint; + let userUSDCAccount; + + const liquidatorKeyPair = new Keypair(); + let liquidatorUSDCAccount: Keypair; + let liquidatorDriftClient: TestClient; + + let makerDriftClient: TestClient; + let makerUSDCAccount: PublicKey; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + const makerUsdcAmount = new BN(1000 * 10 ** 6); + + let oracle: PublicKey; + + before(async () => { + const context = await startAnchor('', [], []); + + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + eventSubscriber = new EventSubscriber( + bankrunContextWrapper.connection.toConnection(), + //@ts-ignore + chProgram + ); + + await eventSubscriber.subscribe(); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper + ); + + oracle = await mockOracleNoProgram(bankrunContextWrapper, 1); + + driftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + await driftClient.subscribe(); + + await driftClient.updateInitialPctToLiquidate( + LIQUIDATION_PCT_PRECISION.toNumber() + ); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + const oracleGuardRails: OracleGuardRails = { + priceDivergence: { + markOraclePercentDivergence: PERCENTAGE_PRECISION.muln(100), + oracleTwap5MinPercentDivergence: PERCENTAGE_PRECISION.muln(100), + }, + validity: { + slotsBeforeStaleForAmm: new BN(100), + slotsBeforeStaleForMargin: new BN(100), + confidenceIntervalMaxSize: new BN(100000), + tooVolatileRatio: new BN(11), // allow 11x change + }, + }; + + await driftClient.updateOracleGuardRails(oracleGuardRails); + + const periodicity = new BN(0); + + await driftClient.initializePerpMarket( + 0, + + oracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity + ); + + await driftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + + await driftClient.transferIsolatedPerpPositionDeposit(usdcAmount, 0); + + await driftClient.openPosition( + PositionDirection.LONG, + new BN(175).mul(BASE_PRECISION).div(new BN(10)), // 17.5 SOL + 0, + new BN(0) + ); + + bankrunContextWrapper.fundKeypair(liquidatorKeyPair, LAMPORTS_PER_SOL); + liquidatorUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount, + bankrunContextWrapper, + liquidatorKeyPair.publicKey + ); + liquidatorDriftClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new Wallet(liquidatorKeyPair), + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0], + subAccountIds: [], + oracleInfos: [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await liquidatorDriftClient.subscribe(); + + await liquidatorDriftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + liquidatorUSDCAccount.publicKey + ); + + [makerDriftClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + makerUsdcAmount, + [0], + [0], + [ + { + publicKey: oracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await makerDriftClient.deposit(makerUsdcAmount, 0, makerUSDCAccount); + }); + + after(async () => { + await driftClient.unsubscribe(); + await liquidatorDriftClient.unsubscribe(); + await makerDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('liquidate', async () => { + await setFeedPriceNoProgram(bankrunContextWrapper, 0.1, oracle); + await driftClient.updatePerpMarketPausedOperations( + 0, + PerpOperation.AMM_FILL + ); + + try { + const failToPlaceTxSig = await driftClient.placePerpOrder({ + direction: PositionDirection.SHORT, + baseAssetAmount: BASE_PRECISION, + price: PRICE_PRECISION.divn(10), + orderType: OrderType.LIMIT, + reduceOnly: true, + marketIndex: 0, + }); + bankrunContextWrapper.connection.printTxLogs(failToPlaceTxSig); + throw new Error('Expected placePerpOrder to throw an error'); + } catch (error) { + if ( + error.message !== + 'Error processing Instruction 1: custom program error: 0x1773' + ) { + throw new Error(`Unexpected error message: ${error.message}`); + } + } + + await makerDriftClient.placePerpOrder({ + direction: PositionDirection.LONG, + baseAssetAmount: new BN(175).mul(BASE_PRECISION), + price: PRICE_PRECISION.divn(10), + orderType: OrderType.LIMIT, + marketIndex: 0, + }); + + const makerInfos = [ + { + maker: await makerDriftClient.getUserAccountPublicKey(), + makerStats: makerDriftClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerDriftClient.getUserAccount(), + }, + ]; + + const txSig = await liquidatorDriftClient.liquidatePerpWithFill( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + makerInfos + ); + + bankrunContextWrapper.connection.printTxLogs(txSig); + + for (let i = 0; i < 32; i++) { + assert(!isVariant(driftClient.getUserAccount().orders[i].status, 'open')); + } + + assert( + liquidatorDriftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(175)) + ); + + assert( + driftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(0)) + ); + + assert( + driftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-15769403)) + ); + + assert( + liquidatorDriftClient.getPerpMarketAccount(0).ifLiquidationFee === 10000 + ); + + assert( + makerDriftClient + .getUserAccount() + .perpPositions[0].baseAssetAmount.eq(new BN(17500000000)) + ); + + assert( + makerDriftClient + .getUserAccount() + .perpPositions[0].quoteAssetAmount.eq(new BN(-1749650)) + ); + + assert( + liquidatorDriftClient.getPerpMarketAccount(0).ifLiquidationFee === 10000 + ); + + await makerDriftClient.liquidatePerpPnlForDeposit( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0, + 0, + QUOTE_PRECISION.muln(20) + ); + + await makerDriftClient.resolvePerpBankruptcy( + await driftClient.getUserAccountPublicKey(), + driftClient.getUserAccount(), + 0 + ); + }); +}); From c643a50213f1ee64decd8462a92f860e365a9b1b Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 23 Sep 2025 09:37:38 -0400 Subject: [PATCH 73/91] fix expired position --- programs/drift/src/controller/amm.rs | 40 +++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index 93a190a4c7..ef920e7804 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -771,7 +771,7 @@ pub fn update_pool_balances( pub fn update_pnl_pool_and_user_balance( market: &mut PerpMarket, - bank: &mut SpotMarket, + quote_spot_market: &mut SpotMarket, user: &mut User, unrealized_pnl_with_fee: i128, ) -> DriftResult { @@ -779,7 +779,7 @@ pub fn update_pnl_pool_and_user_balance( unrealized_pnl_with_fee.min( get_token_amount( market.pnl_pool.scaled_balance, - bank, + quote_spot_market, market.pnl_pool.balance_type(), )? .cast()?, @@ -810,14 +810,36 @@ pub fn update_pnl_pool_and_user_balance( return Ok(0); } - let user_spot_position = user.get_quote_spot_position_mut(); + let is_isolated_position = user.get_perp_position(market.market_index)?.is_isolated(); + if is_isolated_position { + let perp_position = user.force_get_isolated_perp_position_mut(market.market_index)?; + let perp_position_token_amount = perp_position.get_isolated_token_amount(quote_spot_market)?; + + if pnl_to_settle_with_user < 0 { + validate!( + perp_position_token_amount >= pnl_to_settle_with_user.unsigned_abs(), + ErrorCode::InsufficientCollateral, + "user has insufficient deposit for market {}", + market.market_index + )?; + } - transfer_spot_balances( - pnl_to_settle_with_user, - bank, - &mut market.pnl_pool, - user_spot_position, - )?; + transfer_spot_balances( + pnl_to_settle_with_user, + quote_spot_market, + &mut market.pnl_pool, + perp_position, + )?; + } else { + let user_spot_position = user.get_quote_spot_position_mut(); + + transfer_spot_balances( + pnl_to_settle_with_user, + quote_spot_market, + &mut market.pnl_pool, + user_spot_position, + )?; + } Ok(pnl_to_settle_with_user) } From 16bea307238c3df88df9af00b00fbe9475b4eeee Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Tue, 23 Sep 2025 09:42:01 -0400 Subject: [PATCH 74/91] cargo fmt -- --- programs/drift/src/controller/amm.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index ef920e7804..f08b3b546d 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -813,7 +813,8 @@ pub fn update_pnl_pool_and_user_balance( let is_isolated_position = user.get_perp_position(market.market_index)?.is_isolated(); if is_isolated_position { let perp_position = user.force_get_isolated_perp_position_mut(market.market_index)?; - let perp_position_token_amount = perp_position.get_isolated_token_amount(quote_spot_market)?; + let perp_position_token_amount = + perp_position.get_isolated_token_amount(quote_spot_market)?; if pnl_to_settle_with_user < 0 { validate!( From e1bde42ac567abe7bece46526e3ffc3245741369 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 25 Aug 2025 11:11:09 -0600 Subject: [PATCH 75/91] feat: initial SDK Changes for iso pos --- sdk/src/margin/README.md | 143 ++++++++++ sdk/src/user.ts | 602 +++++++++++++++++++++++++++++++++++---- 2 files changed, 685 insertions(+), 60 deletions(-) create mode 100644 sdk/src/margin/README.md diff --git a/sdk/src/margin/README.md b/sdk/src/margin/README.md new file mode 100644 index 0000000000..b074e96ff2 --- /dev/null +++ b/sdk/src/margin/README.md @@ -0,0 +1,143 @@ +## Margin Calculation Snapshot (SDK) + +This document describes the single-source-of-truth margin engine in the SDK that mirrors the on-chain `MarginCalculation` and related semantics. The goal is to compute an immutable snapshot in one pass and have existing `User` getters delegate to it, eliminating duplicative work across getters and UI hooks while maintaining parity with the program. + +### Alignment with on-chain + +- The SDK snapshot shape mirrors `programs/drift/src/state/margin_calculation.rs` field-for-field. +- The inputs and ordering mirror `calculate_margin_requirement_and_total_collateral_and_liability_info` in `programs/drift/src/math/margin.rs`. +- Isolated positions are represented as `isolated_margin_calculations` keyed by perp `market_index`, matching program logic. + +### Core SDK types (shape parity) + +```ts +// Types reflect on-chain names and numeric signs +export type MarginRequirementType = 'Initial' | 'Fill' | 'Maintenance'; +export type MarketType = 'Spot' | 'Perp'; + +export type MarketIdentifier = { + marketType: MarketType; + marketIndex: number; // u16 +}; + +export type MarginCalculationMode = + | { kind: 'Standard' } + | { kind: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + +export type MarginContext = { + marginType: MarginRequirementType; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // u128 + fuelBonusNumerator: number; // i64 + fuelBonus: number; // u64 + fuelPerpDelta?: { marketIndex: number; delta: BN }; // (u16, i64) + fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 entries + marginRatioOverride?: number; // u32 +}; + +export type IsolatedMarginCalculation = { + marginRequirement: BN; // u128 + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirementPlusBuffer: BN; // u128 +}; + +export type MarginCalculation = { + context: MarginContext; + + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirement: BN; // u128 + marginRequirementPlusBuffer: BN; // u128 + + isolatedMarginCalculations: Map; // BTreeMap + + numSpotLiabilities: number; // u8 + numPerpLiabilities: number; // u8 + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + + totalSpotAssetValue: BN; // i128 + totalSpotLiabilityValue: BN; // u128 + totalPerpLiabilityValue: BN; // u128 + totalPerpPnl: BN; // i128 + + trackedMarketMarginRequirement: BN; // u128 + fuelDeposits: number; // u32 + fuelBorrows: number; // u32 + fuelPositions: number; // u32 +}; +``` + +### Engine API + +```ts +// Pure computation, no I/O; uses data already cached in the client/subscribers +export function computeMarginCalculation(user: User, context: MarginContext): MarginCalculation; + +// Helpers that mirror on-chain semantics +export function meets_margin_requirement(calc: MarginCalculation): boolean; +export function meets_margin_requirement_with_buffer(calc: MarginCalculation): boolean; +export function get_cross_free_collateral(calc: MarginCalculation): BN; +export function get_isolated_free_collateral(calc: MarginCalculation, marketIndex: number): BN; +export function cross_margin_shortage(calc: MarginCalculation): BN; // requires buffer mode +export function isolated_margin_shortage(calc: MarginCalculation, marketIndex: number): BN; // requires buffer mode +``` + +### Computation model (on-demand) + +- The SDK computes the snapshot on-demand when `getMarginCalculation(...)` is called. +- No event-driven recomputation by default (oracle prices can change every slot; recomputing every update would be wasteful). +- Callers (UI/bots) decide polling frequency (e.g., UI can refresh every ~1s on active trade forms). + +### User integration + +- Add `user.getMarginCalculation(margin_type = 'Initial', overrides?: Partial)`. +- Existing getters delegate to the snapshot to avoid duplicate work: + - `getTotalCollateral()` → `snapshot.total_collateral` + - `getMarginRequirement(mode)` → `snapshot.margin_requirement` + - `getFreeCollateral()` → `get_cross_free_collateral(snapshot)` + - Per-market isolated FC → `get_isolated_free_collateral(snapshot, marketIndex)` + +Suggested `User` API surface (non-breaking): + +```ts +// Primary entrypoint +getMarginCalculation( + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): MarginCalculation; + +// Optional conveniences for consumers +getIsolatedMarginCalculation( + marketIndex: number, + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): IsolatedMarginCalculation | undefined; + +// Cross views can continue to use helpers on the snapshot: +// get_cross_free_collateral(snapshot), meets_margin_requirement(snapshot), etc. +``` + +### UI compatibility + +- All existing `User` getters remain and delegate to the snapshot, so current UI keeps working without call-site changes. +- New consumers can call `user.getMarginCalculation()` to access isolated breakdowns. + +### Testing and parity + +- Golden tests comparing SDK snapshot against program outputs (cross and isolated, edge cases). +- Keep math/rounding identical to program (ordering, buffers, funding, open-order IM, oracle strictness). + +### Migration plan (brief) + +1. Implement `types` and `engine` with strict parity; land behind a feature flag. +2. Add `user.getMarginCalculation()` and delegate legacy getters. +3. Optionally update UI hooks to read richer fields; not required for compatibility. +4. Expand parity tests; enable by default after validation. + + diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7b78495e21..cbcc2ccf47 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -107,6 +107,32 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; +// type for account-level margin calculation results. +// Mirrors key fields from on-chain MarginCalculation; can be extended as needed. +export type IsolatedMarginCalculation = { + marginRequirement: BN; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirementPlusBuffer: BN; +}; + +export type UserMarginCalculation = { + context: { marginType: MarginCategory; strict: boolean; marginBuffer?: BN }; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirement: BN; + marginRequirementPlusBuffer: BN; + isolatedMarginCalculations: Map; + numSpotLiabilities: number; + numPerpLiabilities: number; + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; +}; + +export type MarginType = 'Cross' | 'Isolated'; + export class User { driftClient: DriftClient; userAccountPublicKey: PublicKey; @@ -118,6 +144,313 @@ export class User { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + /** + * Compute a consolidated margin snapshot once, without caching. + * Consumers can use this to avoid duplicating work across separate calls. + */ + // TODO: verify this truly matches on-chain logic well + public getMarginCalculation( + marginCategory: MarginCategory = 'Initial', + opts?: { + strict?: boolean; // mirror StrictOraclePrice application + includeOpenOrders?: boolean; + enteringHighLeverage?: boolean; + liquidationBuffer?: BN; // margin_buffer analog for buffer mode + marginRatioOverride?: number; // mirrors context.margin_ratio_override + } + ): UserMarginCalculation { + const strict = opts?.strict ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? + const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided + const marginRatioOverride = opts?.marginRatioOverride; + + // Equivalent to on-chain user_custom_margin_ratio + let userCustomMarginRatio = + marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0; + if (marginRatioOverride !== undefined) { + userCustomMarginRatio = Math.max( + userCustomMarginRatio, + marginRatioOverride + ); + } + + // Initialize calculation (mirrors MarginCalculation::new) + let totalCollateral = ZERO; + let totalCollateralBuffer = ZERO; + let marginRequirement = ZERO; + let marginRequirementPlusBuffer = ZERO; + const isolatedMarginCalculations: Map = + new Map(); + let numSpotLiabilities = 0; + let numPerpLiabilities = 0; + let allDepositOraclesValid = true; + let allLiabilityOraclesValid = true; + let withPerpIsolatedLiability = false; + let withSpotIsolatedLiability = false; + + // SPOT POSITIONS + for (const spotPosition of this.getUserAccount().spotPositions) { + if (isSpotPositionAvailable(spotPosition)) continue; + + const spotMarket = this.driftClient.getSpotMarketAccount( + spotPosition.marketIndex + ); + const oraclePriceData = this.getOracleDataForSpotMarket( + spotPosition.marketIndex + ); + const twap5 = strict + ? calculateLiveOracleTwap( + spotMarket.historicalOracleData, + oraclePriceData, + new BN(Math.floor(Date.now() / 1000)), + FIVE_MINUTE + ) + : undefined; + const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5); + + if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { + const tokenAmount = getSignedTokenAmount( + getTokenAmount( + spotPosition.scaledBalance, + spotMarket, + spotPosition.balanceType + ), + spotPosition.balanceType + ); + if (isVariant(spotPosition.balanceType, 'deposit')) { + // add deposit value to total collateral + const tokenValue = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ); + totalCollateral = totalCollateral.add(tokenValue); + // deposit oracle validity only affects flags; keep it true by default + } else { + // borrow on quote contributes to margin requirement + const tokenValueAbs = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ).abs(); + marginRequirement = marginRequirement.add(tokenValueAbs); + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + tokenValueAbs.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + numSpotLiabilities += 1; + } + continue; + } + + // Non-quote spot: worst-case simulation + const { + tokenAmount: worstCaseTokenAmount, + ordersValue: worstCaseOrdersValue, + tokenValue: worstCaseTokenValue, + weightedTokenValue: worstCaseWeightedTokenValue, + } = getWorstCaseTokenAmounts( + spotPosition, + spotMarket, + strictOracle, + marginCategory, + userCustomMarginRatio + ); + + // open order IM + marginRequirement = marginRequirement.add( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + if (worstCaseTokenAmount.gt(ZERO)) { + // asset side increases total collateral (weighted) + totalCollateral = totalCollateral.add(worstCaseWeightedTokenValue); + } else if (worstCaseTokenAmount.lt(ZERO)) { + // liability side increases margin requirement (weighted >= abs(token_value)) + const liabilityWeighted = worstCaseWeightedTokenValue.abs(); + const liabilityBase = worstCaseTokenValue.abs(); + marginRequirement = marginRequirement.add(liabilityWeighted); + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + liabilityBase.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + numSpotLiabilities += 1; + // flag isolated tier if applicable (approx: isolated asset tier → not available here) + } else if (spotPosition.openOrders !== 0) { + numSpotLiabilities += 1; + } + + // orders value contributes to collateral or requirement + if (worstCaseOrdersValue.gt(ZERO)) { + totalCollateral = totalCollateral.add(worstCaseOrdersValue); + } else if (worstCaseOrdersValue.lt(ZERO)) { + const absVal = worstCaseOrdersValue.abs(); + marginRequirement = marginRequirement.add(absVal); + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + absVal.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + } + } + + // PERP POSITIONS + for (const marketPosition of this.getActivePerpPositions()) { + const market = this.driftClient.getPerpMarketAccount( + marketPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const oraclePriceData = this.getOracleDataForPerpMarket( + market.marketIndex + ); + + // Worst-case perp liability and weighted pnl + const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = + calculateWorstCasePerpLiabilityValue( + marketPosition, + market, + oraclePriceData.price + ); + + // margin ratio for this perp + let marginRatio = new BN( + calculateMarketMarginRatio( + market, + worstCaseBaseAssetAmount.abs(), + marginCategory, + this.getUserAccount().maxMarginRatio, + this.isHighLeverageMode() || enteringHighLeverage + ) + ); + if (isVariant(market.status, 'settlement')) { + marginRatio = ZERO; + } + + // convert liability to quote value and apply margin ratio + const quotePrice = strict + ? BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ) + : quoteOraclePriceData.price; + let perpMarginRequirement = worstCaseLiabilityValue + .mul(quotePrice) + .div(PRICE_PRECISION) + .mul(marginRatio) + .div(MARGIN_PRECISION); + // add open orders IM + perpMarginRequirement = perpMarginRequirement.add( + new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + // weighted unrealized pnl + let positionUnrealizedPnl = calculatePositionPNL( + market, + marketPosition, + true, + oraclePriceData + ); + let pnlQuotePrice: BN; + if (strict && positionUnrealizedPnl.gt(ZERO)) { + pnlQuotePrice = BN.min( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else if (strict && positionUnrealizedPnl.lt(ZERO)) { + pnlQuotePrice = BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else { + pnlQuotePrice = quoteOraclePriceData.price; + } + positionUnrealizedPnl = positionUnrealizedPnl + .mul(pnlQuotePrice) + .div(PRICE_PRECISION); + + // Add perp contribution: isolated vs cross + const isIsolated = false; // TODO: wire to marketPosition.is_isolated when available in TS types + if (isIsolated) { + const existing = isolatedMarginCalculations.get(market.marketIndex) || { + marginRequirement: ZERO, + totalCollateral: ZERO, + totalCollateralBuffer: ZERO, + marginRequirementPlusBuffer: ZERO, + }; + existing.marginRequirement = existing.marginRequirement.add( + perpMarginRequirement + ); + existing.totalCollateral = existing.totalCollateral.add( + positionUnrealizedPnl + ); + if (marginBuffer) { + existing.totalCollateralBuffer = existing.totalCollateralBuffer.add( + positionUnrealizedPnl.isNeg() + ? positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) + : ZERO + ); + existing.marginRequirementPlusBuffer = + existing.marginRequirementPlusBuffer.add( + perpMarginRequirement.add( + worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) + ) + ); + } + isolatedMarginCalculations.set(market.marketIndex, existing); + numPerpLiabilities += 1; + withPerpIsolatedLiability = withPerpIsolatedLiability || false; // TODO: derive from market tier + } else { + // cross: add to global requirement and collateral + marginRequirement = marginRequirement.add(perpMarginRequirement); + totalCollateral = totalCollateral.add(positionUnrealizedPnl); + numPerpLiabilities += + marketPosition.baseAssetAmount.eq(ZERO) && + marketPosition.openOrders === 0 + ? 0 + : 1; + if (marginBuffer) { + marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( + perpMarginRequirement.add( + worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) + ) + ); + if (positionUnrealizedPnl.isNeg()) { + totalCollateralBuffer = totalCollateralBuffer.add( + positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) + ); + } + } + } + } + + return { + context: { + marginType: marginCategory, + strict, + marginBuffer: marginBuffer, + }, + totalCollateral, + totalCollateralBuffer, + marginRequirement, + marginRequirementPlusBuffer, + isolatedMarginCalculations, + numSpotLiabilities, + numPerpLiabilities, + allDepositOraclesValid, + allLiabilityOraclesValid, + withPerpIsolatedLiability, + withSpotIsolatedLiability, + }; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -521,62 +854,128 @@ export class User { */ public getFreeCollateral( marginCategory: MarginCategory = 'Initial', - enterHighLeverageMode = undefined + enterHighLeverageMode = false, + perpMarketIndex?: number ): BN { const totalCollateral = this.getTotalCollateral(marginCategory, true); - const marginRequirement = - marginCategory === 'Initial' - ? this.getInitialMarginRequirement(enterHighLeverageMode) - : this.getMaintenanceMarginRequirement(); + const marginRequirement = this.getMarginRequirement( + marginCategory, + undefined, + true, + true, // includeOpenOrders default + enterHighLeverageMode, + perpMarketIndex ? 'Isolated' : undefined, + perpMarketIndex + ); const freeCollateral = totalCollateral.sub(marginRequirement); return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; } /** - * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION + * @deprecated Use the overload that includes { marginType, perpMarketIndex } */ public getMarginRequirement( marginCategory: MarginCategory, liquidationBuffer?: BN, - strict = false, - includeOpenOrders = true, - enteringHighLeverage = undefined + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean + ): BN; + + /** + * Calculates the margin requirement based on the specified parameters. + * + * @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance'). + * @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios. + * @param strict - Optional flag to enforce strict margin calculations. + * @param includeOpenOrders - Optional flag to include open orders in the margin calculation. + * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode. + * @param marginType - Optional type of margin ('Cross' or 'Isolated'). If 'Isolated', perpMarketIndex must be provided. + * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'. + * + * @returns The calculated margin requirement as a BN (BigNumber). + */ + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + marginType?: MarginType, + perpMarketIndex?: number + ): BN; + + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + marginType?: MarginType, + perpMarketIndex?: number ): BN { - return this.getTotalPerpPositionLiability( - marginCategory, - liquidationBuffer, - includeOpenOrders, + const marginCalc = this.getMarginCalculation(marginCategory, { strict, - enteringHighLeverage - ).add( - this.getSpotMarketLiabilityValue( - undefined, - marginCategory, - liquidationBuffer, - includeOpenOrders, - strict - ) - ); + includeOpenOrders, + enteringHighLeverage, + liquidationBuffer, + }); + + // If marginType is provided and is Isolated, compute only for that market index + if (marginType === 'Isolated') { + if (perpMarketIndex === undefined) { + throw new Error( + 'perpMarketIndex is required when marginType = Isolated' + ); + } + const isolatedMarginCalculation = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + const { marginRequirement } = isolatedMarginCalculation; + + return marginRequirement; + } + + // Default: Cross margin requirement + // TODO: should we be using plus buffer sometimes? + return marginCalc.marginRequirement; } /** * @returns The initial margin requirement in USDC. : QUOTE_PRECISION */ - public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN { + public getInitialMarginRequirement( + enterHighLeverageMode = false, + marginType?: MarginType, + perpMarketIndex?: number + ): BN { return this.getMarginRequirement( 'Initial', undefined, true, undefined, - enterHighLeverageMode + enterHighLeverageMode, + marginType, + perpMarketIndex ); } /** * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION */ - public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN { - return this.getMarginRequirement('Maintenance', liquidationBuffer); + public getMaintenanceMarginRequirement( + liquidationBuffer?: BN, + marginType?: MarginType, + perpMarketIndex?: number + ): BN { + return this.getMarginRequirement( + 'Maintenance', + liquidationBuffer, + true, // strict default + true, // includeOpenOrders default + false, // enteringHighLeverage default + marginType, + perpMarketIndex + ); } public getActivePerpPositionsForUserAccount( @@ -1162,20 +1561,11 @@ export class User { includeOpenOrders = true, liquidationBuffer?: BN ): BN { - return this.getSpotMarketAssetValue( - undefined, - marginCategory, + return this.getMarginCalculation(marginCategory, { + strict, includeOpenOrders, - strict - ).add( - this.getUnrealizedPNL( - true, - undefined, - marginCategory, - strict, - liquidationBuffer - ) - ); + liquidationBuffer, + }).totalCollateral; } public getLiquidationBuffer(): BN | undefined { @@ -1193,13 +1583,27 @@ export class User { * calculates User Health by comparing total collateral and maint. margin requirement * @returns : number (value from [0, 100]) */ - public getHealth(): number { - if (this.isBeingLiquidated()) { + public getHealth(perpMarketIndex?: number): number { + if (this.isBeingLiquidated() && !perpMarketIndex) { return 0; } - const totalCollateral = this.getTotalCollateral('Maintenance'); - const maintenanceMarginReq = this.getMaintenanceMarginRequirement(); + const marginCalc = this.getMarginCalculation('Maintenance'); + + let totalCollateral: BN; + let maintenanceMarginReq: BN; + + if (perpMarketIndex) { + const isolatedMarginCalc = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + if (isolatedMarginCalc) { + totalCollateral = isolatedMarginCalc.totalCollateral; + maintenanceMarginReq = isolatedMarginCalc.marginRequirement; + } + } else { + totalCollateral = marginCalc.totalCollateral; + maintenanceMarginReq = marginCalc.marginRequirement; + } let health: number; @@ -1495,9 +1899,9 @@ export class User { * calculates current user leverage which is (total liability size) / (net asset value) * @returns : Precision TEN_THOUSAND */ - public getLeverage(includeOpenOrders = true): BN { + public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN { return this.calculateLeverageFromComponents( - this.getLeverageComponents(includeOpenOrders) + this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex) ); } @@ -1525,13 +1929,44 @@ export class User { getLeverageComponents( includeOpenOrders = true, - marginCategory: MarginCategory = undefined + marginCategory: MarginCategory = undefined, + perpMarketIndex?: number ): { perpLiabilityValue: BN; perpPnl: BN; spotAssetValue: BN; spotLiabilityValue: BN; } { + if (perpMarketIndex) { + const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex); + const perpLiability = this.calculateWeightedPerpPositionLiability( + perpPosition, + marginCategory, + undefined, + includeOpenOrders + ); + const perpMarket = this.driftClient.getPerpMarketAccount( + perpPosition.marketIndex + ); + + const oraclePriceData = this.getOracleDataForPerpMarket( + perpPosition.marketIndex + ); + + const positionUnrealizedPnl = calculatePositionPNL( + perpMarket, + perpPosition, + true, + oraclePriceData + ); + return { + perpLiabilityValue: perpLiability, + perpPnl: positionUnrealizedPnl, + spotAssetValue: ZERO, + spotLiabilityValue: ZERO, + }; + } + const perpLiability = this.getTotalPerpPositionLiability( marginCategory, undefined, @@ -1821,7 +2256,7 @@ export class User { return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue); } - public canBeLiquidated(): { + public canBeLiquidated(perpMarketIndex?: number): { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; @@ -1835,8 +2270,11 @@ export class User { liquidationBuffer ); - const marginRequirement = - this.getMaintenanceMarginRequirement(liquidationBuffer); + const marginRequirement = this.getMaintenanceMarginRequirement( + liquidationBuffer, + perpMarketIndex ? 'Isolated' : 'Cross', + perpMarketIndex + ); const canBeLiquidated = totalCollateral.lt(marginRequirement); return { @@ -2010,8 +2448,61 @@ export class User { marginCategory: MarginCategory = 'Maintenance', includeOpenOrders = false, offsetCollateral = ZERO, - enteringHighLeverage = undefined + enteringHighLeverage = false, + marginType?: MarginType ): BN { + const market = this.driftClient.getPerpMarketAccount(marketIndex); + + const oracle = + this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; + + const oraclePrice = + this.driftClient.getOracleDataForPerpMarket(marketIndex).price; + + const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); + + if (marginType === 'Isolated') { + const marginCalculation = this.getMarginCalculation(marginCategory, { + strict: false, + includeOpenOrders, + enteringHighLeverage, + }); + const isolatedMarginCalculation = + marginCalculation.isolatedMarginCalculations.get(marketIndex); + const { totalCollateral, marginRequirement } = isolatedMarginCalculation; + + let freeCollateral = BN.max( + ZERO, + totalCollateral.sub(marginRequirement) + ).add(offsetCollateral); + + let freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( + market, + currentPerpPosition, + positionBaseSizeChange, + oraclePrice, + marginCategory, + includeOpenOrders, + enteringHighLeverage + ); + + if (freeCollateralDelta.eq(ZERO)) { + return new BN(-1); + } + + const liqPriceDelta = freeCollateral + .mul(QUOTE_PRECISION) + .div(freeCollateralDelta); + + const liqPrice = oraclePrice.sub(liqPriceDelta); + + if (liqPrice.lt(ZERO)) { + return new BN(-1); + } + + return liqPrice; + } + const totalCollateral = this.getTotalCollateral( marginCategory, false, @@ -2029,15 +2520,6 @@ export class User { totalCollateral.sub(marginRequirement) ).add(offsetCollateral); - const oracle = - this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; - - const oraclePrice = - this.driftClient.getOracleDataForPerpMarket(marketIndex).price; - - const market = this.driftClient.getPerpMarketAccount(marketIndex); - const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); - positionBaseSizeChange = standardizeBaseAssetAmount( positionBaseSizeChange, market.amm.orderStepSize From db4a374e02d0321bfb6a0c6ea2cd71b8754a4797 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Mon, 25 Aug 2025 16:46:35 -0600 Subject: [PATCH 76/91] feat: margin calc unit tests --- sdk/tests/dlob/helpers.ts | 4 + sdk/tests/user/getMarginCalculation.test.ts | 213 ++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 sdk/tests/user/getMarginCalculation.test.ts diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d1b68abe8c..88d203f875 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -27,6 +27,8 @@ import { DataAndSlot, } from '../../src'; import { EventEmitter } from 'events'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { UserEvents } from '../../src/accounts/types'; export const mockPerpPosition: PerpPosition = { baseAssetAmount: new BN(0), @@ -660,6 +662,7 @@ export class MockUserMap implements UserMapInterface { private userMap = new Map(); private userAccountToAuthority = new Map(); private driftClient: DriftClient; + eventEmitter: StrictEventEmitter; constructor() { this.userMap = new Map(); @@ -669,6 +672,7 @@ export class MockUserMap implements UserMapInterface { wallet: new Wallet(new Keypair()), programID: PublicKey.default, }); + this.eventEmitter = new EventEmitter(); } public async subscribe(): Promise {} diff --git a/sdk/tests/user/getMarginCalculation.test.ts b/sdk/tests/user/getMarginCalculation.test.ts new file mode 100644 index 0000000000..36ce90ecb0 --- /dev/null +++ b/sdk/tests/user/getMarginCalculation.test.ts @@ -0,0 +1,213 @@ +import { + BN, + ZERO, + User, + UserAccount, + PublicKey, + PerpMarketAccount, + SpotMarketAccount, + PRICE_PRECISION, + OraclePriceData, + BASE_PRECISION, + QUOTE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + MARGIN_PRECISION, + OPEN_ORDER_MARGIN_REQUIREMENT, +} from '../../src'; +import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; +import { assert } from '../../src/assert/assert'; +import { mockUserAccount as baseMockUserAccount } from './helpers'; +import * as _ from 'lodash'; + +async function makeMockUser( + myMockPerpMarkets: Array, + myMockSpotMarkets: Array, + myMockUserAccount: UserAccount, + perpOraclePriceList: number[], + spotOraclePriceList: number[] +): Promise { + const umap = new MockUserMap(); + const mockUser: User = await umap.mustGet('1'); + mockUser._isSubscribed = true; + mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + + const oraclePriceMap: Record = {}; + for (let i = 0; i < myMockPerpMarkets.length; i++) { + oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = + perpOraclePriceList[i] ?? 1; + } + for (let i = 0; i < myMockSpotMarkets.length; i++) { + oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = + spotOraclePriceList[i] ?? 1; + } + + function getMockUserAccount(): UserAccount { + return myMockUserAccount; + } + function getMockPerpMarket(marketIndex: number): PerpMarketAccount { + return myMockPerpMarkets[marketIndex]; + } + function getMockSpotMarket(marketIndex: number): SpotMarketAccount { + return myMockSpotMarkets[marketIndex]; + } + function getMockOracle(oracleKey: PublicKey) { + const data: OraclePriceData = { + price: new BN( + (oraclePriceMap[oracleKey.toString()] ?? 1) * + PRICE_PRECISION.toNumber() + ), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + return { data, slot: 0 }; + } + function getOracleDataForPerpMarket(marketIndex: number) { + const oracle = getMockPerpMarket(marketIndex).amm.oracle; + return getMockOracle(oracle).data; + } + function getOracleDataForSpotMarket(marketIndex: number) { + const oracle = getMockSpotMarket(marketIndex).oracle; + return getMockOracle(oracle).data; + } + + mockUser.getUserAccount = getMockUserAccount; + mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; + mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; + mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; + mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; + return mockUser; +} + +describe('getMarginCalculation snapshot', () => { + it('empty account returns zeroed snapshot', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(ZERO)); + assert(calc.numSpotLiabilities === 0); + assert(calc.numPerpLiabilities === 0); + }); + + it('quote deposit increases totalCollateral, no requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expected = new BN('10000000000'); // $10k + assert(calc.totalCollateral.eq(expected)); + assert(calc.marginRequirement.eq(ZERO)); + }); + + it('quote borrow increases requirement and buffer applies', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Borrow 100 quote + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const tenPercent = MARGIN_PRECISION.divn(10); + const calc = user.getMarginCalculation('Initial', { + liquidationBuffer: tenPercent, + }); + const liability = new BN(100).mul(QUOTE_PRECISION); // $100 + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(liability)); + assert( + calc.marginRequirementPlusBuffer.eq( + liability.mul(tenPercent).div(MARGIN_PRECISION) + ) + ); + assert(calc.numSpotLiabilities === 1); + }); + + it('non-quote spot open orders add IM', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Market 1 (e.g., SOL) with 2 open orders + myMockUserAccount.spotPositions[1].marketIndex = 1; + myMockUserAccount.spotPositions[1].openOrders = 2; + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); + assert(calc.marginRequirement.eq(expectedIM)); + }); + + it('perp long liability reflects maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // 20 base long, -$10 quote (liability) + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + // From existing liquidation test expectations: 2_000_000 + assert(calc.marginRequirement.eq(new BN('2000000'))); + }); +}); + + From 950b3c3e9098831cab628ee052d9b8cfbdd75b9b Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 26 Aug 2025 15:14:17 -0600 Subject: [PATCH 77/91] temp --- sdk/tests/amm/test.ts | 2 +- sdk/tests/user/getMarginCalculation.test.ts | 213 ---------- sdk/tests/user/getMarginCalculation.ts | 430 ++++++++++++++++++++ 3 files changed, 431 insertions(+), 214 deletions(-) delete mode 100644 sdk/tests/user/getMarginCalculation.test.ts create mode 100644 sdk/tests/user/getMarginCalculation.ts diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index ab849c57d6..b4377f4066 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -279,7 +279,7 @@ describe('AMM Tests', () => { longIntensity, shortIntensity, volume24H, - 0 + 0, ); const l1 = spreads[0]; const s1 = spreads[1]; diff --git a/sdk/tests/user/getMarginCalculation.test.ts b/sdk/tests/user/getMarginCalculation.test.ts deleted file mode 100644 index 36ce90ecb0..0000000000 --- a/sdk/tests/user/getMarginCalculation.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { - BN, - ZERO, - User, - UserAccount, - PublicKey, - PerpMarketAccount, - SpotMarketAccount, - PRICE_PRECISION, - OraclePriceData, - BASE_PRECISION, - QUOTE_PRECISION, - SPOT_MARKET_BALANCE_PRECISION, - SpotBalanceType, - MARGIN_PRECISION, - OPEN_ORDER_MARGIN_REQUIREMENT, -} from '../../src'; -import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; -import { assert } from '../../src/assert/assert'; -import { mockUserAccount as baseMockUserAccount } from './helpers'; -import * as _ from 'lodash'; - -async function makeMockUser( - myMockPerpMarkets: Array, - myMockSpotMarkets: Array, - myMockUserAccount: UserAccount, - perpOraclePriceList: number[], - spotOraclePriceList: number[] -): Promise { - const umap = new MockUserMap(); - const mockUser: User = await umap.mustGet('1'); - mockUser._isSubscribed = true; - mockUser.driftClient._isSubscribed = true; - mockUser.driftClient.accountSubscriber.isSubscribed = true; - - const oraclePriceMap: Record = {}; - for (let i = 0; i < myMockPerpMarkets.length; i++) { - oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = - perpOraclePriceList[i] ?? 1; - } - for (let i = 0; i < myMockSpotMarkets.length; i++) { - oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = - spotOraclePriceList[i] ?? 1; - } - - function getMockUserAccount(): UserAccount { - return myMockUserAccount; - } - function getMockPerpMarket(marketIndex: number): PerpMarketAccount { - return myMockPerpMarkets[marketIndex]; - } - function getMockSpotMarket(marketIndex: number): SpotMarketAccount { - return myMockSpotMarkets[marketIndex]; - } - function getMockOracle(oracleKey: PublicKey) { - const data: OraclePriceData = { - price: new BN( - (oraclePriceMap[oracleKey.toString()] ?? 1) * - PRICE_PRECISION.toNumber() - ), - slot: new BN(0), - confidence: new BN(1), - hasSufficientNumberOfDataPoints: true, - }; - return { data, slot: 0 }; - } - function getOracleDataForPerpMarket(marketIndex: number) { - const oracle = getMockPerpMarket(marketIndex).amm.oracle; - return getMockOracle(oracle).data; - } - function getOracleDataForSpotMarket(marketIndex: number) { - const oracle = getMockSpotMarket(marketIndex).oracle; - return getMockOracle(oracle).data; - } - - mockUser.getUserAccount = getMockUserAccount; - mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; - mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; - mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; - mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; - mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; - return mockUser; -} - -describe('getMarginCalculation snapshot', () => { - it('empty account returns zeroed snapshot', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Initial'); - assert(calc.totalCollateral.eq(ZERO)); - assert(calc.marginRequirement.eq(ZERO)); - assert(calc.numSpotLiabilities === 0); - assert(calc.numPerpLiabilities === 0); - }); - - it('quote deposit increases totalCollateral, no requirement', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; - myMockUserAccount.spotPositions[0].scaledBalance = new BN( - 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() - ); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Initial'); - const expected = new BN('10000000000'); // $10k - assert(calc.totalCollateral.eq(expected)); - assert(calc.marginRequirement.eq(ZERO)); - }); - - it('quote borrow increases requirement and buffer applies', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - // Borrow 100 quote - myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; - myMockUserAccount.spotPositions[0].scaledBalance = new BN( - 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() - ); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const tenPercent = MARGIN_PRECISION.divn(10); - const calc = user.getMarginCalculation('Initial', { - liquidationBuffer: tenPercent, - }); - const liability = new BN(100).mul(QUOTE_PRECISION); // $100 - assert(calc.totalCollateral.eq(ZERO)); - assert(calc.marginRequirement.eq(liability)); - assert( - calc.marginRequirementPlusBuffer.eq( - liability.mul(tenPercent).div(MARGIN_PRECISION) - ) - ); - assert(calc.numSpotLiabilities === 1); - }); - - it('non-quote spot open orders add IM', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - // Market 1 (e.g., SOL) with 2 open orders - myMockUserAccount.spotPositions[1].marketIndex = 1; - myMockUserAccount.spotPositions[1].openOrders = 2; - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Initial'); - const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); - assert(calc.marginRequirement.eq(expectedIM)); - }); - - it('perp long liability reflects maintenance requirement', async () => { - const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); - const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); - const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - - // 20 base long, -$10 quote (liability) - myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( - BASE_PRECISION - ); - myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( - QUOTE_PRECISION - ); - - const user: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - myMockUserAccount, - [1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1] - ); - - const calc = user.getMarginCalculation('Maintenance'); - // From existing liquidation test expectations: 2_000_000 - assert(calc.marginRequirement.eq(new BN('2000000'))); - }); -}); - - diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts new file mode 100644 index 0000000000..9ca9b5e4aa --- /dev/null +++ b/sdk/tests/user/getMarginCalculation.ts @@ -0,0 +1,430 @@ +import { + BN, + ZERO, + User, + UserAccount, + PublicKey, + PerpMarketAccount, + SpotMarketAccount, + PRICE_PRECISION, + OraclePriceData, + BASE_PRECISION, + QUOTE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + MARGIN_PRECISION, + OPEN_ORDER_MARGIN_REQUIREMENT, + SPOT_MARKET_WEIGHT_PRECISION, +} from '../../src'; +import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; +import { assert } from '../../src/assert/assert'; +import { mockUserAccount as baseMockUserAccount } from './helpers'; +import * as _ from 'lodash'; + +async function makeMockUser( + myMockPerpMarkets: Array, + myMockSpotMarkets: Array, + myMockUserAccount: UserAccount, + perpOraclePriceList: number[], + spotOraclePriceList: number[] +): Promise { + const umap = new MockUserMap(); + const mockUser: User = await umap.mustGet('1'); + mockUser._isSubscribed = true; + mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + + const oraclePriceMap: Record = {}; + for (let i = 0; i < myMockPerpMarkets.length; i++) { + oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = + perpOraclePriceList[i] ?? 1; + } + for (let i = 0; i < myMockSpotMarkets.length; i++) { + oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = + spotOraclePriceList[i] ?? 1; + } + + function getMockUserAccount(): UserAccount { + return myMockUserAccount; + } + function getMockPerpMarket(marketIndex: number): PerpMarketAccount { + return myMockPerpMarkets[marketIndex]; + } + function getMockSpotMarket(marketIndex: number): SpotMarketAccount { + return myMockSpotMarkets[marketIndex]; + } + function getMockOracle(oracleKey: PublicKey) { + const data: OraclePriceData = { + price: new BN( + (oraclePriceMap[oracleKey.toString()] ?? 1) * + PRICE_PRECISION.toNumber() + ), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + return { data, slot: 0 }; + } + function getOracleDataForPerpMarket(marketIndex: number) { + const oracle = getMockPerpMarket(marketIndex).amm.oracle; + return getMockOracle(oracle).data; + } + function getOracleDataForSpotMarket(marketIndex: number) { + const oracle = getMockSpotMarket(marketIndex).oracle; + return getMockOracle(oracle).data; + } + + mockUser.getUserAccount = getMockUserAccount; + mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; + mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; + mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; + mockUser.driftClient.getOracleDataForPerpMarket = getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = getOracleDataForSpotMarket as any; + return mockUser; +} + +describe('getMarginCalculation snapshot', () => { + it('empty account returns zeroed snapshot', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(ZERO)); + assert(calc.numSpotLiabilities === 0); + assert(calc.numPerpLiabilities === 0); + }); + + it('quote deposit increases totalCollateral, no requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expected = new BN('10000000000'); // $10k + assert(calc.totalCollateral.eq(expected)); + assert(calc.marginRequirement.eq(ZERO)); + }); + + it('quote borrow increases requirement and buffer applies', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Borrow 100 quote + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const tenPercent = MARGIN_PRECISION.divn(10); + const calc = user.getMarginCalculation('Initial', { + liquidationBuffer: tenPercent, + }); + const liability = new BN(100).mul(QUOTE_PRECISION); // $100 + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(liability)); + assert( + calc.marginRequirementPlusBuffer.eq( + liability.mul(tenPercent).div(MARGIN_PRECISION) + ) + ); + assert(calc.numSpotLiabilities === 1); + }); + + it('non-quote spot open orders add IM', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Market 1 (e.g., SOL) with 2 open orders + myMockUserAccount.spotPositions[1].marketIndex = 1; + myMockUserAccount.spotPositions[1].openOrders = 2; + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); + assert(calc.marginRequirement.eq(expectedIM)); + }); + + it('perp long liability reflects maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // 20 base long, -$10 quote (liability) + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + // From existing liquidation test expectations: 2_000_000 + assert(calc.marginRequirement.eq(new BN('2000000'))); + }); + + it.skip('maker position reducing: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Perp exposure: 20 base notional at oracle price 1 → maintenance MR = 10% of $20 = $2 + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + // Set entry/breakeven at $1 so unrealized PnL = $0 + myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + // Provide exactly $2 in quote collateral + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN(2).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + console.log('calc.marginRequirement', calc.marginRequirement.toString()); + console.log('calc.totalCollateral', calc.totalCollateral.toString()); + assert(calc.marginRequirement.eq(calc.totalCollateral)); + }); + + it('maker reducing after simulated fill: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + + // Build maker and taker accounts + const makerAccount = _.cloneDeep(baseMockUserAccount); + const takerAccount = _.cloneDeep(baseMockUserAccount); + + // Oracle price = 1 for perp and spot + const perpOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + const spotOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + + // Pre-fill: maker has 21 base long at entry 1 ($21 notional), taker flat + makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul(BASE_PRECISION); + makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul(QUOTE_PRECISION); + makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul(QUOTE_PRECISION); + // Provide exactly $2 in quote collateral to equal 10% maintenance of 20 notional post-fill + makerAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + makerAccount.spotPositions[0].scaledBalance = new BN(2).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + // Simulate fill: maker sells 1 base to taker at price = oracle = 1 + // Post-fill maker position: 20 base long with zero unrealized PnL + const maker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + makerAccount, + perpOracles, + spotOracles + ); + const taker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + takerAccount, + perpOracles, + spotOracles + ); + + // Apply synthetic trade deltas to both user accounts + // Maker: base 21 -> 20; taker: base 0 -> 1. Use quote deltas consistent with price 1, fee 0 + maker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + // Align quoteAssetAmount with base value so unrealized PnL = 0 at price 1 + maker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + + taker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(1).mul( + BASE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + // Also set taker's quoteAssetAmount consistently + taker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + + const makerCalc = maker.getMarginCalculation('Maintenance'); + assert(makerCalc.marginRequirement.eq(makerCalc.totalCollateral)); + assert(makerCalc.marginRequirement.gt(ZERO)); + }); + + it('isolated position margin requirement (SDK parity)', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + + // Configure perp market 0 ratios to match on-chain test + myMockPerpMarkets[0].marginRatioInitial = 1000; // 10% + myMockPerpMarkets[0].marginRatioMaintenance = 500; // 5% + + // Configure spot market 1 (e.g., SOL) weights to match on-chain test + myMockSpotMarkets[1].initialAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 8) / 10; // 0.8 + myMockSpotMarkets[1].maintenanceAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 9) / 10; // 0.9 + myMockSpotMarkets[1].initialLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 12) / 10; // 1.2 + myMockSpotMarkets[1].maintenanceLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 11) / 10; // 1.1 + + // ---------- Cross margin only (spot positions) ---------- + const crossAccount = _.cloneDeep(baseMockUserAccount); + // USDC deposit: $20,000 + crossAccount.spotPositions[0].marketIndex = 0; + crossAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + crossAccount.spotPositions[0].scaledBalance = new BN(20000).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // SOL borrow: 100 units + crossAccount.spotPositions[1].marketIndex = 1; + crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW; + crossAccount.spotPositions[1].scaledBalance = new BN(10000).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // No perp exposure in cross calc + crossAccount.perpPositions[0].baseAssetAmount = ZERO; + crossAccount.perpPositions[0].quoteAssetAmount = ZERO; + + const userCross: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + crossAccount, + [100, 1, 1, 1, 1, 1, 1, 1], // perp oracle for market 0 = 100 + [1, 100, 1, 1, 1, 1, 1, 1] // spot oracle: usdc=1, sol=100 + ); + + const crossCalc = userCross.getMarginCalculation('Initial'); + // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 + assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); + // Expect: cross total collateral from USDC deposit only = $20,000 + assert(crossCalc.totalCollateral.eq(new BN('20000000000'))); + // Meets cross margin requirement + assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral)); + + // With 10% buffer + const tenPct = new BN(1000); + const crossCalcBuf = userCross.getMarginCalculation('Initial', { + liquidationBuffer: tenPct, + }); + console.log('crossCalcBuf.marginRequirementPlusBuffer', crossCalcBuf.marginRequirementPlusBuffer.toString()); + console.log('crossCalcBuf.totalCollateralBuffer', crossCalcBuf.totalCollateralBuffer.toString()); + assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); + const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add( + crossCalcBuf.totalCollateralBuffer + ); + assert(crossTotalPlusBuffer.eq(new BN('20000000000'))); + + // ---------- Isolated perp position (simulate isolated by separate user) ---------- + const isolatedAccount = _.cloneDeep(baseMockUserAccount); + // Perp: 100 base long, quote -11,000 => PnL = 10k - 11k = -$1,000 + isolatedAccount.perpPositions[0].baseAssetAmount = new BN(100).mul( + BASE_PRECISION + ); + isolatedAccount.perpPositions[0].quoteAssetAmount = new BN(-11000).mul( + QUOTE_PRECISION + ); + // Simulate isolated balance: $100 quote deposit on this user + isolatedAccount.spotPositions[0].marketIndex = 0; + isolatedAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + isolatedAccount.spotPositions[0].scaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + const userIsolated: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + isolatedAccount, + [100, 1, 1, 1, 1, 1, 1, 1], + [1, 100, 1, 1, 1, 1, 1, 1] + ); + + const isoCalc = userIsolated.getMarginCalculation('Initial'); + // Expect: perp initial MR = 10% * $10,000 = $1,000 + assert(isoCalc.marginRequirement.eq(new BN('1000000000'))); + // Expect: total collateral = $100 (deposit) + (-$1,000) (PnL) = -$900 + assert(isoCalc.totalCollateral.eq(new BN('-900000000'))); + assert(isoCalc.marginRequirement.gt(isoCalc.totalCollateral)); + + const isoCalcBuf = userIsolated.getMarginCalculation('Initial', { + liquidationBuffer: tenPct, + }); + assert(isoCalcBuf.marginRequirementPlusBuffer.eq(new BN('2000000000'))); + const isoTotalPlusBuffer = isoCalcBuf.totalCollateral.add( + isoCalcBuf.totalCollateralBuffer + ); + assert(isoTotalPlusBuffer.eq(new BN('-1000000000'))); + }); +}); + + From b24334c52dc0a55e39081e655ba63d790439d326 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 27 Aug 2025 23:45:41 -0600 Subject: [PATCH 78/91] feat: finally - parity with on-chain cargo test --- sdk/src/decode/user.ts | 5 +- sdk/src/marginCalculation.ts | 351 +++++++++++++++++++++++++ sdk/src/types.ts | 6 + sdk/src/user.ts | 216 ++++++++------- sdk/tests/dlob/helpers.ts | 1 + sdk/tests/user/getMarginCalculation.ts | 64 ++--- sdk/tests/user/test.ts | 7 - 7 files changed, 484 insertions(+), 166 deletions(-) create mode 100644 sdk/src/marginCalculation.ts diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index e0d852f6e8..c3022d06aa 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -117,7 +117,9 @@ export function decodeUser(buffer: Buffer): UserAccount { offset += 3; const perLpBase = buffer.readUInt8(offset); offset += 1; - + // TODO: verify this works + const positionFlag = buffer.readUInt8(offset); + offset += 1; perpPositions.push({ lastCumulativeFundingRate, baseAssetAmount, @@ -135,6 +137,7 @@ export function decodeUser(buffer: Buffer): UserAccount { openOrders, perLpBase, maxMarginRatio, + positionFlag, }); } diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts new file mode 100644 index 0000000000..2cb1dbe839 --- /dev/null +++ b/sdk/src/marginCalculation.ts @@ -0,0 +1,351 @@ +import { BN } from './'; +import { MARGIN_PRECISION } from './constants/numericConstants'; +import { MarketType } from './types'; + +export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill'; + +export type MarginCalculationMode = + | { type: 'Standard' } + | { type: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + +export class MarketIdentifier { + marketType: MarketType; + marketIndex: number; + + private constructor(marketType: MarketType, marketIndex: number) { + this.marketType = marketType; + this.marketIndex = marketIndex; + } + + static spot(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.SPOT, marketIndex); + } + + static perp(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.PERP, marketIndex); + } + + equals(other: MarketIdentifier | undefined): boolean { + return ( + !!other && + this.marketType === other.marketType && + this.marketIndex === other.marketIndex + ); + } +} + +export class MarginContext { + marginType: MarginCategory; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // scaled by MARGIN_PRECISION + fuelBonusNumerator: BN; // seconds since last update + fuelBonus: BN; // not used in calculation aggregation here + fuelPerpDelta?: { marketIndex: number; delta: BN }; + fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 in rust + marginRatioOverride?: number; + + private constructor(marginType: MarginCategory) { + this.marginType = marginType; + this.mode = { type: 'Standard' }; + this.strict = false; + this.ignoreInvalidDepositOracles = false; + this.marginBuffer = new BN(0); + this.fuelBonusNumerator = new BN(0); + this.fuelBonus = new BN(0); + this.fuelSpotDeltas = []; + } + + static standard(marginType: MarginCategory): MarginContext { + return new MarginContext(marginType); + } + + static liquidation(marginBuffer: BN): MarginContext { + const ctx = new MarginContext('Maintenance'); + ctx.mode = { type: 'Liquidation' }; + ctx.marginBuffer = marginBuffer ?? new BN(0); + return ctx; + } + + strictMode(strict: boolean): this { + this.strict = strict; + return this; + } + + ignoreInvalidDeposits(ignore: boolean): this { + this.ignoreInvalidDepositOracles = ignore; + return this; + } + + setMarginBuffer(buffer?: BN): this { + this.marginBuffer = buffer ?? new BN(0); + return this; + } + + setFuelPerpDelta(marketIndex: number, delta: BN): this { + this.fuelPerpDelta = { marketIndex, delta }; + return this; + } + + setFuelSpotDelta(marketIndex: number, delta: BN): this { + this.fuelSpotDeltas = [{ marketIndex, delta }]; + return this; + } + + setFuelSpotDeltas(deltas: Array<{ marketIndex: number; delta: BN }>): this { + this.fuelSpotDeltas = deltas; + return this; + } + + setFuelNumerator(numerator: BN): this { + this.fuelBonusNumerator = numerator ?? new BN(0); + return this; + } + + setMarginRatioOverride(ratio: number): this { + this.marginRatioOverride = ratio; + return this; + } + + trackMarketMarginRequirement(marketIdentifier: MarketIdentifier): this { + if (this.mode.type !== 'Liquidation') { + throw new Error( + 'InvalidMarginCalculation: Cant track market outside of liquidation mode' + ); + } + this.mode.marketToTrackMarginRequirement = marketIdentifier; + return this; + } +} + +export class IsolatedMarginCalculation { + marginRequirement: BN; + totalCollateral: BN; // deposit + pnl + totalCollateralBuffer: BN; + marginRequirementPlusBuffer: BN; + + constructor() { + this.marginRequirement = new BN(0); + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + } + + getTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsMarginRequirementWithBuffer(): boolean { + return this.getTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + marginShortage(): BN { + const shortage = this.marginRequirementPlusBuffer.sub( + this.getTotalCollateralPlusBuffer() + ); + return shortage.isNeg() ? new BN(0) : shortage; + } +} + +export class MarginCalculation { + context: MarginContext; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirement: BN; + marginRequirementPlusBuffer: BN; + isolatedMarginCalculations: Map; + numSpotLiabilities: number; + numPerpLiabilities: number; + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + trackedMarketMarginRequirement: BN; + fuelDeposits: number; + fuelBorrows: number; + fuelPositions: number; + + constructor(context: MarginContext) { + this.context = context; + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirement = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + this.isolatedMarginCalculations = new Map(); + this.numSpotLiabilities = 0; + this.numPerpLiabilities = 0; + this.allDepositOraclesValid = true; + this.allLiabilityOraclesValid = true; + this.withPerpIsolatedLiability = false; + this.withSpotIsolatedLiability = false; + this.trackedMarketMarginRequirement = new BN(0); + this.fuelDeposits = 0; + this.fuelBorrows = 0; + this.fuelPositions = 0; + } + + addIsolatedTotalCollateral(delta: BN): void { + this.totalCollateral = this.totalCollateral.add(delta); + if (this.context.marginBuffer.gt(new BN(0)) && delta.isNeg()) { + this.totalCollateralBuffer = this.totalCollateralBuffer.add( + delta.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ); + } + } + + addIsolatedMarginRequirement( + marginRequirement: BN, + liabilityValue: BN, + marketIdentifier: MarketIdentifier + ): void { + this.marginRequirement = this.marginRequirement.add(marginRequirement); + if (this.context.marginBuffer.gt(new BN(0))) { + this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add( + marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + ); + } + const tracked = this.marketToTrackMarginRequirement(); + if (tracked && tracked.equals(marketIdentifier)) { + this.trackedMarketMarginRequirement = + this.trackedMarketMarginRequirement.add(marginRequirement); + } + } + + addIsolatedMarginCalculation( + marketIndex: number, + depositValue: BN, + pnl: BN, + liabilityValue: BN, + marginRequirement: BN + ): void { + const totalCollateral = depositValue.add(pnl); + const totalCollateralBuffer = + this.context.marginBuffer.gt(new BN(0)) && pnl.isNeg() + ? pnl.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + : new BN(0); + + const marginRequirementPlusBuffer = this.context.marginBuffer.gt(new BN(0)) + ? marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + : new BN(0); + + const iso = new IsolatedMarginCalculation(); + iso.marginRequirement = marginRequirement; + iso.totalCollateral = totalCollateral; + iso.totalCollateralBuffer = totalCollateralBuffer; + iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer; + this.isolatedMarginCalculations.set(marketIndex, iso); + + const tracked = this.marketToTrackMarginRequirement(); + if (tracked && tracked.equals(MarketIdentifier.perp(marketIndex))) { + this.trackedMarketMarginRequirement = + this.trackedMarketMarginRequirement.add(marginRequirement); + } + } + + addSpotLiability(): void { + this.numSpotLiabilities += 1; + } + + addPerpLiability(): void { + this.numPerpLiabilities += 1; + } + + updateAllDepositOraclesValid(valid: boolean): void { + this.allDepositOraclesValid = this.allDepositOraclesValid && valid; + } + + updateAllLiabilityOraclesValid(valid: boolean): void { + this.allLiabilityOraclesValid = this.allLiabilityOraclesValid && valid; + } + + updateWithSpotIsolatedLiability(isolated: boolean): void { + this.withSpotIsolatedLiability = this.withSpotIsolatedLiability || isolated; + } + + updateWithPerpIsolatedLiability(isolated: boolean): void { + this.withPerpIsolatedLiability = this.withPerpIsolatedLiability || isolated; + } + + validateNumSpotLiabilities(): void { + if (this.numSpotLiabilities > 0 && this.marginRequirement.eq(new BN(0))) { + throw new Error( + 'InvalidMarginRatio: num_spot_liabilities>0 but margin_requirement=0' + ); + } + } + + getNumOfLiabilities(): number { + return this.numSpotLiabilities + this.numPerpLiabilities; + } + + getCrossTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsCrossMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsCrossMarginRequirementWithBuffer(): boolean { + return this.getCrossTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + meetsMarginRequirement(): boolean { + if (!this.meetsCrossMarginRequirement()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirement()) return false; + } + return true; + } + + meetsMarginRequirementWithBuffer(): boolean { + if (!this.meetsCrossMarginRequirementWithBuffer()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirementWithBuffer()) return false; + } + return true; + } + + getCrossFreeCollateral(): BN { + const free = this.totalCollateral.sub(this.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedFreeCollateral(marketIndex: number): BN { + const iso = this.isolatedMarginCalculations.get(marketIndex); + if (!iso) + throw new Error('InvalidMarginCalculation: missing isolated calc'); + const free = iso.totalCollateral.sub(iso.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedMarginCalculation( + marketIndex: number + ): IsolatedMarginCalculation | undefined { + return this.isolatedMarginCalculations.get(marketIndex); + } + + hasIsolatedMarginCalculation(marketIndex: number): boolean { + return this.isolatedMarginCalculations.has(marketIndex); + } + + private marketToTrackMarginRequirement(): MarketIdentifier | undefined { + if (this.context.mode.type === 'Liquidation') { + return this.context.mode.marketToTrackMarginRequirement; + } + return undefined; + } +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f263e2b9cc..c161e724a5 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1264,6 +1264,12 @@ export class OrderParamsBitFlag { static readonly UpdateHighLeverageMode = 2; } +export class PositionFlag { + static readonly IsolatedPosition = 1; + static readonly BeingLiquidated = 2; + static readonly Bankruptcy = 3; +} + export type NecessaryOrderParams = { orderType: OrderType; marketIndex: number; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index cbcc2ccf47..8aa3098539 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -68,6 +68,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade'; import { MarketType, PositionDirection, + PositionFlag, SpotBalanceType, SpotMarketAccount, } from './types'; @@ -106,15 +107,12 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; - -// type for account-level margin calculation results. -// Mirrors key fields from on-chain MarginCalculation; can be extended as needed. -export type IsolatedMarginCalculation = { - marginRequirement: BN; - totalCollateral: BN; - totalCollateralBuffer: BN; - marginRequirementPlusBuffer: BN; -}; +import { + MarginCalculation as JsMarginCalculation, + MarginContext, + MarketIdentifier, + IsolatedMarginCalculation, +} from './marginCalculation'; export type UserMarginCalculation = { context: { marginType: MarginCategory; strict: boolean; marginBuffer?: BN }; @@ -148,7 +146,6 @@ export class User { * Compute a consolidated margin snapshot once, without caching. * Consumers can use this to avoid duplicating work across separate calls. */ - // TODO: verify this truly matches on-chain logic well public getMarginCalculation( marginCategory: MarginCategory = 'Initial', opts?: { @@ -160,8 +157,8 @@ export class User { } ): UserMarginCalculation { const strict = opts?.strict ?? false; - const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided const marginRatioOverride = opts?.marginRatioOverride; @@ -175,19 +172,12 @@ export class User { ); } - // Initialize calculation (mirrors MarginCalculation::new) - let totalCollateral = ZERO; - let totalCollateralBuffer = ZERO; - let marginRequirement = ZERO; - let marginRequirementPlusBuffer = ZERO; - const isolatedMarginCalculations: Map = - new Map(); - let numSpotLiabilities = 0; - let numPerpLiabilities = 0; - let allDepositOraclesValid = true; - let allLiabilityOraclesValid = true; - let withPerpIsolatedLiability = false; - let withSpotIsolatedLiability = false; + // Initialize calc via JS mirror of Rust MarginCalculation + const ctx = MarginContext.standard(marginCategory) + .strictMode(strict) + .setMarginBuffer(marginBuffer) + .setMarginRatioOverride(userCustomMarginRatio); + const calc = new JsMarginCalculation(ctx); // SPOT POSITIONS for (const spotPosition of this.getUserAccount().spotPositions) { @@ -225,8 +215,7 @@ export class User { spotMarket.decimals, strictOracle ); - totalCollateral = totalCollateral.add(tokenValue); - // deposit oracle validity only affects flags; keep it true by default + calc.addIsolatedTotalCollateral(tokenValue); } else { // borrow on quote contributes to margin requirement const tokenValueAbs = getStrictTokenValue( @@ -234,13 +223,12 @@ export class User { spotMarket.decimals, strictOracle ).abs(); - marginRequirement = marginRequirement.add(tokenValueAbs); - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - tokenValueAbs.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } - numSpotLiabilities += 1; + calc.addIsolatedMarginRequirement( + tokenValueAbs, + tokenValueAbs, + MarketIdentifier.spot(0) + ); + calc.addSpotLiability(); } continue; } @@ -260,40 +248,40 @@ export class User { ); // open order IM - marginRequirement = marginRequirement.add( - new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) - ); + if (includeOpenOrders) { + calc.addIsolatedMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO, + MarketIdentifier.spot(spotPosition.marketIndex) + ); + } if (worstCaseTokenAmount.gt(ZERO)) { // asset side increases total collateral (weighted) - totalCollateral = totalCollateral.add(worstCaseWeightedTokenValue); + calc.addIsolatedTotalCollateral(worstCaseWeightedTokenValue); } else if (worstCaseTokenAmount.lt(ZERO)) { // liability side increases margin requirement (weighted >= abs(token_value)) const liabilityWeighted = worstCaseWeightedTokenValue.abs(); - const liabilityBase = worstCaseTokenValue.abs(); - marginRequirement = marginRequirement.add(liabilityWeighted); - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - liabilityBase.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } - numSpotLiabilities += 1; - // flag isolated tier if applicable (approx: isolated asset tier → not available here) + calc.addIsolatedMarginRequirement( + liabilityWeighted, + worstCaseTokenValue.abs(), + MarketIdentifier.spot(spotPosition.marketIndex) + ); + calc.addSpotLiability(); } else if (spotPosition.openOrders !== 0) { - numSpotLiabilities += 1; + calc.addSpotLiability(); } // orders value contributes to collateral or requirement if (worstCaseOrdersValue.gt(ZERO)) { - totalCollateral = totalCollateral.add(worstCaseOrdersValue); + calc.addIsolatedTotalCollateral(worstCaseOrdersValue); } else if (worstCaseOrdersValue.lt(ZERO)) { const absVal = worstCaseOrdersValue.abs(); - marginRequirement = marginRequirement.add(absVal); - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - absVal.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } + calc.addIsolatedMarginRequirement( + absVal, + absVal, + MarketIdentifier.spot(0) + ); } } @@ -377,56 +365,56 @@ export class User { .div(PRICE_PRECISION); // Add perp contribution: isolated vs cross - const isIsolated = false; // TODO: wire to marketPosition.is_isolated when available in TS types + const isIsolated = this.isPerpPositionIsolated(marketPosition); if (isIsolated) { - const existing = isolatedMarginCalculations.get(market.marketIndex) || { - marginRequirement: ZERO, - totalCollateral: ZERO, - totalCollateralBuffer: ZERO, - marginRequirementPlusBuffer: ZERO, - }; - existing.marginRequirement = existing.marginRequirement.add( - perpMarginRequirement - ); - existing.totalCollateral = existing.totalCollateral.add( - positionUnrealizedPnl - ); - if (marginBuffer) { - existing.totalCollateralBuffer = existing.totalCollateralBuffer.add( - positionUnrealizedPnl.isNeg() - ? positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) - : ZERO + // derive isolated quote deposit value, mirroring on-chain logic + let depositValue = ZERO; + if (marketPosition.isolatedPositionScaledBalance) { + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const strictQuote = new StrictOraclePrice( + quoteOraclePriceData.price, + strict + ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + : undefined + ); + const quoteTokenAmount = getTokenAmount( + marketPosition.isolatedPositionScaledBalance, + quoteSpotMarket, + SpotBalanceType.DEPOSIT + ); + depositValue = getStrictTokenValue( + quoteTokenAmount, + quoteSpotMarket.decimals, + strictQuote ); - existing.marginRequirementPlusBuffer = - existing.marginRequirementPlusBuffer.add( - perpMarginRequirement.add( - worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) - ) - ); } - isolatedMarginCalculations.set(market.marketIndex, existing); - numPerpLiabilities += 1; - withPerpIsolatedLiability = withPerpIsolatedLiability || false; // TODO: derive from market tier + calc.addIsolatedMarginCalculation( + market.marketIndex, + depositValue, + positionUnrealizedPnl, + worstCaseLiabilityValue, + perpMarginRequirement + ); + calc.addPerpLiability(); } else { // cross: add to global requirement and collateral - marginRequirement = marginRequirement.add(perpMarginRequirement); - totalCollateral = totalCollateral.add(positionUnrealizedPnl); - numPerpLiabilities += - marketPosition.baseAssetAmount.eq(ZERO) && - marketPosition.openOrders === 0 - ? 0 - : 1; - if (marginBuffer) { - marginRequirementPlusBuffer = marginRequirementPlusBuffer.add( - perpMarginRequirement.add( - worstCaseLiabilityValue.mul(marginBuffer).div(MARGIN_PRECISION) - ) - ); - if (positionUnrealizedPnl.isNeg()) { - totalCollateralBuffer = totalCollateralBuffer.add( - positionUnrealizedPnl.mul(marginBuffer).div(MARGIN_PRECISION) - ); - } + calc.addIsolatedMarginRequirement( + perpMarginRequirement, + worstCaseLiabilityValue, + MarketIdentifier.perp(market.marketIndex) + ); + calc.addIsolatedTotalCollateral(positionUnrealizedPnl); + const hasPerpLiability = + !marketPosition.baseAssetAmount.eq(ZERO) || + marketPosition.quoteAssetAmount.lt(ZERO) || + marketPosition.openOrders !== 0; + if (hasPerpLiability) { + calc.addPerpLiability(); } } } @@ -437,17 +425,17 @@ export class User { strict, marginBuffer: marginBuffer, }, - totalCollateral, - totalCollateralBuffer, - marginRequirement, - marginRequirementPlusBuffer, - isolatedMarginCalculations, - numSpotLiabilities, - numPerpLiabilities, - allDepositOraclesValid, - allLiabilityOraclesValid, - withPerpIsolatedLiability, - withSpotIsolatedLiability, + totalCollateral: calc.totalCollateral, + totalCollateralBuffer: calc.totalCollateralBuffer, + marginRequirement: calc.marginRequirement, + marginRequirementPlusBuffer: calc.marginRequirementPlusBuffer, + isolatedMarginCalculations: calc.isolatedMarginCalculations, + numSpotLiabilities: calc.numSpotLiabilities, + numPerpLiabilities: calc.numPerpLiabilities, + allDepositOraclesValid: calc.allDepositOraclesValid, + allLiabilityOraclesValid: calc.allLiabilityOraclesValid, + withPerpIsolatedLiability: calc.withPerpIsolatedLiability, + withSpotIsolatedLiability: calc.withSpotIsolatedLiability, }; } @@ -664,6 +652,7 @@ export class User { lastQuoteAssetAmountPerLp: ZERO, perLpBase: 0, maxMarginRatio: 0, + positionFlag: 0, }; } @@ -2471,12 +2460,12 @@ export class User { marginCalculation.isolatedMarginCalculations.get(marketIndex); const { totalCollateral, marginRequirement } = isolatedMarginCalculation; - let freeCollateral = BN.max( + const freeCollateral = BN.max( ZERO, totalCollateral.sub(marginRequirement) ).add(offsetCollateral); - let freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( + const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( market, currentPerpPosition, positionBaseSizeChange, @@ -4371,4 +4360,7 @@ export class User { activeSpotPositions: activeSpotMarkets, }; } + private isPerpPositionIsolated(perpPosition: PerpPosition): boolean { + return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; + } } diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 88d203f875..90dfac1df8 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -46,6 +46,7 @@ export const mockPerpPosition: PerpPosition = { lastBaseAssetAmountPerLp: new BN(0), lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, + positionFlag: 0, }; export const mockAMM: AMM = { diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index 9ca9b5e4aa..880276ebe6 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -15,6 +15,7 @@ import { MARGIN_PRECISION, OPEN_ORDER_MARGIN_REQUIREMENT, SPOT_MARKET_WEIGHT_PRECISION, + PositionFlag, } from '../../src'; import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; import { assert } from '../../src/assert/assert'; @@ -320,9 +321,12 @@ describe('getMarginCalculation snapshot', () => { assert(makerCalc.marginRequirement.gt(ZERO)); }); - it('isolated position margin requirement (SDK parity)', async () => { + it.only('isolated position margin requirement (SDK parity)', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + myMockSpotMarkets[0].oracle = new PublicKey(2); + myMockSpotMarkets[1].oracle = new PublicKey(5); + myMockPerpMarkets[0].amm.oracle = new PublicKey(5); // Configure perp market 0 ratios to match on-chain test myMockPerpMarkets[0].marginRatioInitial = 1000; // 10% @@ -349,12 +353,16 @@ describe('getMarginCalculation snapshot', () => { // SOL borrow: 100 units crossAccount.spotPositions[1].marketIndex = 1; crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW; - crossAccount.spotPositions[1].scaledBalance = new BN(10000).mul( + crossAccount.spotPositions[1].scaledBalance = new BN(100).mul( SPOT_MARKET_BALANCE_PRECISION ); // No perp exposure in cross calc - crossAccount.perpPositions[0].baseAssetAmount = ZERO; - crossAccount.perpPositions[0].quoteAssetAmount = ZERO; + crossAccount.perpPositions[0].baseAssetAmount = new BN(100 * BASE_PRECISION.toNumber()); + crossAccount.perpPositions[0].quoteAssetAmount = new BN(-11000 * QUOTE_PRECISION.toNumber()); + crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition; + crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); const userCross: User = await makeMockUser( myMockPerpMarkets, @@ -365,6 +373,8 @@ describe('getMarginCalculation snapshot', () => { ); const crossCalc = userCross.getMarginCalculation('Initial'); + // console.log('crossCalc.marginRequirement.toString()', crossCalc.marginRequirement.toString()); + // console.log('crossCalc.totalCollateral.toString()', crossCalc.totalCollateral.toString()); // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); // Expect: cross total collateral from USDC deposit only = $20,000 @@ -377,53 +387,15 @@ describe('getMarginCalculation snapshot', () => { const crossCalcBuf = userCross.getMarginCalculation('Initial', { liquidationBuffer: tenPct, }); - console.log('crossCalcBuf.marginRequirementPlusBuffer', crossCalcBuf.marginRequirementPlusBuffer.toString()); - console.log('crossCalcBuf.totalCollateralBuffer', crossCalcBuf.totalCollateralBuffer.toString()); - assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); + assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); // replicate 10% buffer const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add( crossCalcBuf.totalCollateralBuffer ); assert(crossTotalPlusBuffer.eq(new BN('20000000000'))); - // ---------- Isolated perp position (simulate isolated by separate user) ---------- - const isolatedAccount = _.cloneDeep(baseMockUserAccount); - // Perp: 100 base long, quote -11,000 => PnL = 10k - 11k = -$1,000 - isolatedAccount.perpPositions[0].baseAssetAmount = new BN(100).mul( - BASE_PRECISION - ); - isolatedAccount.perpPositions[0].quoteAssetAmount = new BN(-11000).mul( - QUOTE_PRECISION - ); - // Simulate isolated balance: $100 quote deposit on this user - isolatedAccount.spotPositions[0].marketIndex = 0; - isolatedAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; - isolatedAccount.spotPositions[0].scaledBalance = new BN(100).mul( - SPOT_MARKET_BALANCE_PRECISION - ); - - const userIsolated: User = await makeMockUser( - myMockPerpMarkets, - myMockSpotMarkets, - isolatedAccount, - [100, 1, 1, 1, 1, 1, 1, 1], - [1, 100, 1, 1, 1, 1, 1, 1] - ); - - const isoCalc = userIsolated.getMarginCalculation('Initial'); - // Expect: perp initial MR = 10% * $10,000 = $1,000 - assert(isoCalc.marginRequirement.eq(new BN('1000000000'))); - // Expect: total collateral = $100 (deposit) + (-$1,000) (PnL) = -$900 - assert(isoCalc.totalCollateral.eq(new BN('-900000000'))); - assert(isoCalc.marginRequirement.gt(isoCalc.totalCollateral)); - - const isoCalcBuf = userIsolated.getMarginCalculation('Initial', { - liquidationBuffer: tenPct, - }); - assert(isoCalcBuf.marginRequirementPlusBuffer.eq(new BN('2000000000'))); - const isoTotalPlusBuffer = isoCalcBuf.totalCollateral.add( - isoCalcBuf.totalCollateralBuffer - ); - assert(isoTotalPlusBuffer.eq(new BN('-1000000000'))); + const isoPosition = crossCalcBuf.isolatedMarginCalculations.get(0); + assert(isoPosition?.marginRequirementPlusBuffer.eq(new BN('2000000000'))); + assert(isoPosition?.totalCollateralBuffer.add(isoPosition?.totalCollateral).eq(new BN('-1000000000'))); }); }); diff --git a/sdk/tests/user/test.ts b/sdk/tests/user/test.ts index 6c431b7226..990abc473e 100644 --- a/sdk/tests/user/test.ts +++ b/sdk/tests/user/test.ts @@ -49,7 +49,6 @@ async function makeMockUser( oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = spotOraclePriceList[i]; } - // console.log(oraclePriceMap); function getMockUserAccount(): UserAccount { return myMockUserAccount; @@ -61,12 +60,6 @@ async function makeMockUser( return myMockSpotMarkets[marketIndex]; } function getMockOracle(oracleKey: PublicKey) { - // console.log('oracleKey.toString():', oracleKey.toString()); - // console.log( - // 'oraclePriceMap[oracleKey.toString()]:', - // oraclePriceMap[oracleKey.toString()] - // ); - const QUOTE_ORACLE_PRICE_DATA: OraclePriceData = { price: new BN( oraclePriceMap[oracleKey.toString()] * PRICE_PRECISION.toNumber() From 3606bb431e8ab3e3c2612aa3b2478054cc8850c3 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Thu, 28 Aug 2025 13:57:55 -0600 Subject: [PATCH 79/91] fix: PR feedback and cleanup + decoding position flag wrong --- sdk/src/decode/user.ts | 4 +- sdk/src/marginCalculation.ts | 55 ++------------------------ sdk/src/types.ts | 5 +++ sdk/src/user.ts | 26 +++++------- sdk/tests/user/getMarginCalculation.ts | 25 +++++------- 5 files changed, 30 insertions(+), 85 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index c3022d06aa..4c35b4f6ed 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -84,6 +84,7 @@ export function decodeUser(buffer: Buffer): UserAccount { const quoteAssetAmount = readSignedBigInt64LE(buffer, offset + 16); const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); + const positionFlag = buffer.readUInt8(offset + 95); if ( baseAssetAmount.eq(ZERO) && @@ -117,9 +118,6 @@ export function decodeUser(buffer: Buffer): UserAccount { offset += 3; const perLpBase = buffer.readUInt8(offset); offset += 1; - // TODO: verify this works - const positionFlag = buffer.readUInt8(offset); - offset += 1; perpPositions.push({ lastCumulativeFundingRate, baseAssetAmount, diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts index 2cb1dbe839..d5507d9eef 100644 --- a/sdk/src/marginCalculation.ts +++ b/sdk/src/marginCalculation.ts @@ -1,4 +1,4 @@ -import { BN } from './'; +import { BN } from '@coral-xyz/anchor'; import { MARGIN_PRECISION } from './constants/numericConstants'; import { MarketType } from './types'; @@ -6,7 +6,7 @@ export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill'; export type MarginCalculationMode = | { type: 'Standard' } - | { type: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + | { type: 'Liquidation' }; export class MarketIdentifier { marketType: MarketType; @@ -40,10 +40,6 @@ export class MarginContext { strict: boolean; ignoreInvalidDepositOracles: boolean; marginBuffer: BN; // scaled by MARGIN_PRECISION - fuelBonusNumerator: BN; // seconds since last update - fuelBonus: BN; // not used in calculation aggregation here - fuelPerpDelta?: { marketIndex: number; delta: BN }; - fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 in rust marginRatioOverride?: number; private constructor(marginType: MarginCategory) { @@ -52,9 +48,6 @@ export class MarginContext { this.strict = false; this.ignoreInvalidDepositOracles = false; this.marginBuffer = new BN(0); - this.fuelBonusNumerator = new BN(0); - this.fuelBonus = new BN(0); - this.fuelSpotDeltas = []; } static standard(marginType: MarginCategory): MarginContext { @@ -83,26 +76,6 @@ export class MarginContext { return this; } - setFuelPerpDelta(marketIndex: number, delta: BN): this { - this.fuelPerpDelta = { marketIndex, delta }; - return this; - } - - setFuelSpotDelta(marketIndex: number, delta: BN): this { - this.fuelSpotDeltas = [{ marketIndex, delta }]; - return this; - } - - setFuelSpotDeltas(deltas: Array<{ marketIndex: number; delta: BN }>): this { - this.fuelSpotDeltas = deltas; - return this; - } - - setFuelNumerator(numerator: BN): this { - this.fuelBonusNumerator = numerator ?? new BN(0); - return this; - } - setMarginRatioOverride(ratio: number): this { this.marginRatioOverride = ratio; return this; @@ -114,7 +87,6 @@ export class MarginContext { 'InvalidMarginCalculation: Cant track market outside of liquidation mode' ); } - this.mode.marketToTrackMarginRequirement = marketIdentifier; return this; } } @@ -191,7 +163,7 @@ export class MarginCalculation { this.fuelPositions = 0; } - addIsolatedTotalCollateral(delta: BN): void { + addCrossMarginTotalCollateral(delta: BN): void { this.totalCollateral = this.totalCollateral.add(delta); if (this.context.marginBuffer.gt(new BN(0)) && delta.isNeg()) { this.totalCollateralBuffer = this.totalCollateralBuffer.add( @@ -200,10 +172,9 @@ export class MarginCalculation { } } - addIsolatedMarginRequirement( + addCrossMarginRequirement( marginRequirement: BN, liabilityValue: BN, - marketIdentifier: MarketIdentifier ): void { this.marginRequirement = this.marginRequirement.add(marginRequirement); if (this.context.marginBuffer.gt(new BN(0))) { @@ -213,11 +184,6 @@ export class MarginCalculation { ) ); } - const tracked = this.marketToTrackMarginRequirement(); - if (tracked && tracked.equals(marketIdentifier)) { - this.trackedMarketMarginRequirement = - this.trackedMarketMarginRequirement.add(marginRequirement); - } } addIsolatedMarginCalculation( @@ -245,12 +211,6 @@ export class MarginCalculation { iso.totalCollateralBuffer = totalCollateralBuffer; iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer; this.isolatedMarginCalculations.set(marketIndex, iso); - - const tracked = this.marketToTrackMarginRequirement(); - if (tracked && tracked.equals(MarketIdentifier.perp(marketIndex))) { - this.trackedMarketMarginRequirement = - this.trackedMarketMarginRequirement.add(marginRequirement); - } } addSpotLiability(): void { @@ -341,11 +301,4 @@ export class MarginCalculation { hasIsolatedMarginCalculation(marketIndex: number): boolean { return this.isolatedMarginCalculations.has(marketIndex); } - - private marketToTrackMarginRequirement(): MarketIdentifier | undefined { - if (this.context.mode.type === 'Liquidation') { - return this.context.mode.marketToTrackMarginRequirement; - } - return undefined; - } } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index c161e724a5..cce95639db 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -504,6 +504,7 @@ export type LiquidationRecord = { liquidatePerpPnlForDeposit: LiquidatePerpPnlForDepositRecord; perpBankruptcy: PerpBankruptcyRecord; spotBankruptcy: SpotBankruptcyRecord; + bitFlags: number; }; export class LiquidationType { @@ -582,6 +583,10 @@ export type SpotBankruptcyRecord = { ifPayment: BN; }; +export class LiquidationBitFlag { + static readonly IsolatedPosition = 1; +} + export type SettlePnlRecord = { ts: BN; user: PublicKey; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 8aa3098539..7451d09122 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -110,7 +110,6 @@ import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber' import { MarginCalculation as JsMarginCalculation, MarginContext, - MarketIdentifier, IsolatedMarginCalculation, } from './marginCalculation'; @@ -215,7 +214,7 @@ export class User { spotMarket.decimals, strictOracle ); - calc.addIsolatedTotalCollateral(tokenValue); + calc.addCrossMarginTotalCollateral(tokenValue); } else { // borrow on quote contributes to margin requirement const tokenValueAbs = getStrictTokenValue( @@ -223,10 +222,9 @@ export class User { spotMarket.decimals, strictOracle ).abs(); - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( tokenValueAbs, tokenValueAbs, - MarketIdentifier.spot(0) ); calc.addSpotLiability(); } @@ -249,23 +247,21 @@ export class User { // open order IM if (includeOpenOrders) { - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), ZERO, - MarketIdentifier.spot(spotPosition.marketIndex) ); } if (worstCaseTokenAmount.gt(ZERO)) { // asset side increases total collateral (weighted) - calc.addIsolatedTotalCollateral(worstCaseWeightedTokenValue); + calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue); } else if (worstCaseTokenAmount.lt(ZERO)) { // liability side increases margin requirement (weighted >= abs(token_value)) const liabilityWeighted = worstCaseWeightedTokenValue.abs(); - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( liabilityWeighted, worstCaseTokenValue.abs(), - MarketIdentifier.spot(spotPosition.marketIndex) ); calc.addSpotLiability(); } else if (spotPosition.openOrders !== 0) { @@ -274,13 +270,12 @@ export class User { // orders value contributes to collateral or requirement if (worstCaseOrdersValue.gt(ZERO)) { - calc.addIsolatedTotalCollateral(worstCaseOrdersValue); + calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); } else if (worstCaseOrdersValue.lt(ZERO)) { const absVal = worstCaseOrdersValue.abs(); - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( absVal, absVal, - MarketIdentifier.spot(0) ); } } @@ -403,12 +398,11 @@ export class User { calc.addPerpLiability(); } else { // cross: add to global requirement and collateral - calc.addIsolatedMarginRequirement( + calc.addCrossMarginRequirement( perpMarginRequirement, worstCaseLiabilityValue, - MarketIdentifier.perp(market.marketIndex) ); - calc.addIsolatedTotalCollateral(positionUnrealizedPnl); + calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); const hasPerpLiability = !marketPosition.baseAssetAmount.eq(ZERO) || marketPosition.quoteAssetAmount.lt(ZERO) || @@ -1951,7 +1945,7 @@ export class User { return { perpLiabilityValue: perpLiability, perpPnl: positionUnrealizedPnl, - spotAssetValue: ZERO, + spotAssetValue: perpPosition.isolatedPositionScaledBalance, spotLiabilityValue: ZERO, }; } diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index 880276ebe6..5e66fa0887 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -148,7 +148,7 @@ describe('getMarginCalculation snapshot', () => { [1, 1, 1, 1, 1, 1, 1, 1] ); - const tenPercent = MARGIN_PRECISION.divn(10); + const tenPercent = new BN(1000); const calc = user.getMarginCalculation('Initial', { liquidationBuffer: tenPercent, }); @@ -157,7 +157,7 @@ describe('getMarginCalculation snapshot', () => { assert(calc.marginRequirement.eq(liability)); assert( calc.marginRequirementPlusBuffer.eq( - liability.mul(tenPercent).div(MARGIN_PRECISION) + liability.div(new BN(10)).add(calc.marginRequirement) // 10% of liability + margin requirement ) ); assert(calc.numSpotLiabilities === 1); @@ -211,27 +211,21 @@ describe('getMarginCalculation snapshot', () => { assert(calc.marginRequirement.eq(new BN('2000000'))); }); - it.skip('maker position reducing: collateral equals maintenance requirement', async () => { + it.only('maker position reducing: collateral equals maintenance requirement', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); const myMockUserAccount = _.cloneDeep(baseMockUserAccount); // Perp exposure: 20 base notional at oracle price 1 → maintenance MR = 10% of $20 = $2 - myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(2).mul( BASE_PRECISION ); - // Set entry/breakeven at $1 so unrealized PnL = $0 - myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20).mul( + myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20000000000).mul( QUOTE_PRECISION ); - myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20).mul( + myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20000000000).mul( QUOTE_PRECISION ); - // Provide exactly $2 in quote collateral - myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; - myMockUserAccount.spotPositions[0].scaledBalance = new BN(2).mul( - SPOT_MARKET_BALANCE_PRECISION - ); const user: User = await makeMockUser( myMockPerpMarkets, @@ -321,7 +315,7 @@ describe('getMarginCalculation snapshot', () => { assert(makerCalc.marginRequirement.gt(ZERO)); }); - it.only('isolated position margin requirement (SDK parity)', async () => { + it('isolated position margin requirement (SDK parity)', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); myMockSpotMarkets[0].oracle = new PublicKey(2); @@ -373,8 +367,7 @@ describe('getMarginCalculation snapshot', () => { ); const crossCalc = userCross.getMarginCalculation('Initial'); - // console.log('crossCalc.marginRequirement.toString()', crossCalc.marginRequirement.toString()); - // console.log('crossCalc.totalCollateral.toString()', crossCalc.totalCollateral.toString()); + const isolatedMarginCalc = crossCalc.isolatedMarginCalculations.get(0); // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); // Expect: cross total collateral from USDC deposit only = $20,000 @@ -382,6 +375,8 @@ describe('getMarginCalculation snapshot', () => { // Meets cross margin requirement assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral)); + assert(isolatedMarginCalc?.marginRequirement.eq(new BN('1000000000'))); + assert(isolatedMarginCalc?.totalCollateral.eq(new BN('-900000000'))); // With 10% buffer const tenPct = new BN(1000); const crossCalcBuf = userCross.getMarginCalculation('Initial', { From 99c826895cd8c80166708612b016a4b6fbb8e9d6 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Fri, 29 Aug 2025 23:25:47 -0600 Subject: [PATCH 80/91] feat: deposit into iso position ixs --- sdk/src/driftClient.ts | 177 +++++++++++++++++++++---- sdk/src/math/position.ts | 1 - sdk/src/types.ts | 4 + sdk/tests/user/getMarginCalculation.ts | 8 +- 4 files changed, 156 insertions(+), 34 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index abff8433df..b7ed95003c 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -57,7 +57,6 @@ import { StateAccount, SwapReduceOnly, SignedMsgOrderParamsMessage, - TakerInfo, TxParams, UserAccount, UserStatsAccount, @@ -191,6 +190,8 @@ import nacl from 'tweetnacl'; import { Slothash } from './slot/SlothashSubscriber'; import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; +import { TakerInfo } from './types'; +// BN is already imported globally in this file via other imports import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; import { Commitment } from 'gill'; @@ -263,6 +264,69 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + public async getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex, + amount, + spotMarketIndex, + subAccountId, + userTokenAccount, + }: { + perpMarketIndex: number; + amount: BN; + spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex + subAccountId?: number; + userTokenAccount?: PublicKey; // defaults ATA for spot market mint + }): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + const userStats = this.getUserStatsAccountPublicKey(); + const statePk = await this.getStatePublicKey(); + const perp = this.getPerpMarketAccount(perpMarketIndex); + const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; + const spot = this.getSpotMarketAccount(spotIndex); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + readablePerpMarketIndex: perpMarketIndex, + writableSpotMarketIndexes: [spotIndex], + }); + + // token program and transfer hook mints need to be present for deposit + this.addTokenMintToRemainingAccounts(spot, remainingAccounts); + if (this.isTransferHook(spot)) { + await this.addExtraAccountMetasToRemainingAccounts( + spot.mint, + remainingAccounts + ); + } + + const tokenProgram = this.getTokenProgramForSpotMarket(spot); + const ata = + userTokenAccount ?? + (await this.getAssociatedTokenAccount( + spotIndex, + false, + tokenProgram + )); + + return await this.program.instruction.depositIntoIsolatedPerpPosition( + spotIndex, + perpMarketIndex, + amount, + { + accounts: { + state: statePk, + user, + userStats, + authority: this.wallet.publicKey, + spotMarketVault: spot.vault, + userTokenAccount: ata, + tokenProgram, + }, + remainingAccounts, + } + ); + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -733,7 +797,6 @@ export class DriftClient { return lookupTableAccount; } - public async fetchAllLookupTableAccounts(): Promise< AddressLookupTableAccount[] > { @@ -1511,7 +1574,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdateUserCustomMarginRatioIx( marginRatio: number, subAccountId = 0 @@ -2290,7 +2352,6 @@ export class DriftClient { this.mustIncludeSpotMarketIndexes.add(spotMarketIndex); }); } - getRemainingAccounts(params: RemainingAccountParams): AccountMeta[] { const { oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap } = this.getRemainingAccountMapsForUsers(params.userAccounts); @@ -3124,7 +3185,6 @@ export class DriftClient { userAccountPublicKey, }; } - public async createInitializeUserAccountAndDepositCollateral( amount: BN, userTokenAccount: PublicKey, @@ -4109,7 +4169,6 @@ export class DriftClient { } ); } - public async getRemovePerpLpSharesIx( marketIndex: number, sharesToBurn?: BN, @@ -4265,7 +4324,8 @@ export class DriftClient { bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, - settlePnl?: boolean + settlePnl?: boolean, + isolatedPositionDepositAmount?: BN ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; settlePnlTx?: Transaction | VersionedTransaction; @@ -4290,10 +4350,32 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - ixPromisesForTxs.marketOrderTx = this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); + + const preIxs: TransactionInstruction[] = []; + if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: orderParams.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId: userAccount.subAccountId, + }) + ); + } + + + ixPromisesForTxs.marketOrderTx = (async () => { + const placeOrdersIx = await this.getPlaceOrdersIx( + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); + if (preIxs.length) { + return [ + ...preIxs, + placeOrdersIx, + ] as unknown as TransactionInstruction; + } + return placeOrdersIx; + })(); /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -4408,12 +4490,29 @@ export class DriftClient { public async placePerpOrder( orderParams: OptionalOrderParams, txParams?: TxParams, - subAccountId?: number + subAccountId?: number, + isolatedPositionDepositAmount?: BN ): Promise { + const preIxs: TransactionInstruction[] = []; + if (isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: orderParams.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId, + }) + ); + } + const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlacePerpOrderIx(orderParams, subAccountId), - txParams + txParams, + undefined, + undefined, + undefined, + undefined, + preIxs ), [], this.opts @@ -4785,6 +4884,7 @@ export class DriftClient { useMarketLastSlotCache: true, }); + return await this.program.instruction.cancelOrders( marketType ?? null, marketIndex ?? null, @@ -4828,7 +4928,8 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise { const { txSig } = await this.sendTransaction( ( @@ -4850,10 +4951,25 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ) { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); + const preIxs: TransactionInstruction[] = []; + if (params?.length === 1) { + const p = params[0]; + if (isVariant(p.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + preIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: p.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId, + }) + ); + } + } + const tx = await this.buildTransaction( await this.getPlaceOrdersIx(params, subAccountId), txParams, @@ -4861,14 +4977,13 @@ export class DriftClient { lookupTableAccounts, undefined, undefined, - optionalIxs + [...preIxs, ...(optionalIxs ?? [])] ); return { placeOrdersTx: tx, }; } - public async getPlaceOrdersIx( params: OptionalOrderParams[], subAccountId?: number @@ -5606,7 +5721,6 @@ export class DriftClient { return txSig; } - public async getJupiterSwapIxV6({ jupiterClient, outMarketIndex, @@ -6276,7 +6390,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } - public async preparePlaceAndTakePerpOrderWithAdditionalOrders( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], @@ -6288,7 +6401,8 @@ export class DriftClient { settlePnl?: boolean, exitEarlyIfSimFails?: boolean, auctionDurationPercentage?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise<{ placeAndTakeTx: Transaction | VersionedTransaction; cancelExistingOrdersTx: Transaction | VersionedTransaction; @@ -6322,6 +6436,16 @@ export class DriftClient { subAccountId ); + if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + placeAndTakeIxs.push( + await this.getDepositIntoIsolatedPerpPositionIx({ + perpMarketIndex: orderParams.marketIndex, + amount: isolatedPositionDepositAmount as BN, + subAccountId, + }) + ); + } + placeAndTakeIxs.push(placeAndTakeIx); if (bracketOrdersParams.length > 0) { @@ -6332,6 +6456,11 @@ export class DriftClient { placeAndTakeIxs.push(bracketOrdersIx); } + // Optional extra ixs can be appended at the front + if (optionalIxs?.length) { + placeAndTakeIxs.unshift(...optionalIxs); + } + const shouldUseSimulationComputeUnits = txParams?.useSimulatedComputeUnits; const shouldExitIfSimulationFails = exitEarlyIfSimFails; @@ -7064,7 +7193,6 @@ export class DriftClient { this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } - public async getPlaceAndTakeSpotOrderIx( orderParams: OptionalOrderParams, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, @@ -7517,7 +7645,6 @@ export class DriftClient { bitFlags?: number; policy?: ModifyOrderPolicy; maxTs?: BN; - txParams?: TxParams; }, subAccountId?: number ): Promise { @@ -7859,7 +7986,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(marketIndex, slot); return txSig; } - public async getLiquidatePerpIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -8650,7 +8776,6 @@ export class DriftClient { } ); } - public async resolveSpotBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -9482,7 +9607,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getSettleRevenueToInsuranceFundIx( spotMarketIndex: number ): Promise { @@ -10277,7 +10401,6 @@ export class DriftClient { ); return config as ProtectedMakerModeConfig; } - public async updateUserProtectedMakerOrders( subAccountId: number, protectedOrders: boolean, @@ -10601,4 +10724,4 @@ export class DriftClient { forceVersionedTransaction, }); } -} +} \ No newline at end of file diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 3db5007a20..0cae66ab05 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -127,7 +127,6 @@ export function calculatePositionPNL( if (withFunding) { const fundingRatePnL = calculateUnsettledFundingPnl(market, perpPosition); - pnl = pnl.add(fundingRatePnL); } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index cce95639db..9d935099a9 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1286,6 +1286,10 @@ export type OptionalOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property]; } & NecessaryOrderParams; +export type PerpOrderIsolatedExtras = { + isolatedPositionDepositAmount?: BN; +}; + export type ModifyOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property] | null; } & { policy?: ModifyOrderPolicy }; diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts index 5e66fa0887..afc2111996 100644 --- a/sdk/tests/user/getMarginCalculation.ts +++ b/sdk/tests/user/getMarginCalculation.ts @@ -216,14 +216,10 @@ describe('getMarginCalculation snapshot', () => { const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); const myMockUserAccount = _.cloneDeep(baseMockUserAccount); - // Perp exposure: 20 base notional at oracle price 1 → maintenance MR = 10% of $20 = $2 - myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(2).mul( + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(200000000).mul( BASE_PRECISION ); - myMockUserAccount.perpPositions[0].quoteEntryAmount = new BN(-20000000000).mul( - QUOTE_PRECISION - ); - myMockUserAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-20000000000).mul( + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-180000000).mul( QUOTE_PRECISION ); From 88d7b76917bfc0a0164d7aff91b378abf7a31903 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 11:27:54 -0600 Subject: [PATCH 81/91] temp: pr feedback nother round --- sdk/src/decode/user.ts | 6 +- sdk/src/marginCalculation.ts | 19 ++++- sdk/src/math/margin.ts | 14 +++- sdk/src/math/spotPosition.ts | 5 +- sdk/src/types.ts | 2 +- sdk/src/user.ts | 151 +++++++++++++++-------------------- 6 files changed, 103 insertions(+), 94 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index 4c35b4f6ed..b3b2e2fca3 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -85,6 +85,8 @@ export function decodeUser(buffer: Buffer): UserAccount { const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); const positionFlag = buffer.readUInt8(offset + 95); + const isolatedPositionScaledBalance = readUnsignedBigInt64LE(buffer, offset + 96); + const customMarginRatio = buffer.readUInt32LE(offset + 97); if ( baseAssetAmount.eq(ZERO) && @@ -136,7 +138,9 @@ export function decodeUser(buffer: Buffer): UserAccount { perLpBase, maxMarginRatio, positionFlag, - }); + isolatedPositionScaledBalance, + customMarginRatio, + }); } const orders: Order[] = []; diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts index d5507d9eef..fe9fa8d7eb 100644 --- a/sdk/src/marginCalculation.ts +++ b/sdk/src/marginCalculation.ts @@ -139,6 +139,8 @@ export class MarginCalculation { allLiabilityOraclesValid: boolean; withPerpIsolatedLiability: boolean; withSpotIsolatedLiability: boolean; + totalSpotLiabilityValue: BN; + totalPerpLiabilityValue: BN; trackedMarketMarginRequirement: BN; fuelDeposits: number; fuelBorrows: number; @@ -157,6 +159,8 @@ export class MarginCalculation { this.allLiabilityOraclesValid = true; this.withPerpIsolatedLiability = false; this.withSpotIsolatedLiability = false; + this.totalSpotLiabilityValue = new BN(0); + this.totalPerpLiabilityValue = new BN(0); this.trackedMarketMarginRequirement = new BN(0); this.fuelDeposits = 0; this.fuelBorrows = 0; @@ -172,10 +176,7 @@ export class MarginCalculation { } } - addCrossMarginRequirement( - marginRequirement: BN, - liabilityValue: BN, - ): void { + addCrossMarginRequirement(marginRequirement: BN, liabilityValue: BN): void { this.marginRequirement = this.marginRequirement.add(marginRequirement); if (this.context.marginBuffer.gt(new BN(0))) { this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add( @@ -221,6 +222,16 @@ export class MarginCalculation { this.numPerpLiabilities += 1; } + addSpotLiabilityValue(spotLiabilityValue: BN): void { + this.totalSpotLiabilityValue = + this.totalSpotLiabilityValue.add(spotLiabilityValue); + } + + addPerpLiabilityValue(perpLiabilityValue: BN): void { + this.totalPerpLiabilityValue = + this.totalPerpLiabilityValue.add(perpLiabilityValue); + } + updateAllDepositOraclesValid(valid: boolean): void { this.allDepositOraclesValid = this.allDepositOraclesValid && valid; } diff --git a/sdk/src/math/margin.ts b/sdk/src/math/margin.ts index 63fada436b..d67a5e2b67 100644 --- a/sdk/src/math/margin.ts +++ b/sdk/src/math/margin.ts @@ -160,8 +160,20 @@ export function calculateWorstCaseBaseAssetAmount( export function calculateWorstCasePerpLiabilityValue( perpPosition: PerpPosition, perpMarket: PerpMarketAccount, - oraclePrice: BN + oraclePrice: BN, + includeOpenOrders: boolean = true ): { worstCaseBaseAssetAmount: BN; worstCaseLiabilityValue: BN } { + // return early if no open orders required + if (!includeOpenOrders) { + return { + worstCaseBaseAssetAmount: perpPosition.baseAssetAmount, + worstCaseLiabilityValue: calculatePerpLiabilityValue( + perpPosition.baseAssetAmount, + oraclePrice, + isVariant(perpMarket.contractType, 'prediction') + ), + }; + } const allBids = perpPosition.baseAssetAmount.add(perpPosition.openBids); const allAsks = perpPosition.baseAssetAmount.add(perpPosition.openAsks); diff --git a/sdk/src/math/spotPosition.ts b/sdk/src/math/spotPosition.ts index 61eae83ec1..ac5c6d7611 100644 --- a/sdk/src/math/spotPosition.ts +++ b/sdk/src/math/spotPosition.ts @@ -33,7 +33,8 @@ export function getWorstCaseTokenAmounts( spotMarketAccount: SpotMarketAccount, strictOraclePrice: StrictOraclePrice, marginCategory: MarginCategory, - customMarginRatio?: number + customMarginRatio?: number, + includeOpenOrders: boolean = true ): OrderFillSimulation { const tokenAmount = getSignedTokenAmount( getTokenAmount( @@ -50,7 +51,7 @@ export function getWorstCaseTokenAmounts( strictOraclePrice ); - if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) { + if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO) || !includeOpenOrders) { const { weight, weightedTokenValue } = calculateWeightedTokenValue( tokenAmount, tokenValue, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 9d935099a9..df9596f9fd 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1115,8 +1115,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; - isolatedPositionScaledBalance: BN; positionFlag: number; + isolatedPositionScaledBalance: BN; }; export type UserStatsAccount = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7451d09122..d0223a3c91 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -110,23 +110,10 @@ import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber' import { MarginCalculation as JsMarginCalculation, MarginContext, - IsolatedMarginCalculation, } from './marginCalculation'; -export type UserMarginCalculation = { - context: { marginType: MarginCategory; strict: boolean; marginBuffer?: BN }; - totalCollateral: BN; - totalCollateralBuffer: BN; - marginRequirement: BN; - marginRequirementPlusBuffer: BN; - isolatedMarginCalculations: Map; - numSpotLiabilities: number; - numPerpLiabilities: number; - allDepositOraclesValid: boolean; - allLiabilityOraclesValid: boolean; - withPerpIsolatedLiability: boolean; - withSpotIsolatedLiability: boolean; -}; +// Backwards compatibility: alias SDK MarginCalculation shape +export type UserMarginCalculation = JsMarginCalculation; export type MarginType = 'Cross' | 'Isolated'; @@ -145,6 +132,8 @@ export class User { * Compute a consolidated margin snapshot once, without caching. * Consumers can use this to avoid duplicating work across separate calls. */ + // TODO: need another param to tell it give it back leverage compnents + // TODO: change get leverage functions need to pull the right values from public getMarginCalculation( marginCategory: MarginCategory = 'Initial', opts?: { @@ -154,7 +143,7 @@ export class User { liquidationBuffer?: BN; // margin_buffer analog for buffer mode marginRatioOverride?: number; // mirrors context.margin_ratio_override } - ): UserMarginCalculation { + ): JsMarginCalculation { const strict = opts?.strict ?? false; const enteringHighLeverage = opts?.enteringHighLeverage ?? false; const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? @@ -179,6 +168,7 @@ export class User { const calc = new JsMarginCalculation(ctx); // SPOT POSITIONS + // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions for (const spotPosition of this.getUserAccount().spotPositions) { if (isSpotPositionAvailable(spotPosition)) continue; @@ -222,10 +212,7 @@ export class User { spotMarket.decimals, strictOracle ).abs(); - calc.addCrossMarginRequirement( - tokenValueAbs, - tokenValueAbs, - ); + calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs); calc.addSpotLiability(); } continue; @@ -242,16 +229,15 @@ export class User { spotMarket, strictOracle, marginCategory, - userCustomMarginRatio + userCustomMarginRatio, + includeOpenOrders ); // open order IM - if (includeOpenOrders) { - calc.addCrossMarginRequirement( - new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), - ZERO, - ); - } + calc.addCrossMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO + ); if (worstCaseTokenAmount.gt(ZERO)) { // asset side increases total collateral (weighted) @@ -261,11 +247,13 @@ export class User { const liabilityWeighted = worstCaseWeightedTokenValue.abs(); calc.addCrossMarginRequirement( liabilityWeighted, - worstCaseTokenValue.abs(), + worstCaseTokenValue.abs() ); calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); } else if (spotPosition.openOrders !== 0) { calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); } // orders value contributes to collateral or requirement @@ -273,10 +261,7 @@ export class User { calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); } else if (worstCaseOrdersValue.lt(ZERO)) { const absVal = worstCaseOrdersValue.abs(); - calc.addCrossMarginRequirement( - absVal, - absVal, - ); + calc.addCrossMarginRequirement(absVal, absVal); } } @@ -300,7 +285,8 @@ export class User { calculateWorstCasePerpLiabilityValue( marketPosition, market, - oraclePriceData.price + oraclePriceData.price, + includeOpenOrders ); // margin ratio for this perp @@ -364,7 +350,7 @@ export class User { if (isIsolated) { // derive isolated quote deposit value, mirroring on-chain logic let depositValue = ZERO; - if (marketPosition.isolatedPositionScaledBalance) { + if (marketPosition.isolatedPositionScaledBalance?.gt(ZERO)) { const quoteSpotMarket = this.driftClient.getSpotMarketAccount( market.quoteSpotMarketIndex ); @@ -396,11 +382,12 @@ export class User { perpMarginRequirement ); calc.addPerpLiability(); + calc.addPerpLiabilityValue(worstCaseLiabilityValue); } else { // cross: add to global requirement and collateral calc.addCrossMarginRequirement( perpMarginRequirement, - worstCaseLiabilityValue, + worstCaseLiabilityValue ); calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); const hasPerpLiability = @@ -413,24 +400,7 @@ export class User { } } - return { - context: { - marginType: marginCategory, - strict, - marginBuffer: marginBuffer, - }, - totalCollateral: calc.totalCollateral, - totalCollateralBuffer: calc.totalCollateralBuffer, - marginRequirement: calc.marginRequirement, - marginRequirementPlusBuffer: calc.marginRequirementPlusBuffer, - isolatedMarginCalculations: calc.isolatedMarginCalculations, - numSpotLiabilities: calc.numSpotLiabilities, - numPerpLiabilities: calc.numPerpLiabilities, - allDepositOraclesValid: calc.allDepositOraclesValid, - allLiabilityOraclesValid: calc.allLiabilityOraclesValid, - withPerpIsolatedLiability: calc.withPerpIsolatedLiability, - withSpotIsolatedLiability: calc.withSpotIsolatedLiability, - }; + return calc; } public set isSubscribed(val: boolean) { @@ -647,6 +617,8 @@ export class User { perLpBase: 0, maxMarginRatio: 0, positionFlag: 0, + isolatedPositionScaledBalance: ZERO, + customMarginRatio: 0, }; } @@ -840,18 +812,15 @@ export class User { enterHighLeverageMode = false, perpMarketIndex?: number ): BN { - const totalCollateral = this.getTotalCollateral(marginCategory, true); - const marginRequirement = this.getMarginRequirement( - marginCategory, - undefined, - true, - true, // includeOpenOrders default - enterHighLeverageMode, - perpMarketIndex ? 'Isolated' : undefined, - perpMarketIndex - ); - const freeCollateral = totalCollateral.sub(marginRequirement); - return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; + const marginCalc = this.getMarginCalculation(marginCategory, { + enteringHighLeverage: enterHighLeverageMode, + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.getIsolatedFreeCollateral(perpMarketIndex); + } else { + return marginCalc.getCrossFreeCollateral(); + } } /** @@ -873,7 +842,6 @@ export class User { * @param strict - Optional flag to enforce strict margin calculations. * @param includeOpenOrders - Optional flag to include open orders in the margin calculation. * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode. - * @param marginType - Optional type of margin ('Cross' or 'Isolated'). If 'Isolated', perpMarketIndex must be provided. * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'. * * @returns The calculated margin requirement as a BN (BigNumber). @@ -884,7 +852,6 @@ export class User { strict?: boolean, includeOpenOrders?: boolean, enteringHighLeverage?: boolean, - marginType?: MarginType, perpMarketIndex?: number ): BN; @@ -894,7 +861,6 @@ export class User { strict?: boolean, includeOpenOrders?: boolean, enteringHighLeverage?: boolean, - marginType?: MarginType, perpMarketIndex?: number ): BN { const marginCalc = this.getMarginCalculation(marginCategory, { @@ -904,13 +870,8 @@ export class User { liquidationBuffer, }); - // If marginType is provided and is Isolated, compute only for that market index - if (marginType === 'Isolated') { - if (perpMarketIndex === undefined) { - throw new Error( - 'perpMarketIndex is required when marginType = Isolated' - ); - } + // If perpMarketIndex is provided, compute only for that market index + if (perpMarketIndex !== undefined) { const isolatedMarginCalculation = marginCalc.isolatedMarginCalculations.get(perpMarketIndex); const { marginRequirement } = isolatedMarginCalculation; @@ -928,16 +889,14 @@ export class User { */ public getInitialMarginRequirement( enterHighLeverageMode = false, - marginType?: MarginType, perpMarketIndex?: number ): BN { return this.getMarginRequirement( 'Initial', undefined, - true, + false, undefined, enterHighLeverageMode, - marginType, perpMarketIndex ); } @@ -947,7 +906,6 @@ export class User { */ public getMaintenanceMarginRequirement( liquidationBuffer?: BN, - marginType?: MarginType, perpMarketIndex?: number ): BN { return this.getMarginRequirement( @@ -956,7 +914,6 @@ export class User { true, // strict default true, // includeOpenOrders default false, // enteringHighLeverage default - marginType, perpMarketIndex ); } @@ -1542,13 +1499,21 @@ export class User { marginCategory: MarginCategory = 'Initial', strict = false, includeOpenOrders = true, - liquidationBuffer?: BN + liquidationBuffer?: BN, + perpMarketIndex?: number ): BN { - return this.getMarginCalculation(marginCategory, { + const marginCalc = this.getMarginCalculation(marginCategory, { strict, includeOpenOrders, liquidationBuffer, - }).totalCollateral; + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.isolatedMarginCalculations.get(perpMarketIndex) + .totalCollateral; + } + + return marginCalc.totalCollateral; } public getLiquidationBuffer(): BN | undefined { @@ -1935,6 +1900,16 @@ export class User { const oraclePriceData = this.getOracleDataForPerpMarket( perpPosition.marketIndex ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + perpMarket.quoteSpotMarketIndex + ); + const strictOracle = new StrictOraclePrice( + quoteOraclePriceData.price, + quoteOraclePriceData.twap + ); const positionUnrealizedPnl = calculatePositionPNL( perpMarket, @@ -1942,10 +1917,17 @@ export class User { true, oraclePriceData ); + + const spotAssetValue = getStrictTokenValue( + perpPosition.isolatedPositionScaledBalance, + quoteSpotMarket.decimals, + strictOracle + ); + return { perpLiabilityValue: perpLiability, perpPnl: positionUnrealizedPnl, - spotAssetValue: perpPosition.isolatedPositionScaledBalance, + spotAssetValue, spotLiabilityValue: ZERO, }; } @@ -2255,7 +2237,6 @@ export class User { const marginRequirement = this.getMaintenanceMarginRequirement( liquidationBuffer, - perpMarketIndex ? 'Isolated' : 'Cross', perpMarketIndex ); const canBeLiquidated = totalCollateral.lt(marginRequirement); From d956c23e43155dd23a70bc5f5a3eb07310e5eff2 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 14:06:11 -0600 Subject: [PATCH 82/91] feat: per perp pos max margin ratio --- sdk/src/user.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index d0223a3c91..636eac164b 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -290,12 +290,13 @@ export class User { ); // margin ratio for this perp + const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.customMarginRatio); let marginRatio = new BN( calculateMarketMarginRatio( market, worstCaseBaseAssetAmount.abs(), marginCategory, - this.getUserAccount().maxMarginRatio, + customMarginRatio, this.isHighLeverageMode() || enteringHighLeverage ) ); @@ -350,7 +351,7 @@ export class User { if (isIsolated) { // derive isolated quote deposit value, mirroring on-chain logic let depositValue = ZERO; - if (marketPosition.isolatedPositionScaledBalance?.gt(ZERO)) { + if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) { const quoteSpotMarket = this.driftClient.getSpotMarketAccount( market.quoteSpotMarketIndex ); From 826c962dc14249d5b9ad8cc9aed132f2ea33aabb Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 14:56:55 -0600 Subject: [PATCH 83/91] feat: additional ixs for transfer into iso + update perp margin ratio --- sdk/src/driftClient.ts | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index b7ed95003c..c74f3cd042 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -327,6 +327,40 @@ export class DriftClient { ); } + public async getTransferIsolatedPerpPositionDepositIx({ + perpMarketIndex, + amount, + spotMarketIndex, + subAccountId, + }: { + perpMarketIndex: number; + amount: BN; + spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex + subAccountId?: number; + }): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + const userStats = this.getUserStatsAccountPublicKey(); + const statePk = await this.getStatePublicKey(); + const perp = this.getPerpMarketAccount(perpMarketIndex); + const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; + const spot = this.getSpotMarketAccount(spotIndex); + + return await this.program.instruction.transferIsolatedPerpPositionDeposit( + spotIndex, + perpMarketIndex, + amount, + { + accounts: { + user, + userStats, + authority: this.wallet.publicKey, + state: statePk, + spotMarketVault: spot.vault, + }, + } + ); + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4354,7 +4388,7 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: orderParams.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId: userAccount.subAccountId, @@ -4496,7 +4530,7 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: orderParams.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId, @@ -4961,7 +4995,7 @@ export class DriftClient { const p = params[0]; if (isVariant(p.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: p.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId, @@ -6438,7 +6472,7 @@ export class DriftClient { if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { placeAndTakeIxs.push( - await this.getDepositIntoIsolatedPerpPositionIx({ + await this.getTransferIsolatedPerpPositionDepositIx({ perpMarketIndex: orderParams.marketIndex, amount: isolatedPositionDepositAmount as BN, subAccountId, From 46cc352fad5ac3213a179ece9df84131c7d388c7 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 20:20:53 -0600 Subject: [PATCH 84/91] feat: revamp liquidation checker functions for cross vs iso margin --- sdk/src/user.ts | 102 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 636eac164b..89a7dceca6 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -108,13 +108,10 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; import { - MarginCalculation as JsMarginCalculation, + MarginCalculation, MarginContext, } from './marginCalculation'; -// Backwards compatibility: alias SDK MarginCalculation shape -export type UserMarginCalculation = JsMarginCalculation; - export type MarginType = 'Cross' | 'Isolated'; export class User { @@ -143,7 +140,7 @@ export class User { liquidationBuffer?: BN; // margin_buffer analog for buffer mode marginRatioOverride?: number; // mirrors context.margin_ratio_override } - ): JsMarginCalculation { + ): MarginCalculation { const strict = opts?.strict ?? false; const enteringHighLeverage = opts?.enteringHighLeverage ?? false; const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? @@ -165,7 +162,7 @@ export class User { .strictMode(strict) .setMarginBuffer(marginBuffer) .setMarginRatioOverride(userCustomMarginRatio); - const calc = new JsMarginCalculation(ctx); + const calc = new MarginCalculation(ctx); // SPOT POSITIONS // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions @@ -2222,41 +2219,90 @@ export class User { return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue); } - public canBeLiquidated(perpMarketIndex?: number): { + public canBeLiquidated(): { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; } { - const liquidationBuffer = this.getLiquidationBuffer(); + // Deprecated signature retained for backward compatibility in type only + // but implementation now delegates to the new Map-based API and returns cross margin status. + const map = this.getLiquidationStatuses(); + const cross = map.get('cross'); + return cross ?? { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO }; + } - const totalCollateral = this.getTotalCollateral( - 'Maintenance', - undefined, - undefined, - liquidationBuffer - ); + /** + * New API: Returns liquidation status for cross and each isolated perp position. + * Map keys: + * - 'cross' for cross margin + * - marketIndex (number) for each isolated perp position + */ + public getLiquidationStatuses(marginCalc?: MarginCalculation): Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }> { + // If not provided, use buffer-aware calc for canBeLiquidated checks + if (!marginCalc) { + const liquidationBuffer = this.getLiquidationBuffer(); + marginCalc = this.getMarginCalculation('Maintenance', { liquidationBuffer }); + } - const marginRequirement = this.getMaintenanceMarginRequirement( - liquidationBuffer, - perpMarketIndex - ); - const canBeLiquidated = totalCollateral.lt(marginRequirement); + const result = new Map<'cross' | number, { + canBeLiquidated: boolean; + marginRequirement: BN; + totalCollateral: BN; + }>(); + + // Cross margin status + const crossTotalCollateral = marginCalc.totalCollateral; + const crossMarginRequirement = marginCalc.marginRequirement; + result.set('cross', { + canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement), + marginRequirement: crossMarginRequirement, + totalCollateral: crossTotalCollateral, + }); - return { - canBeLiquidated, - marginRequirement, - totalCollateral, - }; + // Isolated positions status + for (const [marketIndex, isoCalc] of marginCalc.isolatedMarginCalculations) { + const isoTotalCollateral = isoCalc.totalCollateral; + const isoMarginRequirement = isoCalc.marginRequirement; + result.set(marketIndex, { + canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement), + marginRequirement: isoMarginRequirement, + totalCollateral: isoTotalCollateral, + }); + } + + return result; } - public isBeingLiquidated(): boolean { - return ( + public isBeingLiquidated(marginCalc?: MarginCalculation): boolean { + // Consider on-chain flags OR computed margin status (cross or any isolated) + const hasOnChainFlag = (this.getUserAccount().status & - (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > - 0 + (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > 0; + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return ( + hasOnChainFlag || + this.isCrossMarginBeingLiquidated(calc) || + this.isIsolatedMarginBeingLiquidated(calc) ); } + /** Returns true if cross margin is currently below maintenance requirement (no buffer). */ + public isCrossMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return calc.totalCollateral.lt(calc.marginRequirement); + } + + /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */ + public isIsolatedMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + for (const [, isoCalc] of calc.isolatedMarginCalculations) { + if (isoCalc.totalCollateral.lt(isoCalc.marginRequirement)) { + return true; + } + } + return false; + } + public hasStatus(status: UserStatus): boolean { return (this.getUserAccount().status & status) > 0; } From 9c6b4b08e696239638b237a48452314e52889f02 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 20:23:40 -0600 Subject: [PATCH 85/91] fix: adjust health getter for user --- sdk/src/user.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 89a7dceca6..b0cfb591e5 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -1530,11 +1530,11 @@ export class User { * @returns : number (value from [0, 100]) */ public getHealth(perpMarketIndex?: number): number { - if (this.isBeingLiquidated() && !perpMarketIndex) { + const marginCalc = this.getMarginCalculation('Maintenance'); + if (this.isCrossMarginBeingLiquidated(marginCalc) && !perpMarketIndex) { return 0; } - const marginCalc = this.getMarginCalculation('Maintenance'); let totalCollateral: BN; let maintenanceMarginReq: BN; From 929434094f333769d6dee9257186b57484eab135 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 2 Sep 2025 20:29:14 -0600 Subject: [PATCH 86/91] fix: liq statuses add to return signature --- sdk/src/user.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/src/user.ts b/sdk/src/user.ts index b0cfb591e5..68e8879bef 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -2223,12 +2223,13 @@ export class User { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; + liquidationStatuses: Map<'cross' | number, { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN }>; } { // Deprecated signature retained for backward compatibility in type only // but implementation now delegates to the new Map-based API and returns cross margin status. const map = this.getLiquidationStatuses(); const cross = map.get('cross'); - return cross ?? { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO }; + return cross ? { ...cross, liquidationStatuses: map } : { canBeLiquidated: false, marginRequirement: ZERO, totalCollateral: ZERO, liquidationStatuses: map }; } /** From c45be38b45ca7144f7cd3877b64228a06316abff Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Tue, 14 Oct 2025 13:01:05 -0600 Subject: [PATCH 87/91] chore: post rebase cleaner upper --- sdk/src/decode/user.ts | 2 - sdk/src/driftClient.ts | 162 ++++++++------------------------------ sdk/src/user.ts | 5 +- sdk/tests/dlob/helpers.ts | 3 +- 4 files changed, 38 insertions(+), 134 deletions(-) diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index b3b2e2fca3..706a0698ba 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -86,7 +86,6 @@ export function decodeUser(buffer: Buffer): UserAccount { const openOrders = buffer.readUInt8(offset + 94); const positionFlag = buffer.readUInt8(offset + 95); const isolatedPositionScaledBalance = readUnsignedBigInt64LE(buffer, offset + 96); - const customMarginRatio = buffer.readUInt32LE(offset + 97); if ( baseAssetAmount.eq(ZERO) && @@ -139,7 +138,6 @@ export function decodeUser(buffer: Buffer): UserAccount { maxMarginRatio, positionFlag, isolatedPositionScaledBalance, - customMarginRatio, }); } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index c74f3cd042..96d917e9e1 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -264,103 +264,6 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } - public async getDepositIntoIsolatedPerpPositionIx({ - perpMarketIndex, - amount, - spotMarketIndex, - subAccountId, - userTokenAccount, - }: { - perpMarketIndex: number; - amount: BN; - spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex - subAccountId?: number; - userTokenAccount?: PublicKey; // defaults ATA for spot market mint - }): Promise { - const user = await this.getUserAccountPublicKey(subAccountId); - const userStats = this.getUserStatsAccountPublicKey(); - const statePk = await this.getStatePublicKey(); - const perp = this.getPerpMarketAccount(perpMarketIndex); - const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; - const spot = this.getSpotMarketAccount(spotIndex); - - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], - readablePerpMarketIndex: perpMarketIndex, - writableSpotMarketIndexes: [spotIndex], - }); - - // token program and transfer hook mints need to be present for deposit - this.addTokenMintToRemainingAccounts(spot, remainingAccounts); - if (this.isTransferHook(spot)) { - await this.addExtraAccountMetasToRemainingAccounts( - spot.mint, - remainingAccounts - ); - } - - const tokenProgram = this.getTokenProgramForSpotMarket(spot); - const ata = - userTokenAccount ?? - (await this.getAssociatedTokenAccount( - spotIndex, - false, - tokenProgram - )); - - return await this.program.instruction.depositIntoIsolatedPerpPosition( - spotIndex, - perpMarketIndex, - amount, - { - accounts: { - state: statePk, - user, - userStats, - authority: this.wallet.publicKey, - spotMarketVault: spot.vault, - userTokenAccount: ata, - tokenProgram, - }, - remainingAccounts, - } - ); - } - - public async getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex, - amount, - spotMarketIndex, - subAccountId, - }: { - perpMarketIndex: number; - amount: BN; - spotMarketIndex?: number; // defaults to perp.quoteSpotMarketIndex - subAccountId?: number; - }): Promise { - const user = await this.getUserAccountPublicKey(subAccountId); - const userStats = this.getUserStatsAccountPublicKey(); - const statePk = await this.getStatePublicKey(); - const perp = this.getPerpMarketAccount(perpMarketIndex); - const spotIndex = spotMarketIndex ?? perp.quoteSpotMarketIndex; - const spot = this.getSpotMarketAccount(spotIndex); - - return await this.program.instruction.transferIsolatedPerpPositionDeposit( - spotIndex, - perpMarketIndex, - amount, - { - accounts: { - user, - userStats, - authority: this.wallet.publicKey, - state: statePk, - spotMarketVault: spot.vault, - }, - } - ); - } - public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4384,29 +4287,27 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - const preIxs: TransactionInstruction[] = []; - if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isVariant(orderParams.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: orderParams.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId: userAccount.subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + userAccount.subAccountId + ) ); } - ixPromisesForTxs.marketOrderTx = (async () => { const placeOrdersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); if (preIxs.length) { - return [ - ...preIxs, - placeOrdersIx, - ] as unknown as TransactionInstruction; + return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; } return placeOrdersIx; })(); @@ -4530,11 +4431,11 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (isolatedPositionDepositAmount?.gt?.(ZERO)) { preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: orderParams.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) ); } @@ -4918,7 +4819,6 @@ export class DriftClient { useMarketLastSlotCache: true, }); - return await this.program.instruction.cancelOrders( marketType ?? null, marketIndex ?? null, @@ -4993,13 +4893,16 @@ export class DriftClient { const preIxs: TransactionInstruction[] = []; if (params?.length === 1) { const p = params[0]; - if (isVariant(p.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isVariant(p.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: p.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + p.marketIndex, + subAccountId + ) ); } } @@ -6470,13 +6373,16 @@ export class DriftClient { subAccountId ); - if (isVariant(orderParams.marketType, 'perp') && isolatedPositionDepositAmount?.gt?.(ZERO)) { + if ( + isVariant(orderParams.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) + ) { placeAndTakeIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx({ - perpMarketIndex: orderParams.marketIndex, - amount: isolatedPositionDepositAmount as BN, - subAccountId, - }) + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) ); } @@ -10758,4 +10664,4 @@ export class DriftClient { forceVersionedTransaction, }); } -} \ No newline at end of file +} diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 68e8879bef..48b4a8306d 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -287,14 +287,14 @@ export class User { ); // margin ratio for this perp - const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.customMarginRatio); + const customMarginRatio = Math.max(this.getUserAccount().maxMarginRatio, marketPosition.maxMarginRatio); let marginRatio = new BN( calculateMarketMarginRatio( market, worstCaseBaseAssetAmount.abs(), marginCategory, customMarginRatio, - this.isHighLeverageMode() || enteringHighLeverage + this.isHighLeverageMode(marginCategory) || enteringHighLeverage ) ); if (isVariant(market.status, 'settlement')) { @@ -616,7 +616,6 @@ export class User { maxMarginRatio: 0, positionFlag: 0, isolatedPositionScaledBalance: ZERO, - customMarginRatio: 0, }; } diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 90dfac1df8..b36effd2d7 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -47,6 +47,8 @@ export const mockPerpPosition: PerpPosition = { lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, positionFlag: 0, + isolatedPositionScaledBalance: new BN(0), + maxMarginRatio: 0, }; export const mockAMM: AMM = { @@ -663,7 +665,6 @@ export class MockUserMap implements UserMapInterface { private userMap = new Map(); private userAccountToAuthority = new Map(); private driftClient: DriftClient; - eventEmitter: StrictEventEmitter; constructor() { this.userMap = new Map(); From afd9feefbded0949b1e6eb27a86d81a65435fc57 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 15 Oct 2025 07:10:37 -0600 Subject: [PATCH 88/91] fix: missing params from per market lev --- sdk/src/driftClient.ts | 4 +++- sdk/src/types.ts | 2 ++ sdk/src/user.ts | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 96d917e9e1..21eb752908 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1569,7 +1569,8 @@ export class DriftClient { perpMarketIndex: number, marginRatio: number, subAccountId = 0, - txParams?: TxParams + txParams?: TxParams, + enteringHighLeverageMode?: boolean ): Promise { const ix = await this.getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex, @@ -4262,6 +4263,7 @@ export class DriftClient { referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, settlePnl?: boolean, + positionMaxLev?: number, isolatedPositionDepositAmount?: BN ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; diff --git a/sdk/src/types.ts b/sdk/src/types.ts index df9596f9fd..0a5d320d88 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1327,6 +1327,7 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDepositAmount?: BN | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1337,6 +1338,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + isolatedPositionDepositAmount?: BN | null; }; export type SignedMsgTriggerOrderParams = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 48b4a8306d..7a3b7440e7 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -984,6 +984,7 @@ export class User { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); + if(!market) return unrealizedPnl; const oraclePriceData = this.getMMOracleDataForPerpMarket( market.marketIndex ); @@ -1584,6 +1585,8 @@ export class User { perpPosition.marketIndex ); + if(!market) return ZERO; + let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; @@ -2895,7 +2898,8 @@ export class User { targetMarketIndex: number, tradeSide: PositionDirection, isLp = false, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; From 788bdef624df22ad1b8a7d29f39b2748aa7e62f4 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 15 Oct 2025 14:27:29 -0600 Subject: [PATCH 89/91] feat: zero out account withdraw from perp position --- sdk/src/driftClient.ts | 55 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 21eb752908..04a61ce932 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -46,6 +46,7 @@ import { PhoenixV1FulfillmentConfigAccount, PlaceAndTakeOrderSuccessCondition, PositionDirection, + PositionFlag, ReferrerInfo, ReferrerNameAccount, SerumV3FulfillmentConfigAccount, @@ -264,6 +265,51 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + private async getPostIxsForIsolatedWithdrawAfterMarketOrder( + orderParams: OptionalOrderParams, + userAccount: UserAccount + ): Promise { + const postIxs: TransactionInstruction[] = []; + const perpPosition = userAccount.perpPositions.find( + (p) => p.marketIndex === orderParams.marketIndex + ); + if (!perpPosition) return postIxs; + + const isIsolated = + (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; + if (!isIsolated) return postIxs; + + const currentBase = perpPosition.baseAssetAmount; + if (currentBase.eq(ZERO)) return postIxs; + + const signedOrderBase = + orderParams.direction === PositionDirection.LONG + ? orderParams.baseAssetAmount + : (orderParams.baseAssetAmount as BN).neg(); + const postBase = currentBase.add(signedOrderBase as BN); + if (!postBase.eq(ZERO)) return postIxs; + + const withdrawAmount = this.getIsolatedPerpPositionTokenAmount( + orderParams.marketIndex, + userAccount.subAccountId + ); + if (withdrawAmount.lte(ZERO)) return postIxs; + + const userTokenAccount = await this.getAssociatedTokenAccount( + QUOTE_SPOT_MARKET_INDEX + ); + postIxs.push( + await this.getWithdrawFromIsolatedPerpPositionIx( + withdrawAmount, + orderParams.marketIndex, + userTokenAccount, + userAccount.subAccountId + ) + ); + + return postIxs; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -4303,13 +4349,18 @@ export class DriftClient { ); } + // Build post-order instructions for perp (e.g., withdraw isolated margin on close) + const postIxs: TransactionInstruction[] = isVariant(orderParams.marketType, 'perp') + ? await this.getPostIxsForIsolatedWithdrawAfterMarketOrder(orderParams, userAccount) + : []; + ixPromisesForTxs.marketOrderTx = (async () => { const placeOrdersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); - if (preIxs.length) { - return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; + if (preIxs.length || postIxs.length) { + return [...preIxs, placeOrdersIx, ...postIxs] as unknown as TransactionInstruction; } return placeOrdersIx; })(); From 5f90487d6a094f28c695dd9e4b7e8b7419dbd221 Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Wed, 15 Oct 2025 15:38:11 -0600 Subject: [PATCH 90/91] fix: available positions logic update for iso --- sdk/src/math/position.ts | 9 ++++++++- sdk/src/types.ts | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 0cae66ab05..6a5da647ec 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -14,6 +14,7 @@ import { PositionDirection, PerpPosition, SpotMarketAccount, + PositionFlag, } from '../types'; import { calculateUpdatedAMM, @@ -243,10 +244,16 @@ export function positionIsAvailable(position: PerpPosition): boolean { position.baseAssetAmount.eq(ZERO) && position.openOrders === 0 && position.quoteAssetAmount.eq(ZERO) && - position.lpShares.eq(ZERO) + position.lpShares.eq(ZERO) && + position.isolatedPositionScaledBalance.eq(ZERO) + && !positionIsBeingLiquidated(position) ); } +export function positionIsBeingLiquidated(position: PerpPosition): boolean { + return (position.positionFlag & (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) > 0; +} + /** * * @param userPosition diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 0a5d320d88..6acd8e0a7f 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -504,7 +504,6 @@ export type LiquidationRecord = { liquidatePerpPnlForDeposit: LiquidatePerpPnlForDepositRecord; perpBankruptcy: PerpBankruptcyRecord; spotBankruptcy: SpotBankruptcyRecord; - bitFlags: number; }; export class LiquidationType { From e11a2ec81974191410558dcdf047715134949ebe Mon Sep 17 00:00:00 2001 From: Lukas deConantsesznak Date: Sun, 19 Oct 2025 15:40:12 -0600 Subject: [PATCH 91/91] feat: iso position txs cleanup + ix ordering --- sdk/src/driftClient.ts | 163 +++++++++++++++++++++-------------------- sdk/src/types.ts | 8 +- 2 files changed, 86 insertions(+), 85 deletions(-) diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 714221e11f..d39f70dcd9 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -274,49 +274,44 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } - private async getPostIxsForIsolatedWithdrawAfterMarketOrder( + private async getPrePlaceOrderIxs( orderParams: OptionalOrderParams, - userAccount: UserAccount + userAccount: UserAccount, + options?: { positionMaxLev?: number; isolatedPositionDepositAmount?: BN } ): Promise { - const postIxs: TransactionInstruction[] = []; - const perpPosition = userAccount.perpPositions.find( - (p) => p.marketIndex === orderParams.marketIndex - ); - if (!perpPosition) return postIxs; - - const isIsolated = - (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; - if (!isIsolated) return postIxs; - - const currentBase = perpPosition.baseAssetAmount; - if (currentBase.eq(ZERO)) return postIxs; + const preIxs: TransactionInstruction[] = []; - const signedOrderBase = - orderParams.direction === PositionDirection.LONG - ? orderParams.baseAssetAmount - : (orderParams.baseAssetAmount as BN).neg(); - const postBase = currentBase.add(signedOrderBase as BN); - if (!postBase.eq(ZERO)) return postIxs; + if (isVariant(orderParams.marketType, 'perp')) { + const { positionMaxLev, isolatedPositionDepositAmount } = options ?? {}; - const withdrawAmount = this.getIsolatedPerpPositionTokenAmount( - orderParams.marketIndex, - userAccount.subAccountId - ); - if (withdrawAmount.lte(ZERO)) return postIxs; + if ( + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, userAccount) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + userAccount.subAccountId + ) + ); + } - const userTokenAccount = await this.getAssociatedTokenAccount( - QUOTE_SPOT_MARKET_INDEX - ); - postIxs.push( - await this.getWithdrawFromIsolatedPerpPositionIx( - withdrawAmount, - orderParams.marketIndex, - userTokenAccount, - userAccount.subAccountId - ) - ); + if (positionMaxLev) { + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + preIxs.push( + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + orderParams.marketIndex, + marginRatio, + userAccount.subAccountId + ) + ); + } + } - return postIxs; + return preIxs; } public set isSubscribed(val: boolean) { @@ -4206,16 +4201,26 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + const userAccount = this.getUserAccount(subAccountId); + const settleIx = await this.settleMultiplePNLsIx( + userAccountPublicKey, + userAccount, + [perpMarketIndex], + SettlePnlMode.TRY_SETTLE + ); + const withdrawIx = await this.getWithdrawFromIsolatedPerpPositionIx( + amount, + perpMarketIndex, + userTokenAccount, + subAccountId + ); const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getWithdrawFromIsolatedPerpPositionIx( - amount, - perpMarketIndex, - userTokenAccount, - subAccountId - ), - txParams - ) + await this.buildTransaction([settleIx, withdrawIx], txParams) ); return txSig; } @@ -4588,47 +4593,25 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - const preIxs: TransactionInstruction[] = []; - if ( - isVariant(orderParams.marketType, 'perp') && - isolatedPositionDepositAmount?.gt?.(ZERO) - ) { - preIxs.push( - await this.getTransferIsolatedPerpPositionDepositIx( - isolatedPositionDepositAmount as BN, - orderParams.marketIndex, - userAccount.subAccountId - ) - ); - } - - // Build post-order instructions for perp (e.g., withdraw isolated margin on close) - const postIxs: TransactionInstruction[] = isVariant(orderParams.marketType, 'perp') - ? await this.getPostIxsForIsolatedWithdrawAfterMarketOrder(orderParams, userAccount) - : []; + const preIxs: TransactionInstruction[] = await this.getPrePlaceOrderIxs( + orderParams, + userAccount, + { + positionMaxLev, + isolatedPositionDepositAmount, + } + ); ixPromisesForTxs.marketOrderTx = (async () => { const placeOrdersIx = await this.getPlaceOrdersIx( [orderParams, ...bracketOrdersParams], userAccount.subAccountId ); - if (preIxs.length || postIxs.length) { - return [...preIxs, placeOrdersIx, ...postIxs] as unknown as TransactionInstruction; + if (preIxs.length) { + return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; } return placeOrdersIx; })(); - const marketOrderTxIxs = positionMaxLev - ? this.getPlaceOrdersAndSetPositionMaxLevIx( - [orderParams, ...bracketOrdersParams], - positionMaxLev, - userAccount.subAccountId - ) - : this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId - ); - - ixPromisesForTxs.marketOrderTx = marketOrderTxIxs; /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { @@ -5205,7 +5188,8 @@ export class DriftClient { params, txParams, subAccountId, - optionalIxs + optionalIxs, + isolatedPositionDepositAmount ) ).placeOrdersTx, [], @@ -5363,8 +5347,7 @@ export class DriftClient { const marginRatio = Math.floor( (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() ); - - // TODO: Handle multiple markets? + // Keep existing behavior but note: prefer using getPostPlaceOrderIxs path const setPositionMaxLevIxs = await this.getUpdateUserPerpPositionCustomMarginRatioIx( readablePerpMarketIndex[0], @@ -11360,4 +11343,22 @@ export class DriftClient { forceVersionedTransaction, }); } + + isOrderIncreasingPosition( + orderParams: OptionalOrderParams, + userAccount: UserAccount + ): boolean { + const perpPosition = userAccount.perpPositions.find( + (p) => p.marketIndex === orderParams.marketIndex + ); + if (!perpPosition) return true; + + const currentBase = perpPosition.baseAssetAmount; + if (currentBase.eq(ZERO)) return true; + + return currentBase + .add(orderParams.baseAssetAmount) + .abs() + .gt(currentBase.abs()); + } } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index dc84698abd..dd4a9d8b8c 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -585,7 +585,7 @@ export type SpotBankruptcyRecord = { }; export class LiquidationBitFlag { - static readonly IsolatedPosition = 1; + static readonly IsolatedPosition = 1; } export type SettlePnlRecord = { @@ -1310,7 +1310,7 @@ export type OptionalOrderParams = { } & NecessaryOrderParams; export type PerpOrderIsolatedExtras = { - isolatedPositionDepositAmount?: BN; + isolatedPositionDepositAmount?: BN; }; export type ModifyOrderParams = { @@ -1350,7 +1350,7 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; - isolatedPositionDepositAmount?: BN | null; + isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; }; @@ -1363,7 +1363,7 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; - isolatedPositionDepositAmount?: BN | null; + isolatedPositionDeposit?: BN | null; builderIdx?: number | null; builderFeeTenthBps?: number | null; };