Browse Source

Add safety comments (#8)

* Remove spl-token test cases

* Update CU values

* Add safety comments

* Use git dependencies
febo 8 months ago
parent
commit
ba9856902b
45 changed files with 205 additions and 118 deletions
  1. 0 1
      p-token/Cargo.toml
  2. 2 1
      p-token/src/processor/amount_to_ui_amount.rs
  3. 15 15
      p-token/src/processor/burn_checked.rs
  4. 27 21
      p-token/src/processor/close_account.rs
  5. 2 0
      p-token/src/processor/get_account_data_size.rs
  6. 14 2
      p-token/src/processor/initialize_account2.rs
  7. 14 2
      p-token/src/processor/initialize_account3.rs
  8. 1 0
      p-token/src/processor/initialize_immutable_owner.rs
  9. 8 2
      p-token/src/processor/initialize_mint.rs
  10. 15 12
      p-token/src/processor/mint_to_checked.rs
  11. 6 1
      p-token/src/processor/mod.rs
  12. 2 0
      p-token/src/processor/revoke.rs
  13. 7 1
      p-token/src/processor/set_authority.rs
  14. 4 0
      p-token/src/processor/shared/approve.rs
  15. 4 0
      p-token/src/processor/shared/burn.rs
  16. 7 1
      p-token/src/processor/shared/initialize_account.rs
  17. 4 1
      p-token/src/processor/shared/initialize_multisig.rs
  18. 4 0
      p-token/src/processor/shared/mint_to.rs
  19. 5 1
      p-token/src/processor/shared/toggle_account_state.rs
  20. 45 19
      p-token/src/processor/shared/transfer.rs
  21. 2 0
      p-token/src/processor/sync_native.rs
  22. 15 15
      p-token/src/processor/transfer_checked.rs
  23. 2 1
      p-token/src/processor/ui_amount_to_amount.rs
  24. 0 1
      p-token/tests/amount_to_ui_amount.rs
  25. 0 1
      p-token/tests/approve.rs
  26. 0 1
      p-token/tests/approve_checked.rs
  27. 0 1
      p-token/tests/burn.rs
  28. 0 1
      p-token/tests/burn_checked.rs
  29. 0 1
      p-token/tests/close_account.rs
  30. 0 1
      p-token/tests/freeze_account.rs
  31. 0 1
      p-token/tests/initialize_account.rs
  32. 0 1
      p-token/tests/initialize_account2.rs
  33. 0 1
      p-token/tests/initialize_account3.rs
  34. 0 1
      p-token/tests/initialize_mint.rs
  35. 0 1
      p-token/tests/initialize_mint2.rs
  36. 0 1
      p-token/tests/initialize_multisig.rs
  37. 0 1
      p-token/tests/initialize_multisig2.rs
  38. 0 1
      p-token/tests/mint_to.rs
  39. 0 1
      p-token/tests/mint_to_checked.rs
  40. 0 1
      p-token/tests/revoke.rs
  41. 0 1
      p-token/tests/set_authority.rs
  42. 0 1
      p-token/tests/thaw_account.rs
  43. 0 1
      p-token/tests/transfer.rs
  44. 0 1
      p-token/tests/transfer_checked.rs
  45. 0 1
      p-token/tests/ui_amount_to_amount.rs

+ 0 - 1
p-token/Cargo.toml

@@ -20,7 +20,6 @@ test-sbf = []
 [dependencies]
 pinocchio = { workspace = true }
 pinocchio-log = { workspace = true }
-pinocchio-pubkey = { workspace = true }
 token-interface = { version = "^0", path = "../interface" }
 
 [dev-dependencies]

+ 2 - 1
p-token/src/processor/amount_to_ui_amount.rs

@@ -23,7 +23,8 @@ pub fn process_amount_to_ui_amount(
 
     let mint_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
     check_account_owner(mint_info)?;
-    // SAFETY: there is a single borrow to the `Mint` account.
+    // SAFETY: single immutable borrow to `mint_info` account data and
+    // `load` validates that the mint is initialized.
     let mint = unsafe {
         load::<Mint>(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)?
     };

+ 15 - 15
p-token/src/processor/burn_checked.rs

@@ -4,20 +4,20 @@ use super::shared;
 
 #[inline(always)]
 pub fn process_burn_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
-    let (amount, decimals) = instruction_data.split_at(core::mem::size_of::<u64>());
-    let amount = u64::from_le_bytes(
-        amount
-            .try_into()
-            .map_err(|_error| ProgramError::InvalidInstructionData)?,
-    );
+    // expected u64 (8) + u8 (1)
+    let (amount, decimals) = if instruction_data.len() == 9 {
+        let (amount, decimals) = instruction_data.split_at(core::mem::size_of::<u64>());
+        (
+            u64::from_le_bytes(
+                amount
+                    .try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            ),
+            decimals.first(),
+        )
+    } else {
+        return Err(ProgramError::InvalidInstructionData);
+    };
 
-    shared::burn::process_burn(
-        accounts,
-        amount,
-        Some(
-            *decimals
-                .first()
-                .ok_or(ProgramError::InvalidInstructionData)?,
-        ),
-    )
+    shared::burn::process_burn(accounts, amount, decimals.copied())
 }

+ 27 - 21
p-token/src/processor/close_account.rs

@@ -3,14 +3,16 @@ use pinocchio::{
 };
 use token_interface::{
     error::TokenError,
-    state::{account::Account, load_mut},
+    state::{account::Account, load},
 };
 
 use super::validate_owner;
 
-/// Incinerator address.
-const INCINERATOR_ID: Pubkey =
-    pinocchio_pubkey::pubkey!("1nc1nerator11111111111111111111111111111111");
+/// Incinerator (`1nc1nerator11111111111111111111111111111111`) address.
+const INCINERATOR_ID: Pubkey = [
+    0, 51, 144, 114, 141, 52, 17, 96, 121, 189, 201, 17, 191, 255, 0, 219, 212, 77, 46, 205, 204,
+    247, 156, 166, 225, 0, 56, 225, 0, 0, 0, 0,
+];
 
 #[inline(always)]
 pub fn process_close_account(accounts: &[AccountInfo]) -> ProgramResult {
@@ -24,26 +26,30 @@ pub fn process_close_account(accounts: &[AccountInfo]) -> ProgramResult {
     // raw pointer.
     if source_account_info == destination_account_info {
         return Err(ProgramError::InvalidAccountData);
-    }
-
-    let source_account =
-        unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
-
-    if !source_account.is_native() && source_account.amount() != 0 {
-        return Err(TokenError::NonNativeHasBalance.into());
-    }
-
-    let authority = source_account
-        .close_authority()
-        .unwrap_or(&source_account.owner);
-
-    if !source_account.is_owned_by_system_program_or_incinerator() {
-        validate_owner(authority, authority_info, remaining)?;
-    } else if destination_account_info.key() != &INCINERATOR_ID {
-        return Err(ProgramError::InvalidAccountData);
+    } else {
+        // SAFETY: scoped immutable borrow to `source_account_info` account data and
+        // `load` validates that the account is initialized.
+        let source_account =
+            unsafe { load::<Account>(source_account_info.borrow_data_unchecked())? };
+
+        if !source_account.is_native() && source_account.amount() != 0 {
+            return Err(TokenError::NonNativeHasBalance.into());
+        }
+
+        let authority = source_account
+            .close_authority()
+            .unwrap_or(&source_account.owner);
+
+        if !source_account.is_owned_by_system_program_or_incinerator() {
+            validate_owner(authority, authority_info, remaining)?;
+        } else if destination_account_info.key() != &INCINERATOR_ID {
+            return Err(ProgramError::InvalidAccountData);
+        }
     }
 
     let destination_starting_lamports = destination_account_info.lamports();
+    // SAFETY: single mutable borrow to `destination_account_info` lamports and
+    // there are no "active" borrows of `source_account_info` account data.
     unsafe {
         // Moves the lamports to the destination account.
         *destination_account_info.borrow_mut_lamports_unchecked() = destination_starting_lamports

+ 2 - 0
p-token/src/processor/get_account_data_size.rs

@@ -17,6 +17,8 @@ pub fn process_get_account_data_size(accounts: &[AccountInfo]) -> ProgramResult
     // Make sure the mint is valid.
     check_account_owner(mint_info)?;
 
+    // SAFETY: single immutable borrow to `mint_info` account data and
+    // `load` validates that the mint is initialized.
     let _ = unsafe {
         load::<Mint>(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)?
     };

+ 14 - 2
p-token/src/processor/initialize_account2.rs

@@ -1,4 +1,9 @@
-use pinocchio::{account_info::AccountInfo, pubkey::Pubkey, ProgramResult};
+use pinocchio::{
+    account_info::AccountInfo,
+    program_error::ProgramError,
+    pubkey::{Pubkey, PUBKEY_BYTES},
+    ProgramResult,
+};
 
 use super::shared;
 
@@ -7,6 +12,13 @@ pub fn process_initialize_account2(
     accounts: &[AccountInfo],
     instruction_data: &[u8],
 ) -> ProgramResult {
-    let owner = unsafe { &*(instruction_data.as_ptr() as *const Pubkey) };
+    // SAFETY: validate `instruction_data` length.
+    let owner = unsafe {
+        if instruction_data.len() != PUBKEY_BYTES {
+            return Err(ProgramError::InvalidInstructionData);
+        } else {
+            &*(instruction_data.as_ptr() as *const Pubkey)
+        }
+    };
     shared::initialize_account::process_initialize_account(accounts, Some(owner), true)
 }

+ 14 - 2
p-token/src/processor/initialize_account3.rs

@@ -1,4 +1,9 @@
-use pinocchio::{account_info::AccountInfo, pubkey::Pubkey, ProgramResult};
+use pinocchio::{
+    account_info::AccountInfo,
+    program_error::ProgramError,
+    pubkey::{Pubkey, PUBKEY_BYTES},
+    ProgramResult,
+};
 
 use super::shared;
 
@@ -7,6 +12,13 @@ pub fn process_initialize_account3(
     accounts: &[AccountInfo],
     instruction_data: &[u8],
 ) -> ProgramResult {
-    let owner = unsafe { &*(instruction_data.as_ptr() as *const Pubkey) };
+    // SAFETY: validate `instruction_data` length.
+    let owner = unsafe {
+        if instruction_data.len() != PUBKEY_BYTES {
+            return Err(ProgramError::InvalidInstructionData);
+        } else {
+            &*(instruction_data.as_ptr() as *const Pubkey)
+        }
+    };
     shared::initialize_account::process_initialize_account(accounts, Some(owner), false)
 }

+ 1 - 0
p-token/src/processor/initialize_immutable_owner.rs

@@ -8,6 +8,7 @@ use token_interface::{
 pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult {
     let token_account_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
 
+    // SAFETY: single immutable borrow to `token_account_info` account data.
     let account = unsafe { load_unchecked::<Account>(token_account_info.borrow_data_unchecked())? };
 
     if account.is_initialized() {

+ 8 - 2
p-token/src/processor/initialize_mint.rs

@@ -35,6 +35,7 @@ pub fn process_initialize_mint(
         (mint_info, None)
     };
 
+    // SAFETY: single mutable borrow to `mint_info` account data.
     let mint = unsafe { load_mut_unchecked::<Mint>(mint_info.borrow_mut_data_unchecked())? };
 
     if mint.is_initialized() {
@@ -44,7 +45,9 @@ pub fn process_initialize_mint(
     // Check rent-exempt status of the mint account.
 
     let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info {
-        let rent = unsafe { Rent::from_bytes(rent_sysvar_info.borrow_data_unchecked()) };
+        // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are
+        // checked by `from_account_info_unchecked`.
+        let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? };
         rent.is_exempt(mint_info.lamports(), size_of::<Mint>())
     } else {
         Rent::get()?.is_exempt(mint_info.lamports(), size_of::<Mint>())
@@ -81,7 +84,7 @@ impl InitializeMint<'_> {
         // - decimals (1 byte)
         // - mint_authority (32 bytes)
         // - option + freeze_authority (1 byte + 32 bytes)
-        if bytes.len() < 34 {
+        if bytes.len() < 34 || (bytes[33] == 1 && bytes.len() < 66) {
             return Err(ProgramError::InvalidInstructionData);
         }
 
@@ -93,16 +96,19 @@ impl InitializeMint<'_> {
 
     #[inline]
     pub fn decimals(&self) -> u8 {
+        // SAFETY: the `bytes` length was validated in `try_from_bytes`.
         unsafe { *self.raw }
     }
 
     #[inline]
     pub fn mint_authority(&self) -> &Pubkey {
+        // SAFETY: the `bytes` length was validated in `try_from_bytes`.
         unsafe { &*(self.raw.add(1) as *const Pubkey) }
     }
 
     #[inline]
     pub fn freeze_authority(&self) -> Option<&Pubkey> {
+        // SAFETY: the `bytes` length was validated in `try_from_bytes`.
         unsafe {
             if *self.raw.add(33) == 0 {
                 Option::None

+ 15 - 12
p-token/src/processor/mint_to_checked.rs

@@ -4,17 +4,20 @@ use super::shared;
 
 #[inline(always)]
 pub fn process_mint_to_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
-    let (amount, decimals) = instruction_data.split_at(core::mem::size_of::<u64>());
+    // expected u64 (8) + u8 (1)
+    let (amount, decimals) = if instruction_data.len() == 9 {
+        let (amount, decimals) = instruction_data.split_at(core::mem::size_of::<u64>());
+        (
+            u64::from_le_bytes(
+                amount
+                    .try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            ),
+            decimals.first(),
+        )
+    } else {
+        return Err(ProgramError::InvalidInstructionData);
+    };
 
-    let amount = u64::from_le_bytes(
-        amount
-            .try_into()
-            .map_err(|_error| ProgramError::InvalidInstructionData)?,
-    );
-
-    shared::mint_to::process_mint_to(
-        accounts,
-        amount,
-        Some(*decimals.first().ok_or(ProgramError::InvalidAccountData)?),
-    )
+    shared::mint_to::process_mint_to(accounts, amount, decimals.copied())
 }

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

@@ -92,7 +92,10 @@ fn check_account_owner(account_info: &AccountInfo) -> ProgramResult {
     }
 }
 
-/// Validates owner(s) are present
+/// Validates owner(s) are present.
+///
+/// Note that `owner_account_info` will be immutable borrowed when it represents
+/// a multisig account.
 #[inline(always)]
 fn validate_owner(
     expected_owner: &Pubkey,
@@ -106,6 +109,8 @@ fn validate_owner(
     if owner_account_info.data_len() == Multisig::LEN
         && owner_account_info.owner() == &TOKEN_PROGRAM_ID
     {
+        // SAFETY: the caller guarantees that there are no mutable borrows of `owner_account_info`
+        // account data and the `load` validates that the account is initialized.
         let multisig = unsafe { load::<Multisig>(owner_account_info.borrow_data_unchecked())? };
 
         let mut num_signers = 0;

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

@@ -12,6 +12,8 @@ pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> Pro
         return Err(ProgramError::NotEnoughAccountKeys);
     };
 
+    // SAFETY: single mutable borrow to `source_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let source_account =
         unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
 

+ 7 - 1
p-token/src/processor/set_authority.rs

@@ -27,6 +27,8 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
     };
 
     if account_info.data_len() == Account::LEN {
+        // SAFETY: single mutable borrow to `account_info` account data and
+        // `load_mut` validates that the account is initialized.
         let account = unsafe { load_mut::<Account>(account_info.borrow_mut_data_unchecked())? };
 
         if account.is_frozen() {
@@ -65,6 +67,8 @@ pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8])
             }
         }
     } else if account_info.data_len() == Mint::LEN {
+        // SAFETY: single mutable borrow to `account_info` account data and
+        // `load_mut` validates that the mint is initialized.
         let mint = unsafe { load_mut::<Mint>(account_info.borrow_mut_data_unchecked())? };
 
         match authority_type {
@@ -119,7 +123,7 @@ impl SetAuthority<'_> {
         // The minimum expected size of the instruction data.
         // - authority_type (1 byte)
         // - option + new_authority (1 byte + 32 bytes)
-        if bytes.len() < 2 {
+        if bytes.len() < 2 || (bytes[1] == 1 && bytes.len() < 34) {
             return Err(ProgramError::InvalidInstructionData);
         }
 
@@ -131,11 +135,13 @@ impl SetAuthority<'_> {
 
     #[inline(always)]
     pub fn authority_type(&self) -> Result<AuthorityType, ProgramError> {
+        // SAFETY: `bytes` length is validated in `try_from_bytes`.
         unsafe { AuthorityType::from(*self.raw) }
     }
 
     #[inline(always)]
     pub fn new_authority(&self) -> Option<&Pubkey> {
+        // SAFETY: `bytes` length is validated in `try_from_bytes`.
         unsafe {
             if *self.raw.add(1) == 0 {
                 Option::None

+ 4 - 0
p-token/src/processor/shared/approve.rs

@@ -45,6 +45,8 @@ pub fn process_approve(
 
     // Validates source account.
 
+    // SAFETY: single mutable borrow to `source_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let source_account =
         unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
 
@@ -57,6 +59,8 @@ pub fn process_approve(
             return Err(TokenError::MintMismatch.into());
         }
 
+        // SAFETY: single immutable borrow of `mint_info` account data and
+        // `load` validates that the mint is initialized.
         let mint = unsafe { load::<Mint>(mint_info.borrow_data_unchecked())? };
 
         if expected_decimals != mint.decimals {

+ 4 - 0
p-token/src/processor/shared/burn.rs

@@ -16,6 +16,8 @@ pub fn process_burn(
         return Err(ProgramError::NotEnoughAccountKeys);
     };
 
+    // SAFETY: single mutable borrow to `source_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let source_account =
         unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
 
@@ -33,6 +35,8 @@ pub fn process_burn(
         .checked_sub(amount)
         .ok_or(TokenError::InsufficientFunds)?;
 
+    // SAFETY: single mutable borrow to `mint_info` account data and
+    // `load_mut` validates that the mint is initialized.
     let mint = unsafe { load_mut::<Mint>(mint_info.borrow_mut_data_unchecked())? };
 
     if mint_info.key() != &source_account.mint {

+ 7 - 1
p-token/src/processor/shared/initialize_account.rs

@@ -42,7 +42,9 @@ pub fn process_initialize_account(
 
     let minimum_balance = 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()) };
+        // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are
+        // checked by `from_account_info_unchecked`.
+        let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? };
         rent.minimum_balance(new_account_info_data_len)
     } else {
         Rent::get()?.minimum_balance(new_account_info_data_len)
@@ -52,6 +54,7 @@ pub fn process_initialize_account(
 
     // Initialize the account.
 
+    // SAFETY: single mutable borrow of the 'new_account_info' account data.
     let account =
         unsafe { load_mut_unchecked::<Account>(new_account_info.borrow_mut_data_unchecked())? };
 
@@ -66,6 +69,8 @@ pub fn process_initialize_account(
     if !is_native_mint {
         check_account_owner(mint_info)?;
 
+        // SAFETY: single immutable borrow of `mint_info` account data and
+        // `load` validates that the mint is initialized.
         let _ = unsafe {
             load::<Mint>(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)?
         };
@@ -78,6 +83,7 @@ pub fn process_initialize_account(
     if is_native_mint {
         account.set_native(true);
         account.set_native_amount(minimum_balance);
+        // SAFETY: single mutable borrow to `new_account_info` lamports.
         unsafe {
             account.set_amount(
                 new_account_info

+ 4 - 1
p-token/src/processor/shared/initialize_multisig.rs

@@ -32,12 +32,15 @@ pub fn process_initialize_multisig(
     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()) };
+        // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are
+        // checked by `from_account_info_unchecked`.
+        let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? };
         rent.is_exempt(multisig_info.lamports(), multisig_info_data_len)
     } else {
         Rent::get()?.is_exempt(multisig_info.lamports(), multisig_info_data_len)
     };
 
+    // SAFETY: single mutable borrow to `multisig_info` account data.
     let multisig =
         unsafe { load_mut_unchecked::<Multisig>(multisig_info.borrow_mut_data_unchecked())? };
 

+ 4 - 0
p-token/src/processor/shared/mint_to.rs

@@ -18,6 +18,8 @@ pub fn process_mint_to(
 
     // Validates the destination account.
 
+    // SAFETY: single mutable borrow to `destination_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let destination_account =
         unsafe { load_mut::<Account>(destination_account_info.borrow_mut_data_unchecked())? };
 
@@ -33,6 +35,8 @@ pub fn process_mint_to(
         return Err(TokenError::MintMismatch.into());
     }
 
+    // SAFETY: single mutable borrow to `mint_info` account data and
+    // `load_mut` validates that the mint is initialized.
     let mint = unsafe { load_mut::<Mint>(mint_info.borrow_mut_data_unchecked())? };
 
     if let Some(expected_decimals) = expected_decimals {

+ 5 - 1
p-token/src/processor/shared/toggle_account_state.rs

@@ -12,10 +12,12 @@ pub fn process_toggle_account_state(accounts: &[AccountInfo], freeze: bool) -> P
         return Err(ProgramError::NotEnoughAccountKeys);
     };
 
+    // SAFETY: single mutable borrow to `source_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let source_account =
         unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
 
-    if freeze && source_account.is_frozen() || !freeze && !source_account.is_frozen() {
+    if freeze == source_account.is_frozen() {
         return Err(TokenError::InvalidState.into());
     }
     if source_account.is_native() {
@@ -25,6 +27,8 @@ pub fn process_toggle_account_state(accounts: &[AccountInfo], freeze: bool) -> P
         return Err(TokenError::MintMismatch.into());
     }
 
+    // SAFETY: single immutable borrow of `mint_info` account data and
+    // `load` validates that the mint is initialized.
     let mint = unsafe { load::<Mint>(mint_info.borrow_data_unchecked())? };
 
     match mint.freeze_authority() {

+ 45 - 19
p-token/src/processor/shared/transfer.rs

@@ -1,7 +1,7 @@
 use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult};
 use token_interface::{
     error::TokenError,
-    state::{account::Account, load, load_mut, mint::Mint},
+    state::{account::Account, load, load_mut, load_mut_unchecked, mint::Mint},
 };
 
 use crate::processor::{check_account_owner, validate_owner};
@@ -51,27 +51,49 @@ pub fn process_transfer(
 
     // Validates source and destination accounts.
 
+    // SAFETY: single mutable borrow to `source_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let source_account =
         unsafe { load_mut::<Account>(source_account_info.borrow_mut_data_unchecked())? };
 
-    let destination_account =
-        unsafe { load_mut::<Account>(destination_account_info.borrow_mut_data_unchecked())? };
-
-    if source_account.is_frozen() || destination_account.is_frozen() {
-        return Err(TokenError::AccountFrozen.into());
-    }
+    // Comparing whether the AccountInfo's "point" to the same account or
+    // not - this is a faster comparison since it just checks the internal
+    // raw pointer.
+    let self_transfer = source_account_info == destination_account_info;
 
     // Implicitly validates that the account has enough tokens by calculating the
     // remaining amount - the amount is only updated on the account if the transfer
     // is successful.
-    let remaining_amount = source_account
-        .amount()
-        .checked_sub(amount)
-        .ok_or(TokenError::InsufficientFunds)?;
+    let remaining_amount = if self_transfer {
+        if source_account.is_frozen() {
+            return Err(TokenError::AccountFrozen.into());
+        }
 
-    if source_account.mint != destination_account.mint {
-        return Err(TokenError::MintMismatch.into());
-    }
+        source_account
+            .amount()
+            .checked_sub(amount)
+            .ok_or(TokenError::InsufficientFunds)?
+    } else {
+        // SAFETY: scoped immutable borrow to `destination_account_info` account data and
+        // `load` validates that the account is initialized.
+        let destination_account =
+            unsafe { load::<Account>(destination_account_info.borrow_data_unchecked())? };
+
+        if source_account.is_frozen() || destination_account.is_frozen() {
+            return Err(TokenError::AccountFrozen.into());
+        }
+
+        let remaining_amount = source_account
+            .amount()
+            .checked_sub(amount)
+            .ok_or(TokenError::InsufficientFunds)?;
+
+        if source_account.mint != destination_account.mint {
+            return Err(TokenError::MintMismatch.into());
+        }
+
+        remaining_amount
+    };
 
     // Validates the mint information.
 
@@ -80,6 +102,8 @@ pub fn process_transfer(
             return Err(TokenError::MintMismatch.into());
         }
 
+        // SAFETY: single immutable borrow of `mint_info` account data and
+        // `load` validates that the mint is initialized.
         let mint = unsafe { load::<Mint>(mint_info.borrow_data_unchecked())? };
 
         if decimals != mint.decimals {
@@ -87,11 +111,6 @@ pub fn process_transfer(
         }
     }
 
-    // Comparing whether the AccountInfo's "point" to the same account or
-    // not - this is a faster comparison since it just checks the internal
-    // raw pointer.
-    let self_transfer = source_account_info == destination_account_info;
-
     // Validates the authority (delegate or owner).
 
     if source_account.delegate() == Some(authority_info.key()) {
@@ -123,6 +142,11 @@ pub fn process_transfer(
 
         source_account.set_amount(remaining_amount);
 
+        // SAFETY: single mutable borrow to `destination_account_info` account data; the account
+        // is guaranteed to be initialized and different than `source_account_info`.
+        let destination_account = unsafe {
+            load_mut_unchecked::<Account>(destination_account_info.borrow_mut_data_unchecked())?
+        };
         let destination_amount = destination_account
             .amount()
             .checked_add(amount)
@@ -130,11 +154,13 @@ pub fn process_transfer(
         destination_account.set_amount(destination_amount);
 
         if source_account.is_native() {
+            // SAFETY: single mutable borrow to `source_account_info` lamports.
             let source_lamports = unsafe { source_account_info.borrow_mut_lamports_unchecked() };
             *source_lamports = source_lamports
                 .checked_sub(amount)
                 .ok_or(TokenError::Overflow)?;
 
+            // SAFETY: single mutable borrow to `destination_account_info` lamports.
             let destination_lamports =
                 unsafe { destination_account_info.borrow_mut_lamports_unchecked() };
             *destination_lamports = destination_lamports

+ 2 - 0
p-token/src/processor/sync_native.rs

@@ -12,6 +12,8 @@ pub fn process_sync_native(accounts: &[AccountInfo]) -> ProgramResult {
 
     check_account_owner(native_account_info)?;
 
+    // SAFETY: single mutable borrow to `native_account_info` account data and
+    // `load_mut` validates that the account is initialized.
     let native_account =
         unsafe { load_mut::<Account>(native_account_info.borrow_mut_data_unchecked())? };
 

+ 15 - 15
p-token/src/processor/transfer_checked.rs

@@ -7,20 +7,20 @@ pub fn process_transfer_checked(
     accounts: &[AccountInfo],
     instruction_data: &[u8],
 ) -> ProgramResult {
-    let (amount, decimals) = instruction_data.split_at(core::mem::size_of::<u64>());
-    let amount = u64::from_le_bytes(
-        amount
-            .try_into()
-            .map_err(|_error| ProgramError::InvalidInstructionData)?,
-    );
+    // expected u64 (8) + u8 (1)
+    let (amount, decimals) = if instruction_data.len() == 9 {
+        let (amount, decimals) = instruction_data.split_at(core::mem::size_of::<u64>());
+        (
+            u64::from_le_bytes(
+                amount
+                    .try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            ),
+            decimals.first(),
+        )
+    } else {
+        return Err(ProgramError::InvalidInstructionData);
+    };
 
-    shared::transfer::process_transfer(
-        accounts,
-        amount,
-        Some(
-            *decimals
-                .first()
-                .ok_or(ProgramError::InvalidInstructionData)?,
-        ),
-    )
+    shared::transfer::process_transfer(accounts, amount, decimals.copied())
 }

+ 2 - 1
p-token/src/processor/ui_amount_to_amount.rs

@@ -19,7 +19,8 @@ pub fn process_ui_amount_to_amount(
 
     let mint_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?;
     check_account_owner(mint_info)?;
-    // SAFETY: there is a single borrow to the `Mint` account.
+    // SAFETY: single immutable borrow to `mint_info` account data and
+    // `load` validates that the mint is initialized.
     let mint = unsafe {
         load::<Mint>(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)?
     };

+ 0 - 1
p-token/tests/amount_to_ui_amount.rs

@@ -6,7 +6,6 @@ use setup::{mint, TOKEN_PROGRAM_ID};
 use solana_program_test::{tokio, ProgramTest};
 use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction};
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn amount_to_ui_amount(token_program: Pubkey) {

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

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn approve(token_program: Pubkey) {

+ 0 - 1
p-token/tests/approve_checked.rs

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn approve_checked(token_program: Pubkey) {

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

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn burn(token_program: Pubkey) {

+ 0 - 1
p-token/tests/burn_checked.rs

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn burn_checked(token_program: Pubkey) {

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

@@ -10,7 +10,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn close_account(token_program: Pubkey) {

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

@@ -12,7 +12,6 @@ use solana_sdk::{
 };
 use spl_token::state::AccountState;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn freeze_account(token_program: Pubkey) {

+ 0 - 1
p-token/tests/initialize_account.rs

@@ -12,7 +12,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_account(token_program: Pubkey) {

+ 0 - 1
p-token/tests/initialize_account2.rs

@@ -12,7 +12,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_account2(token_program: Pubkey) {

+ 0 - 1
p-token/tests/initialize_account3.rs

@@ -12,7 +12,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_account3(token_program: Pubkey) {

+ 0 - 1
p-token/tests/initialize_mint.rs

@@ -16,7 +16,6 @@ use solana_sdk::{
 };
 use token_interface::state::mint::Mint;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_mint(token_program: Pubkey) {

+ 0 - 1
p-token/tests/initialize_mint2.rs

@@ -16,7 +16,6 @@ use solana_sdk::{
 };
 use token_interface::state::mint::Mint;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_mint2(token_program: Pubkey) {

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

@@ -13,7 +13,6 @@ use solana_sdk::{
 };
 use spl_token::state::Multisig;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_multisig(token_program: Pubkey) {

+ 0 - 1
p-token/tests/initialize_multisig2.rs

@@ -13,7 +13,6 @@ use solana_sdk::{
 };
 use spl_token::state::Multisig;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn initialize_multisig2(token_program: Pubkey) {

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

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn mint_to(token_program: Pubkey) {

+ 0 - 1
p-token/tests/mint_to_checked.rs

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn mint_to_checked(token_program: Pubkey) {

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

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn revoke(token_program: Pubkey) {

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

@@ -13,7 +13,6 @@ use solana_sdk::{
 };
 use spl_token::instruction::AuthorityType;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn set_authority(token_program: Pubkey) {

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

@@ -12,7 +12,6 @@ use solana_sdk::{
 };
 use spl_token::state::AccountState;
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn thaw_account(token_program: Pubkey) {

+ 0 - 1
p-token/tests/transfer.rs

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn transfer(token_program: Pubkey) {

+ 0 - 1
p-token/tests/transfer_checked.rs

@@ -11,7 +11,6 @@ use solana_sdk::{
     transaction::Transaction,
 };
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn transfer_checked(token_program: Pubkey) {

+ 0 - 1
p-token/tests/ui_amount_to_amount.rs

@@ -6,7 +6,6 @@ use setup::{mint, TOKEN_PROGRAM_ID};
 use solana_program_test::{tokio, ProgramTest};
 use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction};
 
-#[test_case::test_case(spl_token::ID ; "spl-token")]
 #[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")]
 #[tokio::test]
 async fn ui_amount_to_amount(token_program: Pubkey) {