gmsol_model/action/
withdraw.rs

1use crate::{
2    market::{BaseMarket, BaseMarketExt, LiquidityMarketExt, LiquidityMarketMut},
3    num::{MulDiv, Unsigned, UnsignedAbs},
4    params::Fees,
5    price::{Price, Prices},
6    utils, BalanceExt, PnlFactorKind, PoolExt,
7};
8use num_traits::{CheckedAdd, CheckedDiv, Signed, Zero};
9
10use super::MarketAction;
11
12/// A withdrawal.
13#[must_use = "actions do nothing unless you `execute` them"]
14pub struct Withdrawal<M: BaseMarket<DECIMALS>, const DECIMALS: u8> {
15    market: M,
16    params: WithdrawParams<M::Num>,
17}
18
19/// Withdraw params.
20#[derive(Debug, Clone, Copy)]
21#[cfg_attr(
22    feature = "anchor-lang",
23    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
24)]
25pub struct WithdrawParams<T> {
26    market_token_amount: T,
27    prices: Prices<T>,
28}
29
30#[cfg(feature = "gmsol-utils")]
31impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for WithdrawParams<T> {
32    const INIT_SPACE: usize = T::INIT_SPACE + Prices::<T>::INIT_SPACE;
33}
34
35impl<T> WithdrawParams<T> {
36    /// Get market token amount to burn.
37    pub fn market_token_amount(&self) -> &T {
38        &self.market_token_amount
39    }
40
41    /// Get long token price.
42    pub fn long_token_price(&self) -> &Price<T> {
43        &self.prices.long_token_price
44    }
45
46    /// Get short token price.
47    pub fn short_token_price(&self) -> &Price<T> {
48        &self.prices.short_token_price
49    }
50}
51
52/// Report of the execution of withdrawal.
53#[must_use = "`long_token_output` and `short_token_output` must be used"]
54#[derive(Debug, Clone, Copy)]
55#[cfg_attr(
56    feature = "anchor-lang",
57    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
58)]
59pub struct WithdrawReport<T> {
60    params: WithdrawParams<T>,
61    long_token_fees: Fees<T>,
62    short_token_fees: Fees<T>,
63    long_token_output: T,
64    short_token_output: T,
65}
66
67#[cfg(feature = "gmsol-utils")]
68impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for WithdrawReport<T> {
69    const INIT_SPACE: usize =
70        WithdrawParams::<T>::INIT_SPACE + 2 * Fees::<T>::INIT_SPACE + 2 * T::INIT_SPACE;
71}
72
73impl<T> WithdrawReport<T> {
74    /// Get withdraw params.
75    pub fn params(&self) -> &WithdrawParams<T> {
76        &self.params
77    }
78
79    /// Get long token fees.
80    pub fn long_token_fees(&self) -> &Fees<T> {
81        &self.long_token_fees
82    }
83
84    /// Get short token fees.
85    pub fn short_token_fees(&self) -> &Fees<T> {
86        &self.short_token_fees
87    }
88
89    /// Get the output amount of long tokens.
90    #[must_use = "the returned amount of long tokens should be transferred out from the market vault"]
91    pub fn long_token_output(&self) -> &T {
92        &self.long_token_output
93    }
94
95    /// Get the output amount of short tokens.
96    #[must_use = "the returned amount of short tokens should be transferred out from the market vault"]
97    pub fn short_token_output(&self) -> &T {
98        &self.short_token_output
99    }
100}
101
102impl<const DECIMALS: u8, M: LiquidityMarketMut<DECIMALS>> Withdrawal<M, DECIMALS> {
103    /// Create a new withdrawal from the given market.
104    pub fn try_new(
105        market: M,
106        market_token_amount: M::Num,
107        prices: Prices<M::Num>,
108    ) -> crate::Result<Self> {
109        if market_token_amount.is_zero() {
110            return Err(crate::Error::EmptyWithdrawal);
111        }
112        prices.validate()?;
113        Ok(Self {
114            market,
115            params: WithdrawParams {
116                market_token_amount,
117                prices,
118            },
119        })
120    }
121
122    fn output_amounts(&self) -> crate::Result<(M::Num, M::Num)> {
123        let pool_value = self.market.pool_value(
124            &self.params.prices,
125            PnlFactorKind::MaxAfterWithdrawal,
126            false,
127        )?;
128        if pool_value.is_negative() {
129            return Err(crate::Error::InvalidPoolValue(
130                "withdrawal: current pool value is negative",
131            ));
132        }
133        if pool_value.is_zero() {
134            return Err(crate::Error::InvalidPoolValue(
135                "withdrawal: current pool value is zero",
136            ));
137        }
138        let total_supply = self.market.total_supply();
139
140        // We use the liquidity pool value instead of the pool value with pending values to calculate the fraction of
141        // long token and short token.
142        let pool = self.market.liquidity_pool()?;
143        let long_token_value =
144            pool.long_usd_value(self.params.long_token_price().pick_price(true))?;
145        let short_token_value =
146            pool.short_usd_value(self.params.short_token_price().pick_price(true))?;
147        let total_pool_token_value =
148            long_token_value
149                .checked_add(&short_token_value)
150                .ok_or(crate::Error::Computation(
151                    "calculating total liquidity pool value",
152                ))?;
153
154        let market_token_value = utils::market_token_amount_to_usd(
155            &self.params.market_token_amount,
156            &pool_value.unsigned_abs(),
157            &total_supply,
158        )
159        .ok_or(crate::Error::Computation("amount to usd"))?;
160
161        debug_assert!(!self.params.long_token_price().has_zero());
162        debug_assert!(!self.params.short_token_price().has_zero());
163        let long_token_amount = market_token_value
164            .checked_mul_div(&long_token_value, &total_pool_token_value)
165            .and_then(|a| a.checked_div(self.params.long_token_price().pick_price(true)))
166            .ok_or(crate::Error::Computation("long token amount"))?;
167        let short_token_amount = market_token_value
168            .checked_mul_div(&short_token_value, &total_pool_token_value)
169            .and_then(|a| a.checked_div(self.params.short_token_price().pick_price(true)))
170            .ok_or(crate::Error::Computation("short token amount"))?;
171        Ok((long_token_amount, short_token_amount))
172    }
173
174    fn charge_fees(&self, amount: &mut M::Num) -> crate::Result<Fees<M::Num>> {
175        let (amount_after_fees, fees) = self
176            .market
177            .swap_fee_params()?
178            .apply_fees(false, amount)
179            .ok_or(crate::Error::Computation("apply fees"))?;
180        *amount = amount_after_fees;
181        Ok(fees)
182    }
183}
184
185impl<const DECIMALS: u8, M: LiquidityMarketMut<DECIMALS>> MarketAction for Withdrawal<M, DECIMALS> {
186    type Report = WithdrawReport<M::Num>;
187
188    fn execute(mut self) -> crate::Result<Self::Report> {
189        let (mut long_token_amount, mut short_token_amount) = self.output_amounts()?;
190        let long_token_fees = self.charge_fees(&mut long_token_amount)?;
191        let short_token_fees = self.charge_fees(&mut short_token_amount)?;
192        // Apply claimable fees delta.
193        let pool = self.market.claimable_fee_pool_mut()?;
194        pool.apply_delta_amount(
195            true,
196            &long_token_fees
197                .fee_amount_for_receiver()
198                .clone()
199                .try_into()
200                .map_err(|_| crate::Error::Convert)?,
201        )?;
202        pool.apply_delta_amount(
203            false,
204            &short_token_fees
205                .fee_amount_for_receiver()
206                .clone()
207                .try_into()
208                .map_err(|_| crate::Error::Convert)?,
209        )?;
210        // Apply pool delta.
211        // The delta must be the amount leaves the pool: -(amount_after_fees + fee_receiver_amount)
212        let pool = self.market.liquidity_pool_mut()?;
213
214        let delta = long_token_fees
215            .fee_amount_for_receiver()
216            .checked_add(&long_token_amount)
217            .ok_or(crate::Error::Overflow)?
218            .to_opposite_signed()?;
219        pool.apply_delta_amount(true, &delta)?;
220
221        let delta = short_token_fees
222            .fee_amount_for_receiver()
223            .checked_add(&short_token_amount)
224            .ok_or(crate::Error::Overflow)?
225            .to_opposite_signed()?;
226        pool.apply_delta_amount(false, &delta)?;
227
228        self.market.validate_reserve(&self.params.prices, true)?;
229        self.market.validate_reserve(&self.params.prices, false)?;
230        self.market.validate_max_pnl(
231            &self.params.prices,
232            PnlFactorKind::MaxAfterWithdrawal,
233            PnlFactorKind::MaxAfterWithdrawal,
234        )?;
235
236        self.market.burn(&self.params.market_token_amount)?;
237
238        Ok(WithdrawReport {
239            params: self.params,
240            long_token_fees,
241            short_token_fees,
242            long_token_output: long_token_amount,
243            short_token_output: short_token_amount,
244        })
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use crate::{
251        market::LiquidityMarketMutExt, pool::Balance, price::Prices, test::TestMarket, BaseMarket,
252        LiquidityMarket, MarketAction,
253    };
254
255    #[test]
256    fn basic() -> crate::Result<()> {
257        let mut market = TestMarket::<u64, 9>::default();
258        let prices = Prices::new_for_test(120, 120, 1);
259        market.deposit(1_000_000_000, 0, prices)?.execute()?;
260        market.deposit(1_000_000_000, 0, prices)?.execute()?;
261        market.deposit(0, 1_000_000_000, prices)?.execute()?;
262        println!("{market:#?}");
263        let before_supply = market.total_supply();
264        let before_long_amount = market.liquidity_pool()?.long_amount()?;
265        let before_short_amount = market.liquidity_pool()?.short_amount()?;
266        let prices = Prices::new_for_test(120, 120, 1);
267        let report = market.withdraw(1_000_000_000, prices)?.execute()?;
268        println!("{report:#?}");
269        println!("{market:#?}");
270        assert_eq!(
271            market.total_supply() + report.params.market_token_amount,
272            before_supply
273        );
274        assert_eq!(
275            market.liquidity_pool()?.long_amount()?
276                + report.long_token_fees.fee_amount_for_receiver()
277                + report.long_token_output,
278            before_long_amount
279        );
280        assert_eq!(
281            market.liquidity_pool()?.short_amount()?
282                + report.short_token_fees.fee_amount_for_receiver()
283                + report.short_token_output,
284            before_short_amount
285        );
286        Ok(())
287    }
288
289    /// A test for zero amount withdrawal.
290    #[test]
291    fn zero_amount_withdrawal() -> crate::Result<()> {
292        let mut market = TestMarket::<u64, 9>::default();
293        let prices = Prices::new_for_test(120, 120, 1);
294        market.deposit(1_000_000_000, 0, prices)?.execute()?;
295        market.deposit(0, 1_000_000_000, prices)?.execute()?;
296        let result = market.withdraw(0, prices);
297        assert!(result.is_err());
298        Ok(())
299    }
300
301    /// A test for over amount withdrawal.
302    #[test]
303    fn over_amount_withdrawal() -> crate::Result<()> {
304        let mut market = TestMarket::<u64, 9>::default();
305        let prices = Prices::new_for_test(120, 120, 1);
306        market.deposit(1_000_000, 0, prices)?.execute()?;
307        market.deposit(0, 1_000_000, prices)?.execute()?;
308        println!("{market:#?}");
309
310        let result = market.withdraw(1_000_000_000, prices)?.execute();
311        assert!(result.is_err());
312        println!("{market:#?}");
313        Ok(())
314    }
315
316    /// A test for small amount withdrawal.
317    #[test]
318    fn small_amount_withdrawal() -> crate::Result<()> {
319        let mut market = TestMarket::<u64, 9>::default();
320        let prices = Prices::new_for_test(120, 120, 1);
321        market.deposit(1_000_000_000, 0, prices)?.execute()?;
322        market.deposit(1_000_000_000, 0, prices)?.execute()?;
323        market.deposit(0, 1_000_000_000, prices)?.execute()?;
324        println!("{market:#?}");
325        let before_supply = market.total_supply();
326        let before_long_amount = market.liquidity_pool()?.long_amount()?;
327        let before_short_amount = market.liquidity_pool()?.short_amount()?;
328        let prices = Prices::new_for_test(120, 120, 1);
329
330        let small_amount = 1;
331        let report = market.withdraw(small_amount, prices)?.execute()?;
332        println!("{report:#?}");
333        println!("{market:#?}");
334        assert_eq!(
335            market.total_supply() + report.params.market_token_amount,
336            before_supply
337        );
338        assert_eq!(
339            market.liquidity_pool()?.long_amount()?
340                + report.long_token_fees.fee_amount_for_receiver()
341                + report.long_token_output,
342            before_long_amount
343        );
344        assert_eq!(
345            market.liquidity_pool()?.short_amount()?
346                + report.short_token_fees.fee_amount_for_receiver()
347                + report.short_token_output,
348            before_short_amount
349        );
350
351        Ok(())
352    }
353}