gmsol_model/action/
deposit.rs

1use num_traits::{CheckedAdd, CheckedMul, CheckedSub, Signed, Zero};
2
3use crate::{
4    market::{
5        BaseMarket, BaseMarketExt, BaseMarketMutExt, LiquidityMarketExt, LiquidityMarketMut,
6        SwapMarketMutExt,
7    },
8    num::{MulDiv, Unsigned, UnsignedAbs},
9    params::Fees,
10    price::{Price, Prices},
11    utils, BalanceExt, PnlFactorKind, PoolExt,
12};
13
14use super::MarketAction;
15
16/// A deposit.
17#[must_use = "actions do nothing unless you `execute` them"]
18pub struct Deposit<M: BaseMarket<DECIMALS>, const DECIMALS: u8> {
19    market: M,
20    params: DepositParams<M::Num>,
21}
22
23/// Deposit params.
24#[derive(Debug, Clone, Copy)]
25#[cfg_attr(
26    feature = "anchor-lang",
27    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
28)]
29pub struct DepositParams<T> {
30    long_token_amount: T,
31    short_token_amount: T,
32    prices: Prices<T>,
33}
34
35#[cfg(feature = "gmsol-utils")]
36impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for DepositParams<T> {
37    const INIT_SPACE: usize = 2 * T::INIT_SPACE + Prices::<T>::INIT_SPACE;
38}
39
40impl<T> DepositParams<T> {
41    /// Get long token amount.
42    pub fn long_token_amount(&self) -> &T {
43        &self.long_token_amount
44    }
45
46    /// Get short token amount.
47    pub fn short_token_amount(&self) -> &T {
48        &self.short_token_amount
49    }
50
51    /// Get long token price.
52    pub fn long_token_price(&self) -> &Price<T> {
53        &self.prices.long_token_price
54    }
55
56    /// Get short token price.
57    pub fn short_token_price(&self) -> &Price<T> {
58        &self.prices.short_token_price
59    }
60
61    fn reassign_values(&self, is_long_token: bool) -> ReassignedValues<T>
62    where
63        T: Clone,
64    {
65        if is_long_token {
66            ReassignedValues {
67                amount: self.long_token_amount.clone(),
68                price: self.long_token_price(),
69                opposite_price: self.short_token_price(),
70            }
71        } else {
72            ReassignedValues {
73                amount: self.short_token_amount.clone(),
74                price: self.short_token_price(),
75                opposite_price: self.long_token_price(),
76            }
77        }
78    }
79}
80
81/// Report of the execution of deposit.
82#[derive(Debug, Clone, Copy)]
83#[cfg_attr(
84    feature = "anchor-lang",
85    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
86)]
87pub struct DepositReport<Unsigned, Signed> {
88    params: DepositParams<Unsigned>,
89    minted: Unsigned,
90    price_impact: Signed,
91    fees: [Fees<Unsigned>; 2],
92}
93
94#[cfg(feature = "gmsol-utils")]
95impl<Unsigned, Signed> gmsol_utils::InitSpace for DepositReport<Unsigned, Signed>
96where
97    Unsigned: gmsol_utils::InitSpace,
98    Signed: gmsol_utils::InitSpace,
99{
100    const INIT_SPACE: usize = DepositParams::<Unsigned>::INIT_SPACE
101        + Unsigned::INIT_SPACE
102        + Signed::INIT_SPACE
103        + 2 * Fees::<Unsigned>::INIT_SPACE;
104}
105
106impl<T> DepositReport<T, T::Signed>
107where
108    T: Unsigned,
109{
110    fn new(
111        params: DepositParams<T>,
112        price_impact: T::Signed,
113        minted: T,
114        fees: [Fees<T>; 2],
115    ) -> Self {
116        Self {
117            params,
118            minted,
119            price_impact,
120            fees,
121        }
122    }
123
124    /// Get minted.
125    pub fn minted(&self) -> &T {
126        &self.minted
127    }
128
129    /// Get price impact.
130    pub fn price_impact(&self) -> &T::Signed {
131        &self.price_impact
132    }
133
134    /// Get the deposit params.
135    pub fn params(&self) -> &DepositParams<T> {
136        &self.params
137    }
138
139    /// Get long token fees.
140    pub fn long_token_fees(&self) -> &Fees<T> {
141        &self.fees[0]
142    }
143
144    /// Get short token fees.
145    pub fn short_token_fees(&self) -> &Fees<T> {
146        &self.fees[1]
147    }
148}
149
150impl<const DECIMALS: u8, M: LiquidityMarketMut<DECIMALS>> Deposit<M, DECIMALS> {
151    /// Create a new deposit to the given market.
152    pub fn try_new(
153        market: M,
154        long_token_amount: M::Num,
155        short_token_amount: M::Num,
156        prices: Prices<M::Num>,
157    ) -> Result<Self, crate::Error> {
158        if long_token_amount.is_zero() && short_token_amount.is_zero() {
159            return Err(crate::Error::EmptyDeposit);
160        }
161        Ok(Self {
162            market,
163            params: DepositParams {
164                long_token_amount,
165                short_token_amount,
166                prices,
167            },
168        })
169    }
170
171    /// Get the price impact USD value.
172    fn price_impact(&self) -> crate::Result<(M::Signed, M::Num, M::Num)> {
173        let delta = self.market.liquidity_pool()?.pool_delta_with_amounts(
174            &self
175                .params
176                .long_token_amount
177                .clone()
178                .try_into()
179                .map_err(|_| crate::Error::Convert)?,
180            &self
181                .params
182                .short_token_amount
183                .clone()
184                .try_into()
185                .map_err(|_| crate::Error::Convert)?,
186            &self.params.long_token_price().mid(),
187            &self.params.short_token_price().mid(),
188        )?;
189        let price_impact = delta.price_impact(&self.market.swap_impact_params()?)?;
190        let delta = delta.delta();
191        debug_assert!(!delta.long_value().is_negative(), "must be non-negative");
192        debug_assert!(!delta.short_value().is_negative(), "must be non-negative");
193        Ok((
194            price_impact,
195            delta.long_value().unsigned_abs(),
196            delta.short_value().unsigned_abs(),
197        ))
198    }
199
200    /// Charge swap fees.
201    ///
202    /// The `amount` will become the amount after fees.
203    fn charge_fees(
204        &self,
205        is_positive_impact: bool,
206        amount: &mut M::Num,
207    ) -> crate::Result<Fees<M::Num>> {
208        let (amount_after_fees, fees) = self
209            .market
210            .swap_fee_params()?
211            .apply_fees(is_positive_impact, amount)
212            .ok_or(crate::Error::Computation("apply fees"))?;
213        *amount = amount_after_fees;
214        Ok(fees)
215    }
216
217    fn execute_deposit(
218        &mut self,
219        is_long_token: bool,
220        pool_value: M::Num,
221        mut price_impact: M::Signed,
222    ) -> Result<(M::Num, Fees<M::Num>), crate::Error> {
223        let mut mint_amount: M::Num = Zero::zero();
224        let supply = self.market.total_supply();
225
226        if pool_value.is_zero() && !supply.is_zero() {
227            return Err(crate::Error::InvalidPoolValue("deposit"));
228        }
229
230        let ReassignedValues {
231            mut amount,
232            price,
233            opposite_price,
234        } = self.params.reassign_values(is_long_token);
235
236        let fees = self.charge_fees(price_impact.is_positive(), &mut amount)?;
237        self.market.claimable_fee_pool_mut()?.apply_delta_amount(
238            is_long_token,
239            &fees
240                .fee_amount_for_receiver()
241                .clone()
242                .try_into()
243                .map_err(|_| crate::Error::Convert)?,
244        )?;
245
246        if price_impact.is_positive() && supply.is_zero() {
247            price_impact = Zero::zero();
248        }
249        if price_impact.is_positive() {
250            let positive_impact_amount = self.market.apply_swap_impact_value_with_cap(
251                !is_long_token,
252                opposite_price,
253                &price_impact,
254            )?;
255            mint_amount = mint_amount
256                .checked_add(
257                    &utils::usd_to_market_token_amount(
258                        positive_impact_amount
259                            .checked_mul(opposite_price.pick_price(true))
260                            .ok_or(crate::Error::Overflow)?,
261                        pool_value.clone(),
262                        supply.clone(),
263                        self.market.usd_to_amount_divisor(),
264                    )
265                    .ok_or(crate::Error::Computation("convert positive usd to amount"))?,
266                )
267                .ok_or(crate::Error::Overflow)?;
268            self.market.apply_delta(
269                !is_long_token,
270                &positive_impact_amount
271                    .try_into()
272                    .map_err(|_| crate::Error::Convert)?,
273            )?;
274            self.market.validate_pool_amount(!is_long_token)?;
275        } else if price_impact.is_negative() {
276            let negative_impact_amount = self.market.apply_swap_impact_value_with_cap(
277                is_long_token,
278                price,
279                &price_impact,
280            )?;
281            amount =
282                amount
283                    .checked_sub(&negative_impact_amount)
284                    .ok_or(crate::Error::Computation(
285                        "deposit: not enough fund to pay negative impact amount",
286                    ))?;
287        }
288        mint_amount = mint_amount
289            .checked_add(
290                &utils::usd_to_market_token_amount(
291                    amount
292                        .checked_mul(price.pick_price(false))
293                        .ok_or(crate::Error::Overflow)?,
294                    pool_value,
295                    supply.clone(),
296                    self.market.usd_to_amount_divisor(),
297                )
298                .ok_or(crate::Error::Computation("convert negative usd to amount"))?,
299            )
300            .ok_or(crate::Error::Overflow)?;
301        self.market.apply_delta(
302            is_long_token,
303            &(amount
304                .checked_add(fees.fee_amount_for_pool())
305                .ok_or(crate::Error::Overflow)?)
306            .clone()
307            .try_into()
308            .map_err(|_| crate::Error::Convert)?,
309        )?;
310        self.market.validate_pool_amount(is_long_token)?;
311        self.market
312            .validate_pool_value_for_deposit(&self.params.prices, is_long_token)?;
313        Ok((mint_amount, fees))
314    }
315}
316
317impl<const DECIMALS: u8, M> MarketAction for Deposit<M, DECIMALS>
318where
319    M: LiquidityMarketMut<DECIMALS>,
320{
321    type Report = DepositReport<M::Num, <M::Num as Unsigned>::Signed>;
322
323    fn execute(mut self) -> crate::Result<Self::Report> {
324        debug_assert!(
325            !self.params.long_token_amount.is_zero() || !self.params.short_token_amount.is_zero(),
326            "shouldn't be empty deposit"
327        );
328
329        // Validate max pnl first.
330        // Deposits should improve the pool state but it should be checked if
331        // the max pnl factor for deposits is exceeded as this would lead to the
332        // price of the market token decreasing below a target minimum percentage
333        // due to pnl.
334        // Note that this is just a validation for deposits, there is no actual
335        // minimum price for a market token
336        self.market.validate_max_pnl(
337            &self.params.prices,
338            PnlFactorKind::MaxAfterDeposit,
339            PnlFactorKind::MaxAfterDeposit,
340        )?;
341
342        let report = {
343            let (price_impact, long_token_usd_value, short_token_usd_value) =
344                self.price_impact()?;
345            let mut market_token_to_mint: M::Num = Zero::zero();
346            let pool_value = self.market.pool_value(
347                &self.params.prices,
348                PnlFactorKind::MaxAfterDeposit,
349                true,
350            )?;
351            if pool_value.is_negative() {
352                return Err(crate::Error::InvalidPoolValue(
353                    "deposit: current pool value is negative",
354                ));
355            }
356            let mut all_fees = [Default::default(), Default::default()];
357            if !self.params.long_token_amount.is_zero() {
358                let price_impact = long_token_usd_value
359                    .clone()
360                    .checked_mul_div_with_signed_numerator(
361                        &price_impact,
362                        &long_token_usd_value
363                            .checked_add(&short_token_usd_value)
364                            .ok_or(crate::Error::Overflow)?,
365                    )
366                    .ok_or(crate::Error::Computation("price impact for long"))?;
367                let (mint_amount, fees) =
368                    self.execute_deposit(true, pool_value.unsigned_abs(), price_impact)?;
369                market_token_to_mint = market_token_to_mint
370                    .checked_add(&mint_amount)
371                    .ok_or(crate::Error::Overflow)?;
372                all_fees[0] = fees;
373            }
374            if !self.params.short_token_amount.is_zero() {
375                let price_impact = short_token_usd_value
376                    .clone()
377                    .checked_mul_div_with_signed_numerator(
378                        &price_impact,
379                        &long_token_usd_value
380                            .checked_add(&short_token_usd_value)
381                            .ok_or(crate::Error::Overflow)?,
382                    )
383                    .ok_or(crate::Error::Computation("price impact for short"))?;
384                let (mint_amount, fees) =
385                    self.execute_deposit(false, pool_value.unsigned_abs(), price_impact)?;
386                market_token_to_mint = market_token_to_mint
387                    .checked_add(&mint_amount)
388                    .ok_or(crate::Error::Overflow)?;
389                all_fees[1] = fees;
390            }
391            DepositReport::new(self.params, price_impact, market_token_to_mint, all_fees)
392        };
393        self.market.mint(&report.minted)?;
394        Ok(report)
395    }
396}
397
398struct ReassignedValues<'a, T> {
399    amount: T,
400    price: &'a Price<T>,
401    opposite_price: &'a Price<T>,
402}
403
404#[cfg(test)]
405mod tests {
406    use crate::{
407        market::LiquidityMarketMutExt,
408        price::Prices,
409        test::{TestMarket, TestMarketConfig},
410        MarketAction,
411    };
412
413    #[test]
414    fn basic() -> Result<(), crate::Error> {
415        let mut market = TestMarket::<u64, 9>::with_config(TestMarketConfig {
416            reserve_factor: 1_050_000_000,
417            ..Default::default()
418        });
419        let prices = Prices::new_for_test(120, 120, 1);
420        println!(
421            "{:#?}",
422            market.deposit(1_000_000_000, 0, prices)?.execute()?
423        );
424        println!("{market:#?}");
425        println!(
426            "{:#?}",
427            market.deposit(1_000_000_000, 0, prices)?.execute()?
428        );
429        println!("{market:#?}");
430        println!(
431            "{:#?}",
432            market.deposit(0, 1_000_000_000, prices)?.execute()?
433        );
434        println!("{market:#?}");
435        Ok(())
436    }
437
438    #[test]
439    fn sequence() -> crate::Result<()> {
440        let mut market_1 = TestMarket::<u64, 9>::default();
441        let prices = Prices::new_for_test(120, 120, 1);
442        println!(
443            "{:#?}",
444            market_1.deposit(1_000_000_000, 0, prices)?.execute()?
445        );
446        println!(
447            "{:#?}",
448            market_1.deposit(1_000_000_000, 0, prices)?.execute()?
449        );
450        println!("{market_1:#?}");
451        let mut market_2 = TestMarket::<u64, 9>::default();
452        println!(
453            "{:#?}",
454            market_2.deposit(2_000_000_000, 0, prices)?.execute()?
455        );
456        println!("{market_1:#?}");
457        println!("{market_2:#?}");
458        Ok(())
459    }
460
461    #[cfg(feature = "u128")]
462    #[test]
463    fn basic_u128() -> Result<(), crate::Error> {
464        let mut market = TestMarket::<u128, 20>::default();
465        let prices = Prices::new_for_test(12_000_000_000_000, 12_000_000_000_000, 100_000_000_000);
466        println!(
467            "{:#?}",
468            market.deposit(1_000_000_000, 0, prices)?.execute()?
469        );
470        println!(
471            "{:#?}",
472            market.deposit(1_000_000_000, 0, prices)?.execute()?
473        );
474        println!(
475            "{:#?}",
476            market.deposit(0, 1_000_000_000, prices)?.execute()?
477        );
478        println!("{market:#?}");
479        Ok(())
480    }
481
482    /// A test for zero amount deposit.
483    #[test]
484    fn zero_amount_deposit() -> Result<(), crate::Error> {
485        let mut market = TestMarket::<u64, 9>::default();
486        let prices = Prices::new_for_test(120, 120, 1);
487        let result = market.deposit(0, 0, prices);
488        assert!(result.is_err());
489
490        Ok(())
491    }
492
493    /// A test for large and small deposit.
494    #[test]
495    fn extreme_amount_deposit() -> Result<(), crate::Error> {
496        let mut market = TestMarket::<u64, 9>::default();
497        let prices = Prices::new_for_test(120, 120, 1);
498        let small_amount = 1;
499        let large_amount = u64::MAX;
500        let max_pool_amount = 1000000000000000000;
501        println!("{:#?}", market.deposit(small_amount, 0, prices)?.execute()?);
502        println!("{market:#?}");
503
504        let result = market.deposit(large_amount, 0, prices)?.execute();
505        assert!(result.is_err());
506        println!("{market:#?}");
507
508        let result = market.deposit(max_pool_amount, 0, prices)?.execute();
509        assert!(result.is_err());
510        println!("{market:#?}");
511
512        Ok(())
513    }
514
515    /// A test for round attack.
516    #[test]
517    fn round_attack_deposit() -> Result<(), crate::Error> {
518        let mut market = TestMarket::<u64, 9>::default();
519        let prices = Prices::new_for_test(120, 120, 1);
520        let mut i = 1;
521        while i < 10000000 {
522            i += 1;
523            market.deposit(1, 0, prices)?.execute()?;
524        }
525        println!("{market:#?}");
526
527        let mut market_compare = TestMarket::<u64, 9>::default();
528        market_compare.deposit(10000000 - 1, 0, prices)?.execute()?;
529        println!("{market_compare:#?}");
530        Ok(())
531    }
532
533    #[test]
534    fn concurrent_deposits() -> Result<(), crate::Error> {
535        use std::sync::{Arc, Mutex};
536        use std::thread;
537
538        let market = Arc::new(Mutex::new(TestMarket::<u64, 9>::default()));
539        let prices = Prices::new_for_test(120, 120, 1);
540
541        let handles: Vec<_> = (0..10)
542            .map(|_| {
543                let market = Arc::clone(&market);
544                thread::spawn(move || {
545                    let mut market = market.lock().unwrap();
546                    market
547                        .deposit(1_000_000_000, 0, prices)
548                        .unwrap()
549                        .execute()
550                        .unwrap();
551                })
552            })
553            .collect();
554
555        for handle in handles {
556            handle.join().unwrap();
557        }
558
559        let market = market.lock().unwrap();
560        println!("{:#?}", *market);
561        Ok(())
562    }
563
564    #[test]
565    fn deposit_with_price_fluctuations() -> Result<(), crate::Error> {
566        let mut market = TestMarket::<u64, 9>::default();
567        let initial_prices = Prices::new_for_test(120, 120, 1);
568        let fluctuated_prices = Prices::new_for_test(240, 240, 1);
569        println!(
570            "{:#?}",
571            market
572                .deposit(1_000_000_000, 0, initial_prices)?
573                .execute()?
574        );
575        println!("{market:#?}");
576
577        println!(
578            "{:#?}",
579            market
580                .deposit(1_000_000_000, 0, fluctuated_prices)?
581                .execute()?
582        );
583        println!("{market:#?}");
584        Ok(())
585    }
586}