Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit af90fac

Browse files
samkim-cryptomvinesCriesofCarrots
authored
token-2022: add fee withdraw and harvest instructions (#2907)
* token-2022: add fee harvest instruction for confidential extension * token-2022: cargo fmt * token-2022: add `WithdrawWithheldTokensMint` processor * token-2022: add `WithdrawWithheldTokensAccounts` processor * token-2022: incorporate VerifyWithdrawWithheldTokens instruction * token-2022: minor variable name changes * token-2022: additional comments + renaming variables * token-2022: check if confidential extension was approved for fee harvest Co-authored-by: Michael Vines <[email protected]> * token-2022: fix error type for overflow * token-2022: remove unnecessary fee extension check for confidential token harvest * token-2022: add extra detail on front-running for harvesting fees * token-2022: fix pending balance * token-2022: bump zk-token-sdk to 0.8.0 * token-2022: fix minor variable name issue * token-2022: minor spelling Co-authored-by: Tyera Eulberg <[email protected]> * token-2022: addressing smaller review feedback * token-2022: change fee_mint to transfer_fee_config in confidential transfer * Update token/program-2022/Cargo.toml * token-2022: fix cargo bpf-test fail * token-2022: fixing a previous comment mistake Co-authored-by: Michael Vines <[email protected]> Co-authored-by: Tyera Eulberg <[email protected]>
1 parent 0268d76 commit af90fac

File tree

8 files changed

+690
-147
lines changed

8 files changed

+690
-147
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/program-2022-test/tests/confidential_transfer.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ use {
1010
},
1111
spl_token_2022::{
1212
extension::{
13-
confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint},
13+
confidential_transfer::{
14+
ConfidentialTransferAccount, ConfidentialTransferMint, EncryptedWithheldAmount,
15+
},
1416
ExtensionType,
1517
},
16-
solana_zk_token_sdk::encryption::elgamal::*,
18+
solana_zk_token_sdk::{encryption::elgamal::*, zk_token_elgamal::pod::Zeroable},
1719
},
1820
spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError},
1921
std::convert::TryInto,
@@ -36,8 +38,9 @@ impl ConfidentialTransferMintWithKeypairs {
3638
let ct_mint = ConfidentialTransferMint {
3739
authority: ct_mint_authority.pubkey().into(),
3840
auto_approve_new_accounts: true.into(),
39-
transfer_auditor: ct_mint_transfer_auditor.public.into(),
40-
withdraw_withheld_authority: ct_mint_withdraw_withheld_authority.public.into(),
41+
pubkey_auditor: ct_mint_transfer_auditor.public.into(),
42+
pubkey_withdraw_withheld_authority: ct_mint_withdraw_withheld_authority.public.into(),
43+
withheld_amount: EncryptedWithheldAmount::zeroed(),
4144
};
4245
Self {
4346
ct_mint,
@@ -162,7 +165,7 @@ async fn ct_configure_token_account() {
162165
assert!(!bool::from(&extension.approved));
163166
assert!(bool::from(&extension.allow_balance_credits));
164167
assert_eq!(
165-
extension.elgamal_pubkey,
168+
extension.pubkey_elgamal,
166169
alice_elgamal_keypair.public.into()
167170
);
168171
assert_eq!(

token/program-2022/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ num-derive = "0.3"
1919
num-traits = "0.2"
2020
num_enum = "0.5.4"
2121
solana-program = "1.9.9"
22-
solana-zk-token-sdk = "0.6.0"
22+
solana-zk-token-sdk = "0.8.1"
2323
spl-memo = { version = "3.0.1", path = "../../memo/program", features = [ "no-entrypoint" ] }
2424
spl-token = { version = "3.3", path = "../program", features = ["no-entrypoint"] }
2525
thiserror = "1.0"

token/program-2022/src/error.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ pub enum TokenError {
101101
/// ElGamal public key mismatch
102102
#[error("ElGamal public key mismatch")]
103103
ConfidentialTransferElGamalPubkeyMismatch,
104-
/// Available balance mismatch
105-
#[error("Available balance mismatch")]
106-
ConfidentialTransferAvailableBalanceMismatch,
104+
/// Balance mismatch
105+
#[error("Balance mismatch")]
106+
ConfidentialTransferBalanceMismatch,
107107
/// Mint has non-zero supply. Burn all tokens before closing the mint.
108108
#[error("Mint has non-zero supply. Burn all tokens before closing the mint")]
109109
MintHasSupply,

token/program-2022/src/extension/confidential_transfer/instruction.rs

Lines changed: 243 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use solana_zk_token_sdk::encryption::{auth_encryption::AeCiphertext, elgamal::El
33
pub use solana_zk_token_sdk::zk_token_proof_instruction::*;
44
use {
55
crate::{
6-
check_program_account, extension::confidential_transfer::ConfidentialTransferMint,
7-
instruction::TokenInstruction, pod::*,
6+
check_program_account, extension::confidential_transfer::*, instruction::TokenInstruction,
87
},
98
bytemuck::{Pod, Zeroable},
109
num_derive::{FromPrimitive, ToPrimitive},
@@ -16,6 +15,7 @@ use {
1615
sysvar,
1716
},
1817
solana_zk_token_sdk::zk_token_elgamal::pod,
18+
std::convert::TryFrom,
1919
};
2020

2121
/// Confidential Transfer extension instructions
@@ -228,16 +228,89 @@ pub enum ConfidentialTransferInstruction {
228228
/// None
229229
///
230230
DisableBalanceCredits,
231+
232+
/// Transfer all withheld confidential tokens in the mint to an account. Signed by the mint's
233+
/// withdraw withheld tokens authority.
234+
///
235+
/// Accounts expected by this instruction:
236+
///
237+
/// 0. `[writable]` The token mint. Must include the `TransferFeeConfig` extension.
238+
/// 1. `[writable]` The fee receiver account. Must include the `TransferFeeAmount` and
239+
/// `ConfidentialTransferAccount` extensions.
240+
/// 2. `[]` Instructions sysvar
241+
/// 3. `[signer]` The mint's `withdraw_withheld_authority`.
242+
/// or:
243+
/// 3. `[]` The mint's `withdraw_withheld_authority`'s multisignature owner/delegate.
244+
/// 4. ..3+M `[signer]` M signer accounts.
245+
///
246+
/// Data expected by this instruction:
247+
/// WithdrawWithheldTokensFromMintData
248+
///
249+
WithdrawWithheldTokensFromMint,
250+
251+
/// Transfer all withheld tokens to an account. Signed by the mint's withdraw withheld tokens
252+
/// authority. This instruction is susceptible to front-running. Use
253+
/// `HarvestWithheldTokensToMint` and `WithdrawWithheldTokensFromMint` as an alternative.
254+
///
255+
/// Note on front-running: This instruction requires a zero-knowledge proof verification
256+
/// instruction that is checked with respect to the account state (the currently withheld
257+
/// fees). Suppose that a withdraw withheld authority generates the
258+
/// `WithdrawWithheldTokensFromAccounts` instruction along with a corresponding zero-knowledge
259+
/// proof for a specified set of accounts, and submits it on chain. If the withheld fees at any
260+
/// of the specified accounts change before the `WithdrawWithheldTokensFromAccounts` is
261+
/// executed on chain, the zero-knowledge proof will not verify with respect to the new state,
262+
/// forcing the transaction to fail.
263+
///
264+
/// If front-running occurs, then users can look up the updated states of the accounts,
265+
/// generate a new zero-knowledge proof and try again. Alternatively, withdraw withheld
266+
/// authority can first move the withheld amount to the mint using
267+
/// `HarvestWithheldTokensToMint` and then move the withheld fees from mint to a specified
268+
/// destination account using `WithdrawWithheldTokensFromMint`.
269+
///
270+
/// Accounts expected by this instruction:
271+
///
272+
/// 0. `[]` The token mint. Must include the `TransferFeeConfig` extension.
273+
/// 1. `[writable]` The fee receiver account. Must include the `TransferFeeAmount` and
274+
/// `ConfidentialTransferAccount` extensions.
275+
/// 2. `[]` Instructions sysvar
276+
/// 3. `[signer]` The mint's `withdraw_withheld_authority`.
277+
/// 4. ..3+N `[writable]` The source accounts to withdraw from.
278+
/// or:
279+
/// 3. `[]` The mint's `withdraw_withheld_authority`'s multisignature owner/delegate.
280+
/// 4. ..4+M `[signer]` M signer accounts.
281+
/// 4+M+1. ..3+M+N `[writable]` The source accounts to withdraw from.
282+
///
283+
/// Data expected by this instruction:
284+
/// WithdrawWithheldTokensFromAccountsData
285+
///
286+
WithdrawWithheldTokensFromAccounts,
287+
288+
/// Permissionless instruction to transfer all withheld confidential tokens to the mint.
289+
///
290+
/// Succeeds for frozen accounts.
291+
///
292+
/// Accounts provided should include both the `TransferFeeAmount` and
293+
/// `ConfidentialTransferAccount` extension. If not, the account is skipped.
294+
///
295+
/// Accounts expected by this instruction:
296+
///
297+
/// 0. `[writable]` The mint.
298+
/// 1. ..1+N `[writable]` The source accounts to harvest from.
299+
///
300+
/// Data expected by this instruction:
301+
/// None
302+
///
303+
HarvestWithheldTokensToMint,
231304
}
232305

233306
/// Data expected by `ConfidentialTransferInstruction::ConfigureAccount`
234307
#[derive(Clone, Copy, Pod, Zeroable)]
235308
#[repr(C)]
236309
pub struct ConfigureAccountInstructionData {
237310
/// The public key associated with the account
238-
pub elgamal_pubkey: pod::ElGamalPubkey,
311+
pub elgamal_pubkey: EncryptionPubkey,
239312
/// The decryptable balance (always 0) once the configure account succeeds
240-
pub decryptable_zero_balance: pod::AeCiphertext,
313+
pub decryptable_zero_balance: DecryptableBalance,
241314
}
242315

243316
/// Data expected by `ConfidentialTransferInstruction::EmptyAccount`
@@ -268,7 +341,7 @@ pub struct WithdrawInstructionData {
268341
/// Expected number of base 10 digits to the right of the decimal place
269342
pub decimals: u8,
270343
/// The new decryptable balance if the withrawal succeeds
271-
pub new_decryptable_available_balance: pod::AeCiphertext,
344+
pub new_decryptable_available_balance: DecryptableBalance,
272345
/// Relative location of the `ProofInstruction::VerifyWithdraw` instruction to the `Withdraw`
273346
/// instruction in the transaction
274347
pub proof_instruction_offset: i8,
@@ -279,7 +352,7 @@ pub struct WithdrawInstructionData {
279352
#[repr(C)]
280353
pub struct TransferInstructionData {
281354
/// The new source decryptable balance if the transfer succeeds
282-
pub new_source_decryptable_available_balance: pod::AeCiphertext,
355+
pub new_source_decryptable_available_balance: DecryptableBalance,
283356
/// Relative location of the `ProofInstruction::VerifyTransfer` instruction to the
284357
/// `Transfer` instruction in the transaction
285358
pub proof_instruction_offset: i8,
@@ -296,6 +369,26 @@ pub struct ApplyPendingBalanceData {
296369
pub new_decryptable_available_balance: pod::AeCiphertext,
297370
}
298371

372+
/// Data expected by `ConfidentialTransferInstruction::WithdrawWithheldTokensFromMint`
373+
#[derive(Clone, Copy, Pod, Zeroable)]
374+
#[repr(C)]
375+
pub struct WithdrawWithheldTokensFromMintData {
376+
/// Relative location of the `ProofInstruction::VerifyWithdrawWithheld` instruction to the
377+
/// `WithdrawWithheldTokensFromMint` instruction in the transaction
378+
pub proof_instruction_offset: i8,
379+
}
380+
381+
/// Data expected by `ConfidentialTransferInstruction::WithdrawWithheldTokensFromAccounts`
382+
#[derive(Clone, Copy, Pod, Zeroable)]
383+
#[repr(C)]
384+
pub struct WithdrawWithheldTokensFromAccountsData {
385+
/// Number of token accounts harvested
386+
pub num_token_accounts: u8,
387+
/// Relative location of the `ProofInstruction::VerifyWithdrawWithheld` instruction to the
388+
/// `VerifyWithdrawWithheldTokensFromAccounts` instruction in the transaction
389+
pub proof_instruction_offset: i8,
390+
}
391+
299392
pub(crate) fn decode_instruction_type(
300393
input: &[u8],
301394
) -> Result<ConfidentialTransferInstruction, ProgramError> {
@@ -521,7 +614,7 @@ pub fn inner_withdraw(
521614
mint: &Pubkey,
522615
amount: u64,
523616
decimals: u8,
524-
new_decryptable_available_balance: pod::AeCiphertext,
617+
new_decryptable_available_balance: DecryptableBalance,
525618
authority: &Pubkey,
526619
multisig_signers: &[&Pubkey],
527620
proof_instruction_offset: i8,
@@ -593,7 +686,7 @@ pub fn inner_transfer(
593686
source_token_account: &Pubkey,
594687
destination_token_account: &Pubkey,
595688
mint: &Pubkey,
596-
new_source_decryptable_available_balance: pod::AeCiphertext,
689+
new_source_decryptable_available_balance: DecryptableBalance,
597690
authority: &Pubkey,
598691
multisig_signers: &[&Pubkey],
599692
proof_instruction_offset: i8,
@@ -657,7 +750,7 @@ pub fn inner_apply_pending_balance(
657750
token_program_id: &Pubkey,
658751
token_account: &Pubkey,
659752
expected_pending_balance_credit_counter: u64,
660-
new_decryptable_available_balance: pod::AeCiphertext,
753+
new_decryptable_available_balance: DecryptableBalance,
661754
authority: &Pubkey,
662755
multisig_signers: &[&Pubkey],
663756
) -> Result<Instruction, ProgramError> {
@@ -760,3 +853,144 @@ pub fn disable_balance_credits(
760853
multisig_signers,
761854
)
762855
}
856+
857+
/// Create a inner `WithdrawWithheldTokensFromMint` instruction
858+
///
859+
/// This instruction is suitable for use with a cross-program `invoke`
860+
pub fn inner_withdraw_withheld_tokens_from_mint(
861+
token_program_id: &Pubkey,
862+
mint: &Pubkey,
863+
destination: &Pubkey,
864+
authority: &Pubkey,
865+
multisig_signers: &[&Pubkey],
866+
proof_instruction_offset: i8,
867+
) -> Result<Instruction, ProgramError> {
868+
check_program_account(token_program_id)?;
869+
let mut accounts = vec![
870+
AccountMeta::new(*mint, false),
871+
AccountMeta::new(*destination, false),
872+
AccountMeta::new_readonly(sysvar::instructions::id(), false),
873+
AccountMeta::new_readonly(*authority, multisig_signers.is_empty()),
874+
];
875+
876+
for multisig_signer in multisig_signers.iter() {
877+
accounts.push(AccountMeta::new(**multisig_signer, false));
878+
}
879+
880+
Ok(encode_instruction(
881+
token_program_id,
882+
accounts,
883+
ConfidentialTransferInstruction::WithdrawWithheldTokensFromMint,
884+
&WithdrawWithheldTokensFromMintData {
885+
proof_instruction_offset,
886+
},
887+
))
888+
}
889+
890+
/// Create a `WithdrawWithheldTokensFromMint` instruction
891+
pub fn withdraw_withheld_tokens_from_mint(
892+
token_program_id: &Pubkey,
893+
mint: &Pubkey,
894+
destination: &Pubkey,
895+
authority: &Pubkey,
896+
multisig_signers: &[&Pubkey],
897+
proof_data: &WithdrawWithheldTokensData,
898+
) -> Result<Vec<Instruction>, ProgramError> {
899+
Ok(vec![
900+
verify_withdraw_withheld_tokens(proof_data),
901+
inner_withdraw_withheld_tokens_from_mint(
902+
token_program_id,
903+
mint,
904+
destination,
905+
authority,
906+
multisig_signers,
907+
-1,
908+
)?,
909+
])
910+
}
911+
912+
/// Create a inner `WithdrawWithheldTokensFromMint` instruction
913+
///
914+
/// This instruction is suitable for use with a cross-program `invoke`
915+
pub fn inner_withdraw_withheld_tokens_from_accounts(
916+
token_program_id: &Pubkey,
917+
mint: &Pubkey,
918+
destination: &Pubkey,
919+
authority: &Pubkey,
920+
multisig_signers: &[&Pubkey],
921+
sources: &[&Pubkey],
922+
proof_instruction_offset: i8,
923+
) -> Result<Instruction, ProgramError> {
924+
check_program_account(token_program_id)?;
925+
let num_token_accounts =
926+
u8::try_from(sources.len()).map_err(|_| ProgramError::InvalidInstructionData)?;
927+
let mut accounts = vec![
928+
AccountMeta::new(*mint, false),
929+
AccountMeta::new(*destination, false),
930+
AccountMeta::new_readonly(sysvar::instructions::id(), false),
931+
AccountMeta::new_readonly(*authority, multisig_signers.is_empty()),
932+
];
933+
934+
for multisig_signer in multisig_signers.iter() {
935+
accounts.push(AccountMeta::new(**multisig_signer, false));
936+
}
937+
938+
for source in sources.iter() {
939+
accounts.push(AccountMeta::new(**source, false));
940+
}
941+
942+
Ok(encode_instruction(
943+
token_program_id,
944+
accounts,
945+
ConfidentialTransferInstruction::WithdrawWithheldTokensFromAccounts,
946+
&WithdrawWithheldTokensFromAccountsData {
947+
proof_instruction_offset,
948+
num_token_accounts,
949+
},
950+
))
951+
}
952+
953+
/// Create a `WithdrawWithheldTokensFromAccounts` instruction
954+
pub fn withdraw_withheld_tokens_from_accounts(
955+
token_program_id: &Pubkey,
956+
mint: &Pubkey,
957+
destination: &Pubkey,
958+
authority: &Pubkey,
959+
multisig_signers: &[&Pubkey],
960+
sources: &[&Pubkey],
961+
proof_data: &WithdrawWithheldTokensData,
962+
) -> Result<Vec<Instruction>, ProgramError> {
963+
Ok(vec![
964+
verify_withdraw_withheld_tokens(proof_data),
965+
inner_withdraw_withheld_tokens_from_accounts(
966+
token_program_id,
967+
mint,
968+
destination,
969+
authority,
970+
multisig_signers,
971+
sources,
972+
-1,
973+
)?,
974+
])
975+
}
976+
977+
/// Creates a `HarvestWithheldTokensToMint` instruction
978+
pub fn harvest_withheld_tokens_to_mint(
979+
token_program_id: &Pubkey,
980+
mint: &Pubkey,
981+
sources: &[&Pubkey],
982+
) -> Result<Instruction, ProgramError> {
983+
check_program_account(token_program_id)?;
984+
let mut accounts = vec![AccountMeta::new(*mint, false)];
985+
986+
for source in sources.iter() {
987+
accounts.push(AccountMeta::new(**source, false));
988+
}
989+
990+
Ok(encode_instruction(
991+
token_program_id,
992+
accounts,
993+
ConfidentialTransferInstruction::HarvestWithheldTokensToMint,
994+
&(),
995+
))
996+
}

0 commit comments

Comments
 (0)