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}