diff --git a/programs/openbook-v2/src/state/orderbook/book.rs b/programs/openbook-v2/src/state/orderbook/book.rs index 0a5dbbf3e..4d5e20037 100644 --- a/programs/openbook-v2/src/state/orderbook/book.rs +++ b/programs/openbook-v2/src/state/orderbook/book.rs @@ -162,18 +162,22 @@ impl<'a> Orderbook<'a> { if !side.is_price_within_limit(best_opposing_price, price_lots) { break; - } else if post_only { + } + if post_only { msg!("Order could not be placed due to PostOnly"); post_target = None; break; // return silently to not fail other instructions in tx - } else if limit == 0 { + } + if limit == 0 { msg!("Order matching limit reached"); post_target = None; break; } let max_match_by_quote = remaining_quote_lots / best_opposing_price; + // Do not post orders in the book due to bad pricing and negative spread if max_match_by_quote == 0 { + post_target = None; break; } diff --git a/programs/openbook-v2/tests/cases/test_take_order.rs b/programs/openbook-v2/tests/cases/test_take_order.rs index 1912ddcef..618264e43 100644 --- a/programs/openbook-v2/tests/cases/test_take_order.rs +++ b/programs/openbook-v2/tests/cases/test_take_order.rs @@ -288,3 +288,82 @@ async fn test_take_bid_order() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_negative_spread_ask() -> Result<(), TransportError> { + let TestInitialize { + context, + owner, + owner_token_0, + owner_token_1, + market, + market_base_vault, + market_quote_vault, + account_1, + account_2, + .. + } = TestContext::new_with_market(TestNewMarketInitialize { + quote_lot_size: 100, + base_lot_size: 1_000_000_000, + ..TestNewMarketInitialize::default() + }) + .await?; + let solana = &context.solana.clone(); + + send_tx( + solana, + PlaceOrderInstruction { + open_orders_account: account_1, + open_orders_admin: None, + market, + signer: owner, + user_token_account: owner_token_1, + market_vault: market_quote_vault, + side: Side::Bid, + price_lots: 10_000, // $1 + max_base_lots: 1000000, // wahtever + max_quote_lots_including_fees: 10_000, + client_order_id: 1, + expiry_timestamp: 0, + order_type: PlaceOrderType::Limit, + self_trade_behavior: SelfTradeBehavior::default(), + remainings: vec![], + }, + ) + .await + .unwrap(); + + // This order doesn't take any due max_quote_lots_including_fees but it's also don't post in on the book + send_tx( + solana, + PlaceOrderInstruction { + open_orders_account: account_2, + open_orders_admin: None, + market, + signer: owner, + user_token_account: owner_token_0, + market_vault: market_base_vault, + side: Side::Ask, + price_lots: 7_500, + max_base_lots: 1, + max_quote_lots_including_fees: 7_500, + client_order_id: 25, + expiry_timestamp: 0, + order_type: PlaceOrderType::Limit, + self_trade_behavior: SelfTradeBehavior::AbortTransaction, + remainings: vec![], + }, + ) + .await + .unwrap(); + + let position = solana + .get_account::(account_2) + .await + .position; + + assert_eq!(position.asks_base_lots, 0); + assert_eq!(position.bids_base_lots, 0); + + Ok(()) +}