manifest/program/processor/
swap.rs

1use std::cell::RefMut;
2
3use crate::{
4    logs::{emit_stack, PlaceOrderLogV2},
5    program::expand_market_if_needed,
6    quantities::{BaseAtoms, QuoteAtoms, QuoteAtomsPerBaseAtom, WrapperU64},
7    require,
8    state::{
9        AddOrderToMarketArgs, AddOrderToMarketResult, MarketRefMut, OrderType,
10        NO_EXPIRATION_LAST_VALID_SLOT,
11    },
12    validation::loaders::SwapContext,
13};
14#[cfg(not(feature = "certora"))]
15use crate::{
16    market_vault_seeds_with_bump,
17    program::{invoke, ManifestError},
18};
19use borsh::{BorshDeserialize, BorshSerialize};
20use hypertree::{trace, DataIndex, NIL};
21use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
22
23use super::shared::get_mut_dynamic_account;
24
25#[cfg(feature = "certora")]
26use {
27    crate::certora::summaries::place_order::place_fully_match_order_with_same_base_and_quote,
28    early_panic::early_panic,
29    solana_cvt::token::{spl_token_2022_transfer, spl_token_transfer},
30};
31
32use crate::validation::{MintAccountInfo, Signer, TokenAccountInfo, TokenProgram};
33use solana_program::program_error::ProgramError;
34
35#[derive(BorshDeserialize, BorshSerialize)]
36pub struct SwapParams {
37    pub in_atoms: u64,
38    pub out_atoms: u64,
39    pub is_base_in: bool,
40    // Exact in is a technical term that doesnt actually mean exact. It is
41    // desired. If not that much can be fulfilled, less will be allowed assuming
42    // the min_out/max_in is satisfied.
43    pub is_exact_in: bool,
44}
45
46impl SwapParams {
47    pub fn new(in_atoms: u64, out_atoms: u64, is_base_in: bool, is_exact_in: bool) -> Self {
48        SwapParams {
49            in_atoms,
50            out_atoms,
51            is_base_in,
52            is_exact_in,
53        }
54    }
55}
56
57pub(crate) fn process_swap(
58    program_id: &Pubkey,
59    accounts: &[AccountInfo],
60    data: &[u8],
61) -> ProgramResult {
62    let params = SwapParams::try_from_slice(data)?;
63    process_swap_core(program_id, accounts, params)
64}
65
66#[cfg_attr(all(feature = "certora", not(feature = "certora-test")), early_panic)]
67pub(crate) fn process_swap_core(
68    _program_id: &Pubkey,
69    accounts: &[AccountInfo],
70    params: SwapParams,
71) -> ProgramResult {
72    let swap_context: SwapContext = SwapContext::load(accounts)?;
73
74    let SwapContext {
75        market,
76        payer,
77        owner,
78        trader_base: trader_base_account,
79        trader_quote: trader_quote_account,
80        base_vault,
81        quote_vault,
82        token_program_base,
83        token_program_quote,
84        base_mint,
85        quote_mint,
86        global_trade_accounts_opts,
87    } = swap_context;
88
89    let (existing_seat_index, trader_index, initial_base_atoms, initial_quote_atoms) = {
90        let market_data: &mut RefMut<&mut [u8]> = &mut market.try_borrow_mut_data()?;
91        let mut dynamic_account: MarketRefMut = get_mut_dynamic_account(market_data);
92
93        // Claim seat if needed
94        let existing_seat_index: DataIndex = dynamic_account.get_trader_index(owner.key);
95        if existing_seat_index == NIL {
96            dynamic_account.claim_seat(owner.key)?;
97        }
98        let trader_index: DataIndex = dynamic_account.get_trader_index(owner.key);
99
100        let (initial_base_atoms, initial_quote_atoms) =
101            dynamic_account.get_trader_balance(owner.key);
102
103        (
104            existing_seat_index,
105            trader_index,
106            initial_base_atoms,
107            initial_quote_atoms,
108        )
109    };
110
111    // Might need a free list spot for both the temporary claimed seat as well
112    // as for a partially filled reverse order.
113    expand_market_if_needed(&payer, &market)?;
114
115    let market_data: &mut RefMut<&mut [u8]> = &mut market.try_borrow_mut_data()?;
116    let mut dynamic_account: MarketRefMut = get_mut_dynamic_account(market_data);
117
118    let SwapParams {
119        in_atoms,
120        out_atoms,
121        is_base_in,
122        is_exact_in,
123    } = params;
124
125    trace!("swap in_atoms:{in_atoms} out_atoms:{out_atoms} is_base_in:{is_base_in} is_exact_in:{is_exact_in}");
126
127    // This check is redundant with the check that will be done within token
128    // program on deposit, but it is done here to future proof in case we later
129    // remove checked math.
130    // This actually adds a new restriction that the wallet can fully fund the
131    // swap instead of a combination of wallet and existing withdrawable
132    // balance.
133    if is_exact_in {
134        if is_base_in {
135            require!(
136                in_atoms <= trader_base_account.get_balance_atoms(),
137                ManifestError::Overflow,
138                "Insufficient base in atoms for swap has: {} requires: {}",
139                trader_base_account.get_balance_atoms(),
140                in_atoms,
141            )?;
142        } else {
143            require!(
144                in_atoms <= trader_quote_account.get_balance_atoms(),
145                ManifestError::Overflow,
146                "Insufficient quote in atoms for swap has: {} requires: {}",
147                trader_quote_account.get_balance_atoms(),
148                in_atoms,
149            )?;
150        }
151    }
152
153    // this is a virtual credit to ensure matching always proceeds
154    // net token transfers will be handled later
155    dynamic_account.deposit(trader_index, in_atoms, is_base_in)?;
156
157    // 4 cases:
158    // 1. Exact in base. Simplest case, just use the base atoms given.
159    // 2. Exact in quote. Search the asks for the number of base atoms in bids to match.
160    // 3. Exact out quote. Search the bids for the number of base atoms needed to match to get the right quote out.
161    // 4. Exact out base. Use the number of out atoms as the number of atoms to place_order against.
162    let base_atoms: BaseAtoms = if is_exact_in {
163        if is_base_in {
164            // input=desired(base) output=min(quote)
165            BaseAtoms::new(in_atoms)
166        } else {
167            // input=desired(quote)* output=min(base)
168            // round down base amount to not cross quote limit
169            dynamic_account.impact_base_atoms(
170                true,
171                QuoteAtoms::new(in_atoms),
172                &global_trade_accounts_opts,
173            )?
174        }
175    } else {
176        if is_base_in {
177            // input=max(base) output=desired(quote)
178            // round up base amount to ensure not staying below quote limit
179            dynamic_account.impact_base_atoms(
180                false,
181                QuoteAtoms::new(out_atoms),
182                &global_trade_accounts_opts,
183            )?
184        } else {
185            // input=max(quote) output=desired(base)
186            BaseAtoms::new(out_atoms)
187        }
188    };
189
190    // Note that in the case of fully exhausting the book, exact in/out will not
191    // be respected. It should be treated as a desired in/out. This pushes the
192    // burden of checking the results onto the caller program.
193
194    // Example case is exact quote in. User wants exact quote in of 1_000_000
195    // and min base out of 1_000. Suppose they fully exhaust the book and get
196    // out 2_000 but that is not enough to fully use the entire 1_000_000. In
197    // this case the ix will succeed.
198
199    // Another interesting case is exact quote out. Suppose the user is doing
200    // exact quote out 1_000_000 with max_base_in of 1_000. If it fully exhausts
201    // the book without using the entire max_base_in and that is still not
202    // enough for the exact quote amount, the transaction will still succeed.
203
204    let price: QuoteAtomsPerBaseAtom = if is_base_in {
205        QuoteAtomsPerBaseAtom::MIN
206    } else {
207        QuoteAtomsPerBaseAtom::MAX
208    };
209    let last_valid_slot: u32 = NO_EXPIRATION_LAST_VALID_SLOT;
210    let order_type: OrderType = OrderType::ImmediateOrCancel;
211
212    trace!("swap in:{in_atoms} out:{out_atoms} base/quote:{is_base_in} in/out:{is_exact_in} base:{base_atoms} price:{price}",);
213
214    let AddOrderToMarketResult {
215        base_atoms_traded,
216        quote_atoms_traded,
217        order_sequence_number,
218        order_index,
219        ..
220    } = place_order(
221        &mut dynamic_account,
222        AddOrderToMarketArgs {
223            market: *market.key,
224            trader_index,
225            num_base_atoms: base_atoms,
226            price,
227            is_bid: !is_base_in,
228            last_valid_slot,
229            order_type,
230            global_trade_accounts_opts: &global_trade_accounts_opts,
231            current_slot: None,
232        },
233    )?;
234
235    if is_exact_in {
236        let out_atoms_traded: u64 = if is_base_in {
237            quote_atoms_traded.as_u64()
238        } else {
239            base_atoms_traded.as_u64()
240        };
241        require!(
242            out_atoms <= out_atoms_traded,
243            ManifestError::InsufficientOut,
244            "Insufficient out atoms returned. Minimum: {} Actual: {}",
245            out_atoms,
246            out_atoms_traded
247        )?;
248    } else {
249        let in_atoms_traded = if is_base_in {
250            base_atoms_traded.as_u64()
251        } else {
252            quote_atoms_traded.as_u64()
253        };
254        require!(
255            in_atoms >= in_atoms_traded,
256            ManifestError::InsufficientOut,
257            "Excessive in atoms charged. Maximum: {} Actual: {}",
258            in_atoms,
259            in_atoms_traded
260        )?;
261    }
262
263    let (end_base_atoms, end_quote_atoms) = dynamic_account.get_trader_balance(owner.key);
264
265    let extra_base_atoms: BaseAtoms = end_base_atoms.checked_sub(initial_base_atoms)?;
266    let extra_quote_atoms: QuoteAtoms = end_quote_atoms.checked_sub(initial_quote_atoms)?;
267
268    // Transfer tokens
269    if is_base_in {
270        // Trader is depositing base.
271
272        // In order to make the trade, we previously credited the seat with the
273        // maximum they could possibly need,
274        // The amount to take from them is repaying the full credit, minus the
275        // unused amount.
276        let initial_credit_base_atoms: BaseAtoms = BaseAtoms::new(in_atoms);
277
278        if *token_program_base.key == spl_token_2022::id() {
279            spl_token_2022_transfer_from_trader_to_vault(
280                &token_program_base,
281                &trader_base_account,
282                base_mint,
283                dynamic_account.fixed.get_base_mint(),
284                &base_vault,
285                &owner,
286                (initial_credit_base_atoms.checked_sub(extra_base_atoms)?).as_u64(),
287                dynamic_account.fixed.get_base_mint_decimals(),
288            )?;
289        } else {
290            spl_token_transfer_from_trader_to_vault(
291                &token_program_base,
292                &trader_base_account,
293                &base_vault,
294                &owner,
295                (initial_credit_base_atoms.checked_sub(extra_base_atoms)?).as_u64(),
296            )?;
297        }
298
299        // Give all but what started there.
300        let quote_vault_bump: u8 = dynamic_account.fixed.get_quote_vault_bump();
301        if *token_program_quote.key == spl_token_2022::id() {
302            spl_token_2022_transfer_from_vault_to_trader(
303                &token_program_quote,
304                quote_mint,
305                dynamic_account.fixed.get_quote_mint(),
306                &quote_vault,
307                &trader_quote_account,
308                extra_quote_atoms.as_u64(),
309                dynamic_account.fixed.get_quote_mint_decimals(),
310                market.key,
311                quote_vault_bump,
312            )?;
313        } else {
314            spl_token_transfer_from_vault_to_trader(
315                &token_program_quote,
316                &quote_vault,
317                &trader_quote_account,
318                extra_quote_atoms.as_u64(),
319                market.key,
320                quote_vault_bump,
321                dynamic_account.fixed.get_quote_mint(),
322            )?;
323        }
324    } else {
325        // Trader is depositing quote.
326
327        // In order to make the trade, we previously credited the seat with the
328        // maximum they could possibly need.
329        // The amount to take from them is repaying the full credit, minus the
330        // unused amount.
331        let initial_credit_quote_atoms: QuoteAtoms = QuoteAtoms::new(in_atoms);
332        if *token_program_quote.key == spl_token_2022::id() {
333            spl_token_2022_transfer_from_trader_to_vault(
334                &token_program_quote,
335                &trader_quote_account,
336                quote_mint,
337                dynamic_account.fixed.get_quote_mint(),
338                &quote_vault,
339                &owner,
340                (initial_credit_quote_atoms.checked_sub(extra_quote_atoms)?).as_u64(),
341                dynamic_account.fixed.get_quote_mint_decimals(),
342            )?;
343        } else {
344            spl_token_transfer_from_trader_to_vault(
345                &token_program_quote,
346                &trader_quote_account,
347                &quote_vault,
348                &owner,
349                (initial_credit_quote_atoms.checked_sub(extra_quote_atoms)?).as_u64(),
350            )?;
351        }
352
353        // Give all but what started there.
354        let base_vault_bump: u8 = dynamic_account.fixed.get_base_vault_bump();
355        if *token_program_base.key == spl_token_2022::id() {
356            spl_token_2022_transfer_from_vault_to_trader(
357                &token_program_base,
358                base_mint,
359                dynamic_account.get_base_mint(),
360                &base_vault,
361                &trader_base_account,
362                extra_base_atoms.as_u64(),
363                dynamic_account.fixed.get_base_mint_decimals(),
364                market.key,
365                base_vault_bump,
366            )?;
367        } else {
368            spl_token_transfer_from_vault_to_trader(
369                &token_program_base,
370                &base_vault,
371                &trader_base_account,
372                extra_base_atoms.as_u64(),
373                market.key,
374                base_vault_bump,
375                dynamic_account.get_base_mint(),
376            )?;
377        }
378    }
379
380    if existing_seat_index == NIL {
381        dynamic_account.release_seat(owner.key)?;
382    } else {
383        // Withdraw in case there already was a seat so it doesnt mess with their
384        // balances. Need to withdraw base and quote in case the order wasnt fully
385        // filled.
386        dynamic_account.withdraw(trader_index, extra_base_atoms.as_u64(), true)?;
387        dynamic_account.withdraw(trader_index, extra_quote_atoms.as_u64(), false)?;
388    }
389    // Verify that there wasnt a reverse order that took the only spare block.
390    require!(
391        dynamic_account.has_free_block(),
392        ManifestError::InvalidFreeList,
393        "Cannot swap against a reverse order unless there is a free block"
394    )?;
395
396    emit_stack(PlaceOrderLogV2 {
397        market: *market.key,
398        trader: *owner.key,
399        payer: *payer.key,
400        base_atoms,
401        price,
402        order_type,
403        is_bid: (!is_base_in).into(),
404        _padding: [0; 6],
405        order_sequence_number,
406        order_index,
407        last_valid_slot,
408    })?;
409
410    Ok(())
411}
412
413#[cfg(not(feature = "certora"))]
414fn place_order(
415    dynamic_account: &mut MarketRefMut,
416    args: AddOrderToMarketArgs,
417) -> Result<AddOrderToMarketResult, ProgramError> {
418    dynamic_account.place_order(args)
419}
420
421#[cfg(feature = "certora")]
422fn place_order(
423    market: &mut MarketRefMut,
424    args: AddOrderToMarketArgs,
425) -> Result<AddOrderToMarketResult, ProgramError> {
426    place_fully_match_order_with_same_base_and_quote(market, args)
427}
428
429/** Transfer from base (quote) trader to base (quote) vault using SPL Token **/
430#[cfg(not(feature = "certora"))]
431fn spl_token_transfer_from_trader_to_vault<'a, 'info>(
432    token_program: &TokenProgram<'a, 'info>,
433    trader_account: &TokenAccountInfo<'a, 'info>,
434    vault: &TokenAccountInfo<'a, 'info>,
435    owner: &Signer<'a, 'info>,
436    amount: u64,
437) -> ProgramResult {
438    invoke(
439        &spl_token::instruction::transfer(
440            token_program.key,
441            trader_account.key,
442            vault.key,
443            owner.key,
444            &[],
445            amount,
446        )?,
447        &[
448            token_program.as_ref().clone(),
449            trader_account.as_ref().clone(),
450            vault.as_ref().clone(),
451            owner.as_ref().clone(),
452        ],
453    )
454}
455#[cfg(feature = "certora")]
456/** (Summary) Transfer from base (quote) trader to base (quote) vault using SPL Token **/
457fn spl_token_transfer_from_trader_to_vault<'a, 'info>(
458    _token_program: &TokenProgram<'a, 'info>,
459    trader_account: &TokenAccountInfo<'a, 'info>,
460    vault: &TokenAccountInfo<'a, 'info>,
461    owner: &Signer<'a, 'info>,
462    amount: u64,
463) -> ProgramResult {
464    spl_token_transfer(trader_account.info, vault.info, owner.info, amount)
465}
466
467/** Transfer from base (quote) trader to base (quote) vault using SPL Token 2022 **/
468#[cfg(not(feature = "certora"))]
469fn spl_token_2022_transfer_from_trader_to_vault<'a, 'info>(
470    token_program: &TokenProgram<'a, 'info>,
471    trader_account: &TokenAccountInfo<'a, 'info>,
472    mint: Option<MintAccountInfo<'a, 'info>>,
473    mint_pubkey: &Pubkey,
474    vault: &TokenAccountInfo<'a, 'info>,
475    owner: &Signer<'a, 'info>,
476    amount: u64,
477    decimals: u8,
478) -> ProgramResult {
479    invoke(
480        &spl_token_2022::instruction::transfer_checked(
481            token_program.key,
482            trader_account.key,
483            mint_pubkey,
484            vault.key,
485            owner.key,
486            &[],
487            amount,
488            decimals,
489        )?,
490        &[
491            token_program.as_ref().clone(),
492            trader_account.as_ref().clone(),
493            vault.as_ref().clone(),
494            mint.unwrap().as_ref().clone(),
495            owner.as_ref().clone(),
496        ],
497    )
498}
499
500#[cfg(feature = "certora")]
501/** (Summary) Transfer from base (quote) trader to base (quote) vault using SPL Token 2022 **/
502fn spl_token_2022_transfer_from_trader_to_vault<'a, 'info>(
503    _token_program: &TokenProgram<'a, 'info>,
504    trader_account: &TokenAccountInfo<'a, 'info>,
505    _mint: Option<MintAccountInfo<'a, 'info>>,
506    _mint_pubkey: &Pubkey,
507    vault: &TokenAccountInfo<'a, 'info>,
508    owner: &Signer<'a, 'info>,
509    amount: u64,
510    _decimals: u8,
511) -> ProgramResult {
512    spl_token_2022_transfer(trader_account.info, vault.info, owner.info, amount)
513}
514
515/** Transfer from base (quote) vault to base (quote) trader using SPL Token **/
516#[cfg(not(feature = "certora"))]
517fn spl_token_transfer_from_vault_to_trader<'a, 'info>(
518    token_program: &TokenProgram<'a, 'info>,
519    vault: &TokenAccountInfo<'a, 'info>,
520    trader_account: &TokenAccountInfo<'a, 'info>,
521    amount: u64,
522    market_key: &Pubkey,
523    vault_bump: u8,
524    mint_pubkey: &Pubkey,
525) -> ProgramResult {
526    solana_program::program::invoke_signed(
527        &spl_token::instruction::transfer(
528            token_program.key,
529            vault.key,
530            trader_account.key,
531            vault.key,
532            &[],
533            amount,
534        )?,
535        &[
536            token_program.as_ref().clone(),
537            vault.as_ref().clone(),
538            trader_account.as_ref().clone(),
539        ],
540        market_vault_seeds_with_bump!(market_key, mint_pubkey, vault_bump),
541    )
542}
543
544#[cfg(feature = "certora")]
545/** (Summary) Transfer from base (quote) vault to base (quote) trader using SPL Token **/
546fn spl_token_transfer_from_vault_to_trader<'a, 'info>(
547    _token_program: &TokenProgram<'a, 'info>,
548    vault: &TokenAccountInfo<'a, 'info>,
549    trader_account: &TokenAccountInfo<'a, 'info>,
550    amount: u64,
551    _market_key: &Pubkey,
552    _vault_bump: u8,
553    _mint_pubkey: &Pubkey,
554) -> ProgramResult {
555    spl_token_transfer(vault.info, trader_account.info, vault.info, amount)
556}
557
558/** Transfer from base (quote) vault to base (quote) trader using SPL Token 2022 **/
559#[cfg(not(feature = "certora"))]
560fn spl_token_2022_transfer_from_vault_to_trader<'a, 'info>(
561    token_program: &TokenProgram<'a, 'info>,
562    mint: Option<MintAccountInfo<'a, 'info>>,
563    mint_pubkey: &Pubkey,
564    vault: &TokenAccountInfo<'a, 'info>,
565    trader_account: &TokenAccountInfo<'a, 'info>,
566    amount: u64,
567    decimals: u8,
568    market_key: &Pubkey,
569    vault_bump: u8,
570) -> ProgramResult {
571    solana_program::program::invoke_signed(
572        &spl_token_2022::instruction::transfer_checked(
573            token_program.key,
574            vault.key,
575            mint_pubkey,
576            trader_account.key,
577            vault.key,
578            &[],
579            amount,
580            decimals,
581        )?,
582        &[
583            token_program.as_ref().clone(),
584            vault.as_ref().clone(),
585            mint.unwrap().as_ref().clone(),
586            trader_account.as_ref().clone(),
587        ],
588        market_vault_seeds_with_bump!(market_key, mint_pubkey, vault_bump),
589    )
590}
591
592#[cfg(feature = "certora")]
593/** (Summary) Transfer from base (quote) vault to base (quote) trader using SPL Token 2022 **/
594fn spl_token_2022_transfer_from_vault_to_trader<'a, 'info>(
595    _token_program: &TokenProgram<'a, 'info>,
596    _mint: Option<MintAccountInfo<'a, 'info>>,
597    _mint_pubkey: &Pubkey,
598    vault: &TokenAccountInfo<'a, 'info>,
599    trader_account: &TokenAccountInfo<'a, 'info>,
600    amount: u64,
601    _decimals: u8,
602    _market_key: &Pubkey,
603    _vault_bump: u8,
604) -> ProgramResult {
605    spl_token_2022_transfer(vault.info, trader_account.info, vault.info, amount)
606}