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

Commit ba46fed

Browse files
authored
token-2022: Add init transfer fee config (#2757)
1 parent b6bafc7 commit ba46fed

File tree

10 files changed

+281
-65
lines changed

10 files changed

+281
-65
lines changed

token/program-2022/src/extension/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ mod test {
906906
let new_extension = state.init_extension::<TransferFeeConfig>().unwrap();
907907
new_extension.transfer_fee_config_authority =
908908
mint_transfer_fee.transfer_fee_config_authority;
909-
new_extension.withheld_withdraw_authority = mint_transfer_fee.withheld_withdraw_authority;
909+
new_extension.withdraw_withheld_authority = mint_transfer_fee.withdraw_withheld_authority;
910910
new_extension.withheld_amount = mint_transfer_fee.withheld_amount;
911911
new_extension.older_transfer_fee = mint_transfer_fee.older_transfer_fee;
912912
new_extension.newer_transfer_fee = mint_transfer_fee.newer_transfer_fee;
@@ -958,7 +958,7 @@ mod test {
958958
let mint_transfer_fee = test_transfer_fee_config();
959959
let extension = state.init_extension::<TransferFeeConfig>().unwrap();
960960
extension.transfer_fee_config_authority = mint_transfer_fee.transfer_fee_config_authority;
961-
extension.withheld_withdraw_authority = mint_transfer_fee.withheld_withdraw_authority;
961+
extension.withdraw_withheld_authority = mint_transfer_fee.withdraw_withheld_authority;
962962
extension.withheld_amount = mint_transfer_fee.withheld_amount;
963963
extension.older_transfer_fee = mint_transfer_fee.older_transfer_fee;
964964
extension.newer_transfer_fee = mint_transfer_fee.newer_transfer_fee;
@@ -990,7 +990,7 @@ mod test {
990990
let mint_transfer_fee = test_transfer_fee_config();
991991
let extension = state.init_extension::<TransferFeeConfig>().unwrap();
992992
extension.transfer_fee_config_authority = mint_transfer_fee.transfer_fee_config_authority;
993-
extension.withheld_withdraw_authority = mint_transfer_fee.withheld_withdraw_authority;
993+
extension.withdraw_withheld_authority = mint_transfer_fee.withdraw_withheld_authority;
994994
extension.withheld_amount = mint_transfer_fee.withheld_amount;
995995
extension.older_transfer_fee = mint_transfer_fee.older_transfer_fee;
996996
extension.newer_transfer_fee = mint_transfer_fee.newer_transfer_fee;

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub enum TransferFeeInstruction {
2727
/// 0. `[writable]` The mint to initialize.
2828
InitializeTransferFeeConfig {
2929
/// Pubkey that may update the fees
30-
fee_config_authority: COption<Pubkey>,
30+
transfer_fee_config_authority: COption<Pubkey>,
3131
/// Withdraw instructions must be signed by this key
3232
withdraw_withheld_authority: COption<Pubkey>,
3333
/// Amount of transfer collected as fees, expressed as basis points of the
@@ -137,7 +137,8 @@ impl TransferFeeInstruction {
137137
let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?;
138138
Ok(match tag {
139139
0 => {
140-
let (fee_config_authority, rest) = TokenInstruction::unpack_pubkey_option(rest)?;
140+
let (transfer_fee_config_authority, rest) =
141+
TokenInstruction::unpack_pubkey_option(rest)?;
141142
let (withdraw_withheld_authority, rest) =
142143
TokenInstruction::unpack_pubkey_option(rest)?;
143144
let (transfer_fee_basis_points, rest) = rest.split_at(2);
@@ -153,7 +154,7 @@ impl TransferFeeInstruction {
153154
.map(u64::from_le_bytes)
154155
.ok_or(InvalidInstruction)?;
155156
let instruction = Self::InitializeTransferFeeConfig {
156-
fee_config_authority,
157+
transfer_fee_config_authority,
157158
withdraw_withheld_authority,
158159
transfer_fee_basis_points,
159160
maximum_fee,
@@ -211,13 +212,13 @@ impl TransferFeeInstruction {
211212
pub fn pack(&self, buffer: &mut Vec<u8>) {
212213
match *self {
213214
Self::InitializeTransferFeeConfig {
214-
ref fee_config_authority,
215+
ref transfer_fee_config_authority,
215216
ref withdraw_withheld_authority,
216217
transfer_fee_basis_points,
217218
maximum_fee,
218219
} => {
219220
buffer.push(0);
220-
TokenInstruction::pack_pubkey_option(fee_config_authority, buffer);
221+
TokenInstruction::pack_pubkey_option(transfer_fee_config_authority, buffer);
221222
TokenInstruction::pack_pubkey_option(withdraw_withheld_authority, buffer);
222223
buffer.extend_from_slice(&transfer_fee_basis_points.to_le_bytes());
223224
buffer.extend_from_slice(&maximum_fee.to_le_bytes());
@@ -256,14 +257,14 @@ impl TransferFeeInstruction {
256257
/// Create a `InitializeTransferFeeConfig` instruction
257258
pub fn initialize_transfer_fee_config(
258259
mint: Pubkey,
259-
fee_config_authority: COption<Pubkey>,
260+
transfer_fee_config_authority: COption<Pubkey>,
260261
withdraw_withheld_authority: COption<Pubkey>,
261262
transfer_fee_basis_points: u16,
262263
maximum_fee: u64,
263264
) -> Instruction {
264265
let data = TokenInstruction::TransferFeeExtension(
265266
TransferFeeInstruction::InitializeTransferFeeConfig {
266-
fee_config_authority,
267+
transfer_fee_config_authority,
267268
withdraw_withheld_authority,
268269
transfer_fee_basis_points,
269270
maximum_fee,
@@ -419,7 +420,7 @@ mod test {
419420
fn test_instruction_packing() {
420421
let check = TokenInstruction::TransferFeeExtension(
421422
TransferFeeInstruction::InitializeTransferFeeConfig {
422-
fee_config_authority: COption::Some(Pubkey::new(&[11u8; 32])),
423+
transfer_fee_config_authority: COption::Some(Pubkey::new(&[11u8; 32])),
423424
withdraw_withheld_authority: COption::None,
424425
transfer_fee_basis_points: 111,
425426
maximum_fee: u64::MAX,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub struct TransferFeeConfig {
3232
/// Optional authority to set the fee
3333
pub transfer_fee_config_authority: OptionalNonZeroPubkey,
3434
/// Withdraw from mint instructions must be signed by this key
35-
pub withheld_withdraw_authority: OptionalNonZeroPubkey,
35+
pub withdraw_withheld_authority: OptionalNonZeroPubkey,
3636
/// Withheld transfer fee tokens that have been moved to the mint for withdrawal
3737
pub withheld_amount: PodU64,
3838
/// Older transfer fee, used if the current epoch < new_transfer_fee.epoch
@@ -65,7 +65,7 @@ pub(crate) mod test {
6565
&[10; 32],
6666
)))
6767
.unwrap(),
68-
withheld_withdraw_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new(
68+
withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new(
6969
&[11; 32],
7070
)))
7171
.unwrap(),

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

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
11
use {
2-
crate::{check_program_account, extension::transfer_fee::instruction::TransferFeeInstruction},
2+
crate::{
3+
check_program_account,
4+
extension::{
5+
transfer_fee::{instruction::TransferFeeInstruction, TransferFee, TransferFeeConfig},
6+
StateWithExtensionsMut,
7+
},
8+
state::Mint,
9+
},
310
solana_program::{
4-
account_info::AccountInfo, entrypoint::ProgramResult, program_option::COption,
11+
account_info::{next_account_info, AccountInfo},
12+
clock::Clock,
13+
entrypoint::ProgramResult,
14+
program_option::COption,
515
pubkey::Pubkey,
16+
sysvar::Sysvar,
617
},
18+
std::convert::TryInto,
719
};
820

921
fn process_initialize_transfer_fee_config(
10-
_accounts: &[AccountInfo],
11-
_fee_config_authority: COption<Pubkey>,
12-
_withdraw_withheld_authority: COption<Pubkey>,
13-
_transfer_fee_basis_points: u16,
14-
_maximum_fee: u64,
22+
accounts: &[AccountInfo],
23+
transfer_fee_config_authority: COption<Pubkey>,
24+
withdraw_withheld_authority: COption<Pubkey>,
25+
transfer_fee_basis_points: u16,
26+
maximum_fee: u64,
1527
) -> ProgramResult {
16-
unimplemented!();
28+
let account_info_iter = &mut accounts.iter();
29+
let mint_account_info = next_account_info(account_info_iter)?;
30+
31+
let mut mint_data = mint_account_info.data.borrow_mut();
32+
let mut mint = StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut mint_data)?;
33+
let extension = mint.init_extension::<TransferFeeConfig>()?;
34+
extension.transfer_fee_config_authority = transfer_fee_config_authority.try_into()?;
35+
extension.withdraw_withheld_authority = withdraw_withheld_authority.try_into()?;
36+
extension.withheld_amount = 0u64.into();
37+
// To be safe, set newer and older transfer fees to the same thing on init,
38+
// but only newer will actually be used
39+
let epoch = Clock::get()?.epoch;
40+
let transfer_fee = TransferFee {
41+
epoch: epoch.into(),
42+
transfer_fee_basis_points: transfer_fee_basis_points.into(),
43+
maximum_fee: maximum_fee.into(),
44+
};
45+
extension.older_transfer_fee = transfer_fee;
46+
extension.newer_transfer_fee = transfer_fee;
47+
48+
Ok(())
1749
}
1850

1951
pub(crate) fn process_instruction(
@@ -25,13 +57,13 @@ pub(crate) fn process_instruction(
2557

2658
match instruction {
2759
TransferFeeInstruction::InitializeTransferFeeConfig {
28-
fee_config_authority,
60+
transfer_fee_config_authority,
2961
withdraw_withheld_authority,
3062
transfer_fee_basis_points,
3163
maximum_fee,
3264
} => process_initialize_transfer_fee_config(
3365
accounts,
34-
fee_config_authority,
66+
transfer_fee_config_authority,
3567
withdraw_withheld_authority,
3668
transfer_fee_basis_points,
3769
maximum_fee,

token/program-2022/src/pod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,23 @@ impl TryFrom<COption<Pubkey>> for OptionalNonZeroPubkey {
4141
}
4242
}
4343
impl From<OptionalNonZeroPubkey> for Option<Pubkey> {
44-
fn from(p: OptionalNonZeroPubkey) -> Option<Pubkey> {
44+
fn from(p: OptionalNonZeroPubkey) -> Self {
4545
if p.0 == Pubkey::default() {
4646
None
4747
} else {
4848
Some(p.0)
4949
}
5050
}
5151
}
52+
impl From<OptionalNonZeroPubkey> for COption<Pubkey> {
53+
fn from(p: OptionalNonZeroPubkey) -> Self {
54+
if p.0 == Pubkey::default() {
55+
COption::None
56+
} else {
57+
COption::Some(p.0)
58+
}
59+
}
60+
}
5261

5362
/// The standard `bool` is not a `Pod`, define a replacement that is
5463
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]

token/program-2022/tests/initialize_mint.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use {
1616
},
1717
spl_token_2022::{
1818
error::TokenError,
19-
extension::{mint_close_authority::MintCloseAuthority, ExtensionType},
19+
extension::{mint_close_authority::MintCloseAuthority, transfer_fee, ExtensionType},
2020
id, instruction,
2121
processor::Processor,
2222
state::Mint,
@@ -32,7 +32,7 @@ async fn success_base() {
3232
mint_authority,
3333
token,
3434
..
35-
} = TestContext::new(vec![]).await;
35+
} = TestContext::new(vec![]).await.unwrap();
3636

3737
let mint = token.get_mint_info().await.unwrap();
3838
assert_eq!(mint.base.decimals, decimals);
@@ -159,12 +159,11 @@ async fn success_extension_and_base() {
159159
mint_authority,
160160
token,
161161
..
162-
} = TestContext::new(vec![
163-
ExtensionInitializationParams::InitializeMintCloseAuthority {
164-
close_authority: close_authority.clone(),
165-
},
166-
])
167-
.await;
162+
} = TestContext::new(vec![ExtensionInitializationParams::MintCloseAuthority {
163+
close_authority: close_authority.clone(),
164+
}])
165+
.await
166+
.unwrap();
168167

169168
let state = token.get_mint_info().await.unwrap();
170169
assert_eq!(state.base.decimals, decimals);
@@ -415,3 +414,57 @@ async fn fail_account_init_after_mint_init_with_extension() {
415414
TransactionError::InstructionError(3, InstructionError::InvalidAccountData)
416415
);
417416
}
417+
418+
#[tokio::test]
419+
async fn fail_fee_init_after_mint_init() {
420+
let program_test = ProgramTest::new("spl_token_2022", id(), processor!(Processor::process));
421+
let mut ctx = program_test.start_with_context().await;
422+
let rent = ctx.banks_client.get_rent().await.unwrap();
423+
let mint_account = Keypair::new();
424+
let mint_authority_pubkey = Pubkey::new_unique();
425+
426+
let space = ExtensionType::get_account_len::<Mint>(&[ExtensionType::TransferFeeConfig]);
427+
let instructions = vec![
428+
system_instruction::create_account(
429+
&ctx.payer.pubkey(),
430+
&mint_account.pubkey(),
431+
rent.minimum_balance(space),
432+
space as u64,
433+
&spl_token_2022::id(),
434+
),
435+
instruction::initialize_mint(
436+
&spl_token_2022::id(),
437+
&mint_account.pubkey(),
438+
&mint_authority_pubkey,
439+
None,
440+
9,
441+
)
442+
.unwrap(),
443+
transfer_fee::instruction::initialize_transfer_fee_config(
444+
mint_account.pubkey(),
445+
COption::Some(Pubkey::new_unique()),
446+
COption::Some(Pubkey::new_unique()),
447+
10,
448+
100,
449+
),
450+
];
451+
452+
let tx = Transaction::new_signed_with_payer(
453+
&instructions,
454+
Some(&ctx.payer.pubkey()),
455+
&[&ctx.payer, &mint_account],
456+
ctx.last_blockhash,
457+
);
458+
#[allow(clippy::useless_conversion)]
459+
let err: TransactionError = ctx
460+
.banks_client
461+
.process_transaction(tx)
462+
.await
463+
.unwrap_err()
464+
.unwrap()
465+
.into();
466+
assert_eq!(
467+
err,
468+
TransactionError::InstructionError(1, InstructionError::InvalidAccountData)
469+
);
470+
}

token/program-2022/tests/mint_close_authority.rs

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ async fn success_init() {
2323
mint_authority,
2424
token,
2525
..
26-
} = TestContext::new(vec![
27-
ExtensionInitializationParams::InitializeMintCloseAuthority {
28-
close_authority: close_authority.clone(),
29-
},
30-
])
31-
.await;
26+
} = TestContext::new(vec![ExtensionInitializationParams::MintCloseAuthority {
27+
close_authority: close_authority.clone(),
28+
}])
29+
.await
30+
.unwrap();
3231

3332
let state = token.get_mint_info().await.unwrap();
3433
assert_eq!(state.base.decimals, decimals);
@@ -49,12 +48,12 @@ async fn success_init() {
4948
#[tokio::test]
5049
async fn set_authority() {
5150
let close_authority = Keypair::new();
52-
let TestContext { token, .. } = TestContext::new(vec![
53-
ExtensionInitializationParams::InitializeMintCloseAuthority {
51+
let TestContext { token, .. } =
52+
TestContext::new(vec![ExtensionInitializationParams::MintCloseAuthority {
5453
close_authority: COption::Some(close_authority.pubkey()),
55-
},
56-
])
57-
.await;
54+
}])
55+
.await
56+
.unwrap();
5857
let new_authority = Keypair::new();
5958

6059
// fail, wrong signature
@@ -149,12 +148,12 @@ async fn set_authority() {
149148
#[tokio::test]
150149
async fn success_close() {
151150
let close_authority = Keypair::new();
152-
let TestContext { token, .. } = TestContext::new(vec![
153-
ExtensionInitializationParams::InitializeMintCloseAuthority {
151+
let TestContext { token, .. } =
152+
TestContext::new(vec![ExtensionInitializationParams::MintCloseAuthority {
154153
close_authority: COption::Some(close_authority.pubkey()),
155-
},
156-
])
157-
.await;
154+
}])
155+
.await
156+
.unwrap();
158157

159158
let destination = Pubkey::new_unique();
160159
token
@@ -172,7 +171,7 @@ async fn fail_without_extension() {
172171
mint_authority,
173172
token,
174173
..
175-
} = TestContext::new(vec![]).await;
174+
} = TestContext::new(vec![]).await.unwrap();
176175

177176
// fail set
178177
let err = token
@@ -212,12 +211,11 @@ async fn fail_close_with_supply() {
212211
token,
213212
mint_authority,
214213
..
215-
} = TestContext::new(vec![
216-
ExtensionInitializationParams::InitializeMintCloseAuthority {
217-
close_authority: COption::Some(close_authority.pubkey()),
218-
},
219-
])
220-
.await;
214+
} = TestContext::new(vec![ExtensionInitializationParams::MintCloseAuthority {
215+
close_authority: COption::Some(close_authority.pubkey()),
216+
}])
217+
.await
218+
.unwrap();
221219

222220
// mint a token
223221
let owner = Pubkey::new_unique();

0 commit comments

Comments
 (0)