diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 6827c92fe0..4318efa001 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -3,6 +3,8 @@ use std::ops::{Deref, DerefMut}; use anchor_lang::prelude::*; use solana_program::msg; +use crate::math::margin::calculate_free_collateral; + use crate::controller::funding::settle_funding_payment; use crate::controller::lp::burn_lp_shares; use crate::controller::orders; @@ -21,7 +23,10 @@ use crate::error::{DriftResult, ErrorCode}; use crate::get_then_update_id; use crate::math::bankruptcy::is_user_bankrupt; use crate::math::casting::Cast; -use crate::math::constants::{LIQUIDATION_FEE_PRECISION_U128, SPOT_WEIGHT_PRECISION}; +use crate::math::constants::{ + BASE_PRECISION_I128, LIQUIDATION_FEE_PRECISION_U128, MAX_FUNDING_SOCIAL_LOSS_MULT, + SPOT_WEIGHT_PRECISION, +}; use crate::math::liquidation::{ calculate_asset_transfer_for_liability_transfer, calculate_base_asset_amount_to_cover_margin_shortage, @@ -29,7 +34,7 @@ use crate::math::liquidation::{ calculate_funding_rate_deltas_to_resolve_bankruptcy, calculate_liability_transfer_implied_by_asset_amount, calculate_liability_transfer_to_cover_margin_shortage, calculate_liquidation_multiplier, - LiquidationMultiplierType, + calculate_perp_market_deleverage_payment, DeleverageUserStats, LiquidationMultiplierType, }; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral, meets_initial_margin_requirement, @@ -1512,8 +1517,10 @@ pub fn set_being_liquidated_and_get_liquidation_id(user: &mut User) -> DriftResu pub fn resolve_perp_bankruptcy( market_index: u16, - user: &mut User, - user_key: &Pubkey, + bankrupt_user: &mut User, + bankrupt_user_key: &Pubkey, + clawback_user: Option<&mut User>, + clawback_user_key: Option<&Pubkey>, liquidator: &mut User, liquidator_key: &Pubkey, perp_market_map: &PerpMarketMap, @@ -1523,7 +1530,7 @@ pub fn resolve_perp_bankruptcy( insurance_fund_vault_balance: u64, ) -> DriftResult { validate!( - user.is_bankrupt, + bankrupt_user.is_bankrupt, ErrorCode::UserNotBankrupt, "user not bankrupt", )?; @@ -1540,7 +1547,7 @@ pub fn resolve_perp_bankruptcy( "liquidator bankrupt", )?; - user.get_perp_position(market_index).map_err(|e| { + bankrupt_user.get_perp_position(market_index).map_err(|e| { msg!( "User does not have a position for perp market {}", market_index @@ -1548,7 +1555,7 @@ pub fn resolve_perp_bankruptcy( e })?; - let loss = user + let loss = bankrupt_user .get_perp_position(market_index) .unwrap() .quote_asset_amount @@ -1562,7 +1569,7 @@ pub fn resolve_perp_bankruptcy( let (margin_requirement, total_collateral, _, _) = calculate_margin_requirement_and_total_collateral( - user, + bankrupt_user, perp_market_map, MarginRequirementType::Maintenance, spot_market_map, @@ -1596,16 +1603,50 @@ pub fn resolve_perp_bankruptcy( let loss_to_socialize = loss.safe_add(if_payment.cast::()?)?; - let cumulative_funding_rate_delta = calculate_funding_rate_deltas_to_resolve_bankruptcy( - loss_to_socialize, - perp_market_map.get_ref(&market_index)?.deref(), + validate!( + loss_to_socialize <= 0, + ErrorCode::DefaultError, + "IF Payment to resolve bankruptcy exceeded bankrupcy loss" )?; + // must socialise losses among users in market + let mut cumulative_funding_rate_delta: i128 = 0; + let mut clawback_user_payment: i128 = 0; // socialize loss if loss_to_socialize < 0 { + let market = perp_market_map.get_ref_mut(&market_index)?; + + let total_base = market + .amm + .base_asset_amount_long + .safe_add(market.amm.base_asset_amount_short.abs())?; + + let cost_per_base = if total_base > 0 { + loss_to_socialize + .abs() + .safe_mul(BASE_PRECISION_I128)? + .safe_div(total_base)? + } else { + 0 + }; + + let order_tick_size = market.amm.order_tick_size; + + drop(market); + + if total_base > 0 + && (cost_per_base + <= order_tick_size + .cast::()? + .safe_mul(MAX_FUNDING_SOCIAL_LOSS_MULT)? + || order_tick_size == 0) { + msg!("bankruptcy resolution with funding rates"); let mut market = perp_market_map.get_ref_mut(&market_index)?; - let perp_position = user.force_get_perp_position_mut(market_index)?; + cumulative_funding_rate_delta = + calculate_funding_rate_deltas_to_resolve_bankruptcy(loss_to_socialize, &market)?; + + let perp_position = bankrupt_user.force_get_perp_position_mut(market_index)?; update_quote_asset_amount( perp_position, &mut market, @@ -1626,22 +1667,89 @@ pub fn resolve_perp_bankruptcy( .amm .cumulative_funding_rate_short .safe_sub(cumulative_funding_rate_delta)?; + } else { + msg!("bankruptcy resolution with clawback"); + + clawback_user_payment = if let Some(clawback_user) = clawback_user { + let clawback_user_free_collateral = calculate_free_collateral( + clawback_user, + perp_market_map, + spot_market_map, + oracle_map, + MarginRequirementType::Maintenance, + )?; + msg!( + "clawback_user_free_collateral={}", + clawback_user_free_collateral + ); + + if clawback_user_free_collateral > 0 { + let mut market = perp_market_map.get_ref_mut(&market_index)?; + let clawback_user_position = + clawback_user.get_perp_position_mut(market_index).unwrap(); + let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; + + let deleverage_user_stats = DeleverageUserStats { + base_asset_amount: clawback_user_position.base_asset_amount, + quote_asset_amount: clawback_user_position.quote_asset_amount, + quote_break_even_amount: clawback_user_position.quote_break_even_amount, + free_collateral: clawback_user_free_collateral, + }; + + let clawback_user_payment = calculate_perp_market_deleverage_payment( + loss_to_socialize, + deleverage_user_stats, + &market, + oracle_price_data.price, + )?; + let bankrupt_perp_position = + bankrupt_user.force_get_perp_position_mut(market_index)?; + + validate!( + clawback_user_payment >= 0 && bankrupt_perp_position.quote_asset_amount < 0, + ErrorCode::DefaultError, + "Deleverage User Payment to resolve bankruptcy mismatched" + )?; + + update_quote_asset_amount( + bankrupt_perp_position, + &mut market, + clawback_user_payment.cast()?, + )?; + + let delever_perp_position = + clawback_user.force_get_perp_position_mut(market_index)?; + + update_quote_asset_and_break_even_amount( + delever_perp_position, + &mut market, + -clawback_user_payment.cast()?, + )?; + + clawback_user_payment + } else { + 0 + } + } else { + 0 + }; + msg!("clawback_user_payment={}", clawback_user_payment); } } // exit bankruptcy - if !is_user_bankrupt(user) { - user.is_bankrupt = false; - user.is_being_liquidated = false; + if !is_user_bankrupt(bankrupt_user) { + bankrupt_user.is_bankrupt = false; + bankrupt_user.is_being_liquidated = false; } - let liquidation_id = user.next_liquidation_id.safe_sub(1)?; + let liquidation_id = bankrupt_user.next_liquidation_id.safe_sub(1)?; emit!(LiquidationRecord { ts: now, liquidation_id, liquidation_type: LiquidationType::PerpBankruptcy, - user: *user_key, + user: *bankrupt_user_key, liquidator: *liquidator_key, margin_requirement, total_collateral, @@ -1650,8 +1758,12 @@ pub fn resolve_perp_bankruptcy( market_index, if_payment, pnl: loss, - clawback_user: None, - clawback_user_payment: None, + clawback_user: clawback_user_key.copied(), + clawback_user_payment: if clawback_user_payment == 0 { + None + } else { + Some(clawback_user_payment.unsigned_abs()) + }, cumulative_funding_rate_delta, }, ..LiquidationRecord::default() diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 0a9eb4931a..08280064d9 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2688,9 +2688,13 @@ pub mod resolve_perp_bankruptcy { use crate::math::constants::{ AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, FUNDING_RATE_PRECISION_I128, FUNDING_RATE_PRECISION_I64, LIQUIDATION_FEE_PRECISION, - PEG_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + PEG_PRECISION, PRICE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, + SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral, MarginRequirementType, }; + use crate::state::oracle::HistoricalOracleData; use crate::state::oracle::OracleSource; use crate::state::oracle_map::OracleMap; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -2817,6 +2821,8 @@ pub mod resolve_perp_bankruptcy { 0, &mut user, &user_key, + None, + None, &mut liquidator, &liquidator_key, &market_map, @@ -2906,6 +2912,436 @@ pub mod resolve_perp_bankruptcy { assert_eq!(expected_affected_short_user, affected_short_user); } + + #[test] + pub fn successful_resolve_perp_bankruptcy_clawback_no_base() { + 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_long: 5 * BASE_PRECISION_I128, + // base_asset_amount_short: -5 * BASE_PRECISION_I128, + // base_asset_amount_with_amm: BASE_PRECISION_I128, + oracle: oracle_price_key, + cumulative_funding_rate_long: 0, + cumulative_funding_rate_short: 0, + + ..AMM::default() + }, + unrealized_pnl_initial_asset_weight: 0, + unrealized_pnl_maintenance_asset_weight: 100 * SPOT_WEIGHT_PRECISION, //todo: required to be 100 for this to work + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + number_of_users: 2, + ..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, + ..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: 0, + quote_asset_amount: -100 * QUOTE_PRECISION_I64, + quote_entry_amount: -100 * QUOTE_PRECISION_I64, + quote_break_even_amount: -100 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: [SpotPosition::default(); 8], + is_bankrupt: true, + is_being_liquidated: false, + next_liquidation_id: 2, + ..User::default() + }; + + let mut clawback_user = User { + orders: get_orders(Order { ..Order::default() }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 0, + quote_asset_amount: 100 * QUOTE_PRECISION_I64, + quote_entry_amount: 0, + quote_break_even_amount: 0, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + is_bankrupt: true, + is_being_liquidated: false, + next_liquidation_id: 2, + ..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 clawback_user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut expected_user = user; + expected_user.is_being_liquidated = false; + expected_user.is_bankrupt = false; + expected_user.perp_positions[0].quote_asset_amount = 0; + + let mut expected_market = market; + // expected_market.amm.cumulative_funding_rate_long = 1010 * FUNDING_RATE_PRECISION_I128; + // expected_market.amm.cumulative_funding_rate_short = -1010 * FUNDING_RATE_PRECISION_I128; + // expected_market.amm.cumulative_social_loss = -100000000; + // expected_market.amm.quote_asset_amount = -50 * QUOTE_PRECISION_I128; + expected_market.number_of_users = 1; + + assert_eq!( + clawback_user + .force_get_perp_position_mut(0) + .unwrap() + .quote_asset_amount, + 100 * QUOTE_PRECISION_I64 + ); + resolve_perp_bankruptcy( + 0, + &mut user, + &user_key, + Some(&mut clawback_user), + Some(&clawback_user_key), + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + 0, + ) + .unwrap(); + + assert_eq!( + clawback_user + .force_get_perp_position_mut(0) + .unwrap() + .quote_asset_amount, + 0 + ); + assert_eq!(expected_user, user); + // assert_eq!(expected_market, market_map.get_ref(&0).unwrap().clone()); + + let mut expected_affected_clawback_user = clawback_user; + expected_affected_clawback_user.perp_positions[0].quote_asset_amount = 0; + + { + let mut market = market_map.get_ref_mut(&0).unwrap(); + settle_funding_payment(&mut clawback_user, &Pubkey::default(), &mut market, now) + .unwrap() + } + + assert_eq!(expected_affected_clawback_user, clawback_user); + } + + #[test] + pub fn successful_resolve_perp_bankruptcy_clawback_with_base() { + 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: -(1243 + 1389) * QUOTE_PRECISION_I128, + base_asset_amount_long: 2 * BASE_PRECISION_I128 + 9508342, + // base_asset_amount_short: -5 * BASE_PRECISION_I128, + base_asset_amount_with_amm: 2 * BASE_PRECISION_I128 + 9508342, + oracle: oracle_price_key, + cumulative_funding_rate_long: 1000 * FUNDING_RATE_PRECISION_I128, + cumulative_funding_rate_short: 1000 * FUNDING_RATE_PRECISION_I128, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() + }, + order_tick_size: ((PRICE_PRECISION / 1000) as u64), + + ..AMM::default() + }, + market_index: 0, + unrealized_pnl_initial_asset_weight: 0, + unrealized_pnl_maintenance_asset_weight: 100 * SPOT_WEIGHT_PRECISION, //todo: required to be 100 for this to work + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 100, + number_of_users: 2, + ..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, + ..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: 0, + quote_asset_amount: -1243 * QUOTE_PRECISION_I64, + quote_entry_amount: -100 * QUOTE_PRECISION_I64, + quote_break_even_amount: -100 * QUOTE_PRECISION_I64, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: [SpotPosition::default(); 8], + is_bankrupt: true, + is_being_liquidated: false, + next_liquidation_id: 2, + ..User::default() + }; + + let mut clawback_user = User { + orders: get_orders(Order { ..Order::default() }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: (2 * BASE_PRECISION_I64) + 9508342, + quote_asset_amount: -1389, + quote_entry_amount: 0, + quote_break_even_amount: 0, + open_orders: 0, + open_bids: 0, + last_cumulative_funding_rate: (1000 * FUNDING_RATE_PRECISION_I128) as i64, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + is_bankrupt: false, + is_being_liquidated: false, + next_liquidation_id: 2, + ..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 clawback_user_key = Pubkey::default(); + let liquidator_key = Pubkey::default(); + + let mut expected_user = user; + expected_user.is_being_liquidated = false; + expected_user.is_bankrupt = false; + expected_user.perp_positions[0].quote_asset_amount = 0; + + let mut expected_market = market; + expected_market.number_of_users = 1; + resolve_perp_bankruptcy( + 0, + &mut user, + &user_key, + Some(&mut clawback_user), + Some(&clawback_user_key), + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + 0, + ) + .unwrap(); + // assert_eq!(0, 1); + + assert_eq!( + clawback_user + .force_get_perp_position_mut(0) + .unwrap() + .quote_asset_amount, + -1243001389 // $-1243 + ); + assert_eq!( + clawback_user + .force_get_perp_position_mut(0) + .unwrap() + .quote_break_even_amount, + -1243000000 // $-1243 + ); + + assert!(!user.is_bankrupt); + assert!(!user.is_being_liquidated); + let bankrupt_user_pos_after = user.force_get_perp_position_mut(0).unwrap(); + + assert_eq!( + bankrupt_user_pos_after.quote_asset_amount, + 0 // $-621 + ); + assert_eq!( + bankrupt_user_pos_after.open_orders, + 1 // not cancelled.. assumed to have been cancelled in liq + ); + assert_eq!(bankrupt_user_pos_after.quote_break_even_amount, -100000000); + + assert_eq!( + expected_market.amm.quote_asset_amount, + market.amm.quote_asset_amount + ); + assert_eq!( + expected_market.amm.quote_break_even_amount_long, + market.amm.quote_break_even_amount_long + ); + assert_eq!( + expected_market.amm.quote_break_even_amount_short, + market.amm.quote_break_even_amount_short + ); + + let (_, b_total_collateral, _b_margin_requirement_plus_buffer, _) = + calculate_margin_requirement_and_total_collateral( + &user, + &market_map, + MarginRequirementType::Maintenance, + &spot_market_map, + &mut oracle_map, + Some(1), + ) + .unwrap(); + + let (_, c_total_collateral, _c_margin_requirement_plus_buffer, _) = + calculate_margin_requirement_and_total_collateral( + &clawback_user, + &market_map, + MarginRequirementType::Maintenance, + &spot_market_map, + &mut oracle_map, + Some(1), + ) + .unwrap(); + + assert_eq!(c_total_collateral, -992050555); + assert_eq!(b_total_collateral, 0); + + // try to resolve for clawback_user + assert!(resolve_perp_bankruptcy( + 0, + &mut clawback_user, + &user_key, + None, + None, + &mut liquidator, + &liquidator_key, + &market_map, + &spot_market_map, + &mut oracle_map, + now, + 0, + ) + .is_err()); + } } pub mod resolve_spot_bankruptcy { diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 763d5bea21..a7fb0b120a 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -38,7 +38,6 @@ pub mod fulfill_order_with_maker_order { }; use crate::state::perp_market::PerpMarket; use crate::state::user::{Order, OrderType, PerpPosition, User, UserStats}; - use crate::test_utils::{get_orders, get_positions}; use super::*; @@ -4819,8 +4818,7 @@ pub mod fulfill_spot_order_with_match { pub mod fulfill_spot_order { use std::str::FromStr; - use anchor_lang::prelude::{AccountLoader, Clock}; - + use super::*; use crate::controller::orders::fill_spot_order; use crate::controller::position::PositionDirection; use crate::create_account_info; @@ -4836,8 +4834,8 @@ pub mod fulfill_spot_order { use crate::state::user::{MarketType, OrderStatus, OrderType, SpotPosition, User, UserStats}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; - - use super::*; + use anchor_lang::prelude::AccountLoader; + use anchor_lang::prelude::Clock; // Add back if we check free collateral in fill again // #[test] diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index ed610019bf..73684a38e0 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -2468,7 +2468,7 @@ pub mod delisting_test { ); assert_eq!( liquidator.perp_positions[0].quote_asset_amount, - // market.amm.quote_asset_amount_short + // market.amm.quote_break_even_amount_short 97000000000 + 23250001000 ); @@ -2700,6 +2700,8 @@ pub mod delisting_test { 0, &mut shorter, &maker_key, + None, + None, &mut liquidator, &liq_key, &market_map, diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index fd033445ef..3bbf35c59e 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -4,7 +4,7 @@ use anchor_spl::token::{Token, TokenAccount}; use crate::error::ErrorCode; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{ - get_maker_and_maker_stats, get_referrer_and_referrer_stats, get_serum_fulfillment_accounts, + get_maker_and_maker_stats, get_optional_user_and_user_stats, get_serum_fulfillment_accounts, get_spot_market_vaults, load_maps, AccountMaps, }; use crate::load_mut; @@ -88,7 +88,7 @@ fn fill_order( None => (None, None), }; - let (referrer, referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (referrer, referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; controller::repeg::update_amm( market_index, @@ -196,7 +196,7 @@ fn fill_spot_order( None => (None, None), }; - let (_referrer, _referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (_referrer, _referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; let mut serum_fulfillment_params = match fulfillment_type { Some(SpotFulfillmentType::SerumV3) => { @@ -834,6 +834,18 @@ pub fn handle_resolve_perp_bankruptcy( ErrorCode::InvalidSpotMarketAccount )?; + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + + let (delever_user_loader, _) = get_optional_user_and_user_stats(remaining_accounts_iter)?; + let (mut delever_user, delever_user_key) = match delever_user_loader.as_ref() { + Some(delever_user_loader) => { + let delevered_user_key = delever_user_loader.key(); + let delevered_user = load_mut!(delever_user_loader)?; + (Some(delevered_user), Some(delevered_user_key)) + } + None => (None, None), + }; + let user = &mut load_mut!(ctx.accounts.user)?; let liquidator = &mut load_mut!(ctx.accounts.liquidator)?; let state = &ctx.accounts.state; @@ -843,7 +855,7 @@ pub fn handle_resolve_perp_bankruptcy( spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + remaining_accounts_iter, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(quote_spot_market_index), clock.slot, @@ -874,6 +886,8 @@ pub fn handle_resolve_perp_bankruptcy( market_index, user, &user_key, + delever_user.as_deref_mut(), + delever_user_key.as_ref(), liquidator, &liquidator_key, &perp_market_map, diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index c294ab80ae..d5cc27d247 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -74,7 +74,7 @@ pub fn get_maker_and_maker_stats<'a>( } #[allow(clippy::type_complexity)] -pub fn get_referrer_and_referrer_stats<'a>( +pub fn get_optional_user_and_user_stats<'a>( account_info_iter: &mut Peekable>>, ) -> DriftResult<( Option>, diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 7d83a85c80..11cae9ac68 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -7,7 +7,7 @@ use crate::error::ErrorCode; use crate::get_then_update_id; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{ - get_maker_and_maker_stats, get_referrer_and_referrer_stats, get_serum_fulfillment_accounts, + get_maker_and_maker_stats, get_optional_user_and_user_stats, get_serum_fulfillment_accounts, get_spot_market_vaults, get_whitelist_token, load_maps, AccountMaps, }; use crate::instructions::SpotFulfillmentType; @@ -64,7 +64,7 @@ pub fn handle_initialize_user( // Only try to add referrer if it is the first user if user_stats.number_of_sub_accounts == 1 { - let (referrer, referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (referrer, referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; let referrer = if let (Some(referrer), Some(referrer_stats)) = (referrer, referrer_stats) { let referrer = load!(referrer)?; let mut referrer_stats = load_mut!(referrer_stats)?; @@ -819,7 +819,7 @@ pub fn handle_place_and_take_perp_order<'info>( None => (None, None), }; - let (referrer, referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (referrer, referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; let is_immediate_or_cancel = params.immediate_or_cancel; @@ -905,7 +905,7 @@ pub fn handle_place_and_make_perp_order<'info>( Some(state.oracle_guard_rails), )?; - let (referrer, referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (referrer, referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; if !params.immediate_or_cancel || !params.post_only || params.order_type != OrderType::Limit { msg!("place_and_make must use IOC post only limit order"); @@ -1038,7 +1038,7 @@ pub fn handle_place_and_take_spot_order<'info>( None => (None, None), }; - let (_referrer, _referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (_referrer, _referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; let is_immediate_or_cancel = params.immediate_or_cancel; @@ -1153,7 +1153,7 @@ pub fn handle_place_and_make_spot_order<'info>( None, )?; - let (_referrer, _referrer_stats) = get_referrer_and_referrer_stats(remaining_accounts_iter)?; + let (_referrer, _referrer_stats) = get_optional_user_and_user_stats(remaining_accounts_iter)?; if !params.immediate_or_cancel || !params.post_only || params.order_type != OrderType::Limit { msg!("place_and_make must use IOC post only limit order"); diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index ea4c192488..c107991e2e 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -173,6 +173,9 @@ pub const MIN_MARGIN_RATIO: u32 = MARGIN_PRECISION as u32 / 50; // 50x leverage pub const MAX_BID_ASK_INVENTORY_SKEW_FACTOR: u64 = 10 * BID_ASK_SPREAD_PRECISION; pub const MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN: i128 = 100 * QUOTE_PRECISION_I128; // max upnl for initial margin calc + +pub const MAX_FUNDING_SOCIAL_LOSS_MULT: i128 = 100; // 100 * the order tick size + pub const DEFAULT_MAX_TWAP_UPDATE_PRICE_BAND_DENOMINATOR: i64 = 3; // '3' here means clamp new data point to 33% (1/3) divergence from current twap (if twap > 0) // DEFAULTS diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 547b4f58e7..cd20d13811 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -1,10 +1,12 @@ use crate::error::{DriftResult, ErrorCode}; +use crate::math::amm::calculate_net_user_cost_basis; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION_I128, FUNDING_RATE_TO_QUOTE_PRECISION_PRECISION_RATIO, - LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_PRECISION_U128, - LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, PRICE_PRECISION, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, QUOTE_PRECISION, SPOT_WEIGHT_PRECISION_U128, + AMM_RESERVE_PRECISION_I128, BASE_PRECISION_I128, + FUNDING_RATE_TO_QUOTE_PRECISION_PRECISION_RATIO, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, PRICE_PRECISION, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, + QUOTE_PRECISION, SPOT_WEIGHT_PRECISION_U128, }; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral, MarginRequirementType, @@ -21,6 +23,8 @@ use crate::state::user::User; use crate::validate; use solana_program::msg; +use super::constants::PRICE_TO_QUOTE_PRECISION_RATIO; + #[cfg(test)] mod tests; @@ -295,3 +299,141 @@ pub fn calculate_cumulative_deposit_interest_delta_to_resolve_bankruptcy( .safe_div(total_deposits) .or(Ok(0)) } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DeleverageUserStats { + pub base_asset_amount: i64, + pub quote_asset_amount: i64, + pub quote_break_even_amount: i64, + pub free_collateral: i128, +} + +pub fn calculate_perp_market_deleverage_payment( + loss_to_socialize: i128, + deleverage_user_stats: DeleverageUserStats, + market: &PerpMarket, + oracle_price: i64, +) -> DriftResult { + validate!( + market.number_of_users > 0, + ErrorCode::InvalidAmmDetected, + "Market in corrupted state" + )?; + + let mean_short_price_per_base = if market.amm.base_asset_amount_short != 0 { + market + .amm + .quote_break_even_amount_short + .safe_mul(BASE_PRECISION_I128)? + .safe_div(market.amm.base_asset_amount_short)? + } else { + 0 + }; + + let mean_short_pnl_per_base = mean_short_price_per_base + .safe_sub(oracle_price.cast()?)? + .safe_div(PRICE_TO_QUOTE_PRECISION_RATIO.cast()?)?; + + let mean_long_price_per_base = if market.amm.base_asset_amount_long != 0 { + market + .amm + .quote_break_even_amount_long + .safe_mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128)? + .safe_div(market.amm.base_asset_amount_long)? + } else { + 0 + }; + + let mean_long_pnl_per_base = mean_long_price_per_base + .safe_add(oracle_price.cast()?)? + .safe_div(PRICE_TO_QUOTE_PRECISION_RATIO.cast()?)?; + + let mean_entry_price_per_base: i128 = if deleverage_user_stats.base_asset_amount > 0 { + mean_long_price_per_base + } else if deleverage_user_stats.base_asset_amount < 0 { + mean_short_price_per_base + } else { + 0 + }; + + let user_entry_price_per_base: i128 = if deleverage_user_stats.base_asset_amount != 0 { + BASE_PRECISION_I128 + .safe_mul(deleverage_user_stats.quote_break_even_amount.cast()?)? + .safe_div(deleverage_user_stats.base_asset_amount.cast()?)? + } else { + 0 + }; + + let exit_price_per_base = oracle_price + .safe_mul(deleverage_user_stats.base_asset_amount.signum().cast()?)? + .safe_div(PRICE_TO_QUOTE_PRECISION_RATIO.cast()?)?; + + let mean_pnl_per_base = mean_entry_price_per_base.safe_add(exit_price_per_base.cast()?)?; + let user_pnl_per_base = user_entry_price_per_base.safe_add(exit_price_per_base.cast()?)?; + + let profit_above_mean = if deleverage_user_stats.base_asset_amount == 0 { + msg!("user has no base"); + if deleverage_user_stats.quote_asset_amount != 0 { + let mean_quote = (market.amm.base_asset_amount_short) + .safe_add(market.amm.base_asset_amount_long)? + .safe_mul(oracle_price.cast()?)? + .safe_div(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128)? + .safe_add(calculate_net_user_cost_basis(&market.amm)?)? + .safe_add(loss_to_socialize)? + .safe_div(market.number_of_users.cast()?)?; + + -(mean_quote.safe_sub(deleverage_user_stats.quote_asset_amount.cast()?)?) + } else { + 0 + } + } else if user_pnl_per_base > mean_pnl_per_base && user_pnl_per_base > 0 { + msg!( + "user pays profit surplus, {} > {}", + user_pnl_per_base, + mean_pnl_per_base, + ); + + user_entry_price_per_base + .safe_sub(mean_entry_price_per_base)? + .safe_mul(deleverage_user_stats.base_asset_amount.cast()?)? + .safe_div(BASE_PRECISION_I128)? + } else if user_pnl_per_base == mean_pnl_per_base && user_pnl_per_base > 0 { + msg!("user pays loss_to_socialize / N users"); + + // more than 2, split + if market.number_of_users > 2 { + loss_to_socialize + .abs() + .safe_div(market.number_of_users.cast()?)? + .max(1) + } else { + loss_to_socialize.abs() + } + } else if (mean_long_pnl_per_base > mean_short_pnl_per_base + && deleverage_user_stats.base_asset_amount < 0) + || (mean_long_pnl_per_base < mean_short_pnl_per_base + && deleverage_user_stats.base_asset_amount > 0) + { + msg!("user on side that does not owe"); + 0 + } else { + msg!("user not above mean excess profit"); + 0 + }; + + // never let deleveraging put user into liquidation territory + let max_user_payment = if deleverage_user_stats.base_asset_amount == 0 { + deleverage_user_stats + .quote_asset_amount + .min(deleverage_user_stats.free_collateral.cast()?) + } else { + deleverage_user_stats.free_collateral.cast()? + }; + + let deleverage_payment = profit_above_mean + .min(max_user_payment.cast()?) + .min(-loss_to_socialize) + .max(0); + + Ok(deleverage_payment) +} diff --git a/programs/drift/src/math/liquidation/tests.rs b/programs/drift/src/math/liquidation/tests.rs index 34cabe7797..be78018beb 100644 --- a/programs/drift/src/math/liquidation/tests.rs +++ b/programs/drift/src/math/liquidation/tests.rs @@ -433,3 +433,1166 @@ mod calculate_cumulative_deposit_interest_delta_to_resolve_bankruptcy { assert_eq!(delta, 916666666); } } + +mod auto_deleveraging { + use crate::math::constants::{ + BASE_PRECISION_I128, BASE_PRECISION_I64, PRICE_PRECISION_I64, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, QUOTE_PRECISION_I128, QUOTE_PRECISION_I64, + }; + use crate::math::liquidation::{calculate_perp_market_deleverage_payment, DeleverageUserStats}; + use crate::state::perp_market::{PerpMarket, AMM}; + use solana_program::msg; + + #[test] + fn no_position_base_case_adl() { + let market = PerpMarket { + amm: AMM { + base_asset_amount_long: 0, + base_asset_amount_short: 0, + ..AMM::default() + }, + number_of_users: 1, + ..PerpMarket::default() + }; + + // user has no position / funds + let dus = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: 0, + quote_break_even_amount: 0, + // unrealized_pnl: 0, + free_collateral: 0, + }; + + let delev_payment = + calculate_perp_market_deleverage_payment(0, dus, &market, 100 * PRICE_PRECISION_I64) + .unwrap(); + assert_eq!(delev_payment, 0); + + let delev_payment = calculate_perp_market_deleverage_payment( + -QUOTE_PRECISION_I128, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + //todo + } + + #[test] + fn small_unsettled_pnl_adl() { + let market = PerpMarket { + amm: AMM { + base_asset_amount_long: 0, + base_asset_amount_short: 0, + ..AMM::default() + }, + number_of_users: 2, + ..PerpMarket::default() + }; + + // user has positive upnl + let dus = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: QUOTE_PRECISION_I64, + quote_break_even_amount: 0, + // unrealized_pnl: QUOTE_PRECISION_I128, + free_collateral: QUOTE_PRECISION_I128 * 2, + }; + + // user has negative upnl + let dus_neg = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: -QUOTE_PRECISION_I64, + quote_break_even_amount: 0, + // unrealized_pnl: -QUOTE_PRECISION_I128, + free_collateral: 0, + }; + + let delev_payment = calculate_perp_market_deleverage_payment( + -QUOTE_PRECISION_I128, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, QUOTE_PRECISION_I128); + + let delev_payment = calculate_perp_market_deleverage_payment( + -QUOTE_PRECISION_I128 / 2, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, QUOTE_PRECISION_I128 / 2); + + let delev_payment = calculate_perp_market_deleverage_payment( + -QUOTE_PRECISION_I128 * 200, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, QUOTE_PRECISION_I128); + + let delev_payment = calculate_perp_market_deleverage_payment( + -QUOTE_PRECISION_I128 * 200, + dus_neg, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + // strange input should be 0 still + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * 200, + dus_neg, + &market, + -100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + } + + #[test] + fn imbalance_base_pnl_adl() { + let market = PerpMarket { + amm: AMM { + base_asset_amount_long: BASE_PRECISION_I128 * 100, + base_asset_amount_short: -BASE_PRECISION_I128, + quote_asset_amount: QUOTE_PRECISION_I128 * 5, + quote_break_even_amount_long: -QUOTE_PRECISION_I128 * 100 * 100 + + QUOTE_PRECISION_I128 * 5, + quote_break_even_amount_short: QUOTE_PRECISION_I128 * 100, + quote_entry_amount_long: -QUOTE_PRECISION_I128 * 100 * 100, + quote_entry_amount_short: QUOTE_PRECISION_I128 * 100, + ..AMM::default() + }, + number_of_users: 3, + ..PerpMarket::default() + }; + + // user has positive upnl but 0 lifetime pnl + let dus = DeleverageUserStats { + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -QUOTE_PRECISION_I64 * 95, + quote_break_even_amount: -QUOTE_PRECISION_I64 * 100, + // unrealized_pnl: QUOTE_PRECISION_I128 * 5, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + // user has positive lifetime upnl but below mean + let dus = DeleverageUserStats { + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -QUOTE_PRECISION_I64 * 95, + quote_break_even_amount: -QUOTE_PRECISION_I64 * 99, + // unrealized_pnl: QUOTE_PRECISION_I128 * 6, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 950000); + + // user has positive lifetime upnl but above mean + let dus = DeleverageUserStats { + base_asset_amount: BASE_PRECISION_I64, + quote_asset_amount: -QUOTE_PRECISION_I64 * 95, + quote_break_even_amount: -QUOTE_PRECISION_I64 * 90, + // unrealized_pnl: QUOTE_PRECISION_I128 * 11, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 9950000); + + // user has odd-lot base and positive lifetime upnl but above mean ex-loss_to_socialize + let dus = DeleverageUserStats { + base_asset_amount: BASE_PRECISION_I64 * 25 / 23, + quote_asset_amount: (-QUOTE_PRECISION_I64 * 95 + 908324) * 25 / 23, + quote_break_even_amount: (-QUOTE_PRECISION_I64 * 90 - 43634) * 25 / 23, + // unrealized_pnl: (QUOTE_PRECISION_I128 * 11 + 43634) * 25/23, + free_collateral: 1000 * QUOTE_PRECISION_I128 + 463444, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 10767790); + + // user has odd-lot base and positive lifetime upnl but above mean ex-loss_to_socialize and near liq + let dus = DeleverageUserStats { + base_asset_amount: BASE_PRECISION_I64 * 25 / 23, + quote_asset_amount: (-QUOTE_PRECISION_I64 * 95 + 908324) * 25 / 23, + quote_break_even_amount: (-QUOTE_PRECISION_I64 * 90 - 43634) * 25 / 23, + // unrealized_pnl: (QUOTE_PRECISION_I128 * 11 + 43634) * 25/23, + free_collateral: 2 * QUOTE_PRECISION_I128, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 2000000); + } + + #[test] + fn imbalance_base_shorts_pnl_adl() { + let market = PerpMarket { + amm: AMM { + base_asset_amount_long: BASE_PRECISION_I128, + base_asset_amount_short: -BASE_PRECISION_I128 * 100, + quote_break_even_amount_long: -QUOTE_PRECISION_I128 * 100 + + QUOTE_PRECISION_I128 * 5, + quote_break_even_amount_short: QUOTE_PRECISION_I128 * 100 * 100, + quote_entry_amount_long: -QUOTE_PRECISION_I128 * 100, + quote_entry_amount_short: QUOTE_PRECISION_I128 * 100 * 100, + ..AMM::default() + }, + number_of_users: 3, + ..PerpMarket::default() + }; + + // user has positive upnl + let dus = DeleverageUserStats { + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: QUOTE_PRECISION_I64 * 105, + quote_break_even_amount: QUOTE_PRECISION_I64 * 100, + // unrealized_pnl: QUOTE_PRECISION_I128 * 5, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + // user has positive lifetime upnl but below mean ex-loss_to_socialize + let dus = DeleverageUserStats { + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: QUOTE_PRECISION_I64 * 105, + quote_break_even_amount: QUOTE_PRECISION_I64 * 101, + // unrealized_pnl: QUOTE_PRECISION_I128 * 6, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + // user has positive lifetime upnl but above mean ex-loss_to_socialize + let dus = DeleverageUserStats { + base_asset_amount: -BASE_PRECISION_I64, + quote_asset_amount: QUOTE_PRECISION_I64 * 105, + quote_break_even_amount: QUOTE_PRECISION_I64 * 110, + // unrealized_pnl: QUOTE_PRECISION_I128 * 11, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); // todo: 1 more than long version? + + // user has odd-lot base and positive lifetime upnl but above mean ex-loss_to_socialize + let dus = DeleverageUserStats { + base_asset_amount: -BASE_PRECISION_I64 * 25 / 23, + quote_asset_amount: (QUOTE_PRECISION_I64 * 105 - 908324) * 25 / 23, + quote_break_even_amount: (QUOTE_PRECISION_I64 * 110 + 43634) * 25 / 23, + // unrealized_pnl: (QUOTE_PRECISION_I128 * 11 + 43634) * 25/23, + free_collateral: 1000 * QUOTE_PRECISION_I128 + 463444, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + // user has odd-lot base and positive lifetime upnl but above mean ex-loss_to_socialize and near liq + let dus = DeleverageUserStats { + base_asset_amount: -BASE_PRECISION_I64 * 25 / 23, + quote_asset_amount: (QUOTE_PRECISION_I64 * 105 - 908324) * 25 / 23, + quote_break_even_amount: (QUOTE_PRECISION_I64 * 110 + 43634) * 25 / 23, + // unrealized_pnl: (QUOTE_PRECISION_I128 * 11 + 43634) * 25/23, + free_collateral: 2 * QUOTE_PRECISION_I128, + }; + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus, + &market, + 101 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + } + + #[test] + fn multiple_users_first_to_adl_test() { + let quote_asset_amount_long = -QUOTE_PRECISION_I128 * 30 * 9 / 10 * 102 + + QUOTE_PRECISION_I128 * 5 + + (200 * QUOTE_PRECISION_I128); + let quote_asset_amount_short = QUOTE_PRECISION_I128 * 30 * 11 / 10; + + let market = PerpMarket { + amm: AMM { + base_asset_amount_long: BASE_PRECISION_I128 * 102, + base_asset_amount_short: -BASE_PRECISION_I128 * 54, + quote_asset_amount: quote_asset_amount_long + quote_asset_amount_short, + quote_break_even_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_break_even_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + quote_entry_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_entry_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + ..AMM::default() + }, + number_of_users_with_base: 5, + number_of_users: 5, // todo, at least 6 with base or quote including the loss + ..PerpMarket::default() + }; + + let dus1 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 30 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 29 / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 29 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // tiny + let dus2 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long / 100) as i64, + quote_asset_amount: (quote_asset_amount_long / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long / 100) as i64, + free_collateral: 100 * QUOTE_PRECISION_I128, + }; + + let dus3 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 69 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 70 / 100) as i64 - 100, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 70 / 100) as i64, + free_collateral: 5000 * QUOTE_PRECISION_I128, + }; + + let dus4 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 49 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 49 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let dus5 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 51 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 51 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // levered loss + let dus6 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: -(200 * QUOTE_PRECISION_I128) as i64, + quote_break_even_amount: 0, + free_collateral: 0, + }; + + // filler + let dus7 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: 100_i64, + quote_break_even_amount: 0, + free_collateral: 10 * QUOTE_PRECISION_I128, + }; + + assert_eq!( + dus1.base_asset_amount + + dus2.base_asset_amount + + dus3.base_asset_amount + + dus7.base_asset_amount, + market.amm.base_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_asset_amount + + dus2.quote_asset_amount + + dus3.quote_asset_amount + + dus7.quote_asset_amount, + quote_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_break_even_amount + + dus2.quote_break_even_amount + + dus3.quote_break_even_amount + + dus7.quote_break_even_amount, + market.amm.quote_break_even_amount_long as i64 + ); + + assert_eq!( + dus4.base_asset_amount + dus5.base_asset_amount + dus6.base_asset_amount, + market.amm.base_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_asset_amount + dus5.quote_asset_amount + dus6.quote_asset_amount, + quote_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_break_even_amount + + dus5.quote_break_even_amount + + dus6.quote_break_even_amount, + market.amm.quote_break_even_amount_short as i64 + ); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus1, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 23_999_977); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus2, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 40_000_000); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus3, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus4, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus5, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus6, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus7, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + // rich filler + let dus8 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: QUOTE_PRECISION_I64 * 2000, + quote_break_even_amount: 0, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let delev_payment = calculate_perp_market_deleverage_payment( + QUOTE_PRECISION_I128 * -200, + dus8, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 200000000); + } + + #[test] + fn multiple_users_adl_sequence_test() { + let quote_asset_amount_long = -QUOTE_PRECISION_I128 * 30 * 9 / 10 * 102 + + QUOTE_PRECISION_I128 * 5 + + (200 * QUOTE_PRECISION_I128); + let quote_asset_amount_short = QUOTE_PRECISION_I128 * 30 * 11 / 10; + + let mut market = PerpMarket { + amm: AMM { + base_asset_amount_long: BASE_PRECISION_I128 * 102, + base_asset_amount_short: -BASE_PRECISION_I128 * 54, + quote_asset_amount: quote_asset_amount_long + quote_asset_amount_short, + quote_break_even_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_break_even_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + quote_entry_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_entry_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + ..AMM::default() + }, + number_of_users_with_base: 5, + number_of_users: 5, // todo, at least 6 with base or quote including the loss + ..PerpMarket::default() + }; + + let mut dus1 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 30 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 29 / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 29 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // tiny + let mut dus2 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long / 100) as i64, + quote_asset_amount: (quote_asset_amount_long / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long / 100) as i64, + free_collateral: 100 * QUOTE_PRECISION_I128, + }; + + let mut dus3 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 69 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 70 / 100) as i64 - 100, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 70 / 100) as i64, + free_collateral: 5000 * QUOTE_PRECISION_I128, + }; + + let dus4 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 49 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 49 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let dus5 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 51 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 51 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // levered loss + let dus6 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: -(200 * QUOTE_PRECISION_I128) as i64, + quote_break_even_amount: 0, + free_collateral: 0, + }; + + // filler + let dus7 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: 100_i64, + quote_break_even_amount: 0, + free_collateral: 10 * QUOTE_PRECISION_I128, + }; + + assert_eq!( + dus1.base_asset_amount + + dus2.base_asset_amount + + dus3.base_asset_amount + + dus7.base_asset_amount, + market.amm.base_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_asset_amount + + dus2.quote_asset_amount + + dus3.quote_asset_amount + + dus7.quote_asset_amount, + quote_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_break_even_amount + + dus2.quote_break_even_amount + + dus3.quote_break_even_amount + + dus7.quote_break_even_amount, + market.amm.quote_break_even_amount_long as i64 + ); + + assert_eq!( + dus4.base_asset_amount + dus5.base_asset_amount + dus6.base_asset_amount, + market.amm.base_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_asset_amount + dus5.quote_asset_amount + dus6.quote_asset_amount, + quote_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_break_even_amount + + dus5.quote_break_even_amount + + dus6.quote_break_even_amount, + market.amm.quote_break_even_amount_short as i64 + ); + + let mut remaining_levered_loss = QUOTE_PRECISION_I128 * -200; + + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus1, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 23_999_977); + assert_eq!( + dus1.quote_break_even_amount * PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 as i64 + / dus1.base_asset_amount, + -22745098 + ); + + dus1.quote_asset_amount -= delev_payment as i64; + dus1.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + + assert_eq!(dus1.quote_asset_amount, -763209977); + assert_eq!(dus1.quote_break_even_amount, -719999977); + assert_eq!(dus1.base_asset_amount, 30600000000); + assert_eq!(remaining_levered_loss, -176_000_023); + + assert_eq!( + dus1.quote_break_even_amount * PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 as i64 + / dus1.base_asset_amount, + -23529411 + ); + + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus1, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 7_199_996); + + dus1.quote_asset_amount -= delev_payment as i64; + dus1.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + + assert_eq!(dus1.quote_asset_amount, -770409973); + assert_eq!(dus1.quote_break_even_amount, -727199973); + assert_eq!(dus1.base_asset_amount, 30600000000); + + assert_eq!( + dus1.quote_break_even_amount * PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 as i64 + / dus1.base_asset_amount, + -23764705 + ); + + // shrinking rets + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus1, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 2_159_992); + + // lets switch over now + assert_eq!( + dus2.quote_break_even_amount * PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 as i64 + / dus2.base_asset_amount, + -23529411 + ); + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus2, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 311_999); + + dus2.quote_asset_amount -= delev_payment as i64; + dus2.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + + assert_eq!(dus2.quote_asset_amount, -25801999); + assert_eq!(dus2.quote_break_even_amount, -24311999); + assert_eq!(dus2.base_asset_amount, 1020000000); + assert_eq!(remaining_levered_loss, -168_488_028); + + // lets switch over again now + assert_eq!( + dus3.quote_break_even_amount * PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128 as i64 + / dus3.base_asset_amount, + -23870417 + ); + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus3, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + + dus3.quote_asset_amount -= delev_payment as i64; + dus3.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus3, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + assert_eq!(delev_payment, 0); + dus3.quote_asset_amount -= delev_payment as i64; + dus3.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + + assert_eq!(remaining_levered_loss, -168488028); + } + + #[test] + fn multiple_users_adl_automated_sequence_test() { + let quote_asset_amount_long = -QUOTE_PRECISION_I128 * 30 * 9 / 10 * 102 + + QUOTE_PRECISION_I128 * 5 + + (200 * QUOTE_PRECISION_I128); + let quote_asset_amount_short = QUOTE_PRECISION_I128 * 30 * 11 / 10; + + let mut market = PerpMarket { + amm: AMM { + base_asset_amount_long: BASE_PRECISION_I128 * 102, + base_asset_amount_short: -BASE_PRECISION_I128 * 54, + quote_asset_amount: quote_asset_amount_long + quote_asset_amount_short, + quote_break_even_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_break_even_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + quote_entry_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_entry_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + ..AMM::default() + }, + number_of_users_with_base: 5, + number_of_users: 5, // todo, at least 6 with base or quote including the loss + ..PerpMarket::default() + }; + + let mut dus1 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 30 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 29 / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 29 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // tiny + let mut dus2 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long / 100) as i64, + quote_asset_amount: (quote_asset_amount_long / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long / 100) as i64, + free_collateral: 100 * QUOTE_PRECISION_I128, + }; + + let mut dus3 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 69 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 70 / 100) as i64 - 100, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 70 / 100) as i64, + free_collateral: 5000 * QUOTE_PRECISION_I128, + }; + + let mut dus4 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 49 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 49 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let mut dus5 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 51 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 51 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // levered loss + let mut dus6 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: -(200 * QUOTE_PRECISION_I128) as i64, + quote_break_even_amount: 0, + free_collateral: 0, + }; + + // filler + let mut dus7 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: 100_i64, + quote_break_even_amount: 0, + free_collateral: 10 * QUOTE_PRECISION_I128, + }; + + assert_eq!( + dus1.base_asset_amount + + dus2.base_asset_amount + + dus3.base_asset_amount + + dus7.base_asset_amount, + market.amm.base_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_asset_amount + + dus2.quote_asset_amount + + dus3.quote_asset_amount + + dus7.quote_asset_amount, + quote_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_break_even_amount + + dus2.quote_break_even_amount + + dus3.quote_break_even_amount + + dus7.quote_break_even_amount, + market.amm.quote_break_even_amount_long as i64 + ); + + assert_eq!( + dus4.base_asset_amount + dus5.base_asset_amount + dus6.base_asset_amount, + market.amm.base_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_asset_amount + dus5.quote_asset_amount + dus6.quote_asset_amount, + quote_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_break_even_amount + + dus5.quote_break_even_amount + + dus6.quote_break_even_amount, + market.amm.quote_break_even_amount_short as i64 + ); + let mut remaining_levered_loss = QUOTE_PRECISION_I128 * -200; + + let mut dus_list: Vec<&mut DeleverageUserStats> = vec![ + &mut dus1, &mut dus2, &mut dus3, &mut dus4, &mut dus5, &mut dus6, &mut dus7, + ]; + + let mut v = Vec::new(); + let l = 7; + v.resize(l, 0_i128); + + let mut count = 0; + let mut zaps = 0; + while remaining_levered_loss < 0 && count < 1000 { + for (idx, mut dus) in dus_list.iter_mut().enumerate() { + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + **dus, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + + if delev_payment > 0 { + msg!("{}: delev_payment={}", count, delev_payment); + dus.quote_asset_amount -= delev_payment as i64; + dus.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + zaps += 1; + } + + count += 1; + v[idx] += delev_payment; + } + } + + assert_eq!(remaining_levered_loss, 0); + assert_eq!(count > 500, true); + assert_eq!(zaps, 155); + assert_eq!(v, [83070095, 6026159, 110903746, 0, 0, 0, 0]); + } + + #[test] + fn bully_one_users_adl_automated_sequence_test() { + let quote_asset_amount_long = -QUOTE_PRECISION_I128 * 30 * 9 / 10 * 102 + + QUOTE_PRECISION_I128 * 5 + + (200 * QUOTE_PRECISION_I128); + let quote_asset_amount_short = QUOTE_PRECISION_I128 * 30 * 11 / 10; + + let mut market = PerpMarket { + amm: AMM { + base_asset_amount_long: BASE_PRECISION_I128 * 102, + base_asset_amount_short: -BASE_PRECISION_I128 * 54, + quote_asset_amount: quote_asset_amount_long + quote_asset_amount_short, + quote_break_even_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_break_even_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + quote_entry_amount_long: -QUOTE_PRECISION_I128 * 30 * 100 * 8 / 10, + quote_entry_amount_short: QUOTE_PRECISION_I128 * 30 * 54 * 12 / 10, + ..AMM::default() + }, + number_of_users_with_base: 5, + number_of_users: 5, // todo, at least 6 with base or quote including the loss + ..PerpMarket::default() + }; + + let mut dus1 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 30 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 29 / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 29 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // tiny + let mut dus2 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long / 100) as i64, + quote_asset_amount: (quote_asset_amount_long / 100) as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_long / 100) as i64, + free_collateral: 100 * QUOTE_PRECISION_I128, + }; + + let mut dus3 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_long * 69 / 100) as i64, + quote_asset_amount: (quote_asset_amount_long * 70 / 100) as i64 - 100, + quote_break_even_amount: (market.amm.quote_break_even_amount_long * 70 / 100) as i64, + free_collateral: 5000 * QUOTE_PRECISION_I128, + }; + + let dus4 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 49 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 49 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + let dus5 = DeleverageUserStats { + base_asset_amount: (market.amm.base_asset_amount_short * 50 / 100) as i64, + quote_asset_amount: ((quote_asset_amount_short + 200 * QUOTE_PRECISION_I128) * 51 / 100) + as i64, + quote_break_even_amount: (market.amm.quote_break_even_amount_short * 51 / 100) as i64, + free_collateral: 1000 * QUOTE_PRECISION_I128, + }; + + // levered loss + let dus6 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: -(200 * QUOTE_PRECISION_I128) as i64, + quote_break_even_amount: 0, + free_collateral: 0, + }; + + // filler + let dus7 = DeleverageUserStats { + base_asset_amount: 0, + quote_asset_amount: 100_i64, + quote_break_even_amount: 0, + free_collateral: 10 * QUOTE_PRECISION_I128, + }; + + assert_eq!( + dus1.base_asset_amount + + dus2.base_asset_amount + + dus3.base_asset_amount + + dus7.base_asset_amount, + market.amm.base_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_asset_amount + + dus2.quote_asset_amount + + dus3.quote_asset_amount + + dus7.quote_asset_amount, + quote_asset_amount_long as i64 + ); + assert_eq!( + dus1.quote_break_even_amount + + dus2.quote_break_even_amount + + dus3.quote_break_even_amount + + dus7.quote_break_even_amount, + market.amm.quote_break_even_amount_long as i64 + ); + + assert_eq!( + dus4.base_asset_amount + dus5.base_asset_amount + dus6.base_asset_amount, + market.amm.base_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_asset_amount + dus5.quote_asset_amount + dus6.quote_asset_amount, + quote_asset_amount_short as i64 + ); + assert_eq!( + dus4.quote_break_even_amount + + dus5.quote_break_even_amount + + dus6.quote_break_even_amount, + market.amm.quote_break_even_amount_short as i64 + ); + + let mut remaining_levered_loss = QUOTE_PRECISION_I128 * -200; + + // let mut dus_list: Vec<&mut DeleverageUserStats> = vec![]; + // dus_list.push(&mut dus1); + // dus_list.push(&mut dus2); + // dus_list.push(&mut dus3); + // dus_list.push(&mut dus4); + // dus_list.push(&mut dus5); + // dus_list.push(&mut dus6); + // dus_list.push(&mut dus7); + + let mut v = Vec::new(); + let l = 7; + v.resize(l, 0_i128); + + let mut count = 0; + let mut zaps = 0; + while remaining_levered_loss < 0 && count < 1000 { + let idx = 0; + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus1, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + + if delev_payment > 0 { + msg!("{}: delev_payment={}", count, delev_payment); + dus1.quote_asset_amount -= delev_payment as i64; + dus1.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + zaps += 1; + } + + count += 1; + v[idx] += delev_payment; + // idx += 1; + } + assert_eq!(remaining_levered_loss, -132571414); + assert_eq!(v, [67428586, 0, 0, 0, 0, 0, 0]); + assert_eq!(zaps, 14); + assert_eq!(count, 1000); + + let mut count = 0; + let mut zaps = 0; + while remaining_levered_loss < 0 && count < 1000 { + let idx = 1; + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus2, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + + if delev_payment > 0 { + msg!("{}: delev_payment={}", count, delev_payment); + dus2.quote_asset_amount -= delev_payment as i64; + dus2.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + zaps += 1; + } + + count += 1; + v[idx] += delev_payment; + // idx += 1; + } + assert_eq!(remaining_levered_loss, -105512254); + assert_eq!(v, [67428586, 27059160, 0, 0, 0, 0, 0]); + assert_eq!(zaps, 5); + assert_eq!(count, 1000); + + let mut count = 0; + let mut zaps = 0; + while remaining_levered_loss < 0 && count < 1000 { + let idx = 2; + let delev_payment = calculate_perp_market_deleverage_payment( + remaining_levered_loss, + dus3, + &market, + 100 * PRICE_PRECISION_I64, + ) + .unwrap(); + + if delev_payment > 0 { + msg!("{}: delev_payment={}", count, delev_payment); + dus3.quote_asset_amount -= delev_payment as i64; + dus3.quote_break_even_amount -= delev_payment as i64; + market.amm.quote_break_even_amount_long -= delev_payment; + market.amm.quote_entry_amount_long -= delev_payment; + remaining_levered_loss += delev_payment; + zaps += 1; + } + + count += 1; + v[idx] += delev_payment; + // idx += 1; + } + assert_eq!(remaining_levered_loss, 0); + assert_eq!(v, [67428586, 27059160, 105512254, 0, 0, 0, 0]); + assert_eq!(zaps, 5); + assert_eq!(count, 5); + } +} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 3031b92952..0aec46824e 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -634,12 +634,13 @@ pub fn calculate_free_collateral( perp_market_map: &PerpMarketMap, spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, + margin_type: MarginRequirementType, ) -> DriftResult { let (margin_requirement, total_collateral, _, _) = calculate_margin_requirement_and_total_collateral( user, perp_market_map, - MarginRequirementType::Initial, + margin_type, spot_market_map, oracle_map, None, @@ -655,10 +656,15 @@ pub fn calculate_max_withdrawable_amount( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, ) -> DriftResult { - let free_collateral = - calculate_free_collateral(user, perp_market_map, spot_market_map, oracle_map)? - .max(0) - .cast::()?; + let free_collateral = calculate_free_collateral( + user, + perp_market_map, + spot_market_map, + oracle_map, + MarginRequirementType::Initial, + )? + .max(0) + .cast::()?; let spot_market = &mut spot_market_map.get_ref(&market_index)?; diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index c776b1113d..626e58b3e0 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -24,6 +24,7 @@ import { MarketType, SerumV3FulfillmentConfigAccount, isVariant, + DeleverUserInfo, } from './types'; import * as anchor from '@project-serum/anchor'; import driftIDL from './idl/drift.json'; @@ -3093,14 +3094,16 @@ export class DriftClient { public async resolvePerpBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, - marketIndex: number + marketIndex: number, + deleverUserInfo?: DeleverUserInfo ): Promise { const { txSig } = await this.txSender.send( wrapInTx( await this.getResolvePerpBankruptcyIx( userAccountPublicKey, userAccount, - marketIndex + marketIndex, + deleverUserInfo ) ), [], @@ -3112,7 +3115,8 @@ export class DriftClient { public async getResolvePerpBankruptcyIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, - marketIndex: number + marketIndex: number, + deleverUserInfo?: DeleverUserInfo ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, @@ -3122,12 +3126,30 @@ export class DriftClient { const liquidatorPublicKey = await this.getUserAccountPublicKey(); const liquidatorStatsPublicKey = await this.getUserStatsAccountPublicKey(); + const userAccounts = [this.getUserAccount(), userAccount]; + if (deleverUserInfo !== undefined) { + userAccounts.push(deleverUserInfo.userAccount); + } + const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(), userAccount], + userAccounts, writablePerpMarketIndexes: [marketIndex], writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (deleverUserInfo) { + remainingAccounts.push({ + pubkey: deleverUserInfo.user, + isWritable: true, + isSigner: false, + }); + remainingAccounts.push({ + pubkey: deleverUserInfo.userStats, + isWritable: true, + isSigner: false, + }); + } + const spotMarket = this.getSpotMarketAccount(marketIndex); return await this.program.instruction.resolvePerpBankruptcy( diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 9075223c6d..7fc517a6b5 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -8246,5 +8246,8 @@ "name": "MarketBeingInitialized", "msg": "Market Being Initialized" } - ] + ], + "metadata": { + "address": "jAEeKs9twxAJmXZHqS2p459xW7FMDjoyvuqthRo9qGS" + } } \ No newline at end of file diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 79b66f6d9d..11ef3567c0 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -815,6 +815,12 @@ export type TakerInfo = { order: Order; }; +export type DeleverUserInfo = { + user: PublicKey; + userStats: PublicKey; + userAccount: UserAccount; +}; + export type ReferrerInfo = { referrer: PublicKey; referrerStats: PublicKey;