manifest/state/
utils.rs

1use std::cell::RefMut;
2
3use crate::{
4    global_vault_seeds_with_bump,
5    logs::{emit_stack, GlobalCleanupLog},
6    program::{get_mut_dynamic_account, invoke},
7    quantities::{GlobalAtoms, WrapperU64},
8    require,
9    validation::{loaders::GlobalTradeAccounts, MintAccountInfo, TokenAccountInfo, TokenProgram},
10};
11use hypertree::{DataIndex, NIL};
12#[cfg(not(feature = "no-clock"))]
13use solana_program::sysvar::Sysvar;
14use solana_program::{
15    entrypoint::ProgramResult, program::invoke_signed, program_error::ProgramError, pubkey::Pubkey,
16};
17use spl_token_2022::{
18    extension::{
19        transfer_fee::TransferFeeConfig, transfer_hook::TransferHook, BaseStateWithExtensions,
20        StateWithExtensions,
21    },
22    state::Mint,
23};
24
25use super::{
26    order_type_can_take, GlobalRefMut, OrderType, RestingOrder, GAS_DEPOSIT_LAMPORTS,
27    NO_EXPIRATION_LAST_VALID_SLOT,
28};
29
30pub fn get_now_slot() -> u32 {
31    // If we cannot get the clock (happens in tests, then only match with
32    // orders without expiration). We assume that the clock cannot be
33    // maliciously manipulated to clear all orders with expirations on the
34    // orderbook.
35    #[cfg(feature = "no-clock")]
36    let now_slot: u64 = 0;
37    #[cfg(not(feature = "no-clock"))]
38    let now_slot: u64 = solana_program::clock::Clock::get()
39        .unwrap_or(solana_program::clock::Clock {
40            slot: u64::MAX,
41            epoch_start_timestamp: i64::MAX,
42            epoch: u64::MAX,
43            leader_schedule_epoch: u64::MAX,
44            unix_timestamp: i64::MAX,
45        })
46        .slot;
47    now_slot as u32
48}
49
50pub(crate) fn get_now_epoch() -> u64 {
51    #[cfg(feature = "no-clock")]
52    let now_epoch: u64 = 0;
53    #[cfg(not(feature = "no-clock"))]
54    let now_epoch: u64 = solana_program::clock::Clock::get()
55        .unwrap_or(solana_program::clock::Clock {
56            slot: u64::MAX,
57            epoch_start_timestamp: i64::MAX,
58            epoch: u64::MAX,
59            leader_schedule_epoch: u64::MAX,
60            unix_timestamp: i64::MAX,
61        })
62        .slot;
63    now_epoch
64}
65
66#[inline(always)]
67pub(crate) fn remove_from_global(
68    global_trade_accounts_opt: &Option<GlobalTradeAccounts>,
69) -> ProgramResult {
70    if global_trade_accounts_opt.is_none() {
71        // Payer is forfeiting the right to claim the gas prepayment. This
72        // results in a stranded gas prepayment on the global account.
73        return Ok(());
74    }
75    let global_trade_accounts: &GlobalTradeAccounts = &global_trade_accounts_opt.as_ref().unwrap();
76    let GlobalTradeAccounts {
77        global,
78        gas_receiver_opt,
79        ..
80    } = global_trade_accounts;
81
82    // The simple implementation gets
83    //
84    //     **receiver.lamports.borrow_mut() += GAS_DEPOSIT_LAMPORTS;
85    //     **global.lamports.borrow_mut() -= GAS_DEPOSIT_LAMPORTS;
86    //
87    // failed: sum of account balances before and after instruction do not match
88    //
89    // doesnt make sense, but thats the solana runtime.
90    //
91    // Done here instead of inside the object because the borrow checker needs
92    // to get the data on global which it cannot while there is a mut self
93    // reference. Note that if it isnt claimed here, then nobody does and it is
94    // lost to the global account.
95    //
96    // Then we tried to do a CPI, but that fails because
97    //
98    // `from` must not carry data
99    //
100    // if let Some(system_program) = &global_trade_accounts.system_program {
101    //     solana_program::program::invoke_signed(
102    //         &solana_program::system_instruction::transfer(
103    //             &global.key,
104    //             &trader.info.key,
105    //             GAS_DEPOSIT_LAMPORTS,
106    //         ),
107    //         &[global.info.clone(), trader.info.clone(), system_program.info.clone()],
108    //         global_seeds_with_bump!(mint, global_bump),
109    //     )?;
110    // }
111    //
112    // Somehow, a hybrid works. Dont know why, but it does.
113    //
114    if global_trade_accounts.system_program.is_some() {
115        **global.lamports.borrow_mut() -= GAS_DEPOSIT_LAMPORTS;
116        **gas_receiver_opt.as_ref().unwrap().lamports.borrow_mut() += GAS_DEPOSIT_LAMPORTS;
117    }
118
119    Ok(())
120}
121
122pub(crate) fn try_to_add_to_global(
123    global_trade_accounts: &GlobalTradeAccounts,
124    resting_order: &RestingOrder,
125) -> ProgramResult {
126    let GlobalTradeAccounts {
127        global,
128        gas_payer_opt,
129        ..
130    } = global_trade_accounts;
131
132    {
133        let global_data: &mut RefMut<&mut [u8]> = &mut global.try_borrow_mut_data()?;
134        let mut global_dynamic_account: GlobalRefMut = get_mut_dynamic_account(global_data);
135        global_dynamic_account.add_order(resting_order, gas_payer_opt.as_ref().unwrap().key)?;
136    }
137
138    // Need to CPI because otherwise we get:
139    //
140    // instruction spent from the balance of an account it does not own
141    //
142    // Done here instead of inside the object because the borrow checker needs
143    // to get the data on global which it cannot while there is a mut self
144    // reference.
145    invoke(
146        &solana_program::system_instruction::transfer(
147            &gas_payer_opt.as_ref().unwrap().info.key,
148            &global.key,
149            GAS_DEPOSIT_LAMPORTS,
150        ),
151        &[
152            gas_payer_opt.as_ref().unwrap().info.clone(),
153            global.info.clone(),
154        ],
155    )?;
156
157    Ok(())
158}
159
160pub(crate) fn assert_can_take(order_type: OrderType) -> ProgramResult {
161    require!(
162        order_type_can_take(order_type),
163        crate::program::ManifestError::PostOnlyCrosses,
164        "Post only order would cross",
165    )?;
166    Ok(())
167}
168
169pub(crate) fn assert_not_already_expired(last_valid_slot: u32, now_slot: u32) -> ProgramResult {
170    require!(
171        last_valid_slot == NO_EXPIRATION_LAST_VALID_SLOT || last_valid_slot > now_slot,
172        crate::program::ManifestError::AlreadyExpired,
173        "Placing an already expired order. now: {} last_valid: {}",
174        now_slot,
175        last_valid_slot
176    )?;
177    Ok(())
178}
179
180pub(crate) fn assert_already_has_seat(trader_index: DataIndex) -> ProgramResult {
181    require!(
182        trader_index != NIL,
183        crate::program::ManifestError::AlreadyClaimedSeat,
184        "Need to claim a seat first",
185    )?;
186    Ok(())
187}
188
189pub(crate) fn can_back_order<'a, 'info>(
190    global_trade_accounts_opt: &'a Option<GlobalTradeAccounts<'a, 'info>>,
191    resting_order_trader: &Pubkey,
192    desired_global_atoms: GlobalAtoms,
193) -> bool {
194    if global_trade_accounts_opt.is_none() {
195        return false;
196    }
197    let global_trade_accounts: &GlobalTradeAccounts = &global_trade_accounts_opt.as_ref().unwrap();
198    let GlobalTradeAccounts { global, .. } = global_trade_accounts;
199
200    let global_data: &mut RefMut<&mut [u8]> = &mut global.try_borrow_mut_data().unwrap();
201    let global_dynamic_account: GlobalRefMut = get_mut_dynamic_account(global_data);
202
203    let num_deposited_atoms: GlobalAtoms =
204        global_dynamic_account.get_balance_atoms(resting_order_trader);
205    return desired_global_atoms <= num_deposited_atoms;
206}
207
208pub(crate) fn try_to_move_global_tokens<'a, 'info>(
209    global_trade_accounts_opt: &'a Option<GlobalTradeAccounts<'a, 'info>>,
210    resting_order_trader: &Pubkey,
211    desired_global_atoms: GlobalAtoms,
212) -> Result<bool, ProgramError> {
213    require!(
214        global_trade_accounts_opt.is_some(),
215        crate::program::ManifestError::MissingGlobal,
216        "Missing global accounts when adding a global",
217    )?;
218    let global_trade_accounts: &GlobalTradeAccounts = &global_trade_accounts_opt.as_ref().unwrap();
219    let GlobalTradeAccounts {
220        global,
221        mint_opt,
222        global_vault_opt,
223        gas_receiver_opt,
224        market_vault_opt,
225        token_program_opt,
226        ..
227    } = global_trade_accounts;
228
229    let global_data: &mut RefMut<&mut [u8]> = &mut global.try_borrow_mut_data()?;
230    let mut global_dynamic_account: GlobalRefMut = get_mut_dynamic_account(global_data);
231
232    let num_deposited_atoms: GlobalAtoms =
233        global_dynamic_account.get_balance_atoms(resting_order_trader);
234    // Intentionally does not allow partial fills against a global order. The
235    // reason for this is to punish global orders that are not backed. There is
236    // no technical blocker for supporting partial fills against a global. It is
237    // just because of the mechanism design where we want global to only be used
238    // when needed, not just for all orders.
239    if desired_global_atoms > num_deposited_atoms {
240        emit_stack(GlobalCleanupLog {
241            cleaner: *gas_receiver_opt.as_ref().unwrap().key,
242            maker: *resting_order_trader,
243            amount_desired: desired_global_atoms,
244            amount_deposited: num_deposited_atoms,
245        })?;
246        return Ok(false);
247    }
248
249    // Update the GlobalTrader
250    global_dynamic_account.reduce(resting_order_trader, desired_global_atoms)?;
251
252    let mint_key: &Pubkey = global_dynamic_account.fixed.get_mint();
253
254    let global_vault_bump: u8 = global_dynamic_account.fixed.get_vault_bump();
255
256    let global_vault: &TokenAccountInfo<'a, 'info> = global_vault_opt.as_ref().unwrap();
257    let market_vault: &TokenAccountInfo<'a, 'info> = market_vault_opt.as_ref().unwrap();
258    let token_program: &TokenProgram<'a, 'info> = token_program_opt.as_ref().unwrap();
259
260    if *token_program.key == spl_token_2022::id() {
261        require!(
262            mint_opt.is_some(),
263            crate::program::ManifestError::MissingGlobal,
264            "Missing global mint",
265        )?;
266
267        // Prevent transfer from global to market vault if a token has a non-zero fee.
268        let mint_account_info: &MintAccountInfo = &mint_opt.as_ref().unwrap();
269        if StateWithExtensions::<Mint>::unpack(&mint_account_info.info.data.borrow())?
270            .get_extension::<TransferFeeConfig>()
271            .is_ok_and(|f| f.get_epoch_fee(get_now_epoch()).transfer_fee_basis_points != 0.into())
272        {
273            solana_program::msg!("Treating global order as unbacked because it has a transfer fee");
274            return Ok(false);
275        }
276        if StateWithExtensions::<Mint>::unpack(&mint_account_info.info.data.borrow())?
277            .get_extension::<TransferHook>()
278            .is_ok_and(|f| f.program_id.0 != Pubkey::default())
279        {
280            solana_program::msg!(
281                "Treating global order as unbacked because it has a transfer hook"
282            );
283            return Ok(false);
284        }
285
286        invoke_signed(
287            &spl_token_2022::instruction::transfer_checked(
288                token_program.key,
289                global_vault.key,
290                mint_account_info.info.key,
291                market_vault.key,
292                global_vault.key,
293                &[],
294                desired_global_atoms.as_u64(),
295                mint_account_info.mint.decimals,
296            )?,
297            &[
298                token_program.as_ref().clone(),
299                global_vault.as_ref().clone(),
300                mint_account_info.as_ref().clone(),
301                market_vault.as_ref().clone(),
302            ],
303            global_vault_seeds_with_bump!(mint_key, global_vault_bump),
304        )?;
305    } else {
306        invoke_signed(
307            &spl_token::instruction::transfer(
308                token_program.key,
309                global_vault.key,
310                market_vault.key,
311                global_vault.key,
312                &[],
313                desired_global_atoms.as_u64(),
314            )?,
315            &[
316                token_program.as_ref().clone(),
317                global_vault.as_ref().clone(),
318                market_vault.as_ref().clone(),
319            ],
320            global_vault_seeds_with_bump!(mint_key, global_vault_bump),
321        )?;
322    }
323
324    Ok(true)
325}