From f98d539a2e598c72b49445f7bc08ea95a12f3fb1 Mon Sep 17 00:00:00 2001 From: bigzPubkey Date: Thu, 22 Dec 2022 17:27:28 -0500 Subject: [PATCH] program: user vamm price to guard against bad fills for limit orders (#304) * program: use vamm price to gaurd against bad fill for limit order * dlob tweak * fix build * tweak dlob logic * add tests * update CHANGELOG Co-authored-by: Chris Heaney --- CHANGELOG.md | 2 + programs/drift/src/controller/orders.rs | 28 ++- programs/drift/src/controller/orders/tests.rs | 196 ++++++++++++++++++ sdk/src/dlob/DLOB.ts | 33 ++- 4 files changed, 256 insertions(+), 3 deletions(-) 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 diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 9f53905cf..d9fcde738 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1731,12 +1731,38 @@ 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(slot)? + { + taker_price = match taker_direction { + 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) + } + }; + } + let taker_existing_position = taker .get_perp_position(market.market_index)? .base_asset_amount; diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 837f7fe31..66b781cb4 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 { diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 29ea51de6..d108d1f8e 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -26,6 +26,8 @@ import { UserMap, OrderRecord, OrderActionRecord, + ZERO, + BN_MAX, } from '..'; import { PublicKey } from '@solana/web3.js'; import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..'; @@ -440,7 +442,9 @@ export class DLOB { marketIndex, slot, marketType, - oraclePriceData + oraclePriceData, + fallbackAsk, + fallbackBid ); for (const crossingNode of crossingNodes) { @@ -1009,7 +1013,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 +1053,29 @@ export class DLOB { bidNode ); + // 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')) { + 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 );