manifest/state/
market_helpers.rs

1#[cfg(not(feature = "certora"))]
2mod free_addr_helpers {
3    use crate::state::market::{MarketFixed, MarketUnusedFreeListPadding};
4    use hypertree::{DataIndex, FreeList};
5
6    pub fn get_free_address_on_market_fixed(
7        fixed: &mut MarketFixed,
8        dynamic: &mut [u8],
9    ) -> DataIndex {
10        let mut free_list: FreeList<MarketUnusedFreeListPadding> =
11            FreeList::new(dynamic, fixed.free_list_head_index);
12        let free_address: DataIndex = free_list.remove();
13        fixed.free_list_head_index = free_list.get_head();
14        free_address
15    }
16
17    pub fn get_free_address_on_market_fixed_for_seat(
18        fixed: &mut MarketFixed,
19        dynamic: &mut [u8],
20    ) -> DataIndex {
21        get_free_address_on_market_fixed(fixed, dynamic)
22    }
23
24    pub fn get_free_address_on_market_fixed_for_bid_order(
25        fixed: &mut MarketFixed,
26        dynamic: &mut [u8],
27    ) -> DataIndex {
28        get_free_address_on_market_fixed(fixed, dynamic)
29    }
30
31    pub fn get_free_address_on_market_fixed_for_ask_order(
32        fixed: &mut MarketFixed,
33        dynamic: &mut [u8],
34    ) -> DataIndex {
35        get_free_address_on_market_fixed(fixed, dynamic)
36    }
37
38    pub fn release_address_on_market_fixed(
39        fixed: &mut MarketFixed,
40        dynamic: &mut [u8],
41        index: DataIndex,
42    ) {
43        let mut free_list: FreeList<MarketUnusedFreeListPadding> =
44            FreeList::new(dynamic, fixed.free_list_head_index);
45        free_list.add(index);
46        fixed.free_list_head_index = index;
47    }
48
49    pub fn release_address_on_market_fixed_for_seat(
50        fixed: &mut MarketFixed,
51        dynamic: &mut [u8],
52        index: DataIndex,
53    ) {
54        release_address_on_market_fixed(fixed, dynamic, index);
55    }
56
57    pub fn release_address_on_market_fixed_for_bid_order(
58        fixed: &mut MarketFixed,
59        dynamic: &mut [u8],
60        index: DataIndex,
61    ) {
62        release_address_on_market_fixed(fixed, dynamic, index);
63    }
64
65    pub fn release_address_on_market_fixed_for_ask_order(
66        fixed: &mut MarketFixed,
67        dynamic: &mut [u8],
68        index: DataIndex,
69    ) {
70        release_address_on_market_fixed(fixed, dynamic, index);
71    }
72}
73
74#[cfg(feature = "certora")]
75mod free_addr_helpers {
76    use crate::state::market::MarketFixed;
77
78    use super::{is_main_seat_free, is_second_seat_free, main_trader_index, second_trader_index};
79    use hypertree::DataIndex;
80
81    pub fn get_free_address_on_market_fixed_for_seat(
82        _fixed: &mut MarketFixed,
83        _dynamic: &mut [u8],
84    ) -> DataIndex {
85        // -- return index of the first available trader
86        if is_main_seat_free() {
87            main_trader_index()
88        } else if is_second_seat_free() {
89            second_trader_index()
90        } else {
91            cvt::cvt_assert!(false);
92            crate::state::market::NIL
93        }
94    }
95
96    pub fn get_free_address_on_market_fixed_for_bid_order(
97        _fixed: &mut MarketFixed,
98        _dynamic: &mut [u8],
99    ) -> DataIndex {
100        if super::is_bid_order_free() {
101            super::main_bid_order_index()
102        } else {
103            cvt::cvt_assert!(false);
104            super::NIL
105        }
106    }
107
108    pub fn get_free_address_on_market_fixed_for_ask_order(
109        _fixed: &mut MarketFixed,
110        _dynamic: &mut [u8],
111    ) -> DataIndex {
112        if super::is_ask_order_free() {
113            super::main_ask_order_index()
114        } else {
115            cvt::cvt_assert!(false);
116            super::NIL
117        }
118    }
119
120    pub fn release_address_on_market_fixed_for_seat(
121        _fixed: &mut MarketFixed,
122        _dynamic: &mut [u8],
123        _index: DataIndex,
124    ) {
125    }
126
127    pub fn release_address_on_market_fixed_for_bid_order(
128        _fixed: &mut MarketFixed,
129        _dynamic: &mut [u8],
130        _index: DataIndex,
131    ) {
132    }
133
134    pub fn release_address_on_market_fixed_for_ask_order(
135        _fixed: &mut MarketFixed,
136        _dynamic: &mut [u8],
137        _index: DataIndex,
138    ) {
139    }
140}
141
142pub use free_addr_helpers::*;
143
144// Refactoring of place_order
145
146use super::*;
147
148#[derive(Default, PartialEq)]
149pub enum AddOrderStatus {
150    #[default]
151    Canceled,
152    Filled,
153    PartialFill,
154    Unmatched,
155    GlobalSkip,
156}
157
158#[derive(Default)]
159pub struct AddOrderToMarketInnerResult {
160    pub next_order_index: DataIndex,
161    pub status: AddOrderStatus,
162}
163
164pub struct AddSingleOrderCtx<'a, 'b, 'info> {
165    pub args: AddOrderToMarketArgs<'b, 'info>,
166    fixed: &'a mut MarketFixed,
167    dynamic: &'a mut [u8],
168    pub now_slot: u32,
169    pub remaining_base_atoms: BaseAtoms,
170    pub total_base_atoms_traded: BaseAtoms,
171    pub total_quote_atoms_traded: QuoteAtoms,
172}
173
174impl<'a, 'b, 'info> AddSingleOrderCtx<'a, 'b, 'info> {
175    pub fn new(
176        args: AddOrderToMarketArgs<'b, 'info>,
177        fixed: &'a mut MarketFixed,
178        dynamic: &'a mut [u8],
179        remaining_base_atoms: BaseAtoms,
180        now_slot: u32,
181    ) -> Self {
182        Self {
183            args,
184            fixed,
185            dynamic,
186            now_slot,
187            remaining_base_atoms,
188            total_base_atoms_traded: BaseAtoms::ZERO,
189            total_quote_atoms_traded: QuoteAtoms::ZERO,
190        }
191    }
192    // TODO: Clean this up or prove that it is the same as market::place_order
193    pub fn place_single_order(
194        &mut self,
195        current_order_index: DataIndex,
196    ) -> Result<AddOrderToMarketInnerResult, ProgramError> {
197        let fixed: &mut _ = self.fixed;
198        let dynamic: &mut _ = self.dynamic;
199        let now_slot = self.now_slot;
200        let remaining_base_atoms = self.remaining_base_atoms;
201
202        let AddOrderToMarketArgs {
203            market,
204            trader_index,
205            num_base_atoms: _,
206            price,
207            is_bid,
208            last_valid_slot: _,
209            order_type,
210            global_trade_accounts_opts,
211            current_slot: _,
212        } = self.args;
213
214        let next_order_index: DataIndex =
215            get_next_candidate_match_index(fixed, dynamic, current_order_index, is_bid);
216
217        let other_order: &RestingOrder = get_helper_order(dynamic, current_order_index).get_value();
218
219        // Remove the resting order if expired.
220        if other_order.is_expired(now_slot) {
221            remove_and_update_balances(
222                fixed,
223                dynamic,
224                current_order_index,
225                global_trade_accounts_opts,
226            )?;
227            return Ok(AddOrderToMarketInnerResult {
228                next_order_index,
229                status: AddOrderStatus::Canceled,
230                ..Default::default()
231            });
232        }
233
234        // Stop trying to match if price no longer satisfies limit.
235        if (is_bid && other_order.get_price() > price)
236            || (!is_bid && other_order.get_price() < price)
237        {
238            return Ok(AddOrderToMarketInnerResult {
239                next_order_index: NIL,
240                status: AddOrderStatus::Unmatched,
241                ..Default::default()
242            });
243        }
244
245        // Got a match. First make sure we are allowed to match. We check
246        // inside the matching rather than skipping the matching altogether
247        // because post only orders should fail, not produce a crossed book.
248        trace!(
249            "match {} {order_type:?} {price:?} with {other_order:?}",
250            if is_bid { "bid" } else { "ask" }
251        );
252        assert_can_take(order_type)?;
253
254        let maker_sequence_number = other_order.get_sequence_number();
255        let other_trader_index: DataIndex = other_order.get_trader_index();
256        let did_fully_match_resting_order: bool =
257            remaining_base_atoms >= other_order.get_num_base_atoms();
258        let base_atoms_traded: BaseAtoms = if did_fully_match_resting_order {
259            other_order.get_num_base_atoms()
260        } else {
261            remaining_base_atoms
262        };
263
264        let matched_price: QuoteAtomsPerBaseAtom = other_order.get_price();
265
266        // on full fill: round in favor of the taker
267        // on partial fill: round in favor of the maker
268        let quote_atoms_traded: QuoteAtoms = matched_price
269            .checked_quote_for_base(base_atoms_traded, is_bid != did_fully_match_resting_order)?;
270
271        // If it is a global order, just in time bring the funds over, or
272        // remove from the tree and continue on to the next order.
273        let maker: Pubkey = get_helper_seat(dynamic, other_order.get_trader_index())
274            .get_value()
275            .trader;
276        let taker: Pubkey = get_helper_seat(dynamic, trader_index).get_value().trader;
277
278        if other_order.is_global() {
279            let global_trade_accounts_opt: &Option<GlobalTradeAccounts> = if is_bid {
280                &global_trade_accounts_opts[0]
281            } else {
282                &global_trade_accounts_opts[1]
283            };
284            let has_enough_tokens: bool = try_to_move_global_tokens(
285                global_trade_accounts_opt,
286                &maker,
287                GlobalAtoms::new(if is_bid {
288                    quote_atoms_traded.as_u64()
289                } else {
290                    base_atoms_traded.as_u64()
291                }),
292            )?;
293            if !has_enough_tokens {
294                remove_and_update_balances(
295                    fixed,
296                    dynamic,
297                    current_order_index,
298                    global_trade_accounts_opts,
299                )?;
300                return Ok(AddOrderToMarketInnerResult {
301                    next_order_index,
302                    status: AddOrderStatus::GlobalSkip,
303                    ..Default::default()
304                });
305            }
306        }
307
308        self.total_base_atoms_traded = self
309            .total_base_atoms_traded
310            .checked_add(base_atoms_traded)?;
311        self.total_quote_atoms_traded = self
312            .total_quote_atoms_traded
313            .checked_add(quote_atoms_traded)?;
314
315        // Possibly increase bonus atom maker gets from the rounding the
316        // quote in their favor. They will get one less than expected when
317        // cancelling because of rounding, this counters that. This ensures
318        // that the amount of quote that the maker has credit for when they
319        // cancel/expire is always the maximum amount that could have been
320        // used in matching that order.
321        // Example:
322        // Maker deposits 11            | Balance: 0 base 11 quote | Orders: []
323        // Maker bid for 10@1.15        | Balance: 0 base 0 quote  | Orders: [bid 10@1.15]
324        // Swap    5 base <--> 5 quote  | Balance: 5 base 0 quote  | Orders: [bid 5@1.15]
325        //     <this code block>        | Balance: 5 base 1 quote  | Orders: [bid 5@1.15]
326        // Maker cancel                 | Balance: 5 base 6 quote  | Orders: []
327        //
328        // The swapper deposited 5 base and withdrew 5 quote. The maker deposited 11 quote.
329        // If we didnt do this adjustment, there would be an unaccounted for
330        // quote atom.
331        // Note that we do not have to do this on the other direction
332        // because the amount of atoms that a maker needs to support an ask
333        // is exact. The rounding is always on quote.
334        if !is_bid {
335            // These are only used when is_bid, included up here for borrow checker reasons.
336            let other_order: &RestingOrder =
337                get_helper_order(dynamic, current_order_index).get_value();
338            let previous_maker_quote_atoms_allocated: QuoteAtoms =
339                matched_price.checked_quote_for_base(other_order.get_num_base_atoms(), true)?;
340            let new_maker_quote_atoms_allocated: QuoteAtoms = matched_price
341                .checked_quote_for_base(
342                    other_order
343                        .get_num_base_atoms()
344                        .checked_sub(base_atoms_traded)?,
345                    true,
346                )?;
347            update_balance(
348                fixed,
349                dynamic,
350                other_trader_index,
351                is_bid,
352                true,
353                (previous_maker_quote_atoms_allocated
354                    .checked_sub(new_maker_quote_atoms_allocated)?
355                    .checked_sub(quote_atoms_traded)?)
356                .as_u64(),
357            )?;
358        }
359
360        // Certora : the manifest code first increased the maker for the matched amount,
361        // then decreased the taker. This causes an overflow on withdrawable_balances.
362        // Thus, we changed it to first decrease the taker, and then increase the maker.
363
364        // Decrease taker
365        update_balance(
366            fixed,
367            dynamic,
368            trader_index,
369            !is_bid,
370            false,
371            if is_bid {
372                quote_atoms_traded.into()
373            } else {
374                base_atoms_traded.into()
375            },
376        )?;
377        // Increase maker from the matched amount in the trade.
378        update_balance(
379            fixed,
380            dynamic,
381            other_trader_index,
382            !is_bid,
383            true,
384            if is_bid {
385                quote_atoms_traded.into()
386            } else {
387                base_atoms_traded.into()
388            },
389        )?;
390        // Increase taker
391        update_balance(
392            fixed,
393            dynamic,
394            trader_index,
395            is_bid,
396            true,
397            if is_bid {
398                base_atoms_traded.into()
399            } else {
400                quote_atoms_traded.into()
401            },
402        )?;
403
404        // record maker & taker volume
405        record_volume_by_trader_index(dynamic, other_trader_index, quote_atoms_traded);
406        record_volume_by_trader_index(dynamic, trader_index, quote_atoms_traded);
407
408        emit_stack(FillLog {
409            market,
410            maker,
411            taker,
412            base_atoms: base_atoms_traded,
413            quote_atoms: quote_atoms_traded,
414            price: matched_price,
415            maker_sequence_number,
416            taker_sequence_number: fixed.order_sequence_number,
417            taker_is_buy: PodBool::from(is_bid),
418            base_mint: *fixed.get_base_mint(),
419            quote_mint: *fixed.get_quote_mint(),
420            // TODO: Fix this
421            is_maker_global: PodBool::from(false),
422            _padding: [0; 14],
423        })?;
424
425        if did_fully_match_resting_order {
426            // Get paid for removing a global order.
427            if get_helper_order(dynamic, current_order_index)
428                .get_value()
429                .get_order_type()
430                == OrderType::Global
431            {
432                if is_bid {
433                    remove_from_global(&global_trade_accounts_opts[0])?;
434                } else {
435                    remove_from_global(&global_trade_accounts_opts[1])?;
436                }
437            }
438
439            remove_order_from_tree_and_free(fixed, dynamic, current_order_index, !is_bid)?;
440            self.remaining_base_atoms = self.remaining_base_atoms.checked_sub(base_atoms_traded)?;
441            return Ok(AddOrderToMarketInnerResult {
442                next_order_index,
443                status: AddOrderStatus::Filled,
444                ..Default::default()
445            });
446        } else {
447            #[cfg(feature = "certora")]
448            remove_from_orderbook_balance(fixed, dynamic, current_order_index);
449            let other_order: &mut RestingOrder =
450                get_mut_helper_order(dynamic, current_order_index).get_mut_value();
451            other_order.reduce(base_atoms_traded)?;
452            #[cfg(feature = "certora")]
453            add_to_orderbook_balance(fixed, dynamic, current_order_index);
454            self.remaining_base_atoms = BaseAtoms::ZERO;
455            return Ok(AddOrderToMarketInnerResult {
456                next_order_index: NIL,
457                status: AddOrderStatus::PartialFill,
458                ..Default::default()
459            });
460        }
461    }
462}
463
464pub fn place_order_helper<
465    Fixed: DerefOrBorrowMut<MarketFixed> + DerefOrBorrow<MarketFixed>,
466    Dynamic: DerefOrBorrowMut<[u8]> + DerefOrBorrow<[u8]>,
467>(
468    self_: &mut DynamicAccount<Fixed, Dynamic>,
469    args: AddOrderToMarketArgs,
470) -> Result<AddOrderToMarketResult, ProgramError> {
471    let AddOrderToMarketArgs {
472        market: _,
473        trader_index,
474        num_base_atoms,
475        price: _,
476        is_bid,
477        last_valid_slot,
478        order_type,
479        global_trade_accounts_opts: _,
480        current_slot,
481    } = args;
482    assert_already_has_seat(trader_index)?;
483    let now_slot: u32 = current_slot.unwrap_or_else(|| get_now_slot());
484
485    assert_not_already_expired(last_valid_slot, now_slot)?;
486
487    let DynamicAccount { fixed, dynamic } = self_.borrow_mut();
488
489    let mut current_order_index: DataIndex = if is_bid {
490        fixed.asks_best_index
491    } else {
492        fixed.bids_best_index
493    };
494
495    let mut total_base_atoms_traded: BaseAtoms = BaseAtoms::ZERO;
496    let mut total_quote_atoms_traded: QuoteAtoms = QuoteAtoms::ZERO;
497
498    let mut remaining_base_atoms: BaseAtoms = num_base_atoms;
499
500    let mut ctx: AddSingleOrderCtx =
501        AddSingleOrderCtx::new(args, fixed, dynamic, remaining_base_atoms, now_slot);
502
503    while remaining_base_atoms > BaseAtoms::ZERO && is_not_nil!(current_order_index) {
504        // one step of placing an order
505        let AddOrderToMarketInnerResult {
506            next_order_index,
507            status,
508        } = ctx.place_single_order(current_order_index)?;
509
510        // update global state based on the context
511        // this ensures that each iteration of the loop updates all
512        // variables in scope just as it did originally.
513        current_order_index = next_order_index;
514        remaining_base_atoms = ctx.remaining_base_atoms;
515        total_base_atoms_traded = ctx.total_base_atoms_traded;
516        total_quote_atoms_traded = ctx.total_quote_atoms_traded;
517
518        if status == AddOrderStatus::Unmatched {
519            break;
520        } else if status == AddOrderStatus::PartialFill {
521            break;
522        }
523    }
524    // move out args so that they can be used later
525    let args: AddOrderToMarketArgs = ctx.args;
526    // ctx is dead from this point onward
527
528    // Record volume on market
529    fixed.quote_volume = fixed.quote_volume.wrapping_add(total_quote_atoms_traded);
530
531    // Bump the order sequence number even for orders which do not end up
532    // resting.
533    let order_sequence_number: u64 = fixed.order_sequence_number;
534    fixed.order_sequence_number = order_sequence_number.wrapping_add(1);
535
536    // If there is nothing left to rest, then return before resting.
537    if !order_type_can_rest(order_type) || remaining_base_atoms == BaseAtoms::ZERO {
538        return Ok(AddOrderToMarketResult {
539            order_sequence_number,
540            order_index: NIL,
541            base_atoms_traded: total_base_atoms_traded,
542            quote_atoms_traded: total_quote_atoms_traded,
543        });
544    }
545
546    self_.rest_remaining(
547        args,
548        remaining_base_atoms,
549        order_sequence_number,
550        total_base_atoms_traded,
551        total_quote_atoms_traded,
552    )
553}