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
76 let (from_token, to_token) = match side {
78 Side::Bid => (&ctx.accounts.pc_wallet, &ctx.accounts.market.coin_wallet),
79 Side::Ask => (&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet),
80 };
81
82 let from_amount_before = token::accessor::amount(from_token)?;
84 let to_amount_before = token::accessor::amount(to_token)?;
85 let orderbook: OrderbookClient<'info> = (&*ctx.accounts).into();
87 match side {
88 Side::Bid => orderbook.buy(amount, None)?,
89 Side::Ask => orderbook.sell(amount, None)?,
90 };
91 orderbook.settle(referral)?;
92 let from_amount_after = token::accessor::amount(from_token)?;
94 let to_amount_after = token::accessor::amount(to_token)?;
95 let from_amount = from_amount_before.checked_sub(from_amount_after).unwrap();
97 let to_amount = to_amount_after.checked_sub(to_amount_before).unwrap();
98 apply_risk_checks(DidSwap {
100 authority: *ctx.accounts.authority.key,
101 given_amount: amount,
102 min_exchange_rate,
103 from_amount,
104 to_amount,
105 quote_amount: 0,
106 spill_amount: 0,
107 from_mint: token::accessor::mint(from_token)?,
108 to_mint: token::accessor::mint(to_token)?,
109 quote_mint: match side {
110 Side::Bid => token::accessor::mint(from_token)?,
111 Side::Ask => token::accessor::mint(to_token)?,
112 },
113 })?;
114
115 Ok(())
116 }
117
118 #[access_control(is_valid_swap_transitive(&ctx))]
134 pub fn swap_transitive<'info>(
135 ctx: Context<'_, '_, '_, 'info, SwapTransitive<'info>>,
136 amount: u64,
137 min_exchange_rate: ExchangeRate,
138 ) -> Result<()> {
139 let referral = ctx.remaining_accounts.iter().next().map(Clone::clone);
141
142 let (from_amount, sell_proceeds) = {
144 let base_before = token::accessor::amount(&ctx.accounts.from.coin_wallet)?;
146 let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?;
147
148 let orderbook = ctx.accounts.orderbook_from();
150 orderbook.sell(amount, None)?;
151 orderbook.settle(referral.clone())?;
152
153 let base_after = token::accessor::amount(&ctx.accounts.from.coin_wallet)?;
155 let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?;
156
157 (
159 base_before.checked_sub(base_after).unwrap(),
160 quote_after.checked_sub(quote_before).unwrap(),
161 )
162 };
163
164 let (to_amount, buy_proceeds) = {
166 let base_before = token::accessor::amount(&ctx.accounts.to.coin_wallet)?;
168 let quote_before = token::accessor::amount(&ctx.accounts.pc_wallet)?;
169
170 let orderbook = ctx.accounts.orderbook_to();
172 orderbook.buy(sell_proceeds, None)?;
173 orderbook.settle(referral)?;
174
175 let base_after = token::accessor::amount(&ctx.accounts.to.coin_wallet)?;
177 let quote_after = token::accessor::amount(&ctx.accounts.pc_wallet)?;
178
179 (
181 base_after.checked_sub(base_before).unwrap(),
182 quote_before.checked_sub(quote_after).unwrap(),
183 )
184 };
185
186 let spill_amount = sell_proceeds.checked_sub(buy_proceeds).unwrap();
189
190 apply_risk_checks(DidSwap {
192 given_amount: amount,
193 min_exchange_rate,
194 from_amount,
195 to_amount,
196 quote_amount: sell_proceeds,
197 spill_amount,
198 from_mint: token::accessor::mint(&ctx.accounts.from.coin_wallet)?,
199 to_mint: token::accessor::mint(&ctx.accounts.to.coin_wallet)?,
200 quote_mint: token::accessor::mint(&ctx.accounts.pc_wallet)?,
201 authority: *ctx.accounts.authority.key,
202 })?;
203
204 Ok(())
205 }
206}
207
208fn apply_risk_checks(event: DidSwap) -> Result<()> {
210 emit!(event);
212 if event.to_amount == 0 {
213 return Err(ErrorCode::ZeroSwap.into());
214 }
215
216 let min_expected_amount = u128::from(
226 event.from_amount,
228 )
229 .checked_mul(
230 event.min_exchange_rate.rate.into(),
232 )
233 .unwrap()
234 .checked_mul(
235 10u128
237 .checked_pow(event.min_exchange_rate.quote_decimals.into())
238 .unwrap(),
239 )
240 .unwrap();
241
242 let effective_to_amount = {
246 let spill_surplus = match event.spill_amount == 0 || event.min_exchange_rate.strict {
251 true => 0,
252 false => u128::from(
253 event.to_amount,
255 )
256 .checked_mul(
257 event.spill_amount.into(),
259 )
260 .unwrap()
261 .checked_mul(
262 10u128
264 .checked_pow(event.min_exchange_rate.from_decimals.into())
265 .unwrap(),
266 )
267 .unwrap()
268 .checked_mul(
269 10u128
271 .checked_pow(event.min_exchange_rate.quote_decimals.into())
272 .unwrap(),
273 )
274 .unwrap()
275 .checked_div(
276 event
278 .quote_amount
279 .checked_sub(event.spill_amount)
280 .unwrap()
281 .into(),
282 )
283 .unwrap(),
284 };
285
286 let to_amount = u128::from(
288 event.to_amount,
290 )
291 .checked_mul(
292 10u128
294 .checked_pow(event.min_exchange_rate.from_decimals.into())
295 .unwrap(),
296 )
297 .unwrap()
298 .checked_mul(
299 10u128
301 .checked_pow(event.min_exchange_rate.quote_decimals.into())
302 .unwrap(),
303 )
304 .unwrap();
305
306 to_amount.checked_add(spill_surplus).unwrap()
307 };
308
309 if effective_to_amount < min_expected_amount {
311 msg!(
312 "effective_to_amount, min_expected_amount: {:?}, {:?}",
313 effective_to_amount,
314 min_expected_amount,
315 );
316 return Err(ErrorCode::SlippageExceeded.into());
317 }
318
319 Ok(())
320}
321
322#[derive(Accounts)]
323pub struct InitAccount<'info> {
324 #[account(mut)]
325 open_orders: AccountInfo<'info>,
326 #[account(signer)]
327 authority: AccountInfo<'info>,
328 market: AccountInfo<'info>,
329 dex_program: AccountInfo<'info>,
330 rent: AccountInfo<'info>,
331}
332
333impl<'info> From<&mut InitAccount<'info>> for dex::InitOpenOrders<'info> {
334 fn from(accs: &mut InitAccount<'info>) -> dex::InitOpenOrders<'info> {
335 dex::InitOpenOrders {
336 open_orders: accs.open_orders.clone(),
337 authority: accs.authority.clone(),
338 market: accs.market.clone(),
339 rent: accs.rent.clone(),
340 }
341 }
342}
343
344#[derive(Accounts)]
345pub struct CloseAccount<'info> {
346 #[account(mut)]
347 open_orders: AccountInfo<'info>,
348 #[account(signer)]
349 authority: AccountInfo<'info>,
350 #[account(mut)]
351 destination: AccountInfo<'info>,
352 market: AccountInfo<'info>,
353 dex_program: AccountInfo<'info>,
354}
355
356impl<'info> From<&mut CloseAccount<'info>> for dex::CloseOpenOrders<'info> {
357 fn from(accs: &mut CloseAccount<'info>) -> dex::CloseOpenOrders<'info> {
358 dex::CloseOpenOrders {
359 open_orders: accs.open_orders.clone(),
360 authority: accs.authority.clone(),
361 destination: accs.destination.clone(),
362 market: accs.market.clone(),
363 }
364 }
365}
366
367#[derive(Accounts)]
371pub struct Swap<'info> {
372 pub market: MarketAccounts<'info>,
373 #[account(signer)]
374 pub authority: AccountInfo<'info>,
375 #[account(mut, constraint = pc_wallet.key != &empty::ID)]
376 pub pc_wallet: AccountInfo<'info>,
377 pub dex_program: AccountInfo<'info>,
379 pub token_program: AccountInfo<'info>,
380 pub rent: AccountInfo<'info>,
382}
383
384impl<'info> From<&Swap<'info>> for OrderbookClient<'info> {
385 fn from(accounts: &Swap<'info>) -> OrderbookClient<'info> {
386 OrderbookClient {
387 market: accounts.market.clone(),
388 authority: accounts.authority.clone(),
389 pc_wallet: accounts.pc_wallet.clone(),
390 dex_program: accounts.dex_program.clone(),
391 token_program: accounts.token_program.clone(),
392 rent: accounts.rent.clone(),
393 }
394 }
395}
396
397#[derive(Accounts)]
402pub struct SwapTransitive<'info> {
403 pub from: MarketAccounts<'info>,
404 pub to: MarketAccounts<'info>,
405 #[account(signer)]
407 pub authority: AccountInfo<'info>,
408 #[account(mut, constraint = pc_wallet.key != &empty::ID)]
409 pub pc_wallet: AccountInfo<'info>,
410 pub dex_program: AccountInfo<'info>,
412 pub token_program: AccountInfo<'info>,
413 pub rent: AccountInfo<'info>,
415}
416
417impl<'info> SwapTransitive<'info> {
418 fn orderbook_from(&self) -> OrderbookClient<'info> {
419 OrderbookClient {
420 market: self.from.clone(),
421 authority: self.authority.clone(),
422 pc_wallet: self.pc_wallet.clone(),
423 dex_program: self.dex_program.clone(),
424 token_program: self.token_program.clone(),
425 rent: self.rent.clone(),
426 }
427 }
428 fn orderbook_to(&self) -> OrderbookClient<'info> {
429 OrderbookClient {
430 market: self.to.clone(),
431 authority: self.authority.clone(),
432 pc_wallet: self.pc_wallet.clone(),
433 dex_program: self.dex_program.clone(),
434 token_program: self.token_program.clone(),
435 rent: self.rent.clone(),
436 }
437 }
438}
439
440#[derive(Clone)]
442struct OrderbookClient<'info> {
443 market: MarketAccounts<'info>,
444 authority: AccountInfo<'info>,
445 pc_wallet: AccountInfo<'info>,
446 dex_program: AccountInfo<'info>,
447 token_program: AccountInfo<'info>,
448 rent: AccountInfo<'info>,
449}
450
451impl<'info> OrderbookClient<'info> {
452 fn sell(
458 &self,
459 base_amount: u64,
460 srm_msrm_discount: Option<AccountInfo<'info>>,
461 ) -> ProgramResult {
462 let limit_price = 1;
463 let max_coin_qty = {
464 let market = MarketState::load(&self.market.market, &dex::ID)?;
466 coin_lots(&market, base_amount)
467 };
468 let max_native_pc_qty = u64::MAX;
469 self.order_cpi(
470 limit_price,
471 max_coin_qty,
472 max_native_pc_qty,
473 Side::Ask,
474 srm_msrm_discount,
475 )
476 }
477
478 fn buy(
484 &self,
485 quote_amount: u64,
486 srm_msrm_discount: Option<AccountInfo<'info>>,
487 ) -> ProgramResult {
488 let limit_price = u64::MAX;
489 let max_coin_qty = u64::MAX;
490 let max_native_pc_qty = quote_amount;
491 self.order_cpi(
492 limit_price,
493 max_coin_qty,
494 max_native_pc_qty,
495 Side::Bid,
496 srm_msrm_discount,
497 )
498 }
499
500 fn order_cpi(
509 &self,
510 limit_price: u64,
511 max_coin_qty: u64,
512 max_native_pc_qty: u64,
513 side: Side,
514 srm_msrm_discount: Option<AccountInfo<'info>>,
515 ) -> ProgramResult {
516 let client_order_id = 0;
518 let limit = 65535;
522 let mut ctx = CpiContext::new(self.dex_program.clone(), self.clone().into());
523 if let Some(srm_msrm_discount) = srm_msrm_discount {
524 ctx = ctx.with_remaining_accounts(vec![srm_msrm_discount]);
525 }
526 dex::new_order_v3(
527 ctx,
528 side.into(),
529 NonZeroU64::new(limit_price).unwrap(),
530 NonZeroU64::new(max_coin_qty).unwrap(),
531 NonZeroU64::new(max_native_pc_qty).unwrap(),
532 SelfTradeBehavior::DecrementTake,
533 OrderType::ImmediateOrCancel,
534 client_order_id,
535 limit,
536 )
537 }
538
539 fn settle(&self, referral: Option<AccountInfo<'info>>) -> ProgramResult {
540 let settle_accs = dex::SettleFunds {
541 market: self.market.market.clone(),
542 open_orders: self.market.open_orders.clone(),
543 open_orders_authority: self.authority.clone(),
544 coin_vault: self.market.coin_vault.clone(),
545 pc_vault: self.market.pc_vault.clone(),
546 coin_wallet: self.market.coin_wallet.clone(),
547 pc_wallet: self.pc_wallet.clone(),
548 vault_signer: self.market.vault_signer.clone(),
549 token_program: self.token_program.clone(),
550 };
551 let mut ctx = CpiContext::new(self.dex_program.clone(), settle_accs);
552 if let Some(referral) = referral {
553 ctx = ctx.with_remaining_accounts(vec![referral]);
554 }
555 dex::settle_funds(ctx)
556 }
557}
558
559impl<'info> From<OrderbookClient<'info>> for dex::NewOrderV3<'info> {
560 fn from(c: OrderbookClient<'info>) -> dex::NewOrderV3<'info> {
561 dex::NewOrderV3 {
562 market: c.market.market.clone(),
563 open_orders: c.market.open_orders.clone(),
564 request_queue: c.market.request_queue.clone(),
565 event_queue: c.market.event_queue.clone(),
566 market_bids: c.market.bids.clone(),
567 market_asks: c.market.asks.clone(),
568 order_payer_token_account: c.market.order_payer_token_account.clone(),
569 open_orders_authority: c.authority.clone(),
570 coin_vault: c.market.coin_vault.clone(),
571 pc_vault: c.market.pc_vault.clone(),
572 token_program: c.token_program.clone(),
573 rent: c.rent.clone(),
574 }
575 }
576}
577
578fn coin_lots(market: &MarketState, size: u64) -> u64 {
580 size.checked_div(market.coin_lot_size).unwrap()
581}
582
583#[derive(Accounts, Clone)]
586pub struct MarketAccounts<'info> {
587 #[account(mut)]
588 pub market: AccountInfo<'info>,
589 #[account(mut)]
590 pub open_orders: AccountInfo<'info>,
591 #[account(mut)]
592 pub request_queue: AccountInfo<'info>,
593 #[account(mut)]
594 pub event_queue: AccountInfo<'info>,
595 #[account(mut)]
596 pub bids: AccountInfo<'info>,
597 #[account(mut)]
598 pub asks: AccountInfo<'info>,
599 #[account(mut, constraint = order_payer_token_account.key != &empty::ID)]
604 pub order_payer_token_account: AccountInfo<'info>,
605 #[account(mut)]
608 pub coin_vault: AccountInfo<'info>,
609 #[account(mut)]
612 pub pc_vault: AccountInfo<'info>,
613 pub vault_signer: AccountInfo<'info>,
615 #[account(mut, constraint = coin_wallet.key != &empty::ID)]
617 pub coin_wallet: AccountInfo<'info>,
618}
619
620#[derive(AnchorSerialize, AnchorDeserialize)]
621pub enum Side {
622 Bid,
623 Ask,
624}
625
626impl From<Side> for SerumSide {
627 fn from(side: Side) -> SerumSide {
628 match side {
629 Side::Bid => SerumSide::Bid,
630 Side::Ask => SerumSide::Ask,
631 }
632 }
633}
634
635fn is_valid_swap(ctx: &Context<Swap>) -> Result<()> {
638 _is_valid_swap(&ctx.accounts.market.coin_wallet, &ctx.accounts.pc_wallet)
639}
640
641fn is_valid_swap_transitive(ctx: &Context<SwapTransitive>) -> Result<()> {
642 _is_valid_swap(&ctx.accounts.from.coin_wallet, &ctx.accounts.to.coin_wallet)
643}
644
645fn _is_valid_swap<'info>(from: &AccountInfo<'info>, to: &AccountInfo<'info>) -> Result<()> {
647 let from_token_mint = token::accessor::mint(from)?;
648 let to_token_mint = token::accessor::mint(to)?;
649 if from_token_mint == to_token_mint {
650 return Err(ErrorCode::SwapTokensCannotMatch.into());
651 }
652 Ok(())
653}
654
655#[event]
658pub struct DidSwap {
659 pub given_amount: u64,
661 pub min_exchange_rate: ExchangeRate,
665 pub from_amount: u64,
667 pub to_amount: u64,
669 pub quote_amount: u64,
672 pub spill_amount: u64,
676 pub from_mint: Pubkey,
678 pub to_mint: Pubkey,
680 pub quote_mint: Pubkey,
683 pub authority: Pubkey,
685}
686
687#[derive(AnchorSerialize, AnchorDeserialize)]
689pub struct ExchangeRate {
690 pub rate: u64,
694 pub from_decimals: u8,
696 pub quote_decimals: u8,
699 pub strict: bool,
718}
719
720#[error]
721pub enum ErrorCode {
722 #[msg("The tokens being swapped must have different mints")]
723 SwapTokensCannotMatch,
724 #[msg("Slippage tolerance exceeded")]
725 SlippageExceeded,
726 #[msg("No tokens received when swapping")]
727 ZeroSwap,
728}