manifest/state/
market.rs

1#[cfg(feature = "certora")]
2use {
3    crate::certora::hooks::*, crate::certora::summaries::impact_base_atoms::impact_base_atoms,
4    hook_macro::cvt_hook_end, nondet::nondet,
5};
6
7use bytemuck::{Pod, Zeroable};
8use hypertree::{
9    get_helper, get_mut_helper, is_not_nil, trace, DataIndex, FreeList, FreeListNode, Get, PodBool,
10    RBNode, NIL,
11};
12#[cfg(not(feature = "certora"))]
13use hypertree::{
14    HyperTreeReadOperations, HyperTreeValueIteratorTrait, HyperTreeWriteOperations, RedBlackTree,
15    RedBlackTreeReadOnly,
16};
17use shank::ShankType;
18use solana_program::{entrypoint::ProgramResult, program_error::ProgramError, pubkey::Pubkey};
19use static_assertions::const_assert_eq;
20use std::mem::size_of;
21
22use crate::{
23    logs::{emit_stack, FillLog},
24    program::{batch_update::MarketDataTreeNodeType, ManifestError},
25    quantities::{BaseAtoms, GlobalAtoms, QuoteAtoms, QuoteAtomsPerBaseAtom, WrapperU64},
26    require,
27    state::{
28        utils::{assert_can_take, remove_from_global, try_to_move_global_tokens},
29        OrderType,
30    },
31    validation::{
32        get_vault_address, loaders::GlobalTradeAccounts, ManifestAccount, MintAccountInfo,
33    },
34};
35
36use super::{
37    claimed_seat::ClaimedSeat,
38    constants::{MARKET_BLOCK_SIZE, MARKET_FIXED_SIZE},
39    order_type_can_rest,
40    utils::{
41        assert_already_has_seat, assert_not_already_expired, can_back_order, get_now_slot,
42        try_to_add_to_global,
43    },
44    DerefOrBorrow, DerefOrBorrowMut, DynamicAccount, RestingOrder, MARKET_FIXED_DISCRIMINANT,
45    MARKET_FREE_LIST_BLOCK_SIZE, NO_EXPIRATION_LAST_VALID_SLOT,
46};
47
48#[path = "market_helpers.rs"]
49pub mod market_helpers;
50use market_helpers::*;
51
52#[path = "cvt_munge.rs"]
53#[cfg(feature = "certora")]
54mod cvt_munge;
55#[cfg(feature = "certora")]
56pub use cvt_munge::*;
57
58#[cfg(not(feature = "certora"))]
59mod helpers {
60    use super::*;
61    /// Read a `RBNode<ClaimedSeat>` in an array of data at a given index.
62    pub fn get_helper_seat(data: &[u8], index: DataIndex) -> &RBNode<ClaimedSeat> {
63        get_helper::<RBNode<ClaimedSeat>>(data, index)
64    }
65    /// Read a `RBNode<ClaimedSeat>` in an array of data at a given index.
66    pub fn get_mut_helper_seat(data: &mut [u8], index: DataIndex) -> &mut RBNode<ClaimedSeat> {
67        get_mut_helper::<RBNode<ClaimedSeat>>(data, index)
68    }
69
70    pub fn get_helper_order(data: &[u8], index: DataIndex) -> &RBNode<RestingOrder> {
71        get_helper::<RBNode<RestingOrder>>(data, index)
72    }
73    pub fn get_mut_helper_order(data: &mut [u8], index: DataIndex) -> &mut RBNode<RestingOrder> {
74        get_mut_helper::<RBNode<RestingOrder>>(data, index)
75    }
76
77    pub fn get_helper_bid_order(data: &[u8], index: DataIndex) -> &RBNode<RestingOrder> {
78        get_helper::<RBNode<RestingOrder>>(data, index)
79    }
80    pub fn get_mut_helper_bid_order(
81        data: &mut [u8],
82        index: DataIndex,
83    ) -> &mut RBNode<RestingOrder> {
84        get_mut_helper::<RBNode<RestingOrder>>(data, index)
85    }
86
87    pub fn get_helper_ask_order(data: &[u8], index: DataIndex) -> &RBNode<RestingOrder> {
88        get_helper::<RBNode<RestingOrder>>(data, index)
89    }
90    pub fn get_mut_helper_ask_order(
91        data: &mut [u8],
92        index: DataIndex,
93    ) -> &mut RBNode<RestingOrder> {
94        get_mut_helper::<RBNode<RestingOrder>>(data, index)
95    }
96}
97
98#[cfg(not(feature = "certora"))]
99pub use helpers::*;
100
101#[derive(Clone)]
102pub struct AddOrderToMarketArgs<'a, 'info> {
103    pub market: Pubkey,
104    pub trader_index: DataIndex,
105    pub num_base_atoms: BaseAtoms,
106    pub price: QuoteAtomsPerBaseAtom,
107    pub is_bid: bool,
108    pub last_valid_slot: u32,
109    pub order_type: OrderType,
110    pub global_trade_accounts_opts: &'a [Option<GlobalTradeAccounts<'a, 'info>>; 2],
111    pub current_slot: Option<u32>,
112}
113
114pub struct AddOrderToMarketResult {
115    pub order_sequence_number: u64,
116    pub order_index: DataIndex,
117    pub base_atoms_traded: BaseAtoms,
118    pub quote_atoms_traded: QuoteAtoms,
119}
120
121#[repr(C, packed)]
122#[derive(Default, Copy, Clone, Pod, Zeroable)]
123pub struct MarketUnusedFreeListPadding {
124    _padding: [u64; 9],
125    _padding2: [u8; 4],
126}
127// 4 bytes are for the free list, rest is payload.
128const_assert_eq!(
129    size_of::<MarketUnusedFreeListPadding>(),
130    MARKET_FREE_LIST_BLOCK_SIZE
131);
132// Does not need to align to word boundaries because does not deserialize.
133
134#[repr(C)]
135#[derive(Default, Copy, Clone, Zeroable, Pod, ShankType)]
136pub struct MarketFixed {
137    /// Discriminant for identifying this type of account.
138    pub discriminant: u64,
139
140    /// Version
141    version: u8,
142    base_mint_decimals: u8,
143    quote_mint_decimals: u8,
144    base_vault_bump: u8,
145    quote_vault_bump: u8,
146    _padding1: [u8; 3],
147
148    /// Base mint
149    base_mint: Pubkey,
150    /// Quote mint
151    quote_mint: Pubkey,
152
153    /// Base vault
154    base_vault: Pubkey,
155    /// Quote vault
156    quote_vault: Pubkey,
157
158    /// The sequence number of the next order.
159    order_sequence_number: u64,
160
161    /// Num bytes allocated as RestingOrder or ClaimedSeat or FreeList. Does not
162    /// include the fixed bytes.
163    num_bytes_allocated: u32,
164
165    /// Red-black tree root representing the bids in the order book.
166    bids_root_index: DataIndex,
167    bids_best_index: DataIndex,
168
169    /// Red-black tree root representing the asks in the order book.
170    asks_root_index: DataIndex,
171    asks_best_index: DataIndex,
172
173    /// Red-black tree root representing the seats
174    claimed_seats_root_index: DataIndex,
175
176    /// LinkedList representing all free blocks that could be used for ClaimedSeats or RestingOrders
177    free_list_head_index: DataIndex,
178
179    _padding2: [u32; 1],
180
181    /// Quote volume traded over lifetime, can overflow. This is for
182    /// informational and monitoring purposes only. This is not guaranteed to
183    /// be maintained. It does not secure any value in manifest.
184    /// Use at your own risk.
185    quote_volume: QuoteAtoms,
186
187    // These are not included in the normal usage because they are informational
188    // only and not worth the CU.
189    #[cfg(feature = "certora")]
190    /// Base tokens reserved for seats
191    withdrawable_base_atoms: BaseAtoms,
192    #[cfg(feature = "certora")]
193    /// Quote tokens reserved for seats
194    withdrawable_quote_atoms: QuoteAtoms,
195    #[cfg(feature = "certora")]
196    /// Base tokens reserved for non-global orders
197    pub orderbook_base_atoms: BaseAtoms,
198    #[cfg(feature = "certora")]
199    /// Quote tokens reserved for non-global orders
200    pub orderbook_quote_atoms: QuoteAtoms,
201    #[cfg(feature = "certora")]
202    _padding3: [u64; 4],
203
204    // Unused padding. Saved in case a later version wants to be backwards
205    // compatible. Also, it is nice to have the fixed size be a round number,
206    // 256 bytes.
207    #[cfg(not(feature = "certora"))]
208    _padding3: [u64; 8],
209}
210const_assert_eq!(
211    size_of::<MarketFixed>(),
212    8 +   // discriminant
213    1 +   // version
214    1 +   // base_mint_decimals
215    1 +   // quote_mint_decimals
216    1 +   // base_vault_bump
217    1 +   // quote_vault_bump
218    3 +   // padding
219    32 +  // base_mint
220    32 +  // quote_mint
221    32 +  // base_vault
222    32 +  // quote_vault
223    8 +   // order_sequence_number
224    4 +   // num_bytes_allocated
225    4 +   // bids_root_index
226    4 +   // bids_best_index
227    4 +   // asks_root_index
228    4 +   // asks_best_index
229    4 +   // claimed_seats_root_index
230    4 +   // claimed_seats_best_index
231    4 +   // free_list_head_index
232    8 +   // padding2
233    64 // padding4
234);
235const_assert_eq!(size_of::<MarketFixed>(), MARKET_FIXED_SIZE);
236const_assert_eq!(size_of::<MarketFixed>() % 8, 0);
237impl Get for MarketFixed {}
238
239impl MarketFixed {
240    pub fn new_empty(
241        base_mint: &MintAccountInfo,
242        quote_mint: &MintAccountInfo,
243        market_key: &Pubkey,
244    ) -> Self {
245        let (base_vault, base_vault_bump) = get_vault_address(market_key, base_mint.info.key);
246        let (quote_vault, quote_vault_bump) = get_vault_address(market_key, quote_mint.info.key);
247        MarketFixed {
248            discriminant: MARKET_FIXED_DISCRIMINANT,
249            version: 0,
250            base_mint_decimals: base_mint.mint.decimals,
251            quote_mint_decimals: quote_mint.mint.decimals,
252            base_vault_bump,
253            quote_vault_bump,
254            _padding1: [0; 3],
255            base_mint: *base_mint.info.key,
256            quote_mint: *quote_mint.info.key,
257            base_vault,
258            quote_vault,
259            order_sequence_number: 0,
260            num_bytes_allocated: 0,
261            bids_root_index: NIL,
262            bids_best_index: NIL,
263            asks_root_index: NIL,
264            asks_best_index: NIL,
265            claimed_seats_root_index: NIL,
266            #[cfg(not(feature = "certora"))]
267            free_list_head_index: NIL,
268            #[cfg(feature = "certora")]
269            // non NIL
270            free_list_head_index: 0,
271            _padding2: [0; 1],
272            quote_volume: QuoteAtoms::ZERO,
273            #[cfg(not(feature = "certora"))]
274            _padding3: [0; 8],
275            #[cfg(feature = "certora")]
276            withdrawable_base_atoms: BaseAtoms::new(0),
277            #[cfg(feature = "certora")]
278            withdrawable_quote_atoms: QuoteAtoms::new(0),
279            #[cfg(feature = "certora")]
280            orderbook_base_atoms: BaseAtoms::new(0),
281            #[cfg(feature = "certora")]
282            orderbook_quote_atoms: QuoteAtoms::new(0),
283            #[cfg(feature = "certora")]
284            _padding3: [0; 4],
285        }
286    }
287
288    #[cfg(feature = "certora")]
289    /** All fields are set to nondeterministic values except indexes to the tree **/
290    pub fn new_nondet() -> Self {
291        let claimed_seats_root_index = nondet();
292        cvt::cvt_assume!(claimed_seats_root_index == NIL);
293        MarketFixed {
294            discriminant: MARKET_FIXED_DISCRIMINANT,
295            version: 0,
296            base_mint_decimals: nondet(),
297            quote_mint_decimals: nondet(),
298            base_vault_bump: nondet(),
299            quote_vault_bump: nondet(),
300            _padding1: [0; 3],
301            base_mint: nondet(),
302            quote_mint: nondet(),
303            base_vault: nondet(),
304            quote_vault: nondet(),
305            order_sequence_number: nondet(),
306            num_bytes_allocated: nondet(),
307            bids_root_index: NIL,
308            bids_best_index: NIL,
309            asks_root_index: NIL,
310            asks_best_index: NIL,
311            claimed_seats_root_index,
312            free_list_head_index: 0,
313            _padding2: [0; 1],
314            quote_volume: QuoteAtoms::ZERO,
315            withdrawable_base_atoms: BaseAtoms::new(nondet()),
316            withdrawable_quote_atoms: QuoteAtoms::new(nondet()),
317            orderbook_base_atoms: BaseAtoms::new(nondet()),
318            orderbook_quote_atoms: QuoteAtoms::new(nondet()),
319            _padding3: [0; 4],
320        }
321    }
322
323    pub fn get_base_mint(&self) -> &Pubkey {
324        &self.base_mint
325    }
326    pub fn get_quote_mint(&self) -> &Pubkey {
327        &self.quote_mint
328    }
329    pub fn get_base_vault(&self) -> &Pubkey {
330        &self.base_vault
331    }
332    pub fn get_quote_vault(&self) -> &Pubkey {
333        &self.quote_vault
334    }
335    pub fn get_base_mint_decimals(&self) -> u8 {
336        self.base_mint_decimals
337    }
338    pub fn get_quote_mint_decimals(&self) -> u8 {
339        self.quote_mint_decimals
340    }
341    pub fn get_base_vault_bump(&self) -> u8 {
342        self.base_vault_bump
343    }
344    pub fn get_quote_vault_bump(&self) -> u8 {
345        self.quote_vault_bump
346    }
347    pub fn get_quote_volume(&self) -> QuoteAtoms {
348        self.quote_volume
349    }
350
351    // Used only in this file to construct iterator
352    pub(crate) fn get_bids_root_index(&self) -> DataIndex {
353        self.bids_root_index
354    }
355    pub(crate) fn get_asks_root_index(&self) -> DataIndex {
356        self.asks_root_index
357    }
358    pub(crate) fn get_bids_best_index(&self) -> DataIndex {
359        self.bids_best_index
360    }
361    pub(crate) fn get_asks_best_index(&self) -> DataIndex {
362        self.asks_best_index
363    }
364
365    #[cfg(feature = "certora")]
366    pub fn get_withdrawable_base_atoms(&self) -> BaseAtoms {
367        self.withdrawable_base_atoms
368    }
369    #[cfg(feature = "certora")]
370    pub fn get_withdrawable_quote_atoms(&self) -> QuoteAtoms {
371        self.withdrawable_quote_atoms
372    }
373    #[cfg(feature = "certora")]
374    pub fn get_orderbook_base_atoms(&self) -> BaseAtoms {
375        self.orderbook_base_atoms
376    }
377    #[cfg(feature = "certora")]
378    pub fn get_orderbook_quote_atoms(&self) -> QuoteAtoms {
379        self.orderbook_quote_atoms
380    }
381
382    // Used in benchmark
383    pub fn has_free_block(&self) -> bool {
384        self.free_list_head_index != NIL
385    }
386}
387
388impl ManifestAccount for MarketFixed {
389    fn verify_discriminant(&self) -> ProgramResult {
390        require!(
391            self.discriminant == MARKET_FIXED_DISCRIMINANT,
392            ProgramError::InvalidAccountData,
393            "Invalid market discriminant actual: {} expected: {}",
394            self.discriminant,
395            MARKET_FIXED_DISCRIMINANT
396        )?;
397        Ok(())
398    }
399}
400
401/// Fully owned Market, used in clients that can copy.
402pub type MarketValue = DynamicAccount<MarketFixed, Vec<u8>>;
403/// Full market reference type.
404pub type MarketRef<'a> = DynamicAccount<&'a MarketFixed, &'a [u8]>;
405/// Full market reference type.
406pub type MarketRefMut<'a> = DynamicAccount<&'a mut MarketFixed, &'a mut [u8]>;
407
408#[cfg(not(feature = "certora"))]
409mod types {
410    use super::*;
411    pub type ClaimedSeatTree<'a> = RedBlackTree<'a, ClaimedSeat>;
412    pub type ClaimedSeatTreeReadOnly<'a> = RedBlackTreeReadOnly<'a, ClaimedSeat>;
413    pub type Bookside<'a> = RedBlackTree<'a, RestingOrder>;
414    pub type BooksideReadOnly<'a> = RedBlackTreeReadOnly<'a, RestingOrder>;
415}
416#[cfg(not(feature = "certora"))]
417pub use types::*;
418
419#[cfg(all(feature = "certora"))]
420mod cvt_mock_types {
421    use super::*;
422    pub type ClaimedSeatTree<'a> = CvtClaimedSeatTree<'a>;
423    pub type ClaimedSeatTreeReadOnly<'a> = CvtClaimedSeatTreeReadOnly<'a>;
424    pub type Bookside<'a> = CvtBookside<'a>;
425    pub type BooksideReadOnly<'a> = CvtBooksideReadOnly<'a>;
426}
427#[cfg(feature = "certora")]
428pub use cvt_mock_types::*;
429
430// This generic impl covers MarketRef, MarketRefMut and other
431// DynamicAccount variants that allow read access.
432impl<Fixed: DerefOrBorrow<MarketFixed>, Dynamic: DerefOrBorrow<[u8]>>
433    DynamicAccount<Fixed, Dynamic>
434{
435    fn borrow_market(&self) -> MarketRef {
436        MarketRef {
437            fixed: self.fixed.deref_or_borrow(),
438            dynamic: self.dynamic.deref_or_borrow(),
439        }
440    }
441
442    pub fn get_base_mint(&self) -> &Pubkey {
443        let DynamicAccount { fixed, .. } = self.borrow_market();
444        fixed.get_base_mint()
445    }
446
447    pub fn get_quote_mint(&self) -> &Pubkey {
448        let DynamicAccount { fixed, .. } = self.borrow_market();
449        fixed.get_quote_mint()
450    }
451
452    pub fn has_free_block(&self) -> bool {
453        let DynamicAccount { fixed, .. } = self.borrow_market();
454        let free_list_head_index: DataIndex = fixed.free_list_head_index;
455        return free_list_head_index != NIL;
456    }
457
458    pub fn has_two_free_blocks(&self) -> bool {
459        let DynamicAccount { fixed, dynamic } = self.borrow_market();
460        let free_list_head_index: DataIndex = fixed.free_list_head_index;
461        if free_list_head_index == NIL {
462            return false;
463        }
464        let free_list_head: &FreeListNode<MarketUnusedFreeListPadding> =
465            get_helper::<FreeListNode<MarketUnusedFreeListPadding>>(dynamic, free_list_head_index);
466        free_list_head.has_next()
467    }
468
469    pub fn impact_quote_atoms(
470        &self,
471        is_bid: bool,
472        limit_base_atoms: BaseAtoms,
473        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
474    ) -> Result<QuoteAtoms, ProgramError> {
475        let now_slot: u32 = get_now_slot();
476        self.impact_quote_atoms_with_slot(
477            is_bid,
478            limit_base_atoms,
479            global_trade_accounts_opts,
480            now_slot,
481        )
482    }
483
484    pub fn impact_quote_atoms_with_slot(
485        &self,
486        is_bid: bool,
487        limit_base_atoms: BaseAtoms,
488        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
489        now_slot: u32,
490    ) -> Result<QuoteAtoms, ProgramError> {
491        let book: BooksideReadOnly = if is_bid {
492            self.get_asks()
493        } else {
494            self.get_bids()
495        };
496
497        let mut total_matched_quote_atoms: QuoteAtoms = QuoteAtoms::ZERO;
498        let mut remaining_base_atoms: BaseAtoms = limit_base_atoms;
499        for (_, resting_order) in book.iter::<RestingOrder>() {
500            // Skip expired orders
501            if resting_order.is_expired(now_slot) {
502                continue;
503            }
504            let matched_price: QuoteAtomsPerBaseAtom = resting_order.get_price();
505
506            // Either fill the entire resting order, or only the
507            // remaining_base_atoms, in which case, this is the last iteration
508            let matched_base_atoms: BaseAtoms =
509                resting_order.get_num_base_atoms().min(remaining_base_atoms);
510            let did_fully_match_resting_order: bool =
511                remaining_base_atoms >= resting_order.get_num_base_atoms();
512
513            // Number of quote atoms matched exactly. Round in taker favor if
514            // fully matching.
515            let matched_quote_atoms: QuoteAtoms = matched_price.checked_quote_for_base(
516                matched_base_atoms,
517                is_bid != did_fully_match_resting_order,
518            )?;
519
520            // Stop walking if missing the needed global account.
521            if self.is_missing_global_account(&resting_order, is_bid, global_trade_accounts_opts) {
522                break;
523            }
524
525            // Skip unbacked global orders.
526            if self.is_unbacked_global_order(
527                &resting_order,
528                is_bid,
529                global_trade_accounts_opts,
530                matched_base_atoms,
531                matched_quote_atoms,
532            ) {
533                continue;
534            }
535
536            total_matched_quote_atoms =
537                total_matched_quote_atoms.checked_add(matched_quote_atoms)?;
538
539            if !did_fully_match_resting_order {
540                break;
541            }
542
543            // prepare for next iteration
544            remaining_base_atoms = remaining_base_atoms.checked_sub(matched_base_atoms)?;
545        }
546
547        // Note that when there are not enough orders on the market to use up or
548        // to receive the desired number of base atoms, this returns just the
549        // full amount on the bookside without differentiating that return.
550
551        return Ok(total_matched_quote_atoms);
552    }
553
554    // Simplified version for certora. Those checks are actually stronger than
555    // needed since it shows invariants hold on swap even when impact_base_atoms
556    // returns a wrong value.
557    #[cfg(feature = "certora")]
558    pub fn impact_base_atoms(
559        &self,
560        is_bid: bool,
561        limit_quote_atoms: QuoteAtoms,
562        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
563    ) -> Result<BaseAtoms, ProgramError> {
564        impact_base_atoms_with_slot(self, is_bid, limit_quote_atoms, global_trade_accounts_opts)
565    }
566
567    #[cfg(not(feature = "certora"))]
568    pub fn impact_base_atoms(
569        &self,
570        is_bid: bool,
571        limit_quote_atoms: QuoteAtoms,
572        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
573    ) -> Result<BaseAtoms, ProgramError> {
574        let now_slot: u32 = get_now_slot();
575        self.impact_base_atoms_with_slot(
576            is_bid,
577            limit_quote_atoms,
578            global_trade_accounts_opts,
579            now_slot,
580        )
581    }
582
583    /// How many base atoms you get when you trade in limit_quote_atoms.
584    #[cfg(not(feature = "certora"))]
585    pub fn impact_base_atoms_with_slot(
586        &self,
587        is_bid: bool,
588        limit_quote_atoms: QuoteAtoms,
589        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
590        now_slot: u32,
591    ) -> Result<BaseAtoms, ProgramError> {
592        let book: RedBlackTreeReadOnly<'_, RestingOrder> = if is_bid {
593            self.get_asks()
594        } else {
595            self.get_bids()
596        };
597
598        let mut total_matched_base_atoms: BaseAtoms = BaseAtoms::ZERO;
599        let mut remaining_quote_atoms: QuoteAtoms = limit_quote_atoms;
600
601        for (_, resting_order) in book.iter::<RestingOrder>() {
602            // Skip expired orders.
603            if resting_order.is_expired(now_slot) {
604                continue;
605            }
606
607            let matched_price: QuoteAtomsPerBaseAtom = resting_order.get_price();
608            // base_atoms_limit is the number of base atoms that you get if you
609            // were to trade all of the remaining quote atoms at the current
610            // price. Rounding is done in the taker favor because at the limit,
611            // it is a full match. So if you are checking against asks with 100
612            // quote remaining against price 1.001, then the answer should be
613            // 100, because the rounding is in favor of the taker. It takes 100
614            // base atoms to exhaust 100 quote atoms at that price.
615            let base_atoms_limit: BaseAtoms =
616                matched_price.checked_base_for_quote(remaining_quote_atoms, !is_bid)?;
617            // Either fill the entire resting order, or only the
618            // base_atoms_limit, in which case, this is the last iteration.
619            let matched_base_atoms: BaseAtoms =
620                resting_order.get_num_base_atoms().min(base_atoms_limit);
621            let did_fully_match_resting_order: bool =
622                base_atoms_limit >= resting_order.get_num_base_atoms();
623            // Number of quote atoms matched exactly. Round in taker favor if
624            // fully matching.
625            let matched_quote_atoms: QuoteAtoms = matched_price.checked_quote_for_base(
626                matched_base_atoms,
627                is_bid != did_fully_match_resting_order,
628            )?;
629
630            // Stop walking if missing the needed global account.
631            if self.is_missing_global_account(resting_order, is_bid, global_trade_accounts_opts) {
632                break;
633            }
634
635            // Skip unbacked global orders.
636            if self.is_unbacked_global_order(
637                &resting_order,
638                is_bid,
639                global_trade_accounts_opts,
640                matched_base_atoms,
641                matched_quote_atoms,
642            ) {
643                continue;
644            }
645
646            total_matched_base_atoms = total_matched_base_atoms.checked_add(matched_base_atoms)?;
647
648            if !did_fully_match_resting_order {
649                break;
650            }
651
652            // Prepare for next iteration
653            remaining_quote_atoms = remaining_quote_atoms.checked_sub(matched_quote_atoms)?;
654
655            // we can match exactly in base atoms but also deplete all quote atoms at the same time
656            if remaining_quote_atoms == QuoteAtoms::ZERO {
657                break;
658            }
659        }
660
661        // Note that when there are not enough orders on the market to use up or
662        // to receive the desired number of quote atoms, this returns just the
663        // full amount on the bookside without differentiating that return.
664
665        return Ok(total_matched_base_atoms);
666    }
667
668    #[cfg(not(feature = "certora"))]
669    pub fn get_order_by_index(&self, index: DataIndex) -> &RestingOrder {
670        let DynamicAccount { dynamic, .. } = self.borrow_market();
671        &get_helper::<RBNode<RestingOrder>>(dynamic, index).get_value()
672    }
673
674    #[cfg(feature = "certora")]
675    pub fn get_order_by_index(&self, index: DataIndex) -> &RestingOrder {
676        let DynamicAccount { dynamic, .. } = self.borrow_market();
677        &get_helper_order(dynamic, index).get_value()
678    }
679
680    pub fn get_trader_balance(&self, trader: &Pubkey) -> (BaseAtoms, QuoteAtoms) {
681        let DynamicAccount { fixed, dynamic } = self.borrow_market();
682
683        let claimed_seats_tree: ClaimedSeatTreeReadOnly =
684            ClaimedSeatTreeReadOnly::new(dynamic, fixed.claimed_seats_root_index, NIL);
685        let trader_index: DataIndex =
686            claimed_seats_tree.lookup_index(&ClaimedSeat::new_empty(*trader));
687        let claimed_seat: &ClaimedSeat = get_helper_seat(dynamic, trader_index).get_value();
688        (
689            claimed_seat.base_withdrawable_balance,
690            claimed_seat.quote_withdrawable_balance,
691        )
692    }
693
694    pub fn get_trader_key_by_index(&self, index: DataIndex) -> &Pubkey {
695        let DynamicAccount { dynamic, .. } = self.borrow_market();
696
697        &get_helper_seat(dynamic, index).get_value().trader
698    }
699
700    pub fn get_trader_voume(&self, trader: &Pubkey) -> QuoteAtoms {
701        let DynamicAccount { fixed, dynamic } = self.borrow_market();
702
703        let claimed_seats_tree: ClaimedSeatTreeReadOnly =
704            ClaimedSeatTreeReadOnly::new(dynamic, fixed.claimed_seats_root_index, NIL);
705        let trader_index: DataIndex =
706            claimed_seats_tree.lookup_index(&ClaimedSeat::new_empty(*trader));
707        let claimed_seat: &ClaimedSeat = get_helper_seat(dynamic, trader_index).get_value();
708
709        claimed_seat.quote_volume
710    }
711
712    pub fn get_bids(&self) -> BooksideReadOnly {
713        let DynamicAccount { dynamic, fixed } = self.borrow_market();
714        BooksideReadOnly::new(
715            dynamic,
716            fixed.get_bids_root_index(),
717            fixed.get_bids_best_index(),
718        )
719    }
720
721    pub fn get_asks(&self) -> BooksideReadOnly {
722        let DynamicAccount { dynamic, fixed } = self.borrow_market();
723        BooksideReadOnly::new(
724            dynamic,
725            fixed.get_asks_root_index(),
726            fixed.get_asks_best_index(),
727        )
728    }
729
730    fn is_missing_global_account(
731        &self,
732        resting_order: &RestingOrder,
733        is_bid: bool,
734        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
735    ) -> bool {
736        if resting_order.get_order_type() == OrderType::Global {
737            // If global accounts are needed but not present, then this will
738            // crash. This is an intentional product decision. Would be
739            // valid to walk past, but we have chosen to give no fill rather
740            // than worse price if the taker takes the shortcut of not
741            // including global account.
742            let global_trade_accounts_opt: &Option<GlobalTradeAccounts> = if is_bid {
743                &global_trade_accounts_opts[0]
744            } else {
745                &global_trade_accounts_opts[1]
746            };
747            if global_trade_accounts_opt.is_none() {
748                return true;
749            }
750        }
751        return false;
752    }
753
754    fn is_unbacked_global_order(
755        &self,
756        resting_order: &RestingOrder,
757        is_bid: bool,
758        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
759        matched_base_atoms: BaseAtoms,
760        matched_quote_atoms: QuoteAtoms,
761    ) -> bool {
762        if resting_order.get_order_type() == OrderType::Global {
763            // If global accounts are needed but not present, then this will
764            // crash. This is an intentional product decision. Would be
765            // valid to walk past, but we have chosen to give no fill rather
766            // than worse price if the taker takes the shortcut of not
767            // including global account.
768            let global_trade_accounts_opt: &Option<GlobalTradeAccounts> = if is_bid {
769                &global_trade_accounts_opts[0]
770            } else {
771                &global_trade_accounts_opts[1]
772            };
773            let has_enough_tokens: bool = can_back_order(
774                global_trade_accounts_opt,
775                self.get_trader_key_by_index(resting_order.get_trader_index()),
776                GlobalAtoms::new(if is_bid {
777                    matched_base_atoms.as_u64()
778                } else {
779                    matched_quote_atoms.as_u64()
780                }),
781            );
782            if !has_enough_tokens {
783                return true;
784            }
785        }
786        return false;
787    }
788
789    pub fn get_trader_index(&self, trader: &Pubkey) -> DataIndex {
790        let DynamicAccount { fixed, dynamic } = self.borrow_market();
791
792        let claimed_seats_tree: ClaimedSeatTreeReadOnly =
793            ClaimedSeatTreeReadOnly::new(dynamic, fixed.claimed_seats_root_index, NIL);
794        let trader_index: DataIndex =
795            claimed_seats_tree.lookup_index(&ClaimedSeat::new_empty(*trader));
796        trader_index
797    }
798}
799
800// This generic impl covers MarketRef, MarketRefMut and other
801// DynamicAccount variants that allow write access.
802impl<
803        Fixed: DerefOrBorrowMut<MarketFixed> + DerefOrBorrow<MarketFixed>,
804        Dynamic: DerefOrBorrowMut<[u8]> + DerefOrBorrow<[u8]>,
805    > DynamicAccount<Fixed, Dynamic>
806{
807    fn borrow_mut(&mut self) -> MarketRefMut {
808        MarketRefMut {
809            fixed: self.fixed.deref_or_borrow_mut(),
810            dynamic: self.dynamic.deref_or_borrow_mut(),
811        }
812    }
813
814    pub fn market_expand(&mut self) -> ProgramResult {
815        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
816        let mut free_list: FreeList<MarketUnusedFreeListPadding> =
817            FreeList::new(dynamic, fixed.free_list_head_index);
818
819        free_list.add(fixed.num_bytes_allocated);
820        fixed.num_bytes_allocated += MARKET_BLOCK_SIZE as u32;
821        fixed.free_list_head_index = free_list.get_head();
822        Ok(())
823    }
824
825    pub fn claim_seat(&mut self, trader: &Pubkey) -> ProgramResult {
826        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
827        let free_address: DataIndex = get_free_address_on_market_fixed_for_seat(fixed, dynamic);
828
829        let mut claimed_seats_tree: ClaimedSeatTree =
830            ClaimedSeatTree::new(dynamic, fixed.claimed_seats_root_index, NIL);
831
832        let claimed_seat: ClaimedSeat = ClaimedSeat::new_empty(*trader);
833        require!(
834            claimed_seats_tree.lookup_index(&claimed_seat) == NIL,
835            ManifestError::AlreadyClaimedSeat,
836            "Already claimed seat",
837        )?;
838        claimed_seats_tree.insert(free_address, claimed_seat);
839        // theoretically we need to update the total withdrawable amount, but it's always 0 here.
840        // but let's check here that the assumption holds.
841        #[cfg(feature = "certora")]
842        {
843            cvt::cvt_assert!(claimed_seat.base_withdrawable_balance == BaseAtoms::new(0));
844            cvt::cvt_assert!(claimed_seat.quote_withdrawable_balance == QuoteAtoms::new(0));
845        }
846        fixed.claimed_seats_root_index = claimed_seats_tree.get_root_index();
847
848        get_mut_helper::<RBNode<ClaimedSeat>>(dynamic, free_address)
849            .set_payload_type(MarketDataTreeNodeType::ClaimedSeat as u8);
850        Ok(())
851    }
852
853    // Only used when temporarily claiming for swap and we dont have the system
854    // program to expand. Otherwise, there is no reason to ever give up your
855    // seat.
856    pub fn release_seat(&mut self, trader: &Pubkey) -> ProgramResult {
857        let trader_seat_index: DataIndex = self.get_trader_index(trader);
858        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
859
860        let mut claimed_seats_tree: ClaimedSeatTree =
861            ClaimedSeatTree::new(dynamic, fixed.claimed_seats_root_index, NIL);
862        claimed_seats_tree.remove_by_index(trader_seat_index);
863        fixed.claimed_seats_root_index = claimed_seats_tree.get_root_index();
864
865        // Put back seat on free list.
866        release_address_on_market_fixed_for_seat(fixed, dynamic, trader_seat_index);
867        Ok(())
868    }
869
870    pub fn deposit(
871        &mut self,
872        trader_index: DataIndex,
873        amount_atoms: u64,
874        is_base: bool,
875    ) -> ProgramResult {
876        require!(
877            is_not_nil!(trader_index),
878            ManifestError::InvalidDepositAccounts,
879            "No seat initialized",
880        )?;
881        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
882        update_balance(fixed, dynamic, trader_index, is_base, true, amount_atoms)?;
883        Ok(())
884    }
885
886    pub fn withdraw(
887        &mut self,
888        trader_index: DataIndex,
889        amount_atoms: u64,
890        is_base: bool,
891    ) -> ProgramResult {
892        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
893        update_balance(fixed, dynamic, trader_index, is_base, false, amount_atoms)?;
894        Ok(())
895    }
896
897    pub fn place_order_(
898        &mut self,
899        args: AddOrderToMarketArgs,
900    ) -> Result<AddOrderToMarketResult, ProgramError> {
901        market_helpers::place_order_helper(self, args)
902    }
903
904    /// Place an order and update the market
905    ///
906    /// 1. Check the order against the opposite bookside
907    /// 2. Rest any amount of the order leftover on the book
908    pub fn place_order(
909        &mut self,
910        args: AddOrderToMarketArgs,
911    ) -> Result<AddOrderToMarketResult, ProgramError> {
912        let AddOrderToMarketArgs {
913            market,
914            trader_index,
915            num_base_atoms,
916            price,
917            is_bid,
918            last_valid_slot,
919            order_type,
920            global_trade_accounts_opts,
921            current_slot,
922        } = args;
923        assert_already_has_seat(trader_index)?;
924        let now_slot: u32 = current_slot.unwrap_or_else(|| get_now_slot());
925
926        // Reverse orders will have their last valid slot overriden to no expiration.
927        if order_type != OrderType::Reverse {
928            assert_not_already_expired(last_valid_slot, now_slot)?;
929        }
930
931        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
932
933        let mut current_maker_order_index: DataIndex = if is_bid {
934            fixed.asks_best_index
935        } else {
936            fixed.bids_best_index
937        };
938
939        let mut total_base_atoms_traded: BaseAtoms = BaseAtoms::ZERO;
940        let mut total_quote_atoms_traded: QuoteAtoms = QuoteAtoms::ZERO;
941
942        let mut remaining_base_atoms: BaseAtoms = num_base_atoms;
943        while remaining_base_atoms > BaseAtoms::ZERO && is_not_nil!(current_maker_order_index) {
944            let maker_order: &RestingOrder =
945                get_helper::<RBNode<RestingOrder>>(dynamic, current_maker_order_index).get_value();
946
947            // Remove the resting order if expired or somehow a zero order got on the book.
948            if maker_order.is_expired(now_slot) || maker_order.get_num_base_atoms().as_u64() == 0 {
949                let next_maker_order_index: DataIndex = get_next_candidate_match_index(
950                    fixed,
951                    dynamic,
952                    current_maker_order_index,
953                    is_bid,
954                );
955                remove_and_update_balances(
956                    fixed,
957                    dynamic,
958                    current_maker_order_index,
959                    global_trade_accounts_opts,
960                )?;
961                current_maker_order_index = next_maker_order_index;
962                continue;
963            }
964
965            // Stop trying to match if price no longer satisfies limit.
966            if (is_bid && maker_order.get_price() > price)
967                || (!is_bid && maker_order.get_price() < price)
968            {
969                break;
970            }
971
972            // Got a match. First make sure we are allowed to match. We check
973            // inside the matching rather than skipping the matching altogether
974            // because post only orders should fail, not produce a crossed book.
975            assert_can_take(order_type)?;
976
977            let maker_sequence_number = maker_order.get_sequence_number();
978            let maker_trader_index: DataIndex = maker_order.get_trader_index();
979            let did_fully_match_resting_order: bool =
980                remaining_base_atoms >= maker_order.get_num_base_atoms();
981            let base_atoms_traded: BaseAtoms = if did_fully_match_resting_order {
982                maker_order.get_num_base_atoms()
983            } else {
984                remaining_base_atoms
985            };
986
987            let matched_price: QuoteAtomsPerBaseAtom = maker_order.get_price();
988
989            // on full fill: round in favor of the taker
990            // on partial fill: round in favor of the maker
991            let quote_atoms_traded: QuoteAtoms = matched_price.checked_quote_for_base(
992                base_atoms_traded,
993                is_bid != did_fully_match_resting_order,
994            )?;
995
996            // If it is a global order, just in time bring the funds over, or
997            // remove from the tree and continue on to the next order.
998            let maker: Pubkey = get_helper_seat(dynamic, maker_trader_index)
999                .get_value()
1000                .trader;
1001            let taker: Pubkey = get_helper_seat(dynamic, trader_index).get_value().trader;
1002            let is_global: bool = maker_order.is_global();
1003            let is_maker_reverse: bool = maker_order.is_reverse();
1004            let maker_reverse_spread: u16 = maker_order.get_reverse_spread();
1005
1006            if is_global {
1007                let global_trade_accounts_opt: &Option<GlobalTradeAccounts> = if is_bid {
1008                    &global_trade_accounts_opts[0]
1009                } else {
1010                    &global_trade_accounts_opts[1]
1011                };
1012                // When the global account is not included, a taker order can
1013                // halt here, but a possible maker order will need to crash
1014                // since that would result in a crossed book.
1015                if global_trade_accounts_opt.is_none() {
1016                    if order_type_can_rest(order_type) {
1017                        return Err(ManifestError::MissingGlobal.into());
1018                    } else {
1019                        break;
1020                    }
1021                }
1022                // When is_bid, the taker is supplying quote, so the global
1023                // maker needs to supply base.
1024                let has_enough_tokens: bool = try_to_move_global_tokens(
1025                    global_trade_accounts_opt,
1026                    &maker,
1027                    GlobalAtoms::new(if is_bid {
1028                        base_atoms_traded.as_u64()
1029                    } else {
1030                        quote_atoms_traded.as_u64()
1031                    }),
1032                )?;
1033
1034                if !has_enough_tokens {
1035                    let next_maker_order_index: DataIndex = get_next_candidate_match_index(
1036                        fixed,
1037                        dynamic,
1038                        current_maker_order_index,
1039                        is_bid,
1040                    );
1041                    remove_and_update_balances(
1042                        fixed,
1043                        dynamic,
1044                        current_maker_order_index,
1045                        global_trade_accounts_opts,
1046                    )?;
1047                    current_maker_order_index = next_maker_order_index;
1048                    continue;
1049                }
1050            }
1051
1052            total_base_atoms_traded = total_base_atoms_traded.checked_add(base_atoms_traded)?;
1053            total_quote_atoms_traded = total_quote_atoms_traded.checked_add(quote_atoms_traded)?;
1054
1055            // Possibly increase bonus atom maker gets from the rounding the
1056            // quote in their favor. They will get one less than expected when
1057            // cancelling because of rounding, this counters that. This ensures
1058            // that the amount of quote that the maker has credit for when they
1059            // cancel/expire is always the maximum amount that could have been
1060            // used in matching that order.
1061            // Example:
1062            // Maker deposits 11            | Balance: 0 base 11 quote | Orders: []
1063            // Maker bid for 10@1.15        | Balance: 0 base 0 quote  | Orders: [bid 10@1.15]
1064            // Swap    5 base <--> 5 quote  | Balance: 5 base 0 quote  | Orders: [bid 5@1.15]
1065            //     <this code block>        | Balance: 5 base 1 quote  | Orders: [bid 5@1.15]
1066            // Maker cancel                 | Balance: 5 base 6 quote  | Orders: []
1067            //
1068            // The swapper deposited 5 base and withdrew 5 quote. The maker deposited 11 quote.
1069            // If we didnt do this adjustment, there would be an unaccounted for
1070            // quote atom.
1071            // Note that we do not have to do this on the other direction
1072            // because the amount of atoms that a maker needs to support an ask
1073            // is exact. The rounding is always on quote.
1074            //
1075            // Do not credit the bonus atom on global orders. This is because
1076            // only the number of atoms required for the trade were brought
1077            // over.  The extra one that is no longer needed for taker rounding
1078            // is not brought over, so dont credit the maker for it.
1079            if !is_bid && !is_global {
1080                // These are only used when is_bid.
1081                let previous_maker_quote_atoms_allocated: QuoteAtoms =
1082                    matched_price.checked_quote_for_base(maker_order.get_num_base_atoms(), true)?;
1083                let new_maker_quote_atoms_allocated: QuoteAtoms = matched_price
1084                    .checked_quote_for_base(
1085                        maker_order
1086                            .get_num_base_atoms()
1087                            .checked_sub(base_atoms_traded)?,
1088                        true,
1089                    )?;
1090                let bonus_atom_or_zero: QuoteAtoms = previous_maker_quote_atoms_allocated
1091                    .checked_sub(new_maker_quote_atoms_allocated)?
1092                    .checked_sub(quote_atoms_traded)?;
1093
1094                // The bonus atom isnt actually traded, it is recouped to the
1095                // maker though from the tokens that they had been using to back
1096                // the order since it is no longer needed. So we do not need to
1097                // update the fill logs or amounts.
1098                update_balance(
1099                    fixed,
1100                    dynamic,
1101                    maker_trader_index,
1102                    is_bid,
1103                    true,
1104                    bonus_atom_or_zero.as_u64(),
1105                )?;
1106            }
1107
1108            // Increase maker from the matched amount in the trade.
1109            update_balance(
1110                fixed,
1111                dynamic,
1112                maker_trader_index,
1113                !is_bid,
1114                true,
1115                if is_bid {
1116                    quote_atoms_traded.into()
1117                } else {
1118                    base_atoms_traded.into()
1119                },
1120            )?;
1121            // Decrease taker
1122            update_balance(
1123                fixed,
1124                dynamic,
1125                trader_index,
1126                !is_bid,
1127                false,
1128                if is_bid {
1129                    quote_atoms_traded.into()
1130                } else {
1131                    base_atoms_traded.into()
1132                },
1133            )?;
1134            // Increase taker
1135            update_balance(
1136                fixed,
1137                dynamic,
1138                trader_index,
1139                is_bid,
1140                true,
1141                if is_bid {
1142                    base_atoms_traded.into()
1143                } else {
1144                    quote_atoms_traded.into()
1145                },
1146            )?;
1147
1148            // record maker & taker volume
1149            record_volume_by_trader_index(dynamic, maker_trader_index, quote_atoms_traded);
1150            record_volume_by_trader_index(dynamic, trader_index, quote_atoms_traded);
1151
1152            emit_stack(FillLog {
1153                market,
1154                maker,
1155                taker,
1156                base_mint: fixed.base_mint,
1157                quote_mint: fixed.quote_mint,
1158                base_atoms: base_atoms_traded,
1159                quote_atoms: quote_atoms_traded,
1160                price: matched_price,
1161                maker_sequence_number,
1162                taker_sequence_number: fixed.order_sequence_number,
1163                taker_is_buy: PodBool::from(is_bid),
1164                is_maker_global: PodBool::from(is_global),
1165                _padding: [0; 14],
1166            })?;
1167
1168            if did_fully_match_resting_order {
1169                // Get paid for removing a global order.
1170                if get_helper::<RBNode<RestingOrder>>(dynamic, current_maker_order_index)
1171                    .get_value()
1172                    .is_global()
1173                {
1174                    if is_bid {
1175                        remove_from_global(&global_trade_accounts_opts[0])?;
1176                    } else {
1177                        remove_from_global(&global_trade_accounts_opts[1])?;
1178                    }
1179                }
1180
1181                let next_maker_order_index: DataIndex = get_next_candidate_match_index(
1182                    fixed,
1183                    dynamic,
1184                    current_maker_order_index,
1185                    is_bid,
1186                );
1187                remove_order_from_tree_and_free(
1188                    fixed,
1189                    dynamic,
1190                    current_maker_order_index,
1191                    !is_bid,
1192                )?;
1193                remaining_base_atoms = remaining_base_atoms.checked_sub(base_atoms_traded)?;
1194                current_maker_order_index = next_maker_order_index;
1195            } else {
1196                #[cfg(feature = "certora")]
1197                remove_from_orderbook_balance(fixed, dynamic, current_maker_order_index);
1198                let maker_order: &mut RestingOrder =
1199                    get_mut_helper::<RBNode<RestingOrder>>(dynamic, current_maker_order_index)
1200                        .get_mut_value();
1201                maker_order.reduce(base_atoms_traded)?;
1202                #[cfg(feature = "certora")]
1203                add_to_orderbook_balance(fixed, dynamic, current_maker_order_index);
1204                remaining_base_atoms = BaseAtoms::ZERO;
1205            }
1206
1207            // Place the reverse order if the maker was a reverse order type.
1208            // This is non-trivial because in order to prevent tons of orders
1209            // filling the books on partial fills, we coalesce on top of book.
1210            if is_maker_reverse {
1211                let price_reverse: QuoteAtomsPerBaseAtom = if is_bid {
1212                    // Ask @P --> Bid @P * (1 - spread)
1213                    matched_price.multiply_spread(100_000_u32 - (maker_reverse_spread as u32))
1214                } else {
1215                    // Bid @P * (1 - spread) --> Ask @P
1216                    // equivalent to
1217                    // Bid @P --> Ask @P / (1 - spread)
1218                    matched_price.divide_spread(100_000_u32 - (maker_reverse_spread as u32))
1219                };
1220                let num_base_atoms_reverse: BaseAtoms = if is_bid {
1221                    // Maker is now buying with the exact number of quote atoms.
1222                    // Do not round_up because there might not be enough atoms
1223                    // for that.
1224                    price_reverse.checked_base_for_quote(quote_atoms_traded, false)?
1225                } else {
1226                    base_atoms_traded
1227                };
1228
1229                let mut coalesced: bool = false;
1230                {
1231                    let other_tree: Bookside = if is_bid {
1232                        Bookside::new(dynamic, fixed.bids_root_index, fixed.bids_best_index)
1233                    } else {
1234                        Bookside::new(dynamic, fixed.asks_root_index, fixed.asks_best_index)
1235                    };
1236                    let lookup_resting_order: RestingOrder = RestingOrder::new(
1237                        maker_trader_index,
1238                        BaseAtoms::ZERO, // Size does not matter, just price.
1239                        price_reverse,
1240                        0, // Sequence number does not matter, just price
1241                        NO_EXPIRATION_LAST_VALID_SLOT,
1242                        is_bid,
1243                        OrderType::Reverse,
1244                    )?;
1245
1246                    // Because there is a slight relaxation in matching reverse
1247                    // orders, do not need to worry about off by one errors
1248                    // causing fragmented liqudity.
1249                    let lookup_index: DataIndex = other_tree.lookup_index(&lookup_resting_order);
1250                    if lookup_index != NIL {
1251                        let order_to_coalesce_into: &mut RestingOrder =
1252                            get_mut_helper::<RBNode<RestingOrder>>(dynamic, lookup_index)
1253                                .get_mut_value();
1254                        order_to_coalesce_into.increase(num_base_atoms_reverse)?;
1255                        coalesced = true;
1256                    }
1257                }
1258
1259                // If there was 1 atom and because taker rounding is in effect,
1260                // then this would result in an empty order.
1261                if !coalesced && num_base_atoms_reverse.as_u64() > 0 {
1262                    // This code is similar to rest_remaining except it doesnt
1263                    // require borrowing data.  Non-trivial to combine the code
1264                    // because the certora formal verification inserted itself
1265                    // there.
1266                    let reverse_order_sequence_number: u64 = fixed.order_sequence_number;
1267                    fixed.order_sequence_number = reverse_order_sequence_number.wrapping_add(1);
1268
1269                    // Put the remaining in an order on the other bookside.
1270                    // There are 2 cases, either the maker was fully exhausted and
1271                    // we know that we will be able to use their address, or they
1272                    // were not fully exhausted and we know the order will not rest.
1273                    // In the second case, that uses the free block that was
1274                    // speculatively there for the current trader to rest.
1275                    let free_address: DataIndex = if is_bid {
1276                        get_free_address_on_market_fixed_for_bid_order(fixed, dynamic)
1277                    } else {
1278                        get_free_address_on_market_fixed_for_ask_order(fixed, dynamic)
1279                    };
1280
1281                    let mut new_reverse_resting_order: RestingOrder = RestingOrder::new(
1282                        maker_trader_index,
1283                        num_base_atoms_reverse,
1284                        price_reverse,
1285                        reverse_order_sequence_number,
1286                        // Does not expire.
1287                        NO_EXPIRATION_LAST_VALID_SLOT,
1288                        is_bid,
1289                        OrderType::Reverse,
1290                    )?;
1291                    new_reverse_resting_order.set_reverse_spread(maker_reverse_spread);
1292                    insert_order_into_tree(
1293                        is_bid,
1294                        fixed,
1295                        dynamic,
1296                        free_address,
1297                        &new_reverse_resting_order,
1298                    );
1299                    set_payload_order(dynamic, free_address);
1300                }
1301
1302                update_balance(
1303                    fixed,
1304                    dynamic,
1305                    maker_trader_index,
1306                    !is_bid,
1307                    false,
1308                    if is_bid {
1309                        num_base_atoms_reverse
1310                            .checked_mul(price_reverse, true)?
1311                            .into()
1312                    } else {
1313                        num_base_atoms_reverse.into()
1314                    },
1315                )?;
1316            }
1317
1318            // Stop if the last resting order did not fully match since that
1319            // means the taker was exhausted.
1320            if !did_fully_match_resting_order {
1321                break;
1322            }
1323        }
1324
1325        // Record volume on market
1326        fixed.quote_volume = fixed.quote_volume.wrapping_add(total_quote_atoms_traded);
1327
1328        // Bump the order sequence number even for orders which do not end up
1329        // resting.
1330        let order_sequence_number: u64 = fixed.order_sequence_number;
1331        fixed.order_sequence_number = order_sequence_number.wrapping_add(1);
1332
1333        // If there is nothing left to rest, then return before resting.
1334        if !order_type_can_rest(order_type)
1335            || remaining_base_atoms == BaseAtoms::ZERO
1336            || price == QuoteAtomsPerBaseAtom::ZERO
1337        {
1338            return Ok(AddOrderToMarketResult {
1339                order_sequence_number,
1340                order_index: NIL,
1341                base_atoms_traded: total_base_atoms_traded,
1342                quote_atoms_traded: total_quote_atoms_traded,
1343            });
1344        }
1345
1346        self.rest_remaining(
1347            args,
1348            remaining_base_atoms,
1349            order_sequence_number,
1350            total_base_atoms_traded,
1351            total_quote_atoms_traded,
1352        )
1353    }
1354
1355    /// Rest the remaining order onto the market in a RestingOrder.
1356    #[cfg(feature = "certora")]
1357    pub fn certora_rest_remaining(
1358        &mut self,
1359        args: AddOrderToMarketArgs,
1360        remaining_base_atoms: BaseAtoms,
1361        order_sequence_number: u64,
1362        total_base_atoms_traded: BaseAtoms,
1363        total_quote_atoms_traded: QuoteAtoms,
1364    ) -> Result<AddOrderToMarketResult, ProgramError> {
1365        self.rest_remaining(
1366            args,
1367            remaining_base_atoms,
1368            order_sequence_number,
1369            total_base_atoms_traded,
1370            total_quote_atoms_traded,
1371        )
1372    }
1373
1374    fn rest_remaining(
1375        &mut self,
1376        args: AddOrderToMarketArgs,
1377        remaining_base_atoms: BaseAtoms,
1378        order_sequence_number: u64,
1379        total_base_atoms_traded: BaseAtoms,
1380        total_quote_atoms_traded: QuoteAtoms,
1381    ) -> Result<AddOrderToMarketResult, ProgramError> {
1382        let AddOrderToMarketArgs {
1383            trader_index,
1384            price,
1385            is_bid,
1386            last_valid_slot,
1387            order_type,
1388            global_trade_accounts_opts,
1389            ..
1390        } = args;
1391        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
1392
1393        // Put the remaining in an order on the other bookside.
1394        let free_address: DataIndex = if is_bid {
1395            get_free_address_on_market_fixed_for_bid_order(fixed, dynamic)
1396        } else {
1397            get_free_address_on_market_fixed_for_ask_order(fixed, dynamic)
1398        };
1399
1400        let mut resting_order: RestingOrder = RestingOrder::new(
1401            trader_index,
1402            remaining_base_atoms,
1403            price,
1404            order_sequence_number,
1405            if order_type != OrderType::Reverse {
1406                last_valid_slot
1407            } else {
1408                NO_EXPIRATION_LAST_VALID_SLOT
1409            },
1410            is_bid,
1411            order_type,
1412        )?;
1413
1414        if order_type == OrderType::Reverse {
1415            resting_order.set_reverse_spread(last_valid_slot as u16);
1416        }
1417
1418        if resting_order.is_global() {
1419            if is_bid {
1420                let global_trade_account_opt = &global_trade_accounts_opts[1];
1421                require!(
1422                    global_trade_account_opt.is_some(),
1423                    ManifestError::MissingGlobal,
1424                    "Missing global accounts when adding a global",
1425                )?;
1426                try_to_add_to_global(&global_trade_account_opt.as_ref().unwrap(), &resting_order)?;
1427            } else {
1428                let global_trade_account_opt = &global_trade_accounts_opts[0];
1429                require!(
1430                    global_trade_account_opt.is_some(),
1431                    ManifestError::MissingGlobal,
1432                    "Missing global accounts when adding a global",
1433                )?;
1434                try_to_add_to_global(&global_trade_account_opt.as_ref().unwrap(), &resting_order)?;
1435            }
1436        } else {
1437            // Place the remaining.
1438            // Rounds up quote atoms so price can be rounded in favor of taker
1439            update_balance(
1440                fixed,
1441                dynamic,
1442                trader_index,
1443                !is_bid,
1444                false,
1445                if is_bid {
1446                    remaining_base_atoms.checked_mul(price, true)?.into()
1447                } else {
1448                    remaining_base_atoms.into()
1449                },
1450            )?;
1451        }
1452        insert_order_into_tree(is_bid, fixed, dynamic, free_address, &resting_order);
1453
1454        set_payload_order(dynamic, free_address);
1455
1456        Ok(AddOrderToMarketResult {
1457            order_sequence_number,
1458            order_index: free_address,
1459            base_atoms_traded: total_base_atoms_traded,
1460            quote_atoms_traded: total_quote_atoms_traded,
1461        })
1462    }
1463
1464    // Does a linear scan over the orderbook to find the index to cancel.
1465    pub fn cancel_order(
1466        &mut self,
1467        trader_index: DataIndex,
1468        order_sequence_number: u64,
1469        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
1470    ) -> ProgramResult {
1471        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
1472
1473        let mut index_to_remove: DataIndex = NIL;
1474
1475        // One iteration to find the index to cancel in the ask side.
1476        let tree: BooksideReadOnly =
1477            BooksideReadOnly::new(dynamic, fixed.asks_root_index, fixed.asks_best_index);
1478        for (index, resting_order) in tree.iter::<RestingOrder>() {
1479            if resting_order.get_sequence_number() == order_sequence_number {
1480                require!(
1481                    resting_order.get_trader_index() == trader_index,
1482                    ManifestError::InvalidCancel,
1483                    "Cannot cancel for another trader",
1484                )?;
1485                require!(
1486                    index_to_remove == NIL,
1487                    ManifestError::InvalidCancel,
1488                    "Book is broken, matched multiple orders",
1489                )?;
1490                index_to_remove = index;
1491            }
1492        }
1493
1494        // Second iteration to find the index to cancel in the bid side.
1495        let tree: BooksideReadOnly =
1496            BooksideReadOnly::new(dynamic, fixed.bids_root_index, fixed.bids_best_index);
1497        for (index, resting_order) in tree.iter::<RestingOrder>() {
1498            if resting_order.get_sequence_number() == order_sequence_number {
1499                require!(
1500                    resting_order.get_trader_index() == trader_index,
1501                    ManifestError::InvalidCancel,
1502                    "Cannot cancel for another trader",
1503                )?;
1504                require!(
1505                    index_to_remove == NIL,
1506                    ManifestError::InvalidCancel,
1507                    "Book is broken, matched multiple orders",
1508                )?;
1509                index_to_remove = index;
1510            }
1511        }
1512
1513        if is_not_nil!(index_to_remove) {
1514            // Cancel order by index will update balances.
1515            self.cancel_order_by_index(index_to_remove, global_trade_accounts_opts)?;
1516            return Ok(());
1517        }
1518
1519        // Do not fail silently.
1520        Err(ManifestError::InvalidCancel.into())
1521    }
1522
1523    #[cfg_attr(feature = "certora", cvt_hook_end(cancel_order_by_index_was_called()))]
1524    pub fn cancel_order_by_index(
1525        &mut self,
1526        order_index: DataIndex,
1527        global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
1528    ) -> ProgramResult {
1529        let DynamicAccount { fixed, dynamic } = self.borrow_mut();
1530        // TODO: Undo expansion here when it was just
1531        // remove_and_update_balances(fixed, dynamic, order_index, global_trade_accounts_opts)?;
1532        // because the tracking for
1533
1534        let resting_order: &RestingOrder = get_helper_order(dynamic, order_index).get_value();
1535        let is_bid: bool = resting_order.get_is_bid();
1536
1537        // Important to round up because there was an extra atom taken for full
1538        // taker rounding when the order was placed.
1539        let amount_atoms: u64 = if is_bid {
1540            (resting_order
1541                .get_price()
1542                .checked_quote_for_base(resting_order.get_num_base_atoms(), true)
1543                .unwrap())
1544            .into()
1545        } else {
1546            resting_order.get_num_base_atoms().into()
1547        };
1548
1549        // Update the accounting for the order that was just canceled.
1550        if resting_order.is_global() {
1551            if is_bid {
1552                remove_from_global(&global_trade_accounts_opts[1])?;
1553            } else {
1554                remove_from_global(&global_trade_accounts_opts[0])?;
1555            }
1556
1557            // Certora version was equivalent except it returned early here.
1558            // Thats a bug, but keeping the certora code as close to what they
1559            // wrote as possible. Formal verification does not cover global, so
1560            // that would explain why introducing this bug for formal
1561            // verification does not cause failures.
1562            #[cfg(feature = "certora")]
1563            return Ok(());
1564        } else {
1565            update_balance(
1566                fixed,
1567                dynamic,
1568                resting_order.get_trader_index(),
1569                !is_bid,
1570                true,
1571                amount_atoms,
1572            )?;
1573        }
1574        remove_order_from_tree_and_free(fixed, dynamic, order_index, is_bid)?;
1575
1576        Ok(())
1577    }
1578}
1579
1580fn set_payload_order(dynamic: &mut [u8], free_address: DataIndex) {
1581    get_mut_helper_order(dynamic, free_address)
1582        .set_payload_type(MarketDataTreeNodeType::RestingOrder as u8);
1583}
1584
1585#[cfg(feature = "certora")]
1586fn add_to_orderbook_balance(fixed: &mut MarketFixed, dynamic: &mut [u8], order_index: DataIndex) {
1587    // Update the sum of orderbook balance.
1588    let resting_order = get_helper_order(dynamic, order_index).get_value();
1589    let (base_amount, quote_amount) = match resting_order.get_orderbook_atoms() {
1590        Ok(result) => result,
1591        // make errors nondet(), so we can see the violation
1592        Err(_) => (nondet(), nondet()),
1593    };
1594    fixed.orderbook_base_atoms = fixed.orderbook_base_atoms.saturating_add(base_amount);
1595    fixed.orderbook_quote_atoms = fixed.orderbook_quote_atoms.saturating_add(quote_amount);
1596}
1597
1598#[cfg(feature = "certora")]
1599fn remove_from_orderbook_balance(
1600    fixed: &mut MarketFixed,
1601    dynamic: &mut [u8],
1602    order_index: DataIndex,
1603) {
1604    // Update the sum of orderbook balance.
1605    let resting_order = get_helper_order(dynamic, order_index).get_value();
1606    let (base_amount, quote_amount) = match resting_order.get_orderbook_atoms() {
1607        Ok(result) => result,
1608        // make errors nondet(), so we can see the violation
1609        Err(_) => (nondet(), nondet()),
1610    };
1611    fixed.orderbook_base_atoms = fixed.orderbook_base_atoms.saturating_sub(base_amount);
1612    fixed.orderbook_quote_atoms = fixed.orderbook_quote_atoms.saturating_sub(quote_amount);
1613}
1614
1615fn remove_order_from_tree(
1616    fixed: &mut MarketFixed,
1617    dynamic: &mut [u8],
1618    order_index: DataIndex,
1619    is_bids: bool,
1620) -> ProgramResult {
1621    #[cfg(feature = "certora")]
1622    remove_from_orderbook_balance(fixed, dynamic, order_index);
1623    let mut tree: Bookside = if is_bids {
1624        Bookside::new(dynamic, fixed.bids_root_index, fixed.bids_best_index)
1625    } else {
1626        Bookside::new(dynamic, fixed.asks_root_index, fixed.asks_best_index)
1627    };
1628    tree.remove_by_index(order_index);
1629
1630    // Possibly changes the root and/or best.
1631    if is_bids {
1632        trace!(
1633            "remove order bid root:{}->{} max:{}->{}",
1634            fixed.bids_root_index,
1635            tree.get_root_index(),
1636            fixed.bids_best_index,
1637            tree.get_max_index()
1638        );
1639        fixed.bids_root_index = tree.get_root_index();
1640        fixed.bids_best_index = tree.get_max_index();
1641    } else {
1642        trace!(
1643            "remove order ask root:{}->{} max:{}->{}",
1644            fixed.asks_root_index,
1645            tree.get_root_index(),
1646            fixed.asks_best_index,
1647            tree.get_max_index()
1648        );
1649        fixed.asks_root_index = tree.get_root_index();
1650        fixed.asks_best_index = tree.get_max_index();
1651    }
1652    Ok(())
1653}
1654
1655// Remove order from the tree, free the block.
1656#[cfg_attr(
1657    feature = "certora",
1658    cvt_hook_end(remove_order_from_tree_and_free_was_called())
1659)]
1660fn remove_order_from_tree_and_free(
1661    fixed: &mut MarketFixed,
1662    dynamic: &mut [u8],
1663    order_index: DataIndex,
1664    is_bids: bool,
1665) -> ProgramResult {
1666    remove_order_from_tree(fixed, dynamic, order_index, is_bids)?;
1667    // Separate release functions because certora needs that.
1668    if is_bids {
1669        release_address_on_market_fixed_for_bid_order(fixed, dynamic, order_index);
1670    } else {
1671        release_address_on_market_fixed_for_ask_order(fixed, dynamic, order_index);
1672    }
1673    Ok(())
1674}
1675
1676#[allow(unused_variables)]
1677pub fn update_balance(
1678    fixed: &mut MarketFixed,
1679    dynamic: &mut [u8],
1680    trader_index: DataIndex,
1681    is_base: bool,
1682    is_increase: bool,
1683    amount_atoms: u64,
1684) -> ProgramResult {
1685    let claimed_seat: &mut ClaimedSeat = get_mut_helper_seat(dynamic, trader_index).get_mut_value();
1686
1687    trace!("update_balance_by_trader_index idx:{trader_index} base:{is_base} inc:{is_increase} amount:{amount_atoms}");
1688
1689    // Update the sum of withdrawable balance.
1690    // Subtract the current value from the sum here and at the end of the function, add
1691    // the new value to the sum.
1692    #[cfg(feature = "certora")]
1693    {
1694        fixed.withdrawable_base_atoms = fixed
1695            .withdrawable_base_atoms
1696            .saturating_sub(claimed_seat.base_withdrawable_balance);
1697        fixed.withdrawable_quote_atoms = fixed
1698            .withdrawable_quote_atoms
1699            .saturating_sub(claimed_seat.quote_withdrawable_balance);
1700    }
1701
1702    if is_base {
1703        if is_increase {
1704            claimed_seat.base_withdrawable_balance = claimed_seat
1705                .base_withdrawable_balance
1706                .checked_add(BaseAtoms::new(amount_atoms))?;
1707        } else {
1708            require!(
1709                claimed_seat.base_withdrawable_balance >= BaseAtoms::new(amount_atoms),
1710                ProgramError::InsufficientFunds,
1711                "Not enough base atoms. Has {}, needs {}",
1712                claimed_seat.base_withdrawable_balance,
1713                amount_atoms
1714            )?;
1715            claimed_seat.base_withdrawable_balance = claimed_seat
1716                .base_withdrawable_balance
1717                .checked_sub(BaseAtoms::new(amount_atoms))?;
1718        }
1719    } else if is_increase {
1720        claimed_seat.quote_withdrawable_balance = claimed_seat
1721            .quote_withdrawable_balance
1722            .checked_add(QuoteAtoms::new(amount_atoms))?;
1723    } else {
1724        require!(
1725            claimed_seat.quote_withdrawable_balance >= QuoteAtoms::new(amount_atoms),
1726            ProgramError::InsufficientFunds,
1727            "Not enough quote atoms. Has {}, needs {}",
1728            claimed_seat.quote_withdrawable_balance,
1729            amount_atoms
1730        )?;
1731        claimed_seat.quote_withdrawable_balance = claimed_seat
1732            .quote_withdrawable_balance
1733            .checked_sub(QuoteAtoms::new(amount_atoms))?;
1734    }
1735
1736    // Update the sum of withdrawable balance, second part.
1737    #[cfg(feature = "certora")]
1738    {
1739        fixed.withdrawable_base_atoms = fixed
1740            .withdrawable_base_atoms
1741            .saturating_add(claimed_seat.base_withdrawable_balance);
1742        fixed.withdrawable_quote_atoms = fixed
1743            .withdrawable_quote_atoms
1744            .saturating_add(claimed_seat.quote_withdrawable_balance);
1745    }
1746
1747    Ok(())
1748}
1749
1750fn record_volume_by_trader_index(
1751    dynamic: &mut [u8],
1752    trader_index: DataIndex,
1753    amount_atoms: QuoteAtoms,
1754) {
1755    let claimed_seat: &mut ClaimedSeat = get_mut_helper_seat(dynamic, trader_index).get_mut_value();
1756    claimed_seat.quote_volume = claimed_seat.quote_volume.wrapping_add(amount_atoms);
1757}
1758
1759#[inline(always)]
1760fn insert_order_into_tree(
1761    is_bid: bool,
1762    fixed: &mut MarketFixed,
1763    dynamic: &mut [u8],
1764    free_address: DataIndex,
1765    resting_order: &RestingOrder,
1766) {
1767    let mut tree: Bookside = if is_bid {
1768        Bookside::new(dynamic, fixed.bids_root_index, fixed.bids_best_index)
1769    } else {
1770        Bookside::new(dynamic, fixed.asks_root_index, fixed.asks_best_index)
1771    };
1772    tree.insert(free_address, *resting_order);
1773
1774    if is_bid {
1775        trace!(
1776            "insert order bid {resting_order:?} root:{}->{} max:{}->{}->{}",
1777            fixed.bids_root_index,
1778            tree.get_root_index(),
1779            fixed.bids_best_index,
1780            tree.get_max_index(),
1781            tree.get_next_lower_index::<RestingOrder>(tree.get_max_index()),
1782        );
1783        fixed.bids_root_index = tree.get_root_index();
1784        fixed.bids_best_index = tree.get_max_index();
1785    } else {
1786        trace!(
1787            "insert order ask {resting_order:?} root:{}->{} max:{}->{}->{}",
1788            fixed.asks_root_index,
1789            tree.get_root_index(),
1790            fixed.asks_best_index,
1791            tree.get_max_index(),
1792            tree.get_next_lower_index::<RestingOrder>(tree.get_max_index()),
1793        );
1794        fixed.asks_root_index = tree.get_root_index();
1795        fixed.asks_best_index = tree.get_max_index();
1796    }
1797    #[cfg(feature = "certora")]
1798    add_to_orderbook_balance(fixed, dynamic, free_address);
1799}
1800
1801fn get_next_candidate_match_index(
1802    fixed: &MarketFixed,
1803    dynamic: &[u8],
1804    current_maker_order_index: DataIndex,
1805    is_bid: bool,
1806) -> DataIndex {
1807    if is_bid {
1808        let tree: BooksideReadOnly =
1809            BooksideReadOnly::new(dynamic, fixed.asks_root_index, fixed.asks_best_index);
1810        let next_order_index: DataIndex =
1811            tree.get_next_lower_index::<RestingOrder>(current_maker_order_index);
1812        next_order_index
1813    } else {
1814        let tree: BooksideReadOnly =
1815            BooksideReadOnly::new(dynamic, fixed.bids_root_index, fixed.bids_best_index);
1816        let next_order_index: DataIndex =
1817            tree.get_next_lower_index::<RestingOrder>(current_maker_order_index);
1818        next_order_index
1819    }
1820}
1821
1822fn remove_and_update_balances(
1823    fixed: &mut MarketFixed,
1824    dynamic: &mut [u8],
1825    order_to_remove_index: DataIndex,
1826    global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
1827) -> ProgramResult {
1828    let resting_order_to_remove: &RestingOrder =
1829        get_helper_order(dynamic, order_to_remove_index).get_value();
1830    let order_to_remove_is_bid: bool = resting_order_to_remove.get_is_bid();
1831
1832    // Global order balances are accounted for on the global accounts, not on the market.
1833    if resting_order_to_remove.is_global() {
1834        if order_to_remove_is_bid {
1835            remove_from_global(&global_trade_accounts_opts[1])?;
1836        } else {
1837            remove_from_global(&global_trade_accounts_opts[0])?;
1838        }
1839    } else {
1840        // Return the exact number of atoms if the resting order is an
1841        // ask. If the resting order is bid, multiply by price and round
1842        // in favor of the taker which here means up. The maker places
1843        // the minimum number of atoms required.
1844        let amount_atoms_to_return: u64 = if order_to_remove_is_bid {
1845            resting_order_to_remove
1846                .get_price()
1847                .checked_quote_for_base(resting_order_to_remove.get_num_base_atoms(), true)?
1848                .as_u64()
1849        } else {
1850            resting_order_to_remove.get_num_base_atoms().as_u64()
1851        };
1852        update_balance(
1853            fixed,
1854            dynamic,
1855            resting_order_to_remove.get_trader_index(),
1856            !order_to_remove_is_bid,
1857            true,
1858            amount_atoms_to_return,
1859        )?;
1860    }
1861    remove_order_from_tree_and_free(
1862        fixed,
1863        dynamic,
1864        order_to_remove_index,
1865        order_to_remove_is_bid,
1866    )?;
1867    Ok(())
1868}