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 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 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 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 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 dynamic_account.deposit(trader_index, in_atoms, is_base_in)?;
156
157 let base_atoms: BaseAtoms = if is_exact_in {
163 if is_base_in {
164 BaseAtoms::new(in_atoms)
166 } else {
167 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 dynamic_account.impact_base_atoms(
180 false,
181 QuoteAtoms::new(out_atoms),
182 &global_trade_accounts_opts,
183 )?
184 } else {
185 BaseAtoms::new(out_atoms)
187 }
188 };
189
190 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 if is_base_in {
270 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 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 "e_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 "e_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 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 "e_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 "e_vault,
348 &owner,
349 (initial_credit_quote_atoms.checked_sub(extra_quote_atoms)?).as_u64(),
350 )?;
351 }
352
353 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 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 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#[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")]
456fn 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#[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")]
501fn 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#[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")]
545fn 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#[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")]
593fn 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}