gmsol_model/action/
withdraw.rs

1use crate::{
2    market::{BaseMarket, BaseMarketExt, BaseMarketMutExt, LiquidityMarketExt, LiquidityMarketMut},
3    num::{MulDiv, Unsigned, UnsignedAbs},
4    params::Fees,
5    pool::delta::BalanceChange,
6    price::{Price, Prices},
7    utils, BalanceExt, PnlFactorKind, PoolExt,
8};
9use num_traits::{CheckedAdd, CheckedDiv, Signed, Zero};
10
11use super::MarketAction;
12
13/// A withdrawal.
14#[must_use = "actions do nothing unless you `execute` them"]
15pub struct Withdrawal<M: BaseMarket<DECIMALS>, const DECIMALS: u8> {
16    market: M,
17    params: WithdrawParams<M::Num>,
18}
19
20/// Withdraw params.
21#[derive(Debug, Clone, Copy)]
22#[cfg_attr(
23    feature = "anchor-lang",
24    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
25)]
26pub struct WithdrawParams<T> {
27    market_token_amount: T,
28    prices: Prices<T>,
29}
30
31#[cfg(feature = "gmsol-utils")]
32impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for WithdrawParams<T> {
33    const INIT_SPACE: usize = T::INIT_SPACE + Prices::<T>::INIT_SPACE;
34}
35
36impl<T> WithdrawParams<T> {
37    /// Get market token amount to burn.
38    pub fn market_token_amount(&self) -> &T {
39        &self.market_token_amount
40    }
41
42    /// Get long token price.
43    pub fn long_token_price(&self) -> &Price<T> {
44        &self.prices.long_token_price
45    }
46
47    /// Get short token price.
48    pub fn short_token_price(&self) -> &Price<T> {
49        &self.prices.short_token_price
50    }
51}
52
53/// Report of the execution of withdrawal.
54#[must_use = "`long_token_output` and `short_token_output` must be used"]
55#[derive(Debug, Clone, Copy)]
56#[cfg_attr(
57    feature = "anchor-lang",
58    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
59)]
60pub struct WithdrawReport<T> {
61    params: WithdrawParams<T>,
62    long_token_fees: Fees<T>,
63    short_token_fees: Fees<T>,
64    long_token_output: T,
65    short_token_output: T,
66}
67
68#[cfg(feature = "gmsol-utils")]
69impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for WithdrawReport<T> {
70    const INIT_SPACE: usize =
71        WithdrawParams::<T>::INIT_SPACE + 2 * Fees::<T>::INIT_SPACE + 2 * T::INIT_SPACE;
72}
73
74impl<T> WithdrawReport<T> {
75    /// Get withdraw params.
76    pub fn params(&self) -> &WithdrawParams<T> {
77        &self.params
78    }
79
80    /// Get long token fees.
81    pub fn long_token_fees(&self) -> &Fees<T> {
82        &self.long_token_fees
83    }
84
85    /// Get short token fees.
86    pub fn short_token_fees(&self) -> &Fees<T> {
87        &self.short_token_fees
88    }
89
90    /// Get the output amount of long tokens.
91    #[must_use = "the returned amount of long tokens should be transferred out from the market vault"]
92    pub fn long_token_output(&self) -> &T {
93        &self.long_token_output
94    }
95
96    /// Get the output amount of short tokens.
97    #[must_use = "the returned amount of short tokens should be transferred out from the market vault"]
98    pub fn short_token_output(&self) -> &T {
99        &self.short_token_output
100    }
101}
102
103impl<const DECIMALS: u8, M: LiquidityMarketMut<DECIMALS>> Withdrawal<M, DECIMALS> {
104    /// Create a new withdrawal from the given market.
105    pub fn try_new(
106        market: M,
107        market_token_amount: M::Num,
108        prices: Prices<M::Num>,
109    ) -> crate::Result<Self> {
110        if market_token_amount.is_zero() {
111            return Err(crate::Error::EmptyWithdrawal);
112        }
113        prices.validate()?;
114        Ok(Self {
115            market,
116            params: WithdrawParams {
117                market_token_amount,
118                prices,
119            },
120        })
121    }
122
123    fn output_amounts(&self) -> crate::Result<(M::Num, M::Num)> {
124        let pool_value = self.market.pool_value(
125            &self.params.prices,
126            PnlFactorKind::MaxAfterWithdrawal,
127            false,
128        )?;
129        if pool_value.is_negative() {
130            return Err(crate::Error::InvalidPoolValue(
131                "withdrawal: current pool value is negative",
132            ));
133        }
134        if pool_value.is_zero() {
135            return Err(crate::Error::InvalidPoolValue(
136                "withdrawal: current pool value is zero",
137            ));
138        }
139        let total_supply = self.market.total_supply();
140
141        // We use the liquidity pool value instead of the pool value with pending values to calculate the fraction of
142        // long token and short token.
143        let pool = self.market.liquidity_pool()?;
144        let long_token_value =
145            pool.long_usd_value(self.params.long_token_price().pick_price(true))?;
146        let short_token_value =
147            pool.short_usd_value(self.params.short_token_price().pick_price(true))?;
148        let total_pool_token_value =
149            long_token_value
150                .checked_add(&short_token_value)
151                .ok_or(crate::Error::Computation(
152                    "calculating total liquidity pool value",
153                ))?;
154
155        let market_token_value = utils::market_token_amount_to_usd(
156            &self.params.market_token_amount,
157            &pool_value.unsigned_abs(),
158            &total_supply,
159        )
160        .ok_or(crate::Error::Computation("amount to usd"))?;
161
162        debug_assert!(!self.params.long_token_price().has_zero());
163        debug_assert!(!self.params.short_token_price().has_zero());
164        let long_token_amount = market_token_value
165            .checked_mul_div(&long_token_value, &total_pool_token_value)
166            .and_then(|a| a.checked_div(self.params.long_token_price().pick_price(true)))
167            .ok_or(crate::Error::Computation("long token amount"))?;
168        let short_token_amount = market_token_value
169            .checked_mul_div(&short_token_value, &total_pool_token_value)
170            .and_then(|a| a.checked_div(self.params.short_token_price().pick_price(true)))
171            .ok_or(crate::Error::Computation("short token amount"))?;
172        Ok((long_token_amount, short_token_amount))
173    }
174
175    fn charge_fees(&self, amount: &mut M::Num) -> crate::Result<Fees<M::Num>> {
176        let (amount_after_fees, fees) = self
177            .market
178            .swap_fee_params()?
179            .apply_fees(BalanceChange::Worsened, amount)
180            .ok_or(crate::Error::Computation("apply fees"))?;
181        *amount = amount_after_fees;
182        Ok(fees)
183    }
184}
185
186impl<const DECIMALS: u8, M: LiquidityMarketMut<DECIMALS>> MarketAction for Withdrawal<M, DECIMALS> {
187    type Report = WithdrawReport<M::Num>;
188
189    fn execute(mut self) -> crate::Result<Self::Report> {
190        let (mut long_token_amount, mut short_token_amount) = self.output_amounts()?;
191        let long_token_fees = self.charge_fees(&mut long_token_amount)?;
192        let short_token_fees = self.charge_fees(&mut short_token_amount)?;
193        // Apply claimable fees delta.
194        let pool = self.market.claimable_fee_pool_mut()?;
195        pool.apply_delta_amount(
196            true,
197            &long_token_fees
198                .fee_amount_for_receiver()
199                .clone()
200                .try_into()
201                .map_err(|_| crate::Error::Convert)?,
202        )?;
203        pool.apply_delta_amount(
204            false,
205            &short_token_fees
206                .fee_amount_for_receiver()
207                .clone()
208                .try_into()
209                .map_err(|_| crate::Error::Convert)?,
210        )?;
211        // Apply pool delta.
212        // The delta must be the amount leaves the pool: -(amount_after_fees + fee_receiver_amount)
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        self.market.apply_delta(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        self.market.apply_delta(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}