From e1b1d5fbd8fab200d5b0f68317d37adc8d8e1468 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 21 Dec 2022 21:41:38 -0500 Subject: [PATCH 1/6] program: use vamm price to gaurd against bad fill for limit order --- programs/drift/src/controller/orders.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 2872f7fcb..6d1137132 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1731,12 +1731,24 @@ pub fn fulfill_perp_order_with_match( let oracle_price = oracle_map.get_price_data(&market.amm.oracle)?.price; let taker_direction = taker.orders[taker_order_index].direction; let taker_fallback_price = get_fallback_price(&taker_direction, bid_price, ask_price); - let taker_price = taker.orders[taker_order_index].force_get_limit_price( + let mut taker_price = taker.orders[taker_order_index].force_get_limit_price( Some(oracle_price), Some(taker_fallback_price), slot, market.amm.order_tick_size, )?; + + // if the auction isn't complete, cant fill against vamm yet + // use the vamm price to guard against bad fill for taker + if taker.orders[taker_order_index].is_limit_order() + && !taker.orders[taker_order_index].is_auction_complete() + { + taker_price = match taker_direction { + PositionDirection::Long => taker_price.min(ask_price), + PositionDirection::Short => taker_price.max(bid_price), + }; + } + let taker_existing_position = taker .get_perp_position(market.market_index)? .base_asset_amount; From 7cb05b34578bf28f637609ec4f280ce014c0f3c6 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 21 Dec 2022 22:00:24 -0500 Subject: [PATCH 2/6] dlob tweak --- sdk/src/dlob/DLOB.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 29ea51de6..917308c85 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -26,6 +26,9 @@ import { UserMap, OrderRecord, OrderActionRecord, + isLimitOrder, + ZERO, + BN_MAX, } from '..'; import { PublicKey } from '@solana/web3.js'; import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..'; @@ -1009,7 +1012,9 @@ export class DLOB { marketIndex: number, slot: number, marketType: MarketType, - oraclePriceData: OraclePriceData + oraclePriceData: OraclePriceData, + fallbackAsk: BN | undefined, + fallbackBid: BN | undefined ): NodeToFill[] { const nodesToFill = new Array(); @@ -1047,6 +1052,33 @@ export class DLOB { bidNode ); + // extra guard against bad fills for perp limit orders where auction is incomplete + if ( + isVariant(takerNode.order.marketType, 'perp') && + isLimitOrder(takerNode.order) && + !isAuctionComplete(takerNode.order, slot) + ) { + let bidPrice: BN; + let askPrice: BN; + if (isVariant(takerNode.order.direction, 'long')) { + bidPrice = BN.min( + takerNode.getPrice(oraclePriceData, slot), + fallbackAsk || BN_MAX + ); + askPrice = makerNode.getPrice(oraclePriceData, slot); + } else { + bidPrice = makerNode.getPrice(oraclePriceData, slot); + askPrice = BN.max( + takerNode.getPrice(oraclePriceData, slot), + fallbackBid || ZERO + ); + } + + if (bidPrice.lt(askPrice)) { + continue; + } + } + const bidBaseRemaining = bidOrder.baseAssetAmount.sub( bidOrder.baseAssetAmountFilled ); From 73c79628547f2ae84fd90275c582e5f2a015aae0 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Wed, 21 Dec 2022 22:02:30 -0500 Subject: [PATCH 3/6] fix build --- programs/drift/src/controller/orders.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 6d1137132..eae6c23a3 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1741,7 +1741,7 @@ pub fn fulfill_perp_order_with_match( // if the auction isn't complete, cant fill against vamm yet // use the vamm price to guard against bad fill for taker if taker.orders[taker_order_index].is_limit_order() - && !taker.orders[taker_order_index].is_auction_complete() + && !taker.orders[taker_order_index].is_auction_complete(slot)? { taker_price = match taker_direction { PositionDirection::Long => taker_price.min(ask_price), From 6529e3040459eda5f0c8900711776f9b7e54a641 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 22 Dec 2022 16:16:50 -0500 Subject: [PATCH 4/6] tweak dlob logic --- sdk/src/dlob/DLOB.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 917308c85..d108d1f8e 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -26,7 +26,6 @@ import { UserMap, OrderRecord, OrderActionRecord, - isLimitOrder, ZERO, BN_MAX, } from '..'; @@ -443,7 +442,9 @@ export class DLOB { marketIndex, slot, marketType, - oraclePriceData + oraclePriceData, + fallbackAsk, + fallbackBid ); for (const crossingNode of crossingNodes) { @@ -1052,12 +1053,8 @@ export class DLOB { bidNode ); - // extra guard against bad fills for perp limit orders where auction is incomplete - if ( - isVariant(takerNode.order.marketType, 'perp') && - isLimitOrder(takerNode.order) && - !isAuctionComplete(takerNode.order, slot) - ) { + // extra guard against bad fills for limit orders where auction is incomplete + if (!isAuctionComplete(takerNode.order, slot)) { let bidPrice: BN; let askPrice: BN; if (isVariant(takerNode.order.direction, 'long')) { From c06c8e9eec42ab4c47ca9891a10320137367bb85 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 22 Dec 2022 16:31:34 -0500 Subject: [PATCH 5/6] add tests --- programs/drift/src/controller/orders.rs | 18 +- programs/drift/src/controller/orders/tests.rs | 196 ++++++++++++++++++ 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index eae6c23a3..45695305b 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1744,8 +1744,22 @@ pub fn fulfill_perp_order_with_match( && !taker.orders[taker_order_index].is_auction_complete(slot)? { taker_price = match taker_direction { - PositionDirection::Long => taker_price.min(ask_price), - PositionDirection::Short => taker_price.max(bid_price), + PositionDirection::Long => { + msg!( + "taker limit order auction incomplete. vamm ask {} taker price {}", + ask_price, + taker_price + ); + taker_price.min(ask_price) + } + PositionDirection::Short => { + msg!( + "taker limit order auction incomplete. vamm bid {} taker price {}", + bid_price, + taker_price + ); + taker_price.max(bid_price) + } }; } diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 7ae1a5551..407e731c5 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -2172,6 +2172,202 @@ pub mod fulfill_order_with_maker_order { assert_eq!(market.amm.total_fee_minus_distributions, 20000); assert_eq!(market.amm.net_revenue_since_last_funding, 20000); } + + #[test] + fn taker_limit_bid_fails_to_cross_because_of_vamm_guard() { + let now = 5_i64; + let slot = 5_u64; + + let mut maker = User { + orders: get_orders(Order { + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + price: 150 * PRICE_PRECISION_U64, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut taker = User { + orders: get_orders(Order { + market_index: 0, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + price: 150 * PRICE_PRECISION_U64, + auction_duration: 10, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket::default_test(); + market.amm.peg_multiplier = 100 * PEG_PRECISION; + market.amm.oracle = oracle_price_key; + + let fee_structure = get_fee_structure(); + let (maker_key, taker_key, filler_key) = get_user_keys(); + + let mut order_records = vec![]; + + let mut taker_stats = UserStats::default(); + let mut maker_stats = UserStats::default(); + + let oracle_price = 100 * PRICE_PRECISION_I64; + + let (base_asset_amount, _) = fulfill_perp_order_with_match( + &mut market, + &mut taker, + &mut taker_stats, + 0, + &taker_key, + &mut maker, + &mut Some(&mut maker_stats), + 0, + &maker_key, + &mut None, + &mut None, + &filler_key, + &mut None, + &mut None, + 0, + Some(oracle_price), + now, + slot, + &fee_structure, + &mut oracle_map, + &mut order_records, + ) + .unwrap(); + + assert_eq!(base_asset_amount, 0); + } + + #[test] + fn taker_limit_ask_fails_to_cross_because_of_vamm_guard() { + let now = 5_i64; + let slot = 5_u64; + + let mut maker = User { + orders: get_orders(Order { + market_index: 0, + post_only: true, + order_type: OrderType::Limit, + direction: PositionDirection::Long, + base_asset_amount: BASE_PRECISION_U64, + slot: 0, + price: 50 * PRICE_PRECISION_U64, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_asks: -BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut taker = User { + orders: get_orders(Order { + market_index: 0, + order_type: OrderType::Limit, + direction: PositionDirection::Short, + base_asset_amount: BASE_PRECISION_U64, + price: 50 * PRICE_PRECISION_U64, + auction_duration: 10, + ..Order::default() + }), + perp_positions: get_positions(PerpPosition { + market_index: 0, + open_orders: 1, + open_bids: BASE_PRECISION_I64, + ..PerpPosition::default() + }), + ..User::default() + }; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket::default_test(); + market.amm.peg_multiplier = 100 * PEG_PRECISION; + market.amm.oracle = oracle_price_key; + + let fee_structure = get_fee_structure(); + let (maker_key, taker_key, filler_key) = get_user_keys(); + + let mut order_records = vec![]; + + let mut taker_stats = UserStats::default(); + let mut maker_stats = UserStats::default(); + + let oracle_price = 100 * PRICE_PRECISION_I64; + + let (base_asset_amount, _) = fulfill_perp_order_with_match( + &mut market, + &mut taker, + &mut taker_stats, + 0, + &taker_key, + &mut maker, + &mut Some(&mut maker_stats), + 0, + &maker_key, + &mut None, + &mut None, + &filler_key, + &mut None, + &mut None, + 0, + Some(oracle_price), + now, + slot, + &fee_structure, + &mut oracle_map, + &mut order_records, + ) + .unwrap(); + + assert_eq!(base_asset_amount, 0); + } } pub mod fulfill_order { From 7bbb64fa8bcf29c4c23c858221a4e65386026b42 Mon Sep 17 00:00:00 2001 From: Chris Heaney Date: Thu, 22 Dec 2022 16:32:58 -0500 Subject: [PATCH 6/6] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d28b18c7..e9aa85abe 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: user vamm price to guard against bad fills for limit orders ([#304](https://github.com/drift-labs/protocol-v2/pull/304)) + ### Fixes ### Breaking