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 pub fn get_helper_seat(data: &[u8], index: DataIndex) -> &RBNode<ClaimedSeat> {
63 get_helper::<RBNode<ClaimedSeat>>(data, index)
64 }
65 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}
127const_assert_eq!(
129 size_of::<MarketUnusedFreeListPadding>(),
130 MARKET_FREE_LIST_BLOCK_SIZE
131);
132#[repr(C)]
135#[derive(Default, Copy, Clone, Zeroable, Pod, ShankType)]
136pub struct MarketFixed {
137 pub discriminant: u64,
139
140 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: Pubkey,
150 quote_mint: Pubkey,
152
153 base_vault: Pubkey,
155 quote_vault: Pubkey,
157
158 order_sequence_number: u64,
160
161 num_bytes_allocated: u32,
164
165 bids_root_index: DataIndex,
167 bids_best_index: DataIndex,
168
169 asks_root_index: DataIndex,
171 asks_best_index: DataIndex,
172
173 claimed_seats_root_index: DataIndex,
175
176 free_list_head_index: DataIndex,
178
179 _padding2: [u32; 1],
180
181 quote_volume: QuoteAtoms,
186
187 #[cfg(feature = "certora")]
190 withdrawable_base_atoms: BaseAtoms,
192 #[cfg(feature = "certora")]
193 withdrawable_quote_atoms: QuoteAtoms,
195 #[cfg(feature = "certora")]
196 pub orderbook_base_atoms: BaseAtoms,
198 #[cfg(feature = "certora")]
199 pub orderbook_quote_atoms: QuoteAtoms,
201 #[cfg(feature = "certora")]
202 _padding3: [u64; 4],
203
204 #[cfg(not(feature = "certora"))]
208 _padding3: [u64; 8],
209}
210const_assert_eq!(
211 size_of::<MarketFixed>(),
212 8 + 1 + 1 + 1 + 1 + 1 + 3 + 32 + 32 + 32 + 32 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 8 + 64 );
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 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 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 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 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
401pub type MarketValue = DynamicAccount<MarketFixed, Vec<u8>>;
403pub type MarketRef<'a> = DynamicAccount<&'a MarketFixed, &'a [u8]>;
405pub 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
430impl<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 if resting_order.is_expired(now_slot) {
502 continue;
503 }
504 let matched_price: QuoteAtomsPerBaseAtom = resting_order.get_price();
505
506 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 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 if self.is_missing_global_account(&resting_order, is_bid, global_trade_accounts_opts) {
522 break;
523 }
524
525 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 remaining_base_atoms = remaining_base_atoms.checked_sub(matched_base_atoms)?;
545 }
546
547 return Ok(total_matched_quote_atoms);
552 }
553
554 #[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 #[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 if resting_order.is_expired(now_slot) {
604 continue;
605 }
606
607 let matched_price: QuoteAtomsPerBaseAtom = resting_order.get_price();
608 let base_atoms_limit: BaseAtoms =
616 matched_price.checked_base_for_quote(remaining_quote_atoms, !is_bid)?;
617 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 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 if self.is_missing_global_account(resting_order, is_bid, global_trade_accounts_opts) {
632 break;
633 }
634
635 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 remaining_quote_atoms = remaining_quote_atoms.checked_sub(matched_quote_atoms)?;
654
655 if remaining_quote_atoms == QuoteAtoms::ZERO {
657 break;
658 }
659 }
660
661 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 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 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
800impl<
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 #[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 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 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 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 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 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 if (is_bid && maker_order.get_price() > price)
967 || (!is_bid && maker_order.get_price() < price)
968 {
969 break;
970 }
971
972 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 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 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 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 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 if !is_bid && !is_global {
1080 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 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 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 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 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_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 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 if is_maker_reverse {
1211 let price_reverse: QuoteAtomsPerBaseAtom = if is_bid {
1212 matched_price.multiply_spread(100_000_u32 - (maker_reverse_spread as u32))
1214 } else {
1215 matched_price.divide_spread(100_000_u32 - (maker_reverse_spread as u32))
1219 };
1220 let num_base_atoms_reverse: BaseAtoms = if is_bid {
1221 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, price_reverse,
1240 0, NO_EXPIRATION_LAST_VALID_SLOT,
1242 is_bid,
1243 OrderType::Reverse,
1244 )?;
1245
1246 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 !coalesced && num_base_atoms_reverse.as_u64() > 0 {
1262 let reverse_order_sequence_number: u64 = fixed.order_sequence_number;
1267 fixed.order_sequence_number = reverse_order_sequence_number.wrapping_add(1);
1268
1269 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 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 if !did_fully_match_resting_order {
1321 break;
1322 }
1323 }
1324
1325 fixed.quote_volume = fixed.quote_volume.wrapping_add(total_quote_atoms_traded);
1327
1328 let order_sequence_number: u64 = fixed.order_sequence_number;
1331 fixed.order_sequence_number = order_sequence_number.wrapping_add(1);
1332
1333 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 #[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 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 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 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 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 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 self.cancel_order_by_index(index_to_remove, global_trade_accounts_opts)?;
1516 return Ok(());
1517 }
1518
1519 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 let resting_order: &RestingOrder = get_helper_order(dynamic, order_index).get_value();
1535 let is_bid: bool = resting_order.get_is_bid();
1536
1537 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 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 #[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 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 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 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 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 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#[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 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 #[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 #[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 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 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}