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

Commit eb04855

Browse files
authored
token-2022: Add withdraw withheld tokens from accounts (#2857)
* token-2022: Add withdraw withheld tokens from accounts * Add instruction field for number of signers * Update instruction field * Rename field and update comment * Fix test?
1 parent 27b0df1 commit eb04855

File tree

4 files changed

+367
-24
lines changed

4 files changed

+367
-24
lines changed

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use {
66
program_option::COption,
77
pubkey::Pubkey,
88
},
9+
std::convert::TryFrom,
910
};
1011

1112
/// Transfer Fee extension instructions
@@ -95,7 +96,10 @@ pub enum TransferFeeInstruction {
9596
/// 2. `[]` The mint's `withdraw_withheld_authority`'s multisignature owner/delegate.
9697
/// 3. ..3+M `[signer]` M signer accounts.
9798
/// 3+M+1. ..3+M+N `[writable]` The source accounts to withdraw from.
98-
WithdrawWithheldTokensFromAccounts,
99+
WithdrawWithheldTokensFromAccounts {
100+
/// Number of token accounts harvested
101+
num_token_accounts: u8,
102+
},
99103
/// Permissionless instruction to transfer all withheld tokens to the mint.
100104
///
101105
/// Succeeds for frozen accounts.
@@ -161,7 +165,11 @@ impl TransferFeeInstruction {
161165
(instruction, rest)
162166
}
163167
2 => (Self::WithdrawWithheldTokensFromMint, rest),
164-
3 => (Self::WithdrawWithheldTokensFromAccounts, rest),
168+
3 => {
169+
let (&num_token_accounts, rest) = rest.split_first().ok_or(InvalidInstruction)?;
170+
let instruction = Self::WithdrawWithheldTokensFromAccounts { num_token_accounts };
171+
(instruction, rest)
172+
}
165173
4 => (Self::HarvestWithheldTokensToMint, rest),
166174
5 => {
167175
let (transfer_fee_basis_points, rest) = TokenInstruction::unpack_u16(rest)?;
@@ -204,8 +212,9 @@ impl TransferFeeInstruction {
204212
Self::WithdrawWithheldTokensFromMint => {
205213
buffer.push(2);
206214
}
207-
Self::WithdrawWithheldTokensFromAccounts => {
215+
Self::WithdrawWithheldTokensFromAccounts { num_token_accounts } => {
208216
buffer.push(3);
217+
buffer.push(num_token_accounts);
209218
}
210219
Self::HarvestWithheldTokensToMint => {
211220
buffer.push(4);
@@ -326,6 +335,8 @@ pub fn withdraw_withheld_tokens_from_accounts(
326335
sources: &[&Pubkey],
327336
) -> Result<Instruction, ProgramError> {
328337
check_program_account(token_program_id)?;
338+
let num_token_accounts =
339+
u8::try_from(sources.len()).map_err(|_| ProgramError::InvalidInstructionData)?;
329340
let mut accounts = Vec::with_capacity(3 + signers.len() + sources.len());
330341
accounts.push(AccountMeta::new_readonly(*mint, false));
331342
accounts.push(AccountMeta::new(*destination, false));
@@ -341,7 +352,7 @@ pub fn withdraw_withheld_tokens_from_accounts(
341352
program_id: *token_program_id,
342353
accounts,
343354
data: TokenInstruction::TransferFeeExtension(
344-
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts,
355+
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts },
345356
)
346357
.pack(),
347358
})
@@ -446,11 +457,12 @@ mod test {
446457
let unpacked = TokenInstruction::unpack(&expect).unwrap();
447458
assert_eq!(unpacked, check);
448459

460+
let num_token_accounts = 255;
449461
let check = TokenInstruction::TransferFeeExtension(
450-
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts,
462+
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts },
451463
);
452464
let packed = check.pack();
453-
let expect = [23u8, 3];
465+
let expect = [23u8, 3, num_token_accounts];
454466
assert_eq!(packed, expect);
455467
let unpacked = TokenInstruction::unpack(&expect).unwrap();
456468
assert_eq!(unpacked, check);

token/program-2022/src/extension/transfer_fee/processor.rs

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use {
77
instruction::TransferFeeInstruction, TransferFee, TransferFeeAmount,
88
TransferFeeConfig, MAX_FEE_BASIS_POINTS,
99
},
10-
StateWithExtensionsMut,
10+
StateWithExtensions, StateWithExtensionsMut,
1111
},
1212
processor::Processor,
1313
state::{Account, Mint},
@@ -154,9 +154,8 @@ fn process_withdraw_withheld_tokens_from_mint(
154154

155155
fn harvest_from_account<'a, 'b>(
156156
mint_key: &'b Pubkey,
157-
mint_extension: &'b mut TransferFeeConfig,
158157
token_account_info: &'b AccountInfo<'a>,
159-
) -> Result<(), TokenError> {
158+
) -> Result<u64, TokenError> {
160159
let mut token_account_data = token_account_info.data.borrow_mut();
161160
let mut token_account = StateWithExtensionsMut::<Account>::unpack(&mut token_account_data)
162161
.map_err(|_| TokenError::InvalidState)?;
@@ -168,13 +167,8 @@ fn harvest_from_account<'a, 'b>(
168167
.get_extension_mut::<TransferFeeAmount>()
169168
.map_err(|_| TokenError::InvalidState)?;
170169
let account_withheld_amount = u64::from(token_account_extension.withheld_amount);
171-
let mint_withheld_amount = u64::from(mint_extension.withheld_amount);
172-
mint_extension.withheld_amount = mint_withheld_amount
173-
.checked_add(account_withheld_amount)
174-
.ok_or(TokenError::Overflow)?
175-
.into();
176170
token_account_extension.withheld_amount = 0.into();
177-
Ok(())
171+
Ok(account_withheld_amount)
178172
}
179173

180174
fn process_harvest_withheld_tokens_to_mint(accounts: &[AccountInfo]) -> ProgramResult {
@@ -187,18 +181,92 @@ fn process_harvest_withheld_tokens_to_mint(accounts: &[AccountInfo]) -> ProgramR
187181
let mint_extension = mint.get_extension_mut::<TransferFeeConfig>()?;
188182

189183
for token_account_info in token_account_infos {
190-
match harvest_from_account(mint_account_info.key, mint_extension, token_account_info) {
191-
// Shouldn't ever happen, but if it does, we don't want to propagate any half-done changes
192-
Err(TokenError::Overflow) => return Err(TokenError::Overflow.into()),
184+
match harvest_from_account(mint_account_info.key, token_account_info) {
185+
Ok(amount) => {
186+
let mint_withheld_amount = u64::from(mint_extension.withheld_amount);
187+
mint_extension.withheld_amount = mint_withheld_amount
188+
.checked_add(amount)
189+
.ok_or(TokenError::Overflow)?
190+
.into();
191+
}
193192
Err(e) => {
194193
msg!("Error harvesting from {}: {}", token_account_info.key, e);
195194
}
196-
Ok(_) => {}
197195
}
198196
}
199197
Ok(())
200198
}
201199

200+
fn process_withdraw_withheld_tokens_from_accounts(
201+
program_id: &Pubkey,
202+
accounts: &[AccountInfo],
203+
num_token_accounts: u8,
204+
) -> ProgramResult {
205+
let account_info_iter = &mut accounts.iter();
206+
let mint_account_info = next_account_info(account_info_iter)?;
207+
let dest_account_info = next_account_info(account_info_iter)?;
208+
let authority_info = next_account_info(account_info_iter)?;
209+
let authority_info_data_len = authority_info.data_len();
210+
let account_infos = account_info_iter.as_slice();
211+
let num_signers = account_infos
212+
.len()
213+
.saturating_sub(num_token_accounts as usize);
214+
215+
let mint_data = mint_account_info.data.borrow();
216+
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
217+
let extension = mint.get_extension::<TransferFeeConfig>()?;
218+
219+
let withdraw_withheld_authority = Option::<Pubkey>::from(extension.withdraw_withheld_authority)
220+
.ok_or(TokenError::NoAuthorityExists)?;
221+
Processor::validate_owner(
222+
program_id,
223+
&withdraw_withheld_authority,
224+
authority_info,
225+
authority_info_data_len,
226+
&account_infos[..num_signers],
227+
)?;
228+
229+
let mut dest_account_data = dest_account_info.data.borrow_mut();
230+
let mut dest_account = StateWithExtensionsMut::<Account>::unpack(&mut dest_account_data)?;
231+
if dest_account.base.mint != *mint_account_info.key {
232+
return Err(TokenError::MintMismatch.into());
233+
}
234+
if dest_account.base.is_frozen() {
235+
return Err(TokenError::AccountFrozen.into());
236+
}
237+
for account_info in &account_infos[num_signers..] {
238+
// self-harvest, can't double-borrow the underlying data
239+
if account_info.key == dest_account_info.key {
240+
let token_account_extension = dest_account
241+
.get_extension_mut::<TransferFeeAmount>()
242+
.map_err(|_| TokenError::InvalidState)?;
243+
let account_withheld_amount = u64::from(token_account_extension.withheld_amount);
244+
token_account_extension.withheld_amount = 0.into();
245+
dest_account.base.amount = dest_account
246+
.base
247+
.amount
248+
.checked_add(account_withheld_amount)
249+
.ok_or(TokenError::Overflow)?;
250+
} else {
251+
match harvest_from_account(mint_account_info.key, account_info) {
252+
Ok(amount) => {
253+
dest_account.base.amount = dest_account
254+
.base
255+
.amount
256+
.checked_add(amount)
257+
.ok_or(TokenError::Overflow)?;
258+
}
259+
Err(e) => {
260+
msg!("Error harvesting from {}: {}", account_info.key, e);
261+
}
262+
}
263+
}
264+
}
265+
dest_account.pack_base();
266+
267+
Ok(())
268+
}
269+
202270
pub(crate) fn process_instruction(
203271
program_id: &Pubkey,
204272
accounts: &[AccountInfo],
@@ -231,8 +299,9 @@ pub(crate) fn process_instruction(
231299
msg!("TransferFeeInstruction: WithdrawWithheldTokensFromMint");
232300
process_withdraw_withheld_tokens_from_mint(program_id, accounts)
233301
}
234-
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts => {
235-
unimplemented!();
302+
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts { num_token_accounts } => {
303+
msg!("TransferFeeInstruction: WithdrawWithheldTokensFromAccounts");
304+
process_withdraw_withheld_tokens_from_accounts(program_id, accounts, num_token_accounts)
236305
}
237306
TransferFeeInstruction::HarvestWithheldTokensToMint => {
238307
msg!("TransferFeeInstruction: HarvestWithheldTokensToMint");

0 commit comments

Comments
 (0)