manifest/state/
global.rs

1/// The global order state stores information about all global orders for a given token.
2///
3/// The reason for global orders to be sharded by token is that it will make it
4/// more effective for landing transactions. Rather than requiring a write lock
5/// for state that covers all markets, you just need to write lock state that
6/// covers all orders involving a given token.
7use std::{cmp::Ordering, mem::size_of};
8
9use bytemuck::{Pod, Zeroable};
10use hypertree::{
11    get_helper, get_mut_helper, DataIndex, FreeList, Get, HyperTreeReadOperations,
12    HyperTreeWriteOperations, RBNode, RedBlackTree, RedBlackTreeReadOnly, NIL,
13};
14use shank::ShankType;
15use solana_program::{entrypoint::ProgramResult, pubkey::Pubkey};
16use static_assertions::const_assert_eq;
17
18use crate::{
19    quantities::{GlobalAtoms, WrapperU64},
20    require,
21    validation::{get_global_address, get_global_vault_address, ManifestAccount},
22};
23
24use super::{
25    DerefOrBorrow, DerefOrBorrowMut, DynamicAccount, RestingOrder, GLOBAL_BLOCK_SIZE,
26    GLOBAL_DEPOSIT_SIZE, GLOBAL_FIXED_DISCRIMINANT, GLOBAL_FIXED_SIZE, GLOBAL_FREE_LIST_BLOCK_SIZE,
27    GLOBAL_TRADER_SIZE, MAX_GLOBAL_SEATS,
28};
29
30#[repr(C)]
31#[derive(Default, Copy, Clone, Zeroable, Pod, ShankType)]
32pub struct GlobalFixed {
33    /// Discriminant for identifying this type of account.
34    pub discriminant: u64,
35
36    /// Mint for this global
37    mint: Pubkey,
38
39    /// Vault address
40    vault: Pubkey,
41
42    /// Red-black tree root representing the global orders for the bank.
43    global_traders_root_index: DataIndex,
44
45    /// Red-black tree root representing the global deposits sorted by amount.
46    global_deposits_root_index: DataIndex,
47    /// Max, because the Hypertree provides access to max, but the sort key is
48    /// reversed so this is the smallest balance.
49    global_deposits_max_index: DataIndex,
50
51    /// LinkedList representing all free blocks that could be used for ClaimedSeats or RestingOrders
52    free_list_head_index: DataIndex,
53
54    /// Number of bytes allocated so far.
55    num_bytes_allocated: DataIndex,
56
57    vault_bump: u8,
58
59    /// Unused, but this byte wasnt being used anyways.
60    global_bump: u8,
61
62    num_seats_claimed: u16,
63}
64const_assert_eq!(
65    size_of::<GlobalFixed>(),
66    8  +  // discriminant
67    32 +  // mint
68    32 +  // vault
69    4 +   // global_seats_root_index
70    4 +   // global_amounts_root_index
71    4 +   // global_amounts_max_index 
72    4 +   // free_list_head_index
73    4 +   // num_bytes_allocated
74    1 +   // vault_bump
75    1 +   // global_bump
76    2 // num_seats_claimed
77);
78const_assert_eq!(size_of::<GlobalFixed>(), GLOBAL_FIXED_SIZE);
79const_assert_eq!(size_of::<GlobalFixed>() % 8, 0);
80impl Get for GlobalFixed {}
81
82#[repr(C, packed)]
83#[derive(Default, Copy, Clone, Pod, Zeroable)]
84struct GlobalUnusedFreeListPadding {
85    _padding: [u64; 7],
86    _padding2: [u8; 4],
87}
88// 4 bytes are for the free list, rest is payload.
89const_assert_eq!(
90    size_of::<GlobalUnusedFreeListPadding>(),
91    GLOBAL_FREE_LIST_BLOCK_SIZE
92);
93// Does not need to align to word boundaries because does not deserialize.
94
95#[repr(C)]
96#[derive(Default, Copy, Clone, Zeroable, Pod, ShankType)]
97pub struct GlobalTrader {
98    /// Trader who controls this global trader.
99    trader: Pubkey,
100
101    deposit_index: DataIndex,
102    _padding: u32,
103    _padding2: u64,
104}
105const_assert_eq!(size_of::<GlobalTrader>(), GLOBAL_TRADER_SIZE);
106const_assert_eq!(size_of::<GlobalTrader>() % 8, 0);
107
108impl Ord for GlobalTrader {
109    fn cmp(&self, other: &Self) -> Ordering {
110        (self.trader).cmp(&(other.trader))
111    }
112}
113impl PartialOrd for GlobalTrader {
114    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
115        Some(self.cmp(other))
116    }
117}
118impl PartialEq for GlobalTrader {
119    fn eq(&self, other: &Self) -> bool {
120        (self.trader) == (other.trader)
121    }
122}
123impl Eq for GlobalTrader {}
124impl std::fmt::Display for GlobalTrader {
125    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
126        write!(f, "{}", self.trader)
127    }
128}
129
130#[repr(C)]
131#[derive(Default, Copy, Clone, Zeroable, Pod, ShankType)]
132pub struct GlobalDeposit {
133    /// Trader who controls this global trader.
134    trader: Pubkey,
135
136    /// Token balance in the global account for this trader. The tokens received
137    /// in trades stay in the market.
138    balance_atoms: GlobalAtoms,
139    _padding: u64,
140}
141const_assert_eq!(size_of::<GlobalDeposit>(), GLOBAL_DEPOSIT_SIZE);
142const_assert_eq!(size_of::<GlobalDeposit>() % 8, 0);
143
144impl Ord for GlobalDeposit {
145    fn cmp(&self, other: &Self) -> Ordering {
146        // Reversed order so that the max according to the tree is actually the min.
147        (other.balance_atoms).cmp(&(self.balance_atoms))
148    }
149}
150impl PartialOrd for GlobalDeposit {
151    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
152        Some(self.cmp(other))
153    }
154}
155impl PartialEq for GlobalDeposit {
156    fn eq(&self, other: &Self) -> bool {
157        (self.trader) == (other.trader)
158    }
159}
160impl Eq for GlobalDeposit {}
161impl std::fmt::Display for GlobalDeposit {
162    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
163        write!(f, "{}", self.trader)
164    }
165}
166
167impl GlobalFixed {
168    pub fn new_empty(mint: &Pubkey) -> Self {
169        let (vault, vault_bump) = get_global_vault_address(mint);
170        let (_, global_bump) = get_global_address(mint);
171        GlobalFixed {
172            discriminant: GLOBAL_FIXED_DISCRIMINANT,
173            mint: *mint,
174            vault,
175            global_traders_root_index: NIL,
176            global_deposits_root_index: NIL,
177            global_deposits_max_index: NIL,
178            free_list_head_index: NIL,
179            num_bytes_allocated: 0,
180            vault_bump,
181            global_bump,
182            num_seats_claimed: 0,
183        }
184    }
185    pub fn get_mint(&self) -> &Pubkey {
186        &self.mint
187    }
188    pub fn get_vault(&self) -> &Pubkey {
189        &self.vault
190    }
191    pub fn get_vault_bump(&self) -> u8 {
192        self.vault_bump
193    }
194}
195
196impl ManifestAccount for GlobalFixed {
197    fn verify_discriminant(&self) -> ProgramResult {
198        // Check the discriminant to make sure it is a global account.
199        require!(
200            self.discriminant == GLOBAL_FIXED_DISCRIMINANT,
201            solana_program::program_error::ProgramError::InvalidAccountData,
202            "Invalid market discriminant actual: {} expected: {}",
203            self.discriminant,
204            GLOBAL_FIXED_DISCRIMINANT
205        )?;
206        Ok(())
207    }
208}
209
210impl GlobalTrader {
211    pub fn new_empty(trader: &Pubkey, deposit_index: DataIndex) -> Self {
212        GlobalTrader {
213            trader: *trader,
214            deposit_index,
215            _padding: 0,
216            _padding2: 0,
217        }
218    }
219}
220
221impl GlobalDeposit {
222    pub fn new_empty(trader: &Pubkey) -> Self {
223        GlobalDeposit {
224            trader: *trader,
225            balance_atoms: GlobalAtoms::ZERO,
226            _padding: 0,
227        }
228    }
229}
230
231pub type GlobalTraderTree<'a> = RedBlackTree<'a, GlobalTrader>;
232pub type GlobalTraderTreeReadOnly<'a> = RedBlackTreeReadOnly<'a, GlobalTrader>;
233pub type GlobalDepositTree<'a> = RedBlackTree<'a, GlobalDeposit>;
234pub type GlobalDepositTreeReadOnly<'a> = RedBlackTreeReadOnly<'a, GlobalDeposit>;
235
236/// Fully owned Global, used in clients that can copy.
237pub type GlobalValue = DynamicAccount<GlobalFixed, Vec<u8>>;
238/// Full global reference type.
239pub type GlobalRef<'a> = DynamicAccount<&'a GlobalFixed, &'a [u8]>;
240/// Full global reference type.
241pub type GlobalRefMut<'a> = DynamicAccount<&'a mut GlobalFixed, &'a mut [u8]>;
242
243impl<Fixed: DerefOrBorrow<GlobalFixed>, Dynamic: DerefOrBorrow<[u8]>>
244    DynamicAccount<Fixed, Dynamic>
245{
246    fn borrow_global(&self) -> GlobalRef {
247        GlobalRef {
248            fixed: self.fixed.deref_or_borrow(),
249            dynamic: self.dynamic.deref_or_borrow(),
250        }
251    }
252
253    pub fn get_balance_atoms(&self, trader: &Pubkey) -> GlobalAtoms {
254        let DynamicAccount { fixed, dynamic } = self.borrow_global();
255        // If the trader got evicted, then they wont be found.
256        let global_balance_or: Option<&GlobalDeposit> = get_global_deposit(fixed, dynamic, trader);
257        if let Some(global_deposit) = global_balance_or {
258            global_deposit.balance_atoms
259        } else {
260            GlobalAtoms::ZERO
261        }
262    }
263
264    pub fn verify_min_balance(&self, trader: &Pubkey) -> ProgramResult {
265        let DynamicAccount { fixed, dynamic } = self.borrow_global();
266
267        let existing_global_trader_opt: Option<&GlobalTrader> =
268            get_global_trader(fixed, dynamic, trader);
269        require!(
270            existing_global_trader_opt.is_some(),
271            crate::program::ManifestError::MissingGlobal,
272            "Could not find global trader for {}",
273            trader
274        )?;
275        let existing_global_trader: GlobalTrader = *existing_global_trader_opt.unwrap();
276        let global_trader_tree: GlobalTraderTreeReadOnly = GlobalTraderTreeReadOnly::new(
277            dynamic,
278            fixed.global_traders_root_index,
279            fixed.global_deposits_max_index,
280        );
281        let existing_trader_index: DataIndex =
282            global_trader_tree.lookup_index(&existing_global_trader);
283        let existing_global_trader: &GlobalTrader =
284            get_helper::<RBNode<GlobalTrader>>(dynamic, existing_trader_index).get_value();
285        let existing_deposit_index: DataIndex = existing_global_trader.deposit_index;
286
287        require!(
288            existing_deposit_index == fixed.global_deposits_max_index,
289            crate::program::ManifestError::GlobalInsufficient,
290            "Only can remove trader with lowest deposit"
291        )?;
292
293        Ok(())
294    }
295}
296
297impl<Fixed: DerefOrBorrowMut<GlobalFixed>, Dynamic: DerefOrBorrowMut<[u8]>>
298    DynamicAccount<Fixed, Dynamic>
299{
300    fn borrow_mut_global(&mut self) -> GlobalRefMut {
301        GlobalRefMut {
302            fixed: self.fixed.deref_or_borrow_mut(),
303            dynamic: self.dynamic.deref_or_borrow_mut(),
304        }
305    }
306
307    pub fn global_expand(&mut self) -> ProgramResult {
308        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
309
310        require!(
311            fixed.free_list_head_index == NIL,
312            crate::program::ManifestError::InvalidFreeList,
313            "Expected empty free list, but expand wasnt needed",
314        )?;
315
316        let mut free_list: FreeList<GlobalUnusedFreeListPadding> =
317            FreeList::new(dynamic, fixed.free_list_head_index);
318
319        // Expand twice since there are two trees.
320        free_list.add(fixed.num_bytes_allocated);
321        free_list.add(fixed.num_bytes_allocated + GLOBAL_BLOCK_SIZE as u32);
322        fixed.num_bytes_allocated += 2 * GLOBAL_BLOCK_SIZE as u32;
323        fixed.free_list_head_index = free_list.get_head();
324        Ok(())
325    }
326
327    pub fn reduce(&mut self, trader: &Pubkey, num_atoms: GlobalAtoms) -> ProgramResult {
328        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
329        let global_deposit_opt: Option<&mut GlobalDeposit> =
330            get_mut_global_deposit(fixed, dynamic, trader);
331        require!(
332            global_deposit_opt.is_some(),
333            crate::program::ManifestError::MissingGlobal,
334            "Could not find global deposit for {}",
335            trader
336        )?;
337        let global_deposit: &mut GlobalDeposit = global_deposit_opt.unwrap();
338        global_deposit.balance_atoms = global_deposit.balance_atoms.checked_sub(num_atoms)?;
339        Ok(())
340    }
341
342    /// Add GlobalTrader to the tree of global traders
343    pub fn add_trader(&mut self, trader: &Pubkey) -> ProgramResult {
344        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
345
346        let free_address_trader: DataIndex = get_free_address_on_global_fixed(fixed, dynamic);
347        let free_address_deposit: DataIndex = get_free_address_on_global_fixed(fixed, dynamic);
348        let mut global_trader_tree: GlobalTraderTree =
349            GlobalTraderTree::new(dynamic, fixed.global_traders_root_index, NIL);
350        let global_trader: GlobalTrader = GlobalTrader::new_empty(trader, free_address_deposit);
351
352        require!(
353            global_trader_tree.lookup_index(&global_trader) == NIL,
354            crate::program::ManifestError::AlreadyClaimedSeat,
355            "Already claimed global trader seat",
356        )?;
357
358        global_trader_tree.insert(free_address_trader, global_trader);
359        fixed.global_traders_root_index = global_trader_tree.get_root_index();
360        require!(
361            fixed.num_seats_claimed < MAX_GLOBAL_SEATS,
362            crate::program::ManifestError::TooManyGlobalSeats,
363            "There is a strict limit on number of seats available in a global, use evict",
364        )?;
365
366        fixed.num_seats_claimed += 1;
367
368        let global_deposit: GlobalDeposit = GlobalDeposit::new_empty(trader);
369        let mut global_deposit_tree: GlobalDepositTree = GlobalDepositTree::new(
370            dynamic,
371            fixed.global_deposits_root_index,
372            fixed.global_deposits_max_index,
373        );
374        global_deposit_tree.insert(free_address_deposit, global_deposit);
375        fixed.global_deposits_root_index = global_deposit_tree.get_root_index();
376        fixed.global_deposits_max_index = global_deposit_tree.get_max_index();
377
378        Ok(())
379    }
380
381    /// Evict from the global account and steal their seat
382    pub fn evict_and_take_seat(
383        &mut self,
384        existing_trader: &Pubkey,
385        new_trader: &Pubkey,
386    ) -> ProgramResult {
387        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
388
389        let existing_global_trader_opt: Option<&GlobalTrader> =
390            get_global_trader(fixed, dynamic, existing_trader);
391        require!(
392            existing_global_trader_opt.is_some(),
393            crate::program::ManifestError::MissingGlobal,
394            "Could not find global trader for {}",
395            existing_trader
396        )?;
397        let existing_global_trader: GlobalTrader = *existing_global_trader_opt.unwrap();
398
399        let existing_global_deposit_opt: Option<&mut GlobalDeposit> =
400            get_mut_global_deposit(fixed, dynamic, existing_trader);
401        require!(
402            existing_global_deposit_opt.is_some(),
403            crate::program::ManifestError::MissingGlobal,
404            "Could not find global deposit for {}",
405            existing_trader
406        )?;
407        let existing_global_deposit: &mut GlobalDeposit = existing_global_deposit_opt.unwrap();
408
409        let existing_global_atoms_deposited: GlobalAtoms = existing_global_deposit.balance_atoms;
410        require!(
411            existing_global_atoms_deposited == GlobalAtoms::ZERO,
412            crate::program::ManifestError::GlobalInsufficient,
413            "Error in emptying the existing global",
414        )?;
415
416        // Verification that the max index is the deposit index we are taking happens before withdraw.
417        let global_trader_tree: GlobalTraderTree =
418            GlobalTraderTree::new(dynamic, fixed.global_traders_root_index, NIL);
419        let existing_trader_index: DataIndex =
420            global_trader_tree.lookup_index(&existing_global_trader);
421        let existing_global_trader: &GlobalTrader =
422            get_helper::<RBNode<GlobalTrader>>(dynamic, existing_trader_index).get_value();
423        let existing_deposit_index: DataIndex = existing_global_trader.deposit_index;
424
425        // Update global trader
426        {
427            let mut global_trader_tree: GlobalTraderTree =
428                GlobalTraderTree::new(dynamic, fixed.global_traders_root_index, NIL);
429            require!(
430                existing_deposit_index == fixed.global_deposits_max_index,
431                crate::program::ManifestError::GlobalInsufficient,
432                "Only can remove trader with lowest deposit"
433            )?;
434            let new_global_trader: GlobalTrader =
435                GlobalTrader::new_empty(new_trader, fixed.global_deposits_max_index);
436
437            global_trader_tree.remove_by_index(existing_trader_index);
438
439            // Cannot claim an extra seat.
440            require!(
441                global_trader_tree.lookup_index(&new_global_trader) == NIL,
442                crate::program::ManifestError::AlreadyClaimedSeat,
443                "Already claimed global trader seat",
444            )?;
445
446            global_trader_tree.insert(existing_trader_index, new_global_trader);
447            fixed.global_traders_root_index = global_trader_tree.get_root_index();
448        }
449
450        // Update global deposits
451        {
452            let new_global_deposit: GlobalDeposit = GlobalDeposit::new_empty(new_trader);
453            let mut global_deposit_tree: GlobalDepositTree = GlobalDepositTree::new(
454                dynamic,
455                fixed.global_deposits_root_index,
456                fixed.global_deposits_max_index,
457            );
458            global_deposit_tree.remove_by_index(existing_deposit_index);
459            global_deposit_tree.insert(existing_deposit_index, new_global_deposit);
460            fixed.global_deposits_max_index = global_deposit_tree.get_max_index();
461            fixed.global_deposits_root_index = global_deposit_tree.get_root_index();
462        }
463
464        Ok(())
465    }
466
467    /// Add global order to the global account and specific market.
468    pub fn add_order(
469        &mut self,
470        resting_order: &RestingOrder,
471        global_trade_owner: &Pubkey,
472    ) -> ProgramResult {
473        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
474
475        let num_global_atoms: GlobalAtoms = if resting_order.get_is_bid() {
476            GlobalAtoms::new(
477                resting_order
478                    .get_num_base_atoms()
479                    .checked_mul(resting_order.get_price(), true)
480                    .unwrap()
481                    .as_u64(),
482            )
483        } else {
484            GlobalAtoms::new(resting_order.get_num_base_atoms().as_u64())
485        };
486
487        // Verify that there are enough deposited atoms.
488        {
489            let global_deposit_opt: Option<&mut GlobalDeposit> =
490                get_mut_global_deposit(fixed, dynamic, global_trade_owner);
491            require!(
492                global_deposit_opt.is_some(),
493                crate::program::ManifestError::MissingGlobal,
494                "Could not find global deposit for {}",
495                global_trade_owner
496            )?;
497            let global_deposit: &mut GlobalDeposit = global_deposit_opt.unwrap();
498
499            let global_atoms_deposited: GlobalAtoms = global_deposit.balance_atoms;
500
501            // This can be trivial to circumvent by using flash loans. This is just
502            // an informational safety check.
503            require!(
504                num_global_atoms <= global_atoms_deposited,
505                crate::program::ManifestError::GlobalInsufficient,
506                "Insufficient funds for global order needed {} has {}",
507                num_global_atoms,
508                global_atoms_deposited
509            )?;
510        }
511
512        Ok(())
513    }
514
515    /// Deposit to global account.
516    pub fn deposit_global(&mut self, trader: &Pubkey, num_atoms: GlobalAtoms) -> ProgramResult {
517        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
518        let global_deposit_opt: Option<&mut GlobalDeposit> =
519            get_mut_global_deposit(fixed, dynamic, trader);
520        require!(
521            global_deposit_opt.is_some(),
522            crate::program::ManifestError::MissingGlobal,
523            "Could not find global deposit for {}",
524            trader
525        )?;
526        let global_deposit: &mut GlobalDeposit = global_deposit_opt.unwrap();
527        global_deposit.balance_atoms = global_deposit.balance_atoms.checked_add(num_atoms)?;
528
529        Ok(())
530    }
531
532    /// Withdraw from global account.
533    pub fn withdraw_global(&mut self, trader: &Pubkey, num_atoms: GlobalAtoms) -> ProgramResult {
534        let DynamicAccount { fixed, dynamic } = self.borrow_mut_global();
535        let global_deposit_opt: Option<&mut GlobalDeposit> =
536            get_mut_global_deposit(fixed, dynamic, trader);
537        require!(
538            global_deposit_opt.is_some(),
539            crate::program::ManifestError::MissingGlobal,
540            "Could not find global deposit for {}",
541            trader
542        )?;
543        let global_deposit: &mut GlobalDeposit = global_deposit_opt.unwrap();
544        // Checked sub makes sure there are enough funds.
545        global_deposit.balance_atoms = global_deposit.balance_atoms.checked_sub(num_atoms)?;
546
547        Ok(())
548    }
549}
550
551fn get_free_address_on_global_fixed(fixed: &mut GlobalFixed, dynamic: &mut [u8]) -> DataIndex {
552    let mut free_list: FreeList<GlobalUnusedFreeListPadding> =
553        FreeList::new(dynamic, fixed.free_list_head_index);
554    let free_address: DataIndex = free_list.remove();
555    fixed.free_list_head_index = free_list.get_head();
556    free_address
557}
558
559fn get_global_trader<'a>(
560    fixed: &'a GlobalFixed,
561    dynamic: &'a [u8],
562    trader: &'a Pubkey,
563) -> Option<&'a GlobalTrader> {
564    let global_trader_tree: GlobalTraderTreeReadOnly =
565        GlobalTraderTreeReadOnly::new(dynamic, fixed.global_traders_root_index, NIL);
566    let global_trader_index: DataIndex =
567        global_trader_tree.lookup_index(&GlobalTrader::new_empty(trader, NIL));
568    if global_trader_index == NIL {
569        return None;
570    }
571    let global_trader: &GlobalTrader =
572        get_helper::<RBNode<GlobalTrader>>(dynamic, global_trader_index).get_value();
573    Some(global_trader)
574}
575
576fn get_mut_global_deposit<'a>(
577    fixed: &'a mut GlobalFixed,
578    dynamic: &'a mut [u8],
579    trader: &'a Pubkey,
580) -> Option<&'a mut GlobalDeposit> {
581    let global_trader_tree: GlobalTraderTree =
582        GlobalTraderTree::new(dynamic, fixed.global_traders_root_index, NIL);
583    let global_trader_index: DataIndex =
584        global_trader_tree.lookup_index(&GlobalTrader::new_empty(trader, NIL));
585    if global_trader_index == NIL {
586        return None;
587    }
588    let global_trader: &GlobalTrader =
589        get_helper::<RBNode<GlobalTrader>>(dynamic, global_trader_index).get_value();
590    let global_deposit_index: DataIndex = global_trader.deposit_index;
591    Some(get_mut_helper::<RBNode<GlobalDeposit>>(dynamic, global_deposit_index).get_mut_value())
592}
593
594fn get_global_deposit<'a>(
595    fixed: &'a GlobalFixed,
596    dynamic: &'a [u8],
597    trader: &'a Pubkey,
598) -> Option<&'a GlobalDeposit> {
599    let global_trader_tree: GlobalTraderTreeReadOnly =
600        GlobalTraderTreeReadOnly::new(dynamic, fixed.global_traders_root_index, NIL);
601    let global_trader_index: DataIndex =
602        global_trader_tree.lookup_index(&GlobalTrader::new_empty(trader, NIL));
603    if global_trader_index == NIL {
604        return None;
605    }
606    let global_trader: &GlobalTrader =
607        get_helper::<RBNode<GlobalTrader>>(dynamic, global_trader_index).get_value();
608    let global_deposit_index: DataIndex = global_trader.deposit_index;
609    Some(get_helper::<RBNode<GlobalDeposit>>(dynamic, global_deposit_index).get_value())
610}
611
612#[cfg(test)]
613mod test {
614    use super::*;
615
616    #[test]
617    fn test_display_trader() {
618        format!("{}", GlobalTrader::default());
619    }
620
621    #[test]
622    fn test_cmp_trader() {
623        // Just use token program ids since those have known sort order.
624        let global_trader1: GlobalTrader = GlobalTrader::new_empty(&spl_token::id(), NIL);
625        let global_trader2: GlobalTrader = GlobalTrader::new_empty(&spl_token_2022::id(), NIL);
626        assert!(global_trader1 < global_trader2);
627        assert!(global_trader1 != global_trader2);
628    }
629
630    #[test]
631    fn test_display_deposit() {
632        format!("{}", GlobalDeposit::default());
633    }
634
635    #[test]
636    fn test_cmp_deposit() {
637        let global_deposit1: GlobalDeposit = GlobalDeposit::new_empty(&Pubkey::new_unique());
638        let mut global_deposit2: GlobalDeposit = GlobalDeposit::new_empty(&Pubkey::new_unique());
639        global_deposit2.balance_atoms = GlobalAtoms::new(1);
640        // Reversed order than expected because Hypertrees give max pointer, but we want a min balance.
641        assert!(global_deposit1 > global_deposit2);
642        assert!(global_deposit1 != global_deposit2);
643    }
644}