Skip to main content

sss_token/instructions/
initialize.rs

1use anchor_lang::prelude::*;
2use anchor_lang::solana_program::program::invoke;
3use anchor_spl::token_2022::{self, spl_token_2022};
4use spl_token_2022::{
5    extension::{
6        confidential_transfer::instruction::initialize_mint as init_ct_mint,
7        metadata_pointer::instruction::initialize as init_metadata_pointer,
8        ExtensionType,
9    },
10    instruction::initialize_mint2,
11};
12
13use crate::errors::SssError;
14use crate::events::StablecoinInitialized;
15use crate::state::*;
16
17#[derive(AnchorSerialize, AnchorDeserialize)]
18pub struct InitializeParams {
19    pub name: String,
20    pub symbol: String,
21    pub uri: String,
22    pub decimals: u8,
23    pub preset: StablecoinPreset,
24    pub enable_permanent_delegate: Option<bool>,
25    pub enable_transfer_hook: Option<bool>,
26    pub enable_default_state_frozen: Option<bool>,
27    pub enable_confidential_transfers: Option<bool>,
28}
29
30#[derive(Accounts)]
31#[instruction(params: InitializeParams)]
32pub struct Initialize<'info> {
33    #[account(mut)]
34    pub authority: Signer<'info>,
35
36    /// CHECK: The mint account, created in the instruction handler via CPI.
37    /// We use UncheckedAccount because Token-2022 mints with extensions
38    /// must be created with specific extension space before initialization.
39    #[account(mut)]
40    pub mint: Signer<'info>,
41
42    #[account(
43        init,
44        payer = authority,
45        space = StablecoinConfig::SPACE,
46        seeds = [StablecoinConfig::SEED_PREFIX, mint.key().as_ref()],
47        bump,
48    )]
49    pub config: Account<'info, StablecoinConfig>,
50
51    #[account(
52        init,
53        payer = authority,
54        space = RoleRegistry::SPACE,
55        seeds = [RoleRegistry::SEED_PREFIX, config.key().as_ref()],
56        bump,
57    )]
58    pub role_registry: Account<'info, RoleRegistry>,
59
60    pub system_program: Program<'info, System>,
61    pub token_program: Program<'info, token_2022::Token2022>,
62    pub rent: Sysvar<'info, Rent>,
63}
64
65pub fn handler(ctx: Context<Initialize>, params: InitializeParams) -> Result<()> {
66    require!(
67        ctx.accounts.authority.key() != Pubkey::default(),
68        SssError::ZeroAuthority
69    );
70    require!(
71        !params.name.is_empty() && params.name.len() <= StablecoinConfig::MAX_NAME_LEN,
72        SssError::NameTooLong
73    );
74    require!(
75        !params.symbol.is_empty() && params.symbol.len() <= StablecoinConfig::MAX_SYMBOL_LEN,
76        SssError::SymbolTooLong
77    );
78    require!(
79        !params.uri.is_empty() && params.uri.len() <= StablecoinConfig::MAX_URI_LEN,
80        SssError::UriTooLong
81    );
82    require!(params.decimals <= 18, SssError::InvalidDecimals);
83
84    let clock = Clock::get()?;
85    let config_key = ctx.accounts.config.key();
86
87    // Determine feature flags based on preset
88    let (enable_permanent_delegate, enable_transfer_hook, default_account_frozen, enable_ct) =
89        match params.preset {
90            StablecoinPreset::SSS1 => (false, false, false, false),
91            StablecoinPreset::SSS2 => (true, true, false, false),
92            StablecoinPreset::SSS3 => (true, false, false, true),
93            StablecoinPreset::Custom => {
94                let pd = params.enable_permanent_delegate.ok_or(SssError::CustomFlagsMissing)?;
95                let th = params.enable_transfer_hook.ok_or(SssError::CustomFlagsMissing)?;
96                let df = params.enable_default_state_frozen.ok_or(SssError::CustomFlagsMissing)?;
97                let ct = params.enable_confidential_transfers.ok_or(SssError::CustomFlagsMissing)?;
98                (pd, th, df, ct)
99            }
100        };
101
102    // Build extension list for mint space calculation
103    let mut extensions = vec![ExtensionType::MetadataPointer];
104    if enable_permanent_delegate {
105        extensions.push(ExtensionType::PermanentDelegate);
106    }
107    if enable_transfer_hook {
108        extensions.push(ExtensionType::TransferHook);
109    }
110    if default_account_frozen {
111        extensions.push(ExtensionType::DefaultAccountState);
112    }
113    if enable_ct {
114        extensions.push(ExtensionType::ConfidentialTransferMint);
115    }
116
117    let mint_space = ExtensionType::try_calculate_account_len::<spl_token_2022::state::Mint>(
118        &extensions,
119    )
120    .map_err(|_| SssError::Overflow)?;
121
122    let rent = &ctx.accounts.rent;
123    let lamports = rent.minimum_balance(mint_space);
124
125    // Create the mint account
126    invoke(
127        &anchor_lang::solana_program::system_instruction::create_account(
128            ctx.accounts.authority.key,
129            ctx.accounts.mint.key,
130            lamports,
131            mint_space as u64,
132            &token_2022::Token2022::id(),
133        ),
134        &[
135            ctx.accounts.authority.to_account_info(),
136            ctx.accounts.mint.to_account_info(),
137            ctx.accounts.system_program.to_account_info(),
138        ],
139    )?;
140
141    // Initialize metadata pointer extension (must be before mint init)
142    invoke(
143        &init_metadata_pointer(
144            &token_2022::Token2022::id(),
145            ctx.accounts.mint.key,
146            Some(config_key),
147            Some(ctx.accounts.mint.key()),
148        )?,
149        &[ctx.accounts.mint.to_account_info()],
150    )?;
151
152    // Initialize permanent delegate extension if enabled
153    if enable_permanent_delegate {
154        invoke(
155            &spl_token_2022::instruction::initialize_permanent_delegate(
156                &token_2022::Token2022::id(),
157                ctx.accounts.mint.key,
158                &config_key,
159            )?,
160            &[ctx.accounts.mint.to_account_info()],
161        )?;
162    }
163
164    // Initialize transfer hook extension if enabled
165    // For SSS-2, the hook program ID must be passed as the first remaining account
166    if enable_transfer_hook {
167        let hook_program_id = ctx
168            .remaining_accounts
169            .first()
170            .map(|a| a.key())
171            .ok_or(SssError::TransferHookNotEnabled)?;
172
173        require!(
174            hook_program_id != Pubkey::default(),
175            SssError::InvalidHookProgram
176        );
177
178        invoke(
179            &spl_token_2022::extension::transfer_hook::instruction::initialize(
180                &token_2022::Token2022::id(),
181                ctx.accounts.mint.key,
182                Some(config_key),
183                Some(hook_program_id),
184            )?,
185            &[ctx.accounts.mint.to_account_info()],
186        )?;
187    }
188
189    // Initialize default account state extension if enabled
190    if default_account_frozen {
191        invoke(
192            &spl_token_2022::extension::default_account_state::instruction::initialize_default_account_state(
193                &token_2022::Token2022::id(),
194                ctx.accounts.mint.key,
195                &spl_token_2022::state::AccountState::Frozen,
196            )?,
197            &[ctx.accounts.mint.to_account_info()],
198        )?;
199    }
200
201    // Initialize confidential transfer extension if enabled
202    if enable_ct {
203        invoke(
204            &init_ct_mint(
205                &token_2022::Token2022::id(),
206                ctx.accounts.mint.key,
207                Some(config_key), // CT authority = config PDA
208                true,             // auto-approve new accounts
209                None,             // no auditor ElGamal pubkey
210            )?,
211            &[ctx.accounts.mint.to_account_info()],
212        )?;
213    }
214
215    // Initialize the mint itself
216    invoke(
217        &initialize_mint2(
218            &token_2022::Token2022::id(),
219            ctx.accounts.mint.key,
220            &config_key,      // mint authority = config PDA
221            Some(&config_key), // freeze authority = config PDA
222            params.decimals,
223        )?,
224        &[ctx.accounts.mint.to_account_info()],
225    )?;
226
227    // Initialize StablecoinConfig
228    let config = &mut ctx.accounts.config;
229    config.bump = ctx.bumps.config;
230    config.mint = ctx.accounts.mint.key();
231    config.master_authority = ctx.accounts.authority.key();
232    config.name = params.name.clone();
233    config.symbol = params.symbol.clone();
234    config.uri = params.uri.clone();
235    config.decimals = params.decimals;
236    config.preset = params.preset;
237    config.enable_permanent_delegate = enable_permanent_delegate;
238    config.enable_transfer_hook = enable_transfer_hook;
239    config.default_account_frozen = default_account_frozen;
240    config.enable_confidential_transfers = enable_ct;
241    config.is_paused = false;
242    config.total_minted = 0;
243    config.total_burned = 0;
244    config.total_seized = 0;
245    config.audit_log_index = 0;
246    config.reserve_attestation_index = 0;
247    config.created_at = clock.unix_timestamp;
248    config.updated_at = clock.unix_timestamp;
249
250    // Initialize RoleRegistry
251    let role_registry = &mut ctx.accounts.role_registry;
252    role_registry.bump = ctx.bumps.role_registry;
253    role_registry.config = config.key();
254    role_registry.master_authority = ctx.accounts.authority.key();
255    role_registry.pauser = ctx.accounts.authority.key();
256    role_registry.blacklister = if enable_permanent_delegate {
257        ctx.accounts.authority.key()
258    } else {
259        Pubkey::default()
260    };
261    role_registry.seizer = if enable_permanent_delegate {
262        ctx.accounts.authority.key()
263    } else {
264        Pubkey::default()
265    };
266
267    emit!(StablecoinInitialized {
268        config: config.key(),
269        mint: config.mint,
270        master_authority: config.master_authority,
271        name: config.name.clone(),
272        symbol: config.symbol.clone(),
273        preset: config.preset as u8,
274        timestamp: clock.unix_timestamp,
275    });
276
277    Ok(())
278}