From 38d8634353e5eeb8c015d364df0eaa39f5c48b05 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 15 Oct 2025 02:52:34 +0100 Subject: [PATCH 1/9] refactor: convert to lib, switch program id --- Cargo.lock | 2 +- p-interface/src/lib.rs | 2 +- p-interface/src/state/mod.rs | 13 +++++++------ p-token/Cargo.toml | 6 +++--- p-token/src/lib.rs | 5 +---- p-token/src/processor/mod.rs | 4 ++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aaf232da..08afc074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3070,7 +3070,7 @@ dependencies = [ [[package]] name = "pinocchio-token-program" -version = "0.0.0" +version = "0.1.0" dependencies = [ "agave-feature-set", "assert_matches", diff --git a/p-interface/src/lib.rs b/p-interface/src/lib.rs index ab8c3ecb..68da0366 100644 --- a/p-interface/src/lib.rs +++ b/p-interface/src/lib.rs @@ -6,7 +6,7 @@ pub mod native_mint; pub mod state; pub mod program { - pinocchio_pubkey::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + pinocchio_pubkey::declare_id!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); } /// A "dummy" function with a hint to the compiler that it is unlikely to be diff --git a/p-interface/src/state/mod.rs b/p-interface/src/state/mod.rs index 79505eed..96a9521b 100644 --- a/p-interface/src/state/mod.rs +++ b/p-interface/src/state/mod.rs @@ -1,4 +1,4 @@ -use pinocchio::program_error::ProgramError; +use pinocchio::{msg, program_error::ProgramError}; pub mod account; pub mod account_state; @@ -35,7 +35,7 @@ pub trait Initializable { /// The caller must ensure that `bytes` contains a valid representation of `T`. #[inline(always)] pub unsafe fn load(bytes: &[u8]) -> Result<&T, ProgramError> { - load_unchecked(bytes).and_then(|t: &T| { + load_unchecked(&bytes).and_then(|t: &T| { // checks if the data is initialized if t.is_initialized()? { Ok(t) @@ -54,10 +54,11 @@ pub unsafe fn load(bytes: &[u8]) -> Result<&T, /// The caller must ensure that `bytes` contains a valid representation of `T`. #[inline(always)] pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, ProgramError> { - if bytes.len() != T::LEN { + if bytes.len() != T::LEN && bytes.len() != 260 { + msg!("invalid account data len"); return Err(ProgramError::InvalidAccountData); } - Ok(&*(bytes.as_ptr() as *const T)) + Ok(&*(bytes[..165].as_ptr() as *const T)) } /// Return a mutable reference for an initialized `T` from the given bytes. @@ -90,8 +91,8 @@ pub unsafe fn load_mut( pub unsafe fn load_mut_unchecked( bytes: &mut [u8], ) -> Result<&mut T, ProgramError> { - if bytes.len() != T::LEN { + if bytes.len() != T::LEN && bytes.len() != 260 { return Err(ProgramError::InvalidAccountData); } - Ok(&mut *(bytes.as_mut_ptr() as *mut T)) + Ok(&mut *(bytes[..165].as_mut_ptr() as *mut T)) } diff --git a/p-token/Cargo.toml b/p-token/Cargo.toml index bab4a551..120f0a8f 100644 --- a/p-token/Cargo.toml +++ b/p-token/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinocchio-token-program" -version = "0.0.0" +version = "0.1.0" description = "A pinocchio-based Token (aka 'p-token') program" authors = { workspace = true} repository = { workspace = true} @@ -9,13 +9,13 @@ edition = { workspace = true} readme = "./README.md" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] [features] logging = [] [dependencies] -pinocchio = { workspace = true } +pinocchio = "0.9" pinocchio-log = { version = "0.5", default-features = false } pinocchio-token-interface = { version = "^0", path = "../p-interface" } diff --git a/p-token/src/lib.rs b/p-token/src/lib.rs index 0bd44399..043d44ce 100644 --- a/p-token/src/lib.rs +++ b/p-token/src/lib.rs @@ -1,6 +1,3 @@ //! Another ERC20-like Token program for the Solana blockchain. -#![no_std] - -mod entrypoint; -mod processor; +pub mod processor; diff --git a/p-token/src/processor/mod.rs b/p-token/src/processor/mod.rs index 155e329f..85ae15c1 100644 --- a/p-token/src/processor/mod.rs +++ b/p-token/src/processor/mod.rs @@ -19,7 +19,7 @@ use { pub mod amount_to_ui_amount; pub mod approve; pub mod approve_checked; -pub mod batch; +// pub mod batch; pub mod burn; pub mod burn_checked; pub mod close_account; @@ -48,7 +48,7 @@ pub mod shared; pub use { amount_to_ui_amount::process_amount_to_ui_amount, approve::process_approve, - approve_checked::process_approve_checked, batch::process_batch, burn::process_burn, + approve_checked::process_approve_checked, burn::process_burn, burn_checked::process_burn_checked, close_account::process_close_account, freeze_account::process_freeze_account, get_account_data_size::process_get_account_data_size, initialize_account::process_initialize_account, From 5d2768b98075ba03dfc5d6e6dd8567ba065c84ba Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 29 Nov 2025 13:18:21 +0000 Subject: [PATCH 2/9] extensions compat --- p-interface/src/state/mod.rs | 13 +++------- p-token/src/processor/shared/transfer.rs | 31 +++++++++++++---------- p-token/src/processor/transfer.rs | 8 ++++-- p-token/src/processor/transfer_checked.rs | 3 ++- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/p-interface/src/state/mod.rs b/p-interface/src/state/mod.rs index 96a9521b..23d68b4d 100644 --- a/p-interface/src/state/mod.rs +++ b/p-interface/src/state/mod.rs @@ -1,4 +1,4 @@ -use pinocchio::{msg, program_error::ProgramError}; +use pinocchio::program_error::ProgramError; pub mod account; pub mod account_state; @@ -54,11 +54,7 @@ pub unsafe fn load(bytes: &[u8]) -> Result<&T, /// The caller must ensure that `bytes` contains a valid representation of `T`. #[inline(always)] pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, ProgramError> { - if bytes.len() != T::LEN && bytes.len() != 260 { - msg!("invalid account data len"); - return Err(ProgramError::InvalidAccountData); - } - Ok(&*(bytes[..165].as_ptr() as *const T)) + Ok(&*(bytes[..T::LEN].as_ptr() as *const T)) } /// Return a mutable reference for an initialized `T` from the given bytes. @@ -91,8 +87,5 @@ pub unsafe fn load_mut( pub unsafe fn load_mut_unchecked( bytes: &mut [u8], ) -> Result<&mut T, ProgramError> { - if bytes.len() != T::LEN && bytes.len() != 260 { - return Err(ProgramError::InvalidAccountData); - } - Ok(&mut *(bytes[..165].as_mut_ptr() as *mut T)) + Ok(&mut *(bytes[..T::LEN].as_mut_ptr() as *mut T)) } diff --git a/p-token/src/processor/shared/transfer.rs b/p-token/src/processor/shared/transfer.rs index 13351e06..1974de39 100644 --- a/p-token/src/processor/shared/transfer.rs +++ b/p-token/src/processor/shared/transfer.rs @@ -14,6 +14,7 @@ pub fn process_transfer( accounts: &[AccountInfo], amount: u64, expected_decimals: Option, + signer_is_validated: bool, ) -> ProgramResult { // Accounts expected depend on whether we have the mint `decimals` or not; when // we have the mint `decimals`, we expect the mint account to be present. @@ -127,25 +128,27 @@ pub fn process_transfer( // Validates the authority (delegate or owner). - if source_account.delegate() == Some(authority_info.key()) { - // SAFETY: `authority_info` is not currently borrowed. - unsafe { validate_owner(authority_info.key(), authority_info, remaining)? }; + if !signer_is_validated { + if source_account.delegate() == Some(authority_info.key()) { + // SAFETY: `authority_info` is not currently borrowed. + unsafe { validate_owner(authority_info.key(), authority_info, remaining)? }; - let delegated_amount = source_account - .delegated_amount() - .checked_sub(amount) - .ok_or(TokenError::InsufficientFunds)?; + let delegated_amount = source_account + .delegated_amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; - if !self_transfer { - source_account.set_delegated_amount(delegated_amount); + if !self_transfer { + source_account.set_delegated_amount(delegated_amount); - if delegated_amount == 0 { - source_account.clear_delegate(); + if delegated_amount == 0 { + source_account.clear_delegate(); + } } + } else { + // SAFETY: `authority_info` is not currently borrowed. + unsafe { validate_owner(&source_account.owner, authority_info, remaining)? }; } - } else { - // SAFETY: `authority_info` is not currently borrowed. - unsafe { validate_owner(&source_account.owner, authority_info, remaining)? }; } if self_transfer || amount == 0 { diff --git a/p-token/src/processor/transfer.rs b/p-token/src/processor/transfer.rs index 2d75354e..46d77f66 100644 --- a/p-token/src/processor/transfer.rs +++ b/p-token/src/processor/transfer.rs @@ -4,8 +4,12 @@ use { }; #[inline(always)] -pub fn process_transfer(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { +pub fn process_transfer( + accounts: &[AccountInfo], + instruction_data: &[u8], + signer_is_validated: bool, +) -> ProgramResult { let amount = unpack_amount(instruction_data)?; - shared::transfer::process_transfer(accounts, amount, None) + shared::transfer::process_transfer(accounts, amount, None, signer_is_validated) } diff --git a/p-token/src/processor/transfer_checked.rs b/p-token/src/processor/transfer_checked.rs index 9a1a895d..32dd2a1a 100644 --- a/p-token/src/processor/transfer_checked.rs +++ b/p-token/src/processor/transfer_checked.rs @@ -7,8 +7,9 @@ use { pub fn process_transfer_checked( accounts: &[AccountInfo], instruction_data: &[u8], + signer_is_validated: bool, ) -> ProgramResult { let (amount, decimals) = unpack_amount_and_decimals(instruction_data)?; - shared::transfer::process_transfer(accounts, amount, Some(decimals)) + shared::transfer::process_transfer(accounts, amount, Some(decimals), signer_is_validated) } From 5825b1dd933c2ab77b4c4a954aa20bb8a23b31f8 Mon Sep 17 00:00:00 2001 From: ananas-block Date: Sat, 20 Dec 2025 15:04:19 +0100 Subject: [PATCH 3/9] chore: make unpack_amount_and_decimals public, add lenght checks --- p-interface/src/state/mod.rs | 6 ++++++ p-token/src/processor/mod.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/p-interface/src/state/mod.rs b/p-interface/src/state/mod.rs index 23d68b4d..dd5f0e2e 100644 --- a/p-interface/src/state/mod.rs +++ b/p-interface/src/state/mod.rs @@ -54,6 +54,9 @@ pub unsafe fn load(bytes: &[u8]) -> Result<&T, /// The caller must ensure that `bytes` contains a valid representation of `T`. #[inline(always)] pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, ProgramError> { + if bytes.len() < T::LEN { + return Err(ProgramError::InvalidAccountData); + } Ok(&*(bytes[..T::LEN].as_ptr() as *const T)) } @@ -87,5 +90,8 @@ pub unsafe fn load_mut( pub unsafe fn load_mut_unchecked( bytes: &mut [u8], ) -> Result<&mut T, ProgramError> { + if bytes.len() < T::LEN { + return Err(ProgramError::InvalidAccountData); + } Ok(&mut *(bytes[..T::LEN].as_mut_ptr() as *mut T)) } diff --git a/p-token/src/processor/mod.rs b/p-token/src/processor/mod.rs index 85ae15c1..c87d7694 100644 --- a/p-token/src/processor/mod.rs +++ b/p-token/src/processor/mod.rs @@ -203,7 +203,7 @@ const fn unpack_amount(instruction_data: &[u8]) -> Result { /// Unpacks a `u64` amount and an optional `u8` from the instruction data. #[inline(always)] -const fn unpack_amount_and_decimals(instruction_data: &[u8]) -> Result<(u64, u8), TokenError> { +pub const fn unpack_amount_and_decimals(instruction_data: &[u8]) -> Result<(u64, u8), TokenError> { // expected u64 (8) + u8 (1) if instruction_data.len() >= 9 { let (amount, decimals) = instruction_data.split_at(U64_BYTES); From 3c5db4f20dd57db4cd84c086ce4954d04e30665a Mon Sep 17 00:00:00 2001 From: ananas-block Date: Tue, 30 Dec 2025 00:47:59 +0100 Subject: [PATCH 4/9] feat: add AccountType validation for extended accounts Validate AccountType discriminator at byte 165 for accounts larger than 165 bytes to prevent type confusion between Mint and Account types. - Add ACCOUNT_TYPE_OFFSET, ACCOUNT_TYPE_MINT, ACCOUNT_TYPE_TOKEN_ACCOUNT constants - Add ACCOUNT_TYPE const to Transmutable trait - Check byte[165] in load_unchecked and load_mut_unchecked when size > 165 - Account uses ACCOUNT_TYPE = 2, Mint uses ACCOUNT_TYPE = 1 --- p-interface/src/state/account.rs | 1 + p-interface/src/state/mint.rs | 1 + p-interface/src/state/mod.rs | 27 +++++++++++++++++++++++++++ p-interface/src/state/multisig.rs | 2 ++ 4 files changed, 31 insertions(+) diff --git a/p-interface/src/state/account.rs b/p-interface/src/state/account.rs index a9ade92a..8a47b9c0 100644 --- a/p-interface/src/state/account.rs +++ b/p-interface/src/state/account.rs @@ -154,6 +154,7 @@ impl Account { unsafe impl Transmutable for Account { const LEN: usize = core::mem::size_of::(); + const ACCOUNT_TYPE: u8 = super::ACCOUNT_TYPE_TOKEN_ACCOUNT; } impl Initializable for Account { diff --git a/p-interface/src/state/mint.rs b/p-interface/src/state/mint.rs index 655173e6..2bb57084 100644 --- a/p-interface/src/state/mint.rs +++ b/p-interface/src/state/mint.rs @@ -87,6 +87,7 @@ impl Mint { unsafe impl Transmutable for Mint { /// The length of the `Mint` account data. const LEN: usize = core::mem::size_of::(); + const ACCOUNT_TYPE: u8 = super::ACCOUNT_TYPE_MINT; } impl Initializable for Mint { diff --git a/p-interface/src/state/mod.rs b/p-interface/src/state/mod.rs index dd5f0e2e..3b062a7d 100644 --- a/p-interface/src/state/mod.rs +++ b/p-interface/src/state/mod.rs @@ -8,6 +8,12 @@ pub mod multisig; /// Type alias for fields represented as `COption`. pub type COption = ([u8; 4], T); +/// AccountType discriminator for Mint accounts (at byte 82 for extended mints). +pub const ACCOUNT_TYPE_MINT: u8 = 1; + +/// AccountType discriminator for Token accounts (at byte 165 for extended accounts). +pub const ACCOUNT_TYPE_TOKEN_ACCOUNT: u8 = 2; + /// Marker trait for types that can be cast from a raw pointer. /// /// # Safety @@ -20,6 +26,10 @@ pub unsafe trait Transmutable { /// /// This must be equal to the size of each individual field in the type. const LEN: usize; + + /// The expected AccountType discriminator value at byte offset `LEN` for extended accounts. + /// Used to validate account type when `bytes.len() > LEN`. + const ACCOUNT_TYPE: u8; } /// Trait to represent a type that can be initialized. @@ -45,10 +55,16 @@ pub unsafe fn load(bytes: &[u8]) -> Result<&T, }) } +/// Byte offset of AccountType discriminator in extended accounts. +pub const ACCOUNT_TYPE_OFFSET: usize = 165; + /// Return a `T` reference from the given bytes. /// /// This function does not check if the data is initialized. /// +/// When `bytes.len() > 165` (extended account), validates the AccountType +/// discriminator at byte 165 matches `T::ACCOUNT_TYPE`. +/// /// # Safety /// /// The caller must ensure that `bytes` contains a valid representation of `T`. @@ -57,6 +73,10 @@ pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, Progra if bytes.len() < T::LEN { return Err(ProgramError::InvalidAccountData); } + // For extended accounts (>165 bytes), validate AccountType at byte 165 + if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] != T::ACCOUNT_TYPE { + return Err(ProgramError::InvalidAccountData); + } Ok(&*(bytes[..T::LEN].as_ptr() as *const T)) } @@ -83,6 +103,9 @@ pub unsafe fn load_mut( /// /// This function does not check if the data is initialized. /// +/// When `bytes.len() > 165` (extended account), validates the AccountType +/// discriminator at byte 165 matches `T::ACCOUNT_TYPE`. +/// /// # Safety /// /// The caller must ensure that `bytes` contains a valid representation of `T`. @@ -93,5 +116,9 @@ pub unsafe fn load_mut_unchecked( if bytes.len() < T::LEN { return Err(ProgramError::InvalidAccountData); } + // For extended accounts (>165 bytes), validate AccountType at byte 165 + if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] != T::ACCOUNT_TYPE { + return Err(ProgramError::InvalidAccountData); + } Ok(&mut *(bytes[..T::LEN].as_mut_ptr() as *mut T)) } diff --git a/p-interface/src/state/multisig.rs b/p-interface/src/state/multisig.rs index 7e453fd2..00eb0200 100644 --- a/p-interface/src/state/multisig.rs +++ b/p-interface/src/state/multisig.rs @@ -41,6 +41,8 @@ impl Multisig { unsafe impl Transmutable for Multisig { /// The length of the `Multisig` account data. const LEN: usize = core::mem::size_of::(); + /// Multisig not supported for extended accounts. + const ACCOUNT_TYPE: u8 = 0; } impl Initializable for Multisig { From 5a786730d0c051281232c07102dfa79c69999a2f Mon Sep 17 00:00:00 2001 From: ananas-block Date: Tue, 30 Dec 2025 00:48:09 +0100 Subject: [PATCH 5/9] docs: update README with branch changes and unsupported features Document signer_is_validated parameter, AccountType validation, library conversion, and note that multisig is not supported. --- p-token/README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 4 deletions(-) diff --git a/p-token/README.md b/p-token/README.md index f2c4f515..517d825b 100644 --- a/p-token/README.md +++ b/p-token/README.md @@ -1,17 +1,126 @@ -# `p-token` +# p-token -A `pinocchio`-based Token program. +A `pinocchio`-based Token program with library support for external program integration. ## Overview -`p-token` is a reimplementation of the SPL Token program, one of the most popular programs on Solana, using [`pinocchio`](https://github.com/anza-xyz/pinocchio). The purpose is to have an implementation that optimizes the compute units, while being fully compatible with the original implementation — i.e., support the exact same instruction and account layouts as SPL Token, byte for byte. +`p-token` is a reimplementation of the SPL Token program using [`pinocchio`](https://github.com/anza-xyz/pinocchio). Optimizes compute units while maintaining full compatibility with SPL Token instruction and account layouts. ## Features -- `no_std` crate - Same instruction and account layout as SPL Token - Minimal CU usage +- Library mode for external program integration +- Token-2022 extension account compatibility +## Changes (jorrit/refactor branch) + +### 1. Transfer Authority Pre-validation Parameter + +**path:** `src/processor/shared/transfer.rs:14-17, 127-152` + +Added `signer_is_validated: bool` parameter to transfer functions. + +**behavior:** +- `false` (default): Full authority validation via `validate_owner()` - checks owner/delegate and signer status +- `true`: Authority validation skipped - caller has already validated authority externally + +**affected functions:** +- `shared::transfer::process_transfer(accounts, amount, expected_decimals, signer_is_validated)` +- `transfer::process_transfer(accounts, instruction_data, signer_is_validated)` +- `transfer_checked::process_transfer_checked(accounts, instruction_data, signer_is_validated)` + +**usage:** External programs (e.g., compressed-token) validate permanent delegate authority before calling p-token, then pass `signer_is_validated=true` to avoid redundant validation. + +**security requirement:** Caller MUST verify `authority.is_signer()` before setting `signer_is_validated=true`. Failure to do so enables unauthorized transfers. + +### 2. Account Size Validation with AccountType Check + +**path:** `../p-interface/src/state/mod.rs:58-81, 102-124` + +`load_unchecked` and `load_mut_unchecked` now accept accounts larger than `T::LEN` and validate AccountType. + +**before:** +```rust +if bytes.len() != T::LEN { + return Err(ProgramError::InvalidAccountData); +} +Ok(&*(bytes.as_ptr() as *const T)) +``` + +**after:** +```rust +if bytes.len() < T::LEN { + return Err(ProgramError::InvalidAccountData); +} +// For extended accounts (>165 bytes), validate AccountType at byte 165 +if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] != T::ACCOUNT_TYPE { + return Err(ProgramError::InvalidAccountData); +} +Ok(&*(bytes[..T::LEN].as_ptr() as *const T)) +``` + +**purpose:** Enables processing of Token-2022 extension accounts (165+ bytes) while preventing type confusion. + +**security:** +- Minimum size validated to prevent buffer underflow +- AccountType discriminator at byte 165 validated for extended accounts: + - `ACCOUNT_TYPE_MINT = 1` for Mint accounts + - `ACCOUNT_TYPE_TOKEN_ACCOUNT = 2` for Token accounts +- Prevents loading a Mint as Account or vice versa when extensions present + +### 3. Library Conversion + +**path:** `Cargo.toml`, `src/lib.rs` + +**changes:** +- `crate-type = ["cdylib", "lib"]` - enables both program deployment and library import +- `pub mod processor` - exports processor functions for external use +- Removed `#![no_std]` from lib.rs root (processor module retains no_std behavior) + +**purpose:** Allows external programs to import and call p-token functions directly instead of via CPI. + +### 4. Public API Addition + +**path:** `src/processor/mod.rs:206` + +`unpack_amount_and_decimals` made public: +```rust +pub const fn unpack_amount_and_decimals(instruction_data: &[u8]) -> Result<(u64, u8), TokenError> +``` + +**purpose:** Enables external programs to parse transfer_checked instruction data. + +### 5. Program ID Change + +**path:** `../p-interface/src/lib.rs:9` + +Program ID updated from `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` to `cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m`. + +**purpose:** Distinguishes p-token/ctoken deployments from standard SPL Token program. + +## Integration Pattern + +External programs using p-token as a library: + +1. Validate authority externally (e.g., permanent delegate check with `is_signer()`) +2. Call p-token transfer with appropriate `signer_is_validated` flag: + +```rust +use pinocchio_token_program::processor::shared::transfer::process_transfer; + +// After validating permanent delegate is signer +let signer_is_validated = validate_permanent_delegate(mint_checks, authority)?; + +// Call p-token - authority validation skipped if signer_is_validated=true +process_transfer(accounts, amount, expected_decimals, signer_is_validated)?; +``` + +## Unsupported Features + +**Multisig:** Multisig accounts are not supported. The AccountType validation at byte 165 does not handle Multisig accounts (355 bytes), which have no discriminator at that offset. Token-2022 handles this via size exclusion (`!= 355`), but this implementation assumes no multisig usage. + +**Batch module:** Commented out in mod.rs. Batch instruction processing (discriminator 255) currently disabled. ## License From 1bf7a9e525e753c3eb7bbf9971a26efbc23e5c73 Mon Sep 17 00:00:00 2001 From: ananas-block Date: Tue, 30 Dec 2025 23:48:33 +0100 Subject: [PATCH 6/9] feat: disable wrapped SOL (native mint) transfers Return NativeNotSupported error for native account transfers. Lamport transfer logic commented out for reference. --- p-token/README.md | 2 ++ p-token/src/processor/shared/transfer.rs | 30 ++++++++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/p-token/README.md b/p-token/README.md index 517d825b..8c922998 100644 --- a/p-token/README.md +++ b/p-token/README.md @@ -122,6 +122,8 @@ process_transfer(accounts, amount, expected_decimals, signer_is_validated)?; **Batch module:** Commented out in mod.rs. Batch instruction processing (discriminator 255) currently disabled. +**Wrapped SOL (native mint):** Transfers of wrapped SOL accounts are not supported. The native mint lamport transfer logic in `src/processor/shared/transfer.rs` is commented out, and transfers will return `NativeNotSupported` error. + ## License The code is licensed under the [Apache License Version 2.0](LICENSE) diff --git a/p-token/src/processor/shared/transfer.rs b/p-token/src/processor/shared/transfer.rs index 1974de39..632870d7 100644 --- a/p-token/src/processor/shared/transfer.rs +++ b/p-token/src/processor/shared/transfer.rs @@ -173,20 +173,24 @@ pub fn process_transfer( destination_account.set_amount(destination_account.amount() + 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() }; - // Note: The amount of a source token account is already validated and the - // `lamports` on the account is always greater than `amount`. - *source_lamports -= amount; - - // SAFETY: single mutable borrow to `destination_account_info` lamports; the - // account is already validated to be different from - // `source_account_info`. - let destination_lamports = - unsafe { destination_account_info.borrow_mut_lamports_unchecked() }; - // Note: The total lamports supply is bound to `u64::MAX`. - *destination_lamports += amount; + return Err(TokenError::NativeNotSupported.into()); } + + // 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() }; + // // Note: The amount of a source token account is already validated and the + // // `lamports` on the account is always greater than `amount`. + // *source_lamports -= amount; + // + // // SAFETY: single mutable borrow to `destination_account_info` lamports; the + // // account is already validated to be different from + // // `source_account_info`. + // let destination_lamports = + // unsafe { destination_account_info.borrow_mut_lamports_unchecked() }; + // // Note: The total lamports supply is bound to `u64::MAX`. + // *destination_lamports += amount; + // } } Ok(()) From 0c55d185aaede4e83039ebfeb7a2caa000253450 Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 22:43:08 +0000 Subject: [PATCH 7/9] chore: re-export error module from pinocchio-token-interface --- p-token/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/p-token/src/lib.rs b/p-token/src/lib.rs index 043d44ce..f2df83ff 100644 --- a/p-token/src/lib.rs +++ b/p-token/src/lib.rs @@ -1,3 +1,6 @@ //! Another ERC20-like Token program for the Solana blockchain. pub mod processor; + +// Re-export from pinocchio-token-interface +pub use pinocchio_token_interface::error; From 9ea04560a039d1a44f0411b5eaa7c0b79ed575ab Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 9 Jan 2026 18:59:48 +0000 Subject: [PATCH 8/9] fix: add AccountType validation to prevent type confusion in load functions Fixes vulnerability where Token account (165 bytes) could be loaded as Mint (82 bytes) because AccountType check was only performed for extended accounts (>165 bytes). Now accepts only: - Exact length match (bytes.len() == T::LEN) for standard accounts - Extended accounts (>165 bytes) with matching AccountType discriminator Adds unit tests covering all validation scenarios. --- p-interface/src/state/mod.rs | 36 ++--- p-interface/tests/load_validation.rs | 202 +++++++++++++++++++++++++++ p-token/README.md | 30 ++-- 3 files changed, 241 insertions(+), 27 deletions(-) create mode 100644 p-interface/tests/load_validation.rs diff --git a/p-interface/src/state/mod.rs b/p-interface/src/state/mod.rs index 3b062a7d..70c21f3d 100644 --- a/p-interface/src/state/mod.rs +++ b/p-interface/src/state/mod.rs @@ -62,22 +62,24 @@ pub const ACCOUNT_TYPE_OFFSET: usize = 165; /// /// This function does not check if the data is initialized. /// -/// When `bytes.len() > 165` (extended account), validates the AccountType -/// discriminator at byte 165 matches `T::ACCOUNT_TYPE`. +/// Accepts: +/// - Exact length match: `bytes.len() == T::LEN` (standard account) +/// - Extended account: `bytes.len() > ACCOUNT_TYPE_OFFSET` with matching AccountType at byte 165 +/// +/// Rejects everything else (too short, ambiguous size, wrong AccountType). /// /// # Safety /// /// The caller must ensure that `bytes` contains a valid representation of `T`. #[inline(always)] pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, ProgramError> { - if bytes.len() < T::LEN { - return Err(ProgramError::InvalidAccountData); + if bytes.len() == T::LEN { + return Ok(&*(bytes[..T::LEN].as_ptr() as *const T)); } - // For extended accounts (>165 bytes), validate AccountType at byte 165 - if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] != T::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); + if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] == T::ACCOUNT_TYPE { + return Ok(&*(bytes[..T::LEN].as_ptr() as *const T)); } - Ok(&*(bytes[..T::LEN].as_ptr() as *const T)) + Err(ProgramError::InvalidAccountData) } /// Return a mutable reference for an initialized `T` from the given bytes. @@ -103,8 +105,11 @@ pub unsafe fn load_mut( /// /// This function does not check if the data is initialized. /// -/// When `bytes.len() > 165` (extended account), validates the AccountType -/// discriminator at byte 165 matches `T::ACCOUNT_TYPE`. +/// Accepts: +/// - Exact length match: `bytes.len() == T::LEN` (standard account) +/// - Extended account: `bytes.len() > ACCOUNT_TYPE_OFFSET` with matching AccountType at byte 165 +/// +/// Rejects everything else (too short, ambiguous size, wrong AccountType). /// /// # Safety /// @@ -113,12 +118,11 @@ pub unsafe fn load_mut( pub unsafe fn load_mut_unchecked( bytes: &mut [u8], ) -> Result<&mut T, ProgramError> { - if bytes.len() < T::LEN { - return Err(ProgramError::InvalidAccountData); + if bytes.len() == T::LEN { + return Ok(&mut *(bytes[..T::LEN].as_mut_ptr() as *mut T)); } - // For extended accounts (>165 bytes), validate AccountType at byte 165 - if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] != T::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); + if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] == T::ACCOUNT_TYPE { + return Ok(&mut *(bytes[..T::LEN].as_mut_ptr() as *mut T)); } - Ok(&mut *(bytes[..T::LEN].as_mut_ptr() as *mut T)) + Err(ProgramError::InvalidAccountData) } diff --git a/p-interface/tests/load_validation.rs b/p-interface/tests/load_validation.rs new file mode 100644 index 00000000..719b8f35 --- /dev/null +++ b/p-interface/tests/load_validation.rs @@ -0,0 +1,202 @@ +//! Tests for load_unchecked and load_mut_unchecked AccountType validation. +//! +//! These tests verify that the account loading functions properly validate +//! account types to prevent type confusion vulnerabilities (e.g., loading +//! a Token account as a Mint). + +use pinocchio_token_interface::state::{ + account::Account, load_mut_unchecked, load_unchecked, mint::Mint, Transmutable, + ACCOUNT_TYPE_MINT, ACCOUNT_TYPE_OFFSET, ACCOUNT_TYPE_TOKEN_ACCOUNT, +}; + +// ============================================================================ +// load_unchecked tests +// ============================================================================ + +#[test] +fn test_load_unchecked_exact_length_mint() { + // Standard Mint (82 bytes) should load as Mint + let data = vec![0u8; Mint::LEN]; + let result = unsafe { load_unchecked::(&data) }; + assert!(result.is_ok()); +} + +#[test] +fn test_load_unchecked_exact_length_account() { + // Standard Account (165 bytes) should load as Account + let data = vec![0u8; Account::LEN]; + let result = unsafe { load_unchecked::(&data) }; + assert!(result.is_ok()); +} + +#[test] +fn test_load_unchecked_rejects_token_as_mint() { + // CRITICAL: 165-byte Token account data should NOT load as Mint + // This is the main vulnerability being fixed + let data = vec![0u8; Account::LEN]; // 165 bytes + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject Token account loaded as Mint" + ); +} + +#[test] +fn test_load_unchecked_rejects_ambiguous_size_as_mint() { + // Data between Mint::LEN (82) and ACCOUNT_TYPE_OFFSET (165) should be rejected + // because we can't verify the type without a discriminator + let data = vec![0u8; 100]; // 82 < 100 < 165 + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject ambiguous size without discriminator" + ); +} + +#[test] +fn test_load_unchecked_rejects_extended_token_as_mint() { + // Extended Token account (>165 bytes, type=2) should NOT load as Mint + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN_ACCOUNT; // type = 2 + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject extended Token loaded as Mint" + ); +} + +#[test] +fn test_load_unchecked_rejects_extended_mint_as_account() { + // Extended Mint (>165 bytes, type=1) should NOT load as Account + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT; // type = 1 + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject extended Mint loaded as Account" + ); +} + +#[test] +fn test_load_unchecked_accepts_extended_mint() { + // Extended Mint (>165 bytes, type=1) should load as Mint + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT; // type = 1 + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_ok(), + "Should accept extended Mint with correct type" + ); +} + +#[test] +fn test_load_unchecked_accepts_extended_account() { + // Extended Account (>165 bytes, type=2) should load as Account + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN_ACCOUNT; // type = 2 + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_ok(), + "Should accept extended Account with correct type" + ); +} + +#[test] +fn test_load_unchecked_rejects_too_short_for_mint() { + let data = vec![0u8; 50]; // Too short for Mint (82 bytes) + let result = unsafe { load_unchecked::(&data) }; + assert!(result.is_err(), "Should reject data shorter than Mint::LEN"); +} + +#[test] +fn test_load_unchecked_rejects_too_short_for_account() { + let data = vec![0u8; 100]; // Too short for Account (165 bytes) + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject data shorter than Account::LEN" + ); +} + +// ============================================================================ +// load_mut_unchecked tests +// ============================================================================ + +#[test] +fn test_load_mut_unchecked_exact_length_mint() { + let mut data = vec![0u8; Mint::LEN]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!(result.is_ok()); +} + +#[test] +fn test_load_mut_unchecked_exact_length_account() { + let mut data = vec![0u8; Account::LEN]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!(result.is_ok()); +} + +#[test] +fn test_load_mut_unchecked_rejects_token_as_mint() { + // CRITICAL: Same vulnerability check for mutable reference + let mut data = vec![0u8; Account::LEN]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject Token account loaded as mutable Mint" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_ambiguous_size_as_mint() { + let mut data = vec![0u8; 100]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject ambiguous size without discriminator" + ); +} + +#[test] +fn test_load_mut_unchecked_accepts_extended_mint() { + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_ok(), + "Should accept extended Mint with correct type" + ); +} + +#[test] +fn test_load_mut_unchecked_accepts_extended_account() { + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN_ACCOUNT; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_ok(), + "Should accept extended Account with correct type" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_extended_token_as_mint() { + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN_ACCOUNT; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject extended Token loaded as mutable Mint" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_extended_mint_as_account() { + let mut data = vec![0u8; 200]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject extended Mint loaded as mutable Account" + ); +} diff --git a/p-token/README.md b/p-token/README.md index 8c922998..05c19fa7 100644 --- a/p-token/README.md +++ b/p-token/README.md @@ -36,9 +36,9 @@ Added `signer_is_validated: bool` parameter to transfer functions. ### 2. Account Size Validation with AccountType Check -**path:** `../p-interface/src/state/mod.rs:58-81, 102-124` +**path:** `../p-interface/src/state/mod.rs:61-83, 104-128` -`load_unchecked` and `load_mut_unchecked` now accept accounts larger than `T::LEN` and validate AccountType. +`load_unchecked` and `load_mut_unchecked` validate account data with strict type checking. **before:** ```rust @@ -50,24 +50,32 @@ Ok(&*(bytes.as_ptr() as *const T)) **after:** ```rust -if bytes.len() < T::LEN { - return Err(ProgramError::InvalidAccountData); +if bytes.len() == T::LEN { + return Ok(&*(bytes[..T::LEN].as_ptr() as *const T)); } -// For extended accounts (>165 bytes), validate AccountType at byte 165 -if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] != T::ACCOUNT_TYPE { - return Err(ProgramError::InvalidAccountData); +if bytes.len() > ACCOUNT_TYPE_OFFSET && bytes[ACCOUNT_TYPE_OFFSET] == T::ACCOUNT_TYPE { + return Ok(&*(bytes[..T::LEN].as_ptr() as *const T)); } -Ok(&*(bytes[..T::LEN].as_ptr() as *const T)) +Err(ProgramError::InvalidAccountData) ``` -**purpose:** Enables processing of Token-2022 extension accounts (165+ bytes) while preventing type confusion. +**accepts:** +- Exact length match: `bytes.len() == T::LEN` (standard accounts) +- Extended accounts: `bytes.len() > 165` with matching AccountType at byte 165 + +**rejects:** +- Too short: `bytes.len() < T::LEN` +- Ambiguous size: `T::LEN < bytes.len() <= 165` (cannot verify type) +- Wrong AccountType: extended account with mismatched discriminator + +**purpose:** Enables processing of Token-2022 extension accounts while preventing type confusion attacks. **security:** -- Minimum size validated to prevent buffer underflow - AccountType discriminator at byte 165 validated for extended accounts: - `ACCOUNT_TYPE_MINT = 1` for Mint accounts - `ACCOUNT_TYPE_TOKEN_ACCOUNT = 2` for Token accounts -- Prevents loading a Mint as Account or vice versa when extensions present +- Prevents loading a Token account (165 bytes) as Mint (82 bytes) by rejecting ambiguous sizes +- Prevents loading extended accounts with wrong AccountType ### 3. Library Conversion From f7bee9bbc8039c224a88ea76e9ae2edd78e0f9c3 Mon Sep 17 00:00:00 2001 From: ananas Date: Mon, 12 Jan 2026 14:37:22 +0000 Subject: [PATCH 9/9] test: account load --- Cargo.lock | 1 + p-interface/Cargo.toml | 1 + p-interface/src/state/mod.rs | 6 +- p-interface/src/state/multisig.rs | 4 +- p-interface/tests/load_validation.rs | 260 +++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08afc074..b45c9778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3064,6 +3064,7 @@ version = "0.0.0" dependencies = [ "pinocchio", "pinocchio-pubkey", + "rand 0.8.5", "strum 0.27.1", "strum_macros 0.27.1", ] diff --git a/p-interface/Cargo.toml b/p-interface/Cargo.toml index 20a49ed3..f51dc551 100644 --- a/p-interface/Cargo.toml +++ b/p-interface/Cargo.toml @@ -16,5 +16,6 @@ pinocchio = { workspace = true } pinocchio-pubkey = "0.3" [dev-dependencies] +rand = "0.8" strum = "0.27" strum_macros = "0.27" diff --git a/p-interface/src/state/mod.rs b/p-interface/src/state/mod.rs index 70c21f3d..28f6a23a 100644 --- a/p-interface/src/state/mod.rs +++ b/p-interface/src/state/mod.rs @@ -8,10 +8,12 @@ pub mod multisig; /// Type alias for fields represented as `COption`. pub type COption = ([u8; 4], T); -/// AccountType discriminator for Mint accounts (at byte 82 for extended mints). +/// AccountType discriminator for Mint accounts. +/// Validation checks byte 165 (ACCOUNT_TYPE_OFFSET) for all extended accounts. pub const ACCOUNT_TYPE_MINT: u8 = 1; -/// AccountType discriminator for Token accounts (at byte 165 for extended accounts). +/// AccountType discriminator for Token accounts. +/// Validation checks byte 165 (ACCOUNT_TYPE_OFFSET) for all extended accounts. pub const ACCOUNT_TYPE_TOKEN_ACCOUNT: u8 = 2; /// Marker trait for types that can be cast from a raw pointer. diff --git a/p-interface/src/state/multisig.rs b/p-interface/src/state/multisig.rs index 00eb0200..17efbff0 100644 --- a/p-interface/src/state/multisig.rs +++ b/p-interface/src/state/multisig.rs @@ -41,8 +41,8 @@ impl Multisig { unsafe impl Transmutable for Multisig { /// The length of the `Multisig` account data. const LEN: usize = core::mem::size_of::(); - /// Multisig not supported for extended accounts. - const ACCOUNT_TYPE: u8 = 0; + /// Multisig not supported for extended accounts (255 = invalid/unused type). + const ACCOUNT_TYPE: u8 = 255; } impl Initializable for Multisig { diff --git a/p-interface/tests/load_validation.rs b/p-interface/tests/load_validation.rs index 719b8f35..ce2a90c3 100644 --- a/p-interface/tests/load_validation.rs +++ b/p-interface/tests/load_validation.rs @@ -118,6 +118,114 @@ fn test_load_unchecked_rejects_too_short_for_account() { ); } +#[test] +fn test_load_unchecked_rejects_size_83_as_mint() { + // Boundary test: size 83 (just above Mint::LEN of 82) + let data = vec![0u8; 83]; + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject size just above Mint::LEN without discriminator" + ); +} + +#[test] +fn test_load_unchecked_rejects_size_164_as_mint() { + // Boundary test: size 164 (one below ACCOUNT_TYPE_OFFSET of 165) + let data = vec![0u8; 164]; + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject size just below ACCOUNT_TYPE_OFFSET" + ); +} + +#[test] +fn test_load_unchecked_rejects_mint_as_account() { + // Boundary test: 82-byte Mint data should NOT load as Account (165 bytes) + let data = vec![0u8; Mint::LEN]; // 82 bytes + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject Mint-sized data loaded as Account" + ); +} + +#[test] +fn test_load_unchecked_rejects_empty_data() { + let data = vec![0u8; 0]; + let result = unsafe { load_unchecked::(&data) }; + assert!(result.is_err(), "Should reject empty data"); +} + +#[test] +fn test_load_unchecked_rejects_size_81_as_mint() { + // Boundary test: size 81 (just below Mint::LEN of 82) + let data = vec![0u8; 81]; + let result = unsafe { load_unchecked::(&data) }; + assert!(result.is_err(), "Should reject size just below Mint::LEN"); +} + +#[test] +fn test_load_unchecked_rejects_size_164_as_account() { + // Boundary test: size 164 (just below Account::LEN of 165) + let data = vec![0u8; 164]; + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_err(), + "Should reject size just below Account::LEN" + ); +} + +#[test] +fn test_load_unchecked_accepts_size_166_as_mint() { + // Boundary test: minimal extended account (166 bytes, just above ACCOUNT_TYPE_OFFSET) + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT; + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_ok(), + "Should accept minimal extended Mint (166 bytes)" + ); +} + +#[test] +fn test_load_unchecked_accepts_size_166_as_account() { + // Boundary test: minimal extended account (166 bytes, just above ACCOUNT_TYPE_OFFSET) + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN_ACCOUNT; + let result = unsafe { load_unchecked::(&data) }; + assert!( + result.is_ok(), + "Should accept minimal extended Account (166 bytes)" + ); +} + +#[test] +fn test_load_unchecked_rejects_size_166_with_type_0() { + // Size 166 with type=0 should be rejected for both Mint and Account + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = 0; + let result_mint = unsafe { load_unchecked::(&data) }; + let result_account = unsafe { load_unchecked::(&data) }; + assert!(result_mint.is_err(), "Should reject type=0 as Mint"); + assert!(result_account.is_err(), "Should reject type=0 as Account"); +} + +#[test] +fn test_load_unchecked_rejects_size_166_with_invalid_type() { + // Size 166 with invalid type values should be rejected + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = 255; // Invalid type + let result_mint = unsafe { load_unchecked::(&data) }; + let result_account = unsafe { load_unchecked::(&data) }; + assert!(result_mint.is_err(), "Should reject invalid type as Mint"); + assert!( + result_account.is_err(), + "Should reject invalid type as Account" + ); +} + // ============================================================================ // load_mut_unchecked tests // ============================================================================ @@ -200,3 +308,155 @@ fn test_load_mut_unchecked_rejects_extended_mint_as_account() { "Should reject extended Mint loaded as mutable Account" ); } + +#[test] +fn test_load_mut_unchecked_rejects_size_83_as_mint() { + // Boundary test: size 83 (just above Mint::LEN of 82) + let mut data = vec![0u8; 83]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject size just above Mint::LEN without discriminator" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_size_164_as_mint() { + // Boundary test: size 164 (one below ACCOUNT_TYPE_OFFSET of 165) + let mut data = vec![0u8; 164]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject size just below ACCOUNT_TYPE_OFFSET" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_mint_as_account() { + // Boundary test: 82-byte Mint data should NOT load as Account (165 bytes) + let mut data = vec![0u8; Mint::LEN]; // 82 bytes + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject Mint-sized data loaded as mutable Account" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_empty_data() { + let mut data = vec![0u8; 0]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!(result.is_err(), "Should reject empty data"); +} + +#[test] +fn test_load_mut_unchecked_rejects_size_81_as_mint() { + let mut data = vec![0u8; 81]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!(result.is_err(), "Should reject size just below Mint::LEN"); +} + +#[test] +fn test_load_mut_unchecked_rejects_size_164_as_account() { + let mut data = vec![0u8; 164]; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_err(), + "Should reject size just below Account::LEN" + ); +} + +#[test] +fn test_load_mut_unchecked_accepts_size_166_as_mint() { + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_MINT; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_ok(), + "Should accept minimal extended Mint (166 bytes)" + ); +} + +#[test] +fn test_load_mut_unchecked_accepts_size_166_as_account() { + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = ACCOUNT_TYPE_TOKEN_ACCOUNT; + let result = unsafe { load_mut_unchecked::(&mut data) }; + assert!( + result.is_ok(), + "Should accept minimal extended Account (166 bytes)" + ); +} + +#[test] +fn test_load_mut_unchecked_rejects_size_166_with_type_0() { + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = 0; + let result_mint = unsafe { load_mut_unchecked::(&mut data) }; + let mut data2 = vec![0u8; 166]; + data2[ACCOUNT_TYPE_OFFSET] = 0; + let result_account = unsafe { load_mut_unchecked::(&mut data2) }; + assert!(result_mint.is_err(), "Should reject type=0 as Mint"); + assert!(result_account.is_err(), "Should reject type=0 as Account"); +} + +#[test] +fn test_load_mut_unchecked_rejects_size_166_with_invalid_type() { + let mut data = vec![0u8; 166]; + data[ACCOUNT_TYPE_OFFSET] = 255; + let result_mint = unsafe { load_mut_unchecked::(&mut data) }; + let mut data2 = vec![0u8; 166]; + data2[ACCOUNT_TYPE_OFFSET] = 255; + let result_account = unsafe { load_mut_unchecked::(&mut data2) }; + assert!(result_mint.is_err(), "Should reject invalid type as Mint"); + assert!( + result_account.is_err(), + "Should reject invalid type as Account" + ); +} + +// ============================================================================ +// Randomized fuzz tests +// ============================================================================ + +#[test] +fn test_load_unchecked_fuzz_rejects_invalid_random_data() { + use rand::Rng; + let mut rng = rand::thread_rng(); + + for i in 0..1_000_000 { + // Random size between 0 and 500 + let size = rng.gen_range(0..500); + let mut data: Vec = (0..size).map(|_| rng.gen()).collect(); + + // If size > ACCOUNT_TYPE_OFFSET, ensure byte[165] is never 1 or 2 + if size > ACCOUNT_TYPE_OFFSET { + while data[ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT + || data[ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_TOKEN_ACCOUNT + { + data[ACCOUNT_TYPE_OFFSET] = rng.gen(); + } + } + + // Skip exact length matches (these are valid cases) + if size == Mint::LEN || size == Account::LEN { + continue; + } + + let result_mint = unsafe { load_unchecked::(&data) }; + let result_account = unsafe { load_unchecked::(&data) }; + + assert!( + result_mint.is_err(), + "Iteration {}: Should reject random data (size={}) as Mint", + i, + size + ); + assert!( + result_account.is_err(), + "Iteration {}: Should reject random data (size={}) as Account", + i, + size + ); + } +}