diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e3d0cab..47759d71f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: if order reduces maker position, check maintenance margin requirement ([#714](https://github.com/drift-labs/protocol-v2/pull/714)) + ### Fixes ### Breaking diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 076afe1ab..b43b116e5 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1516,7 +1516,8 @@ fn fulfill_perp_order( let mut base_asset_amount = 0_u64; let mut quote_asset_amount = 0_u64; - let mut makers_filled: BTreeMap = BTreeMap::new(); + let mut maker_fills: BTreeMap = BTreeMap::new(); + let maker_direction = user.orders[user_order_index].direction.opposite(); for fulfillment_method in fulfillment_methods.iter() { if user.orders[user_order_index].status != OrderStatus::Open { break; @@ -1573,7 +1574,7 @@ fn fulfill_perp_order( Some(&maker), )?; - let (fill_base_asset_amount, fill_quote_asset_amount) = + let (fill_base_asset_amount, fill_quote_asset_amount, maker_fill_base_asset_amount) = fulfill_perp_order_with_match( market.deref_mut(), user, @@ -1598,8 +1599,13 @@ fn fulfill_perp_order( oracle_map, )?; - if fill_base_asset_amount != 0 { - makers_filled.insert(*maker_key, true); + if maker_fill_base_asset_amount != 0 { + update_maker_fills_map( + &mut maker_fills, + maker_key, + maker_direction, + maker_fill_base_asset_amount, + )?; } (fill_base_asset_amount, fill_quote_asset_amount) @@ -1621,6 +1627,16 @@ fn fulfill_perp_order( quote_asset_amount )?; + let total_maker_fill = maker_fills.values().sum::(); + + validate!( + total_maker_fill.unsigned_abs() <= base_asset_amount, + ErrorCode::DefaultError, + "invalid total maker fill {} total fill {}", + total_maker_fill, + base_asset_amount + )?; + let taker_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( user, @@ -1643,16 +1659,22 @@ fn fulfill_perp_order( return Err(ErrorCode::InsufficientCollateral); } - for (maker_key, _) in makers_filled { + for (maker_key, maker_base_asset_amount_filled) in maker_fills { let maker = makers_and_referrer.get_ref(&maker_key)?; + let margin_type = select_margin_type_for_perp_maker( + &maker, + maker_base_asset_amount_filled, + market_index, + )?; + let maker_margin_calculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &maker, perp_market_map, spot_market_map, oracle_map, - MarginContext::standard(MarginRequirementType::Fill), + MarginContext::standard(margin_type), )?; if !maker_margin_calculation.meets_margin_requirement() { @@ -1693,6 +1715,27 @@ fn get_referrer<'a>( Ok((Some(referrer), Some(referrer_stats))) } +#[inline(always)] +fn update_maker_fills_map( + map: &mut BTreeMap, + maker_key: &Pubkey, + maker_direction: PositionDirection, + fill: u64, +) -> DriftResult { + let signed_fill = match maker_direction { + PositionDirection::Long => fill.cast::()?, + PositionDirection::Short => -fill.cast::()?, + }; + + if let Some(maker_filled) = map.get_mut(maker_key) { + *maker_filled = maker_filled.safe_add(signed_fill)?; + } else { + map.insert(*maker_key, signed_fill); + } + + Ok(()) +} + fn determine_if_user_order_is_position_decreasing( user: &User, market_index: u16, @@ -2006,12 +2049,12 @@ pub fn fulfill_perp_order_with_match( slot: u64, fee_structure: &FeeStructure, oracle_map: &mut OracleMap, -) -> DriftResult<(u64, u64)> { +) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], &taker.orders[taker_order_index], ) { - return Ok((0_u64, 0_u64)); + return Ok((0_u64, 0_u64, 0_u64)); } let (bid_price, ask_price) = market.amm.bid_ask_price(market.amm.reserve_price()?)?; @@ -2060,7 +2103,7 @@ pub fn fulfill_perp_order_with_match( maker_price, taker_price ); - return Ok((0_u64, 0_u64)); + return Ok((0_u64, 0_u64, 0_u64)); } let (base_asset_amount, _) = calculate_fill_for_matched_orders( @@ -2072,7 +2115,7 @@ pub fn fulfill_perp_order_with_match( )?; if base_asset_amount == 0 { - return Ok((0_u64, 0_u64)); + return Ok((0_u64, 0_u64, 0_u64)); } let sanitize_clamp_denominator = market.get_sanitize_clamp_denominator()?; @@ -2133,17 +2176,18 @@ pub fn fulfill_perp_order_with_match( let taker_base_asset_amount = taker.orders[taker_order_index] .get_base_asset_amount_unfilled(Some(taker_existing_position))?; - let (base_asset_amount_fulfilled, quote_asset_amount) = calculate_fill_for_matched_orders( - maker_base_asset_amount, - maker_price, - taker_base_asset_amount, - PERP_DECIMALS, - maker_direction, - )?; + let (base_asset_amount_fulfilled_by_maker, quote_asset_amount) = + calculate_fill_for_matched_orders( + maker_base_asset_amount, + maker_price, + taker_base_asset_amount, + PERP_DECIMALS, + maker_direction, + )?; validate_fill_price( quote_asset_amount, - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, BASE_PRECISION_U64, taker_direction, taker_price, @@ -2152,14 +2196,15 @@ pub fn fulfill_perp_order_with_match( validate_fill_price( quote_asset_amount, - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, BASE_PRECISION_U64, maker_direction, maker_price, false, )?; - total_base_asset_amount = total_base_asset_amount.safe_add(base_asset_amount_fulfilled)?; + total_base_asset_amount = + total_base_asset_amount.safe_add(base_asset_amount_fulfilled_by_maker)?; total_quote_asset_amount = total_quote_asset_amount.safe_add(quote_asset_amount)?; let maker_position_index = get_position_index( @@ -2168,7 +2213,7 @@ pub fn fulfill_perp_order_with_match( )?; let maker_position_delta = get_position_delta_for_fill( - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, quote_asset_amount, maker.orders[maker_order_index].direction, )?; @@ -2192,7 +2237,7 @@ pub fn fulfill_perp_order_with_match( )?; let taker_position_delta = get_position_delta_for_fill( - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, quote_asset_amount, taker.orders[taker_order_index].direction, )?; @@ -2304,26 +2349,26 @@ pub fn fulfill_perp_order_with_match( update_order_after_fill( &mut taker.orders[taker_order_index], - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, quote_asset_amount, )?; decrease_open_bids_and_asks( &mut taker.perp_positions[taker_position_index], &taker.orders[taker_order_index].direction, - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, )?; update_order_after_fill( &mut maker.orders[maker_order_index], - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, quote_asset_amount, )?; decrease_open_bids_and_asks( &mut maker.perp_positions[maker_position_index], &maker.orders[maker_order_index].direction, - base_asset_amount_fulfilled, + base_asset_amount_fulfilled_by_maker, )?; let fill_record_id = get_then_update_id!(market, next_fill_record_id); @@ -2340,7 +2385,7 @@ pub fn fulfill_perp_order_with_match( Some(*filler_key), Some(fill_record_id), Some(filler_reward), - Some(base_asset_amount_fulfilled), + Some(base_asset_amount_fulfilled_by_maker), Some(quote_asset_amount), Some(taker_fee), Some(maker_rebate), @@ -2369,7 +2414,11 @@ pub fn fulfill_perp_order_with_match( market_position.open_orders -= 1; } - Ok((total_base_asset_amount, total_quote_asset_amount)) + Ok(( + total_base_asset_amount, + total_quote_asset_amount, + base_asset_amount_fulfilled_by_maker, + )) } pub fn update_order_after_fill( diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 72ef03961..c92050ec9 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -584,7 +584,7 @@ pub mod fulfill_order_with_maker_order { .get_limit_price(None, None, slot, market.amm.order_tick_size) .unwrap(); - let (base_asset_amount, _) = fulfill_perp_order_with_match( + let (base_asset_amount, _, _) = fulfill_perp_order_with_match( &mut market, &mut taker, &mut taker_stats, @@ -669,7 +669,7 @@ pub mod fulfill_order_with_maker_order { .get_limit_price(None, None, slot, market.amm.order_tick_size) .unwrap(); - let (base_asset_amount, _) = fulfill_perp_order_with_match( + let (base_asset_amount, _, _) = fulfill_perp_order_with_match( &mut market, &mut taker, &mut taker_stats, @@ -755,7 +755,7 @@ pub mod fulfill_order_with_maker_order { .get_limit_price(None, None, slot, market.amm.order_tick_size) .unwrap(); - let (base_asset_amount, _) = fulfill_perp_order_with_match( + let (base_asset_amount, _, _) = fulfill_perp_order_with_match( &mut market, &mut taker, &mut taker_stats, @@ -841,7 +841,7 @@ pub mod fulfill_order_with_maker_order { .get_limit_price(None, None, slot, market.amm.order_tick_size) .unwrap(); - let (base_asset_amount, _) = fulfill_perp_order_with_match( + let (base_asset_amount, _, _) = fulfill_perp_order_with_match( &mut market, &mut taker, &mut taker_stats, @@ -1516,7 +1516,7 @@ pub mod fulfill_order_with_maker_order { .get_limit_price(None, None, slot, market.amm.order_tick_size) .unwrap(); - let (base_asset_amount, _) = fulfill_perp_order_with_match( + let (base_asset_amount, _, _) = fulfill_perp_order_with_match( &mut market, &mut taker, &mut taker_stats, @@ -1632,7 +1632,7 @@ pub mod fulfill_order_with_maker_order { .get_limit_price(None, None, slot, market.amm.order_tick_size) .unwrap(); - let (base_asset_amount, _) = fulfill_perp_order_with_match( + let (base_asset_amount, _, _) = fulfill_perp_order_with_match( &mut market, &mut taker, &mut taker_stats, @@ -2488,7 +2488,9 @@ pub mod fulfill_order { PRICE_PRECISION_U64, QUOTE_PRECISION_I64, QUOTE_PRECISION_U64, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; + use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::state::fill_mode::FillMode; + use crate::state::margin_calculation::MarginContext; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::perp_market::{PerpMarket, AMM}; use crate::state::perp_market_map::PerpMarketMap; @@ -3776,6 +3778,194 @@ pub mod fulfill_order { assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); } + #[test] + fn maker_position_reducing_above_maintenance_check() { + 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: 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: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + number_of_users_with_base: 1, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); + let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); + + let mut taker = User { + orders: get_orders(Order { + market_index: 0, + status: OrderStatus::Open, + order_type: OrderType::Market, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + auction_start_price: 0, + auction_end_price: 100 * PRICE_PRECISION_I64, + auction_duration: 0, + price: 150 * PRICE_PRECISION_U64, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..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() + }; + + let maker_key = Pubkey::default(); + let maker_authority = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let mut maker = User { + authority: maker_authority, + orders: get_orders!(Order { + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: 2 * BASE_PRECISION_U64, + price: 100 * PRICE_PRECISION_U64, // .01 worse than amm + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: BASE_PRECISION_I64, + open_orders: 1, + open_asks: -2 * BASE_PRECISION_I64, + ..PerpPosition::default() + }), + spot_positions: get_spot_positions(SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 501 * SPOT_BALANCE_PRECISION_U64 / 100, + ..SpotPosition::default() + }), + ..User::default() + }; + create_anchor_account_info!(maker, User, maker_account_info); + let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + + let mut filler = User::default(); + + let fee_structure = get_fee_structure(); + + let (taker_key, _, filler_key) = get_user_keys(); + + let mut taker_stats = UserStats::default(); + + 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 mut filler_stats = UserStats::default(); + + let result = fulfill_perp_order( + &mut taker, + 0, + &taker_key, + &mut taker_stats, + &makers_and_referrers, + &maker_and_referrer_stats, + &[(maker_key, 0, 95 * PRICE_PRECISION_U64)], + &mut Some(&mut filler), + &filler_key, + &mut Some(&mut filler_stats), + None, + &spot_market_map, + &market_map, + &mut oracle_map, + &fee_structure, + 100 * PRICE_PRECISION_U64, + Some(market.amm.historical_oracle_data.last_oracle_price), + now, + slot, + 10, + true, + FillMode::Fill, + ); + + assert!(result.is_ok()); + + let margin_calc = calculate_margin_requirement_and_total_collateral_and_liability_info( + &maker, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::liquidation(0), + ) + .unwrap(); + + assert_eq!( + margin_calc.margin_requirement, + margin_calc.total_collateral as u128 + ); + } + // Add back if we check free collateral in fill again // #[test] // fn fulfill_with_negative_free_collateral() { @@ -10019,3 +10209,36 @@ pub mod update_trigger_order_params { assert!(err.is_err()); } } + +mod update_maker_fills_map { + use crate::controller::orders::update_maker_fills_map; + use crate::PositionDirection; + use solana_program::pubkey::Pubkey; + use std::collections::BTreeMap; + + #[test] + fn test() { + let mut map: BTreeMap = BTreeMap::new(); + + let maker_key = Pubkey::new_unique(); + let fill = 100; + let direction = PositionDirection::Long; + update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap(); + + assert_eq!(*map.get(&maker_key).unwrap(), fill as i64); + + update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap(); + + assert_eq!(*map.get(&maker_key).unwrap(), 2 * fill as i64); + + let maker_key = Pubkey::new_unique(); + let direction = PositionDirection::Short; + update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap(); + + assert_eq!(*map.get(&maker_key).unwrap(), -(fill as i64)); + + update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap(); + + assert_eq!(*map.get(&maker_key).unwrap(), -2 * fill as i64); + } +} diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index c4dbea8df..f66fdfc17 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -1247,3 +1247,26 @@ pub fn estimate_price_from_side(side: &Side, depth: u64) -> DriftResult DriftResult { + let position_after_fill = maker + .get_perp_position(market_index) + .map_or(0, |p| p.base_asset_amount); + let position_before = position_after_fill.safe_sub(base_asset_amount_filled)?; + + if position_after_fill == 0 { + return Ok(MarginRequirementType::Maintenance); + } + + if position_after_fill.signum() == position_before.signum() + && position_after_fill.abs() < position_before.abs() + { + return Ok(MarginRequirementType::Maintenance); + } + + Ok(MarginRequirementType::Fill) +} diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index e8c05e014..e61906b07 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -3481,3 +3481,111 @@ pub mod find_bids_and_asks_from_users { assert_eq!(asks, expected_asks); } } + +mod select_margin_type_for_perp_maker { + use crate::math::margin::MarginRequirementType; + use crate::math::orders::select_margin_type_for_perp_maker; + use crate::state::user::{PerpPosition, User}; + use crate::test_utils::get_positions; + + #[test] + fn test() { + let market_index = 1; + + // Long reduced position to 0 + let position_before = -100; + let base_asset_amount_filled = 100; + let user = User { + perp_positions: get_positions(PerpPosition { + market_index, + base_asset_amount: position_before + base_asset_amount_filled, + ..PerpPosition::default() + }), + ..User::default() + }; + let margin_type = + select_margin_type_for_perp_maker(&user, base_asset_amount_filled, market_index) + .unwrap(); + assert_eq!(margin_type, MarginRequirementType::Maintenance); + + // Short reduced position to 0 + let position_before = 100; + let base_asset_amount_filled = -100; + let user = User { + perp_positions: get_positions(PerpPosition { + market_index, + base_asset_amount: position_before + base_asset_amount_filled, + ..PerpPosition::default() + }), + ..User::default() + }; + let margin_type = + select_margin_type_for_perp_maker(&user, base_asset_amount_filled, market_index) + .unwrap(); + assert_eq!(margin_type, MarginRequirementType::Maintenance); + + // Long flipped short long + let position_before = -80; + let base_asset_amount_filled = 100; + let user = User { + perp_positions: get_positions(PerpPosition { + market_index, + base_asset_amount: position_before + base_asset_amount_filled, + ..PerpPosition::default() + }), + ..User::default() + }; + let margin_type = + select_margin_type_for_perp_maker(&user, base_asset_amount_filled, market_index) + .unwrap(); + assert_eq!(margin_type, MarginRequirementType::Fill); + + // Short flipped long short + let position_before = 80; + let base_asset_amount_filled = -100; + let user = User { + perp_positions: get_positions(PerpPosition { + market_index, + base_asset_amount: position_before + base_asset_amount_filled, + ..PerpPosition::default() + }), + ..User::default() + }; + let margin_type = + select_margin_type_for_perp_maker(&user, base_asset_amount_filled, market_index) + .unwrap(); + assert_eq!(margin_type, MarginRequirementType::Fill); + + // Long reduced short + let position_before = -100; + let base_asset_amount_filled = 50; + let user = User { + perp_positions: get_positions(PerpPosition { + market_index, + base_asset_amount: position_before + base_asset_amount_filled, + ..PerpPosition::default() + }), + ..User::default() + }; + let margin_type = + select_margin_type_for_perp_maker(&user, base_asset_amount_filled, market_index) + .unwrap(); + assert_eq!(margin_type, MarginRequirementType::Maintenance); + + // Short reduced long + let position_before = 100; + let base_asset_amount_filled = -50; + let user = User { + perp_positions: get_positions(PerpPosition { + market_index, + base_asset_amount: position_before + base_asset_amount_filled, + ..PerpPosition::default() + }), + ..User::default() + }; + let margin_type = + select_margin_type_for_perp_maker(&user, base_asset_amount_filled, market_index) + .unwrap(); + assert_eq!(margin_type, MarginRequirementType::Maintenance); + } +}