gmsol_treasury/instructions/
gt_bank.rs

1use anchor_lang::prelude::*;
2use anchor_spl::{
3    associated_token::AssociatedToken,
4    token::Token,
5    token_2022::{transfer_checked, Token2022, TransferChecked},
6    token_interface::{Mint, TokenAccount, TokenInterface},
7};
8use gmsol_store::{
9    cpi::{accounts::CloseGtExchange, close_gt_exchange},
10    program::GmsolStore,
11    states::{
12        gt::{GtExchange, GtExchangeVault},
13        Seed, Store,
14    },
15    utils::{token::validate_associated_token_account, CpiAuthentication, WithStore},
16    CoreError,
17};
18use gmsol_utils::InitSpace;
19
20use crate::states::{Config, GtBank, TreasuryVaultConfig};
21
22/// The accounts definition for [`prepare_gt_bank`](crate::gmsol_treasury::prepare_gt_bank).
23#[derive(Accounts)]
24pub struct PrepareGtBank<'info> {
25    /// Authority.
26    #[account(mut)]
27    pub authority: Signer<'info>,
28    /// Store.
29    /// CHECK: check by CPI.
30    pub store: UncheckedAccount<'info>,
31    /// Config.
32    #[account(
33        has_one = store,
34        // Only allow creating GT bank for the authorized treausry.
35        constraint = config.load()?.treasury_vault_config() == Some(&treasury_vault_config.key()) @ CoreError::InvalidArgument,
36    )]
37    pub config: AccountLoader<'info, Config>,
38    /// Treasury Config.
39    #[account(
40        has_one = config,
41    )]
42    pub treasury_vault_config: AccountLoader<'info, TreasuryVaultConfig>,
43    /// GT exchange vault.
44    #[account(
45        has_one = store,
46        constraint = gt_exchange_vault.load()?.is_initialized() @ CoreError::InvalidArgument,
47        constraint = !gt_exchange_vault.load()?.is_confirmed() @ CoreError::InvalidArgument,
48    )]
49    pub gt_exchange_vault: AccountLoader<'info, GtExchangeVault>,
50    /// GT Bank.
51    #[account(
52        init_if_needed,
53        payer = authority,
54        space = 8 + GtBank::INIT_SPACE,
55        seeds = [
56            GtBank::SEED,
57            treasury_vault_config.key().as_ref(),
58            gt_exchange_vault.key().as_ref(),
59        ],
60        bump,
61    )]
62    pub gt_bank: AccountLoader<'info, GtBank>,
63    /// Store program.
64    pub store_program: Program<'info, GmsolStore>,
65    /// The system program.
66    pub system_program: Program<'info, System>,
67}
68
69/// Prepare a GT Bank.
70/// # CHECK
71/// Only [`TREASURY_KEEPER`](crate::roles::TREASURY_KEEPER) can use.
72pub(crate) fn unchecked_prepare_gt_bank(ctx: Context<PrepareGtBank>) -> Result<()> {
73    let bump = ctx.bumps.gt_bank;
74    let treasury_vault_config = ctx.accounts.treasury_vault_config.key();
75    let gt_exchange_vault = ctx.accounts.gt_exchange_vault.key();
76
77    match ctx.accounts.gt_bank.load_init() {
78        Ok(mut gt_bank) => {
79            gt_bank.try_init(bump, treasury_vault_config, gt_exchange_vault)?;
80            drop(gt_bank);
81            ctx.accounts.gt_bank.exit(&crate::ID)?;
82        }
83        Err(Error::AnchorError(err)) => {
84            if err.error_code_number != ErrorCode::AccountDiscriminatorAlreadySet as u32 {
85                return Err(Error::AnchorError(err));
86            }
87        }
88        Err(err) => {
89            return Err(err);
90        }
91    }
92
93    // Validate.
94    {
95        let gt_bank = ctx.accounts.gt_bank.load()?;
96        require_eq!(gt_bank.bump, bump, CoreError::InvalidArgument);
97        require_keys_eq!(
98            gt_bank.treasury_vault_config,
99            treasury_vault_config,
100            CoreError::InvalidArgument
101        );
102        require_keys_eq!(
103            gt_bank.gt_exchange_vault,
104            gt_exchange_vault,
105            CoreError::InvalidArgument
106        );
107        require!(gt_bank.is_initialized(), CoreError::InvalidArgument);
108    }
109
110    Ok(())
111}
112
113impl<'info> WithStore<'info> for PrepareGtBank<'info> {
114    fn store_program(&self) -> AccountInfo<'info> {
115        self.store_program.to_account_info()
116    }
117
118    fn store(&self) -> AccountInfo<'info> {
119        self.store.to_account_info()
120    }
121}
122
123impl<'info> CpiAuthentication<'info> for PrepareGtBank<'info> {
124    fn authority(&self) -> AccountInfo<'info> {
125        self.authority.to_account_info()
126    }
127
128    fn on_error(&self) -> Result<()> {
129        err!(CoreError::PermissionDenied)
130    }
131}
132
133/// The accounts definition for [`sync_gt_bank`](crate::gmsol_treasury::sync_gt_bank).
134#[derive(Accounts)]
135pub struct SyncGtBank<'info> {
136    /// Authority.
137    #[account(mut)]
138    pub authority: Signer<'info>,
139    /// Store.
140    /// CHECK: check by CPI.
141    pub store: UncheckedAccount<'info>,
142    /// Config.
143    #[account(
144        has_one = store,
145        // Only allow depositing into the authorized treausry.
146        constraint = config.load()?.treasury_vault_config() == Some(&treasury_vault_config.key()) @ CoreError::InvalidArgument,
147    )]
148    pub config: AccountLoader<'info, Config>,
149    /// Treasury Vault Config.
150    #[account(
151        has_one = config,
152        // As long as the treasury allows withdrawals of the given token,
153        // deposits from GT Bank remain permitted. Disabling deposits for
154        // that token won't affect GT Bank deposits already made, which
155        // in turn helps the treasury recover those funds.
156        constraint = treasury_vault_config.load()?.is_withdrawal_allowed(&token.key())? @ CoreError::InvalidArgument,
157    )]
158    pub treasury_vault_config: AccountLoader<'info, TreasuryVaultConfig>,
159    /// GT bank.
160    #[account(
161        mut,
162        has_one = treasury_vault_config,
163    )]
164    pub gt_bank: AccountLoader<'info, GtBank>,
165    /// Token.
166    pub token: InterfaceAccount<'info, Mint>,
167    /// Treasury vault.
168    #[account(
169        mut,
170        associated_token::authority = treasury_vault_config,
171        associated_token::mint =  token,
172    )]
173    pub treasury_vault: InterfaceAccount<'info, TokenAccount>,
174    /// GT bank vault.
175    #[account(
176        mut,
177        associated_token::authority = gt_bank,
178        associated_token::mint = token,
179    )]
180    pub gt_bank_vault: InterfaceAccount<'info, TokenAccount>,
181    /// Store program.
182    pub store_program: Program<'info, GmsolStore>,
183    /// The token program.
184    pub token_program: Interface<'info, TokenInterface>,
185    /// Associated token program.
186    pub associated_token_program: Program<'info, AssociatedToken>,
187}
188
189/// Sync the GT bank and deposit the exceeding amount into treasury vault.
190/// # CHECK
191/// Only [`TREASURY_WITHDRAWER`](crate::roles::TREASURY_WITHDRAWER) can use.
192pub(crate) fn unchecked_sync_gt_bank(ctx: Context<SyncGtBank>) -> Result<()> {
193    let delta = {
194        let gt_bank = ctx.accounts.gt_bank.load_mut()?;
195        let token = ctx.accounts.token.key();
196
197        let recorded_balance = gt_bank.get_balance(&token).unwrap_or(0);
198        let balance = ctx.accounts.gt_bank_vault.amount;
199
200        balance
201            .checked_sub(recorded_balance)
202            .ok_or_else(|| error!(CoreError::NotEnoughTokenAmount))?
203    };
204
205    // Check if the balance is already in sync.
206    require_neq!(delta, 0, CoreError::PreconditionsAreNotMet);
207
208    let cpi_ctx = ctx.accounts.transfer_checked_ctx();
209    let signer = ctx.accounts.gt_bank.load()?.signer();
210    transfer_checked(
211        cpi_ctx.with_signer(&[&signer.as_seeds()]),
212        delta,
213        ctx.accounts.token.decimals,
214    )?;
215    msg!(
216        "[Treasury] Synced GT Bank balance, deposit exceeding {} tokens into treasury",
217        delta
218    );
219
220    Ok(())
221}
222
223impl<'info> WithStore<'info> for SyncGtBank<'info> {
224    fn store_program(&self) -> AccountInfo<'info> {
225        self.store_program.to_account_info()
226    }
227
228    fn store(&self) -> AccountInfo<'info> {
229        self.store.to_account_info()
230    }
231}
232
233impl<'info> CpiAuthentication<'info> for SyncGtBank<'info> {
234    fn authority(&self) -> AccountInfo<'info> {
235        self.authority.to_account_info()
236    }
237
238    fn on_error(&self) -> Result<()> {
239        err!(CoreError::PermissionDenied)
240    }
241}
242
243impl<'info> SyncGtBank<'info> {
244    fn transfer_checked_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
245        CpiContext::new(
246            self.token_program.to_account_info(),
247            TransferChecked {
248                from: self.gt_bank_vault.to_account_info(),
249                mint: self.token.to_account_info(),
250                to: self.treasury_vault.to_account_info(),
251                authority: self.gt_bank.to_account_info(),
252            },
253        )
254    }
255}
256
257/// The accounts definition for [`complete_gt_exchange`](crate::gmsol_treasury::complete_gt_exchange).
258///
259/// Remaining accounts expected by this instruction:
260///
261///   - 0..N. `[]` N token mint accounts, where N represents the total number of tokens defined
262///     in the GT bank.
263///   - N..2N. `[mutable]` N GT bank vault accounts.
264///   - 2N..3N. `[mutable]` N token accounts to receive the funds, owned by the `owner`.
265#[derive(Accounts)]
266pub struct CompleteGtExchange<'info> {
267    /// Owner.
268    pub owner: Signer<'info>,
269    /// Store.
270    #[account(constraint = store.load()?.validate_not_restarted().map(|_| true)?)]
271    pub store: AccountLoader<'info, Store>,
272    /// Config.
273    #[account(
274        has_one = store,
275        // Only allow completing GT exchange with the authorized treasury.
276        constraint = config.load()?.treasury_vault_config() == Some(&treasury_vault_config.key()) @ CoreError::InvalidArgument,
277    )]
278    pub config: AccountLoader<'info, Config>,
279    /// Treasury Config.
280    #[account(
281        has_one = config,
282    )]
283    pub treasury_vault_config: AccountLoader<'info, TreasuryVaultConfig>,
284    /// GT exchange vault.
285    /// CHECK: check by CPI.
286    #[account(mut)]
287    pub gt_exchange_vault: UncheckedAccount<'info>,
288    /// GT bank.
289    #[account(
290        mut,
291        has_one = treasury_vault_config,
292        has_one = gt_exchange_vault,
293    )]
294    pub gt_bank: AccountLoader<'info, GtBank>,
295    /// Exchange to complete.
296    /// The ownership should be checked by the CPI.
297    #[account(mut)]
298    pub exchange: AccountLoader<'info, GtExchange>,
299    /// Store program.
300    pub store_program: Program<'info, GmsolStore>,
301    /// The token program.
302    pub token_program: Program<'info, Token>,
303    /// The token-2022 program.
304    pub token_2022_program: Program<'info, Token2022>,
305}
306
307pub(crate) fn complete_gt_exchange<'info>(
308    ctx: Context<'_, '_, 'info, 'info, CompleteGtExchange<'info>>,
309) -> Result<()> {
310    let remaining_accounts = ctx.remaining_accounts;
311    ctx.accounts.execute(remaining_accounts)?;
312    Ok(())
313}
314
315impl<'info> CompleteGtExchange<'info> {
316    fn execute(&self, remaining_accounts: &'info [AccountInfo<'info>]) -> Result<()> {
317        use gmsol_model::num::MulDiv;
318
319        let signer = self.config.load()?.signer();
320
321        let gt_amount = self.exchange.load()?.amount();
322
323        // Close GT exchange first to validate the preconditions.
324        // This should validate that the GT exchange vault has been confirmed.
325        let ctx = self.close_gt_exchange_ctx();
326        close_gt_exchange(ctx.with_signer(&[&signer.as_seeds()]))?;
327
328        if gt_amount == 0 {
329            return Ok(());
330        }
331
332        let len = self.gt_bank.load()?.num_tokens();
333        let gt_bank_tokens = self.gt_bank.load()?.tokens().collect::<Vec<_>>();
334        let total_len = len.checked_mul(3).expect("must not overflow");
335        require_gte!(remaining_accounts.len(), total_len);
336        let tokens = &remaining_accounts[0..len];
337        let vaults = &remaining_accounts[len..(2 * len)];
338        let targets = &remaining_accounts[(2 * len)..total_len];
339
340        // Transfer funds.
341        {
342            let gt_bank_address = self.gt_bank.key();
343            let owner_address = self.owner.key();
344
345            let gt_bank_signer = self.gt_bank.load()?.signer();
346            let total_gt_amount = self.gt_bank.load()?.remaining_confirmed_gt_amount();
347
348            require_gte!(total_gt_amount, gt_amount, CoreError::Internal);
349
350            for (idx, token) in gt_bank_tokens.iter().enumerate() {
351                let balance = self.gt_bank.load()?.get_balance(token).expect("must exist");
352                if balance == 0 {
353                    continue;
354                }
355                let amount = balance
356                    .checked_mul_div(&gt_amount, &total_gt_amount)
357                    .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
358
359                let mint = &tokens[idx];
360                require_keys_eq!(*mint.key, *token, CoreError::InvalidArgument);
361                let token_program = if mint.owner == self.token_program.key {
362                    self.token_program.to_account_info()
363                } else if mint.owner == self.token_2022_program.key {
364                    self.token_2022_program.to_account_info()
365                } else {
366                    return err!(CoreError::InvalidArgument);
367                };
368
369                let vault = &vaults[idx];
370                validate_associated_token_account(
371                    vault,
372                    &gt_bank_address,
373                    token,
374                    &token_program.key(),
375                )?;
376
377                let target = &targets[idx];
378                require_keys_eq!(
379                    anchor_spl::token::accessor::authority(target)?,
380                    owner_address
381                );
382
383                let mint = InterfaceAccount::<Mint>::try_from(mint)?;
384                let decimals = mint.decimals;
385
386                let ctx = CpiContext::new(
387                    token_program,
388                    TransferChecked {
389                        from: vault.to_account_info(),
390                        mint: mint.to_account_info(),
391                        to: target.to_account_info(),
392                        authority: self.gt_bank.to_account_info(),
393                    },
394                );
395
396                transfer_checked(
397                    ctx.with_signer(&[&gt_bank_signer.as_seeds()]),
398                    amount,
399                    decimals,
400                )?;
401
402                self.gt_bank
403                    .load_mut()?
404                    .record_transferred_out(token, amount)?;
405            }
406
407            self.gt_bank.load_mut()?.record_claimed(gt_amount)?;
408        }
409
410        Ok(())
411    }
412
413    fn close_gt_exchange_ctx(&self) -> CpiContext<'_, '_, '_, 'info, CloseGtExchange<'info>> {
414        CpiContext::new(
415            self.store_program.to_account_info(),
416            CloseGtExchange {
417                authority: self.config.to_account_info(),
418                store: self.store.to_account_info(),
419                owner: self.owner.to_account_info(),
420                vault: self.gt_exchange_vault.to_account_info(),
421                exchange: self.exchange.to_account_info(),
422            },
423        )
424    }
425}