spl_token_2022/extension/confidential_mint_burn/
processor.rs

1#[cfg(feature = "zk-ops")]
2use spl_token_confidential_transfer_ciphertext_arithmetic as ciphertext_arithmetic;
3use {
4    crate::{
5        check_auditor_ciphertext, check_program_account,
6        error::TokenError,
7        extension::{
8            confidential_mint_burn::{
9                instruction::{
10                    BurnInstructionData, ConfidentialMintBurnInstruction, InitializeMintData,
11                    MintInstructionData, RotateSupplyElGamalPubkeyData,
12                    UpdateDecryptableSupplyData,
13                },
14                verify_proof::{verify_burn_proof, verify_mint_proof},
15                ConfidentialMintBurn,
16            },
17            confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint},
18            pausable::PausableConfig,
19            BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensionsMut,
20        },
21        instruction::{decode_instruction_data, decode_instruction_type},
22        pod::{PodAccount, PodMint},
23        processor::Processor,
24    },
25    solana_program::{
26        account_info::{next_account_info, AccountInfo},
27        entrypoint::ProgramResult,
28        msg,
29        program_error::ProgramError,
30        pubkey::Pubkey,
31    },
32    solana_zk_sdk::{
33        encryption::pod::{auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey},
34        zk_elgamal_proof_program::proof_data::{
35            CiphertextCiphertextEqualityProofContext, CiphertextCiphertextEqualityProofData,
36        },
37    },
38    spl_token_confidential_transfer_proof_extraction::instruction::verify_and_extract_context,
39};
40
41/// Processes an [`InitializeMint`] instruction.
42fn process_initialize_mint(accounts: &[AccountInfo], data: &InitializeMintData) -> ProgramResult {
43    let account_info_iter = &mut accounts.iter();
44    let mint_info = next_account_info(account_info_iter)?;
45
46    check_program_account(mint_info.owner)?;
47
48    let mint_data = &mut mint_info.data.borrow_mut();
49    let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack_uninitialized(mint_data)?;
50    let mint_burn_extension = mint.init_extension::<ConfidentialMintBurn>(true)?;
51
52    mint_burn_extension.supply_elgamal_pubkey = data.supply_elgamal_pubkey;
53    mint_burn_extension.decryptable_supply = data.decryptable_supply;
54
55    Ok(())
56}
57
58/// Processes an [`RotateSupplyElGamal`] instruction.
59#[cfg(feature = "zk-ops")]
60fn process_rotate_supply_elgamal_pubkey(
61    program_id: &Pubkey,
62    accounts: &[AccountInfo],
63    data: &RotateSupplyElGamalPubkeyData,
64) -> ProgramResult {
65    let account_info_iter = &mut accounts.iter();
66    let mint_info = next_account_info(account_info_iter)?;
67
68    check_program_account(mint_info.owner)?;
69    let mint_data = &mut mint_info.data.borrow_mut();
70    let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack(mint_data)?;
71    let mint_authority = mint.base.mint_authority;
72    let mint_burn_extension = mint.get_extension_mut::<ConfidentialMintBurn>()?;
73
74    let proof_context = verify_and_extract_context::<
75        CiphertextCiphertextEqualityProofData,
76        CiphertextCiphertextEqualityProofContext,
77    >(
78        account_info_iter,
79        data.proof_instruction_offset as i64,
80        None,
81    )?;
82
83    let supply_elgamal_pubkey: Option<PodElGamalPubkey> =
84        mint_burn_extension.supply_elgamal_pubkey.into();
85    let Some(supply_elgamal_pubkey) = supply_elgamal_pubkey else {
86        return Err(TokenError::InvalidState.into());
87    };
88
89    if !supply_elgamal_pubkey.eq(&proof_context.first_pubkey) {
90        return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into());
91    }
92    if mint_burn_extension.confidential_supply != proof_context.first_ciphertext {
93        return Err(ProgramError::InvalidInstructionData);
94    }
95
96    let authority_info = next_account_info(account_info_iter)?;
97    let authority_info_data_len = authority_info.data_len();
98    let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?;
99
100    Processor::validate_owner(
101        program_id,
102        &authority,
103        authority_info,
104        authority_info_data_len,
105        account_info_iter.as_slice(),
106    )?;
107
108    mint_burn_extension.supply_elgamal_pubkey = proof_context.second_pubkey;
109    mint_burn_extension.confidential_supply = proof_context.second_ciphertext;
110
111    Ok(())
112}
113
114/// Processes an [`UpdateAuthority`] instruction.
115fn process_update_decryptable_supply(
116    program_id: &Pubkey,
117    accounts: &[AccountInfo],
118    new_decryptable_supply: PodAeCiphertext,
119) -> ProgramResult {
120    let account_info_iter = &mut accounts.iter();
121    let mint_info = next_account_info(account_info_iter)?;
122
123    check_program_account(mint_info.owner)?;
124    let mint_data = &mut mint_info.data.borrow_mut();
125    let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack(mint_data)?;
126    let mint_authority = mint.base.mint_authority;
127    let mint_burn_extension = mint.get_extension_mut::<ConfidentialMintBurn>()?;
128
129    let authority_info = next_account_info(account_info_iter)?;
130    let authority_info_data_len = authority_info.data_len();
131    let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?;
132
133    Processor::validate_owner(
134        program_id,
135        &authority,
136        authority_info,
137        authority_info_data_len,
138        account_info_iter.as_slice(),
139    )?;
140
141    mint_burn_extension.decryptable_supply = new_decryptable_supply;
142
143    Ok(())
144}
145
146/// Processes a [`ConfidentialMint`] instruction.
147#[cfg(feature = "zk-ops")]
148fn process_confidential_mint(
149    program_id: &Pubkey,
150    accounts: &[AccountInfo],
151    data: &MintInstructionData,
152) -> ProgramResult {
153    let account_info_iter = &mut accounts.iter();
154    let token_account_info = next_account_info(account_info_iter)?;
155    let mint_info = next_account_info(account_info_iter)?;
156
157    check_program_account(mint_info.owner)?;
158    let mint_data = &mut mint_info.data.borrow_mut();
159    let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack(mint_data)?;
160    let mint_authority = mint.base.mint_authority;
161
162    let auditor_elgamal_pubkey = mint
163        .get_extension::<ConfidentialTransferMint>()?
164        .auditor_elgamal_pubkey;
165    if let Ok(extension) = mint.get_extension::<PausableConfig>() {
166        if extension.paused.into() {
167            return Err(TokenError::MintPaused.into());
168        }
169    }
170    let mint_burn_extension = mint.get_extension_mut::<ConfidentialMintBurn>()?;
171
172    let proof_context = verify_mint_proof(
173        account_info_iter,
174        data.equality_proof_instruction_offset,
175        data.ciphertext_validity_proof_instruction_offset,
176        data.range_proof_instruction_offset,
177    )?;
178
179    check_program_account(token_account_info.owner)?;
180    let token_account_data = &mut token_account_info.data.borrow_mut();
181    let mut token_account = PodStateWithExtensionsMut::<PodAccount>::unpack(token_account_data)?;
182
183    let authority_info = next_account_info(account_info_iter)?;
184    let authority_info_data_len = authority_info.data_len();
185    let authority = mint_authority.ok_or(TokenError::NoAuthorityExists)?;
186
187    Processor::validate_owner(
188        program_id,
189        &authority,
190        authority_info,
191        authority_info_data_len,
192        account_info_iter.as_slice(),
193    )?;
194
195    if token_account.base.is_frozen() {
196        return Err(TokenError::AccountFrozen.into());
197    }
198
199    if token_account.base.mint != *mint_info.key {
200        return Err(TokenError::MintMismatch.into());
201    }
202
203    assert!(!token_account.base.is_native());
204
205    let confidential_transfer_account =
206        token_account.get_extension_mut::<ConfidentialTransferAccount>()?;
207    confidential_transfer_account.valid_as_destination()?;
208
209    if proof_context.mint_pubkeys.destination != confidential_transfer_account.elgamal_pubkey {
210        return Err(ProgramError::InvalidInstructionData);
211    }
212
213    if let Some(auditor_pubkey) = Option::<PodElGamalPubkey>::from(auditor_elgamal_pubkey) {
214        if auditor_pubkey != proof_context.mint_pubkeys.auditor {
215            return Err(ProgramError::InvalidInstructionData);
216        }
217    }
218
219    let proof_context_auditor_ciphertext_lo = proof_context
220        .mint_amount_ciphertext_lo
221        .try_extract_ciphertext(2)
222        .map_err(TokenError::from)?;
223    let proof_context_auditor_ciphertext_hi = proof_context
224        .mint_amount_ciphertext_hi
225        .try_extract_ciphertext(2)
226        .map_err(TokenError::from)?;
227
228    check_auditor_ciphertext(
229        &data.mint_amount_auditor_ciphertext_lo,
230        &data.mint_amount_auditor_ciphertext_hi,
231        &proof_context_auditor_ciphertext_lo,
232        &proof_context_auditor_ciphertext_hi,
233    )?;
234
235    confidential_transfer_account.pending_balance_lo = ciphertext_arithmetic::add(
236        &confidential_transfer_account.pending_balance_lo,
237        &proof_context
238            .mint_amount_ciphertext_lo
239            .try_extract_ciphertext(0)
240            .map_err(TokenError::from)?,
241    )
242    .ok_or(TokenError::CiphertextArithmeticFailed)?;
243    confidential_transfer_account.pending_balance_hi = ciphertext_arithmetic::add(
244        &confidential_transfer_account.pending_balance_hi,
245        &proof_context
246            .mint_amount_ciphertext_hi
247            .try_extract_ciphertext(0)
248            .map_err(TokenError::from)?,
249    )
250    .ok_or(TokenError::CiphertextArithmeticFailed)?;
251
252    confidential_transfer_account.increment_pending_balance_credit_counter()?;
253
254    // update supply
255    if mint_burn_extension.supply_elgamal_pubkey != proof_context.mint_pubkeys.supply {
256        return Err(ProgramError::InvalidInstructionData);
257    }
258    let current_supply = mint_burn_extension.confidential_supply;
259    mint_burn_extension.confidential_supply = ciphertext_arithmetic::add_with_lo_hi(
260        &current_supply,
261        &proof_context
262            .mint_amount_ciphertext_lo
263            .try_extract_ciphertext(2)
264            .map_err(|_| ProgramError::InvalidAccountData)?,
265        &proof_context
266            .mint_amount_ciphertext_hi
267            .try_extract_ciphertext(2)
268            .map_err(|_| ProgramError::InvalidAccountData)?,
269    )
270    .ok_or(TokenError::CiphertextArithmeticFailed)?;
271    mint_burn_extension.decryptable_supply = data.new_decryptable_supply;
272
273    Ok(())
274}
275
276/// Processes a [`ConfidentialBurn`] instruction.
277#[cfg(feature = "zk-ops")]
278fn process_confidential_burn(
279    program_id: &Pubkey,
280    accounts: &[AccountInfo],
281    data: &BurnInstructionData,
282) -> ProgramResult {
283    let account_info_iter = &mut accounts.iter();
284    let token_account_info = next_account_info(account_info_iter)?;
285    let mint_info = next_account_info(account_info_iter)?;
286
287    check_program_account(mint_info.owner)?;
288    let mint_data = &mut mint_info.data.borrow_mut();
289    let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack(mint_data)?;
290
291    let auditor_elgamal_pubkey = mint
292        .get_extension::<ConfidentialTransferMint>()?
293        .auditor_elgamal_pubkey;
294    if let Ok(extension) = mint.get_extension::<PausableConfig>() {
295        if extension.paused.into() {
296            return Err(TokenError::MintPaused.into());
297        }
298    }
299    let mint_burn_extension = mint.get_extension_mut::<ConfidentialMintBurn>()?;
300
301    let proof_context = verify_burn_proof(
302        account_info_iter,
303        data.equality_proof_instruction_offset,
304        data.ciphertext_validity_proof_instruction_offset,
305        data.range_proof_instruction_offset,
306    )?;
307
308    check_program_account(token_account_info.owner)?;
309    let token_account_data = &mut token_account_info.data.borrow_mut();
310    let mut token_account = PodStateWithExtensionsMut::<PodAccount>::unpack(token_account_data)?;
311
312    let authority_info = next_account_info(account_info_iter)?;
313    let authority_info_data_len = authority_info.data_len();
314
315    Processor::validate_owner(
316        program_id,
317        &token_account.base.owner,
318        authority_info,
319        authority_info_data_len,
320        account_info_iter.as_slice(),
321    )?;
322
323    if token_account.base.is_frozen() {
324        return Err(TokenError::AccountFrozen.into());
325    }
326
327    if token_account.base.mint != *mint_info.key {
328        return Err(TokenError::MintMismatch.into());
329    }
330
331    let confidential_transfer_account =
332        token_account.get_extension_mut::<ConfidentialTransferAccount>()?;
333    confidential_transfer_account.valid_as_source()?;
334
335    // Check that the source encryption public key is consistent with what was
336    // actually used to generate the zkp.
337    if proof_context.burn_pubkeys.source != confidential_transfer_account.elgamal_pubkey {
338        return Err(TokenError::ConfidentialTransferElGamalPubkeyMismatch.into());
339    }
340
341    let proof_context_auditor_ciphertext_lo = proof_context
342        .burn_amount_ciphertext_lo
343        .try_extract_ciphertext(2)
344        .map_err(TokenError::from)?;
345    let proof_context_auditor_ciphertext_hi = proof_context
346        .burn_amount_ciphertext_hi
347        .try_extract_ciphertext(2)
348        .map_err(TokenError::from)?;
349
350    check_auditor_ciphertext(
351        &data.burn_amount_auditor_ciphertext_lo,
352        &data.burn_amount_auditor_ciphertext_hi,
353        &proof_context_auditor_ciphertext_lo,
354        &proof_context_auditor_ciphertext_hi,
355    )?;
356
357    let burn_amount_lo = &proof_context
358        .burn_amount_ciphertext_lo
359        .try_extract_ciphertext(0)
360        .map_err(TokenError::from)?;
361    let burn_amount_hi = &proof_context
362        .burn_amount_ciphertext_hi
363        .try_extract_ciphertext(0)
364        .map_err(TokenError::from)?;
365
366    let new_source_available_balance = ciphertext_arithmetic::subtract_with_lo_hi(
367        &confidential_transfer_account.available_balance,
368        burn_amount_lo,
369        burn_amount_hi,
370    )
371    .ok_or(TokenError::CiphertextArithmeticFailed)?;
372
373    // Check that the computed available balance is consistent with what was
374    // actually used to generate the zkp on the client side.
375    if new_source_available_balance != proof_context.remaining_balance_ciphertext {
376        return Err(TokenError::ConfidentialTransferBalanceMismatch.into());
377    }
378
379    confidential_transfer_account.available_balance = new_source_available_balance;
380    confidential_transfer_account.decryptable_available_balance =
381        data.new_decryptable_available_balance;
382
383    if let Some(auditor_pubkey) = Option::<PodElGamalPubkey>::from(auditor_elgamal_pubkey) {
384        if auditor_pubkey != proof_context.burn_pubkeys.auditor {
385            return Err(ProgramError::InvalidInstructionData);
386        }
387    }
388
389    // update supply
390    if mint_burn_extension.supply_elgamal_pubkey != proof_context.burn_pubkeys.supply {
391        return Err(ProgramError::InvalidInstructionData);
392    }
393    let current_supply = mint_burn_extension.confidential_supply;
394    mint_burn_extension.confidential_supply = ciphertext_arithmetic::subtract_with_lo_hi(
395        &current_supply,
396        &proof_context
397            .burn_amount_ciphertext_lo
398            .try_extract_ciphertext(2)
399            .map_err(|_| ProgramError::InvalidAccountData)?,
400        &proof_context
401            .burn_amount_ciphertext_hi
402            .try_extract_ciphertext(2)
403            .map_err(|_| ProgramError::InvalidAccountData)?,
404    )
405    .ok_or(TokenError::CiphertextArithmeticFailed)?;
406
407    Ok(())
408}
409
410#[allow(dead_code)]
411pub(crate) fn process_instruction(
412    program_id: &Pubkey,
413    accounts: &[AccountInfo],
414    input: &[u8],
415) -> ProgramResult {
416    check_program_account(program_id)?;
417
418    match decode_instruction_type(input)? {
419        ConfidentialMintBurnInstruction::InitializeMint => {
420            msg!("ConfidentialMintBurnInstruction::InitializeMint");
421            let data = decode_instruction_data::<InitializeMintData>(input)?;
422            process_initialize_mint(accounts, data)
423        }
424        ConfidentialMintBurnInstruction::RotateSupplyElGamalPubkey => {
425            msg!("ConfidentialMintBurnInstruction::RotateSupplyElGamal");
426            let data = decode_instruction_data::<RotateSupplyElGamalPubkeyData>(input)?;
427            process_rotate_supply_elgamal_pubkey(program_id, accounts, data)
428        }
429        ConfidentialMintBurnInstruction::UpdateDecryptableSupply => {
430            msg!("ConfidentialMintBurnInstruction::UpdateDecryptableSupply");
431            let data = decode_instruction_data::<UpdateDecryptableSupplyData>(input)?;
432            process_update_decryptable_supply(program_id, accounts, data.new_decryptable_supply)
433        }
434        ConfidentialMintBurnInstruction::Mint => {
435            msg!("ConfidentialMintBurnInstruction::ConfidentialMint");
436            let data = decode_instruction_data::<MintInstructionData>(input)?;
437            process_confidential_mint(program_id, accounts, data)
438        }
439        ConfidentialMintBurnInstruction::Burn => {
440            msg!("ConfidentialMintBurnInstruction::ConfidentialBurn");
441            let data = decode_instruction_data::<BurnInstructionData>(input)?;
442            process_confidential_burn(program_id, accounts, data)
443        }
444    }
445}