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

Commit ccebd8d

Browse files
authored
token-2022: Add transfer fee withdrawal from mint (#2849)
1 parent 28f1ba9 commit ccebd8d

File tree

2 files changed

+238
-54
lines changed

2 files changed

+238
-54
lines changed

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,50 @@ fn process_set_transfer_fee(
108108
Ok(())
109109
}
110110

111+
fn process_withdraw_withheld_tokens_from_mint(
112+
program_id: &Pubkey,
113+
accounts: &[AccountInfo],
114+
) -> ProgramResult {
115+
let account_info_iter = &mut accounts.iter();
116+
let mint_account_info = next_account_info(account_info_iter)?;
117+
let dest_account_info = next_account_info(account_info_iter)?;
118+
let authority_info = next_account_info(account_info_iter)?;
119+
let authority_info_data_len = authority_info.data_len();
120+
121+
let mut mint_data = mint_account_info.data.borrow_mut();
122+
let mut mint = StateWithExtensionsMut::<Mint>::unpack(&mut mint_data)?;
123+
let extension = mint.get_extension_mut::<TransferFeeConfig>()?;
124+
125+
let withdraw_withheld_authority = Option::<Pubkey>::from(extension.withdraw_withheld_authority)
126+
.ok_or(TokenError::NoAuthorityExists)?;
127+
Processor::validate_owner(
128+
program_id,
129+
&withdraw_withheld_authority,
130+
authority_info,
131+
authority_info_data_len,
132+
account_info_iter.as_slice(),
133+
)?;
134+
135+
let mut dest_account_data = dest_account_info.data.borrow_mut();
136+
let mut dest_account = StateWithExtensionsMut::<Account>::unpack(&mut dest_account_data)?;
137+
if dest_account.base.mint != *mint_account_info.key {
138+
return Err(TokenError::MintMismatch.into());
139+
}
140+
if dest_account.base.is_frozen() {
141+
return Err(TokenError::AccountFrozen.into());
142+
}
143+
let withheld_amount = u64::from(extension.withheld_amount);
144+
extension.withheld_amount = 0.into();
145+
dest_account.base.amount = dest_account
146+
.base
147+
.amount
148+
.checked_add(withheld_amount)
149+
.ok_or(TokenError::Overflow)?;
150+
dest_account.pack_base();
151+
152+
Ok(())
153+
}
154+
111155
fn harvest_from_account<'a, 'b>(
112156
mint_key: &'b Pubkey,
113157
mint_extension: &'b mut TransferFeeConfig,
@@ -184,7 +228,8 @@ pub(crate) fn process_instruction(
184228
Processor::process_transfer(program_id, accounts, amount, Some(decimals), Some(fee))
185229
}
186230
TransferFeeInstruction::WithdrawWithheldTokensFromMint => {
187-
unimplemented!();
231+
msg!("TransferFeeInstruction: WithdrawWithheldTokensFromMint");
232+
process_withdraw_withheld_tokens_from_mint(program_id, accounts)
188233
}
189234
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts => {
190235
unimplemented!();

token/program-2022/tests/transfer_fee.rs

Lines changed: 192 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ fn test_transfer_fee_config_with_keypairs() -> TransferFeeConfigWithKeypairs {
7373
}
7474

7575
struct TokenWithAccounts {
76+
context: TestContext,
7677
token: Token<ProgramBanksClientProcessTransaction, Keypair>,
7778
transfer_fee_config: TransferFeeConfig,
79+
withdraw_withheld_authority: Keypair,
80+
freeze_authority: Keypair,
7881
alice: Keypair,
7982
alice_account: Pubkey,
8083
bob_account: Pubkey,
@@ -96,7 +99,7 @@ async fn create_mint_with_accounts(alice_amount: u64) -> TokenWithAccounts {
9699
);
97100
let maximum_fee = u64::from(transfer_fee_config.newer_transfer_fee.maximum_fee);
98101
context
99-
.init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig {
102+
.init_token_with_freezing_mint(vec![ExtensionInitializationParams::TransferFeeConfig {
100103
transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(),
101104
withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(),
102105
transfer_fee_basis_points,
@@ -107,11 +110,12 @@ async fn create_mint_with_accounts(alice_amount: u64) -> TokenWithAccounts {
107110
let TokenContext {
108111
decimals,
109112
mint_authority,
113+
freeze_authority,
110114
token,
111115
alice,
112116
bob,
113117
..
114-
} = context.token_context.unwrap();
118+
} = context.token_context.take().unwrap();
115119

116120
// token account is self-owned just to test another case
117121
let alice_account = token
@@ -130,8 +134,11 @@ async fn create_mint_with_accounts(alice_amount: u64) -> TokenWithAccounts {
130134
.await
131135
.unwrap();
132136
TokenWithAccounts {
137+
context,
133138
token,
134139
transfer_fee_config,
140+
withdraw_withheld_authority,
141+
freeze_authority: freeze_authority.unwrap(),
135142
alice,
136143
alice_account,
137144
bob_account,
@@ -407,6 +414,17 @@ async fn fail_unsupported_mint() {
407414
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
408415
)))
409416
);
417+
let error = token
418+
.withdraw_withheld_tokens_from_mint(&Pubkey::new_unique(), &mint_authority)
419+
.await
420+
.err()
421+
.unwrap();
422+
assert_eq!(
423+
error,
424+
TokenClientError::Client(Box::new(TransportError::TransactionError(
425+
TransactionError::InstructionError(0, InstructionError::InvalidAccountData)
426+
)))
427+
);
410428
}
411429

412430
#[tokio::test]
@@ -997,49 +1015,17 @@ async fn create_and_transfer_to_account(
9971015

9981016
#[tokio::test]
9991017
async fn harvest_withheld_tokens_to_mint() {
1000-
let TransferFeeConfigWithKeypairs {
1001-
transfer_fee_config_authority,
1002-
withdraw_withheld_authority,
1003-
transfer_fee_config,
1004-
..
1005-
} = test_transfer_fee_config_with_keypairs();
1006-
let mut context = TestContext::new().await;
1007-
let transfer_fee_basis_points = u16::from(
1008-
transfer_fee_config
1009-
.newer_transfer_fee
1010-
.transfer_fee_basis_points,
1011-
);
1012-
let maximum_fee = u64::from(transfer_fee_config.newer_transfer_fee.maximum_fee);
1013-
context
1014-
.init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig {
1015-
transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(),
1016-
withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(),
1017-
transfer_fee_basis_points,
1018-
maximum_fee,
1019-
}])
1020-
.await
1021-
.unwrap();
1022-
let TokenContext {
1023-
decimals,
1024-
mint_authority,
1018+
let amount = TEST_MAXIMUM_FEE;
1019+
let alice_amount = amount * 100;
1020+
let TokenWithAccounts {
1021+
mut context,
10251022
token,
1023+
transfer_fee_config,
10261024
alice,
1025+
alice_account,
1026+
decimals,
10271027
..
1028-
} = context.token_context.as_ref().unwrap();
1029-
1030-
let alice_account = Keypair::new();
1031-
let alice_account = token
1032-
.create_auxiliary_token_account(&alice_account, &alice.pubkey())
1033-
.await
1034-
.unwrap();
1035-
1036-
// mint a lot of tokens
1037-
let amount = maximum_fee;
1038-
let alice_amount = amount * 100;
1039-
token
1040-
.mint_to(&alice_account, mint_authority, alice_amount)
1041-
.await
1042-
.unwrap();
1028+
} = create_mint_with_accounts(alice_amount).await;
10431029

10441030
// harvest from zero accounts
10451031
token.harvest_withheld_tokens_to_mint(&[]).await.unwrap();
@@ -1050,12 +1036,12 @@ async fn harvest_withheld_tokens_to_mint() {
10501036
// harvest from one account
10511037
let accumulated_fees = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap();
10521038
let account = create_and_transfer_to_account(
1053-
token,
1039+
&token,
10541040
&alice_account,
1055-
alice,
1041+
&alice,
10561042
&alice.pubkey(),
10571043
amount,
1058-
*decimals,
1044+
decimals,
10591045
)
10601046
.await;
10611047
token
@@ -1084,24 +1070,24 @@ async fn harvest_withheld_tokens_to_mint() {
10841070
// no fail harvesting from account belonging to different mint, but nothing
10851071
// happens
10861072
let account = create_and_transfer_to_account(
1087-
token,
1073+
&token,
10881074
&alice_account,
1089-
alice,
1075+
&alice,
10901076
&alice.pubkey(),
10911077
amount,
1092-
*decimals,
1078+
decimals,
10931079
)
10941080
.await;
10951081
context
10961082
.init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig {
1097-
transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(),
1098-
withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(),
1099-
transfer_fee_basis_points,
1100-
maximum_fee,
1083+
transfer_fee_config_authority: Some(Pubkey::new_unique()),
1084+
withdraw_withheld_authority: Some(Pubkey::new_unique()),
1085+
transfer_fee_basis_points: TEST_FEE_BASIS_POINTS,
1086+
maximum_fee: TEST_MAXIMUM_FEE,
11011087
}])
11021088
.await
11031089
.unwrap();
1104-
let TokenContext { token, .. } = context.token_context.unwrap();
1090+
let TokenContext { token, .. } = context.token_context.take().unwrap();
11051091
token
11061092
.harvest_withheld_tokens_to_mint(&[&account])
11071093
.await
@@ -1156,3 +1142,156 @@ async fn max_harvest_withheld_tokens_to_mint() {
11561142
let extension = state.get_extension::<TransferFeeConfig>().unwrap();
11571143
assert_eq!(extension.withheld_amount, accumulated_fees.into());
11581144
}
1145+
1146+
#[tokio::test]
1147+
async fn withdraw_withheld_tokens_from_mint() {
1148+
let amount = TEST_MAXIMUM_FEE;
1149+
let alice_amount = amount * 100;
1150+
let TokenWithAccounts {
1151+
mut context,
1152+
token,
1153+
transfer_fee_config,
1154+
withdraw_withheld_authority,
1155+
freeze_authority,
1156+
alice,
1157+
alice_account,
1158+
decimals,
1159+
bob_account,
1160+
..
1161+
} = create_mint_with_accounts(alice_amount).await;
1162+
1163+
// no tokens withheld on mint
1164+
token
1165+
.withdraw_withheld_tokens_from_mint(&alice_account, &withdraw_withheld_authority)
1166+
.await
1167+
.unwrap();
1168+
let state = token.get_account_info(&alice_account).await.unwrap();
1169+
assert_eq!(state.base.amount, alice_amount);
1170+
let extension = state.get_extension::<TransferFeeAmount>().unwrap();
1171+
assert_eq!(extension.withheld_amount, 0.into());
1172+
let state = token.get_mint_info().await.unwrap();
1173+
let extension = state.get_extension::<TransferFeeConfig>().unwrap();
1174+
assert_eq!(extension.withheld_amount, 0.into());
1175+
1176+
// transfer + harvest to mint
1177+
let fee = transfer_fee_config.calculate_epoch_fee(0, amount).unwrap();
1178+
let account = create_and_transfer_to_account(
1179+
&token,
1180+
&alice_account,
1181+
&alice,
1182+
&alice.pubkey(),
1183+
amount,
1184+
decimals,
1185+
)
1186+
.await;
1187+
1188+
let state = token.get_account_info(&account).await.unwrap();
1189+
let extension = state.get_extension::<TransferFeeAmount>().unwrap();
1190+
assert_eq!(extension.withheld_amount, fee.into());
1191+
1192+
token
1193+
.harvest_withheld_tokens_to_mint(&[&account])
1194+
.await
1195+
.unwrap();
1196+
1197+
let state = token.get_mint_info().await.unwrap();
1198+
let extension = state.get_extension::<TransferFeeConfig>().unwrap();
1199+
assert_eq!(extension.withheld_amount, fee.into());
1200+
1201+
// success
1202+
token
1203+
.withdraw_withheld_tokens_from_mint(&bob_account, &withdraw_withheld_authority)
1204+
.await
1205+
.unwrap();
1206+
let state = token.get_account_info(&bob_account).await.unwrap();
1207+
assert_eq!(state.base.amount, fee);
1208+
let state = token.get_account_info(&account).await.unwrap();
1209+
let extension = state.get_extension::<TransferFeeAmount>().unwrap();
1210+
assert_eq!(extension.withheld_amount, 0.into());
1211+
let state = token.get_mint_info().await.unwrap();
1212+
let extension = state.get_extension::<TransferFeeConfig>().unwrap();
1213+
assert_eq!(extension.withheld_amount, 0.into());
1214+
1215+
// fail wrong signer
1216+
let error = token
1217+
.withdraw_withheld_tokens_from_mint(&alice_account, &alice)
1218+
.await
1219+
.unwrap_err();
1220+
assert_eq!(
1221+
error,
1222+
TokenClientError::Client(Box::new(TransportError::TransactionError(
1223+
TransactionError::InstructionError(
1224+
0,
1225+
InstructionError::Custom(TokenError::OwnerMismatch as u32)
1226+
)
1227+
)))
1228+
);
1229+
1230+
// fail frozen account
1231+
token
1232+
.freeze_account(&bob_account, &freeze_authority)
1233+
.await
1234+
.unwrap();
1235+
let error = token
1236+
.withdraw_withheld_tokens_from_mint(&bob_account, &withdraw_withheld_authority)
1237+
.await
1238+
.unwrap_err();
1239+
assert_eq!(
1240+
error,
1241+
TokenClientError::Client(Box::new(TransportError::TransactionError(
1242+
TransactionError::InstructionError(
1243+
0,
1244+
InstructionError::Custom(TokenError::AccountFrozen as u32)
1245+
)
1246+
)))
1247+
);
1248+
1249+
// set to none, fail
1250+
token
1251+
.set_authority(
1252+
token.get_address(),
1253+
None,
1254+
instruction::AuthorityType::WithheldWithdraw,
1255+
&withdraw_withheld_authority,
1256+
)
1257+
.await
1258+
.unwrap();
1259+
let error = token
1260+
.withdraw_withheld_tokens_from_mint(&alice_account, &withdraw_withheld_authority)
1261+
.await
1262+
.unwrap_err();
1263+
assert_eq!(
1264+
error,
1265+
TokenClientError::Client(Box::new(TransportError::TransactionError(
1266+
TransactionError::InstructionError(
1267+
0,
1268+
InstructionError::Custom(TokenError::NoAuthorityExists as u32)
1269+
)
1270+
)))
1271+
);
1272+
1273+
// fail on new mint with mint mismatch
1274+
context
1275+
.init_token_with_mint(vec![ExtensionInitializationParams::TransferFeeConfig {
1276+
transfer_fee_config_authority: Some(Pubkey::new_unique()),
1277+
withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()),
1278+
transfer_fee_basis_points: TEST_FEE_BASIS_POINTS,
1279+
maximum_fee: TEST_MAXIMUM_FEE,
1280+
}])
1281+
.await
1282+
.unwrap();
1283+
let TokenContext { token, .. } = context.token_context.take().unwrap();
1284+
let error = token
1285+
.withdraw_withheld_tokens_from_mint(&account, &withdraw_withheld_authority)
1286+
.await
1287+
.unwrap_err();
1288+
assert_eq!(
1289+
error,
1290+
TokenClientError::Client(Box::new(TransportError::TransactionError(
1291+
TransactionError::InstructionError(
1292+
0,
1293+
InstructionError::Custom(TokenError::MintMismatch as u32)
1294+
)
1295+
)))
1296+
);
1297+
}

0 commit comments

Comments
 (0)