|
13 | 13 | entrypoint::ProgramResult,
|
14 | 14 | instruction::{AccountMeta, Instruction, InstructionError},
|
15 | 15 | program_error::ProgramError,
|
| 16 | + program_option::COption, |
16 | 17 | pubkey::Pubkey,
|
17 | 18 | signature::Signer,
|
18 | 19 | signer::keypair::Keypair,
|
|
23 | 24 | spl_token_2022::{
|
24 | 25 | error::TokenError,
|
25 | 26 | extension::{
|
| 27 | + transfer_fee::{TransferFee, TransferFeeAmount, TransferFeeConfig}, |
26 | 28 | transfer_hook::{TransferHook, TransferHookAccount},
|
27 | 29 | BaseStateWithExtensions,
|
28 | 30 | },
|
|
36 | 38 | std::{convert::TryInto, sync::Arc},
|
37 | 39 | };
|
38 | 40 |
|
| 41 | +const TEST_MAXIMUM_FEE: u64 = 10_000_000; |
| 42 | +const TEST_FEE_BASIS_POINTS: u16 = 250; |
| 43 | + |
39 | 44 | /// Test program to fail transfer hook, conforms to transfer-hook-interface
|
40 | 45 | pub fn process_instruction_fail(
|
41 | 46 | _program_id: &Pubkey,
|
@@ -261,6 +266,63 @@ async fn setup(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestCo
|
261 | 266 | context
|
262 | 267 | }
|
263 | 268 |
|
| 269 | +async fn setup_with_fee(mint: Keypair, program_id: &Pubkey, authority: &Pubkey) -> TestContext { |
| 270 | + let mut program_test = setup_program_test(program_id); |
| 271 | + |
| 272 | + let transfer_fee_config_authority = Keypair::new(); |
| 273 | + let withdraw_withheld_authority = Keypair::new(); |
| 274 | + let transfer_fee_basis_points = TEST_FEE_BASIS_POINTS; |
| 275 | + let maximum_fee = TEST_MAXIMUM_FEE; |
| 276 | + add_validation_account(&mut program_test, &mint.pubkey(), program_id); |
| 277 | + |
| 278 | + let context = program_test.start_with_context().await; |
| 279 | + let context = Arc::new(tokio::sync::Mutex::new(context)); |
| 280 | + |
| 281 | + let mut context = TestContext { |
| 282 | + context, |
| 283 | + token_context: None, |
| 284 | + }; |
| 285 | + context |
| 286 | + .init_token_with_mint_keypair_and_freeze_authority( |
| 287 | + mint, |
| 288 | + vec![ |
| 289 | + ExtensionInitializationParams::TransferHook { |
| 290 | + authority: Some(*authority), |
| 291 | + program_id: Some(*program_id), |
| 292 | + }, |
| 293 | + ExtensionInitializationParams::TransferFeeConfig { |
| 294 | + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), |
| 295 | + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), |
| 296 | + transfer_fee_basis_points, |
| 297 | + maximum_fee, |
| 298 | + }, |
| 299 | + ], |
| 300 | + None, |
| 301 | + ) |
| 302 | + .await |
| 303 | + .unwrap(); |
| 304 | + context |
| 305 | +} |
| 306 | + |
| 307 | +fn test_transfer_fee() -> TransferFee { |
| 308 | + TransferFee { |
| 309 | + epoch: 0.into(), |
| 310 | + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), |
| 311 | + maximum_fee: TEST_MAXIMUM_FEE.into(), |
| 312 | + } |
| 313 | +} |
| 314 | + |
| 315 | +fn test_transfer_fee_config() -> TransferFeeConfig { |
| 316 | + let transfer_fee = test_transfer_fee(); |
| 317 | + TransferFeeConfig { |
| 318 | + transfer_fee_config_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), |
| 319 | + withdraw_withheld_authority: COption::Some(Pubkey::new_unique()).try_into().unwrap(), |
| 320 | + withheld_amount: 0.into(), |
| 321 | + older_transfer_fee: transfer_fee, |
| 322 | + newer_transfer_fee: transfer_fee, |
| 323 | + } |
| 324 | +} |
| 325 | + |
264 | 326 | async fn setup_with_confidential_transfers(
|
265 | 327 | mint: Keypair,
|
266 | 328 | program_id: &Pubkey,
|
@@ -549,6 +611,88 @@ async fn success_transfer() {
|
549 | 611 | );
|
550 | 612 | }
|
551 | 613 |
|
| 614 | +#[tokio::test] |
| 615 | +async fn success_transfer_with_fee() { |
| 616 | + let authority = Keypair::new(); |
| 617 | + let program_id = Pubkey::new_unique(); |
| 618 | + let mint_keypair = Keypair::new(); |
| 619 | + |
| 620 | + let maximum_fee = TEST_MAXIMUM_FEE; |
| 621 | + let alice_amount = maximum_fee * 100; |
| 622 | + let transfer_amount = maximum_fee; |
| 623 | + |
| 624 | + let token_context = setup_with_fee(mint_keypair, &program_id, &authority.pubkey()) |
| 625 | + .await |
| 626 | + .token_context |
| 627 | + .take() |
| 628 | + .unwrap(); |
| 629 | + |
| 630 | + let (alice_account, bob_account) = |
| 631 | + setup_accounts(&token_context, Keypair::new(), Keypair::new(), alice_amount).await; |
| 632 | + |
| 633 | + let transfer_fee_config = test_transfer_fee_config(); |
| 634 | + let fee = transfer_fee_config |
| 635 | + .calculate_epoch_fee(0, transfer_amount) |
| 636 | + .unwrap(); |
| 637 | + |
| 638 | + token_context |
| 639 | + .token |
| 640 | + .transfer_with_fee( |
| 641 | + &alice_account, |
| 642 | + &bob_account, |
| 643 | + &token_context.alice.pubkey(), |
| 644 | + transfer_amount, |
| 645 | + fee, |
| 646 | + &[&token_context.alice], |
| 647 | + ) |
| 648 | + .await |
| 649 | + .unwrap(); |
| 650 | + |
| 651 | + // Get the accounts' state after the transfer |
| 652 | + let alice_state = token_context |
| 653 | + .token |
| 654 | + .get_account_info(&alice_account) |
| 655 | + .await |
| 656 | + .unwrap(); |
| 657 | + let bob_state = token_context |
| 658 | + .token |
| 659 | + .get_account_info(&bob_account) |
| 660 | + .await |
| 661 | + .unwrap(); |
| 662 | + |
| 663 | + // Check that the correct amount was deducted from Alice's account |
| 664 | + assert_eq!(alice_state.base.amount, alice_amount - transfer_amount); |
| 665 | + |
| 666 | + // Check the there are no tokens withheld in Alice's account |
| 667 | + let extension = alice_state.get_extension::<TransferFeeAmount>().unwrap(); |
| 668 | + assert_eq!(extension.withheld_amount, 0.into()); |
| 669 | + |
| 670 | + // Check the fee tokens are withheld in Bobs's account |
| 671 | + let extension = bob_state.get_extension::<TransferFeeAmount>().unwrap(); |
| 672 | + assert_eq!(extension.withheld_amount, fee.into()); |
| 673 | + |
| 674 | + // Check that the correct amount was added to Bobs's account |
| 675 | + assert_eq!(bob_state.base.amount, transfer_amount - fee); |
| 676 | + |
| 677 | + // the example program checks that the transferring flag was set to true, |
| 678 | + // so make sure that it was correctly unset by the token program |
| 679 | + assert_eq!( |
| 680 | + bob_state |
| 681 | + .get_extension::<TransferHookAccount>() |
| 682 | + .unwrap() |
| 683 | + .transferring, |
| 684 | + false.into() |
| 685 | + ); |
| 686 | + |
| 687 | + assert_eq!( |
| 688 | + alice_state |
| 689 | + .get_extension::<TransferHookAccount>() |
| 690 | + .unwrap() |
| 691 | + .transferring, |
| 692 | + false.into() |
| 693 | + ); |
| 694 | +} |
| 695 | + |
552 | 696 | #[tokio::test]
|
553 | 697 | async fn fail_transfer_hook_program() {
|
554 | 698 | let authority = Pubkey::new_unique();
|
|
0 commit comments