ソースを参照

More instructions

febo 10 ヶ月 前
コミット
ed0a15760b

+ 8 - 8
interface/src/instruction.rs

@@ -1,8 +1,8 @@
 //! Instruction types
 
-use pinocchio::{program_error::ProgramError, pubkey::Pubkey};
+use pinocchio::pubkey::Pubkey;
 
-use crate::{error::TokenError, state::PodCOption};
+use crate::state::PodCOption;
 
 /// Instructions supported by the token program.
 #[repr(C)]
@@ -506,13 +506,13 @@ impl AuthorityType {
         }
     }
 
-    pub fn from(index: u8) -> Result<Self, ProgramError> {
+    pub fn from(index: u8) -> Self {
         match index {
-            0 => Ok(AuthorityType::MintTokens),
-            1 => Ok(AuthorityType::FreezeAccount),
-            2 => Ok(AuthorityType::AccountOwner),
-            3 => Ok(AuthorityType::CloseAccount),
-            _ => Err(TokenError::InvalidInstruction.into()),
+            0 => AuthorityType::MintTokens,
+            1 => AuthorityType::FreezeAccount,
+            2 => AuthorityType::AccountOwner,
+            3 => AuthorityType::CloseAccount,
+            _ => panic!("invalid authority type: {index}"),
         }
     }
 }

+ 4 - 0
interface/src/lib.rs

@@ -2,3 +2,7 @@ pub mod error;
 pub mod instruction;
 pub mod native_mint;
 pub mod state;
+
+pub mod program {
+    pinocchio_pubkey::declare_id!("11111111111111111111111111111111");
+}

+ 3 - 0
interface/src/state/account.rs

