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