Skip to main content

sss_token/instructions/
mint.rs

1use anchor_lang::prelude::*;
2use anchor_spl::{
3    token_2022::{self, MintTo, Token2022},
4    token_interface::TokenAccount,
5};
6
7use crate::errors::SssError;
8use crate::events::TokensMinted;
9use crate::state::*;
10use crate::utils::{require_blacklist_enabled, require_not_paused};
11
12#[derive(Accounts)]
13pub struct MintTokens<'info> {
14    #[account(mut)]
15    pub minter_authority: Signer<'info>,
16
17    #[account(
18        mut,
19        seeds = [StablecoinConfig::SEED_PREFIX, config.mint.as_ref()],
20        bump = config.bump,
21    )]
22    pub config: Account<'info, StablecoinConfig>,
23
24    #[account(
25        mut,
26        seeds = [MinterInfo::SEED_PREFIX, config.key().as_ref(), minter_authority.key().as_ref()],
27        bump = minter_info.bump,
28        constraint = minter_info.config == config.key(),
29    )]
30    pub minter_info: Account<'info, MinterInfo>,
31
32    /// CHECK: The Token-2022 mint account. Address validated against config, owner against Token-2022.
33    #[account(
34        mut,
35        address = config.mint,
36        constraint = mint.owner == &token_program.key() @ SssError::InvalidAuthority,
37    )]
38    pub mint: UncheckedAccount<'info>,
39
40    #[account(
41        mut,
42        token::mint = config.mint,
43        token::token_program = token_program,
44    )]
45    pub recipient_token_account: InterfaceAccount<'info, TokenAccount>,
46
47    /// CHECK: Blacklist PDA for the recipient token account owner.
48    /// Mandatory account — callers cannot omit it, preventing blacklist bypass.
49    /// The PDA seeds are derived from the on-chain token account owner (not caller input)
50    /// so callers cannot spoof a clean wallet address.
51    /// When permanent_delegate is enabled and this PDA has data, the mint is rejected
52    /// because a BlacklistEntry exists for the recipient.
53    /// When permanent_delegate is disabled, the account is still required but ignored.
54    #[account(
55        seeds = [
56            BlacklistEntry::SEED_PREFIX,
57            config.key().as_ref(),
58            recipient_token_account.owner.as_ref(),
59        ],
60        bump,
61    )]
62    pub recipient_blacklist: UncheckedAccount<'info>,
63
64    pub token_program: Program<'info, Token2022>,
65}
66
67pub fn handler(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
68    require!(amount > 0, SssError::MintAmountZero);
69
70    let config = &ctx.accounts.config;
71    require_not_paused(config)?;
72
73    // SSS-2: Mandatory blacklist check — recipient_blacklist is always required (not Optional),
74    // so callers cannot skip the check by omitting the account.
75    // The PDA seeds constraint above validates derivation from the actual token account owner.
76    if require_blacklist_enabled(config).is_ok() {
77        // If the blacklist PDA has data, a BlacklistEntry exists for this recipient — reject.
78        if !ctx.accounts.recipient_blacklist.data_is_empty() {
79            return Err(SssError::RecipientBlacklisted.into());
80        }
81    }
82
83    let minter_info = &ctx.accounts.minter_info;
84    require!(minter_info.is_active, SssError::MinterNotActive);
85    require!(minter_info.can_mint(amount), SssError::MintQuotaExceeded);
86
87    if config.supply_cap > 0 {
88        let new_supply = config
89            .current_supply()
90            .checked_add(amount)
91            .ok_or(SssError::Overflow)?;
92        require!(new_supply <= config.supply_cap, SssError::SupplyCapExceeded);
93    }
94
95    let clock = Clock::get()?;
96
97    // Mint tokens via CPI (config PDA is mint authority)
98    let mint_key = config.mint;
99    let signer_seeds: &[&[&[u8]]] = &[&[
100        StablecoinConfig::SEED_PREFIX,
101        mint_key.as_ref(),
102        &[config.bump],
103    ]];
104
105    token_2022::mint_to(
106        CpiContext::new_with_signer(
107            ctx.accounts.token_program.to_account_info(),
108            MintTo {
109                mint: ctx.accounts.mint.to_account_info(),
110                to: ctx.accounts.recipient_token_account.to_account_info(),
111                authority: ctx.accounts.config.to_account_info(),
112            },
113            signer_seeds,
114        ),
115        amount,
116    )?;
117
118    // Update minter stats
119    let minter_info = &mut ctx.accounts.minter_info;
120    minter_info.total_minted = minter_info
121        .total_minted
122        .checked_add(amount)
123        .ok_or(SssError::Overflow)?;
124    minter_info.last_mint_at = clock.unix_timestamp;
125
126    // Update config stats
127    let config = &mut ctx.accounts.config;
128    config.total_minted = config
129        .total_minted
130        .checked_add(amount)
131        .ok_or(SssError::Overflow)?;
132    config.updated_at = clock.unix_timestamp;
133
134    emit!(TokensMinted {
135        config: config.key(),
136        minter: ctx.accounts.minter_authority.key(),
137        recipient: ctx.accounts.recipient_token_account.key(),
138        amount,
139        total_minted: config.total_minted,
140        timestamp: clock.unix_timestamp,
141    });
142
143    Ok(())
144}