spl_token_2022_interface/extension/confidential_transfer_fee/
instruction.rs

1#[cfg(feature = "serde")]
2use {
3    crate::serialization::{aeciphertext_fromstr, elgamalpubkey_fromstr},
4    serde::{Deserialize, Serialize},
5};
6use {
7    crate::{
8        check_program_account,
9        error::TokenError,
10        extension::confidential_transfer::{
11            instruction::CiphertextCiphertextEqualityProofData, DecryptableBalance,
12        },
13        instruction::{encode_instruction, TokenInstruction},
14        solana_zk_sdk::{
15            encryption::pod::elgamal::PodElGamalPubkey,
16            zk_elgamal_proof_program::instruction::ProofInstruction,
17        },
18    },
19    bytemuck::{Pod, Zeroable},
20    num_enum::{IntoPrimitive, TryFromPrimitive},
21    solana_instruction::{AccountMeta, Instruction},
22    solana_program_error::ProgramError,
23    solana_pubkey::Pubkey,
24    solana_sdk_ids::sysvar,
25    spl_pod::optional_keys::OptionalNonZeroPubkey,
26    spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation,
27    std::convert::TryFrom,
28};
29
30/// Confidential Transfer extension instructions
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
33#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)]
34#[repr(u8)]
35pub enum ConfidentialTransferFeeInstruction {
36    /// Initializes confidential transfer fees for a mint.
37    ///
38    /// The `ConfidentialTransferFeeInstruction::InitializeConfidentialTransferFeeConfig`
39    /// instruction requires no signers and MUST be included within the same
40    /// Transaction as `TokenInstruction::InitializeMint`. Otherwise another
41    /// party can initialize the configuration.
42    ///
43    /// The instruction fails if the `TokenInstruction::InitializeMint`
44    /// instruction has already executed for the mint.
45    ///
46    /// Accounts expected by this instruction:
47    ///
48    ///   0. `[writable]` The SPL Token mint.
49    ///
50    /// Data expected by this instruction:
51    ///   `InitializeConfidentialTransferFeeConfigData`
52    InitializeConfidentialTransferFeeConfig,
53
54    /// Transfer all withheld confidential tokens in the mint to an account.
55    /// Signed by the mint's withdraw withheld tokens authority.
56    ///
57    /// The withheld confidential tokens are aggregated directly into the
58    /// destination available balance.
59    ///
60    /// In order for this instruction to be successfully processed, it must be
61    /// accompanied by the `VerifyCiphertextCiphertextEquality` instruction
62    /// of the `zk_elgamal_proof` program in the same transaction or the
63    /// address of a context state account for the proof must be provided.
64    ///
65    /// Accounts expected by this instruction:
66    ///
67    ///   * Single owner/delegate
68    ///   0. `[writable]` The token mint. Must include the `TransferFeeConfig`
69    ///      extension.
70    ///   1. `[writable]` The fee receiver account. Must include the
71    ///      `TransferFeeAmount` and `ConfidentialTransferAccount` extensions.
72    ///   2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is
73    ///      included in the same transaction or context state account if
74    ///      `VerifyCiphertextCiphertextEquality` is pre-verified into a context
75    ///      state account.
76    ///   3. `[signer]` The mint's `withdraw_withheld_authority`.
77    ///
78    ///   * Multisignature owner/delegate
79    ///   0. `[writable]` The token mint. Must include the `TransferFeeConfig`
80    ///      extension.
81    ///   1. `[writable]` The fee receiver account. Must include the
82    ///      `TransferFeeAmount` and `ConfidentialTransferAccount` extensions.
83    ///   2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is
84    ///      included in the same transaction or context state account if
85    ///      `VerifyCiphertextCiphertextEquality` is pre-verified into a context
86    ///      state account.
87    ///   3. `[]` The mint's multisig `withdraw_withheld_authority`.
88    ///   4. ..`4+M` `[signer]` M signer accounts.
89    ///
90    /// Data expected by this instruction:
91    ///   `WithdrawWithheldTokensFromMintData`
92    WithdrawWithheldTokensFromMint,
93
94    /// Transfer all withheld tokens to an account. Signed by the mint's
95    /// withdraw withheld tokens authority. This instruction is susceptible
96    /// to front-running. Use `HarvestWithheldTokensToMint` and
97    /// `WithdrawWithheldTokensFromMint` as an alternative.
98    ///
99    /// The withheld confidential tokens are aggregated directly into the
100    /// destination available balance.
101    ///
102    /// Note on front-running: This instruction requires a zero-knowledge proof
103    /// verification instruction that is checked with respect to the account
104    /// state (the currently withheld fees). Suppose that a withdraw
105    /// withheld authority generates the
106    /// `WithdrawWithheldTokensFromAccounts` instruction along with a
107    /// corresponding zero-knowledge proof for a specified set of accounts,
108    /// and submits it on chain. If the withheld fees at any
109    /// of the specified accounts change before the
110    /// `WithdrawWithheldTokensFromAccounts` is executed on chain, the
111    /// zero-knowledge proof will not verify with respect to the new state,
112    /// forcing the transaction to fail.
113    ///
114    /// If front-running occurs, then users can look up the updated states of
115    /// the accounts, generate a new zero-knowledge proof and try again.
116    /// Alternatively, withdraw withheld authority can first move the
117    /// withheld amount to the mint using `HarvestWithheldTokensToMint` and
118    /// then move the withheld fees from mint to a specified destination
119    /// account using `WithdrawWithheldTokensFromMint`.
120    ///
121    /// In order for this instruction to be successfully processed, it must be
122    /// accompanied by the `VerifyWithdrawWithheldTokens` instruction of the
123    /// `zk_elgamal_proof` program in the same transaction or the address of a
124    /// context state account for the proof must be provided.
125    ///
126    /// Accounts expected by this instruction:
127    ///
128    ///   * Single owner/delegate
129    ///   0. `[]` The token mint. Must include the `TransferFeeConfig`
130    ///      extension.
131    ///   1. `[writable]` The fee receiver account. Must include the
132    ///      `TransferFeeAmount` and `ConfidentialTransferAccount` extensions.
133    ///   2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is
134    ///      included in the same transaction or context state account if
135    ///      `VerifyCiphertextCiphertextEquality` is pre-verified into a context
136    ///      state account.
137    ///   3. `[signer]` The mint's `withdraw_withheld_authority`.
138    ///   4. ..`4+N` `[writable]` The source accounts to withdraw from.
139    ///
140    ///   * Multisignature owner/delegate
141    ///   0. `[]` The token mint. Must include the `TransferFeeConfig`
142    ///      extension.
143    ///   1. `[writable]` The fee receiver account. Must include the
144    ///      `TransferFeeAmount` and `ConfidentialTransferAccount` extensions.
145    ///   2. `[]` Instructions sysvar if `VerifyCiphertextCiphertextEquality` is
146    ///      included in the same transaction or context state account if
147    ///      `VerifyCiphertextCiphertextEquality` is pre-verified into a context
148    ///      state account.
149    ///   3. `[]` The mint's multisig `withdraw_withheld_authority`.
150    ///   4. ..`4+M` `[signer]` M signer accounts.
151    ///   5. `5+M+1..5+M+N` `[writable]` The source accounts to withdraw from.
152    ///
153    /// Data expected by this instruction:
154    ///   `WithdrawWithheldTokensFromAccountsData`
155    WithdrawWithheldTokensFromAccounts,
156
157    /// Permissionless instruction to transfer all withheld confidential tokens
158    /// to the mint.
159    ///
160    /// Succeeds for frozen accounts.
161    ///
162    /// Accounts provided should include both the `TransferFeeAmount` and
163    /// `ConfidentialTransferAccount` extension. If not, the account is skipped.
164    ///
165    /// Accounts expected by this instruction:
166    ///
167    ///   0. `[writable]` The mint.
168    ///   1. ..`1+N` `[writable]` The source accounts to harvest from.
169    ///
170    /// Data expected by this instruction:
171    ///   None
172    HarvestWithheldTokensToMint,
173
174    /// Configure a confidential transfer fee mint to accept harvested
175    /// confidential fees.
176    ///
177    /// Accounts expected by this instruction:
178    ///
179    ///   * Single owner/delegate
180    ///   0. `[writable]` The token mint.
181    ///   1. `[signer]` The confidential transfer fee authority.
182    ///
183    ///   *Multisignature owner/delegate
184    ///   0. `[writable]` The token mint.
185    ///   1. `[]` The confidential transfer fee multisig authority,
186    ///   2. `[signer]` Required M signer accounts for the SPL Token Multisig
187    ///      account.
188    ///
189    /// Data expected by this instruction:
190    ///   None
191    EnableHarvestToMint,
192
193    /// Configure a confidential transfer fee mint to reject any harvested
194    /// confidential fees.
195    ///
196    /// Accounts expected by this instruction:
197    ///
198    ///   * Single owner/delegate
199    ///   0. `[writable]` The token mint.
200    ///   1. `[signer]` The confidential transfer fee authority.
201    ///
202    ///   *Multisignature owner/delegate
203    ///   0. `[writable]` The token mint.
204    ///   1. `[]` The confidential transfer fee multisig authority,
205    ///   2. `[signer]` Required M signer accounts for the SPL Token Multisig
206    ///      account.
207    ///
208    /// Data expected by this instruction:
209    ///   None
210    DisableHarvestToMint,
211}
212
213/// Data expected by `InitializeConfidentialTransferFeeConfig`
214#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
215#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
216#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
217#[repr(C)]
218pub struct InitializeConfidentialTransferFeeConfigData {
219    /// confidential transfer fee authority
220    pub authority: OptionalNonZeroPubkey,
221
222    /// ElGamal public key used to encrypt withheld fees.
223    #[cfg_attr(feature = "serde", serde(with = "elgamalpubkey_fromstr"))]
224    pub withdraw_withheld_authority_elgamal_pubkey: PodElGamalPubkey,
225}
226
227/// Data expected by
228/// `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint`
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
231#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
232#[repr(C)]
233pub struct WithdrawWithheldTokensFromMintData {
234    /// Relative location of the `ProofInstruction::VerifyWithdrawWithheld`
235    /// instruction to the `WithdrawWithheldTokensFromMint` instruction in
236    /// the transaction. If the offset is `0`, then use a context state
237    /// account for the proof.
238    pub proof_instruction_offset: i8,
239    /// The new decryptable balance in the destination token account.
240    #[cfg_attr(feature = "serde", serde(with = "aeciphertext_fromstr"))]
241    pub new_decryptable_available_balance: DecryptableBalance,
242}
243
244/// Data expected by
245/// `ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts`
246#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
247#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
248#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
249#[repr(C)]
250pub struct WithdrawWithheldTokensFromAccountsData {
251    /// Number of token accounts harvested
252    pub num_token_accounts: u8,
253    /// Relative location of the `ProofInstruction::VerifyWithdrawWithheld`
254    /// instruction to the `VerifyWithdrawWithheldTokensFromAccounts`
255    /// instruction in the transaction. If the offset is `0`, then use a
256    /// context state account for the proof.
257    pub proof_instruction_offset: i8,
258    /// The new decryptable balance in the destination token account.
259    #[cfg_attr(feature = "serde", serde(with = "aeciphertext_fromstr"))]
260    pub new_decryptable_available_balance: DecryptableBalance,
261}
262
263/// Create a `InitializeConfidentialTransferFeeConfig` instruction
264pub fn initialize_confidential_transfer_fee_config(
265    token_program_id: &Pubkey,
266    mint: &Pubkey,
267    authority: Option<Pubkey>,
268    withdraw_withheld_authority_elgamal_pubkey: &PodElGamalPubkey,
269) -> Result<Instruction, ProgramError> {
270    check_program_account(token_program_id)?;
271    let accounts = vec![AccountMeta::new(*mint, false)];
272
273    Ok(encode_instruction(
274        token_program_id,
275        accounts,
276        TokenInstruction::ConfidentialTransferFeeExtension,
277        ConfidentialTransferFeeInstruction::InitializeConfidentialTransferFeeConfig,
278        &InitializeConfidentialTransferFeeConfigData {
279            authority: authority.try_into()?,
280            withdraw_withheld_authority_elgamal_pubkey: *withdraw_withheld_authority_elgamal_pubkey,
281        },
282    ))
283}
284
285/// Create an inner `WithdrawWithheldTokensFromMint` instruction
286///
287/// This instruction is suitable for use with a cross-program `invoke`
288pub fn inner_withdraw_withheld_tokens_from_mint(
289    token_program_id: &Pubkey,
290    mint: &Pubkey,
291    destination: &Pubkey,
292    new_decryptable_available_balance: &DecryptableBalance,
293    authority: &Pubkey,
294    multisig_signers: &[&Pubkey],
295    proof_data_location: ProofLocation<CiphertextCiphertextEqualityProofData>,
296) -> Result<Instruction, ProgramError> {
297    check_program_account(token_program_id)?;
298    let mut accounts = vec![
299        AccountMeta::new(*mint, false),
300        AccountMeta::new(*destination, false),
301    ];
302
303    let proof_instruction_offset = match proof_data_location {
304        ProofLocation::InstructionOffset(proof_instruction_offset, _) => {
305            accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false));
306            proof_instruction_offset.into()
307        }
308        ProofLocation::ContextStateAccount(context_state_account) => {
309            accounts.push(AccountMeta::new_readonly(*context_state_account, false));
310            0
311        }
312    };
313
314    accounts.push(AccountMeta::new_readonly(
315        *authority,
316        multisig_signers.is_empty(),
317    ));
318
319    for multisig_signer in multisig_signers.iter() {
320        accounts.push(AccountMeta::new_readonly(**multisig_signer, true));
321    }
322
323    Ok(encode_instruction(
324        token_program_id,
325        accounts,
326        TokenInstruction::ConfidentialTransferFeeExtension,
327        ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromMint,
328        &WithdrawWithheldTokensFromMintData {
329            proof_instruction_offset,
330            new_decryptable_available_balance: *new_decryptable_available_balance,
331        },
332    ))
333}
334
335/// Create an `WithdrawWithheldTokensFromMint` instruction
336pub fn withdraw_withheld_tokens_from_mint(
337    token_program_id: &Pubkey,
338    mint: &Pubkey,
339    destination: &Pubkey,
340    new_decryptable_available_balance: &DecryptableBalance,
341    authority: &Pubkey,
342    multisig_signers: &[&Pubkey],
343    proof_data_location: ProofLocation<CiphertextCiphertextEqualityProofData>,
344) -> Result<Vec<Instruction>, ProgramError> {
345    let mut instructions = vec![inner_withdraw_withheld_tokens_from_mint(
346        token_program_id,
347        mint,
348        destination,
349        new_decryptable_available_balance,
350        authority,
351        multisig_signers,
352        proof_data_location,
353    )?];
354
355    if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) =
356        proof_data_location
357    {
358        // This constructor appends the proof instruction right after the
359        // `WithdrawWithheldTokensFromMint` instruction. This means that the proof
360        // instruction offset must be always be 1. To use an arbitrary proof
361        // instruction offset, use the
362        // `inner_withdraw_withheld_tokens_from_mint` constructor.
363        let proof_instruction_offset: i8 = proof_instruction_offset.into();
364        if proof_instruction_offset != 1 {
365            return Err(TokenError::InvalidProofInstructionOffset.into());
366        }
367        instructions.push(
368            ProofInstruction::VerifyCiphertextCiphertextEquality
369                .encode_verify_proof(None, proof_data),
370        );
371    };
372
373    Ok(instructions)
374}
375
376/// Create an inner `WithdrawWithheldTokensFromMint` instruction
377///
378/// This instruction is suitable for use with a cross-program `invoke`
379#[allow(clippy::too_many_arguments)]
380pub fn inner_withdraw_withheld_tokens_from_accounts(
381    token_program_id: &Pubkey,
382    mint: &Pubkey,
383    destination: &Pubkey,
384    new_decryptable_available_balance: &DecryptableBalance,
385    authority: &Pubkey,
386    multisig_signers: &[&Pubkey],
387    sources: &[&Pubkey],
388    proof_data_location: ProofLocation<CiphertextCiphertextEqualityProofData>,
389) -> Result<Instruction, ProgramError> {
390    check_program_account(token_program_id)?;
391    let num_token_accounts =
392        u8::try_from(sources.len()).map_err(|_| ProgramError::InvalidInstructionData)?;
393    let mut accounts = vec![
394        AccountMeta::new(*mint, false),
395        AccountMeta::new(*destination, false),
396    ];
397
398    let proof_instruction_offset = match proof_data_location {
399        ProofLocation::InstructionOffset(proof_instruction_offset, _) => {
400            accounts.push(AccountMeta::new_readonly(sysvar::instructions::id(), false));
401            proof_instruction_offset.into()
402        }
403        ProofLocation::ContextStateAccount(context_state_account) => {
404            accounts.push(AccountMeta::new_readonly(*context_state_account, false));
405            0
406        }
407    };
408
409    accounts.push(AccountMeta::new_readonly(
410        *authority,
411        multisig_signers.is_empty(),
412    ));
413
414    for multisig_signer in multisig_signers.iter() {
415        accounts.push(AccountMeta::new_readonly(**multisig_signer, true));
416    }
417
418    for source in sources.iter() {
419        accounts.push(AccountMeta::new(**source, false));
420    }
421
422    Ok(encode_instruction(
423        token_program_id,
424        accounts,
425        TokenInstruction::ConfidentialTransferFeeExtension,
426        ConfidentialTransferFeeInstruction::WithdrawWithheldTokensFromAccounts,
427        &WithdrawWithheldTokensFromAccountsData {
428            proof_instruction_offset,
429            num_token_accounts,
430            new_decryptable_available_balance: *new_decryptable_available_balance,
431        },
432    ))
433}
434
435/// Create a `WithdrawWithheldTokensFromAccounts` instruction
436#[allow(clippy::too_many_arguments)]
437pub fn withdraw_withheld_tokens_from_accounts(
438    token_program_id: &Pubkey,
439    mint: &Pubkey,
440    destination: &Pubkey,
441    new_decryptable_available_balance: &DecryptableBalance,
442    authority: &Pubkey,
443    multisig_signers: &[&Pubkey],
444    sources: &[&Pubkey],
445    proof_data_location: ProofLocation<CiphertextCiphertextEqualityProofData>,
446) -> Result<Vec<Instruction>, ProgramError> {
447    let mut instructions = vec![inner_withdraw_withheld_tokens_from_accounts(
448        token_program_id,
449        mint,
450        destination,
451        new_decryptable_available_balance,
452        authority,
453        multisig_signers,
454        sources,
455        proof_data_location,
456    )?];
457
458    if let ProofLocation::InstructionOffset(proof_instruction_offset, proof_data) =
459        proof_data_location
460    {
461        // This constructor appends the proof instruction right after the
462        // `WithdrawWithheldTokensFromAccounts` instruction. This means that the proof
463        // instruction offset must always be 1. To use an arbitrary proof
464        // instruction offset, use the
465        // `inner_withdraw_withheld_tokens_from_accounts` constructor.
466        let proof_instruction_offset: i8 = proof_instruction_offset.into();
467        if proof_instruction_offset != 1 {
468            return Err(TokenError::InvalidProofInstructionOffset.into());
469        }
470        instructions.push(
471            ProofInstruction::VerifyCiphertextCiphertextEquality
472                .encode_verify_proof(None, proof_data),
473        );
474    };
475
476    Ok(instructions)
477}
478
479/// Creates a `HarvestWithheldTokensToMint` instruction
480pub fn harvest_withheld_tokens_to_mint(
481    token_program_id: &Pubkey,
482    mint: &Pubkey,
483    sources: &[&Pubkey],
484) -> Result<Instruction, ProgramError> {
485    check_program_account(token_program_id)?;
486    let mut accounts = vec![AccountMeta::new(*mint, false)];
487
488    for source in sources.iter() {
489        accounts.push(AccountMeta::new(**source, false));
490    }
491
492    Ok(encode_instruction(
493        token_program_id,
494        accounts,
495        TokenInstruction::ConfidentialTransferFeeExtension,
496        ConfidentialTransferFeeInstruction::HarvestWithheldTokensToMint,
497        &(),
498    ))
499}
500
501/// Create an `EnableHarvestToMint` instruction
502pub fn enable_harvest_to_mint(
503    token_program_id: &Pubkey,
504    mint: &Pubkey,
505    authority: &Pubkey,
506    multisig_signers: &[&Pubkey],
507) -> Result<Instruction, ProgramError> {
508    check_program_account(token_program_id)?;
509    let mut accounts = vec![
510        AccountMeta::new(*mint, false),
511        AccountMeta::new_readonly(*authority, multisig_signers.is_empty()),
512    ];
513
514    for multisig_signer in multisig_signers.iter() {
515        accounts.push(AccountMeta::new_readonly(**multisig_signer, true));
516    }
517
518    Ok(encode_instruction(
519        token_program_id,
520        accounts,
521        TokenInstruction::ConfidentialTransferFeeExtension,
522        ConfidentialTransferFeeInstruction::EnableHarvestToMint,
523        &(),
524    ))
525}
526
527/// Create a `DisableHarvestToMint` instruction
528pub fn disable_harvest_to_mint(
529    token_program_id: &Pubkey,
530    mint: &Pubkey,
531    authority: &Pubkey,
532    multisig_signers: &[&Pubkey],
533) -> Result<Instruction, ProgramError> {
534    check_program_account(token_program_id)?;
535    let mut accounts = vec![
536        AccountMeta::new(*mint, false),
537        AccountMeta::new_readonly(*authority, multisig_signers.is_empty()),
538    ];
539
540    for multisig_signer in multisig_signers.iter() {
541        accounts.push(AccountMeta::new_readonly(**multisig_signer, true));
542    }
543
544    Ok(encode_instruction(
545        token_program_id,
546        accounts,
547        TokenInstruction::ConfidentialTransferFeeExtension,
548        ConfidentialTransferFeeInstruction::DisableHarvestToMint,
549        &(),
550    ))
551}