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
6 changes: 5 additions & 1 deletion program-tests/compressed-token-test/tests/mint/random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,17 @@ async fn test_random_mint_action() {
println!("\n\ntest seed {}\n\n", seed);
let mut rng = StdRng::seed_from_u64(seed);

// Generate random custom metadata keys (max 20)
// Generate random custom metadata keys (max 20, deduplicated)
let num_keys = rng.gen_range(1..=20);
let mut available_keys = Vec::new();
let mut initial_metadata = Vec::new();
for i in 0..num_keys {
let key_len = rng.gen_range(1..=8); // Random key length 1-8 bytes
let key: Vec<u8> = (0..key_len).map(|_| rng.gen()).collect();
// Skip duplicate keys to avoid DuplicateMetadataKey error
if available_keys.contains(&key) {
continue;
}
let value_len = rng.gen_range(5..=32); // Random value length
let value = vec![(i + 2) as u8; value_len];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use pinocchio_token_program::processor::{
unpack_amount_and_decimals,
};

use super::shared::{process_transfer_extensions_transfer_checked, TransferAccounts};
use super::shared::{
process_transfer_extensions_transfer_checked, validate_self_transfer, TransferAccounts,
};
use crate::shared::{
convert_pinocchio_token_error, convert_token_error, owner_validation::check_token_program_owner,
};
Expand Down Expand Up @@ -48,6 +50,12 @@ pub fn process_ctoken_transfer_checked(
let source = &accounts[ACCOUNT_SOURCE];
let destination = &accounts[ACCOUNT_DESTINATION];

// Self-transfer: validate authority but skip token movement to avoid
// double mutable borrow panic in pinocchio process_transfer.
if validate_self_transfer(source, destination, &accounts[ACCOUNT_AUTHORITY])? {
return Ok(());
}

// Hot path: 165-byte accounts have no extensions, skip all extension processing
if source.data_len() == 165 && destination.data_len() == 165 {
// Slice to exactly 4 accounts: [source, mint, destination, authority]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use light_program_profiler::profile;
use pinocchio::account_info::AccountInfo;
use pinocchio_token_program::processor::transfer::process_transfer;

use super::shared::{process_transfer_extensions_transfer, TransferAccounts};
use super::shared::{
process_transfer_extensions_transfer, validate_self_transfer, TransferAccounts,
};
use crate::shared::convert_pinocchio_token_error;

/// Account indices for CToken transfer instruction
Expand Down Expand Up @@ -38,10 +40,17 @@ pub fn process_ctoken_transfer(
return Err(ProgramError::InvalidInstructionData);
}

// Hot path: 165-byte accounts have no extensions, skip all extension processing
// SAFETY: accounts.len() >= 3 validated at function entry
let source = &accounts[ACCOUNT_SOURCE];
let destination = &accounts[ACCOUNT_DESTINATION];

// Self-transfer: validate authority but skip token movement to avoid
// double mutable borrow panic in pinocchio process_transfer.
if validate_self_transfer(source, destination, &accounts[ACCOUNT_AUTHORITY])? {
return Ok(());
}

// Hot path: 165-byte accounts have no extensions, skip all extension processing
if source.data_len() == 165 && destination.data_len() == 165 {
// Slice to exactly 3 accounts: [source, destination, authority]
return process_transfer(&accounts[..3], &instruction_data[..8], false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anchor_compressed_token::ErrorCode;
use anchor_lang::solana_program::program_error::ProgramError;
use anchor_lang::solana_program::{msg, program_error::ProgramError};
use light_program_profiler::profile;
use light_token_interface::{
state::{Token, ZExtensionStructMut},
Expand All @@ -15,6 +15,45 @@ use crate::{
},
};

/// Validates self-transfer: if source == destination, checks authority is signer
/// and is owner or delegate of the token account.
/// Returns Ok(true) if self-transfer was validated (caller should return Ok(())),
/// Returns Ok(false) if not a self-transfer (caller should continue).
#[inline(always)]
pub fn validate_self_transfer(
source: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
) -> Result<bool, ProgramError> {
if !pubkey_eq(source.key(), destination.key()) {
return Ok(false);
}
validate_self_transfer_authority(source, authority)?;
Ok(true)
}

#[cold]
fn validate_self_transfer_authority(
source: &AccountInfo,
authority: &AccountInfo,
) -> Result<(), ProgramError> {
if !authority.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
let token =
Token::from_account_info_checked(source).map_err(|_| ProgramError::InvalidAccountData)?;
let is_owner = pubkey_eq(authority.key(), token.base.owner.array_ref());
let is_delegate = token
.base
.delegate()
.is_some_and(|d| pubkey_eq(authority.key(), d.array_ref()));
Comment on lines +46 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In SPL, it will check if the delegate amount is greater than the transferred amount, even when self-transferring. So there's a bit of inconsistency here

if !is_owner && !is_delegate {
msg!("Self-transfer authority must be owner or delegate");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}

/// Extension information detected from a single account deserialization.
/// Uses `MintExtensionFlags` for T22 extension flags to avoid duplication.
#[derive(Debug, Default)]
Expand Down
Loading