Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.
17 changes: 16 additions & 1 deletion interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use pinocchio::{program_error::ProgramError, pubkey::Pubkey};
use crate::error::TokenError;

/// Instructions supported by the token program.
#[repr(C)]
#[repr(C, u8)]
#[derive(Clone, Debug, PartialEq)]
pub enum TokenInstruction<'a> {
/// Initializes a new mint and optionally deposits all the newly minted
Expand Down Expand Up @@ -477,6 +477,21 @@ pub enum TokenInstruction<'a> {
/// The ui_amount of tokens to reformat.
ui_amount: &'a str,
},

/// Executes a batch of instructions. The instructions to be executed are specified
/// in sequence on the instruction data. Each instruction provides:
/// - `u8`: number of accounts
/// - `u8`: instruction data length (includes the discriminator)
/// - `u8`: instruction discriminator
/// - `[u8]`: instruction data
///
/// Accounts follow a similar pattern, where accounts for each instruction are
/// specified in sequence. Therefore, the number of accounts expected by this
/// instruction is variable – i.e., it depends on the instructions provided.
///
/// Both the number of accountsa and instruction data length are used to identify
/// the slice of accounts and instruction data for each instruction.
Batch = 255,
// Any new variants also need to be added to program-2022 `TokenInstruction`, so that the
// latter remains a superset of this instruction set. New variants also need to be added to
// token/js/src/instructions/types.ts to maintain @solana/spl-token compatibility
Expand Down
6 changes: 3 additions & 3 deletions interface/src/state/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct Account {
delegate: COption<Pubkey>,

/// The account's state.
pub state: AccountState,
pub state: u8,

/// Indicates whether this account represents a native token or not.
is_native: [u8; 4],
Expand Down Expand Up @@ -131,7 +131,7 @@ impl Account {

#[inline(always)]
pub fn is_frozen(&self) -> bool {
self.state == AccountState::Frozen
self.state == AccountState::Frozen as u8
}

#[inline(always)]
Expand All @@ -147,6 +147,6 @@ impl RawType for Account {
impl Initializable for Account {
#[inline(always)]
fn is_initialized(&self) -> bool {
self.state != AccountState::Uninitialized
self.state != AccountState::Uninitialized as u8
}
}
72 changes: 45 additions & 27 deletions program/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ no_allocator!();
// Use the default panic handler.
default_panic_handler!();

#[inline(always)]
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let [discriminator, instruction_data @ ..] = instruction_data else {
return Err(ProgramError::InvalidInstructionData);
};

if *discriminator == 255 {
// 255 - Batch
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: Batch");

return process_batch(accounts, instruction_data);
}

inner_process_instruction(accounts, instruction_data, *discriminator)
}

/// Process an instruction.
///
/// The processor of the token program is divided into two parts to reduce the overhead
Expand All @@ -21,31 +42,35 @@ default_panic_handler!();
///
/// Instructions on the first part of the processor:
///
/// - `0`: `InitializeMint`
/// - `3`: `Transfer`
/// - `7`: `MintTo`
/// - `9`: `CloseAccount`
/// - `0`: `InitializeMint`
/// - `1`: `InitializeAccount`
/// - `3`: `Transfer`
/// - `7`: `MintTo`
/// - `9`: `CloseAccount`
/// - `18`: `InitializeAccount2`
/// - `18`: `InitializeAccount3`
/// - `20`: `InitializeMint2`
#[inline(always)]
pub fn process_instruction(
_program_id: &Pubkey,
pub fn inner_process_instruction(
accounts: &[AccountInfo],
instruction_data: &[u8],
discriminator: u8,
) -> ProgramResult {
let (discriminator, instruction_data) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;

match *discriminator {
match discriminator {
// 0 - InitializeMint
0 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeMint");

process_initialize_mint(accounts, instruction_data, true)
}
// 1 - InitializeAccount
1 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount");

process_initialize_account(accounts)
}
// 3 - Transfer
3 => {
#[cfg(feature = "logging")]
Expand All @@ -67,6 +92,13 @@ pub fn process_instruction(

process_close_account(accounts)
}
// 16 - InitializeAccount2
16 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount2");

process_initialize_account2(accounts, instruction_data)
}
// 18 - InitializeAccount3
18 => {
#[cfg(feature = "logging")]
Expand All @@ -81,7 +113,7 @@ pub fn process_instruction(

process_initialize_mint2(accounts, instruction_data)
}
_ => process_remaining_instruction(accounts, instruction_data, *discriminator),
_ => inner_process_remaining_instruction(accounts, instruction_data, discriminator),
}
}

Expand All @@ -90,19 +122,12 @@ pub fn process_instruction(
/// This function is called by the `process_instruction` function if the discriminator
/// does not match any of the common instructions. This function is used to reduce the
/// overhead of having a large `match` statement in the `process_instruction` function.
fn process_remaining_instruction(
fn inner_process_remaining_instruction(
accounts: &[AccountInfo],
instruction_data: &[u8],
discriminator: u8,
) -> ProgramResult {
match discriminator {
// 1 - InitializeAccount
1 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount");

process_initialize_account(accounts)
}
// 2 - InitializeMultisig
2 => {
#[cfg(feature = "logging")]
Expand Down Expand Up @@ -180,13 +205,6 @@ fn process_remaining_instruction(

process_burn_checked(accounts, instruction_data)
}
// 16 - InitializeAccount2
16 => {
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: InitializeAccount2");

process_initialize_account2(accounts, instruction_data)
}
// 17 - SyncNative
17 => {
#[cfg(feature = "logging")]
Expand Down
95 changes: 95 additions & 0 deletions program/src/processor/batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use core::mem::MaybeUninit;

use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult};

use crate::entrypoint::inner_process_instruction;

macro_rules! write_account {
( $index_source:expr, $source:ident, $index_target:literal, $target:ident ) => {
// TODO: need to validate that the indices are within bounds.
unsafe {
$target
.get_unchecked_mut($index_target)
.write($source.get_unchecked($index_source).clone())
}
};
}

macro_rules! fill_accounts {
( $indices:ident, $accounts:ident, $instruction_accounts:ident ) => {
match $indices.len() {
1 => {
write_account!($indices[0] as usize, $accounts, 0, $instruction_accounts);
}
2 => {
write_account!($indices[0] as usize, $accounts, 0, $instruction_accounts);
write_account!($indices[1] as usize, $accounts, 1, $instruction_accounts);
}
3 => {
write_account!($indices[0] as usize, $accounts, 0, $instruction_accounts);
write_account!($indices[1] as usize, $accounts, 1, $instruction_accounts);
write_account!($indices[2] as usize, $accounts, 2, $instruction_accounts);
}
4 => {
write_account!($indices[0] as usize, $accounts, 0, $instruction_accounts);
write_account!($indices[1] as usize, $accounts, 1, $instruction_accounts);
write_account!($indices[2] as usize, $accounts, 2, $instruction_accounts);
write_account!($indices[3] as usize, $accounts, 3, $instruction_accounts);
}
// TODO: Add more cases up to 15.
_ => return Err(ProgramError::NotEnoughAccountKeys),
}
};
}

pub fn process_batch(accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult {
const UNINIT_ACCOUNT: MaybeUninit<AccountInfo> = MaybeUninit::<AccountInfo>::uninit();
// Instructions take at most 15 accounts.
let mut instruction_accounts: [MaybeUninit<AccountInfo>; 15] = [UNINIT_ACCOUNT; 15];

if instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}

loop {
let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize };
// There must be at least:
// - 1 byte for the number of accounts.
// - `expected_accounts` bytes for instruction accounts indices.
// - 1 byte for the length of the instruction data.
let data_offset = expected_accounts + 2;

if instruction_data.len() < data_offset {
return Err(ProgramError::InvalidInstructionData);
}

let indices = unsafe { instruction_data.get_unchecked(1..1 + expected_accounts) };
fill_accounts!(indices, accounts, instruction_accounts);

let expected_data =
data_offset + unsafe { *instruction_data.get_unchecked(data_offset - 1) as usize };

if instruction_data.len() < expected_data || expected_data == 0 {
return Err(ProgramError::InvalidInstructionData);
}

// SAFETY: The instruction data and accounts lengths are already validated so all
// the slices are guaranteed to be valid.
inner_process_instruction(
unsafe {
core::slice::from_raw_parts(instruction_accounts.as_ptr() as _, expected_accounts)
},
unsafe { instruction_data.get_unchecked(data_offset + 1..expected_data) },
unsafe { *instruction_data.get_unchecked(data_offset) },
)?;

if expected_data == instruction_data.len() {
// The batch is complete.
break;
}

instruction_data = &instruction_data[expected_data..];
}

Ok(())
}
2 changes: 1 addition & 1 deletion program/src/processor/get_account_data_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::check_account_owner;

#[inline(always)]
pub fn process_get_account_data_size(accounts: &[AccountInfo]) -> ProgramResult {
let [mint_info, _remaning @ ..] = accounts else {
let [mint_info, _remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Expand Down
2 changes: 2 additions & 0 deletions program/src/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use token_interface::{
pub mod amount_to_ui_amount;
pub mod approve;
pub mod approve_checked;
pub mod batch;
pub mod burn;
pub mod burn_checked;
pub mod close_account;
Expand Down Expand Up @@ -49,6 +50,7 @@ pub mod shared;
pub use amount_to_ui_amount::process_amount_to_ui_amount;
pub use approve::process_approve;
pub use approve_checked::process_approve_checked;
pub use batch::process_batch;
pub use burn::process_burn;
pub use burn_checked::process_burn_checked;
pub use close_account::process_close_account;
Expand Down
4 changes: 2 additions & 2 deletions program/src/processor/revoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::validate_owner;

#[inline(always)]
pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> ProgramResult {
let [source_account_info, owner_info, remaning @ ..] = accounts else {
let [source_account_info, owner_info, remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Expand All @@ -21,7 +21,7 @@ pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> Pro
return Err(TokenError::AccountFrozen.into());
}

validate_owner(&source_account.owner, owner_info, remaning)?;
validate_owner(&source_account.owner, owner_info, remaining)?;

source_account.clear_delegate();
source_account.set_delegated_amount(0);
Expand Down
10 changes: 5 additions & 5 deletions program/src/processor/set_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])

// Validates the accounts.

let [account_info, authority_info, remaning @ ..] = accounts else {
let [account_info, authority_info, remaining @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Expand All @@ -37,7 +37,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])

match authority_type {
AuthorityType::AccountOwner => {
validate_owner(&account.owner, authority_info, remaning)?;
validate_owner(&account.owner, authority_info, remaining)?;

if let Some(authority) = new_authority {
account.owner = *authority;
Expand All @@ -54,7 +54,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
}
AuthorityType::CloseAccount => {
let authority = account.close_authority().unwrap_or(&account.owner);
validate_owner(authority, authority_info, remaning)?;
validate_owner(authority, authority_info, remaining)?;

if let Some(authority) = new_authority {
account.set_close_authority(authority);
Expand All @@ -77,7 +77,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
// mint_authority.
let mint_authority = mint.mint_authority().ok_or(TokenError::FixedSupply)?;

validate_owner(mint_authority, authority_info, remaning)?;
validate_owner(mint_authority, authority_info, remaining)?;

if let Some(authority) = new_authority {
mint.set_mint_authority(authority);
Expand All @@ -92,7 +92,7 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
.freeze_authority()
.ok_or(TokenError::MintCannotFreeze)?;

validate_owner(freeze_authority, authority_info, remaning)?;
validate_owner(freeze_authority, authority_info, remaining)?;

if let Some(authority) = new_authority {
mint.set_freeze_authority(authority);
Expand Down
Loading