phoenix/program/
accounts.rs

1use borsh::{BorshDeserialize, BorshSerialize};
2use bytemuck::{Pod, Zeroable};
3use sokoban::node_allocator::ZeroCopy;
4use solana_program::{keccak, program_error::ProgramError, pubkey::Pubkey};
5
6use crate::quantities::{
7    BaseAtomsPerBaseLot, QuoteAtomsPerBaseUnitPerTick, QuoteAtomsPerQuoteLot, WrapperU64,
8};
9
10use super::status::{MarketStatus, SeatApprovalStatus};
11
12/// This function returns the canonical discriminant of the given type. It is the result
13/// of hashing together the program ID and the name of the type.
14///
15/// Suppose a program has an account type named `Foo` and another type named `Bar`.
16/// A common attack vector would be to pass an account of type `Bar` to a function
17/// expecting an account of type `Foo`, but by checking the discriminants, the function
18/// would be able to detect that the `Bar` account is not of the expected type `Foo`.
19pub fn get_discriminant<T>() -> Result<u64, ProgramError> {
20    let type_name = std::any::type_name::<T>();
21    let discriminant = u64::from_le_bytes(
22        keccak::hashv(&[crate::ID.as_ref(), type_name.as_bytes()]).as_ref()[..8]
23            .try_into()
24            .map_err(|_| {
25                phoenix_log!("Failed to convert discriminant hash to u64");
26                ProgramError::InvalidAccountData
27            })?,
28    );
29    phoenix_log!("Discriminant for {} is {}", type_name, discriminant);
30    Ok(discriminant)
31}
32
33#[derive(Default, Debug, Copy, Clone, BorshDeserialize, BorshSerialize, Zeroable, Pod)]
34#[repr(C)]
35pub struct MarketSizeParams {
36    pub bids_size: u64,
37    pub asks_size: u64,
38    pub num_seats: u64,
39}
40impl ZeroCopy for MarketSizeParams {}
41
42#[derive(Debug, Copy, Clone, BorshDeserialize, BorshSerialize, Zeroable, Pod)]
43#[repr(C)]
44pub struct TokenParams {
45    /// Number of decimals for the token (e.g. 9 for SOL, 6 for USDC).
46    pub decimals: u32,
47
48    /// Bump used for generating the PDA for the market's token vault.
49    pub vault_bump: u32,
50
51    /// Pubkey of the token mint.
52    pub mint_key: Pubkey,
53
54    /// Pubkey of the token vault.
55    pub vault_key: Pubkey,
56}
57impl ZeroCopy for TokenParams {}
58
59#[derive(Debug, Clone, Copy, Zeroable, Pod)]
60#[repr(C)]
61pub struct MarketHeader {
62    pub discriminant: u64,
63    pub status: u64,
64    pub market_size_params: MarketSizeParams,
65    pub base_params: TokenParams,
66    base_lot_size: BaseAtomsPerBaseLot,
67    pub quote_params: TokenParams,
68    quote_lot_size: QuoteAtomsPerQuoteLot,
69    tick_size_in_quote_atoms_per_base_unit: QuoteAtomsPerBaseUnitPerTick,
70    pub authority: Pubkey,
71    pub fee_recipient: Pubkey,
72    pub market_sequence_number: u64,
73    pub successor: Pubkey,
74    pub raw_base_units_per_base_unit: u32,
75    _padding1: u32,
76    _padding2: [u64; 32],
77}
78impl ZeroCopy for MarketHeader {}
79
80impl MarketHeader {
81    #[allow(clippy::too_many_arguments)]
82    pub fn new(
83        market_size_params: MarketSizeParams,
84        base_params: TokenParams,
85        base_lot_size: BaseAtomsPerBaseLot,
86        quote_params: TokenParams,
87        quote_lot_size: QuoteAtomsPerQuoteLot,
88        tick_size_in_quote_atoms_per_base_unit: QuoteAtomsPerBaseUnitPerTick,
89        authority: Pubkey,
90        successor: Pubkey,
91        fee_recipient: Pubkey,
92        raw_base_units_per_base_unit: u32,
93    ) -> Self {
94        Self {
95            discriminant: get_discriminant::<MarketHeader>().unwrap(),
96            status: MarketStatus::PostOnly as u64,
97            market_size_params,
98            base_params,
99            base_lot_size,
100            quote_params,
101            quote_lot_size,
102            tick_size_in_quote_atoms_per_base_unit,
103            authority,
104            fee_recipient,
105            market_sequence_number: 0,
106            successor,
107            raw_base_units_per_base_unit,
108            _padding1: 0,
109            _padding2: [0; 32],
110        }
111    }
112
113    /// Converts a price from quote atoms per base unit to ticks.
114    pub fn price_in_ticks(&self, price: u64) -> u64 {
115        price / self.tick_size_in_quote_atoms_per_base_unit.as_u64()
116    }
117
118    pub fn get_base_lot_size(&self) -> BaseAtomsPerBaseLot {
119        self.base_lot_size
120    }
121
122    pub fn get_quote_lot_size(&self) -> QuoteAtomsPerQuoteLot {
123        self.quote_lot_size
124    }
125
126    pub fn get_tick_size_in_quote_atoms_per_base_unit(&self) -> QuoteAtomsPerBaseUnitPerTick {
127        self.tick_size_in_quote_atoms_per_base_unit
128    }
129
130    pub fn increment_sequence_number(&mut self) {
131        self.market_sequence_number += 1;
132    }
133}
134
135/// This struct represents the state of a seat. Only traders with seats can
136/// place limit orders on the market. The seat is valid when the approval_status
137/// field is set to Approved. The initial state is NotApproved, and the seat will
138/// be retired if it is a Retired state.
139#[derive(Debug, Clone, Copy, BorshDeserialize, BorshSerialize, Zeroable, Pod)]
140#[repr(C)]
141pub struct Seat {
142    pub discriminant: u64,
143    pub market: Pubkey,
144    pub trader: Pubkey,
145    pub approval_status: u64,
146    // Padding
147    _padding: [u64; 6],
148}
149
150impl ZeroCopy for Seat {}
151
152impl Seat {
153    pub fn new_init(market: Pubkey, trader: Pubkey) -> Result<Self, ProgramError> {
154        Ok(Self {
155            discriminant: get_discriminant::<Seat>()?,
156            market,
157            trader,
158            approval_status: SeatApprovalStatus::NotApproved as u64,
159            _padding: [0; 6],
160        })
161    }
162}
163
164// Always run tests before every deploy
165#[test]
166fn test_valid_discriminants() {
167    assert_eq!(
168        std::any::type_name::<MarketHeader>(),
169        "phoenix::program::accounts::MarketHeader"
170    );
171    assert_eq!(
172        std::any::type_name::<Seat>(),
173        "phoenix::program::accounts::Seat"
174    );
175    assert_eq!(
176        get_discriminant::<MarketHeader>().unwrap(),
177        8167313896524341111
178    );
179    assert_eq!(get_discriminant::<Seat>().unwrap(), 2002603505298356104);
180}