Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions clients/typescript/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ async function getTransferDelegationOverlayInstructionAsync(
delegatorAta: input.delegatorAta,
receiverAta: input.receiverAta,
subscriptionAuthority,
tokenMint: input.tokenMint,
tokenProgram: input.tokenProgram,
transferData: {
amount: input.amount,
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions idl/subscriptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,15 @@
"kind": "instructionAccountNode",
"name": "receiverAta"
},
{
"docs": [
"The token mint"
],
"isSigner": false,
"isWritable": false,
"kind": "instructionAccountNode",
"name": "tokenMint"
},
{
"docs": [
"Token program"
Expand Down Expand Up @@ -1755,6 +1764,15 @@
"kind": "instructionAccountNode",
"name": "receiverAta"
},
{
"docs": [
"The token mint"
],
"isSigner": false,
"isWritable": false,
"kind": "instructionAccountNode",
"name": "tokenMint"
},
{
"docs": [
"Token program"
Expand Down Expand Up @@ -2129,6 +2147,15 @@
"kind": "instructionAccountNode",
"name": "caller"
},
{
"docs": [
"The token mint"
],
"isSigner": false,
"isWritable": false,
"kind": "instructionAccountNode",
"name": "tokenMint"
},
{
"docs": [
"Token program"
Expand Down
2 changes: 2 additions & 0 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
23 changes: 1 addition & 22 deletions program/src/instructions/helpers/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());
}
_ => {}
}

Expand Down
30 changes: 26 additions & 4 deletions program/src/instructions/helpers/transfer_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -45,12 +47,17 @@ pub fn get_token_account_owner(data: &[u8]) -> Result<Address, SubscriptionsErro
Ok(Address::from(owner))
}

fn get_mint_decimals(data: &[u8]) -> Result<u8, SubscriptionsError> {
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,
Expand All @@ -61,7 +68,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for DelegationTransferAccounts<'a> {
type Error = ProgramError;

fn try_from(accounts: &'a mut [AccountView]) -> Result<Self, Self::Error> {
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());
Expand All @@ -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)?;

Expand All @@ -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,
Expand All @@ -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).
Expand All @@ -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()?;
Expand Down Expand Up @@ -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),
Expand All @@ -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)?;
Expand Down
3 changes: 3 additions & 0 deletions program/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions program/src/instructions/transfer_fixed_delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
8 changes: 6 additions & 2 deletions program/src/instructions/transfer_subscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
Expand All @@ -162,7 +164,7 @@ impl<'a> TryFrom<&'a mut [AccountView]> for TransferSubscriptionAccounts<'a> {
type Error = ProgramError;

fn try_from(accounts: &'a mut [AccountView]) -> Result<Self, Self::Error> {
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());
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,35 +113,35 @@ 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],
Some(SubscriptionsError::MintHasTransferHook)
)]
#[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],
Expand Down
Loading
Loading