spl_token_2022/extension/confidential_transfer_fee/
instruction.rs

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