manifest/program/processor/
batch_update.rs

1use std::cell::RefMut;
2
3use crate::{
4    logs::{emit_stack, CancelOrderLog, PlaceOrderLog},
5    program::get_trader_index_with_hint,
6    quantities::{BaseAtoms, PriceConversionError, QuoteAtomsPerBaseAtom, WrapperU64},
7    require,
8    state::{
9        utils::get_now_slot, AddOrderToMarketArgs, AddOrderToMarketResult, MarketRefMut, OrderType,
10        RestingOrder, MARKET_BLOCK_SIZE,
11    },
12    validation::loaders::BatchUpdateContext,
13};
14use borsh::{BorshDeserialize, BorshSerialize};
15
16use hypertree::{get_helper, trace, DataIndex, PodBool, RBNode};
17use solana_program::{
18    account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
19    pubkey::Pubkey,
20};
21
22use super::{expand_market_if_needed, shared::get_mut_dynamic_account};
23
24use crate::validation::loaders::GlobalTradeAccounts;
25#[cfg(feature = "certora")]
26use {
27    crate::certora::mocks_batch_update::{mock_cancel_order, mock_place_order},
28    early_panic::early_panic,
29    vectors::no_resizable_vec::NoResizableVec,
30};
31
32#[derive(Debug, BorshDeserialize, BorshSerialize, Clone)]
33pub struct CancelOrderParams {
34    order_sequence_number: u64,
35    order_index_hint: Option<DataIndex>,
36}
37
38impl CancelOrderParams {
39    pub fn new(order_sequence_number: u64) -> Self {
40        CancelOrderParams {
41            order_sequence_number,
42            order_index_hint: None,
43        }
44    }
45    pub fn new_with_hint(order_sequence_number: u64, order_index_hint: Option<DataIndex>) -> Self {
46        CancelOrderParams {
47            order_sequence_number,
48            order_index_hint,
49        }
50    }
51    pub fn order_sequence_number(&self) -> u64 {
52        self.order_sequence_number
53    }
54    pub fn order_index_hint(&self) -> Option<DataIndex> {
55        self.order_index_hint
56    }
57}
58
59#[derive(Debug, BorshDeserialize, BorshSerialize, Clone)]
60pub struct PlaceOrderParams {
61    base_atoms: u64,
62    price_mantissa: u32,
63    price_exponent: i8,
64    is_bid: bool,
65    last_valid_slot: u32,
66    order_type: OrderType,
67}
68
69impl PlaceOrderParams {
70    pub fn new(
71        base_atoms: u64,
72        price_mantissa: u32,
73        price_exponent: i8,
74        is_bid: bool,
75        order_type: OrderType,
76        last_valid_slot: u32,
77    ) -> Self {
78        PlaceOrderParams {
79            base_atoms,
80            price_mantissa,
81            price_exponent,
82            is_bid,
83            order_type,
84            last_valid_slot,
85        }
86    }
87    pub fn base_atoms(&self) -> u64 {
88        self.base_atoms
89    }
90
91    pub fn try_price(&self) -> Result<QuoteAtomsPerBaseAtom, PriceConversionError> {
92        QuoteAtomsPerBaseAtom::try_from_mantissa_and_exponent(
93            self.price_mantissa,
94            self.price_exponent,
95        )
96    }
97    pub fn is_bid(&self) -> bool {
98        self.is_bid
99    }
100    pub fn last_valid_slot(&self) -> u32 {
101        self.last_valid_slot
102    }
103    pub fn order_type(&self) -> OrderType {
104        self.order_type
105    }
106}
107
108#[derive(BorshDeserialize, BorshSerialize)]
109pub struct BatchUpdateParams {
110    /// Optional hint for what index the trader's ClaimedSeat is at.
111    pub trader_index_hint: Option<DataIndex>,
112    #[cfg(not(feature = "certora"))]
113    pub cancels: Vec<CancelOrderParams>,
114    #[cfg(feature = "certora")]
115    pub cancels: NoResizableVec<CancelOrderParams>,
116    #[cfg(not(feature = "certora"))]
117    pub orders: Vec<PlaceOrderParams>,
118    #[cfg(feature = "certora")]
119    pub orders: NoResizableVec<PlaceOrderParams>,
120}
121
122impl BatchUpdateParams {
123    pub fn new(
124        trader_index_hint: Option<DataIndex>,
125        #[cfg(not(feature = "certora"))] cancels: Vec<CancelOrderParams>,
126        #[cfg(feature = "certora")] cancels: NoResizableVec<CancelOrderParams>,
127        #[cfg(not(feature = "certora"))] orders: Vec<PlaceOrderParams>,
128        #[cfg(feature = "certora")] orders: NoResizableVec<PlaceOrderParams>,
129    ) -> Self {
130        BatchUpdateParams {
131            trader_index_hint,
132            cancels,
133            orders,
134        }
135    }
136}
137
138#[derive(BorshDeserialize, BorshSerialize)]
139pub struct BatchUpdateReturn {
140    /// Vector of tuples of (order_sequence_number, DataIndex)
141    pub orders: Vec<(u64, DataIndex)>,
142}
143
144#[repr(u8)]
145#[derive(Debug, Copy, Clone, PartialEq, Default)]
146pub enum MarketDataTreeNodeType {
147    // 0 is reserved because zeroed byte arrays should be empty.
148    Empty = 0,
149    #[default]
150    ClaimedSeat = 1,
151    RestingOrder = 2,
152}
153
154pub(crate) fn process_batch_update(
155    program_id: &Pubkey,
156    accounts: &[AccountInfo],
157    data: &[u8],
158) -> ProgramResult {
159    let params: BatchUpdateParams = BatchUpdateParams::try_from_slice(data)?;
160    process_batch_update_core(program_id, accounts, params)
161}
162
163#[cfg(not(feature = "certora"))]
164fn batch_cancel_order(
165    dynamic_account: &mut MarketRefMut,
166    trader_index: DataIndex,
167    order_sequence_number: u64,
168    global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
169) -> ProgramResult {
170    dynamic_account.cancel_order(
171        trader_index,
172        order_sequence_number,
173        &global_trade_accounts_opts,
174    )
175}
176
177#[cfg(feature = "certora")]
178fn batch_cancel_order(
179    dynamic_account: &mut MarketRefMut,
180    trader_index: DataIndex,
181    order_sequence_number: u64,
182    global_trade_accounts_opts: &[Option<GlobalTradeAccounts>; 2],
183) -> ProgramResult {
184    mock_cancel_order(
185        &dynamic_account,
186        trader_index,
187        order_sequence_number,
188        &global_trade_accounts_opts,
189    )
190}
191
192#[cfg(not(feature = "certora"))]
193fn batch_place_order(
194    dynamic_account: &mut MarketRefMut,
195    args: AddOrderToMarketArgs,
196) -> Result<AddOrderToMarketResult, ProgramError> {
197    dynamic_account.place_order(args)
198}
199
200#[cfg(feature = "certora")]
201fn batch_place_order(
202    dynamic_account: &mut MarketRefMut,
203    args: AddOrderToMarketArgs,
204) -> Result<AddOrderToMarketResult, ProgramError> {
205    mock_place_order(dynamic_account, args)
206}
207
208#[cfg_attr(all(feature = "certora", not(feature = "certora-test")), early_panic)]
209pub(crate) fn process_batch_update_core(
210    _program_id: &Pubkey,
211    accounts: &[AccountInfo],
212    params: BatchUpdateParams,
213) -> ProgramResult {
214    let batch_update_context: BatchUpdateContext = BatchUpdateContext::load(accounts)?;
215
216    let BatchUpdateContext {
217        market,
218        payer,
219        global_trade_accounts_opts,
220        ..
221    } = batch_update_context;
222
223    let BatchUpdateParams {
224        trader_index_hint,
225        cancels,
226        orders,
227    } = params;
228
229    let current_slot: Option<u32> = Some(get_now_slot());
230
231    trace!("batch_update trader_index_hint:{trader_index_hint:?} cancels:{cancels:?} orders:{orders:?}");
232
233    let trader_index: DataIndex = {
234        let market_data: &mut RefMut<&mut [u8]> = &mut market.try_borrow_mut_data()?;
235
236        let mut dynamic_account: MarketRefMut = get_mut_dynamic_account(market_data);
237        let trader_index: DataIndex =
238            get_trader_index_with_hint(trader_index_hint, &dynamic_account, &payer)?;
239
240        for cancel_order_params in cancels {
241            // Hinted is preferred because that is O(1) to find and O(log n) to
242            // remove. Without the hint, we lookup by order_sequence_number and
243            // that is O(n) lookup and O(log n) delete.
244            match cancel_order_params.order_index_hint() {
245                None => {
246                    // Cancels must succeed otherwise we fail the tx.
247                    batch_cancel_order(
248                        &mut dynamic_account,
249                        trader_index,
250                        cancel_order_params.order_sequence_number(),
251                        &global_trade_accounts_opts,
252                    )?;
253                }
254                Some(hinted_cancel_index) => {
255                    // Simple sanity check on the hint given. Make sure that it
256                    // aligns with block boundaries. We do a check that it is an
257                    // order owned by the payer inside the handler.
258                    require!(
259                        hinted_cancel_index % (MARKET_BLOCK_SIZE as DataIndex) == 0,
260                        crate::program::ManifestError::WrongIndexHintParams,
261                        "Invalid cancel hint index {}",
262                        hinted_cancel_index,
263                    )?;
264                    require!(
265                        get_helper::<RBNode<RestingOrder>>(
266                            &dynamic_account.dynamic,
267                            hinted_cancel_index,
268                        )
269                        .get_payload_type()
270                            == MarketDataTreeNodeType::RestingOrder as u8,
271                        crate::program::ManifestError::WrongIndexHintParams,
272                        "Invalid cancel hint index {}",
273                        hinted_cancel_index,
274                    )?;
275                    let order: &RestingOrder =
276                        dynamic_account.get_order_by_index(hinted_cancel_index);
277                    require!(
278                        trader_index == order.get_trader_index(),
279                        crate::program::ManifestError::WrongIndexHintParams,
280                        "Invalid cancel hint index {}",
281                        hinted_cancel_index,
282                    )?;
283                    require!(
284                        cancel_order_params.order_sequence_number() == order.get_sequence_number(),
285                        crate::program::ManifestError::WrongIndexHintParams,
286                        "Invalid cancel hint sequence number index {}",
287                        hinted_cancel_index,
288                    )?;
289                    dynamic_account
290                        .cancel_order_by_index(hinted_cancel_index, &global_trade_accounts_opts)?;
291                }
292            };
293
294            emit_stack(CancelOrderLog {
295                market: *market.key,
296                trader: *payer.key,
297                order_sequence_number: cancel_order_params.order_sequence_number(),
298            })?;
299        }
300        trader_index
301    };
302
303    // Result is a vector of (order_sequence_number, data_index)
304    #[cfg(not(feature = "certora"))]
305    let mut result: Vec<(u64, DataIndex)> = Vec::with_capacity(orders.len());
306    #[cfg(feature = "certora")]
307    let mut result = NoResizableVec::<(u64, DataIndex)>::new(10);
308    for place_order_params in orders {
309        {
310            let base_atoms: BaseAtoms = BaseAtoms::new(place_order_params.base_atoms());
311            let price: QuoteAtomsPerBaseAtom = place_order_params.try_price()?;
312            let order_type: OrderType = place_order_params.order_type();
313            let last_valid_slot: u32 = place_order_params.last_valid_slot();
314
315            // Need to reborrow every iteration so we can borrow later for expanding.
316            let market_data: &mut RefMut<&mut [u8]> = &mut market.try_borrow_mut_data()?;
317            let mut dynamic_account: MarketRefMut = get_mut_dynamic_account(market_data);
318
319            let add_order_to_market_result: AddOrderToMarketResult = batch_place_order(
320                &mut dynamic_account,
321                AddOrderToMarketArgs {
322                    market: *market.key,
323                    trader_index,
324                    num_base_atoms: base_atoms,
325                    price,
326                    is_bid: place_order_params.is_bid(),
327                    last_valid_slot,
328                    order_type,
329                    global_trade_accounts_opts: &global_trade_accounts_opts,
330                    current_slot,
331                },
332            )?;
333
334            let AddOrderToMarketResult {
335                order_index,
336                order_sequence_number,
337                ..
338            } = add_order_to_market_result;
339
340            emit_stack(PlaceOrderLog {
341                market: *market.key,
342                trader: *payer.key,
343                base_atoms,
344                price,
345                order_type,
346                is_bid: PodBool::from(place_order_params.is_bid()),
347                _padding: [0; 6],
348                order_sequence_number,
349                order_index,
350                last_valid_slot,
351            })?;
352            result.push((order_sequence_number, order_index));
353        }
354        expand_market_if_needed(&payer, &market)?;
355    }
356
357    // Formal verification does not cover return values.
358    #[cfg(not(feature = "certora"))]
359    {
360        let mut buffer: Vec<u8> = Vec::with_capacity(
361            std::mem::size_of::<BatchUpdateReturn>()
362                + result.len() * 2 * std::mem::size_of::<u64>(),
363        );
364        let return_data: BatchUpdateReturn = BatchUpdateReturn { orders: result };
365        return_data.serialize(&mut buffer).unwrap();
366        solana_program::program::set_return_data(&buffer[..]);
367    }
368
369    Ok(())
370}