@@ -37,6 +37,9 @@ pub struct Account {
 }
 
 impl Account {
+    /// Size of the `Account` account.
+    pub const LEN: usize = core::mem::size_of::<Self>();
+
     #[inline]
     pub fn is_initialized(&self) -> bool {
         self.state != AccountState::Uninitialized as u8

+ 5 - 0
interface/src/state/mint.rs

@@ -25,3 +25,8 @@ pub struct Mint {
     /// Optional authority to freeze token accounts.
     pub freeze_authority: PodCOption<Pubkey>,
 }
+
+impl Mint {
+    /// Size of the `Mint` account.
+    pub const LEN: usize = core::mem::size_of::<Self>();
+}

+ 8 - 1
interface/src/state/mod.rs

@@ -4,7 +4,7 @@ use bytemuck::{Pod, Zeroable};
 
 pub mod account;
 pub mod mint;
-pub mod multisignature;
+pub mod multisig;
 
 #[repr(C)]
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -40,6 +40,13 @@ impl<T: Default + PartialEq + Pod + Sized> PodCOption<T> {
 
     pub const SOME: [u8; 4] = [1, 0, 0, 0];
 
+    pub fn some(value: T) -> Self {
+        Self {
+            tag: [1, 0, 0, 0],
+            value,
+        }
+    }
+
     /// Returns `true` if the option is a `None` value.
     #[inline]
     pub fn is_none(&self) -> bool {

+ 5 - 0
interface/src/state/multisignature.rs → interface/src/state/multisig.rs

@@ -24,4 +24,9 @@ pub struct Multisig {
 
 impl Multisig {
     pub const LEN: usize = core::mem::size_of::<Multisig>();
+
+    /// Utility function that checks index is between [`MIN_SIGNERS`] and [`MAX_SIGNERS`].
+    pub fn is_valid_signer_index(index: usize) -> bool {
+        (MIN_SIGNERS..=MAX_SIGNERS).contains(&index)
+    }
 }

+ 82 - 0
p-token/src/entrypoint.rs

@@ -4,9 +4,17 @@ use pinocchio::{
 };
 
 use crate::processor::{
+    approve::process_approve,
+    burn::process_burn,
+    close_account::process_close_account,
+    freeze_account::process_freeze_account,
     initialize_account::process_initialize_account,
     initialize_mint::{process_initialize_mint, InitializeMint},
+    initialize_multisig::process_initialize_multisig,
     mint_to::process_mint_to,
+    revoke::process_revoke,
+    set_authority::{process_set_authority, SetAuthority},
+    thaw_account::process_thaw_account,
     transfer::process_transfer,
 };
 
@@ -34,6 +42,15 @@ pub fn process_instruction(
 
             process_initialize_account(program_id, accounts, None, true)
         }
+        // 2 - InitializeMultisig
+        Some((&2, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: InitializeMultisig");
+
+            let m = data.first().ok_or(ProgramError::InvalidInstructionData)?;
+
+            process_initialize_multisig(accounts, *m, true)
+        }
         // 3 - Transfer
         Some((&3, data)) => {
             #[cfg(feature = "logging")]
@@ -46,6 +63,38 @@ pub fn process_instruction(
 
             process_transfer(program_id, accounts, amount, None)
         }
+        // 4 - Approve
+        Some((&4, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: Approve");
+
+            let amount = u64::from_le_bytes(
+                data.try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            );
+
+            process_approve(program_id, accounts, amount, None)
+        }
+        // 5 - Revoke
+        Some((&5, _)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: Revoke");
+
+            process_revoke(program_id, accounts)
+        }
+        // 6 - SetAuthority
+        Some((&6, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: SetAuthority");
+
+            let instruction = SetAuthority::try_from_bytes(data)?;
+            process_set_authority(
+                program_id,
+                accounts,
+                instruction.authority_type,
+                instruction.new_authority,
+            )
+        }
         // 7 - InitializeMint
         Some((&7, data)) => {
             #[cfg(feature = "logging")]
@@ -58,6 +107,39 @@ pub fn process_instruction(
 
             process_mint_to(program_id, accounts, amount, None)
         }
+        // 8 - Burn
+        Some((&8, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: Burn");
+
+            let amount = u64::from_le_bytes(
+                data.try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            );
+
+            process_burn(program_id, accounts, amount, None)
+        }
+        // 9 - CloseAccount
+        Some((&9, _)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: CloseAccount");
+
+            process_close_account(program_id, accounts)
+        }
+        // 10 - FreezeAccount
+        Some((&10, _)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: FreezeAccount");
+
+            process_freeze_account(program_id, accounts)
+        }
+        // 10 - ThawAccount
+        Some((&11, _)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: ThawAccount");
+
+            process_thaw_account(program_id, accounts)
+        }
         _ => Err(ProgramError::InvalidInstructionData),
     }
 }

+ 73 - 0
p-token/src/processor/approve.rs

@@ -0,0 +1,73 @@
+use pinocchio::{
+    account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult,
+};
+use token_interface::{
+    error::TokenError,
+    state::{account::Account, mint::Mint, PodCOption},
+};
+
+use super::validate_owner;
+
+pub fn process_approve(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    amount: u64,
+    expected_decimals: Option<u8>,
+) -> ProgramResult {
+    let (source_account_info, expected_mint_info, delegate_info, owner_info, remaining) =
+        if let Some(expected_decimals) = expected_decimals {
+            let [source_account_info, expected_mint_info, delegate_info, owner_info, remaning @ ..] =
+                accounts
+            else {
+                return Err(ProgramError::NotEnoughAccountKeys);
+            };
+
+            (
+                source_account_info,
+                Some((expected_mint_info, expected_decimals)),
+                delegate_info,
+                owner_info,
+                remaning,
+            )
+        } else {
+            let [source_account_info, delegate_info, owner_info, remaning @ ..] = accounts else {
+                return Err(ProgramError::NotEnoughAccountKeys);
+            };
+            (
+                source_account_info,
+                None,
+                delegate_info,
+                owner_info,
+                remaning,
+            )
+        };
+
+    let source_account = bytemuck::try_from_bytes_mut::<Account>(unsafe {
+        source_account_info.borrow_mut_data_unchecked()
+    })
+    .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if source_account.is_frozen() {
+        return Err(TokenError::AccountFrozen.into());
+    }
+
+    if let Some((mint_info, expected_decimals)) = expected_mint_info {
+        if mint_info.key() != &source_account.mint {
+            return Err(TokenError::MintMismatch.into());
+        }
+
+        let mint = bytemuck::try_from_bytes::<Mint>(unsafe { mint_info.borrow_data_unchecked() })
+            .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+        if expected_decimals != mint.decimals {
+            return Err(TokenError::MintDecimalsMismatch.into());
+        }
+    }
+
+    validate_owner(program_id, &source_account.owner, owner_info, remaining)?;
+
+    source_account.delegate = PodCOption::some(*delegate_info.key());
+    source_account.delegated_amount = amount.into();
+
+    Ok(())
+}

+ 89 - 0
p-token/src/processor/burn.rs

@@ -0,0 +1,89 @@
+use pinocchio::{
+    account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult,
+};
+use token_interface::{
+    error::TokenError,
+    state::{account::Account, mint::Mint},
+};
+
+use super::{check_account_owner, is_owned_by_system_program_or_incinerator, validate_owner};
+
+/// Processes a [Burn](enum.TokenInstruction.html) instruction.
+pub fn process_burn(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    amount: u64,
+    expected_decimals: Option<u8>,
+) -> ProgramResult {
+    let [source_account_info, mint_info, authority_info, remaining @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    // Safety: There are no conflicting borrows – the source account is only borrowed once.
+    let source_account = bytemuck::try_from_bytes_mut::<Account>(unsafe {
+        source_account_info.borrow_mut_data_unchecked()
+    })
+    .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if source_account.is_frozen() {
+        return Err(TokenError::AccountFrozen.into());
+    }
+    if source_account.is_native.is_some() {
+        return Err(TokenError::NativeNotSupported.into());
+    }
+
+    // Ensure the source account has the sufficient amount. This is done before
+    // the value is updated on the account.
+    let updated_source_amount = u64::from(source_account.amount)
+        .checked_sub(amount)
+        .ok_or(TokenError::InsufficientFunds)?;
+
+    // Safety: There are no conflicting borrows – the mint account is only borrowed once.
+    let mint =
+        bytemuck::try_from_bytes_mut::<Mint>(unsafe { mint_info.borrow_mut_data_unchecked() })
+            .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if mint_info.key() != &source_account.mint {
+        return Err(TokenError::MintMismatch.into());
+    }
+
+    if let Some(expected_decimals) = expected_decimals {
+        if expected_decimals != mint.decimals {
+            return Err(TokenError::MintDecimalsMismatch.into());
+        }
+    }
+
+    if !is_owned_by_system_program_or_incinerator(&source_account.owner) {
+        match source_account.delegate.as_ref() {
+            Some(delegate) if authority_info.key() == delegate => {
+                validate_owner(program_id, delegate, authority_info, remaining)?;
+
+                let delegated_amount = u64::from(source_account.delegated_amount)
+                    .checked_sub(amount)
+                    .ok_or(TokenError::InsufficientFunds)?;
+                source_account.delegated_amount = delegated_amount.into();
+
+                if delegated_amount == 0 {
+                    source_account.delegate.clear();
+                }
+            }
+            _ => {
+                validate_owner(program_id, &source_account.owner, authority_info, remaining)?;
+            }
+        }
+    }
+
+    if amount == 0 {
+        check_account_owner(program_id, source_account_info)?;
+        check_account_owner(program_id, mint_info)?;
+    }
+
+    source_account.amount = updated_source_amount.into();
+
+    let mint_supply = u64::from(mint.supply)
+        .checked_sub(amount)
+        .ok_or(TokenError::Overflow)?;
+    mint.supply = mint_supply.into();
+
+    Ok(())
+}

+ 49 - 0
p-token/src/processor/close_account.rs

@@ -0,0 +1,49 @@
+use pinocchio::{
+    account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult,
+};
+use token_interface::{error::TokenError, state::account::Account};
+
+use super::{is_owned_by_system_program_or_incinerator, validate_owner, INCINERATOR_ID};
+
+/// Processes a [CloseAccount](enum.TokenInstruction.html) instruction.
+pub fn process_close_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+    let [source_account_info, destination_account_info, authority_info, remaining @ ..] = accounts
+    else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    if source_account_info.key() == destination_account_info.key() {
+        return Err(ProgramError::InvalidAccountData);
+    }
+
+    let source_account =
+        bytemuck::try_from_bytes::<Account>(unsafe { source_account_info.borrow_data_unchecked() })
+            .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if source_account.is_native.is_none() && source_account.amount() != 0 {
+        return Err(TokenError::NonNativeHasBalance.into());
+    }
+
+    let authority = source_account
+        .close_authority
+        .get()
+        .unwrap_or(source_account.owner);
+
+    if !is_owned_by_system_program_or_incinerator(source_account_info.owner()) {
+        validate_owner(program_id, &authority, authority_info, remaining)?;
+    } else if destination_account_info.key() != &INCINERATOR_ID {
+        return Err(ProgramError::InvalidAccountData);
+    }
+
+    let destination_starting_lamports = destination_account_info.lamports();
+    unsafe {
+        // Moves the lamports to the destination account and closes the source account.
+        *destination_account_info.borrow_mut_lamports_unchecked() = destination_starting_lamports
+            .checked_add(source_account_info.lamports())
+            .ok_or(TokenError::Overflow)?;
+
+        source_account_info.close();
+    }
+
+    Ok(())
+}

+ 7 - 0
p-token/src/processor/freeze_account.rs

@@ -0,0 +1,7 @@
+use pinocchio::{account_info::AccountInfo, pubkey::Pubkey, ProgramResult};
+
+use super::toggle_account_state::process_toggle_account_state;
+
+pub fn process_freeze_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+    process_toggle_account_state(program_id, accounts, true)
+}

+ 14 - 12
p-token/src/processor/initialize_account.rs

@@ -22,29 +22,31 @@ pub fn process_initialize_account(
     program_id: &Pubkey,
     accounts: &[AccountInfo],
     owner: Option<&Pubkey>,
-    _rent_sysvar_account: bool,
+    rent_sysvar_account: bool,
 ) -> ProgramResult {
-    let (new_account_info, mint_info, owner) = if let Some(owner) = owner {
-        let [new_account_info, mint_info, _remaning @ ..] = accounts else {
+    let (new_account_info, mint_info, owner, remaning) = if let Some(owner) = owner {
+        let [new_account_info, mint_info, remaning @ ..] = accounts else {
             return Err(ProgramError::NotEnoughAccountKeys);
         };
-        (new_account_info, mint_info, owner)
+        (new_account_info, mint_info, owner, remaning)
     } else {
-        let [new_account_info, mint_info, owner_info, _remaning @ ..] = accounts else {
+        let [new_account_info, mint_info, owner_info, remaning @ ..] = accounts else {
             return Err(ProgramError::NotEnoughAccountKeys);
         };
-        (new_account_info, mint_info, owner_info.key())
+        (new_account_info, mint_info, owner_info.key(), remaning)
     };
 
     // Check rent-exempt status of the token account.
-    //
-    // WIP: This is an expensive check (~400 CU) since it involves floating-point
-    // operations. Currently we are using a 'scaled' version, which is faster but
-    // not as precise as the `f64` version when there are decimal places.
 
-    let rent = Rent::get()?;
+    let is_exempt = if rent_sysvar_account {
+        let rent_sysvar_info = remaning.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
+        let rent = unsafe { Rent::from_bytes(rent_sysvar_info.borrow_data_unchecked()) };
+        rent.is_exempt(new_account_info.lamports(), size_of::<Account>())
+    } else {
+        Rent::get()?.is_exempt(new_account_info.lamports(), size_of::<Account>())
+    };
 
-    if !rent.is_exempt_scaled(new_account_info.lamports(), size_of::<Account>()) {
+    if !is_exempt {
         return Err(TokenError::NotRentExempt.into());
     }
 

+ 21 - 14
p-token/src/processor/initialize_mint.rs

@@ -15,31 +15,38 @@ use token_interface::{
 pub fn process_initialize_mint(
     accounts: &[AccountInfo],
     args: &InitializeMint,
-    _rent_sysvar_account: bool,
+    rent_sysvar_account: bool,
 ) -> ProgramResult {
-    // Validate the mint account.
-
-    let [mint_info, _remaining @ ..] = accounts else {
-        return Err(ProgramError::NotEnoughAccountKeys);
+    let (mint_info, rent_sysvar_info) = if rent_sysvar_account {
+        let [mint_info, rent_sysvar_info, _remaining @ ..] = accounts else {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        };
+        (mint_info, Some(rent_sysvar_info))
+    } else {
+        let [mint_info, _remaining @ ..] = accounts else {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        };
+        (mint_info, None)
     };
 
-    let mint_data = unsafe { mint_info.borrow_mut_data_unchecked() };
-    let mint = bytemuck::try_from_bytes_mut::<Mint>(mint_data)
-        .map_err(|_error| ProgramError::InvalidAccountData)?;
+    let mint =
+        bytemuck::try_from_bytes_mut::<Mint>(unsafe { mint_info.borrow_mut_data_unchecked() })
+            .map_err(|_error| ProgramError::InvalidAccountData)?;
 
     if mint.is_initialized.into() {
         return Err(TokenError::AlreadyInUse.into());
     }
 
     // Check rent-exempt status of the mint account.
-    //
-    // WIP: This is an expensive check (~400 CU) since it involves floating-point
-    // operations. Currently we are using a 'scaled' version, which is faster but
-    // not as precise as the `f64` version when there are decimal places.
 
-    let rent = Rent::get()?;
+    let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info {
+        let rent = unsafe { Rent::from_bytes(rent_sysvar_info.borrow_data_unchecked()) };
+        rent.is_exempt(mint_info.lamports(), size_of::<Mint>())
+    } else {
+        Rent::get()?.is_exempt(mint_info.lamports(), size_of::<Mint>())
+    };
 
-    if !rent.is_exempt_scaled(mint_info.lamports(), size_of::<Mint>()) {
+    if !is_exempt {
         return Err(TokenError::NotRentExempt.into());
     }
 

+ 63 - 0
p-token/src/processor/initialize_multisig.rs

@@ -0,0 +1,63 @@
+use pinocchio::{
+    account_info::AccountInfo,
+    program_error::ProgramError,
+    sysvars::{rent::Rent, Sysvar},
+    ProgramResult,
+};
+use token_interface::{error::TokenError, state::multisig::Multisig};
+
+pub fn process_initialize_multisig(
+    accounts: &[AccountInfo],
+    m: u8,
+    rent_sysvar_account: bool,
+) -> ProgramResult {
+    let (multisig_info, rent_sysvar_info, remaining) = if rent_sysvar_account {
+        let [multisig_info, rent_sysvar_info, remaining @ ..] = accounts else {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        };
+        (multisig_info, Some(rent_sysvar_info), remaining)
+    } else {
+        let [multisig_info, remaining @ ..] = accounts else {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        };
+        (multisig_info, None, remaining)
+    };
+
+    let multisig_info_data_len = multisig_info.data_len();
+
+    let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info {
+        let rent = unsafe { Rent::from_bytes(rent_sysvar_info.borrow_data_unchecked()) };
+        rent.is_exempt(multisig_info.lamports(), multisig_info_data_len)
+    } else {
+        Rent::get()?.is_exempt(multisig_info.lamports(), multisig_info_data_len)
+    };
+
+    if !is_exempt {
+        return Err(TokenError::NotRentExempt.into());
+    }
+
+    let multisig = bytemuck::try_from_bytes_mut::<Multisig>(unsafe {
+        multisig_info.borrow_mut_data_unchecked()
+    })
+    .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if multisig.is_initialized.into() {
+        return Err(TokenError::AlreadyInUse.into());
+    }
+
+    multisig.m = m;
+    multisig.n = remaining.len() as u8;
+
+    if !Multisig::is_valid_signer_index(multisig.n as usize) {
+        return Err(TokenError::InvalidNumberOfProvidedSigners.into());
+    }
+    if !Multisig::is_valid_signer_index(multisig.m as usize) {
+        return Err(TokenError::InvalidNumberOfRequiredSigners.into());
+    }
+    for (i, signer_info) in remaining.iter().enumerate() {
+        multisig.signers[i] = *signer_info.key();
+    }
+    multisig.is_initialized = true.into();
+
+    Ok(())
+}

+ 24 - 1
p-token/src/processor/mod.rs

@@ -3,13 +3,36 @@ use pinocchio::{
 };
 use token_interface::{
     error::TokenError,
-    state::multisignature::{Multisig, MAX_SIGNERS},
+    state::multisig::{Multisig, MAX_SIGNERS},
 };
 
+pub mod approve;
+pub mod burn;
+pub mod close_account;
+pub mod freeze_account;
 pub mod initialize_account;
 pub mod initialize_mint;
+pub mod initialize_multisig;
 pub mod mint_to;
+pub mod revoke;
+pub mod set_authority;
+pub mod thaw_account;
 pub mod transfer;
+// Private processor to toggle the account state. This logic is reused by the
+// freeze and thaw account instructions.
+mod toggle_account_state;
+
+/// Incinerator address.
+const INCINERATOR_ID: Pubkey =
+    pinocchio_pubkey::pubkey!("1nc1nerator11111111111111111111111111111111");
+
+/// System program id.
+const SYSTEM_PROGRAM_ID: Pubkey = pinocchio_pubkey::pubkey!("11111111111111111111111111111111");
+
+#[inline(always)]
+pub fn is_owned_by_system_program_or_incinerator(owner: &Pubkey) -> bool {
+    SYSTEM_PROGRAM_ID == *owner || INCINERATOR_ID == *owner
+}
 
 /// Checks that the account is owned by the expected program.
 #[inline(always)]

+ 29 - 0
p-token/src/processor/revoke.rs

@@ -0,0 +1,29 @@
+use pinocchio::{
+    account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult,
+};
+use token_interface::{error::TokenError, state::account::Account};
+
+use super::validate_owner;
+
+/// Processes an [Revoke](enum.TokenInstruction.html) instruction.
+pub fn process_revoke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+    let [source_account_info, owner_info, remaning @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let source_account = bytemuck::try_from_bytes_mut::<Account>(unsafe {
+        source_account_info.borrow_mut_data_unchecked()
+    })
+    .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if source_account.is_frozen() {
+        return Err(TokenError::AccountFrozen.into());
+    }
+
+    validate_owner(program_id, &source_account.owner, owner_info, remaning)?;
+
+    source_account.delegate.clear();
+    source_account.delegated_amount = 0.into();
+
+    Ok(())
+}

+ 132 - 0
p-token/src/processor/set_authority.rs

@@ -0,0 +1,132 @@
+use pinocchio::{
+    account_info::AccountInfo,
+    program_error::ProgramError,
+    pubkey::{Pubkey, PUBKEY_BYTES},
+    ProgramResult,
+};
+use token_interface::{
+    error::TokenError,
+    instruction::AuthorityType,
+    state::{account::Account, mint::Mint, PodCOption},
+};
+
+use super::validate_owner;
+
+pub fn process_set_authority(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    authority_type: AuthorityType,
+    new_authority: Option<&Pubkey>,
+) -> ProgramResult {
+    let [account_info, authority_info, remaning @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    if account_info.data_len() == Account::LEN {
+        let account = bytemuck::try_from_bytes_mut::<Account>(unsafe {
+            account_info.borrow_mut_data_unchecked()
+        })
+        .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+        if account.is_frozen() {
+            return Err(TokenError::AccountFrozen.into());
+        }
+
+        match authority_type {
+            AuthorityType::AccountOwner => {
+                validate_owner(program_id, &account.owner, authority_info, remaning)?;
+
+                if let Some(authority) = new_authority {
+                    account.owner = *authority;
+                } else {
+                    return Err(TokenError::InvalidInstruction.into());
+                }
+
+                account.delegate.clear();
+                account.delegated_amount = 0.into();
+
+                if account.is_native.is_some() {
+                    account.close_authority.clear();
+                }
+            }
+            AuthorityType::CloseAccount => {
+                let authority = account.close_authority.as_ref().unwrap_or(&account.owner);
+                validate_owner(program_id, authority, authority_info, remaning)?;
+                account.close_authority = PodCOption::from(new_authority.copied());
+            }
+            _ => {
+                return Err(TokenError::AuthorityTypeNotSupported.into());
+            }
+        }
+    } else if account_info.data_len() == Mint::LEN {
+        let mint = bytemuck::try_from_bytes_mut::<Mint>(unsafe {
+            account_info.borrow_mut_data_unchecked()
+        })
+        .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+        match authority_type {
+            AuthorityType::MintTokens => {
+                // Once a mint's supply is fixed, it cannot be undone by setting a new
+                // mint_authority
+                let mint_authority = mint
+                    .mint_authority
+                    .as_ref()
+                    .ok_or(TokenError::FixedSupply)?;
+
+                validate_owner(program_id, mint_authority, authority_info, remaning)?;
+                mint.mint_authority = PodCOption::from(new_authority.copied());
+            }
+            AuthorityType::FreezeAccount => {
+                // Once a mint's freeze authority is disabled, it cannot be re-enabled by
+                // setting a new freeze_authority
+                let freeze_authority = mint
+                    .freeze_authority
+                    .as_ref()
+                    .ok_or(TokenError::MintCannotFreeze)?;
+
+                validate_owner(program_id, freeze_authority, authority_info, remaning)?;
+                mint.freeze_authority = PodCOption::from(new_authority.copied());
+            }
+            _ => {
+                return Err(TokenError::AuthorityTypeNotSupported.into());
+            }
+        }
+    } else {
+        return Err(ProgramError::InvalidArgument);
+    }
+
+    Ok(())
+}
+
+/// Instruction data for the `InitializeMint` instruction.
+pub struct SetAuthority<'a> {
+    pub authority_type: AuthorityType,
+
+    /// New authority.
+    pub new_authority: Option<&'a Pubkey>,
+}
+
+impl<'a> SetAuthority<'a> {
+    pub fn try_from_bytes(data: &'a [u8]) -> Result<Self, ProgramError> {
+        // We expect the data to be at least the size of the u8 (authority_type)
+        // plus one byte for the authority option.
+        if data.len() <= 2 {
+            return Err(ProgramError::InvalidInstructionData);
+        }
+
+        let (authority_type, remaining) = data.split_at(1);
+
+        let new_authority = match remaining.split_first() {
+            Some((&0, _)) => None,
+            Some((&1, pubkey)) if pubkey.len() == PUBKEY_BYTES => {
+                Some(bytemuck::from_bytes::<Pubkey>(pubkey))
+            }
+            _ => return Err(ProgramError::InvalidInstructionData),
+        };
+
+        Ok(Self {
+            authority_type: AuthorityType::from(authority_type[0]),
+            new_authority,
+        })
+    }
+}

+ 7 - 0
p-token/src/processor/thaw_account.rs

@@ -0,0 +1,7 @@
+use pinocchio::{account_info::AccountInfo, pubkey::Pubkey, ProgramResult};
+
+use super::toggle_account_state::process_toggle_account_state;
+
+pub fn process_thaw_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+    process_toggle_account_state(program_id, accounts, false)
+}

+ 54 - 0
p-token/src/processor/toggle_account_state.rs

@@ -0,0 +1,54 @@
+use pinocchio::{
+    account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult,
+};
+use token_interface::{
+    error::TokenError,
+    state::{
+        account::{Account, AccountState},
+        mint::Mint,
+    },
+};
+
+use super::validate_owner;
+
+#[inline(always)]
+pub fn process_toggle_account_state(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    freeze: bool,
+) -> ProgramResult {
+    let [source_account_info, mint_info, authority_info, remaining @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let source_account = bytemuck::try_from_bytes_mut::<Account>(unsafe {
+        source_account_info.borrow_mut_data_unchecked()
+    })
+    .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if freeze && source_account.is_frozen() || !freeze && !source_account.is_frozen() {
+        return Err(TokenError::InvalidState.into());
+    }
+    if source_account.is_native.is_some() {
+        return Err(TokenError::NativeNotSupported.into());
+    }
+    if mint_info.key() != &source_account.mint {
+        return Err(TokenError::MintMismatch.into());
+    }
+
+    let mint = bytemuck::try_from_bytes::<Mint>(unsafe { mint_info.borrow_data_unchecked() })
+        .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    match mint.freeze_authority.as_ref() {
+        Option::Some(authority) => validate_owner(program_id, authority, authority_info, remaining),
+        Option::None => Err(TokenError::MintCannotFreeze.into()),
+    }?;
+
+    source_account.state = if freeze {
+        AccountState::Frozen
+    } else {
+        AccountState::Initialized
+    } as u8;
+
+    Ok(())
+}

+ 89 - 0
p-token/tests/approve.rs

@@ -0,0 +1,89 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn approve(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account with 100 tokens.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
+
+    mint::mint(
+        &mut context,
+        &mint,
+        &account,
+        &mint_authority,
+        100,
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // When we approve a delegate.
+
+    let delegate = Pubkey::new_unique();
+
+    let mut approve_ix = spl_token::instruction::approve(
+        &spl_token::ID,
+        &account,
+        &delegate,
+        &owner.pubkey(),
+        &[],
+        50,
+    )
+    .unwrap();
+    approve_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[approve_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &owner],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the account should have the delegate and delegated amount.
+
+    let account = context.banks_client.get_account(account).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let account = spl_token::state::Account::unpack(&account.data).unwrap();
+
+    assert!(account.delegate.is_some());
+    assert!(account.delegate.unwrap() == delegate);
+    assert!(account.delegated_amount == 50);
+}

+ 79 - 0
p-token/tests/burn.rs

@@ -0,0 +1,79 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn burn(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account with 100 tokens.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
+
+    mint::mint(
+        &mut context,
+        &mint,
+        &account,
+        &mint_authority,
+        100,
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // When we burn 50 tokens.
+
+    let mut burn_ix =
+        spl_token::instruction::burn(&spl_token::ID, &account, &mint, &owner.pubkey(), &[], 50)
+            .unwrap();
+    burn_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[burn_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &owner],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the account should have 50 tokens remaining.
+
+    let account = context.banks_client.get_account(account).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let account = spl_token::state::Account::unpack(&account.data).unwrap();
+
+    assert!(account.amount == 50);
+}

+ 69 - 0
p-token/tests/close_account.rs

@@ -0,0 +1,69 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn close_account(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
+
+    let token_account = context.banks_client.get_account(account).await.unwrap();
+    assert!(token_account.is_some());
+
+    // When we close the account.
+
+    let mut close_account_ix = spl_token::instruction::close_account(
+        &spl_token::ID,
+        &account,
+        &owner.pubkey(),
+        &owner.pubkey(),
+        &[],
+    )
+    .unwrap();
+    close_account_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[close_account_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &owner],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then an account must not exist.
+
+    let token_account = context.banks_client.get_account(account).await.unwrap();
+    assert!(token_account.is_none());
+}

+ 76 - 0
p-token/tests/freeze_account.rs

@@ -0,0 +1,76 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+use spl_token::state::AccountState;
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn freeze_account(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Keypair::new();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority.pubkey()),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
+
+    let token_account = context.banks_client.get_account(account).await.unwrap();
+    assert!(token_account.is_some());
+
+    // When we freeze the account.
+
+    let mut freeze_account_ix = spl_token::instruction::freeze_account(
+        &spl_token::ID,
+        &account,
+        &mint,
+        &freeze_authority.pubkey(),
+        &[],
+    )
+    .unwrap();
+    freeze_account_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[freeze_account_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &freeze_authority],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the account is frozen.
+
+    let token_account = context.banks_client.get_account(account).await.unwrap();
+    assert!(token_account.is_some());
+
+    let token_account = token_account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&token_account.data).unwrap();
+
+    assert_eq!(token_account.state, AccountState::Frozen);
+}

+ 79 - 0
p-token/tests/initialize_multisig.rs

@@ -0,0 +1,79 @@
+#![cfg(feature = "test-sbf")]
+
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    system_instruction,
+    transaction::Transaction,
+};
+use spl_token::state::Multisig;
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn initialize_multisig(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given an account
+
+    let multisig = Keypair::new();
+    let signer1 = Pubkey::new_unique();
+    let signer2 = Pubkey::new_unique();
+    let signer3 = Pubkey::new_unique();
+    let signers = vec![&signer1, &signer2, &signer3];
+
+    let rent = context.banks_client.get_rent().await.unwrap();
+
+    let mut initialize_ix = spl_token::instruction::initialize_multisig(
+        &spl_token::ID,
+        &multisig.pubkey(),
+        &signers,
+        2,
+    )
+    .unwrap();
+    // Switches the program id to the token program.
+    initialize_ix.program_id = token_program;
+
+    // When a new multisig account is created and initialized.
+
+    let instructions = vec![
+        system_instruction::create_account(
+            &context.payer.pubkey(),
+            &multisig.pubkey(),
+            rent.minimum_balance(Multisig::LEN),
+            Multisig::LEN as u64,
+            &token_program,
+        ),
+        initialize_ix,
+    ];
+
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &multisig],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the multisig has the correct data.
+
+    let account = context
+        .banks_client
+        .get_account(multisig.pubkey())
+        .await
+        .unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let multisig = spl_token::state::Multisig::unpack(&account.data).unwrap();
+
+    assert!(multisig.is_initialized);
+    assert_eq!(multisig.n, 3);
+    assert_eq!(multisig.m, 2);
+}

+ 1 - 3
p-token/tests/mint_to.rs

@@ -38,9 +38,7 @@ async fn mint_to(token_program: Pubkey) {
 
     let owner = Keypair::new();
 
-    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program)
-        .await
-        .unwrap();
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
 
     // When we mint tokens to it.
 

+ 93 - 0
p-token/tests/revoke.rs

@@ -0,0 +1,93 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn revoke(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account with 100 tokens.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
+
+    mint::mint(
+        &mut context,
+        &mint,
+        &account,
+        &mint_authority,
+        100,
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And 50 tokens delegated.
+
+    let delegate = Pubkey::new_unique();
+
+    account::approve(
+        &mut context,
+        &account,
+        &delegate,
+        &owner,
+        50,
+        &token_program,
+    )
+    .await;
+
+    // When we revoke the delegation.
+
+    let mut revoke_ix =
+        spl_token::instruction::revoke(&spl_token::ID, &account, &owner.pubkey(), &[]).unwrap();
+    revoke_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[revoke_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &owner],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the account should not have a delegate nor delegated amount.
+
+    let account = context.banks_client.get_account(account).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let account = spl_token::state::Account::unpack(&account.data).unwrap();
+
+    assert!(account.delegate.is_none());
+    assert!(account.delegated_amount == 0);
+}

+ 72 - 0
p-token/tests/set_authority.rs

@@ -0,0 +1,72 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::mint;
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_option::COption,
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+use spl_token::instruction::AuthorityType;
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn set_authority(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Keypair::new();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority.pubkey()),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // When we set a new freeze authority.
+
+    let new_authority = Pubkey::new_unique();
+
+    let mut set_authority_ix = spl_token::instruction::set_authority(
+        &spl_token::ID,
+        &mint,
+        Some(&new_authority),
+        AuthorityType::FreezeAccount,
+        &freeze_authority.pubkey(),
+        &[],
+    )
+    .unwrap();
+    set_authority_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[set_authority_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &freeze_authority],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the account should have the delegate and delegated amount.
+
+    let account = context.banks_client.get_account(mint).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let mint = spl_token::state::Mint::unpack(&account.data).unwrap();
+
+    assert!(mint.freeze_authority == COption::Some(new_authority));
+}

+ 58 - 4
p-token/tests/setup/account.rs

@@ -1,7 +1,7 @@
 use solana_program_test::ProgramTestContext;
 use solana_sdk::{
-    program_error::ProgramError, pubkey::Pubkey, signature::Keypair, signer::Signer,
-    system_instruction, transaction::Transaction,
+    pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction,
+    transaction::Transaction,
 };
 
 pub async fn initialize(
@@ -9,7 +9,7 @@ pub async fn initialize(
     mint: &Pubkey,
     owner: &Pubkey,
     program_id: &Pubkey,
-) -> Result<Pubkey, ProgramError> {
+) -> Pubkey {
     let account = Keypair::new();
 
     let account_size = 165;
@@ -39,5 +39,59 @@ pub async fn initialize(
     );
     context.banks_client.process_transaction(tx).await.unwrap();
 
-    Ok(account.pubkey())
+    account.pubkey()
+}
+
+pub async fn approve(
+    context: &mut ProgramTestContext,
+    account: &Pubkey,
+    delegate: &Pubkey,
+    owner: &Keypair,
+    amount: u64,
+    program_id: &Pubkey,
+) {
+    let mut approve_ix = spl_token::instruction::approve(
+        &spl_token::ID,
+        account,
+        delegate,
+        &owner.pubkey(),
+        &[],
+        amount,
+    )
+    .unwrap();
+    approve_ix.program_id = *program_id;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[approve_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, owner],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+}
+
+pub async fn freeze(
+    context: &mut ProgramTestContext,
+    account: &Pubkey,
+    mint: &Pubkey,
+    freeze_authority: &Keypair,
+    program_id: &Pubkey,
+) {
+    let mut freeze_account_ix = spl_token::instruction::freeze_account(
+        &spl_token::ID,
+        account,
+        mint,
+        &freeze_authority.pubkey(),
+        &[],
+    )
+    .unwrap();
+    freeze_account_ix.program_id = *program_id;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[freeze_account_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, freeze_authority],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
 }

+ 85 - 0
p-token/tests/thaw_account.rs

@@ -0,0 +1,85 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+use spl_token::state::AccountState;
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "p-token")]
+#[tokio::test]
+async fn thaw_account(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Keypair::new();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority.pubkey()),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a frozen token account.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
+
+    let token_account = context.banks_client.get_account(account).await.unwrap();
+    assert!(token_account.is_some());
+
+    account::freeze(
+        &mut context,
+        &account,
+        &mint,
+        &freeze_authority,
+        &token_program,
+    )
+    .await;
+
+    // When we thaw the account.
+
+    let mut thaw_account_ix = spl_token::instruction::thaw_account(
+        &spl_token::ID,
+        &account,
+        &mint,
+        &freeze_authority.pubkey(),
+        &[],
+    )
+    .unwrap();
+    thaw_account_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[thaw_account_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &freeze_authority],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then the account is frozen.
+
+    let token_account = context.banks_client.get_account(account).await.unwrap();
+    assert!(token_account.is_some());
+
+    let token_account = token_account.unwrap();
+    let token_account = spl_token::state::Account::unpack(&token_account.data).unwrap();
+
+    assert_eq!(token_account.state, AccountState::Initialized);
+}

+ 2 - 6
p-token/tests/transfer.rs

@@ -38,9 +38,7 @@ async fn transfer(token_program: Pubkey) {
 
     let owner = Keypair::new();
 
-    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program)
-        .await
-        .unwrap();
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await;
 
     mint::mint(
         &mut context,
@@ -58,9 +56,7 @@ async fn transfer(token_program: Pubkey) {
     let destination = Pubkey::new_unique();
 
     let destination_account =
-        account::initialize(&mut context, &mint, &destination, &token_program)
-            .await
-            .unwrap();
+        account::initialize(&mut context, &mint, &destination, &token_program).await;
 
     let mut transfer_ix = spl_token::instruction::transfer(
         &spl_token::ID,