manifest/state/
resting_order.rs

1use std::mem::size_of;
2
3use crate::quantities::{u64_slice_to_u128, BaseAtoms, QuoteAtomsPerBaseAtom};
4#[cfg(feature = "certora")]
5use crate::quantities::{QuoteAtoms, WrapperU64};
6use borsh::{BorshDeserialize, BorshSerialize};
7use bytemuck::{Pod, Zeroable};
8use hypertree::{DataIndex, PodBool};
9use num_enum::{IntoPrimitive, TryFromPrimitive};
10use shank::ShankType;
11use solana_program::{entrypoint::ProgramResult, program_error::ProgramError};
12use static_assertions::const_assert_eq;
13use std::cmp::Ordering;
14
15use super::{constants::NO_EXPIRATION_LAST_VALID_SLOT, RESTING_ORDER_SIZE};
16
17#[derive(
18    Debug,
19    BorshDeserialize,
20    BorshSerialize,
21    PartialEq,
22    Clone,
23    Copy,
24    ShankType,
25    IntoPrimitive,
26    TryFromPrimitive,
27)]
28#[repr(u8)]
29pub enum OrderType {
30    // Normal limit order.
31    Limit = 0,
32
33    // Does not rest. Take only.
34    ImmediateOrCancel = 1,
35
36    // Fails if would cross the orderbook.
37    PostOnly = 2,
38
39    // Global orders are post only but use funds from the global account.
40    Global = 3,
41
42    // Reverse orders behave like an AMM. When filled, they place an order on
43    // the other side of the book with a small fee (spread).
44    Reverse = 4,
45}
46unsafe impl bytemuck::Zeroable for OrderType {}
47unsafe impl bytemuck::Pod for OrderType {}
48impl Default for OrderType {
49    fn default() -> Self {
50        OrderType::Limit
51    }
52}
53
54pub fn order_type_can_rest(order_type: OrderType) -> bool {
55    order_type != OrderType::ImmediateOrCancel
56}
57
58pub fn order_type_can_take(order_type: OrderType) -> bool {
59    order_type != OrderType::PostOnly && order_type != OrderType::Global
60}
61
62#[repr(C)]
63#[derive(Default, Debug, Copy, Clone, Zeroable, Pod, ShankType)]
64pub struct RestingOrder {
65    price: QuoteAtomsPerBaseAtom,
66    num_base_atoms: BaseAtoms,
67    sequence_number: u64,
68    trader_index: DataIndex,
69    last_valid_slot: u32,
70    is_bid: PodBool,
71    order_type: OrderType,
72    // Spread for reverse orders. Defaults to zero.
73    reverse_spread: u16,
74    _padding: [u8; 20],
75}
76
77// 16 +  // price
78//  8 +  // num_base_atoms
79//  8 +  // sequence_number
80//  4 +  // trader_index
81//  4 +  // last_valid_slot
82//  1 +  // is_bid
83//  1 +  // order_type
84//  2 +  // spread
85// 20    // padding 2
86// = 64
87const_assert_eq!(size_of::<RestingOrder>(), RESTING_ORDER_SIZE);
88const_assert_eq!(size_of::<RestingOrder>() % 8, 0);
89
90impl RestingOrder {
91    pub fn new(
92        trader_index: DataIndex,
93        num_base_atoms: BaseAtoms,
94        price: QuoteAtomsPerBaseAtom,
95        sequence_number: u64,
96        last_valid_slot: u32,
97        is_bid: bool,
98        order_type: OrderType,
99    ) -> Result<Self, ProgramError> {
100        // Reverse orders cannot have expiration. The purpose of those orders is to
101        // be a permanent liquidity on the book.
102        assert!(
103            !(order_type == OrderType::Reverse && last_valid_slot != NO_EXPIRATION_LAST_VALID_SLOT)
104        );
105
106        Ok(RestingOrder {
107            trader_index,
108            num_base_atoms,
109            last_valid_slot,
110            price,
111            sequence_number,
112            is_bid: PodBool::from_bool(is_bid),
113            order_type,
114            reverse_spread: 0,
115            _padding: Default::default(),
116        })
117    }
118
119    pub fn get_trader_index(&self) -> DataIndex {
120        self.trader_index
121    }
122
123    pub fn get_num_base_atoms(&self) -> BaseAtoms {
124        self.num_base_atoms
125    }
126
127    pub fn get_price(&self) -> QuoteAtomsPerBaseAtom {
128        self.price
129    }
130
131    #[cfg(any(test, feature = "no-clock"))]
132    pub fn set_sequence_number(&mut self, sequence_number: u64) {
133        self.sequence_number = sequence_number;
134    }
135    #[cfg(any(test, feature = "no-clock"))]
136    pub fn set_last_valid_slot(&mut self, last_valid_slot: u32) {
137        self.last_valid_slot = last_valid_slot;
138    }
139
140    pub fn get_order_type(&self) -> OrderType {
141        self.order_type
142    }
143
144    #[cfg(feature = "certora")]
145    pub fn is_global(&self) -> bool {
146        false
147    }
148
149    #[cfg(not(feature = "certora"))]
150    pub fn is_global(&self) -> bool {
151        self.order_type == OrderType::Global
152    }
153
154    pub fn is_reverse(&self) -> bool {
155        self.order_type == OrderType::Reverse
156    }
157
158    pub fn get_reverse_spread(self) -> u16 {
159        self.reverse_spread
160    }
161
162    pub fn set_reverse_spread(&mut self, spread: u16) {
163        self.reverse_spread = spread;
164    }
165
166    pub fn get_sequence_number(&self) -> u64 {
167        self.sequence_number
168    }
169
170    pub fn is_expired(&self, current_slot: u32) -> bool {
171        self.last_valid_slot != NO_EXPIRATION_LAST_VALID_SLOT && self.last_valid_slot < current_slot
172    }
173
174    pub fn get_is_bid(&self) -> bool {
175        self.is_bid.0 == 1
176    }
177
178    // compute the "value" of an order, i.e. the tokens that are reserved for the trade and
179    // that will be returned when it is cancelled.
180    #[cfg(feature = "certora")]
181    pub fn get_orderbook_atoms(&self) -> Result<(BaseAtoms, QuoteAtoms), ProgramError> {
182        if self.is_global() {
183            return Ok((BaseAtoms::new(0), QuoteAtoms::new(0)));
184        } else if self.get_is_bid() {
185            let quote_amount = self.num_base_atoms.checked_mul(self.price, true)?;
186            return Ok((BaseAtoms::new(0), quote_amount));
187        } else {
188            return Ok((self.num_base_atoms, QuoteAtoms::new(0)));
189        }
190    }
191
192    pub fn reduce(&mut self, size: BaseAtoms) -> ProgramResult {
193        self.num_base_atoms = self.num_base_atoms.checked_sub(size)?;
194        Ok(())
195    }
196
197    // Only needed for combining orders. There is no edit_order function.
198    pub fn increase(&mut self, size: BaseAtoms) -> ProgramResult {
199        self.num_base_atoms = self.num_base_atoms.checked_add(size)?;
200        Ok(())
201    }
202}
203
204impl Ord for RestingOrder {
205    fn cmp(&self, other: &Self) -> Ordering {
206        // We only compare bids with bids or asks with asks. If you want to
207        // check if orders match, directly access their prices.
208        debug_assert!(self.get_is_bid() == other.get_is_bid());
209
210        if self.get_is_bid() {
211            (self.price).cmp(&other.price)
212        } else {
213            (other.price).cmp(&(self.price))
214        }
215    }
216}
217
218impl PartialOrd for RestingOrder {
219    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
220        Some(self.cmp(other))
221    }
222}
223
224impl PartialEq for RestingOrder {
225    fn eq(&self, other: &Self) -> bool {
226        if self.trader_index != other.trader_index || self.order_type != other.order_type {
227            return false;
228        }
229        if self.order_type == OrderType::Reverse {
230            // Allow off by 1 for reverse orders to enable coalescing. Otherwise there is a back and forth that fragments into many orders.
231            self.price == other.price
232                || u64_slice_to_u128(self.price.inner) + 1 == u64_slice_to_u128(other.price.inner)
233                || u64_slice_to_u128(self.price.inner) - 1 == u64_slice_to_u128(other.price.inner)
234        } else {
235            // Only used in equality check of lookups, so we can ignore size, seqnum, ...
236            self.price == other.price
237        }
238    }
239}
240
241impl Eq for RestingOrder {}
242
243impl std::fmt::Display for RestingOrder {
244    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
245        write!(f, "{}@{}", self.num_base_atoms, self.price)
246    }
247}
248
249#[cfg(test)]
250mod test {
251    use super::*;
252    use crate::quantities::WrapperU64;
253
254    #[test]
255    fn test_default() {
256        assert_eq!(OrderType::default(), OrderType::Limit);
257    }
258
259    #[test]
260    fn test_display() {
261        let resting_order: RestingOrder = RestingOrder::new(
262            0,
263            BaseAtoms::ZERO,
264            QuoteAtomsPerBaseAtom::ZERO,
265            0,
266            NO_EXPIRATION_LAST_VALID_SLOT,
267            true,
268            OrderType::Limit,
269        )
270        .unwrap();
271        format!("{}", resting_order);
272    }
273
274    #[test]
275    fn test_cmp() {
276        let resting_order_1: RestingOrder = RestingOrder::new(
277            0,
278            BaseAtoms::new(1),
279            QuoteAtomsPerBaseAtom::try_from(1.00000000000001).unwrap(),
280            0,
281            NO_EXPIRATION_LAST_VALID_SLOT,
282            true,
283            OrderType::Limit,
284        )
285        .unwrap();
286        let resting_order_2: RestingOrder = RestingOrder::new(
287            0,
288            BaseAtoms::new(1_000_000_000),
289            QuoteAtomsPerBaseAtom::try_from(1.01).unwrap(),
290            1,
291            NO_EXPIRATION_LAST_VALID_SLOT,
292            true,
293            OrderType::Limit,
294        )
295        .unwrap();
296        assert!(resting_order_1 < resting_order_2);
297        assert!(resting_order_1 != resting_order_2);
298
299        let resting_order_1: RestingOrder = RestingOrder::new(
300            0,
301            BaseAtoms::new(1),
302            QuoteAtomsPerBaseAtom::try_from(1.00000000000001).unwrap(),
303            0,
304            NO_EXPIRATION_LAST_VALID_SLOT,
305            false,
306            OrderType::Limit,
307        )
308        .unwrap();
309        let resting_order_2: RestingOrder = RestingOrder::new(
310            0,
311            BaseAtoms::new(1_000_000_000),
312            QuoteAtomsPerBaseAtom::try_from(1.01).unwrap(),
313            1,
314            NO_EXPIRATION_LAST_VALID_SLOT,
315            false,
316            OrderType::Limit,
317        )
318        .unwrap();
319        assert!(resting_order_1 > resting_order_2);
320        assert!(resting_order_1 != resting_order_2);
321    }
322
323    #[test]
324    fn test_setters() {
325        let mut resting_order: RestingOrder = RestingOrder::new(
326            0,
327            BaseAtoms::ZERO,
328            QuoteAtomsPerBaseAtom::ZERO,
329            0,
330            NO_EXPIRATION_LAST_VALID_SLOT,
331            true,
332            OrderType::Limit,
333        )
334        .unwrap();
335        resting_order.set_last_valid_slot(1);
336        resting_order.set_sequence_number(1);
337    }
338}