1pub extern crate NT_anchor_lang as anchor_lang;
10pub extern crate NT_anchor_spl as anchor_spl;
11use anchor_lang::prelude::*;
12use anchor_spl::dex;
13use anchor_spl::dex::serum_dex::instruction::SelfTradeBehavior;
14use anchor_spl::dex::serum_dex::matching::{OrderType, Side as SerumSide};
15use anchor_spl::dex::serum_dex::state::MarketState;
16use anchor_spl::token;
17use solana_program::declare_id;
18use std::num::NonZeroU64;
19
20declare_id!("22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD");mod empty {
25 use super::*;
26 declare_id!("HJt8Tjdsc9ms9i4WCZEzhzr4oyf3ANcdzXrNdLPFqm3M");
27}
28
29#[program]
30pub mod serum_swap {
31 use super::*;
32
33 pub fn init_account<'info>(ctx: Context<'_, '_, '_, 'info, InitAccount<'info>>) -> Result<()> {
35 let ctx = CpiContext::new(ctx.accounts.dex_program.clone(), ctx.accounts.into());
36 dex::init_open_orders(ctx)?;
37 Ok(())
38 }
39
40 pub fn close_account<'info>(
42 ctx: Context<'_, '_, '_, 'info, CloseAccount<'info>>,
43 ) -> Result<()> {
44 let ctx = CpiContext::new(ctx.accounts.dex_program.clone(), ctx.accounts.into());
45 dex::close_open_orders(ctx)?;
46 Ok(())
47 }
48
49 #[access_control(is_valid_swap(&ctx))]
63 pub fn swap<'info>(
64 ctx: Context<'_, '_, '_, 'info, Swap<'info>>,
65 side: Side,
66 amount: u64,
67 min_exchange_rate: ExchangeRate,
68 ) -> Result<()> {
69 let mut min_exchange_rate = min_exchange_rate;
70
71 min_exchange_rate.quote_decimals = 0;
73 let referral = ctx.remaining_accounts.iter().next().map(Clone::clone);
75 let (from_token, to_token) = match side {
77 Side::Bid => (&ctx.accounts.pc_wallet, &ctx.accounts.market.coin_wallet),
78 Side::Ask => (&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet),
79 };
80 let from_amount_before = token::accessor::amount(from_token)?;
82 let to_amount_before = token::accessor::amount(to_token)?;
83 let orderbook: OrderbookClient<'info> = (&*ctx.accounts).into();
85 match side {
86 Side::Bid => orderbook.buy(amount, None)?,
87 Side::Ask => orderbook.sell(amount, None)?,
88 };
89 orderbook.settle(referral)?;
90 let from_amount_after = token::accessor::amount(from_token)?;
92 let to_amount_after = token::accessor::amount(to_token)?;
93 let from_amount = from_amount_before.checked_sub(from_amount_after).unwrap();
95 let to_amount = to_amount_after.checked_sub(to_amount_before).unwrap();
96 apply_risk_checks(DidSwap {
98 authority: *ctx.accounts.authority.key,
99 given_amount: amount,
100 min_exchange_rate,
101 from_amount,
102 to_amount,
103 quote_amount: 0,
104 spill_amount: 0,
105 from_mint: token::accessor::mint(from_token)?,
106 to_mint: token::accessor::mint(to_token)?,
107 quote_mint: match side {
108 Side::Bid => token::accessor::mint(from_token)?,
109 Side::Ask => token::accessor::mint(to_token)?,
110 },
111 })?;
112
113 Ok(())
114 }
115
116 #[access_control(is_valid_swap_transitive(&ctx))]
132 pub fn swap_transitive<'info>(
133 ctx: Context<'_, '_, '_, 'info, SwapTransitive<'info>>,
134 amount: u64,
135 min_exchange_rate: ExchangeRate,
136 ) -> Result<()> {
137 let referral = ctx.remaining_accounts.iter().next().map(Clone::clone);
139
140 let (from_amount, sell_proceeds) = {
142 let base_before = token::accessor::amount(&ctx.accounts.from.coin_wallet)?;
144 let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?;
145
146 let orderbook = ctx.accounts.orderbook_from();
148 orderbook.sell(amount, None)?;
149 orderbook.settle(referral.clone())?;
150
151 let base_after = token::accessor::amount(&ctx.accounts.from.coin_wallet)?;
153 let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?;
154
155 (
157 base_before.checked_sub(base_after).unwrap(),
158 quote_after.checked_sub(quote_before).unwrap(),
159 )
160 };
161
162 let (to_amount, buy_proceeds) = {
164 let base_before = token::accessor::amount(&ctx.accounts.to.coin_wallet)?;
166 let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?;
167
168 let orderbook = ctx.accounts.orderbook_to();
170 orderbook.buy(sell_proceeds, None)?;
171 orderbook.settle(referral)?;
172
173 let base_after = token::accessor::amount(&ctx.accounts.to.coin_wallet)?;
175 let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?;
176
177 (
179 base_after.checked_sub(base_before).unwrap(),
180 quote_before.checked_sub(quote_after).unwrap(),
181 )
182 };
183
184 let spill_amount = sell_proceeds.checked_sub(buy_proceeds).unwrap();
187
188 apply_risk_checks(DidSwap {
190 given_amount: amount,
191 min_exchange_rate,
192 from_amount,
193 to_amount,
194 quote_amount: sell_proceeds,
195 spill_amount,
196 from_mint: token::accessor::mint(&ctx.accounts.from.coin_wallet)?,
197 to_mint: token::accessor::mint(&ctx.accounts.to.coin_wallet)?,
198 quote_mint: token::accessor::mint(&ctx.accounts.pc_wallet)?,
199 authority: *ctx.accounts.authority.key,
200 })?;
201
202 Ok(())
203 }
204}
205
206fn apply_risk_checks(event: DidSwap) -> Result<()> {
208 emit!(event);
210 if event.to_amount == 0 {
211 return Err(ErrorCode::ZeroSwap.into());
212 }
213
214 let min_expected_amount = u128::from(
224 event.from_amount,
226 )
227 .checked_mul(
228 event.min_exchange_rate.rate.into(),
230 )
231 .unwrap()
232 .checked_mul(
233 10u128
235 .checked_pow(event.min_exchange_rate.quote_decimals.into())
236 .unwrap(),
237 )
238 .unwrap();
239
240 let effective_to_amount = {
244 let spill_surplus = match event.spill_amount == 0 || event.min_exchange_rate.strict {
249 true => 0,
250 false => u128::from(
251 event.to_amount,
253 )
254 .checked_mul(
255 event.spill_amount.into(),
257 )
258 .unwrap()
259 .checked_mul(
260 10u128
262 .checked_pow(event.min_exchange_rate.from_decimals.into())
263 .unwrap(),
264 )
265 .unwrap()
266 .checked_mul(
267 10u128
269 .checked_pow(event.min_exchange_rate.quote_decimals.into())
270 .unwrap(),
271 )
272 .unwrap()
273 .checked_div(
274 event
276 .quote_amount
277 .checked_sub(event.spill_amount)
278 .unwrap()
279 .into(),
280 )
281 .unwrap(),
282 };
283
284 let to_amount = u128::from(
286 event.to_amount,
288 )
289 .checked_mul(
290 10u128
292 .checked_pow(event.min_exchange_rate.from_decimals.into())
293 .unwrap(),
294 )
295 .unwrap()
296 .checked_mul(
297 10u128
299 .checked_pow(event.min_exchange_rate.quote_decimals.into())
300 .unwrap(),
301 )
302 .unwrap();
303
304 to_amount.checked_add(spill_surplus).unwrap()
305 };
306
307 if effective_to_amount < min_expected_amount {
309 msg!(
310 "effective_to_amount, min_expected_amount: {:?}, {:?}",
311 effective_to_amount,
312 min_expected_amount,
313 );
314 return Err(ErrorCode::SlippageExceeded.into());
315 }
316
317 Ok(())
318}
319
320#[derive(Accounts)]
321pub struct InitAccount<'info> {
322 #[account(mut)]
323 open_orders: AccountInfo<'info>,
324 #[account(signer)]
325 authority: AccountInfo<'info>,
326 market: AccountInfo<'info>,
327 dex_program: AccountInfo<'info>,
328 rent: AccountInfo<'info>,
329}
330
331impl<'info> From<&mut InitAccount<'info>> for dex::InitOpenOrders<'info> {
332 fn from(accs: &mut InitAccount<'info>) -> dex::InitOpenOrders<'info> {
333 dex::InitOpenOrders {
334 open_orders: accs.open_orders.clone(),
335 authority: accs.authority.clone(),
336 market: accs.market.clone(),
337 rent: accs.rent.clone(),
338 }
339 }
340}
341
342#[derive(Accounts)]
343pub struct CloseAccount<'info> {
344 #[account(mut)]
345 open_orders: AccountInfo<'info>,
346 #[account(signer)]
347 authority: AccountInfo<'info>,
348 #[account(mut)]
349 destination: AccountInfo<'info>,
350 market: AccountInfo<'info>,
351 dex_program: AccountInfo<'info>,
352}
353
354impl<'info> From<&mut CloseAccount<'info>> for dex::CloseOpenOrders<'info> {
355 fn from(accs: &mut CloseAccount<'info>) -> dex::CloseOpenOrders<'info> {
356 dex::CloseOpenOrders {
357 open_orders: accs.open_orders.clone(),
358 authority: accs.authority.clone(),
359 destination: accs.destination.clone(),
360 market: accs.market.clone(),
361 }
362 }
363}
364
365#[derive(Accounts)]
369pub struct Swap<'info> {
370 pub market: MarketAccounts<'info>,
371 #[account(signer)]
372 pub authority: AccountInfo<'info>,
373 #[account(mut, constraint = pc_wallet.key != &empty::ID)]
374 pub pc_wallet: AccountInfo<'info>,
375 pub dex_program: AccountInfo<'info>,
377 pub token_program: AccountInfo<'info>,
378 pub rent: AccountInfo<'info>,
380}
381
382impl<'info> From<&Swap<'info>> for OrderbookClient<'info> {
383 fn from(accounts: &Swap<'info>) -> OrderbookClient<'info> {
384 OrderbookClient {
385 market: accounts.market.clone(),
386 authority: accounts.authority.clone(),
387 pc_wallet: accounts.pc_wallet.clone(),
388 dex_program: accounts.dex_program.clone(),
389 token_program: accounts.token_program.clone(),
390 rent: accounts.rent.clone(),
391 }
392 }
393}
394
395#[derive(Accounts)]
400pub struct SwapTransitive<'info> {
401 pub from: MarketAccounts<'info>,
402 pub to: MarketAccounts<'info>,
403 #[account(signer)]
405 pub authority: AccountInfo<'info>,
406 #[account(mut, constraint = pc_wallet.key != &empty::ID)]
407 pub pc_wallet: AccountInfo<'info>,
408 pub dex_program: AccountInfo<'info>,
410 pub token_program: AccountInfo<'info>,
411 pub rent: AccountInfo<'info>,
413}
414
415impl<'info> SwapTransitive<'info> {
416 fn orderbook_from(&self) -> OrderbookClient<'info> {
417 OrderbookClient {
418 market: self.from.clone(),
419 authority: self.authority.clone(),
420 pc_wallet: self.pc_wallet.clone(),
421 dex_program: self.dex_program.clone(),
422 token_program: self.token_program.clone(),
423 rent: self.rent.clone(),
424 }
425 }
426 fn orderbook_to(&self) -> OrderbookClient<'info> {
427 OrderbookClient {
428 market: self.to.clone(),
429 authority: self.authority.clone(),
430 pc_wallet: self.pc_wallet.clone(),
431 dex_program: self.dex_program.clone(),
432 token_program: self.token_program.clone(),
433 rent: self.rent.clone(),
434 }
435 }
436}
437
438#[derive(Clone)]
440struct OrderbookClient<'info> {
441 market: MarketAccounts<'info>,
442 authority: AccountInfo<'info>,
443 pc_wallet: AccountInfo<'info>,
444 dex_program: AccountInfo<'info>,
445 token_program: AccountInfo<'info>,
446 rent: AccountInfo<'info>,
447}
448
449impl<'info> OrderbookClient<'info> {
450 fn sell(
456 &self,
457 base_amount: u64,
458 srm_msrm_discount: Option<AccountInfo<'info>>,
459 ) -> ProgramResult {
460 let limit_price = 1;
461 let max_coin_qty = {
462 let market = MarketState::load(&self.market.market, &dex::ID)?;
464 coin_lots(&market, base_amount)
465 };
466 let max_native_pc_qty = u64::MAX;
467 self.order_cpi(
468 limit_price,
469 max_coin_qty,
470 max_native_pc_qty,
471 Side::Ask,
472 srm_msrm_discount,
473 )
474 }
475
476 fn buy(
482 &self,
483 quote_amount: u64,
484 srm_msrm_discount: Option<AccountInfo<'info>>,
485 ) -> ProgramResult {
486 let limit_price = u64::MAX;
487 let max_coin_qty = u64::MAX;
488 let max_native_pc_qty = quote_amount;
489 self.order_cpi(
490 limit_price,
491 max_coin_qty,
492 max_native_pc_qty,
493 Side::Bid,
494 srm_msrm_discount,
495 )
496 }
497
498 fn order_cpi(
507 &self,
508 limit_price: u64,
509 max_coin_qty: u64,
510 max_native_pc_qty: u64,
511 side: Side,
512 srm_msrm_discount: Option<AccountInfo<'info>>,
513 ) -> ProgramResult {
514 let client_order_id = 0;
516 let limit = 65535;
520 let mut ctx = CpiContext::new(self.dex_program.clone(), self.clone().into());
521 if let Some(srm_msrm_discount) = srm_msrm_discount {
522 ctx = ctx.with_remaining_accounts(vec![srm_msrm_discount]);
523 }
524 dex::new_order_v3(
525 ctx,
526 side.into(),
527 NonZeroU64::new(limit_price).unwrap(),
528 NonZeroU64::new(max_coin_qty).unwrap(),
529 NonZeroU64::new(max_native_pc_qty).unwrap(),
530 SelfTradeBehavior::DecrementTake,
531 OrderType::ImmediateOrCancel,
532 client_order_id,
533 limit,
534 )
535 }
536
537 fn settle(&self, referral: Option<AccountInfo<'info>>) -> ProgramResult {
538 let settle_accs = dex::SettleFunds {
539 market: self.market.market.clone(),
540 open_orders: self.market.open_orders.clone(),
541 open_orders_authority: self.authority.clone(),
542 coin_vault: self.market.coin_vault.clone(),
543 pc_vault: self.market.pc_vault.clone(),
544 coin_wallet: self.market.coin_wallet.clone(),
545 pc_wallet: self.pc_wallet.clone(),
546 vault_signer: self.market.vault_signer.clone(),
547 token_program: self.token_program.clone(),
548 };
549 let mut ctx = CpiContext::new(self.dex_program.clone(), settle_accs);
550 if let Some(referral) = referral {
551 ctx = ctx.with_remaining_accounts(vec![referral]);
552 }
553 dex::settle_funds(ctx)
554 }
555}
556
557impl<'info> From<OrderbookClient<'info>> for dex::NewOrderV3<'info> {
558 fn from(c: OrderbookClient<'info>) -> dex::NewOrderV3<'info> {
559 dex::NewOrderV3 {
560 market: c.market.market.clone(),
561 open_orders: c.market.open_orders.clone(),
562 request_queue: c.market.request_queue.clone(),
563 event_queue: c.market.event_queue.clone(),
564 market_bids: c.market.bids.clone(),
565 market_asks: c.market.asks.clone(),
566 order_payer_token_account: c.market.order_payer_token_account.clone(),
567 open_orders_authority: c.authority.clone(),
568 coin_vault: c.market.coin_vault.clone(),
569 pc_vault: c.market.pc_vault.clone(),
570 token_program: c.token_program.clone(),
571 rent: c.rent.clone(),
572 }
573 }
574}
575
576fn coin_lots(market: &MarketState, size: u64) -> u64 {
578 size.checked_div(market.coin_lot_size).unwrap()
579}
580
581#[derive(Accounts, Clone)]
584pub struct MarketAccounts<'info> {
585 #[account(mut)]
586 pub market: AccountInfo<'info>,
587 #[account(mut)]
588 pub open_orders: AccountInfo<'info>,
589 #[account(mut)]
590 pub request_queue: AccountInfo<'info>,
591 #[account(mut)]
592 pub event_queue: AccountInfo<'info>,
593 #[account(mut)]
594 pub bids: AccountInfo<'info>,
595 #[account(mut)]
596 pub asks: AccountInfo<'info>,
597 #[account(mut, constraint = order_payer_token_account.key != &empty::ID)]
602 pub order_payer_token_account: AccountInfo<'info>,
603 #[account(mut)]
606 pub coin_vault: AccountInfo<'info>,
607 #[account(mut)]
610 pub pc_vault: AccountInfo<'info>,
611 pub vault_signer: AccountInfo<'info>,
613 #[account(mut, constraint = coin_wallet.key != &empty::ID)]
615 pub coin_wallet: AccountInfo<'info>,
616}
617
618#[derive(AnchorSerialize, AnchorDeserialize)]
619pub enum Side {
620 Bid,
621 Ask,
622}
623
624impl From<Side> for SerumSide {
625 fn from(side: Side) -> SerumSide {
626 match side {
627 Side::Bid => SerumSide::Bid,
628 Side::Ask => SerumSide::Ask,
629 }
630 }
631}
632
633fn is_valid_swap(ctx: &Context<Swap>) -> Result<()> {
636 _is_valid_swap(&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet)
637}
638
639fn is_valid_swap_transitive(ctx: &Context<SwapTransitive>) -> Result<()> {
640 _is_valid_swap(&ctx.accounts.from.coin_wallet, &ctx.accounts.to.coin_wallet)
641}
642
643fn _is_valid_swap<'info>(from: &AccountInfo<'info>, to: &AccountInfo<'info>) -> Result<()> {
645 let from_token_mint = token::accessor::mint(from)?;
646 let to_token_mint = token::accessor::mint(to)?;
647 if from_token_mint == to_token_mint {
648 return Err(ErrorCode::SwapTokensCannotMatch.into());
649 }
650 Ok(())
651}
652
653#[event]
656pub struct DidSwap {
657 pub given_amount: u64,
659 pub min_exchange_rate: ExchangeRate,
663 pub from_amount: u64,
665 pub to_amount: u64,
667 pub quote_amount: u64,
670 pub spill_amount: u64,
674 pub from_mint: Pubkey,
676 pub to_mint: Pubkey,
678 pub quote_mint: Pubkey,
681 pub authority: Pubkey,
683}
684
685#[derive(AnchorSerialize, AnchorDeserialize)]
687pub struct ExchangeRate {
688 pub rate: u64,
692 pub from_decimals: u8,
694 pub quote_decimals: u8,
697 pub strict: bool,
716}
717
718#[error]
719pub enum ErrorCode {
720 #[msg("The tokens being swapped must have different mints")]
721 SwapTokensCannotMatch,
722 #[msg("Slippage tolerance exceeded")]
723 SlippageExceeded,
724 #[msg("No tokens received when swapping")]
725 ZeroSwap,
726}