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}