phoenix/program/processor/
new_order.rs

1use crate::{
2    program::{
3        dispatch_market::load_with_dispatch_mut,
4        error::{assert_with_msg, PhoenixError},
5        loaders::NewOrderContext,
6        status::MarketStatus,
7        token_utils::{maybe_invoke_deposit, maybe_invoke_withdraw},
8        MarketHeader, PhoenixMarketContext, PhoenixVaultContext,
9    },
10    quantities::{
11        BaseAtoms, BaseAtomsPerBaseLot, BaseLots, QuoteAtoms, QuoteAtomsPerQuoteLot, QuoteLots,
12        Ticks, WrapperU64,
13    },
14    state::{
15        decode_order_packet,
16        markets::{FIFOOrderId, FIFORestingOrder, MarketEvent, MarketWrapperMut},
17        OrderPacket, OrderPacketMetadata, Side,
18    },
19};
20use borsh::{BorshDeserialize, BorshSerialize};
21use itertools::Itertools;
22use solana_program::{
23    account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, log::sol_log_compute_units,
24    program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar,
25};
26use std::mem::size_of;
27
28#[derive(BorshDeserialize, BorshSerialize, Debug)]
29pub enum FailedMultipleLimitOrderBehavior {
30    /// Orders will never cross the spread. Instead they will be amended to the closest non-crossing price.
31    /// The entire transaction will fail if matching engine returns None for any order, which indicates an error.
32    ///
33    /// If an order has insufficient funds, the entire transaction will fail.
34    FailOnInsufficientFundsAndAmendOnCross,
35
36    /// If any order crosses the spread or has insufficient funds, the entire transaction will fail.
37    FailOnInsufficientFundsAndFailOnCross,
38
39    /// Orders will be skipped if the user has insufficient funds.
40    /// Crossing orders will be amended to the closest non-crossing price.
41    SkipOnInsufficientFundsAndAmendOnCross,
42
43    /// Orders will be skipped if the user has insufficient funds.
44    /// If any order crosses the spread, the entire transaction will fail.
45    SkipOnInsufficientFundsAndFailOnCross,
46}
47
48impl FailedMultipleLimitOrderBehavior {
49    pub fn should_fail_on_cross(&self) -> bool {
50        matches!(
51            self,
52            FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndFailOnCross
53                | FailedMultipleLimitOrderBehavior::SkipOnInsufficientFundsAndFailOnCross
54        )
55    }
56
57    pub fn should_skip_orders_with_insufficient_funds(&self) -> bool {
58        matches!(
59            self,
60            FailedMultipleLimitOrderBehavior::SkipOnInsufficientFundsAndAmendOnCross
61                | FailedMultipleLimitOrderBehavior::SkipOnInsufficientFundsAndFailOnCross
62        )
63    }
64}
65
66/// Struct to send a vector of bids and asks as PostOnly orders in a single packet.
67#[derive(BorshDeserialize, BorshSerialize, Debug)]
68pub struct MultipleOrderPacket {
69    /// Bids and asks are in the format (price in ticks, size in base lots)
70    pub bids: Vec<CondensedOrder>,
71    pub asks: Vec<CondensedOrder>,
72    pub client_order_id: Option<u128>,
73    pub failed_multiple_limit_order_behavior: FailedMultipleLimitOrderBehavior,
74}
75
76#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)]
77pub struct CondensedOrder {
78    pub price_in_ticks: u64,
79    pub size_in_base_lots: u64,
80    pub last_valid_slot: Option<u64>,
81    pub last_valid_unix_timestamp_in_seconds: Option<u64>,
82}
83
84impl CondensedOrder {
85    pub fn new_default(price_in_ticks: u64, size_in_base_lots: u64) -> Self {
86        CondensedOrder {
87            price_in_ticks,
88            size_in_base_lots,
89            last_valid_slot: None,
90            last_valid_unix_timestamp_in_seconds: None,
91        }
92    }
93}
94
95impl MultipleOrderPacket {
96    pub fn new(
97        bids: Vec<CondensedOrder>,
98        asks: Vec<CondensedOrder>,
99        client_order_id: Option<u128>,
100        reject_post_only: bool,
101    ) -> Self {
102        MultipleOrderPacket {
103            bids,
104            asks,
105            client_order_id,
106            failed_multiple_limit_order_behavior: if reject_post_only {
107                FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndFailOnCross
108            } else {
109                FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndAmendOnCross
110            },
111        }
112    }
113
114    pub fn new_default(bids: Vec<CondensedOrder>, asks: Vec<CondensedOrder>) -> Self {
115        MultipleOrderPacket {
116            bids,
117            asks,
118            client_order_id: None,
119            failed_multiple_limit_order_behavior:
120                FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndFailOnCross,
121        }
122    }
123
124    pub fn new_with_failure_behavior(
125        bids: Vec<CondensedOrder>,
126        asks: Vec<CondensedOrder>,
127        client_order_id: Option<u128>,
128        failed_multiple_limit_order_behavior: FailedMultipleLimitOrderBehavior,
129    ) -> Self {
130        MultipleOrderPacket {
131            bids,
132            asks,
133            client_order_id,
134            failed_multiple_limit_order_behavior,
135        }
136    }
137}
138
139/// This function performs an IOC or FOK order against the specified market.
140pub(crate) fn process_swap<'a, 'info>(
141    _program_id: &Pubkey,
142    market_context: &PhoenixMarketContext<'a, 'info>,
143    accounts: &'a [AccountInfo<'info>],
144    data: &[u8],
145    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
146) -> ProgramResult {
147    sol_log_compute_units();
148    let new_order_context = NewOrderContext::load_cross_only(market_context, accounts, false)?;
149    let mut order_packet = decode_order_packet(data).ok_or_else(|| {
150        phoenix_log!("Failed to decode order packet");
151        ProgramError::InvalidInstructionData
152    })?;
153    assert_with_msg(
154        new_order_context.seat_option.is_none(),
155        ProgramError::InvalidInstructionData,
156        "Too many accounts",
157    )?;
158    assert_with_msg(
159        order_packet.is_take_only(),
160        ProgramError::InvalidInstructionData,
161        "Order type must be IOC or FOK",
162    )?;
163    assert_with_msg(
164        !order_packet.no_deposit_or_withdrawal(),
165        ProgramError::InvalidInstructionData,
166        "Instruction does not allow using deposited funds",
167    )?;
168    let mut order_ids = vec![];
169    process_new_order(
170        new_order_context,
171        market_context,
172        &mut order_packet,
173        record_event_fn,
174        &mut order_ids,
175    )
176}
177
178/// This function performs an IOC or FOK order against the specified market
179/// using only the funds already available to the trader.
180/// Only users with sufficient funds and a "seat" on the market are authorized
181/// to perform this action.
182pub(crate) fn process_swap_with_free_funds<'a, 'info>(
183    _program_id: &Pubkey,
184    market_context: &PhoenixMarketContext<'a, 'info>,
185    accounts: &'a [AccountInfo<'info>],
186    data: &[u8],
187    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
188) -> ProgramResult {
189    let new_order_context = NewOrderContext::load_cross_only(market_context, accounts, true)?;
190    let mut order_packet = decode_order_packet(data).ok_or_else(|| {
191        phoenix_log!("Failed to decode order packet");
192        ProgramError::InvalidInstructionData
193    })?;
194    assert_with_msg(
195        new_order_context.seat_option.is_some(),
196        ProgramError::InvalidInstructionData,
197        "Missing seat for market maker",
198    )?;
199    assert_with_msg(
200        order_packet.is_take_only(),
201        ProgramError::InvalidInstructionData,
202        "Order type must be IOC or FOK",
203    )?;
204    assert_with_msg(
205        order_packet.no_deposit_or_withdrawal(),
206        ProgramError::InvalidInstructionData,
207        "Order must be set to use only deposited funds",
208    )?;
209    let mut order_ids = vec![];
210    process_new_order(
211        new_order_context,
212        market_context,
213        &mut order_packet,
214        record_event_fn,
215        &mut order_ids,
216    )
217}
218
219/// This function performs a Post-Only or Limit order against the specified market.
220/// Only users with a "seat" on the market are authorized to perform this action.
221pub(crate) fn process_place_limit_order<'a, 'info>(
222    _program_id: &Pubkey,
223    market_context: &PhoenixMarketContext<'a, 'info>,
224    accounts: &'a [AccountInfo<'info>],
225    data: &[u8],
226    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
227    order_ids: &mut Vec<FIFOOrderId>,
228) -> ProgramResult {
229    let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, false)?;
230    let mut order_packet = decode_order_packet(data).ok_or_else(|| {
231        phoenix_log!("Failed to decode order packet");
232        ProgramError::InvalidInstructionData
233    })?;
234    assert_with_msg(
235        new_order_context.seat_option.is_some(),
236        ProgramError::InvalidInstructionData,
237        "Missing seat for market maker",
238    )?;
239    assert_with_msg(
240        !order_packet.is_take_only(),
241        ProgramError::InvalidInstructionData,
242        "Order type must be Limit or PostOnly",
243    )?;
244    assert_with_msg(
245        !order_packet.no_deposit_or_withdrawal(),
246        ProgramError::InvalidInstructionData,
247        "Instruction does not allow using deposited funds",
248    )?;
249    process_new_order(
250        new_order_context,
251        market_context,
252        &mut order_packet,
253        record_event_fn,
254        order_ids,
255    )
256}
257
258/// This function performs a Post-Only or Limit order against the specified market
259/// using only the funds already available to the trader.
260/// Only users with sufficient funds and a "seat" on the market are authorized
261/// to perform this action.
262pub(crate) fn process_place_limit_order_with_free_funds<'a, 'info>(
263    _program_id: &Pubkey,
264    market_context: &PhoenixMarketContext<'a, 'info>,
265    accounts: &'a [AccountInfo<'info>],
266    data: &[u8],
267    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
268    order_ids: &mut Vec<FIFOOrderId>,
269) -> ProgramResult {
270    let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, true)?;
271    let mut order_packet = decode_order_packet(data).ok_or_else(|| {
272        phoenix_log!("Failed to decode order packet");
273        ProgramError::InvalidInstructionData
274    })?;
275    assert_with_msg(
276        new_order_context.seat_option.is_some(),
277        ProgramError::InvalidInstructionData,
278        "Missing seat for market maker",
279    )?;
280    assert_with_msg(
281        !order_packet.is_take_only(),
282        ProgramError::InvalidInstructionData,
283        "Order type must be Limit or PostOnly",
284    )?;
285    assert_with_msg(
286        order_packet.no_deposit_or_withdrawal(),
287        ProgramError::InvalidInstructionData,
288        "Order must be set to use only deposited funds",
289    )?;
290    process_new_order(
291        new_order_context,
292        market_context,
293        &mut order_packet,
294        record_event_fn,
295        order_ids,
296    )
297}
298
299/// This function places multiple Post-Only orders against the specified market.
300/// Only users with a "seat" on the market are authorized to perform this action.
301///
302/// Orders at the same price level will be merged.
303pub(crate) fn process_place_multiple_post_only_orders<'a, 'info>(
304    _program_id: &Pubkey,
305    market_context: &PhoenixMarketContext<'a, 'info>,
306    accounts: &'a [AccountInfo<'info>],
307    data: &[u8],
308    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
309    order_ids: &mut Vec<FIFOOrderId>,
310) -> ProgramResult {
311    let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, false)?;
312    let multiple_order_packet = MultipleOrderPacket::try_from_slice(data)?;
313    assert_with_msg(
314        new_order_context.seat_option.is_some(),
315        ProgramError::InvalidInstructionData,
316        "Missing seat for market maker",
317    )?;
318
319    process_multiple_new_orders(
320        new_order_context,
321        market_context,
322        multiple_order_packet,
323        record_event_fn,
324        order_ids,
325        false,
326    )
327}
328
329/// This function plcaces multiple Post-Only orders against the specified market
330/// using only the funds already available to the trader.
331/// Only users with sufficient funds and a "seat" on the market are authorized
332/// to perform this action.
333///
334/// Orders at the same price level will be merged.
335pub(crate) fn process_place_multiple_post_only_orders_with_free_funds<'a, 'info>(
336    _program_id: &Pubkey,
337    market_context: &PhoenixMarketContext<'a, 'info>,
338    accounts: &'a [AccountInfo<'info>],
339    data: &[u8],
340    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
341    order_ids: &mut Vec<FIFOOrderId>,
342) -> ProgramResult {
343    let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, true)?;
344    let multiple_order_packet = MultipleOrderPacket::try_from_slice(data)?;
345    assert_with_msg(
346        new_order_context.seat_option.is_some(),
347        ProgramError::InvalidInstructionData,
348        "Missing seat for market maker",
349    )?;
350    process_multiple_new_orders(
351        new_order_context,
352        market_context,
353        multiple_order_packet,
354        record_event_fn,
355        order_ids,
356        true,
357    )
358}
359
360fn process_new_order<'a, 'info>(
361    new_order_context: NewOrderContext<'a, 'info>,
362    market_context: &PhoenixMarketContext<'a, 'info>,
363    order_packet: &mut OrderPacket,
364    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
365    order_ids: &mut Vec<FIFOOrderId>,
366) -> ProgramResult {
367    let PhoenixMarketContext {
368        market_info,
369        signer: trader,
370    } = market_context;
371    let NewOrderContext { vault_context, .. } = new_order_context;
372    let (quote_lot_size, base_lot_size) = {
373        let header = market_info.get_header()?;
374        (header.get_quote_lot_size(), header.get_base_lot_size())
375    };
376
377    let side = order_packet.side();
378    let (
379        quote_atoms_to_withdraw,
380        quote_atoms_to_deposit,
381        base_atoms_to_withdraw,
382        base_atoms_to_deposit,
383    ) = {
384        let clock = Clock::get()?;
385        let mut get_clock_fn = || (clock.slot, clock.unix_timestamp as u64);
386        let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
387        let market_wrapper = load_with_dispatch_mut(&market_info.size_params, market_bytes)?;
388
389        // If the order should fail silently on insufficient funds, and the trader does not have
390        // sufficient funds for the order, return silently without modifying the book.
391        if order_packet.fail_silently_on_insufficient_funds() {
392            let (base_lots_available, quote_lots_available) = get_available_balances_for_trader(
393                &market_wrapper,
394                trader.key,
395                vault_context.as_ref(),
396                base_lot_size,
397                quote_lot_size,
398            )?;
399            if !order_packet_has_sufficient_funds(
400                &market_wrapper,
401                order_packet,
402                base_lots_available,
403                quote_lots_available,
404            ) {
405                return Ok(());
406            }
407        }
408
409        let (order_id, matching_engine_response) = market_wrapper
410            .inner
411            .place_order(
412                trader.key,
413                *order_packet,
414                record_event_fn,
415                &mut get_clock_fn,
416            )
417            .ok_or(PhoenixError::NewOrderError)?;
418
419        if let Some(order_id) = order_id {
420            order_ids.push(order_id);
421        }
422
423        (
424            matching_engine_response.num_quote_lots_out * quote_lot_size,
425            matching_engine_response.get_deposit_amount_bid_in_quote_lots() * quote_lot_size,
426            matching_engine_response.num_base_lots_out * base_lot_size,
427            matching_engine_response.get_deposit_amount_ask_in_base_lots() * base_lot_size,
428        )
429    };
430    let header = market_info.get_header()?;
431    let quote_params = &header.quote_params;
432    let base_params = &header.base_params;
433
434    if quote_atoms_to_withdraw > QuoteAtoms::ZERO || base_atoms_to_withdraw > BaseAtoms::ZERO {
435        let status = MarketStatus::from(header.status);
436        assert_with_msg(
437            status.cross_allowed(),
438            ProgramError::InvalidAccountData,
439            &format!("Market is not active, market status is {}", status),
440        )?;
441    }
442    if !order_packet.no_deposit_or_withdrawal() {
443        if let Some(PhoenixVaultContext {
444            base_account,
445            quote_account,
446            base_vault,
447            quote_vault,
448            token_program,
449        }) = vault_context
450        {
451            match side {
452                Side::Bid => {
453                    maybe_invoke_withdraw(
454                        market_info.key,
455                        &base_params.mint_key,
456                        base_params.vault_bump as u8,
457                        base_atoms_to_withdraw.as_u64(),
458                        &token_program,
459                        &base_account,
460                        &base_vault,
461                    )?;
462                    maybe_invoke_deposit(
463                        quote_atoms_to_deposit.as_u64(),
464                        &token_program,
465                        &quote_account,
466                        &quote_vault,
467                        trader.as_ref(),
468                    )?;
469                }
470                Side::Ask => {
471                    maybe_invoke_withdraw(
472                        market_info.key,
473                        &quote_params.mint_key,
474                        quote_params.vault_bump as u8,
475                        quote_atoms_to_withdraw.as_u64(),
476                        &token_program,
477                        &quote_account,
478                        &quote_vault,
479                    )?;
480                    maybe_invoke_deposit(
481                        base_atoms_to_deposit.as_u64(),
482                        &token_program,
483                        &base_account,
484                        &base_vault,
485                        trader.as_ref(),
486                    )?;
487                }
488            }
489        } else {
490            // Should never be reached as the account loading logic should fail
491            phoenix_log!("WARNING: Vault context was not provided");
492            return Err(PhoenixError::NewOrderError.into());
493        }
494    } else if quote_atoms_to_deposit > QuoteAtoms::ZERO || base_atoms_to_deposit > BaseAtoms::ZERO {
495        // Should never execute as the matching engine should return None in this case
496        phoenix_log!("WARNING: Deposited amount of funds were insufficient to execute the order");
497        return Err(ProgramError::InsufficientFunds);
498    }
499
500    Ok(())
501}
502
503fn process_multiple_new_orders<'a, 'info>(
504    new_order_context: NewOrderContext<'a, 'info>,
505    market_context: &PhoenixMarketContext<'a, 'info>,
506    multiple_order_packet: MultipleOrderPacket,
507    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
508    order_ids: &mut Vec<FIFOOrderId>,
509    no_deposit: bool,
510) -> ProgramResult {
511    let PhoenixMarketContext {
512        market_info,
513        signer: trader,
514    } = market_context;
515    let NewOrderContext { vault_context, .. } = new_order_context;
516
517    let MultipleOrderPacket {
518        bids,
519        asks,
520        client_order_id,
521        failed_multiple_limit_order_behavior,
522    } = multiple_order_packet;
523
524    let highest_bid = bids
525        .iter()
526        .map(|bid| bid.price_in_ticks)
527        .max_by(|bid1, bid2| bid1.cmp(&bid2))
528        .unwrap_or(0);
529
530    let lowest_ask = asks
531        .iter()
532        .map(|ask| ask.price_in_ticks)
533        .min_by(|ask1, ask2| ask1.cmp(&ask2))
534        .unwrap_or(u64::MAX);
535
536    if highest_bid >= lowest_ask {
537        phoenix_log!("Invalid input. MultipleOrderPacket contains crossing bids and asks");
538        return Err(ProgramError::InvalidArgument.into());
539    }
540
541    let client_order_id = client_order_id.unwrap_or(0);
542    let mut quote_lots_to_deposit = QuoteLots::ZERO;
543    let mut base_lots_to_deposit = BaseLots::ZERO;
544    let (quote_lot_size, base_lot_size) = {
545        let header = market_info.get_header()?;
546        (header.get_quote_lot_size(), header.get_base_lot_size())
547    };
548
549    {
550        let clock = Clock::get()?;
551        let mut get_clock_fn = || (clock.slot, clock.unix_timestamp as u64);
552        let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
553        let market_wrapper = load_with_dispatch_mut(&market_info.size_params, market_bytes)?;
554
555        let (mut base_lots_available, mut quote_lots_available) =
556            get_available_balances_for_trader(
557                &market_wrapper,
558                trader.key,
559                vault_context.as_ref(),
560                base_lot_size,
561                quote_lot_size,
562            )?;
563
564        for (book_orders, side) in [(&bids, Side::Bid), (&asks, Side::Ask)].iter() {
565            for CondensedOrder {
566                price_in_ticks,
567                size_in_base_lots,
568                last_valid_slot,
569                last_valid_unix_timestamp_in_seconds,
570            } in book_orders
571                .iter()
572                .sorted_by(|o1, o2| o1.price_in_ticks.cmp(&o2.price_in_ticks))
573                .group_by(|o| {
574                    (
575                        o.price_in_ticks,
576                        o.last_valid_slot,
577                        o.last_valid_unix_timestamp_in_seconds,
578                    )
579                })
580                .into_iter()
581                .map(
582                    |(
583                        (price_in_ticks, last_valid_slot, last_valid_unix_timestamp_in_seconds),
584                        level,
585                    )| CondensedOrder {
586                        price_in_ticks,
587                        size_in_base_lots: level.fold(0, |acc, o| acc + o.size_in_base_lots),
588                        last_valid_slot,
589                        last_valid_unix_timestamp_in_seconds,
590                    },
591                )
592            {
593                let order_packet = OrderPacket::PostOnly {
594                    side: *side,
595                    price_in_ticks: Ticks::new(price_in_ticks),
596                    num_base_lots: BaseLots::new(size_in_base_lots),
597                    client_order_id,
598                    reject_post_only: failed_multiple_limit_order_behavior.should_fail_on_cross(),
599                    use_only_deposited_funds: no_deposit,
600                    last_valid_slot,
601                    last_valid_unix_timestamp_in_seconds,
602                    fail_silently_on_insufficient_funds: failed_multiple_limit_order_behavior
603                        .should_skip_orders_with_insufficient_funds(),
604                };
605
606                let matching_engine_response = {
607                    if failed_multiple_limit_order_behavior
608                        .should_skip_orders_with_insufficient_funds()
609                        && !order_packet_has_sufficient_funds(
610                            &market_wrapper,
611                            &order_packet,
612                            base_lots_available,
613                            quote_lots_available,
614                        )
615                    {
616                        // Skip this order if the trader does not have sufficient funds
617                        continue;
618                    }
619                    let (order_id, matching_engine_response) = market_wrapper
620                        .inner
621                        .place_order(trader.key, order_packet, record_event_fn, &mut get_clock_fn)
622                        .ok_or(PhoenixError::NewOrderError)?;
623                    if let Some(order_id) = order_id {
624                        order_ids.push(order_id);
625                    }
626                    matching_engine_response
627                };
628
629                let quote_lots_deposited =
630                    matching_engine_response.get_deposit_amount_bid_in_quote_lots();
631                let base_lots_deposited =
632                    matching_engine_response.get_deposit_amount_ask_in_base_lots();
633
634                if failed_multiple_limit_order_behavior.should_skip_orders_with_insufficient_funds()
635                {
636                    // Decrement the available funds by the amount that was deposited after each iteration
637                    // This should never underflow, but if it does, the program will panic and the transaction will fail
638                    quote_lots_available -=
639                        quote_lots_deposited + matching_engine_response.num_free_quote_lots_used;
640                    base_lots_available -=
641                        base_lots_deposited + matching_engine_response.num_free_base_lots_used;
642                }
643
644                quote_lots_to_deposit += quote_lots_deposited;
645                base_lots_to_deposit += base_lots_deposited;
646            }
647        }
648    }
649
650    if !no_deposit {
651        if let Some(PhoenixVaultContext {
652            base_account,
653            quote_account,
654            base_vault,
655            quote_vault,
656            token_program,
657        }) = vault_context
658        {
659            if !bids.is_empty() {
660                maybe_invoke_deposit(
661                    (quote_lots_to_deposit * quote_lot_size).as_u64(),
662                    &token_program,
663                    &quote_account,
664                    &quote_vault,
665                    trader.as_ref(),
666                )?;
667            } else {
668                assert_with_msg(
669                    quote_lots_to_deposit == QuoteLots::ZERO,
670                    PhoenixError::NewOrderError,
671                    "WARNING: Expected quote_lots_to_deposit to be zero",
672                )?;
673            }
674            if !asks.is_empty() {
675                maybe_invoke_deposit(
676                    (base_lots_to_deposit * base_lot_size).as_u64(),
677                    &token_program,
678                    &base_account,
679                    &base_vault,
680                    trader.as_ref(),
681                )?;
682            } else {
683                assert_with_msg(
684                    base_lots_to_deposit == BaseLots::ZERO,
685                    PhoenixError::NewOrderError,
686                    "WARNING: Expected base_lots_to_deposit to be zero",
687                )?;
688            }
689        } else {
690            // Should never be reached as the account loading logic should fail
691            phoenix_log!("WARNING: Vault context was not provided");
692            return Err(PhoenixError::NewOrderError.into());
693        }
694    } else if base_lots_to_deposit > BaseLots::ZERO || quote_lots_to_deposit > QuoteLots::ZERO {
695        phoenix_log!("Deposited amount of funds were insufficient to execute the order");
696        return Err(ProgramError::InsufficientFunds);
697    }
698
699    Ok(())
700}
701
702fn get_available_balances_for_trader<'a>(
703    market_wrapper: &MarketWrapperMut<'a, Pubkey, FIFOOrderId, FIFORestingOrder, OrderPacket>,
704    trader: &Pubkey,
705    vault_context: Option<&PhoenixVaultContext>,
706    base_lot_size: BaseAtomsPerBaseLot,
707    quote_lot_size: QuoteAtomsPerQuoteLot,
708) -> Result<(BaseLots, QuoteLots), ProgramError> {
709    let (base_lots_free, quote_lots_free) = {
710        let trader_index = market_wrapper
711            .inner
712            .get_trader_index(trader)
713            .ok_or(PhoenixError::TraderNotFound)?;
714        let trader_state = market_wrapper
715            .inner
716            .get_trader_state_from_index(trader_index);
717        (trader_state.base_lots_free, trader_state.quote_lots_free)
718    };
719    let (base_lots_available, quote_lots_available) = match vault_context.as_ref() {
720        None => (base_lots_free, quote_lots_free),
721        Some(ctx) => {
722            let quote_account_atoms = ctx.quote_account.amount().map(QuoteAtoms::new)?;
723            let base_account_atoms = ctx.base_account.amount().map(BaseAtoms::new)?;
724            (
725                base_lots_free + base_account_atoms.unchecked_div(base_lot_size),
726                quote_lots_free + quote_account_atoms.unchecked_div(quote_lot_size),
727            )
728        }
729    };
730    Ok((base_lots_available, quote_lots_available))
731}
732
733fn order_packet_has_sufficient_funds<'a>(
734    market_wrapper: &MarketWrapperMut<'a, Pubkey, FIFOOrderId, FIFORestingOrder, OrderPacket>,
735    order_packet: &OrderPacket,
736    base_lots_available: BaseLots,
737    quote_lots_available: QuoteLots,
738) -> bool {
739    match order_packet.side() {
740        Side::Ask => {
741            if base_lots_available < order_packet.num_base_lots() {
742                phoenix_log!(
743                    "Insufficient funds to place order: {} base lots available, {} base lots required",
744                    base_lots_available,
745                    order_packet.num_base_lots()
746                );
747                return false;
748            }
749        }
750        Side::Bid => {
751            let quote_lots_required = order_packet.get_price_in_ticks()
752                * market_wrapper.inner.get_tick_size()
753                * order_packet.num_base_lots()
754                / market_wrapper.inner.get_base_lots_per_base_unit();
755
756            if quote_lots_available < quote_lots_required {
757                phoenix_log!(
758                    "Insufficient funds to place order: {} quote lots available, {} quote lots required",
759                    quote_lots_available,
760                    quote_lots_required
761                );
762                return false;
763            }
764        }
765    }
766    true
767}