1use std::fmt;
2
3use crate::{
4 market::{BaseMarket, BaseMarketExt},
5 num::{MulDiv, Unsigned, UnsignedAbs},
6 params::Fees,
7 price::{Price, Prices},
8 BalanceExt, Delta, PnlFactorKind, Pool, SwapMarketExt, SwapMarketMut,
9};
10
11use num_traits::{CheckedAdd, CheckedMul, CheckedNeg, CheckedSub, Signed, Zero};
12
13use super::MarketAction;
14
15#[must_use = "actions do nothing unless you `execute` them"]
17pub struct Swap<M: BaseMarket<DECIMALS>, const DECIMALS: u8> {
18 market: M,
19 params: SwapParams<M::Num>,
20}
21
22impl<const DECIMALS: u8, M: SwapMarketMut<DECIMALS>> Swap<M, DECIMALS> {
23 pub fn try_new(
25 market: M,
26 is_token_in_long: bool,
27 token_in_amount: M::Num,
28 prices: Prices<M::Num>,
29 ) -> crate::Result<Self> {
30 if token_in_amount.is_zero() {
31 return Err(crate::Error::EmptySwap);
32 }
33 prices.validate()?;
34 Ok(Self {
35 market,
36 params: SwapParams {
37 is_token_in_long,
38 token_in_amount,
39 prices,
40 },
41 })
42 }
43
44 fn reassign_values(&self) -> crate::Result<ReassignedValues<M::Num>> {
47 if self.params.is_token_in_long {
48 let long_delta_value: M::Signed = self
49 .params
50 .token_in_amount
51 .checked_mul(&self.params.long_token_price().mid())
52 .ok_or(crate::Error::Computation("long delta value"))?
53 .try_into()
54 .map_err(|_| crate::Error::Convert)?;
55 Ok(ReassignedValues::new(
56 long_delta_value.clone(),
57 long_delta_value
58 .checked_neg()
59 .ok_or(crate::Error::Computation("negating long delta value"))?,
60 self.params.long_token_price().clone(),
61 self.params.short_token_price().clone(),
62 PnlFactorKind::MaxAfterDeposit,
63 PnlFactorKind::MaxAfterWithdrawal,
64 ))
65 } else {
66 let short_delta_value: M::Signed = self
67 .params
68 .token_in_amount
69 .checked_mul(&self.params.short_token_price().mid())
70 .ok_or(crate::Error::Computation("short delta value"))?
71 .try_into()
72 .map_err(|_| crate::Error::Convert)?;
73 Ok(ReassignedValues::new(
74 short_delta_value
75 .checked_neg()
76 .ok_or(crate::Error::Computation("negating short delta value"))?,
77 short_delta_value,
78 self.params.short_token_price().clone(),
79 self.params.long_token_price().clone(),
80 PnlFactorKind::MaxAfterWithdrawal,
81 PnlFactorKind::MaxAfterDeposit,
82 ))
83 }
84 }
85
86 fn charge_fees(&self, is_positive_impact: bool) -> crate::Result<(M::Num, Fees<M::Num>)> {
87 self.market
88 .swap_fee_params()?
89 .apply_fees(is_positive_impact, &self.params.token_in_amount)
90 .ok_or(crate::Error::Computation("apply fees"))
91 }
92
93 #[allow(clippy::type_complexity)]
94 fn try_execute(
95 &self,
96 ) -> crate::Result<(
97 Cache<'_, M, DECIMALS>,
98 SwapResult<M::Num, <M::Num as Unsigned>::Signed>,
99 )> {
100 let ReassignedValues {
101 long_token_delta_value,
102 short_token_delta_value,
103 token_in_price,
104 token_out_price,
105 long_pnl_factor_kind,
106 short_pnl_factor_kind,
107 } = self.reassign_values()?;
108
109 let price_impact = self
113 .market
114 .liquidity_pool()?
115 .pool_delta_with_values(
116 long_token_delta_value,
117 short_token_delta_value,
118 &self.params.long_token_price().mid(),
119 &self.params.short_token_price().mid(),
120 )?
121 .price_impact(&self.market.swap_impact_params()?)?;
122
123 let (amount_after_fees, fees) = self.charge_fees(price_impact.is_positive())?;
124
125 let claimable_fee =
126 self.market
127 .claimable_fee_pool()?
128 .checked_apply_delta(Delta::new_one_side(
129 self.params.is_token_in_long,
130 &fees.fee_amount_for_receiver().to_signed()?,
131 ))?;
132
133 let mut token_in_amount;
135 let token_out_amount;
136 let pool_amount_out;
137 let price_impact_amount;
138 let swap_impact;
139 if price_impact.is_positive() {
140 token_in_amount = amount_after_fees;
141
142 let swap_impact_deduct_side = !self.params.is_token_in_long;
143 let (signed_price_impact_amount, capped_diff_value) =
144 self.market.swap_impact_amount_with_cap(
145 swap_impact_deduct_side,
146 &token_out_price,
147 &price_impact,
148 )?;
149 debug_assert!(!signed_price_impact_amount.is_negative());
150
151 let capped_diff_token_in_amount = if capped_diff_value.is_zero() {
152 Zero::zero()
153 } else {
154 let (capped_diff_token_in_amount, _) = self.market.swap_impact_amount_with_cap(
157 self.params.is_token_in_long,
158 &token_in_price,
159 &capped_diff_value.to_signed()?,
160 )?;
161 debug_assert!(!capped_diff_token_in_amount.is_negative());
162 token_in_amount = token_in_amount
163 .checked_add(&capped_diff_token_in_amount.unsigned_abs())
164 .ok_or(crate::Error::Computation("swap: adding capped diff amount"))?;
165 capped_diff_token_in_amount
166 };
167
168 swap_impact =
169 self.market
170 .swap_impact_pool()?
171 .checked_apply_delta(Delta::new_both_sides(
172 swap_impact_deduct_side,
173 &signed_price_impact_amount.checked_neg().ok_or(
174 crate::Error::Computation("negating positive price impact amount "),
175 )?,
176 &capped_diff_token_in_amount
177 .checked_neg()
178 .ok_or(crate::Error::Computation("negating capped diff amount "))?,
179 ))?;
180 price_impact_amount = signed_price_impact_amount.unsigned_abs();
181
182 pool_amount_out = token_in_amount
183 .checked_mul_div(
184 token_in_price.pick_price(false),
185 token_out_price.pick_price(true),
186 )
187 .ok_or(crate::Error::Computation(
188 "pool amount out for positive impact",
189 ))?;
190 token_out_amount = pool_amount_out.checked_add(&price_impact_amount).ok_or(
192 crate::Error::Computation("token out amount for positive impact"),
193 )?;
194 } else {
195 let swap_impact_deduct_side = self.params.is_token_in_long;
196 let (signed_price_impact_amount, _) = self.market.swap_impact_amount_with_cap(
197 swap_impact_deduct_side,
198 &token_in_price,
199 &price_impact,
200 )?;
201 debug_assert!(!signed_price_impact_amount.is_positive());
202 swap_impact =
203 self.market
204 .swap_impact_pool()?
205 .checked_apply_delta(Delta::new_one_side(
206 swap_impact_deduct_side,
207 &signed_price_impact_amount.checked_neg().ok_or(
208 crate::Error::Computation("negating negative price impact amount "),
209 )?,
210 ))?;
211 price_impact_amount = signed_price_impact_amount.unsigned_abs();
212
213 token_in_amount = amount_after_fees.checked_sub(&price_impact_amount).ok_or(
214 crate::Error::Computation("swap: not enough fund to pay price impact"),
215 )?;
216
217 if token_in_amount.is_zero() {
218 return Err(crate::Error::Computation(
219 "swap: not enough fund to pay price impact",
220 ));
221 }
222
223 token_out_amount = token_in_amount
224 .checked_mul_div(
225 token_in_price.pick_price(false),
226 token_out_price.pick_price(true),
227 )
228 .ok_or(crate::Error::Computation(
229 "token out amount for negative impact",
230 ))?;
231 pool_amount_out = token_out_amount.clone();
232 }
233
234 let liquidity =
237 self.market
238 .liquidity_pool()?
239 .checked_apply_delta(Delta::new_both_sides(
240 self.params.is_token_in_long,
241 &token_in_amount
242 .checked_add(fees.fee_amount_for_pool())
243 .ok_or(crate::Error::Overflow)?
244 .to_signed()?,
245 &pool_amount_out.to_opposite_signed()?,
246 ))?;
247
248 let cache = Cache {
249 market: &self.market,
250 liquidity,
251 swap_impact,
252 claimable_fee,
253 };
254
255 cache.validate_pool_amount(self.params.is_token_in_long)?;
256 cache.validate_reserve(&self.params.prices, !self.params.is_token_in_long)?;
257 cache.validate_max_pnl(
258 &self.params.prices,
259 long_pnl_factor_kind,
260 short_pnl_factor_kind,
261 )?;
262
263 let result = SwapResult {
264 price_impact_value: price_impact,
265 token_in_fees: fees,
266 token_out_amount,
267 price_impact_amount,
268 };
269
270 Ok((cache, result))
271 }
272}
273
274impl<const DECIMALS: u8, M> MarketAction for Swap<M, DECIMALS>
275where
276 M: SwapMarketMut<DECIMALS>,
277{
278 type Report = SwapReport<M::Num, <M::Num as Unsigned>::Signed>;
279
280 fn execute(mut self) -> crate::Result<Self::Report> {
284 let (cache, result) = self.try_execute()?;
285
286 let Cache {
287 liquidity,
288 swap_impact,
289 claimable_fee,
290 ..
291 } = cache;
292
293 *self
294 .market
295 .liquidity_pool_mut()
296 .expect("liquidity pool must be valid") = liquidity;
297
298 *self
299 .market
300 .swap_impact_pool_mut()
301 .expect("swap impact pool must be valid") = swap_impact;
302
303 *self
304 .market
305 .claimable_fee_pool_mut()
306 .expect("claimable fee pool must be valid") = claimable_fee;
307
308 Ok(SwapReport {
309 params: self.params,
310 result,
311 })
312 }
313}
314
315#[derive(Debug, Clone, Copy)]
317#[cfg_attr(
318 feature = "anchor-lang",
319 derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
320)]
321pub struct SwapParams<T> {
322 is_token_in_long: bool,
323 token_in_amount: T,
324 prices: Prices<T>,
325}
326
327#[cfg(feature = "gmsol-utils")]
328impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for SwapParams<T> {
329 const INIT_SPACE: usize = bool::INIT_SPACE + T::INIT_SPACE + Prices::<T>::INIT_SPACE;
330}
331
332impl<T> SwapParams<T> {
333 pub fn long_token_price(&self) -> &Price<T> {
335 &self.prices.long_token_price
336 }
337
338 pub fn short_token_price(&self) -> &Price<T> {
340 &self.prices.short_token_price
341 }
342
343 pub fn is_token_in_long(&self) -> bool {
345 self.is_token_in_long
346 }
347
348 pub fn token_in_amount(&self) -> &T {
350 &self.token_in_amount
351 }
352}
353
354#[derive(Debug, Clone, Copy)]
355#[cfg_attr(
356 feature = "anchor-lang",
357 derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
358)]
359struct SwapResult<Unsigned, Signed> {
360 token_in_fees: Fees<Unsigned>,
361 token_out_amount: Unsigned,
362 price_impact_value: Signed,
363 price_impact_amount: Unsigned,
364}
365
366#[cfg(feature = "gmsol-utils")]
367impl<Unsigned, Signed> gmsol_utils::InitSpace for SwapResult<Unsigned, Signed>
368where
369 Unsigned: gmsol_utils::InitSpace,
370 Signed: gmsol_utils::InitSpace,
371{
372 const INIT_SPACE: usize = Fees::<Unsigned>::INIT_SPACE
373 + Unsigned::INIT_SPACE
374 + Signed::INIT_SPACE
375 + Unsigned::INIT_SPACE;
376}
377
378#[must_use = "`token_out_amount` must be used"]
380#[cfg_attr(
381 feature = "anchor-lang",
382 derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
383)]
384#[derive(Clone)]
385pub struct SwapReport<Unsigned, Signed> {
386 params: SwapParams<Unsigned>,
387 result: SwapResult<Unsigned, Signed>,
388}
389
390#[cfg(feature = "gmsol-utils")]
391impl<Unsigned, Signed> gmsol_utils::InitSpace for SwapReport<Unsigned, Signed>
392where
393 Unsigned: gmsol_utils::InitSpace,
394 Signed: gmsol_utils::InitSpace,
395{
396 const INIT_SPACE: usize =
397 SwapParams::<Unsigned>::INIT_SPACE + SwapResult::<Unsigned, Signed>::INIT_SPACE;
398}
399
400impl<T> fmt::Debug for SwapReport<T, T::Signed>
401where
402 T: Unsigned,
403 T: fmt::Debug,
404 T::Signed: fmt::Debug,
405{
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 f.debug_struct("SwapReport")
408 .field("params", &self.params)
409 .field("result", &self.result)
410 .finish()
411 }
412}
413
414impl<T: Unsigned> SwapReport<T, T::Signed> {
415 pub fn params(&self) -> &SwapParams<T> {
417 &self.params
418 }
419
420 pub fn token_in_fees(&self) -> &Fees<T> {
422 &self.result.token_in_fees
423 }
424
425 #[must_use = "the returned amount of tokens should be transferred out from the market vault"]
427 pub fn token_out_amount(&self) -> &T {
428 &self.result.token_out_amount
429 }
430
431 pub fn price_impact(&self) -> &T::Signed {
433 &self.result.price_impact_value
434 }
435
436 pub fn price_impact_amount(&self) -> &T {
438 &self.result.price_impact_amount
439 }
440}
441
442struct ReassignedValues<T: Unsigned> {
443 long_token_delta_value: T::Signed,
444 short_token_delta_value: T::Signed,
445 token_in_price: Price<T>,
446 token_out_price: Price<T>,
447 long_pnl_factor_kind: PnlFactorKind,
448 short_pnl_factor_kind: PnlFactorKind,
449}
450
451impl<T: Unsigned> ReassignedValues<T> {
452 fn new(
453 long_token_delta_value: T::Signed,
454 short_token_delta_value: T::Signed,
455 token_in_price: Price<T>,
456 token_out_price: Price<T>,
457 long_pnl_factor_kind: PnlFactorKind,
458 short_pnl_factor_kind: PnlFactorKind,
459 ) -> Self {
460 Self {
461 long_token_delta_value,
462 short_token_delta_value,
463 token_in_price,
464 token_out_price,
465 long_pnl_factor_kind,
466 short_pnl_factor_kind,
467 }
468 }
469}
470
471struct Cache<'a, M, const DECIMALS: u8>
472where
473 M: BaseMarket<DECIMALS>,
474{
475 market: &'a M,
476 liquidity: M::Pool,
477 swap_impact: M::Pool,
478 claimable_fee: M::Pool,
479}
480
481impl<M, const DECIMALS: u8> BaseMarket<DECIMALS> for Cache<'_, M, DECIMALS>
482where
483 M: BaseMarket<DECIMALS>,
484{
485 type Num = M::Num;
486
487 type Signed = M::Signed;
488
489 type Pool = M::Pool;
490
491 fn liquidity_pool(&self) -> crate::Result<&Self::Pool> {
492 Ok(&self.liquidity)
493 }
494
495 fn claimable_fee_pool(&self) -> crate::Result<&Self::Pool> {
496 Ok(&self.claimable_fee)
497 }
498
499 fn swap_impact_pool(&self) -> crate::Result<&Self::Pool> {
500 Ok(&self.swap_impact)
501 }
502
503 fn open_interest_pool(&self, is_long: bool) -> crate::Result<&Self::Pool> {
504 self.market.open_interest_pool(is_long)
505 }
506
507 fn open_interest_in_tokens_pool(&self, is_long: bool) -> crate::Result<&Self::Pool> {
508 self.market.open_interest_in_tokens_pool(is_long)
509 }
510
511 fn collateral_sum_pool(&self, is_long: bool) -> crate::Result<&Self::Pool> {
512 self.market.collateral_sum_pool(is_long)
513 }
514
515 fn usd_to_amount_divisor(&self) -> Self::Num {
516 self.market.usd_to_amount_divisor()
517 }
518
519 fn max_pool_amount(&self, is_long_token: bool) -> crate::Result<Self::Num> {
520 self.market.max_pool_amount(is_long_token)
521 }
522
523 fn pnl_factor_config(&self, kind: PnlFactorKind, is_long: bool) -> crate::Result<Self::Num> {
524 self.market.pnl_factor_config(kind, is_long)
525 }
526
527 fn reserve_factor(&self) -> crate::Result<Self::Num> {
528 self.market.reserve_factor()
529 }
530
531 fn open_interest_reserve_factor(&self) -> crate::Result<Self::Num> {
532 self.market.open_interest_reserve_factor()
533 }
534
535 fn max_open_interest(&self, is_long: bool) -> crate::Result<Self::Num> {
536 self.market.max_open_interest(is_long)
537 }
538
539 fn ignore_open_interest_for_usage_factor(&self) -> crate::Result<bool> {
540 self.market.ignore_open_interest_for_usage_factor()
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use crate::{
547 market::{LiquidityMarketMutExt, SwapMarketMutExt},
548 pool::Balance,
549 price::Prices,
550 test::TestMarket,
551 BaseMarket, LiquidityMarket, MarketAction,
552 };
553
554 #[test]
555 fn basic() -> crate::Result<()> {
556 let mut market = TestMarket::<u64, 9>::default();
557 let mut prices = Prices::new_for_test(120, 120, 1);
558 market.deposit(1_000_000_000, 0, prices)?.execute()?;
559 prices.index_token_price.set_price_for_test(121);
560 prices.long_token_price.set_price_for_test(121);
561 market.deposit(1_000_000_000, 0, prices)?.execute()?;
562 prices.index_token_price.set_price_for_test(122);
563 prices.long_token_price.set_price_for_test(122);
564 market.deposit(0, 1_000_000_000, prices)?.execute()?;
565 println!("{market:#?}");
566
567 let prices = Prices::new_for_test(123, 123, 1);
568
569 let before_market = market.clone();
571 let token_in_amount = 100_000_000;
572 let report = market.swap(false, token_in_amount, prices)?.execute()?;
573 println!("{report:#?}");
574 println!("{market:#?}");
575
576 assert_eq!(before_market.total_supply(), market.total_supply());
577
578 assert_eq!(
579 before_market.liquidity_pool()?.long_amount()?,
580 market.liquidity_pool()?.long_amount()? + report.token_out_amount()
581 - report.price_impact_amount(),
582 );
583 assert_eq!(
584 before_market.liquidity_pool()?.short_amount()? + token_in_amount
585 - report.token_in_fees().fee_amount_for_receiver(),
586 market.liquidity_pool()?.short_amount()?,
587 );
588
589 assert_eq!(
590 before_market.swap_impact_pool()?.long_amount()?,
591 market.swap_impact_pool()?.long_amount()? + report.price_impact_amount(),
592 );
593 assert_eq!(
594 before_market.swap_impact_pool()?.short_amount()?,
595 market.swap_impact_pool()?.short_amount()?
596 );
597
598 assert_eq!(
599 before_market.claimable_fee_pool()?.long_amount()?,
600 market.claimable_fee_pool()?.long_amount()?,
601 );
602 assert_eq!(
603 before_market.claimable_fee_pool()?.short_amount()?
604 + report.token_in_fees().fee_amount_for_receiver(),
605 market.claimable_fee_pool()?.short_amount()?,
606 );
607
608 let before_market = market.clone();
610 let token_in_amount = 100_000;
611
612 let prices = Prices::new_for_test(119, 119, 1);
613
614 let report = market.swap(true, token_in_amount, prices)?.execute()?;
615 println!("{report:#?}");
616 println!("{market:#?}");
617
618 assert_eq!(before_market.total_supply(), market.total_supply());
619
620 assert_eq!(
621 before_market.liquidity_pool()?.long_amount()? + token_in_amount
622 - report.price_impact_amount()
623 - report.token_in_fees().fee_amount_for_receiver(),
624 market.liquidity_pool()?.long_amount()?,
625 );
626 assert_eq!(
627 before_market.liquidity_pool()?.short_amount()? - report.token_out_amount(),
628 market.liquidity_pool()?.short_amount()?,
629 );
630
631 assert_eq!(
632 before_market.swap_impact_pool()?.long_amount()? + report.price_impact_amount(),
633 market.swap_impact_pool()?.long_amount()?,
634 );
635 assert_eq!(
636 before_market.swap_impact_pool()?.short_amount()?,
637 market.swap_impact_pool()?.short_amount()?
638 );
639
640 assert_eq!(
641 before_market.claimable_fee_pool()?.long_amount()?
642 + report.token_in_fees().fee_amount_for_receiver(),
643 market.claimable_fee_pool()?.long_amount()?,
644 );
645 assert_eq!(
646 before_market.claimable_fee_pool()?.short_amount()?,
647 market.claimable_fee_pool()?.short_amount()?,
648 );
649 Ok(())
650 }
651
652 #[test]
654 fn zero_amount_swap() -> crate::Result<()> {
655 let mut market = TestMarket::<u64, 9>::default();
656 let prices = Prices::new_for_test(120, 120, 1);
657 market.deposit(1_000_000_000, 0, prices)?.execute()?;
658 market.deposit(0, 1_000_000_000, prices)?.execute()?;
659 println!("{market:#?}");
660
661 let result = market.swap(true, 0, prices);
662 assert!(result.is_err());
663 println!("{market:#?}");
664
665 Ok(())
666 }
667
668 #[test]
670 fn over_amount_swap() -> crate::Result<()> {
671 let mut market = TestMarket::<u64, 9>::default();
672 let prices = Prices::new_for_test(120, 120, 1);
673 market.deposit(1_000_000_000, 0, prices)?.execute()?;
674 market.deposit(0, 1_000_000_000, prices)?.execute()?;
675 println!("{market:#?}");
676
677 let result = market.swap(true, 2_000_000_000, prices)?.execute();
678 assert!(result.is_err());
679 println!("{market:#?}");
680
681 let token_in_amount =
683 market.liquidity_pool()?.long_amount()? * prices.long_token_price.mid();
684 let report = market.swap(false, token_in_amount, prices)?.execute()?;
685 println!("{report:#?}");
686 println!("{market:#?}");
687
688 Ok(())
689 }
690
691 #[test]
693 fn small_amount_swap() -> crate::Result<()> {
694 let mut market = TestMarket::<u64, 9>::default();
695 let prices = Prices::new_for_test(120, 120, 1);
696 market.deposit(1_000_000_000, 0, prices)?.execute()?;
697 println!("{market:#?}");
698
699 let small_amount = 1;
700
701 let report = market.swap(false, small_amount, prices)?.execute()?;
702 println!("{report:#?}");
703 println!("{market:#?}");
704 assert!(market.liquidity_pool()?.short_amount()? != 0);
705
706 let report = market
707 .swap(false, prices.long_token_price.mid() * small_amount, prices)?
708 .execute()?;
709 println!("{report:#?}");
710 println!("{market:#?}");
711
712 let report = market.swap(false, 200, prices)?.execute()?;
714 println!("{report:#?}");
715 println!("{market:#?}");
716
717 Ok(())
718 }
719}