gmsol_store/states/market/
utils.rs

1use anchor_lang::prelude::*;
2
3use gmsol_model::{BaseMarketExt, ClockKind, PnlFactorKind};
4
5use crate::{constants, states::Oracle, CoreError, CoreResult, ModelError};
6
7use super::{HasMarketMeta, Market};
8
9/// Extension trait for validating market balances.
10pub trait ValidateMarketBalances:
11    gmsol_model::BaseMarket<{ constants::MARKET_DECIMALS }, Num = u128>
12    + gmsol_model::Bank<Pubkey, Num = u64>
13    + HasMarketMeta
14{
15    /// Validate market balance for the given token.
16    ///
17    /// # Notes
18    /// Unlike the Solidity version, this function does not actually verify
19    /// the token balance but only checks the balance recorded in the market state.
20    /// This is because we use a shared vaults design, making it unlikely to sum all
21    /// markets in a single instruction and then verify the vault's balance.
22    ///
23    /// The reason we adopt a shared vaults design is to avoid performing a large number
24    /// of CPIs when executing swaps along the swap path, which could easily lead to heap
25    /// memory overflows if we used Solana's default heap allocator.
26    fn validate_market_balance_for_the_given_token(
27        &self,
28        token: &Pubkey,
29        excluded: u64,
30    ) -> gmsol_model::Result<()> {
31        let is_long_token = self.market_meta().to_token_side(token)?;
32        let is_pure = self.is_pure();
33
34        let balance = u128::from(self.balance_excluding(token, &excluded)?);
35
36        let mut min_token_balance = self
37            .expected_min_token_balance_excluding_collateral_amount_for_one_token_side(
38                is_long_token,
39            )?;
40
41        // Since the `min_token_balance` only accounts for one side, we need to include the other side
42        // for a pure market.
43        if is_pure {
44            min_token_balance = min_token_balance
45                    .checked_add(
46                        self.expected_min_token_balance_excluding_collateral_amount_for_one_token_side(
47                            !is_long_token,
48                        )?,
49                    )
50                    .ok_or(gmsol_model::Error::Computation(
51                        "validate balance: overflow while adding the min token balance for the other side",
52                    ))?;
53        }
54
55        crate::debug_msg!(
56            "[Validation] validating min token balance: {} >= {}",
57            balance,
58            min_token_balance
59        );
60        if balance < min_token_balance {
61            return Err(gmsol_model::Error::InvalidTokenBalance(
62                "Less than expected min token balance excluding collateral amount",
63                min_token_balance.to_string(),
64                balance.to_string(),
65            ));
66        }
67
68        let mut collateral_amount =
69            self.total_collateral_amount_for_one_token_side(is_long_token)?;
70
71        // Since the `collateral_amount` only accounts for one side, we need to include the other side
72        // for a pure market.
73        if is_pure {
74            collateral_amount = collateral_amount
75                    .checked_add(self.total_collateral_amount_for_one_token_side(!is_long_token)?)
76                    .ok_or(gmsol_model::Error::Computation(
77                        "validate balance: overflow while adding the collateral amount for the other side",
78                    ))?;
79        }
80
81        crate::debug_msg!(
82            "[Validation] validating collateral amount: {} >= {}",
83            balance,
84            collateral_amount
85        );
86        if balance < collateral_amount {
87            return Err(gmsol_model::Error::InvalidTokenBalance(
88                "Less than total collateral amount",
89                collateral_amount.to_string(),
90                balance.to_string(),
91            ));
92        }
93
94        // We don't have to validate the claimable funding amount since they are claimed immediately.
95
96        Ok(())
97    }
98
99    /// Validate market balances.
100    fn validate_market_balances(
101        &self,
102        mut long_excluding_amount: u64,
103        mut short_excluding_amount: u64,
104    ) -> Result<()> {
105        if self.is_pure() {
106            let total = long_excluding_amount
107                .checked_add(short_excluding_amount)
108                .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
109            long_excluding_amount = total;
110            short_excluding_amount = 0;
111        }
112        let meta = self.market_meta();
113        self.validate_market_balance_for_the_given_token(
114            &meta.long_token_mint,
115            long_excluding_amount,
116        )
117        .map_err(ModelError::from)?;
118        // Skip the validation for short token if this is a pure market,
119        // where the long token and short token are the same.
120        if !self.is_pure() {
121            self.validate_market_balance_for_the_given_token(
122                &meta.short_token_mint,
123                short_excluding_amount,
124            )
125            .map_err(ModelError::from)?;
126        }
127        Ok(())
128    }
129
130    /// Validate market balances excluding the given token amounts.
131    fn validate_market_balances_excluding_the_given_token_amounts(
132        &self,
133        first_token: &Pubkey,
134        second_token: &Pubkey,
135        first_excluding_amount: u64,
136        second_excluding_amount: u64,
137    ) -> Result<()> {
138        let mut long_excluding_amount = 0u64;
139        let mut short_excluding_amount = 0u64;
140
141        for (token, amount) in [
142            (first_token, first_excluding_amount),
143            (second_token, second_excluding_amount),
144        ] {
145            if amount == 0 {
146                continue;
147            }
148            let is_long = self
149                .market_meta()
150                .to_token_side(token)
151                .map_err(CoreError::from)?;
152            if is_long {
153                long_excluding_amount = long_excluding_amount
154                    .checked_add(amount)
155                    .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
156            } else {
157                short_excluding_amount = short_excluding_amount
158                    .checked_add(amount)
159                    .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
160            }
161        }
162
163        self.validate_market_balances(long_excluding_amount, short_excluding_amount)
164    }
165}
166
167impl<
168        M: gmsol_model::BaseMarket<{ constants::MARKET_DECIMALS }, Num = u128>
169            + gmsol_model::Bank<Pubkey, Num = u64>
170            + HasMarketMeta,
171    > ValidateMarketBalances for M
172{
173}
174
175/// Trait for defining operations related to auto-deleveraging.
176pub trait Adl {
177    /// Validate if the ADL can be executed.
178    fn validate_adl(&self, oracle: &Oracle, is_long: bool, max_staleness: u64) -> CoreResult<()>;
179
180    /// Latest ADL time.
181    fn latest_adl_time(&self, is_long: bool) -> CoreResult<i64>;
182
183    fn update_adl_state(&mut self, oracle: &Oracle, is_long: bool) -> Result<()>;
184}
185
186impl Adl for Market {
187    fn latest_adl_time(&self, is_long: bool) -> CoreResult<i64> {
188        let clock = if is_long {
189            ClockKind::AdlForLong
190        } else {
191            ClockKind::AdlForShort
192        };
193        self.clock(clock).ok_or(CoreError::NotFound)
194    }
195
196    fn validate_adl(&self, oracle: &Oracle, is_long: bool, max_staleness: u64) -> CoreResult<()> {
197        if !self.is_adl_enabled(is_long) {
198            return Err(CoreError::AdlNotEnabled);
199        }
200        if oracle
201            .max_oracle_ts()
202            .saturating_add_unsigned(max_staleness)
203            < self.latest_adl_time(is_long)?
204        {
205            return Err(CoreError::OracleTimestampsAreSmallerThanRequired);
206        }
207        Ok(())
208    }
209
210    fn update_adl_state(&mut self, oracle: &Oracle, is_long: bool) -> Result<()> {
211        if oracle.max_oracle_ts() < self.latest_adl_time(is_long)? {
212            return err!(CoreError::OracleTimestampsAreSmallerThanRequired);
213        }
214        require!(self.is_enabled(), CoreError::DisabledMarket);
215        let prices = self.prices(oracle)?;
216        let is_exceeded = self
217            .pnl_factor_exceeded(&prices, PnlFactorKind::ForAdl, is_long)
218            .map_err(ModelError::from)?
219            .is_some();
220        self.set_adl_enabled(is_long, is_exceeded);
221        let kind = if is_long {
222            ClockKind::AdlForLong
223        } else {
224            ClockKind::AdlForShort
225        };
226        let clock = self
227            .state
228            .clocks
229            .get_mut(kind)
230            .ok_or_else(|| error!(CoreError::NotFound))?;
231        *clock = Clock::get()?.unix_timestamp;
232        Ok(())
233    }
234}