gmsol_store/states/
gt.rs

1//! # GT
2//!
3//! GT is designed to boost trading activity on GMX-Solana by offering a unique incentive mechanism
4//! that provides traders with additional rewards to maximize rebates and minimize trading costs. GT
5//! is a highly customizable token obtainable through trading on GMX-Solana. The initial mint cost
6//! of GT is $0.05, equivalent to a trading volume of $100 with a 5 bps fee. As the total supply
7//! grows, GT's mint cost will increase exponentially. Each cycle, set at 100,000 GT, raises the
8//! mint cost by 1%.
9//!
10//! #### Treasury and Buyback
11//!
12//! GT is non-transferable, and its sale can only occur through the daily Treasury buyback. 60% of
13//! the fees will go to the Treasury. Each day, the Treasury will allocate USDC for buybacks based
14//! on the min {deposited GT * mint cost, 2% of the Treasury, 60% of the Treasury's daily revenue}.
15//! The actual buyback price will be determined by dividing the total allocated USDC by the total
16//! amount of GT deposited by users. After the buyback is complete, users can claim the
17//! corresponding USDC.
18//!
19//! #### VIP Levels (User Ranks)
20//!
21//! VIP levels are assigned based on users' GT holdings. The more GT a user holds, the higher their
22//! VIP level, which grants greater order fee discounts.
23//!
24//! #### Referral Program
25//!
26//! The referral program offers referees an extra 10% order fee discount. The final order fee discount
27//! will be calculated as: order fee discount = 1 - (1 - order fee vip discount) * (1 - order fee
28//! referred discount).
29
30use anchor_lang::prelude::*;
31
32use crate::{constants, CoreError};
33
34use super::{user::UserHeader, Seed};
35
36pub use gmsol_utils::gt::get_time_window_index;
37
38const MAX_RANK: usize = 15;
39const MAX_FLAGS: usize = 8;
40
41#[zero_copy]
42#[cfg_attr(feature = "debug", derive(derive_more::Debug))]
43pub struct GtState {
44    decimals: u8,
45    #[cfg_attr(feature = "debug", debug(skip))]
46    padding_0: [u8; 7],
47    /* States */
48    pub(crate) last_minted_at: i64,
49    total_minted: u64,
50    /// Grow step amount. It must be immutable.
51    grow_step_amount: u64,
52    grow_steps: u64,
53    /// Supply of buybackable GT.
54    supply: u64,
55    #[cfg_attr(feature = "debug", debug(skip))]
56    padding_1: [u8; 8],
57    /// Vault for non-buybackable GT.
58    gt_vault: u64,
59    #[cfg_attr(feature = "debug", debug(skip))]
60    padding_2: [u8; 16],
61    /* Configs */
62    minting_cost_grow_factor: u128,
63    minting_cost: u128,
64    #[cfg_attr(feature = "debug", debug(skip))]
65    padding_3: [u8; 32],
66    exchange_time_window: u32,
67    #[cfg_attr(feature = "debug", debug(skip))]
68    padding_4: [u8; 12],
69    max_rank: u64,
70    ranks: [u64; MAX_RANK],
71    order_fee_discount_factors: [u128; MAX_RANK + 1],
72    referral_reward_factors: [u128; MAX_RANK + 1],
73    #[cfg_attr(feature = "debug", debug(skip))]
74    padding_5: [u8; 32],
75    #[cfg_attr(feature = "debug", debug(skip))]
76    reserved: [u8; 256],
77}
78
79impl GtState {
80    pub(crate) fn init(
81        &mut self,
82        decimals: u8,
83        initial_minting_cost: u128,
84        grow_factor: u128,
85        grow_step: u64,
86        ranks: &[u64],
87    ) -> Result<()> {
88        require!(!self.is_initialized(), CoreError::GTStateHasBeenInitialized);
89        require_eq!(self.last_minted_at, 0, CoreError::GTStateHasBeenInitialized);
90        require_eq!(self.total_minted, 0, CoreError::GTStateHasBeenInitialized);
91        require_eq!(self.supply, 0, CoreError::GTStateHasBeenInitialized);
92        require_eq!(self.gt_vault, 0, CoreError::GTStateHasBeenInitialized);
93
94        require!(grow_step != 0, CoreError::InvalidGTConfig);
95
96        let max_rank = ranks.len().min(MAX_RANK);
97        let ranks = &ranks[0..max_rank];
98
99        // Ranks must be storted.
100        require!(
101            ranks.windows(2).all(|ab| {
102                if let [a, b] = &ab {
103                    a < b
104                } else {
105                    false
106                }
107            }),
108            CoreError::InvalidGTConfig
109        );
110
111        let clock = Clock::get()?;
112
113        self.decimals = decimals;
114        self.last_minted_at = clock.unix_timestamp;
115        self.grow_step_amount = grow_step;
116        self.minting_cost_grow_factor = grow_factor;
117        self.minting_cost = initial_minting_cost;
118
119        let target = &mut self.ranks[0..max_rank];
120        target.copy_from_slice(ranks);
121        self.max_rank = max_rank as u64;
122
123        self.exchange_time_window = constants::DEFAULT_GT_VAULT_TIME_WINDOW;
124
125        Ok(())
126    }
127
128    /// Returns whether the GT state is initialized.
129    pub fn is_initialized(&self) -> bool {
130        self.grow_step_amount != 0
131    }
132
133    pub(crate) fn set_order_fee_discount_factors(&mut self, factors: &[u128]) -> Result<()> {
134        require_eq!(
135            factors.len(),
136            (self.max_rank + 1) as usize,
137            CoreError::InvalidArgument
138        );
139
140        require!(
141            factors
142                .iter()
143                .all(|factor| *factor <= constants::MARKET_USD_UNIT),
144            CoreError::InvalidArgument
145        );
146
147        let target = &mut self.order_fee_discount_factors[0..factors.len()];
148        target.copy_from_slice(factors);
149
150        Ok(())
151    }
152
153    pub(crate) fn set_referral_reward_factors(&mut self, factors: &[u128]) -> Result<()> {
154        require_eq!(
155            factors.len(),
156            (self.max_rank + 1) as usize,
157            CoreError::InvalidArgument
158        );
159
160        // Factors must be storted.
161        require!(
162            factors.windows(2).all(|ab| {
163                if let [a, b] = &ab {
164                    a <= b
165                } else {
166                    false
167                }
168            }),
169            CoreError::InvalidArgument
170        );
171
172        let target = &mut self.referral_reward_factors[0..factors.len()];
173        target.copy_from_slice(factors);
174
175        Ok(())
176    }
177
178    pub(crate) fn order_fee_discount_factor(&self, rank: u8) -> Result<u128> {
179        require_gte!(self.max_rank, rank as u64, CoreError::InvalidArgument);
180        Ok(self.order_fee_discount_factors[rank as usize])
181    }
182
183    pub(crate) fn referral_reward_factor(&self, rank: u8) -> Result<u128> {
184        require_gte!(self.max_rank, rank as u64, CoreError::InvalidArgument);
185        Ok(self.referral_reward_factors[rank as usize])
186    }
187
188    /// Get time window for GT exchange.
189    pub fn exchange_time_window(&self) -> u32 {
190        self.exchange_time_window
191    }
192
193    /// Get GT decimals.
194    pub fn decimals(&self) -> u8 {
195        self.decimals
196    }
197
198    /// Get minting cost.
199    pub fn minting_cost(&self) -> u128 {
200        self.minting_cost
201    }
202
203    /// Get total minted.
204    pub fn total_minted(&self) -> u64 {
205        self.total_minted
206    }
207
208    /// Get grow steps.
209    pub fn grow_steps(&self) -> u64 {
210        self.grow_steps
211    }
212
213    /// Get GT supply.
214    pub fn supply(&self) -> u64 {
215        self.supply
216    }
217
218    /// Get GT vault.
219    pub fn gt_vault(&self) -> u64 {
220        self.gt_vault
221    }
222
223    /// Set exchange time window.
224    pub fn set_exchange_time_window(&mut self, window: u32) -> Result<()> {
225        require_neq!(window, 0, CoreError::InvalidArgument);
226        self.exchange_time_window = window;
227        Ok(())
228    }
229
230    fn next_minting_cost(&self, next_minted: u64) -> Result<Option<(u64, u128)>> {
231        use gmsol_model::utils::apply_factor;
232
233        require!(self.grow_step_amount != 0, CoreError::InvalidGTConfig);
234        let new_steps = next_minted / self.grow_step_amount;
235
236        if new_steps != self.grow_steps {
237            let mut minting_cost = self.minting_cost;
238            for _ in self.grow_steps..new_steps {
239                minting_cost = apply_factor::<_, { constants::MARKET_DECIMALS }>(
240                    &minting_cost,
241                    &self.minting_cost_grow_factor,
242                )
243                .ok_or_else(|| error!(CoreError::Internal))?;
244            }
245            Ok(Some((new_steps, minting_cost)))
246        } else {
247            Ok(None)
248        }
249    }
250
251    /// CHECK: the user must be owned by this store.
252    fn unchecked_update_rank(&self, user: &mut UserHeader) {
253        debug_assert!(self.ranks().len() < u8::MAX as usize);
254        let rank = match self.ranks().binary_search(&user.gt.amount) {
255            Ok(rank) => rank + 1,
256            Err(rank) => rank,
257        };
258
259        let rank = rank as u8;
260        if user.gt.rank != rank {
261            user.gt.rank = rank;
262            msg!("[GT] user rank updated, new rank = {}", rank);
263        }
264    }
265
266    #[inline(never)]
267    pub(crate) fn mint_to(&mut self, user: &mut UserHeader, amount: u64) -> Result<()> {
268        if amount != 0 {
269            let clock = Clock::get()?;
270
271            // Calculate global GT state updates.
272            let next_gt_total_minted = self
273                .total_minted
274                .checked_add(amount)
275                .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
276            let next_minting_cost = self.next_minting_cost(next_gt_total_minted)?;
277
278            // Calculate user GT state updates.
279            let next_user_total_minted = user
280                .gt
281                .total_minted
282                .checked_add(amount)
283                .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
284            let next_amount = user
285                .gt
286                .amount
287                .checked_add(amount)
288                .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
289            let next_supply = self
290                .supply
291                .checked_add(amount)
292                .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
293
294            /* The following steps should be infallible. */
295
296            if let Some((new_steps, new_minting_cost)) = next_minting_cost {
297                self.minting_cost = new_minting_cost;
298                self.grow_steps = new_steps;
299            }
300            self.total_minted = next_gt_total_minted;
301            self.last_minted_at = clock.unix_timestamp;
302
303            user.gt.total_minted = next_user_total_minted;
304            user.gt.amount = next_amount;
305            user.gt.last_minted_at = self.last_minted_at;
306            self.supply = next_supply;
307
308            self.unchecked_update_rank(user);
309        }
310        Ok(())
311    }
312
313    /// Burn GT from the given `user`.
314    ///
315    /// # CHECK
316    /// - The `user` must be owned by this store.
317    ///
318    /// # Errors
319    /// - `user` must have enough amount of GT.
320    pub(crate) fn unchecked_burn_from(&mut self, user: &mut UserHeader, amount: u64) -> Result<()> {
321        if amount != 0 {
322            require_gte!(user.gt.amount, amount, CoreError::NotEnoughTokenAmount);
323            let next_amount = user
324                .gt
325                .amount
326                .checked_sub(amount)
327                .ok_or_else(|| error!(CoreError::Internal))?;
328
329            let next_supply = self
330                .supply
331                .checked_sub(amount)
332                .ok_or_else(|| error!(CoreError::Internal))?;
333
334            /* The following steps should be infallible. */
335
336            user.gt.amount = next_amount;
337            self.supply = next_supply;
338
339            self.unchecked_update_rank(user);
340        }
341        Ok(())
342    }
343
344    #[inline(never)]
345    pub(crate) fn get_mint_amount(&self, size_in_value: u128) -> Result<(u64, u128, u128)> {
346        let minting_cost = self.minting_cost;
347
348        require!(minting_cost != 0, CoreError::InvalidGTConfig);
349
350        let remainder = size_in_value % minting_cost;
351        let minted = (size_in_value / minting_cost)
352            .try_into()
353            .map_err(|_| error!(CoreError::TokenAmountOverflow))?;
354
355        let minted_value = size_in_value - remainder;
356
357        Ok((minted, minted_value, minting_cost))
358    }
359
360    pub(crate) fn ranks(&self) -> &[u64] {
361        &self.ranks[0..(self.max_rank as usize)]
362    }
363
364    /// Request an exchange.
365    ///
366    /// # CHECK
367    /// - `user`, `vault` and `exchange` must owned by this store.
368    ///
369    /// # Errors
370    /// - `user`, `vault` and `exchange` must have been initialized.
371    /// - `vault` must be depositable.
372    /// - `user` must have enough amount of GT.
373    ///
374    /// # Notes
375    /// - This is not an atomic operation.
376    pub(crate) fn unchecked_request_exchange(
377        &mut self,
378        user: &mut UserHeader,
379        vault: &mut GtExchangeVault,
380        exchange: &mut GtExchange,
381        amount: u64,
382    ) -> Result<()> {
383        require!(user.is_initialized(), CoreError::InvalidArgument);
384        require!(vault.is_initialized(), CoreError::InvalidArgument);
385        require!(exchange.is_initialized(), CoreError::InvalidArgument);
386
387        self.unchecked_burn_from(user, amount)?;
388
389        vault.add(amount)?;
390        exchange.add(amount)?;
391
392        Ok(())
393    }
394
395    /// Confirm the exchange vault.
396    ///
397    /// # CHECK
398    /// - `vault` must be owned by this store.
399    ///
400    /// # Errors
401    /// - `vault` must have been initialized.
402    /// - `vault` must be confirmable.
403    pub(crate) fn unchecked_confirm_exchange_vault(
404        &mut self,
405        vault: &mut GtExchangeVault,
406    ) -> Result<()> {
407        require!(vault.is_initialized(), CoreError::InvalidArgument);
408
409        let amount = vault.confirm()?;
410
411        self.process_gt_vault(amount)?;
412
413        Ok(())
414    }
415
416    fn process_gt_vault(&mut self, amount: u64) -> Result<()> {
417        if amount != 0 {
418            let amount_for_vault = amount;
419
420            let next_gt_vault = self
421                .gt_vault
422                .checked_add(amount_for_vault)
423                .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
424
425            self.gt_vault = next_gt_vault;
426        }
427        Ok(())
428    }
429}
430
431/// GT Exchange Vault Flags.
432#[repr(u8)]
433#[non_exhaustive]
434#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive)]
435pub enum GtExchangeVaultFlag {
436    /// Initialized.
437    Intiailized,
438    /// Confirmed.
439    Comfirmed,
440    // CHECK: should have no more than `MAX_FLAGS` of flags.
441}
442
443gmsol_utils::flags!(GtExchangeVaultFlag, MAX_FLAGS, u8);
444
445/// GT Exchange Vault.
446#[account(zero_copy)]
447#[cfg_attr(feature = "debug", derive(derive_more::Debug))]
448#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
449pub struct GtExchangeVault {
450    /// Bump seed.
451    pub bump: u8,
452    flags: GtExchangeVaultFlagContainer,
453    padding: [u8; 6],
454    ts: i64,
455    time_window: i64,
456    amount: u64,
457    /// Store.
458    pub store: Pubkey,
459    #[cfg_attr(feature = "debug", debug(skip))]
460    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
461    reserved: [u8; 64],
462}
463
464impl GtExchangeVault {
465    /// Get amount.
466    pub fn amount(&self) -> u64 {
467        self.amount
468    }
469
470    /// Get whether the vault is initialized.
471    pub fn is_initialized(&self) -> bool {
472        self.flags.get_flag(GtExchangeVaultFlag::Intiailized)
473    }
474
475    /// Get whether the vault is comfirmed.
476    pub fn is_confirmed(&self) -> bool {
477        self.flags.get_flag(GtExchangeVaultFlag::Comfirmed)
478    }
479
480    pub(crate) fn init(&mut self, bump: u8, store: &Pubkey, time_window: u32) -> Result<()> {
481        require!(!self.is_initialized(), CoreError::PreconditionsAreNotMet);
482
483        require!(time_window != 0, CoreError::InvalidArgument);
484
485        let clock = Clock::get()?;
486
487        self.bump = bump;
488        self.ts = clock.unix_timestamp;
489        self.store = *store;
490        self.flags.set_flag(GtExchangeVaultFlag::Intiailized, true);
491        self.time_window = i64::from(time_window);
492
493        Ok(())
494    }
495
496    /// Get current time window index.
497    pub fn time_window_index(&self) -> i64 {
498        get_time_window_index(self.ts, self.time_window)
499    }
500
501    /// Get time window.
502    pub fn time_window(&self) -> i64 {
503        self.time_window
504    }
505
506    /// Get time window as `u32`.
507    pub fn time_window_u32(&self) -> u32 {
508        self.time_window.try_into().expect("invalid vault")
509    }
510
511    /// Validate that this vault is confirmable.
512    pub fn validate_confirmable(&self) -> Result<()> {
513        require!(self.is_initialized(), CoreError::PreconditionsAreNotMet);
514        require!(!self.is_confirmed(), CoreError::PreconditionsAreNotMet);
515
516        let clock = Clock::get()?;
517        let current_index = get_time_window_index(clock.unix_timestamp, self.time_window);
518
519        require_gt!(
520            current_index,
521            self.time_window_index(),
522            CoreError::PreconditionsAreNotMet
523        );
524
525        Ok(())
526    }
527
528    /// Confirm the vault.
529    fn confirm(&mut self) -> Result<u64> {
530        self.validate_confirmable()?;
531        self.flags.set_flag(GtExchangeVaultFlag::Comfirmed, true);
532        Ok(self.amount)
533    }
534
535    /// Validate that this vault is depositable.
536    pub fn validate_depositable(&self) -> Result<()> {
537        require!(!self.is_confirmed(), CoreError::PreconditionsAreNotMet);
538
539        let clock = Clock::get()?;
540        let current_index = get_time_window_index(clock.unix_timestamp, self.time_window);
541        require_eq!(
542            current_index,
543            self.time_window_index(),
544            CoreError::InvalidArgument
545        );
546        Ok(())
547    }
548
549    /// Add GT to this vault.
550    ///
551    /// # Errors
552    /// - This vault must be depositable.
553    /// - Error on amount overflow.
554    fn add(&mut self, amount: u64) -> Result<()> {
555        self.validate_depositable()?;
556
557        self.amount = self
558            .amount
559            .checked_add(amount)
560            .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
561        Ok(())
562    }
563}
564
565impl Seed for GtExchangeVault {
566    const SEED: &'static [u8] = b"gt_exchange_vault";
567}
568
569impl gmsol_utils::InitSpace for GtExchangeVault {
570    const INIT_SPACE: usize = std::mem::size_of::<Self>();
571}
572
573/// GT Exchange Vault Flags.
574#[repr(u8)]
575#[non_exhaustive]
576#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive)]
577pub enum GtExchangeFlag {
578    /// Initialized.
579    Intiailized,
580    // CHECK: should have no more than `MAX_FLAGS` of flags.
581}
582
583gmsol_utils::flags!(GtExchangeFlag, MAX_FLAGS, u8);
584
585/// GT Exchange Account.
586#[account(zero_copy)]
587#[cfg_attr(feature = "debug", derive(derive_more::Debug))]
588#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
589pub struct GtExchange {
590    /// Bump.
591    pub bump: u8,
592    flags: GtExchangeFlagContainer,
593    #[cfg_attr(feature = "debug", debug(skip))]
594    padding: [u8; 6],
595    amount: u64,
596    /// Owner address.
597    pub owner: Pubkey,
598    /// Store address.
599    pub store: Pubkey,
600    /// Vault address.
601    pub vault: Pubkey,
602    #[cfg_attr(feature = "debug", debug(skip))]
603    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
604    reserved: [u8; 64],
605}
606
607impl Default for GtExchange {
608    fn default() -> Self {
609        use bytemuck::Zeroable;
610
611        Self::zeroed()
612    }
613}
614
615impl GtExchange {
616    /// Get whether the vault is initialized.
617    pub fn is_initialized(&self) -> bool {
618        self.flags.get_flag(GtExchangeFlag::Intiailized)
619    }
620
621    pub(crate) fn init(
622        &mut self,
623        bump: u8,
624        owner: &Pubkey,
625        store: &Pubkey,
626        vault: &Pubkey,
627    ) -> Result<()> {
628        require!(!self.is_initialized(), CoreError::PreconditionsAreNotMet);
629
630        self.bump = bump;
631        self.owner = *owner;
632        self.store = *store;
633        self.vault = *vault;
634
635        self.flags.set_flag(GtExchangeFlag::Intiailized, true);
636
637        Ok(())
638    }
639
640    /// Add GT amount.
641    fn add(&mut self, amount: u64) -> Result<()> {
642        self.amount = self
643            .amount
644            .checked_add(amount)
645            .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
646        Ok(())
647    }
648
649    /// Get the owner address.
650    pub fn owner(&self) -> &Pubkey {
651        &self.owner
652    }
653
654    pub(crate) fn store(&self) -> &Pubkey {
655        &self.store
656    }
657
658    /// Get vault.
659    pub fn vault(&self) -> &Pubkey {
660        &self.vault
661    }
662
663    /// Get amount.
664    pub fn amount(&self) -> u64 {
665        self.amount
666    }
667}
668
669impl gmsol_utils::InitSpace for GtExchange {
670    const INIT_SPACE: usize = std::mem::size_of::<Self>();
671}
672
673impl Seed for GtExchange {
674    const SEED: &'static [u8] = b"gt_exchange";
675}