Skip to content

Commit

Permalink
program: user vamm price to guard against bad fills for limit orders (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
0xbigz and crispheaney committed Dec 22, 2022
1 parent 938c1f3 commit f98d539
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 27 additions & 1 deletion programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
196 changes: 196 additions & 0 deletions programs/drift/src/controller/orders/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 31 additions & 2 deletions sdk/src/dlob/DLOB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
UserMap,
OrderRecord,
OrderActionRecord,
ZERO,
BN_MAX,
} from '..';
import { PublicKey } from '@solana/web3.js';
import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..';
Expand Down Expand Up @@ -440,7 +442,9 @@ export class DLOB {
marketIndex,
slot,
marketType,
oraclePriceData
oraclePriceData,
fallbackAsk,
fallbackBid
);

for (const crossingNode of crossingNodes) {
Expand Down Expand Up @@ -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<NodeToFill>();

Expand Down Expand Up @@ -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
);
Expand Down

0 comments on commit f98d539

Please sign in to comment.