diff --git a/CHANGELOG.md b/CHANGELOG.md index 29df7f12a..3ccdf0369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add liquidation via fill ([#1106](https://github.com/drift-labs/protocol-v2/pull/1106)) - program: add switchboard on demand integration ([#1154](https://github.com/drift-labs/protocol-v2/pull/1154)) - program: add support for token 2022 ([#1125](https://github.com/drift-labs/protocol-v2/pull/1125)) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 579ae3cfb..6456ccbc4 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -7,6 +7,7 @@ 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::{ get_position_index, update_position_and_market, update_quote_asset_amount, update_quote_asset_and_break_even_amount, PositionDirection, @@ -18,7 +19,6 @@ use crate::controller::spot_balance::{ }; 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::bankruptcy::is_user_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ @@ -33,7 +33,8 @@ use crate::math::liquidation::{ calculate_liability_transfer_implied_by_asset_amount, calculate_liability_transfer_to_cover_margin_shortage, calculate_liquidation_multiplier, calculate_max_pct_to_liquidate, calculate_perp_if_fee, calculate_spot_if_fee, - validate_transfer_satisfies_limit_price, LiquidationMultiplierType, + get_liquidation_order_params, validate_transfer_satisfies_limit_price, + LiquidationMultiplierType, }; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, @@ -46,6 +47,7 @@ use crate::math::orders::{ }; use crate::math::position::calculate_base_asset_value_with_oracle_price; use crate::math::safe_math::SafeMath; + use crate::math::spot_balance::get_token_value; use crate::state::events::{ emit_stack, LPAction, LPRecord, LiquidateBorrowForPerpPnlRecord, @@ -53,8 +55,10 @@ use crate::state::events::{ LiquidationType, OrderAction, OrderActionExplanation, OrderActionRecord, OrderRecord, PerpBankruptcyRecord, SpotBankruptcyRecord, }; +use crate::state::fill_mode::FillMode; use crate::state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}; use crate::state::oracle_map::OracleMap; +use crate::state::order_params::PlaceOrderOptions; use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::PerpMarketMap; @@ -63,7 +67,9 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; use crate::state::traits::Size; use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; +use crate::state::user_map::{UserMap, UserStatsMap}; use crate::validate; +use crate::{get_then_update_id, load_mut}; #[cfg(test)] mod tests; @@ -510,7 +516,7 @@ pub fn liquidate_perp( ) }; - let margin_freed_for_perp_position = calculate_margin_freed( + let (margin_freed_for_perp_position, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -656,6 +662,451 @@ pub fn liquidate_perp( Ok(()) } +pub fn liquidate_perp_with_fill( + market_index: u16, + user_loader: &AccountLoader, + user_key: &Pubkey, + user_stats_loader: &AccountLoader, + liquidator_loader: &AccountLoader, + liquidator_key: &Pubkey, + liquidator_stats_loader: &AccountLoader, + makers_and_referrer: &UserMap, + makers_and_referrer_stats: &UserStatsMap, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + clock: &Clock, + state: &State, +) -> DriftResult { + let now = clock.unix_timestamp; + let slot = clock.slot; + + let mut user = load_mut!(user_loader)?; + let mut liquidator = load_mut!(liquidator_loader)?; + + let liquidation_margin_buffer_ratio = state.liquidation_margin_buffer_ratio; + let initial_pct_to_liquidate = state.initial_pct_to_liquidate as u128; + let liquidation_duration = state.liquidation_duration as u128; + + validate!( + !user.is_bankrupt(), + ErrorCode::UserBankrupt, + "user bankrupt", + )?; + + validate!( + !liquidator.is_bankrupt(), + ErrorCode::UserBankrupt, + "liquidator bankrupt", + )?; + + let market = perp_market_map.get_ref(&market_index)?; + + validate!( + !market.is_operation_paused(PerpOperation::Liquidation), + ErrorCode::InvalidLiquidation, + "Liquidation operation is paused for market {}", + market_index + )?; + + drop(market); + + // Settle user's funding payments so that collateral is up to date + settle_funding_payment( + &mut user, + user_key, + perp_market_map.get_ref_mut(&market_index)?.deref_mut(), + now, + )?; + + // Settle user's funding payments so that collateral is up to date + settle_funding_payment( + &mut liquidator, + liquidator_key, + perp_market_map.get_ref_mut(&market_index)?.deref_mut(), + now, + )?; + + 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))?, + )?; + + 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(); + return Ok(()); + } + + user.get_perp_position(market_index).map_err(|e| { + msg!( + "User does not have a position for perp market {}", + market_index + ); + e + })?; + + let liquidation_id = user.enter_liquidation(slot)?; + let mut margin_freed = 0_u64; + + 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(), + ErrorCode::PositionDoesntHaveOpenPositionOrOrders + )?; + + let canceled_order_ids = orders::cancel_orders( + &mut user, + user_key, + Some(liquidator_key), + perp_market_map, + spot_market_map, + oracle_map, + now, + slot, + OrderActionExplanation::Liquidation, + None, + None, + None, + )?; + + let mut market = perp_market_map.get_ref_mut(&market_index)?; + let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; + + update_amm_and_check_validity( + &mut market, + oracle_price_data, + state, + now, + slot, + Some(DriftAction::Liquidate), + )?; + + let oracle_price = if market.status == MarketStatus::Settlement { + market.expiry_price + } else { + oracle_price_data.price + }; + + 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, + })?; + } + + // check if user exited liquidation territory + let intermediate_margin_calculation = if !canceled_order_ids.is_empty() || lp_shares > 0 { + 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))?, + )?; + + let initial_margin_shortage = margin_calculation.margin_shortage()?; + let new_margin_shortage = intermediate_margin_calculation.margin_shortage()?; + + margin_freed = initial_margin_shortage + .saturating_sub(new_margin_shortage) + .cast::()?; + user.increment_margin_freed(margin_freed)?; + + if intermediate_margin_calculation.can_exit_liquidation()? { + 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, + bankrupt: user.is_bankrupt(), + canceled_order_ids, + margin_freed, + liquidate_perp: LiquidatePerpRecord { + market_index, + oracle_price, + lp_shares, + ..LiquidatePerpRecord::default() + }, + ..LiquidationRecord::default() + }); + + user.exit_liquidation(); + return Ok(()); + } + + intermediate_margin_calculation + } else { + margin_calculation + }; + + if user.perp_positions[position_index].base_asset_amount == 0 { + msg!("User has no base asset amount"); + return Ok(()); + } + + let oracle_price_too_divergent = is_oracle_too_divergent_with_twap_5min( + oracle_price, + perp_market_map + .get_ref(&market_index)? + .amm + .historical_oracle_data + .last_oracle_price_twap_5min, + state + .oracle_guard_rails + .max_oracle_twap_5min_percent_divergence() + .cast()?, + )?; + + validate!(!oracle_price_too_divergent, ErrorCode::PriceBandsBreached)?; + + let user_base_asset_amount = user.perp_positions[position_index] + .base_asset_amount + .unsigned_abs(); + + let margin_ratio = perp_market_map.get_ref(&market_index)?.get_margin_ratio( + user_base_asset_amount.cast()?, + MarginRequirementType::Maintenance, + )?; + + let margin_ratio_with_buffer = margin_ratio.safe_add(liquidation_margin_buffer_ratio)?; + + let margin_shortage = intermediate_margin_calculation.margin_shortage()?; + + let market = perp_market_map.get_ref(&market_index)?; + let quote_spot_market = spot_market_map.get_ref(&market.quote_spot_market_index)?; + let quote_oracle_price = oracle_map.get_price_data("e_spot_market.oracle)?.price; + let liquidator_fee = market.liquidator_fee; + let if_liquidation_fee = calculate_perp_if_fee( + intermediate_margin_calculation.tracked_market_margin_shortage(margin_shortage)?, + user_base_asset_amount, + margin_ratio_with_buffer, + liquidator_fee, + oracle_price, + quote_oracle_price, + market.if_liquidation_fee, + )?; + let base_asset_amount_to_cover_margin_shortage = standardize_base_asset_amount_ceil( + calculate_base_asset_amount_to_cover_margin_shortage( + margin_shortage, + margin_ratio_with_buffer, + liquidator_fee, + if_liquidation_fee, + oracle_price, + quote_oracle_price, + )?, + market.amm.order_step_size, + )?; + drop(market); + drop(quote_spot_market); + + let max_pct_allowed = calculate_max_pct_to_liquidate( + &user, + margin_shortage, + slot, + initial_pct_to_liquidate, + liquidation_duration, + )?; + let max_base_asset_amount_allowed_to_be_transferred = + base_asset_amount_to_cover_margin_shortage + .cast::()? + .saturating_mul(max_pct_allowed) + .safe_div(LIQUIDATION_PCT_PRECISION)? + .cast::()?; + + if max_base_asset_amount_allowed_to_be_transferred == 0 { + msg!("max_base_asset_amount_allowed_to_be_transferred == 0"); + return Ok(()); + } + + let base_asset_value = + calculate_base_asset_value_with_oracle_price(user_base_asset_amount.cast()?, oracle_price)? + .cast::()?; + + // if position is less than $50, liquidator can liq all of it + let min_base_asset_amount = if base_asset_value > 50 * QUOTE_PRECISION_U64 { + 0_u64 + } else { + user_base_asset_amount + }; + + let base_asset_amount = user_base_asset_amount + .min(max_base_asset_amount_allowed_to_be_transferred.max(min_base_asset_amount)); + let base_asset_amount = standardize_base_asset_amount_ceil( + base_asset_amount, + perp_market_map.get_ref(&market_index)?.amm.order_step_size, + )?; + + let existing_direction = user.perp_positions[position_index].get_direction(); + + let order_params = get_liquidation_order_params( + market_index, + existing_direction, + base_asset_amount, + oracle_price, + liquidator_fee, + )?; + + let order_id = user.next_order_id; + let fill_record_id = perp_market_map.get_ref(&market_index)?.next_fill_record_id; + place_perp_order( + state, + &mut user, + *user_key, + perp_market_map, + spot_market_map, + oracle_map, + &clock, + order_params, + PlaceOrderOptions::default().explanation(OrderActionExplanation::Liquidation), + )?; + + drop(user); + drop(liquidator); + + let (fill_base_asset_amount, fill_quote_asset_amount) = fill_perp_order( + order_id, + state, + user_loader, + user_stats_loader, + spot_market_map, + perp_market_map, + oracle_map, + liquidator_loader, + liquidator_stats_loader, + makers_and_referrer, + makers_and_referrer_stats, + None, + &clock, + FillMode::Liquidation, + )?; + + let mut user = load_mut!(user_loader)?; + + if let Ok(order_index) = user.get_order_index(order_id) { + cancel_order( + order_index, + &mut user, + user_key, + perp_market_map, + spot_market_map, + oracle_map, + clock.unix_timestamp, + clock.slot, + OrderActionExplanation::None, + Some(liquidator_key), + 0, + false, + )?; + } + + // no fill + if fill_base_asset_amount == 0 { + return Err(ErrorCode::LiquidationOrderFailedToFill); + } + + let if_fee = -fill_quote_asset_amount + .cast::()? + .safe_mul(if_liquidation_fee.cast()?)? + .safe_div(LIQUIDATION_FEE_PRECISION_U128)? + .cast::()?; + + { + let mut market = perp_market_map.get_ref_mut(&market_index)?; + + let user_position = user.get_perp_position_mut(market_index)?; + update_quote_asset_and_break_even_amount(user_position, &mut market, if_fee)?; + + market.amm.total_liquidation_fee = market + .amm + .total_liquidation_fee + .safe_add(if_fee.unsigned_abs().cast()?)?; + } + + let (margin_freed_for_perp_position, margin_calculation_after) = calculate_margin_freed( + &user, + perp_market_map, + spot_market_map, + oracle_map, + liquidation_margin_buffer_ratio, + margin_shortage, + )?; + + margin_freed = margin_freed.safe_add(margin_freed_for_perp_position)?; + user.increment_margin_freed(margin_freed_for_perp_position)?; + + if margin_calculation_after.meets_margin_requirement() { + user.exit_liquidation(); + } else if is_user_bankrupt(&user) { + user.enter_bankruptcy(); + } + + let user_position_delta = get_position_delta_for_fill( + fill_base_asset_amount, + fill_quote_asset_amount, + existing_direction, + )?; + + 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, + bankrupt: user.is_bankrupt(), + canceled_order_ids, + margin_freed, + liquidate_perp: LiquidatePerpRecord { + market_index, + oracle_price, + base_asset_amount: user_position_delta.base_asset_amount, + quote_asset_amount: user_position_delta.quote_asset_amount, + lp_shares, + user_order_id: order_id, + liquidator_order_id: 0, + fill_record_id, + liquidator_fee: 0, + if_fee: if_fee.abs().cast()?, + }, + ..LiquidationRecord::default() + }); + + Ok(()) +} + pub fn liquidate_spot( asset_market_index: u16, liability_market_index: u16, @@ -1131,7 +1582,7 @@ pub fn liquidate_spot( )?; } - let margin_freed_from_liability = calculate_margin_freed( + let (margin_freed_from_liability, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -1610,7 +2061,7 @@ pub fn liquidate_borrow_for_perp_pnl( update_quote_asset_amount(user_position, &mut market, -pnl_transfer.cast()?)?; } - let margin_freed_from_liability = calculate_margin_freed( + let (margin_freed_from_liability, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -2105,7 +2556,7 @@ pub fn liquidate_perp_pnl_for_deposit( update_quote_asset_amount(user_position, &mut perp_market, pnl_transfer.cast()?)?; } - let margin_freed_from_liability = calculate_margin_freed( + let (margin_freed_from_liability, _) = calculate_margin_freed( user, perp_market_map, spot_market_map, @@ -2546,7 +2997,7 @@ pub fn calculate_margin_freed( oracle_map: &mut OracleMap, liquidation_margin_buffer_ratio: u32, initial_margin_shortage: u128, -) -> DriftResult { +) -> DriftResult<(u64, MarginCalculation)> { let margin_calculation_after = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -2558,7 +3009,9 @@ pub fn calculate_margin_freed( let new_margin_shortage = margin_calculation_after.margin_shortage()?; - initial_margin_shortage + let margin_freed = initial_margin_shortage .saturating_sub(new_margin_shortage) - .cast::() + .cast::()?; + + Ok((margin_freed, margin_calculation_after)) } diff --git a/programs/drift/src/controller/liquidation/tests.rs b/programs/drift/src/controller/liquidation/tests.rs index 39b4c2365..936b76ed3 100644 --- a/programs/drift/src/controller/liquidation/tests.rs +++ b/programs/drift/src/controller/liquidation/tests.rs @@ -2199,6 +2199,802 @@ pub mod liquidate_perp { } } +pub mod liquidate_perp_with_fill { + + use crate::state::state::State; + use std::str::FromStr; + + use anchor_lang::prelude::AccountLoader; + use anchor_lang::Owner; + use solana_program::clock::Clock; + use solana_program::pubkey::Pubkey; + + use crate::controller::liquidation::liquidate_perp_with_fill; + use crate::controller::position::PositionDirection; + use crate::create_anchor_account_info; + + use crate::math::constants::{ + AMM_RESERVE_PRECISION, BASE_PRECISION_I64, BASE_PRECISION_U64, LIQUIDATION_FEE_PRECISION, + LIQUIDATION_PCT_PRECISION, PEG_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + QUOTE_PRECISION_I64, SPOT_BALANCE_PRECISION_U64, 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::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{ + Order, OrderStatus, OrderType, PerpPosition, SpotPosition, User, UserStats, + }; + use crate::state::user_map::{UserMap, UserStatsMap}; + 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_liquidate_perp_with_fill_long() { + let now = 0_i64; + let slot = 100_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, + terminal_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, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + 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 { + 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 user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + 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, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let maker_key = Pubkey::new_unique(); + let maker_authority = Pubkey::new_unique(); + let mut maker = User { + authority: maker_authority, + orders: get_orders(Order { + status: OrderStatus::Open, + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64 / 2, + price: 100 * PRICE_PRECISION_U64, + slot: slot - 1, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64 / 2, + ..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() + }; + create_anchor_account_info!(maker, &maker_key, User, maker_account_info); + let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + + let mut maker_stats = UserStats { + authority: maker_authority, + ..UserStats::default() + }; + create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); + let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &makers_and_referrers, + &maker_and_referrer_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, 640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -64396000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let maker = makers_and_referrers.get_ref(&maker_key).unwrap(); + assert_eq!(maker.perp_positions[0].base_asset_amount, 360000000); + assert_eq!(maker.perp_positions[0].quote_asset_amount, -35992800); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3600); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 360000); + } + + #[test] + pub fn successful_liquidate_perp_with_fill_short() { + let now = 0_i64; + let slot = 100_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, + terminal_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, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + 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 { + 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 user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + 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, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let maker_key = Pubkey::new_unique(); + let maker_authority = Pubkey::new_unique(); + let mut maker = User { + authority: maker_authority, + orders: get_orders(Order { + status: OrderStatus::Open, + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64 / 2, + price: 100 * PRICE_PRECISION_U64, + slot: slot - 1, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_asks: -BASE_PRECISION_I64 / 2, + ..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() + }; + create_anchor_account_info!(maker, &maker_key, User, maker_account_info); + let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + + let mut maker_stats = UserStats { + authority: maker_authority, + ..UserStats::default() + }; + create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); + let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &makers_and_referrers, + &maker_and_referrer_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, -640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, 63604000); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let maker = makers_and_referrers.get_ref(&maker_key).unwrap(); + assert_eq!(maker.perp_positions[0].base_asset_amount, -360000000); + assert_eq!(maker.perp_positions[0].quote_asset_amount, 36007200); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3600); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 360000); + } + + #[test] + pub fn successful_liquidate_perp_with_fill_long_with_amm() { + let now = 0_i64; + let slot = 100_u64; + + let mut oracle_price = get_pyth_price(100, 6); + oracle_price.curr_slot = slot; + oracle_price.valid_slot = slot; + 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, + terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * 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, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + 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() + }; + market.amm.max_fill_reserve_fraction = 1; + market.amm.max_base_asset_reserve = u64::MAX as u128; + market.amm.min_base_asset_reserve = 0; + 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 user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + 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, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &UserMap::empty(), + &UserStatsMap::empty(), + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, 640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, -64523715); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3587); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 358708); + } + + #[test] + pub fn successful_liquidate_perp_with_fill_short_with_amm() { + let now = 0_i64; + let slot = 100_u64; + + let mut oracle_price = get_pyth_price(100, 6); + oracle_price.curr_slot = slot; + oracle_price.valid_slot = slot; + 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, + terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * 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, + order_tick_size: 1, + quote_asset_amount: -150 * QUOTE_PRECISION_I128, + base_asset_amount_with_amm: 0, + 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() + }; + market.amm.max_fill_reserve_fraction = 1; + market.amm.max_base_asset_reserve = u64::MAX as u128; + market.amm.min_base_asset_reserve = 0; + 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 user_key = Pubkey::new_unique(); + let liquidator_key = Pubkey::new_unique(); + + 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, + open_orders: 0, + open_bids: 0, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 4 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + + ..User::default() + }; + + create_anchor_account_info!(user, &user_key, User, user_account_info); + let user_account_loader: AccountLoader = + AccountLoader::try_from(&user_account_info).unwrap(); + + let liquidator_authority = Pubkey::new_unique(); + let mut liquidator = User { + authority: liquidator_authority, + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 50 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }), + ..User::default() + }; + + create_anchor_account_info!(liquidator, &liquidator_key, User, liquidator_account_info); + let liquidator_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_account_info).unwrap(); + + let mut user_stats = UserStats::default(); + + create_anchor_account_info!(user_stats, UserStats, user_stats_account_info); + let user_stats_account_loader: AccountLoader = + AccountLoader::try_from(&user_stats_account_info).unwrap(); + + let mut liquidator_stats = UserStats::default(); + + create_anchor_account_info!(liquidator_stats, UserStats, liquidator_stats_account_info); + let liquidator_stats_account_loader: AccountLoader = + AccountLoader::try_from(&liquidator_stats_account_info).unwrap(); + + let state = State { + liquidation_margin_buffer_ratio: 10, + initial_pct_to_liquidate: LIQUIDATION_PCT_PRECISION as u16, + liquidation_duration: 150, + ..Default::default() + }; + + let clock = Clock { + slot, + unix_timestamp: now, + ..Clock::default() + }; + + liquidate_perp_with_fill( + 0, + &user_account_loader, + &user_key, + &user_stats_account_loader, + &liquidator_account_loader, + &liquidator_key, + &liquidator_stats_account_loader, + &UserMap::empty(), + &UserStatsMap::empty(), + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + &state, + ) + .unwrap(); + + let user = user_account_loader.load().unwrap(); + assert_eq!(user.perp_positions[0].base_asset_amount, -640000000); + assert_eq!(user.perp_positions[0].quote_asset_amount, 63472500); + assert_eq!(user.perp_positions[0].open_orders, 0); + assert_eq!(user.perp_positions[0].open_bids, 0); + + let liquidator = liquidator_account_loader.load().unwrap(); + assert_eq!(liquidator.perp_positions[0].base_asset_amount, 0); + assert_eq!(liquidator.perp_positions[0].quote_asset_amount, 3613); + + let market_after = perp_market_map.get_ref(&0).unwrap(); + assert_eq!(market_after.amm.total_liquidation_fee, 361300); + } +} + pub mod liquidate_spot { use crate::state::state::State; use std::ops::Deref; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index a58735d67..b97c9deaa 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -108,13 +108,15 @@ pub fn place_perp_order( let now = clock.unix_timestamp; let slot = clock.slot; - validate_user_not_being_liquidated( - user, - perp_market_map, - spot_market_map, - oracle_map, - state.liquidation_margin_buffer_ratio, - )?; + if !options.is_liquidation() { + validate_user_not_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + state.liquidation_margin_buffer_ratio, + )?; + } validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; @@ -211,7 +213,10 @@ pub fn place_perp_order( let oracle_price_data = oracle_map.get_price_data(&market.amm.oracle)?; // updates auction params for crossing limit orders w/out auction duration - params.update_perp_auction_params(market, oracle_price_data.price)?; + // dont modify if it's a liquidation + if !options.is_liquidation() { + params.update_perp_auction_params(market, oracle_price_data.price)?; + } let (auction_start_price, auction_end_price, auction_duration) = get_auction_params( ¶ms, @@ -314,7 +319,7 @@ pub fn place_perp_order( options.update_risk_increasing(risk_increasing); // when orders are placed in bulk, only need to check margin on last place - if options.enforce_margin_check { + if options.enforce_margin_check && !options.is_liquidation() { meets_place_order_margin_requirement( user, perp_market_map, @@ -884,7 +889,7 @@ pub fn fill_perp_order( jit_maker_order_id: Option, clock: &Clock, fill_mode: FillMode, -) -> DriftResult { +) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -948,20 +953,22 @@ pub fn fill_perp_order( if user.is_bankrupt() { msg!("user is bankrupt"); - return Ok(0); + return Ok((0, 0)); } - match validate_user_not_being_liquidated( - user, - perp_market_map, - spot_market_map, - oracle_map, - state.liquidation_margin_buffer_ratio, - ) { - Ok(_) => {} - Err(_) => { - msg!("user is being liquidated"); - return Ok(0); + if !fill_mode.is_liquidation() { + match validate_user_not_being_liquidated( + user, + perp_market_map, + spot_market_map, + oracle_map, + state.liquidation_margin_buffer_ratio, + ) { + Ok(_) => {} + Err(_) => { + msg!("user is being liquidated"); + return Ok((0, 0)); + } } } @@ -1042,13 +1049,18 @@ pub fn fill_perp_order( slot, )?; - let referrer_info = get_referrer_info( - user_stats, - &user_key, - makers_and_referrer, - makers_and_referrer_stats, - slot, - )?; + // no referrer bonus for liquidations + let referrer_info = if !fill_mode.is_liquidation() { + get_referrer_info( + user_stats, + &user_key, + makers_and_referrer, + makers_and_referrer_stats, + slot, + )? + } else { + None + }; let oracle_too_divergent_with_twap_5min = is_oracle_too_divergent_with_twap_5min( oracle_price, @@ -1064,10 +1076,17 @@ pub fn fill_perp_order( if let Some(filler) = filler.as_deref_mut() { filler.update_last_active_slot(slot); } - return Ok(0); + return Ok((0, 0)); } - validate_perp_fill_possible(state, user, order_index, slot, makers_and_referrer.0.len())?; + validate_perp_fill_possible( + state, + user, + order_index, + slot, + makers_and_referrer.0.len(), + fill_mode, + )?; let should_expire_order = should_expire_order_before_fill(user, order_index, now)?; @@ -1116,7 +1135,7 @@ pub fn fill_perp_order( false, )?; - return Ok(0); + return Ok((0, 0)); } let (base_asset_amount, quote_asset_amount) = fulfill_perp_order( @@ -1201,7 +1220,7 @@ pub fn fill_perp_order( } if base_asset_amount == 0 { - return Ok(0); + return Ok((base_asset_amount, quote_asset_amount)); } { @@ -1239,7 +1258,7 @@ pub fn fill_perp_order( user.update_last_active_slot(slot); - Ok(base_asset_amount) + Ok((base_asset_amount, quote_asset_amount)) } pub fn validate_market_within_price_band( @@ -1555,6 +1574,7 @@ fn fulfill_perp_order( amm_is_available, slot, min_auction_duration, + fill_mode, )? }; @@ -1621,6 +1641,7 @@ fn fulfill_perp_order( None, *maker_price, AMMLiquiditySplit::Shared, + fill_mode.is_liquidation(), )?; (fill_base_asset_amount, fill_quote_asset_amount) @@ -1663,6 +1684,7 @@ fn fulfill_perp_order( slot, fee_structure, oracle_map, + fill_mode.is_liquidation(), )?; if maker_fill_base_asset_amount != 0 { @@ -1703,43 +1725,45 @@ fn fulfill_perp_order( base_asset_amount )?; - // if the maker is long, the user sold so - let taker_base_asset_amount_delta = if maker_direction == PositionDirection::Long { - base_asset_amount as i64 - } else { - -(base_asset_amount as i64) - }; + if !fill_mode.is_liquidation() { + // if the maker is long, the user sold so + let taker_base_asset_amount_delta = if maker_direction == PositionDirection::Long { + base_asset_amount as i64 + } else { + -(base_asset_amount as i64) + }; - let taker_margin_calculation = - calculate_margin_requirement_and_total_collateral_and_liability_info( + let taker_margin_calculation = + calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + spot_market_map, + oracle_map, + MarginContext::standard(if user_order_position_decreasing { + MarginRequirementType::Maintenance + } else { + MarginRequirementType::Fill + }) + .fuel_perp_delta(market_index, taker_base_asset_amount_delta) + .fuel_numerator(user, now), + )?; + + user_stats.update_fuel_bonus( user, - perp_market_map, - spot_market_map, - oracle_map, - MarginContext::standard(if user_order_position_decreasing { - MarginRequirementType::Maintenance - } else { - MarginRequirementType::Fill - }) - .fuel_perp_delta(market_index, taker_base_asset_amount_delta) - .fuel_numerator(user, now), + taker_margin_calculation.fuel_deposits, + taker_margin_calculation.fuel_borrows, + taker_margin_calculation.fuel_positions, + now, )?; - user_stats.update_fuel_bonus( - user, - taker_margin_calculation.fuel_deposits, - taker_margin_calculation.fuel_borrows, - taker_margin_calculation.fuel_positions, - now, - )?; - - if !taker_margin_calculation.meets_margin_requirement() { - msg!( - "taker breached fill requirements (margin requirement {}) (total_collateral {})", - taker_margin_calculation.margin_requirement, - taker_margin_calculation.total_collateral - ); - return Err(ErrorCode::InsufficientCollateral); + if !taker_margin_calculation.meets_margin_requirement() { + msg!( + "taker breached fill requirements (margin requirement {}) (total_collateral {})", + taker_margin_calculation.margin_requirement, + taker_margin_calculation.total_collateral + ); + return Err(ErrorCode::InsufficientCollateral); + } } for (maker_key, maker_base_asset_amount_filled) in maker_fills { @@ -1875,6 +1899,7 @@ pub fn fulfill_perp_order_with_amm( override_base_asset_amount: Option, override_fill_price: Option, liquidity_split: AMMLiquiditySplit, + is_liquidation: bool, ) -> DriftResult<(u64, u64)> { let position_index = get_position_index(&user.perp_positions, market.market_index)?; let existing_base_asset_amount = user.perp_positions[position_index].base_asset_amount; @@ -2111,6 +2136,7 @@ pub fn fulfill_perp_order_with_amm( let fill_record_id = get_then_update_id!(market, next_fill_record_id); let order_action_explanation = match (override_base_asset_amount, override_fill_price) { + _ if is_liquidation => OrderActionExplanation::Liquidation, (Some(_), Some(_)) => liquidity_split.get_order_action_explanation(), _ => OrderActionExplanation::OrderFilledWithAMM, }; @@ -2204,6 +2230,7 @@ pub fn fulfill_perp_order_with_match( slot: u64, fee_structure: &FeeStructure, oracle_map: &mut OracleMap, + is_liquidation: bool, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2317,6 +2344,7 @@ pub fn fulfill_perp_order_with_match( Some(jit_base_asset_amount), Some(maker_price), // match the makers price amm_liquidity_split, + is_liquidation, )?; total_base_asset_amount = base_asset_amount_filled_by_amm; @@ -2526,7 +2554,9 @@ pub fn fulfill_perp_order_with_match( )?; let fill_record_id = get_then_update_id!(market, next_fill_record_id); - let order_action_explanation = if maker.orders[maker_order_index].is_jit_maker() { + let order_action_explanation = if is_liquidation { + OrderActionExplanation::Liquidation + } else if maker.orders[maker_order_index].is_jit_maker() { OrderActionExplanation::OrderFilledWithMatchJit } else { OrderActionExplanation::OrderFilledWithMatch diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 1d7094b5f..7113f67f7 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -130,6 +130,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -249,6 +250,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -368,6 +370,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -487,6 +490,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -606,6 +610,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -691,6 +696,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -777,6 +783,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -863,6 +870,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -949,6 +957,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1055,6 +1064,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1164,6 +1174,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1280,6 +1291,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1397,6 +1409,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1538,6 +1551,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1654,6 +1668,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -1772,6 +1787,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -1912,6 +1928,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -2045,6 +2062,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -2184,6 +2202,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut oracle_map, + false, ) .unwrap(); @@ -2311,6 +2330,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -2437,6 +2457,7 @@ pub mod fulfill_order_with_maker_order { slot, &fee_structure, &mut get_oracle_map(), + false, ) .unwrap(); @@ -5144,7 +5165,7 @@ pub mod fill_order { ..State::default() }; - let base_asset_amount = fill_perp_order( + let (base_asset_amount, _) = fill_perp_order( 1, &state, &user_account_loader, @@ -5352,7 +5373,7 @@ pub mod fill_order { ..State::default() }; - let base_asset_amount = fill_perp_order( + let (base_asset_amount, _) = fill_perp_order( 1, &state, &user_account_loader, @@ -5481,7 +5502,7 @@ pub mod fill_order { unix_timestamp: 11, }; - let base_asset_amount = fill_perp_order( + let (base_asset_amount, _) = fill_perp_order( 1, &state, &user_account_loader, diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 4ad92b081..62e8dbea1 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -571,6 +571,8 @@ pub enum ErrorCode { InvalidOpenbookV2Market, #[msg("Non zero transfer fee")] NonZeroTransferFee, + #[msg("Liquidation order failed to fill")] + LiquidationOrderFailedToFill, } #[macro_export] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index e5748cefa..abf5bc682 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -751,6 +751,60 @@ pub fn handle_liquidate_perp<'c: 'info, 'info>( Ok(()) } +#[access_control( +liq_not_paused(&ctx.accounts.state) +)] +pub fn handle_liquidate_perp_with_fill<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LiquidatePerp<'info>>, + market_index: u16, +) -> Result<()> { + let clock = Clock::get()?; + let state = &ctx.accounts.state; + + let user_key = ctx.accounts.user.key(); + let liquidator_key = ctx.accounts.liquidator.key(); + + validate!( + user_key != liquidator_key, + ErrorCode::UserCantLiquidateThemself + )?; + + 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, + &get_writable_perp_market_set(market_index), + &MarketSet::new(), + clock.slot, + Some(state.oracle_guard_rails), + )?; + + let (makers_and_referrer, makers_and_referrer_stats) = + load_user_maps(remaining_accounts_iter, true)?; + + controller::liquidation::liquidate_perp_with_fill( + market_index, + &ctx.accounts.user, + &user_key, + &ctx.accounts.user_stats, + &ctx.accounts.liquidator, + &liquidator_key, + &ctx.accounts.liquidator_stats, + &makers_and_referrer, + &makers_and_referrer_stats, + &perp_market_map, + &spot_market_map, + &mut oracle_map, + &clock, + state, + )?; + + Ok(()) +} + #[access_control( liq_not_paused(&ctx.accounts.state) )] diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 0320eb6ce..10a3bac39 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -412,6 +412,13 @@ pub mod drift { ) } + pub fn liquidate_perp_with_fill<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LiquidatePerp<'info>>, + market_index: u16, + ) -> Result<()> { + handle_liquidate_perp_with_fill(ctx, market_index) + } + pub fn liquidate_spot<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, LiquidateSpot<'info>>, asset_market_index: u16, diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index c24796d00..c9c5bb46a 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -8,6 +8,7 @@ use crate::state::oracle::OraclePriceData; use crate::state::user::{Order, OrderType}; use solana_program::msg; +use crate::state::fill_mode::FillMode; use crate::state::perp_market::PerpMarket; use crate::OrderParams; use std::cmp::min; @@ -207,8 +208,9 @@ pub fn is_amm_available_liquidity_source( order: &Order, min_auction_duration: u8, slot: u64, + fill_mode: FillMode, ) -> DriftResult { - is_auction_complete(order.slot, min_auction_duration, slot) + Ok(is_auction_complete(order.slot, min_auction_duration, slot)? || fill_mode.is_liquidation()) } pub fn calculate_auction_params_for_trigger_order( diff --git a/programs/drift/src/math/fulfillment.rs b/programs/drift/src/math/fulfillment.rs index 985b7c984..337aeedcd 100644 --- a/programs/drift/src/math/fulfillment.rs +++ b/programs/drift/src/math/fulfillment.rs @@ -4,6 +4,7 @@ use crate::math::auction::is_amm_available_liquidity_source; use crate::math::casting::Cast; use crate::math::matching::do_orders_cross; use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; use crate::state::perp_market::AMM; use crate::state::user::Order; @@ -22,6 +23,7 @@ pub fn determine_perp_fulfillment_methods( amm_is_available: bool, slot: u64, min_auction_duration: u8, + fill_mode: FillMode, ) -> DriftResult> { if order.post_only { return determine_perp_fulfillment_methods_for_maker( @@ -33,6 +35,7 @@ pub fn determine_perp_fulfillment_methods( amm_is_available, slot, min_auction_duration, + fill_mode, ); } @@ -40,7 +43,7 @@ pub fn determine_perp_fulfillment_methods( let can_fill_with_amm = amm_is_available && valid_oracle_price.is_some() - && is_amm_available_liquidity_source(order, min_auction_duration, slot)?; + && is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?; let maker_direction = order.direction.opposite(); @@ -104,12 +107,13 @@ fn determine_perp_fulfillment_methods_for_maker( amm_is_available: bool, slot: u64, min_auction_duration: u8, + fill_mode: FillMode, ) -> DriftResult> { let maker_direction = order.direction; let can_fill_with_amm = amm_is_available && valid_oracle_price.is_some() - && is_amm_available_liquidity_source(order, min_auction_duration, slot)?; + && is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?; if !can_fill_with_amm { return Ok(vec![]); diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index 436053da1..a70e0de3e 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -5,6 +5,7 @@ mod determine_perp_fulfillment_methods { PRICE_PRECISION_U64, }; use crate::math::fulfillment::determine_perp_fulfillment_methods; + use crate::state::fill_mode::FillMode; use crate::state::fulfillment::PerpFulfillmentMethod; use crate::state::oracle::HistoricalOracleData; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -65,6 +66,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -125,6 +127,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -197,6 +200,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -267,6 +271,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -342,6 +347,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -412,6 +418,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -484,6 +491,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -555,6 +563,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -625,6 +634,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -697,6 +707,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -760,6 +771,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -821,6 +833,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); @@ -882,6 +895,7 @@ mod determine_perp_fulfillment_methods { true, 0, 0, + FillMode::Fill, ) .unwrap(); diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index 99ef8ad40..26a15572f 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -17,8 +17,8 @@ use crate::state::perp_market::PerpMarket; 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::User; -use crate::{validate, BASE_PRECISION}; +use crate::state::user::{OrderType, User}; +use crate::{validate, MarketType, OrderParams, PositionDirection, BASE_PRECISION}; use solana_program::msg; #[cfg(test)] @@ -445,3 +445,44 @@ pub fn calculate_spot_if_fee( Ok(max_if_fee.min(implied_if_fee)) } + +pub fn get_liquidation_order_params( + market_index: u16, + existing_direction: PositionDirection, + base_asset_amount: u64, + oracle_price: i64, + liquidation_fee: u32, +) -> DriftResult { + let direction = existing_direction.opposite(); + + let oracle_price_u128 = oracle_price.abs().cast::()?; + let limit_price = match direction { + PositionDirection::Long => oracle_price_u128 + .safe_add( + oracle_price_u128 + .safe_mul(liquidation_fee.cast()?)? + .safe_div(LIQUIDATION_FEE_PRECISION_U128)?, + )? + .cast::()?, + PositionDirection::Short => oracle_price_u128 + .safe_sub( + oracle_price_u128 + .safe_mul(liquidation_fee.cast()?)? + .safe_div(LIQUIDATION_FEE_PRECISION_U128)?, + )? + .cast::()?, + }; + + let order_params = OrderParams { + market_index, + direction, + price: limit_price, + order_type: OrderType::Limit, + market_type: MarketType::Perp, + base_asset_amount, + reduce_only: true, + ..OrderParams::default() + }; + + Ok(order_params) +} diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 87fadc736..3966ac6d7 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -9,6 +9,7 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_amm_available_liquidity; use crate::math::auction::is_amm_available_liquidity_source; use crate::math::casting::Cast; +use crate::state::fill_mode::FillMode; use crate::{ load, math, FeeTier, State, BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, OPEN_ORDER_MARGIN_REQUIREMENT, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_U64, @@ -324,11 +325,13 @@ pub fn validate_perp_fill_possible( order_index: usize, slot: u64, num_makers: usize, + fill_mode: FillMode, ) -> DriftResult { let amm_available = is_amm_available_liquidity_source( &user.orders[order_index], state.min_perp_auction_duration, slot, + fill_mode, )?; if !amm_available && num_makers == 0 && user.orders[order_index].is_limit_order() { diff --git a/programs/drift/src/state/fill_mode.rs b/programs/drift/src/state/fill_mode.rs index 9bb7d09bb..94edc21b6 100644 --- a/programs/drift/src/state/fill_mode.rs +++ b/programs/drift/src/state/fill_mode.rs @@ -12,6 +12,7 @@ pub enum FillMode { Fill, PlaceAndMake, PlaceAndTake, + Liquidation, } impl FillMode { @@ -23,7 +24,7 @@ impl FillMode { tick_size: u64, ) -> DriftResult> { match self { - FillMode::Fill | FillMode::PlaceAndMake => { + FillMode::Fill | FillMode::PlaceAndMake | FillMode::Liquidation => { order.get_limit_price(valid_oracle_price, None, slot, tick_size) } FillMode::PlaceAndTake => { @@ -41,4 +42,8 @@ impl FillMode { } } } + + pub fn is_liquidation(&self) -> bool { + self == &FillMode::Liquidation + } } diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 82c858a3f..055eb4769 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -706,4 +706,8 @@ impl PlaceOrderOptions { self.explanation = explanation; self } + + pub fn is_liquidation(&self) -> bool { + self.explanation == OrderActionExplanation::Liquidation + } } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 5849168b1..c8db47a0f 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -5990,6 +5990,81 @@ export class DriftClient { ); } + public async liquidatePerpWithFill( + userAccountPublicKey: PublicKey, + userAccount: UserAccount, + marketIndex: number, + makerInfos: MakerInfo[], + txParams?: TxParams, + liquidatorSubAccountId?: number + ): Promise { + const { txSig, slot } = await this.sendTransaction( + await this.buildTransaction( + await this.getLiquidatePerpWithFillIx( + userAccountPublicKey, + userAccount, + marketIndex, + makerInfos, + liquidatorSubAccountId + ), + txParams + ), + [], + this.opts + ); + this.perpMarketLastSlotCache.set(marketIndex, slot); + return txSig; + } + + public async getLiquidatePerpWithFillIx( + userAccountPublicKey: PublicKey, + userAccount: UserAccount, + marketIndex: number, + makerInfos: MakerInfo[], + liquidatorSubAccountId?: number + ): Promise { + const userStatsPublicKey = getUserStatsAccountPublicKey( + this.program.programId, + userAccount.authority + ); + + const liquidator = await this.getUserAccountPublicKey( + liquidatorSubAccountId + ); + const liquidatorStatsPublicKey = this.getUserStatsAccountPublicKey(); + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(liquidatorSubAccountId), userAccount], + useMarketLastSlotCache: true, + writablePerpMarketIndexes: [marketIndex], + }); + + for (const makerInfo of makerInfos) { + remainingAccounts.push({ + pubkey: makerInfo.maker, + isSigner: false, + isWritable: true, + }); + remainingAccounts.push({ + pubkey: makerInfo.makerStats, + isSigner: false, + isWritable: true, + }); + } + + return await this.program.instruction.liquidatePerpWithFill(marketIndex, { + accounts: { + state: await this.getStatePublicKey(), + authority: this.wallet.publicKey, + user: userAccountPublicKey, + userStats: userStatsPublicKey, + liquidator, + liquidatorStats: liquidatorStatsPublicKey, + }, + remainingAccounts: remainingAccounts, + }); + } + public async liquidateSpot( userAccountPublicKey: PublicKey, userAccount: UserAccount, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 5fd6b97cb..3044050e8 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1675,6 +1675,47 @@ } ] }, + { + "name": "liquidatePerpWithFill", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "liquidator", + "isMut": true, + "isSigner": false + }, + { + "name": "liquidatorStats", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, { "name": "liquidateSpot", "accounts": [ @@ -3362,27 +3403,6 @@ } ] }, - { - "name": "initializePredictionMarket", - "accounts": [ - { - "name": "admin", - "isMut": false, - "isSigner": true - }, - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "perpMarket", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, { "name": "deleteInitializedPerpMarket", "accounts": [ @@ -9858,6 +9878,9 @@ }, { "name": "PlaceAndTake" + }, + { + "name": "Liquidation" } ] } @@ -10125,9 +10148,6 @@ }, { "name": "Future" - }, - { - "name": "Prediction" } ] } @@ -12797,9 +12817,14 @@ "code": 6282, "name": "NonZeroTransferFee", "msg": "Non zero transfer fee" + }, + { + "code": 6283, + "name": "LiquidationOrderFailedToFill", + "msg": "Liquidation order failed to fill" } ], "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index eb65c99dc..d68ff8b0e 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -30,6 +30,7 @@ test_files=( liquidateMaxLps.ts liquidatePerp.ts liquidatePerpAndLp.ts + liquidatePerpWithFill.ts liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index 16b84fcfb..ea34682ad 100644 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,7 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(openbookTest.ts) +test_files=(liquidatePerpWithFill.ts) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/liquidatePerpWithFill.ts b/tests/liquidatePerpWithFill.ts new file mode 100644 index 000000000..c67b115b2 --- /dev/null +++ b/tests/liquidatePerpWithFill.ts @@ -0,0 +1,336 @@ +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.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, 'init')); + } + + 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 + ); + }); +});