diff --git a/Cargo.lock b/Cargo.lock
index 623cf6b..9f39d25 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5361,7 +5361,9 @@ dependencies = [
"pinocchio-token-2022",
"serde_json",
"solana-address 2.6.0",
+ "solana-program-pack",
"solana-security-txt",
+ "spl-token-interface",
"thiserror 2.0.18",
]
diff --git a/Cargo.toml b/Cargo.toml
index 2c3581e..6210a99 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,7 +34,9 @@ pinocchio-token = "0.6.0"
pinocchio-token-2022 = "0.3.1"
serde_json = "1"
solana-address = { version = "2", features = ["curve25519"] }
+solana-program-pack = "3"
solana-security-txt = "1.1.3"
+spl-token-interface = "2.0.0"
thiserror = { version = "2", default-features = false }
[profile.release]
diff --git a/clients/typescript/src/plugin.ts b/clients/typescript/src/plugin.ts
index db5a2d5..24b3e42 100644
--- a/clients/typescript/src/plugin.ts
+++ b/clients/typescript/src/plugin.ts
@@ -435,6 +435,7 @@ async function getTransferDelegationOverlayInstructionAsync(
delegatorAta: input.delegatorAta,
receiverAta: input.receiverAta,
subscriptionAuthority,
+ tokenMint: input.tokenMint,
tokenProgram: input.tokenProgram,
transferData: {
amount: input.amount,
@@ -475,6 +476,7 @@ export async function getTransferSubscriptionOverlayInstructionAsync(
receiverAta: input.receiverAta,
subscriptionAuthority,
subscriptionPda: input.subscriptionPda,
+ tokenMint: input.tokenMint,
tokenProgram: input.tokenProgram,
transferData: {
amount: input.amount,
diff --git a/idl/subscriptions.json b/idl/subscriptions.json
index 58cebcc..c287fdf 100644
--- a/idl/subscriptions.json
+++ b/idl/subscriptions.json
@@ -1638,6 +1638,15 @@
"kind": "instructionAccountNode",
"name": "receiverAta"
},
+ {
+ "docs": [
+ "The token mint"
+ ],
+ "isSigner": false,
+ "isWritable": false,
+ "kind": "instructionAccountNode",
+ "name": "tokenMint"
+ },
{
"docs": [
"Token program"
@@ -1755,6 +1764,15 @@
"kind": "instructionAccountNode",
"name": "receiverAta"
},
+ {
+ "docs": [
+ "The token mint"
+ ],
+ "isSigner": false,
+ "isWritable": false,
+ "kind": "instructionAccountNode",
+ "name": "tokenMint"
+ },
{
"docs": [
"Token program"
@@ -2129,6 +2147,15 @@
"kind": "instructionAccountNode",
"name": "caller"
},
+ {
+ "docs": [
+ "The token mint"
+ ],
+ "isSigner": false,
+ "isWritable": false,
+ "kind": "instructionAccountNode",
+ "name": "tokenMint"
+ },
{
"docs": [
"Token program"
diff --git a/program/Cargo.toml b/program/Cargo.toml
index d0aa66a..2db5ca5 100644
--- a/program/Cargo.toml
+++ b/program/Cargo.toml
@@ -28,5 +28,7 @@ pinocchio-system = { workspace = true }
pinocchio-token = { workspace = true }
pinocchio-token-2022 = { workspace = true }
solana-address = { workspace = true }
+solana-program-pack = { workspace = true }
solana-security-txt = { workspace = true }
+spl-token-interface = { workspace = true }
thiserror = { workspace = true }
diff --git a/program/src/instructions/helpers/token.rs b/program/src/instructions/helpers/token.rs
index fb92bea..fab4308 100644
--- a/program/src/instructions/helpers/token.rs
+++ b/program/src/instructions/helpers/token.rs
@@ -27,21 +27,15 @@ use crate::{
SubscriptionsError,
};
-const EXTENSION_TYPE_TRANSFER_FEE_CONFIG: u16 = 1;
-const EXTENSION_TYPE_MINT_CLOSE_AUTHORITY: u16 = 3;
const EXTENSION_TYPE_CONFIDENTIAL_TRANSFER_MINT: u16 = 4;
-const EXTENSION_TYPE_NON_TRANSFERABLE: u16 = 9;
-const EXTENSION_TYPE_PERMANENT_DELEGATE: u16 = 12;
const EXTENSION_TYPE_TRANSFER_HOOK: u16 = 14;
-const EXTENSION_TYPE_PAUSABLE: u16 = 26;
const TLV_EXTENSIONS_START: usize = 166;
/// Validates that a Token-2022 mint does not contain any blocked extensions.
///
/// Walks the TLV extension entries starting at byte 166 and rejects mints
-/// that have ConfidentialTransfer, NonTransferable, PermanentDelegate,
-/// TransferHook, TransferFee, MintCloseAuthority, or Pausable extensions.
+/// that have ConfidentialTransfer or TransferHook extensions.
fn validate_mint_extensions(data: &[u8]) -> Result<(), ProgramError> {
let mut offset = TLV_EXTENSIONS_START;
@@ -55,27 +49,12 @@ fn validate_mint_extensions(data: &[u8]) -> Result<(), ProgramError> {
}
match ext_type {
- EXTENSION_TYPE_TRANSFER_FEE_CONFIG => {
- return Err(SubscriptionsError::MintHasTransferFee.into());
- }
- EXTENSION_TYPE_MINT_CLOSE_AUTHORITY => {
- return Err(SubscriptionsError::MintHasMintCloseAuthority.into());
- }
EXTENSION_TYPE_CONFIDENTIAL_TRANSFER_MINT => {
return Err(SubscriptionsError::MintHasConfidentialTransfer.into());
}
- EXTENSION_TYPE_NON_TRANSFERABLE => {
- return Err(SubscriptionsError::MintHasNonTransferable.into());
- }
- EXTENSION_TYPE_PERMANENT_DELEGATE => {
- return Err(SubscriptionsError::MintHasPermanentDelegate.into());
- }
EXTENSION_TYPE_TRANSFER_HOOK => {
return Err(SubscriptionsError::MintHasTransferHook.into());
}
- EXTENSION_TYPE_PAUSABLE => {
- return Err(SubscriptionsError::MintHasPausable.into());
- }
_ => {}
}
diff --git a/program/src/instructions/helpers/transfer_utils.rs b/program/src/instructions/helpers/transfer_utils.rs
index b1f3e68..654d123 100644
--- a/program/src/instructions/helpers/transfer_utils.rs
+++ b/program/src/instructions/helpers/transfer_utils.rs
@@ -3,13 +3,15 @@ use pinocchio::{
error::ProgramError,
AccountView, Address, ProgramResult,
};
-use pinocchio_token_2022::instructions::Transfer;
+use pinocchio_token_2022::instructions::TransferChecked;
+use solana_program_pack::Pack;
+use spl_token_interface::state::Mint as TokenMint;
use crate::{
constants::{
TOKEN_ACCOUNT_MINT_END, TOKEN_ACCOUNT_MINT_OFFSET, TOKEN_ACCOUNT_OWNER_END, TOKEN_ACCOUNT_OWNER_OFFSET,
},
- AccountCheck, ProgramAccount, SignerAccount, SubscriptionAuthority, SubscriptionAuthorityAccount,
+ AccountCheck, MintInterface, ProgramAccount, SignerAccount, SubscriptionAuthority, SubscriptionAuthorityAccount,
SubscriptionsError, TokenAccountInterface, TokenProgramInterface, WritableAccount,
};
@@ -45,12 +47,17 @@ pub fn get_token_account_owner(data: &[u8]) -> Result
Result {
+ TokenMint::unpack_from_slice(data).map(|mint| mint.decimals).map_err(|_| SubscriptionsError::InvalidAccountData)
+}
+
/// Validated accounts shared by `TransferFixed` and `TransferRecurring` (identical layouts).
pub struct DelegationTransferAccounts<'a> {
pub delegation_pda: &'a mut AccountView,
pub subscription_authority: &'a AccountView,
pub delegator_ata: &'a AccountView,
pub receiver_ata: &'a AccountView,
+ pub token_mint: &'a AccountView,
pub token_program: &'a AccountView,
pub delegatee: &'a AccountView,
pub event_authority: &'a AccountView,
@@ -61,7 +68,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for DelegationTransferAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a mut [AccountView]) -> Result {
- let [delegation_pda, subscription_authority, delegator_ata, receiver_ata, token_program, delegatee, event_authority, self_program] =
+ let [delegation_pda, subscription_authority, delegator_ata, receiver_ata, token_mint, token_program, delegatee, event_authority, self_program] =
accounts
else {
return Err(SubscriptionsError::NotEnoughAccountKeys.into());
@@ -73,6 +80,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for DelegationTransferAccounts<'a> {
WritableAccount::check(receiver_ata)?;
SubscriptionAuthorityAccount::check(subscription_authority)?;
TokenProgramInterface::check(token_program)?;
+ MintInterface::check_with_program(token_mint, token_program)?;
TokenAccountInterface::check_accounts_with_program(token_program, &[delegator_ata, receiver_ata])?;
SignerAccount::check(delegatee)?;
@@ -81,6 +89,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for DelegationTransferAccounts<'a> {
subscription_authority,
delegator_ata,
receiver_ata,
+ token_mint,
token_program,
delegatee,
event_authority,
@@ -95,6 +104,8 @@ pub struct TransferAccounts<'a> {
pub delegator_ata: &'a AccountView,
/// The receiver's Associated Token Account (destination).
pub to_ata: &'a AccountView,
+ /// The token mint being transferred.
+ pub token_mint: &'a AccountView,
/// The [`SubscriptionAuthority`] PDA that is the SPL delegate on `delegator_ata`.
pub subscription_authority_pda: &'a AccountView,
/// The token program (SPL Token or Token-2022).
@@ -113,6 +124,10 @@ pub fn transfer_with_delegate(
init_id: i64,
accounts: &TransferAccounts,
) -> ProgramResult {
+ if accounts.token_mint.address() != mint {
+ return Err(SubscriptionsError::MintMismatch.into());
+ }
+
let bump = {
// Read the bump from the SubscriptionAuthority account data (cheaper than find_program_address)
let subscription_authority_data = accounts.subscription_authority_pda.try_borrow()?;
@@ -149,6 +164,11 @@ pub fn transfer_with_delegate(
check_token_account_mint(&to_data, mint)?;
}
+ let decimals = {
+ let mint_data = accounts.token_mint.try_borrow()?;
+ get_mint_decimals(&mint_data)?
+ };
+
let bump_bytes = [bump];
let seeds = [
Seed::from(SubscriptionAuthority::SEED),
@@ -158,11 +178,13 @@ pub fn transfer_with_delegate(
];
let signer = [Signer::from(&seeds)];
- Transfer {
+ TransferChecked {
from: accounts.delegator_ata,
+ mint: accounts.token_mint,
to: accounts.to_ata,
authority: accounts.subscription_authority_pda,
amount,
+ decimals,
token_program: accounts.token_program.address(),
}
.invoke_signed(&signer)?;
diff --git a/program/src/instructions/mod.rs b/program/src/instructions/mod.rs
index 26d6533..dab7e3a 100644
--- a/program/src/instructions/mod.rs
+++ b/program/src/instructions/mod.rs
@@ -100,6 +100,7 @@ pub enum SubscriptionsInstruction {
#[codama(account(name = "subscription_authority", docs = "The subscription-authority PDA"))]
#[codama(account(name = "delegator_ata", writable, docs = "The delegator's ATA to transfer from"))]
#[codama(account(name = "receiver_ata", writable, docs = "The receiver's ATA to transfer to"))]
+ #[codama(account(name = "token_mint", docs = "The token mint"))]
#[codama(account(name = "token_program", docs = "Token program"))]
#[codama(account(name = "delegatee", signer, docs = "The delegatee signing the transfer"))]
#[codama(account(
@@ -118,6 +119,7 @@ pub enum SubscriptionsInstruction {
#[codama(account(name = "subscription_authority", docs = "The subscription-authority PDA"))]
#[codama(account(name = "delegator_ata", writable, docs = "The delegator's ATA to transfer from"))]
#[codama(account(name = "receiver_ata", writable, docs = "The receiver's ATA to transfer to"))]
+ #[codama(account(name = "token_mint", docs = "The token mint"))]
#[codama(account(name = "token_program", docs = "Token program"))]
#[codama(account(name = "delegatee", signer, docs = "The delegatee signing the transfer"))]
#[codama(account(
@@ -170,6 +172,7 @@ pub enum SubscriptionsInstruction {
#[codama(account(name = "delegator_ata", writable, docs = "The delegator's ATA to transfer from"))]
#[codama(account(name = "receiver_ata", writable, docs = "The receiver's ATA to transfer to"))]
#[codama(account(name = "caller", signer, docs = "The authorized puller (plan owner or whitelisted)"))]
+ #[codama(account(name = "token_mint", docs = "The token mint"))]
#[codama(account(name = "token_program", docs = "Token program"))]
#[codama(account(
name = "event_authority",
diff --git a/program/src/instructions/transfer_fixed_delegation.rs b/program/src/instructions/transfer_fixed_delegation.rs
index e696a3a..b4ad67f 100644
--- a/program/src/instructions/transfer_fixed_delegation.rs
+++ b/program/src/instructions/transfer_fixed_delegation.rs
@@ -67,6 +67,7 @@ pub fn process(accounts: &mut [AccountView], transfer: &TransferData) -> Program
&TransferAccounts {
delegator_ata: accounts_struct.delegator_ata,
to_ata: accounts_struct.receiver_ata,
+ token_mint: accounts_struct.token_mint,
subscription_authority_pda: accounts_struct.subscription_authority,
token_program: accounts_struct.token_program,
},
diff --git a/program/src/instructions/transfer_recurring_delegation.rs b/program/src/instructions/transfer_recurring_delegation.rs
index 1d5d350..3cdc3d9 100644
--- a/program/src/instructions/transfer_recurring_delegation.rs
+++ b/program/src/instructions/transfer_recurring_delegation.rs
@@ -79,6 +79,7 @@ pub fn process(accounts: &mut [AccountView], transfer_data: &TransferData) -> Pr
&TransferAccounts {
delegator_ata: accounts_struct.delegator_ata,
to_ata: accounts_struct.receiver_ata,
+ token_mint: accounts_struct.token_mint,
subscription_authority_pda: accounts_struct.subscription_authority,
token_program: accounts_struct.token_program,
},
diff --git a/program/src/instructions/transfer_subscription.rs b/program/src/instructions/transfer_subscription.rs
index e1e39a6..ffdc5a3 100644
--- a/program/src/instructions/transfer_subscription.rs
+++ b/program/src/instructions/transfer_subscription.rs
@@ -10,7 +10,7 @@ use crate::{
events::SubscriptionTransferEvent,
helpers::{transfer_with_delegate, validate_recurring_transfer, TransferAccounts, TransferData},
state::{plan::Plan, subscription_delegation::SubscriptionDelegation},
- AccountCheck, ProgramAccount, SignerAccount, SubscriptionAuthorityAccount, SubscriptionsError,
+ AccountCheck, MintInterface, ProgramAccount, SignerAccount, SubscriptionAuthorityAccount, SubscriptionsError,
TokenAccountInterface, TokenProgramInterface, WritableAccount,
};
@@ -114,6 +114,7 @@ pub fn process(accounts: &mut [AccountView], transfer_data: &TransferData) -> Pr
&TransferAccounts {
delegator_ata: accounts_struct.delegator_ata,
to_ata: accounts_struct.receiver_ata,
+ token_mint: accounts_struct.token_mint,
subscription_authority_pda: accounts_struct.subscription_authority,
token_program: accounts_struct.token_program,
},
@@ -153,6 +154,7 @@ pub struct TransferSubscriptionAccounts<'a> {
pub delegator_ata: &'a AccountView,
pub receiver_ata: &'a AccountView,
pub caller: &'a AccountView,
+ pub token_mint: &'a AccountView,
pub token_program: &'a AccountView,
pub event_authority: &'a AccountView,
pub self_program: &'a AccountView,
@@ -162,7 +164,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for TransferSubscriptionAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a mut [AccountView]) -> Result {
- let [subscription_pda, plan_pda, subscription_authority, delegator_ata, receiver_ata, caller, token_program, event_authority, self_program] =
+ let [subscription_pda, plan_pda, subscription_authority, delegator_ata, receiver_ata, caller, token_mint, token_program, event_authority, self_program] =
accounts
else {
return Err(SubscriptionsError::NotEnoughAccountKeys.into());
@@ -179,6 +181,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for TransferSubscriptionAccounts<'a> {
WritableAccount::check(receiver_ata)?;
SignerAccount::check(caller)?;
TokenProgramInterface::check(token_program)?;
+ MintInterface::check_with_program(token_mint, token_program)?;
TokenAccountInterface::check_accounts_with_program(token_program, &[delegator_ata, receiver_ata])?;
Ok(Self {
@@ -188,6 +191,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for TransferSubscriptionAccounts<'a> {
delegator_ata,
receiver_ata,
caller,
+ token_mint,
token_program,
event_authority,
self_program,
diff --git a/tests/integration-tests/src/test_initialize_subscription_authority.rs b/tests/integration-tests/src/test_initialize_subscription_authority.rs
index d0acab6..b46d293 100644
--- a/tests/integration-tests/src/test_initialize_subscription_authority.rs
+++ b/tests/integration-tests/src/test_initialize_subscription_authority.rs
@@ -113,15 +113,15 @@ fn initialize_subscription_authority_with_sponsor() {
)]
#[case::non_transferable(
&[ExtensionType::NonTransferable],
- Some(SubscriptionsError::MintHasNonTransferable)
+ None
)]
#[case::permanent_delegate(
&[ExtensionType::PermanentDelegate],
- Some(SubscriptionsError::MintHasPermanentDelegate)
+ None
)]
#[case::transfer_fee(
&[ExtensionType::TransferFeeConfig],
- Some(SubscriptionsError::MintHasTransferFee)
+ None
)]
#[case::transfer_hook(
&[ExtensionType::TransferHook],
@@ -129,19 +129,19 @@ fn initialize_subscription_authority_with_sponsor() {
)]
#[case::pausable(
&[ExtensionType::Pausable],
- Some(SubscriptionsError::MintHasPausable)
+ None
)]
#[case::close_authority(
&[ExtensionType::MintCloseAuthority],
- Some(SubscriptionsError::MintHasMintCloseAuthority)
+ None
)]
#[case::multiple_blocked(
&[ExtensionType::TransferFeeConfig, ExtensionType::TransferHook],
- Some(SubscriptionsError::MintHasTransferFee)
+ Some(SubscriptionsError::MintHasTransferHook)
)]
#[case::mixed_blocked(
- &[ExtensionType::MintCloseAuthority, ExtensionType::PermanentDelegate],
- Some(SubscriptionsError::MintHasMintCloseAuthority)
+ &[ExtensionType::MintCloseAuthority, ExtensionType::ConfidentialTransferMint],
+ Some(SubscriptionsError::MintHasConfidentialTransfer)
)]
fn initialize_subscription_authority_token_2022(
#[case] extensions: &[ExtensionType],
diff --git a/tests/integration-tests/src/test_transfer_fixed_delegation.rs b/tests/integration-tests/src/test_transfer_fixed_delegation.rs
index 714ad00..3fac111 100644
--- a/tests/integration-tests/src/test_transfer_fixed_delegation.rs
+++ b/tests/integration-tests/src/test_transfer_fixed_delegation.rs
@@ -4,7 +4,7 @@ use crate::{
state::{header::VERSION_OFFSET, FixedDelegation},
tests::{
asserts::TransactionResultExt,
- constants::{MINT_DECIMALS, PROGRAM_ID, TOKEN_PROGRAM_ID},
+ constants::{MINT_DECIMALS, PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID},
idl,
pda::get_subscription_authority_pda,
utils::{
@@ -21,6 +21,7 @@ use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
+use spl_token_2022_interface::extension::ExtensionType;
use spl_token_interface::instruction::TokenInstruction::Approve;
fn setup_fixed_delegation(
@@ -72,6 +73,43 @@ fn test_fixed_transfer_success() {
assert_eq!(del_expiry_s, expiry_ts);
}
+#[test]
+fn test_fixed_transfer_token_2022_transfer_fee() {
+ let (mut litesvm, alice) = setup();
+ let bob = Keypair::new();
+ litesvm.airdrop(&bob.pubkey(), 10_000_000).unwrap();
+
+ let mint = init_mint(
+ &mut litesvm,
+ TOKEN_2022_PROGRAM_ID,
+ MINT_DECIMALS,
+ 1_000_000_000,
+ Some(alice.pubkey()),
+ &[ExtensionType::TransferFeeConfig],
+ );
+ let alice_ata = init_ata(&mut litesvm, mint, alice.pubkey(), 100_000_000);
+ let bob_ata = init_ata(&mut litesvm, mint, bob.pubkey(), 0);
+
+ initialize_subscription_authority_action(&mut litesvm, &alice, mint).0.assert_ok();
+
+ let (res, delegation_pda) = CreateDelegation::new(&mut litesvm, &alice, mint, bob.pubkey())
+ .fixed(50_000_000, current_ts() + days(1) as i64);
+ res.assert_ok();
+
+ TransferDelegation::new(&mut litesvm, &bob, alice.pubkey(), mint, delegation_pda)
+ .amount(10_000_000)
+ .fixed()
+ .assert_ok();
+
+ assert_eq!(get_ata_balance(&litesvm, &alice_ata), 90_000_000);
+ assert_eq!(get_ata_balance(&litesvm, &bob_ata), 9_900_000);
+
+ let delegation_account = litesvm.get_account(&delegation_pda).unwrap();
+ let delegation = FixedDelegation::load(&delegation_account.data).unwrap();
+ let remaining_amount = delegation.amount;
+ assert_eq!(remaining_amount, 40_000_000);
+}
+
#[test]
fn test_fixed_transfer_multiple_times() {
let amount: u64 = 50_000_000;
@@ -321,6 +359,7 @@ fn writable_accounts_must_be_writable() {
AccountMeta::new_readonly(subscription_authority_pda, false),
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
+ AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(bob.pubkey(), true),
AccountMeta::new_readonly(event_authority, false),
@@ -369,6 +408,7 @@ fn signer_accounts_must_be_signers() {
AccountMeta::new_readonly(subscription_authority_pda, false),
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
+ AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(bob.pubkey(), true),
AccountMeta::new_readonly(event_authority, false),
diff --git a/tests/integration-tests/src/test_transfer_recurring_delegation.rs b/tests/integration-tests/src/test_transfer_recurring_delegation.rs
index 127c473..f3d4cdc 100644
--- a/tests/integration-tests/src/test_transfer_recurring_delegation.rs
+++ b/tests/integration-tests/src/test_transfer_recurring_delegation.rs
@@ -410,6 +410,7 @@ fn writable_accounts_must_be_writable() {
AccountMeta::new_readonly(subscription_authority_pda, false),
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
+ AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(bob.pubkey(), true),
AccountMeta::new_readonly(event_authority, false),
@@ -461,6 +462,7 @@ fn signer_accounts_must_be_signers() {
AccountMeta::new_readonly(subscription_authority_pda, false),
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
+ AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(bob.pubkey(), true),
AccountMeta::new_readonly(event_authority, false),
diff --git a/tests/integration-tests/src/test_transfer_subscription.rs b/tests/integration-tests/src/test_transfer_subscription.rs
index eff7045..9ae1651 100644
--- a/tests/integration-tests/src/test_transfer_subscription.rs
+++ b/tests/integration-tests/src/test_transfer_subscription.rs
@@ -523,6 +523,7 @@ fn writable_accounts_must_be_writable() {
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
AccountMeta::new_readonly(merchant.pubkey(), true),
+ AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(event_authority, false),
AccountMeta::new_readonly(PROGRAM_ID, false),
@@ -575,6 +576,7 @@ fn signer_accounts_must_be_signers() {
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
AccountMeta::new_readonly(merchant.pubkey(), true),
+ AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(TOKEN_PROGRAM_ID, false),
AccountMeta::new_readonly(event_authority, false),
AccountMeta::new_readonly(PROGRAM_ID, false),
diff --git a/tests/integration-tests/src/utils/test_helpers.rs b/tests/integration-tests/src/utils/test_helpers.rs
index 2d7b80e..92b4319 100644
--- a/tests/integration-tests/src/utils/test_helpers.rs
+++ b/tests/integration-tests/src/utils/test_helpers.rs
@@ -15,9 +15,15 @@ use solana_transaction::Transaction;
use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
use spl_token_2022_interface::{
extension::{
- confidential_transfer::ConfidentialTransferMint, mint_close_authority::MintCloseAuthority,
- non_transferable::NonTransferable, pausable::PausableConfig, permanent_delegate::PermanentDelegate,
- transfer_fee::TransferFeeConfig, transfer_hook::TransferHook, BaseStateWithExtensionsMut, ExtensionType,
+ confidential_transfer::ConfidentialTransferMint,
+ immutable_owner::ImmutableOwner,
+ mint_close_authority::MintCloseAuthority,
+ non_transferable::{NonTransferable, NonTransferableAccount},
+ pausable::{PausableAccount, PausableConfig},
+ permanent_delegate::PermanentDelegate,
+ transfer_fee::{TransferFeeAmount, TransferFeeConfig},
+ transfer_hook::{TransferHook, TransferHookAccount},
+ BaseStateWithExtensions, BaseStateWithExtensionsMut, ExtensionType, StateWithExtensions,
StateWithExtensionsMut,
},
state::{Account as TokenAccount, AccountState, Mint as Mint2022},
@@ -90,15 +96,9 @@ pub fn setup() -> (LiteSVM, Keypair) {
(litesvm, default_payer)
}
-fn pack_data(state: T) -> Vec {
- let mut data = vec![0; T::LEN];
- T::pack(state, &mut data).unwrap();
- data
-}
-
pub fn fetch_account(litesvm: &LiteSVM, pubkey: &Pubkey) -> T {
let account = litesvm.get_account(pubkey).unwrap();
- T::unpack(account.data.as_ref()).unwrap()
+ T::unpack(&account.data[..T::LEN]).unwrap()
}
#[allow(clippy::result_large_err)]
@@ -174,7 +174,11 @@ pub fn init_mint(
state.init_extension::(true).unwrap();
}
ExtensionType::TransferFeeConfig => {
- state.init_extension::(true).unwrap();
+ let extension = state.init_extension::(true).unwrap();
+ extension.older_transfer_fee.epoch = 0.into();
+ extension.older_transfer_fee.maximum_fee = 1_000_000.into();
+ extension.older_transfer_fee.transfer_fee_basis_points = 100.into();
+ extension.newer_transfer_fee = extension.older_transfer_fee;
}
ExtensionType::TransferHook => {
state.init_extension::(true).unwrap();
@@ -220,6 +224,19 @@ fn init_token_account_at(
amount: u64,
) -> Pubkey {
let token_program = litesvm.get_account(&mint).unwrap().owner;
+ let account_extensions = if token_program == crate::tests::constants::TOKEN_2022_PROGRAM_ID {
+ let mint_account = litesvm.get_account(&mint).unwrap();
+ let mint_state = StateWithExtensions::::unpack(&mint_account.data).unwrap();
+ let mint_extensions = mint_state.get_extension_types().unwrap();
+ ExtensionType::get_required_init_account_extensions(&mint_extensions)
+ } else {
+ vec![]
+ };
+ let space = if account_extensions.is_empty() {
+ TokenAccount::LEN
+ } else {
+ ExtensionType::try_calculate_account_len::(&account_extensions).unwrap()
+ };
let ata_state = TokenAccount {
mint,
owner,
@@ -231,8 +248,37 @@ fn init_token_account_at(
close_authority: None.into(),
};
- let ata_data = pack_data(ata_state);
- let lamports = litesvm.minimum_balance_for_rent_exemption(TokenAccount::LEN);
+ let mut ata_data = vec![0u8; space];
+ if account_extensions.is_empty() {
+ TokenAccount::pack(ata_state, &mut ata_data).unwrap();
+ } else {
+ let mut state = StateWithExtensionsMut::::unpack_uninitialized(&mut ata_data).unwrap();
+ state.base = ata_state;
+ state.pack_base();
+ state.init_account_type().unwrap();
+
+ for ext in account_extensions {
+ match ext {
+ ExtensionType::TransferFeeAmount => {
+ state.init_extension::(true).unwrap();
+ }
+ ExtensionType::NonTransferableAccount => {
+ state.init_extension::(true).unwrap();
+ }
+ ExtensionType::ImmutableOwner => {
+ state.init_extension::(true).unwrap();
+ }
+ ExtensionType::TransferHookAccount => {
+ state.init_extension::(true).unwrap();
+ }
+ ExtensionType::PausableAccount => {
+ state.init_extension::(true).unwrap();
+ }
+ _ => panic!("Unsupported account extension type in test helper: {:?}", ext),
+ }
+ }
+ }
+ let lamports = litesvm.minimum_balance_for_rent_exemption(space);
litesvm
.set_account(
@@ -479,6 +525,7 @@ impl<'a> TransferDelegation<'a> {
AccountMeta::new(subscription_authority_pda, false),
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
+ AccountMeta::new_readonly(self.mint, false),
AccountMeta::new_readonly(token_program, false),
AccountMeta::new_readonly(self.signer.pubkey(), true),
AccountMeta::new_readonly(event_authority, false),
@@ -956,6 +1003,7 @@ impl<'a> TransferSubscription<'a> {
AccountMeta::new(delegator_ata, false),
AccountMeta::new(receiver_ata, false),
AccountMeta::new_readonly(self.caller.pubkey(), true),
+ AccountMeta::new_readonly(self.mint, false),
AccountMeta::new_readonly(token_program, false),
AccountMeta::new_readonly(event_authority, false),
AccountMeta::new_readonly(PROGRAM_ID, false),