From aedc1416ba6e70761de30eb664c22a2f67bcce7f Mon Sep 17 00:00:00 2001 From: Stevengre Date: Thu, 8 Jan 2026 14:59:52 +0800 Subject: [PATCH 1/7] fix(spl): align test_process_mint_to_checked's assumption to p-token --- .../src/entrypoint-runtime-verification.rs | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/program/src/entrypoint-runtime-verification.rs b/program/src/entrypoint-runtime-verification.rs index 800ff2c3..fb6e5aa9 100644 --- a/program/src/entrypoint-runtime-verification.rs +++ b/program/src/entrypoint-runtime-verification.rs @@ -3933,24 +3933,42 @@ fn test_process_mint_to_checked( cheatcode_is_spl_account(&accounts[2]); //-Initial State----------------------------------------------------------- - let initial_supply = get_mint(&accounts[0]).supply(); - let initial_amount = get_account(&accounts[1]).amount(); - let mint_initialised = get_mint(&accounts[0]).is_initialized(); - let dst_initialised = get_account(&accounts[1]).is_initialized(); - let dst_init_state = get_account(&accounts[1]).account_state(); + let mint_old = get_mint(&accounts[0]); + let dst_old = get_account(&accounts[1]); + let initial_supply = mint_old.supply(); + let initial_amount = dst_old.amount(); + let mint_initialised = mint_old.is_initialized(); + let dst_initialised = dst_old.is_initialized(); + let dst_init_state = dst_old.account_state(); let maybe_multisig_is_initialised = None; + #[cfg(feature = "assumptions")] + { + // Do not execute if adding to the account balance would overflow. + // shared::mint_to.rs,L68 is based on the assumption that initial_amount <= + // mint.supply and therefore cannot overflow because the minting itself + // would already error out. + let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; + if initial_amount.checked_add(amount).is_none() { + return Err(ProgramError::Custom(99)); + } + } + //-Process Instruction----------------------------------------------------- let result = Processor::process(program_id, accounts, instruction_data_with_discriminator); //-Assert Postconditions--------------------------------------------------- + let mint_new = get_mint(&accounts[0]); + let dst_new = get_account(&accounts[1]); + if instruction_data.len() < 9 { assert_eq!(result, Err(ProgramError::Custom(12))); return result; } else if accounts.len() < 3 { assert_eq!(result, Err(ProgramError::NotEnoughAccountKeys)); return result; - } else if accounts[1].data_len() != Account::LEN { // TODO Daniel: is it possible for something to be provided that has the same len but is not an account? + } else if accounts[1].data_len() != Account::LEN { + // TODO Daniel: is it possible for something to be provided that has the same len but is not an account? assert_eq!(result, Err(ProgramError::InvalidAccountData)); return result; } else if dst_initialised.is_err() { @@ -3959,13 +3977,14 @@ fn test_process_mint_to_checked( } else if !dst_initialised.unwrap() { assert_eq!(result, Err(ProgramError::UninitializedAccount)); return result; - } else if dst_init_state.unwrap() == AccountState::Frozen { // unwrap must succeed due to dst_initialised not being err + } else if dst_init_state.unwrap() == AccountState::Frozen { + // unwrap must succeed due to dst_initialised not being err assert_eq!(result, Err(ProgramError::Custom(17))); return result; - } else if get_account(&accounts[1]).is_native() { + } else if dst_new.is_native() { assert_eq!(result, Err(ProgramError::Custom(10))); return result; - } else if accounts[0].key != &get_account(&accounts[1]).mint() { + } else if accounts[0].key != &dst_new.mint() { assert_eq!(result, Err(ProgramError::Custom(3))); return result; } else if accounts[0].data_len() != Mint::LEN { @@ -3978,13 +3997,13 @@ fn test_process_mint_to_checked( } else if !mint_initialised.unwrap() { assert_eq!(result, Err(ProgramError::UninitializedAccount)); return result; - } else if instruction_data[8] != get_mint(&accounts[0]).decimals() { + } else if instruction_data[8] != mint_new.decimals() { assert_eq!(result, Err(ProgramError::Custom(18))); return result; } else { - if get_mint(&accounts[0]).mint_authority().is_some() { + if mint_new.mint_authority().is_some() { inner_test_validate_owner( - get_mint(&accounts[0]).mint_authority().unwrap(), + mint_new.mint_authority().unwrap(), &accounts[2], &accounts[3..], maybe_multisig_is_initialised.clone(), @@ -4008,8 +4027,8 @@ fn test_process_mint_to_checked( return result; } - assert_eq!(get_mint(&accounts[0]).supply(), initial_supply + amount); - assert_eq!(get_account(&accounts[1]).amount(), initial_amount + amount); + assert_eq!(mint_new.supply(), initial_supply + amount); + assert_eq!(dst_new.amount(), initial_amount + amount); assert!(result.is_ok()); } From 0dde19a2745abd4dabba56d85990041a157ee807 Mon Sep 17 00:00:00 2001 From: Stevengre Date: Fri, 9 Jan 2026 10:25:02 +0800 Subject: [PATCH 2/7] make the diff less --- .../src/entrypoint-runtime-verification.rs | 40 +++++++---- .../src/entrypoint-runtime-verification.rs | 72 ++++++++++++------- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/p-token/src/entrypoint-runtime-verification.rs b/p-token/src/entrypoint-runtime-verification.rs index 37d51af2..b247ebcd 100644 --- a/p-token/src/entrypoint-runtime-verification.rs +++ b/p-token/src/entrypoint-runtime-verification.rs @@ -21,10 +21,22 @@ use { }, pinocchio_token_interface::{ error::TokenError, - state::{Initializable, Transmutable}, + program::ID as PROGRAM_ID, + state::{account_state::{self, AccountState}, Initializable, Transmutable}, }, }; +/// Macros to abstract API differences between spl-token and p-token. +/// spl-token AccountInfo has fields (.key, .owner), p-token has methods (.key(), .owner()). +/// spl-token wrappers have methods (.mint(), .decimals()), p-token has fields (.mint, .decimals). +macro_rules! key { ($acc:expr) => { $acc.key() }; } +macro_rules! owner { ($acc:expr) => { $acc.owner() }; } +macro_rules! mint { ($acc:expr) => { $acc.mint }; } +macro_rules! decimals { ($m:expr) => { $m.decimals }; } +/// Cheatcode macros to abstract naming differences. +macro_rules! cheatcode_mint { ($acc:expr) => { cheatcode_is_mint($acc) }; } +macro_rules! cheatcode_account { ($acc:expr) => { cheatcode_is_account($acc) }; } + program_entrypoint!(process_instruction); // Do not allocate memory. no_allocator!(); @@ -3956,11 +3968,9 @@ fn test_process_mint_to_checked( accounts: &[AccountInfo; 3], instruction_data: &[u8; 9], ) -> ProgramResult { - use pinocchio_token_interface::state::account_state; - - cheatcode_is_mint(&accounts[0]); - cheatcode_is_account(&accounts[1]); - cheatcode_is_account(&accounts[2]); // Excluding the multisig case + cheatcode_mint!(&accounts[0]); + cheatcode_account!(&accounts[1]); + cheatcode_account!(&accounts[2]); // Excluding the multisig case //-Initial State----------------------------------------------------------- let mint_old = get_mint(&accounts[0]); @@ -3988,6 +3998,9 @@ fn test_process_mint_to_checked( let result = process_mint_to_checked(accounts, instruction_data); //-Assert Postconditions--------------------------------------------------- + let mint_new = get_mint(&accounts[0]); + let dst_new = get_account(&accounts[1]); + if instruction_data.len() < 9 { assert_eq!(result, Err(ProgramError::Custom(12))); return result; @@ -4005,14 +4018,14 @@ fn test_process_mint_to_checked( } else if !dst_initialised.unwrap() { assert_eq!(result, Err(ProgramError::UninitializedAccount)); return result; - } else if dst_init_state.unwrap() == account_state::AccountState::Frozen { + } else if dst_init_state.unwrap() == AccountState::Frozen { // unwrap must succeed due to dst_initialised not being err assert_eq!(result, Err(ProgramError::Custom(17))); return result; - } else if get_account(&accounts[1]).is_native() { + } else if dst_new.is_native() { assert_eq!(result, Err(ProgramError::Custom(10))); return result; - } else if accounts[0].key() != &get_account(&accounts[1]).mint { + } else if key!(accounts[0]) != &mint!(dst_new) { assert_eq!(result, Err(ProgramError::Custom(3))); return result; } else if accounts[0].data_len() != Mint::LEN { @@ -4025,11 +4038,10 @@ fn test_process_mint_to_checked( } else if !mint_initialised.unwrap() { assert_eq!(result, Err(ProgramError::UninitializedAccount)); return result; - } else if instruction_data[8] != get_mint(&accounts[0]).decimals { + } else if instruction_data[8] != decimals!(mint_new) { assert_eq!(result, Err(ProgramError::Custom(18))); return result; } else { - let mint_new = get_mint(&accounts[0]); if mint_new.mint_authority().is_some() { // Validate Owner inner_test_validate_owner( @@ -4046,10 +4058,10 @@ fn test_process_mint_to_checked( let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; - if amount == 0 && accounts[0].owner() != &pinocchio_token_interface::program::ID { + if amount == 0 && owner!(accounts[0]) != &PROGRAM_ID { assert_eq!(result, Err(ProgramError::IncorrectProgramId)); return result; - } else if amount == 0 && accounts[1].owner() != &pinocchio_token_interface::program::ID { + } else if amount == 0 && owner!(accounts[1]) != &PROGRAM_ID { assert_eq!(result, Err(ProgramError::IncorrectProgramId)); return result; } else if amount != 0 && initial_supply.checked_add(amount).is_none() { @@ -4058,7 +4070,7 @@ fn test_process_mint_to_checked( } assert_eq!(mint_new.supply(), initial_supply + amount); - assert_eq!(get_account(&accounts[1]).amount(), initial_amount + amount); + assert_eq!(dst_new.amount(), initial_amount + amount); assert!(result.is_ok()); } diff --git a/program/src/entrypoint-runtime-verification.rs b/program/src/entrypoint-runtime-verification.rs index fb6e5aa9..f7a30d3d 100644 --- a/program/src/entrypoint-runtime-verification.rs +++ b/program/src/entrypoint-runtime-verification.rs @@ -1,7 +1,7 @@ //! Program entrypoint for runtime verification proofs of original spl token implmentation use { - crate::{processor::Processor, state::{Account, AccountState, Mint, Multisig}}, + crate::{processor::Processor, state::{Account, AccountState, Mint, Multisig}, ID as PROGRAM_ID}, solana_account_info::AccountInfo, solana_program_error::{ProgramError, ProgramResult}, solana_program_pack::Pack, @@ -85,6 +85,30 @@ macro_rules! assert_pubkey_from_slice { }}; } +/// Macro to constrain discriminator and program_id, then strip the discriminator byte. +/// Returns (instr_with_disc, instruction_data) where instr_with_disc includes discriminator. +/// This is spl-token specific - p-token harnesses receive instruction data without discriminator. +macro_rules! constrain_and_strip { + ($disc:expr, $program_id:expr, $instruction_data:expr, $size:ty) => {{ + unsafe { assume($disc == $instruction_data[0]); } + unsafe { assume($program_id == &crate::id()); } + let instr_with_disc = &$instruction_data.clone(); + let stripped: &$size = $instruction_data.last_chunk().unwrap(); + (instr_with_disc, stripped) + }}; +} + +/// Macros to abstract API differences between spl-token and p-token. +/// spl-token AccountInfo has fields (.key, .owner), p-token has methods (.key(), .owner()). +/// spl-token wrappers have methods (.mint(), .decimals()), p-token has fields (.mint, .decimals). +macro_rules! key { ($acc:expr) => { $acc.key }; } +macro_rules! owner { ($acc:expr) => { $acc.owner }; } +macro_rules! mint { ($acc:expr) => { $acc.mint() }; } +macro_rules! decimals { ($m:expr) => { $m.decimals() }; } +/// Cheatcode macros to abstract naming differences. +macro_rules! cheatcode_mint { ($acc:expr) => { cheatcode_is_spl_mint($acc) }; } +macro_rules! cheatcode_account { ($acc:expr) => { cheatcode_is_spl_account($acc) }; } + /// A wrapper struct as middleware so that the same functions called /// on the p-token Account are called on the spl Account. However, /// this means that fields have to be accessed through functions. @@ -3918,19 +3942,11 @@ fn test_process_mint_to_checked( accounts: &[AccountInfo; 3], instruction_data: &[u8; 10], ) -> ProgramResult { - use spl_token_interface::state::AccountState; - - // Constrain discriminator and program id - unsafe { assume(14 == instruction_data[0]); } - unsafe { assume(program_id == &crate::id()); } - - // Strip discriminator so instruction data is equivalent p-token harness - let instruction_data_with_discriminator = &instruction_data.clone(); - let instruction_data: &[u8; 9] = instruction_data.last_chunk().unwrap(); + let (instr_with_disc, instruction_data) = constrain_and_strip!(14, program_id, instruction_data, [u8; 9]); - cheatcode_is_spl_mint(&accounts[0]); - cheatcode_is_spl_account(&accounts[1]); - cheatcode_is_spl_account(&accounts[2]); + cheatcode_mint!(&accounts[0]); + cheatcode_account!(&accounts[1]); + cheatcode_account!(&accounts[2]); // Excluding the multisig case //-Initial State----------------------------------------------------------- let mint_old = get_mint(&accounts[0]); @@ -3940,7 +3956,7 @@ fn test_process_mint_to_checked( let mint_initialised = mint_old.is_initialized(); let dst_initialised = dst_old.is_initialized(); let dst_init_state = dst_old.account_state(); - let maybe_multisig_is_initialised = None; + let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account #[cfg(feature = "assumptions")] { @@ -3955,7 +3971,7 @@ fn test_process_mint_to_checked( } //-Process Instruction----------------------------------------------------- - let result = Processor::process(program_id, accounts, instruction_data_with_discriminator); + let result = Processor::process(program_id, accounts, instr_with_disc); //-Assert Postconditions--------------------------------------------------- let mint_new = get_mint(&accounts[0]); @@ -3968,7 +3984,8 @@ fn test_process_mint_to_checked( assert_eq!(result, Err(ProgramError::NotEnoughAccountKeys)); return result; } else if accounts[1].data_len() != Account::LEN { - // TODO Daniel: is it possible for something to be provided that has the same len but is not an account? + // TODO Daniel: is it possible for something to be provided that has the same + // len but is not an account? assert_eq!(result, Err(ProgramError::InvalidAccountData)); return result; } else if dst_initialised.is_err() { @@ -3984,7 +4001,7 @@ fn test_process_mint_to_checked( } else if dst_new.is_native() { assert_eq!(result, Err(ProgramError::Custom(10))); return result; - } else if accounts[0].key != &dst_new.mint() { + } else if key!(accounts[0]) != &mint!(dst_new) { assert_eq!(result, Err(ProgramError::Custom(3))); return result; } else if accounts[0].data_len() != Mint::LEN { @@ -3997,16 +4014,17 @@ fn test_process_mint_to_checked( } else if !mint_initialised.unwrap() { assert_eq!(result, Err(ProgramError::UninitializedAccount)); return result; - } else if instruction_data[8] != mint_new.decimals() { + } else if instruction_data[8] != decimals!(mint_new) { assert_eq!(result, Err(ProgramError::Custom(18))); return result; } else { if mint_new.mint_authority().is_some() { + // Validate Owner inner_test_validate_owner( - mint_new.mint_authority().unwrap(), - &accounts[2], - &accounts[3..], - maybe_multisig_is_initialised.clone(), + mint_new.mint_authority().unwrap(), // expected_owner + &accounts[2], // owner_account_info + &accounts[3..], // tx_signers + maybe_multisig_is_initialised, result.clone(), )?; } else { @@ -4014,15 +4032,15 @@ fn test_process_mint_to_checked( return result; } - let amount = u64::from_le_bytes([instruction_data[0], instruction_data[1], instruction_data[2], instruction_data[3], instruction_data[4], instruction_data[5], instruction_data[6], instruction_data[7]]); + let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; - if amount == 0 && accounts[0].owner != &crate::id() { + if amount == 0 && owner!(accounts[0]) != &PROGRAM_ID { assert_eq!(result, Err(ProgramError::IncorrectProgramId)); return result; - } else if amount == 0 && accounts[1].owner != &crate::id() { + } else if amount == 0 && owner!(accounts[1]) != &PROGRAM_ID { assert_eq!(result, Err(ProgramError::IncorrectProgramId)); return result; - } else if amount != 0 && amount.checked_add(initial_supply).is_none() { + } else if amount != 0 && initial_supply.checked_add(amount).is_none() { assert_eq!(result, Err(ProgramError::Custom(14))); return result; } @@ -4033,7 +4051,7 @@ fn test_process_mint_to_checked( } // Ensure instruction_data was not mutated - assert_eq!(*instruction_data, instruction_data_with_discriminator[1..]); + assert_eq!(*instruction_data, instr_with_disc[1..]); result } From 241e8282afa506d04dc4b83690bf862655221011 Mon Sep 17 00:00:00 2001 From: Stevengre Date: Fri, 9 Jan 2026 10:43:32 +0800 Subject: [PATCH 3/7] npnm p-token:format --- .../src/entrypoint-runtime-verification.rs | 46 ++++++++++++++---- .../src/entrypoint-runtime-verification.rs | 47 +++++++++++++++---- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/p-token/src/entrypoint-runtime-verification.rs b/p-token/src/entrypoint-runtime-verification.rs index b247ebcd..2bcbda5d 100644 --- a/p-token/src/entrypoint-runtime-verification.rs +++ b/p-token/src/entrypoint-runtime-verification.rs @@ -22,20 +22,48 @@ use { pinocchio_token_interface::{ error::TokenError, program::ID as PROGRAM_ID, - state::{account_state::{self, AccountState}, Initializable, Transmutable}, + state::{ + account_state::{self, AccountState}, + Initializable, Transmutable, + }, }, }; /// Macros to abstract API differences between spl-token and p-token. -/// spl-token AccountInfo has fields (.key, .owner), p-token has methods (.key(), .owner()). -/// spl-token wrappers have methods (.mint(), .decimals()), p-token has fields (.mint, .decimals). -macro_rules! key { ($acc:expr) => { $acc.key() }; } -macro_rules! owner { ($acc:expr) => { $acc.owner() }; } -macro_rules! mint { ($acc:expr) => { $acc.mint }; } -macro_rules! decimals { ($m:expr) => { $m.decimals }; } +/// spl-token AccountInfo has fields (.key, .owner), p-token has methods +/// (.key(), .owner()). spl-token wrappers have methods (.mint(), .decimals()), +/// p-token has fields (.mint, .decimals). +macro_rules! key { + ($acc:expr) => { + $acc.key() + }; +} +macro_rules! owner { + ($acc:expr) => { + $acc.owner() + }; +} +macro_rules! mint { + ($acc:expr) => { + $acc.mint + }; +} +macro_rules! decimals { + ($m:expr) => { + $m.decimals + }; +} /// Cheatcode macros to abstract naming differences. -macro_rules! cheatcode_mint { ($acc:expr) => { cheatcode_is_mint($acc) }; } -macro_rules! cheatcode_account { ($acc:expr) => { cheatcode_is_account($acc) }; } +macro_rules! cheatcode_mint { + ($acc:expr) => { + cheatcode_is_mint($acc) + }; +} +macro_rules! cheatcode_account { + ($acc:expr) => { + cheatcode_is_account($acc) + }; +} program_entrypoint!(process_instruction); // Do not allocate memory. diff --git a/program/src/entrypoint-runtime-verification.rs b/program/src/entrypoint-runtime-verification.rs index f7a30d3d..43ab0c19 100644 --- a/program/src/entrypoint-runtime-verification.rs +++ b/program/src/entrypoint-runtime-verification.rs @@ -1,7 +1,11 @@ //! Program entrypoint for runtime verification proofs of original spl token implmentation use { - crate::{processor::Processor, state::{Account, AccountState, Mint, Multisig}, ID as PROGRAM_ID}, + crate::{ + processor::Processor, + state::{Account, AccountState, Mint, Multisig}, + ID as PROGRAM_ID, + }, solana_account_info::AccountInfo, solana_program_error::{ProgramError, ProgramResult}, solana_program_pack::Pack, @@ -99,15 +103,40 @@ macro_rules! constrain_and_strip { } /// Macros to abstract API differences between spl-token and p-token. -/// spl-token AccountInfo has fields (.key, .owner), p-token has methods (.key(), .owner()). -/// spl-token wrappers have methods (.mint(), .decimals()), p-token has fields (.mint, .decimals). -macro_rules! key { ($acc:expr) => { $acc.key }; } -macro_rules! owner { ($acc:expr) => { $acc.owner }; } -macro_rules! mint { ($acc:expr) => { $acc.mint() }; } -macro_rules! decimals { ($m:expr) => { $m.decimals() }; } +/// spl-token AccountInfo has fields (.key, .owner), p-token has methods +/// (.key(), .owner()). spl-token wrappers have methods (.mint(), .decimals()), +/// p-token has fields (.mint, .decimals). +macro_rules! key { + ($acc:expr) => { + $acc.key + }; +} +macro_rules! owner { + ($acc:expr) => { + $acc.owner + }; +} +macro_rules! mint { + ($acc:expr) => { + $acc.mint() + }; +} +macro_rules! decimals { + ($m:expr) => { + $m.decimals() + }; +} /// Cheatcode macros to abstract naming differences. -macro_rules! cheatcode_mint { ($acc:expr) => { cheatcode_is_spl_mint($acc) }; } -macro_rules! cheatcode_account { ($acc:expr) => { cheatcode_is_spl_account($acc) }; } +macro_rules! cheatcode_mint { + ($acc:expr) => { + cheatcode_is_spl_mint($acc) + }; +} +macro_rules! cheatcode_account { + ($acc:expr) => { + cheatcode_is_spl_account($acc) + }; +} /// A wrapper struct as middleware so that the same functions called /// on the p-token Account are called on the spl Account. However, From 8f1519e807030e0179ee49b7d6bb55c79e7bb142 Mon Sep 17 00:00:00 2001 From: Stevengre Date: Fri, 9 Jan 2026 10:51:47 +0800 Subject: [PATCH 4/7] fix error in macro_rule --- program/src/entrypoint-runtime-verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/entrypoint-runtime-verification.rs b/program/src/entrypoint-runtime-verification.rs index 43ab0c19..9b742af6 100644 --- a/program/src/entrypoint-runtime-verification.rs +++ b/program/src/entrypoint-runtime-verification.rs @@ -96,7 +96,7 @@ macro_rules! constrain_and_strip { ($disc:expr, $program_id:expr, $instruction_data:expr, $size:ty) => {{ unsafe { assume($disc == $instruction_data[0]); } unsafe { assume($program_id == &crate::id()); } - let instr_with_disc = &$instruction_data.clone(); + let instr_with_disc = $instruction_data; let stripped: &$size = $instruction_data.last_chunk().unwrap(); (instr_with_disc, stripped) }}; From d5cf3723e33fa227e3f20a6902b55f4670520854 Mon Sep 17 00:00:00 2001 From: Stevengre Date: Fri, 9 Jan 2026 11:22:11 +0800 Subject: [PATCH 5/7] fix pnpm p-token:lint --- p-token/src/entrypoint-runtime-verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p-token/src/entrypoint-runtime-verification.rs b/p-token/src/entrypoint-runtime-verification.rs index 2bcbda5d..279458c0 100644 --- a/p-token/src/entrypoint-runtime-verification.rs +++ b/p-token/src/entrypoint-runtime-verification.rs @@ -23,7 +23,7 @@ use { error::TokenError, program::ID as PROGRAM_ID, state::{ - account_state::{self, AccountState}, + account_state::AccountState, Initializable, Transmutable, }, }, From a7a7592cc8fc661eca3b019a0911cc87507389bc Mon Sep 17 00:00:00 2001 From: Stevengre Date: Fri, 9 Jan 2026 11:31:51 +0800 Subject: [PATCH 6/7] pnpm p-token:format && pnpm p-token:lint --- p-token/src/entrypoint-runtime-verification.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/p-token/src/entrypoint-runtime-verification.rs b/p-token/src/entrypoint-runtime-verification.rs index 279458c0..a75981fc 100644 --- a/p-token/src/entrypoint-runtime-verification.rs +++ b/p-token/src/entrypoint-runtime-verification.rs @@ -22,10 +22,7 @@ use { pinocchio_token_interface::{ error::TokenError, program::ID as PROGRAM_ID, - state::{ - account_state::AccountState, - Initializable, Transmutable, - }, + state::{account_state::AccountState, Initializable, Transmutable}, }, }; From 535230428baf96e06da1604a918c75474758d92c Mon Sep 17 00:00:00 2001 From: Stevengre Date: Fri, 9 Jan 2026 13:17:23 +0800 Subject: [PATCH 7/7] make spl-token and p-token use the same harness for process_mint_to_checked --- .../src/entrypoint-runtime-verification.rs | 125 +------------- .../src/entrypoint-runtime-verification.rs | 161 +++--------------- shared/test_process_mint_to_checked.rs | 117 +++++++++++++ 3 files changed, 144 insertions(+), 259 deletions(-) create mode 100644 shared/test_process_mint_to_checked.rs diff --git a/p-token/src/entrypoint-runtime-verification.rs b/p-token/src/entrypoint-runtime-verification.rs index a75981fc..d12718e2 100644 --- a/p-token/src/entrypoint-runtime-verification.rs +++ b/p-token/src/entrypoint-runtime-verification.rs @@ -61,6 +61,13 @@ macro_rules! cheatcode_account { cheatcode_is_account($acc) }; } +/// Process call macro to abstract the processor call difference. +/// p-token calls process_mint_to_checked directly. +macro_rules! call_process_mint_to_checked { + ($accounts:expr, $instruction_data:expr) => { + process_mint_to_checked($accounts, $instruction_data) + }; +} program_entrypoint!(process_instruction); // Do not allocate memory. @@ -3984,123 +3991,7 @@ fn test_process_approve_checked_multisig( result } -/// accounts[0] // Mint Info -/// accounts[1] // Destination Info -/// accounts[2] // Owner Info -/// instruction_data[0..9] // Little Endian Bytes of u64 amount, and decimals -#[inline(never)] -fn test_process_mint_to_checked( - accounts: &[AccountInfo; 3], - instruction_data: &[u8; 9], -) -> ProgramResult { - cheatcode_mint!(&accounts[0]); - cheatcode_account!(&accounts[1]); - cheatcode_account!(&accounts[2]); // Excluding the multisig case - - //-Initial State----------------------------------------------------------- - let mint_old = get_mint(&accounts[0]); - let dst_old = get_account(&accounts[1]); - let initial_supply = mint_old.supply(); - let initial_amount = dst_old.amount(); - let mint_initialised = mint_old.is_initialized(); - let dst_initialised = dst_old.is_initialized(); - let dst_init_state = dst_old.account_state(); - let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account - - #[cfg(feature = "assumptions")] - { - // Do not execute if adding to the account balance would overflow. - // shared::mint_to.rs,L68 is based on the assumption that initial_amount <= - // mint.supply and therefore cannot overflow because the minting itself - // would already error out. - let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; - if initial_amount.checked_add(amount).is_none() { - return Err(ProgramError::Custom(99)); - } - } - - //-Process Instruction----------------------------------------------------- - let result = process_mint_to_checked(accounts, instruction_data); - - //-Assert Postconditions--------------------------------------------------- - let mint_new = get_mint(&accounts[0]); - let dst_new = get_account(&accounts[1]); - - if instruction_data.len() < 9 { - assert_eq!(result, Err(ProgramError::Custom(12))); - return result; - } else if accounts.len() < 3 { - assert_eq!(result, Err(ProgramError::NotEnoughAccountKeys)); - return result; - } else if accounts[1].data_len() != Account::LEN { - // TODO Daniel: is it possible for something to be provided that has the same - // len but is not an account? - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if dst_initialised.is_err() { - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if !dst_initialised.unwrap() { - assert_eq!(result, Err(ProgramError::UninitializedAccount)); - return result; - } else if dst_init_state.unwrap() == AccountState::Frozen { - // unwrap must succeed due to dst_initialised not being err - assert_eq!(result, Err(ProgramError::Custom(17))); - return result; - } else if dst_new.is_native() { - assert_eq!(result, Err(ProgramError::Custom(10))); - return result; - } else if key!(accounts[0]) != &mint!(dst_new) { - assert_eq!(result, Err(ProgramError::Custom(3))); - return result; - } else if accounts[0].data_len() != Mint::LEN { - // Not sure if this is even possible if we get past the case above - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if mint_initialised.is_err() { - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if !mint_initialised.unwrap() { - assert_eq!(result, Err(ProgramError::UninitializedAccount)); - return result; - } else if instruction_data[8] != decimals!(mint_new) { - assert_eq!(result, Err(ProgramError::Custom(18))); - return result; - } else { - if mint_new.mint_authority().is_some() { - // Validate Owner - inner_test_validate_owner( - mint_new.mint_authority().unwrap(), // expected_owner - &accounts[2], // owner_account_info - &accounts[3..], // tx_signers - maybe_multisig_is_initialised, - result.clone(), - )?; - } else { - assert_eq!(result, Err(ProgramError::Custom(5))); - return result; - } - - let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; - - if amount == 0 && owner!(accounts[0]) != &PROGRAM_ID { - assert_eq!(result, Err(ProgramError::IncorrectProgramId)); - return result; - } else if amount == 0 && owner!(accounts[1]) != &PROGRAM_ID { - assert_eq!(result, Err(ProgramError::IncorrectProgramId)); - return result; - } else if amount != 0 && initial_supply.checked_add(amount).is_none() { - assert_eq!(result, Err(ProgramError::Custom(14))); - return result; - } - - assert_eq!(mint_new.supply(), initial_supply + amount); - assert_eq!(dst_new.amount(), initial_amount + amount); - assert!(result.is_ok()); - } - - result -} +include!("../../shared/test_process_mint_to_checked.rs"); /// accounts[0] // Mint Info /// accounts[1] // Destination Info diff --git a/program/src/entrypoint-runtime-verification.rs b/program/src/entrypoint-runtime-verification.rs index 9b742af6..3af3cc32 100644 --- a/program/src/entrypoint-runtime-verification.rs +++ b/program/src/entrypoint-runtime-verification.rs @@ -2,6 +2,7 @@ use { crate::{ + instruction::TokenInstruction, processor::Processor, state::{Account, AccountState, Mint, Multisig}, ID as PROGRAM_ID, @@ -89,19 +90,6 @@ macro_rules! assert_pubkey_from_slice { }}; } -/// Macro to constrain discriminator and program_id, then strip the discriminator byte. -/// Returns (instr_with_disc, instruction_data) where instr_with_disc includes discriminator. -/// This is spl-token specific - p-token harnesses receive instruction data without discriminator. -macro_rules! constrain_and_strip { - ($disc:expr, $program_id:expr, $instruction_data:expr, $size:ty) => {{ - unsafe { assume($disc == $instruction_data[0]); } - unsafe { assume($program_id == &crate::id()); } - let instr_with_disc = $instruction_data; - let stripped: &$size = $instruction_data.last_chunk().unwrap(); - (instr_with_disc, stripped) - }}; -} - /// Macros to abstract API differences between spl-token and p-token. /// spl-token AccountInfo has fields (.key, .owner), p-token has methods /// (.key(), .owner()). spl-token wrappers have methods (.mint(), .decimals()), @@ -137,6 +125,21 @@ macro_rules! cheatcode_account { cheatcode_is_spl_account($acc) }; } +/// Process call macro to abstract the processor call difference. +/// spl-token prepends discriminator and uses TokenInstruction::unpack. +macro_rules! call_process_mint_to_checked { + ($accounts:expr, $instruction_data:expr) => {{ + let mut data_with_disc = [0u8; 10]; + data_with_disc[0] = 14; // MintToChecked discriminator + data_with_disc[1..].copy_from_slice($instruction_data); + let TokenInstruction::MintToChecked { amount, decimals } = + TokenInstruction::unpack(&data_with_disc)? + else { + unreachable!() + }; + Processor::process_mint_to(&PROGRAM_ID, $accounts, amount, Some(decimals)) + }}; +} /// A wrapper struct as middleware so that the same functions called /// on the p-token Account are called on the spl Account. However, @@ -719,9 +722,8 @@ fn inner_process_instruction( ) } else { test_process_mint_to_checked( - program_id, accounts.first_chunk().ok_or(TokenError::InvalidInstruction)?, // CHANGE P-Token: accounts: &[AccountInfo; 3] - instruction_data.first_chunk().ok_or(TokenError::InvalidInstruction)?, + instruction_data.last_chunk().ok_or(TokenError::InvalidInstruction)?, ) } } @@ -839,7 +841,7 @@ fn inner_process_instruction( // msg!("Testing Instruction: Withdraw Excess Lamports"); test_process_withdraw_excess_lamports( program_id, - accounts, + accounts.first_chunk().ok_or(TokenError::InvalidInstruction)?, instruction_data.first_chunk().ok_or(TokenError::InvalidInstruction)?, ) } @@ -3958,132 +3960,7 @@ fn test_process_approve_checked( result } -/// program_id // Token Program ID -/// accounts[0] // Mint Info -/// accounts[1] // Destination Info -/// accounts[2] // Owner Info -/// accounts[3..14] // Signers -/// instruction_data[0] // Discriminator 14 (Mint To Checked) -/// instruction_data[1..10] // Little Endian Bytes of u64 amount, and decimals -#[inline(never)] -fn test_process_mint_to_checked( - program_id: &Pubkey, - accounts: &[AccountInfo; 3], - instruction_data: &[u8; 10], -) -> ProgramResult { - let (instr_with_disc, instruction_data) = constrain_and_strip!(14, program_id, instruction_data, [u8; 9]); - - cheatcode_mint!(&accounts[0]); - cheatcode_account!(&accounts[1]); - cheatcode_account!(&accounts[2]); // Excluding the multisig case - - //-Initial State----------------------------------------------------------- - let mint_old = get_mint(&accounts[0]); - let dst_old = get_account(&accounts[1]); - let initial_supply = mint_old.supply(); - let initial_amount = dst_old.amount(); - let mint_initialised = mint_old.is_initialized(); - let dst_initialised = dst_old.is_initialized(); - let dst_init_state = dst_old.account_state(); - let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account - - #[cfg(feature = "assumptions")] - { - // Do not execute if adding to the account balance would overflow. - // shared::mint_to.rs,L68 is based on the assumption that initial_amount <= - // mint.supply and therefore cannot overflow because the minting itself - // would already error out. - let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; - if initial_amount.checked_add(amount).is_none() { - return Err(ProgramError::Custom(99)); - } - } - - //-Process Instruction----------------------------------------------------- - let result = Processor::process(program_id, accounts, instr_with_disc); - - //-Assert Postconditions--------------------------------------------------- - let mint_new = get_mint(&accounts[0]); - let dst_new = get_account(&accounts[1]); - - if instruction_data.len() < 9 { - assert_eq!(result, Err(ProgramError::Custom(12))); - return result; - } else if accounts.len() < 3 { - assert_eq!(result, Err(ProgramError::NotEnoughAccountKeys)); - return result; - } else if accounts[1].data_len() != Account::LEN { - // TODO Daniel: is it possible for something to be provided that has the same - // len but is not an account? - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if dst_initialised.is_err() { - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if !dst_initialised.unwrap() { - assert_eq!(result, Err(ProgramError::UninitializedAccount)); - return result; - } else if dst_init_state.unwrap() == AccountState::Frozen { - // unwrap must succeed due to dst_initialised not being err - assert_eq!(result, Err(ProgramError::Custom(17))); - return result; - } else if dst_new.is_native() { - assert_eq!(result, Err(ProgramError::Custom(10))); - return result; - } else if key!(accounts[0]) != &mint!(dst_new) { - assert_eq!(result, Err(ProgramError::Custom(3))); - return result; - } else if accounts[0].data_len() != Mint::LEN { - // Not sure if this is even possible if we get past the case above - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if mint_initialised.is_err() { - assert_eq!(result, Err(ProgramError::InvalidAccountData)); - return result; - } else if !mint_initialised.unwrap() { - assert_eq!(result, Err(ProgramError::UninitializedAccount)); - return result; - } else if instruction_data[8] != decimals!(mint_new) { - assert_eq!(result, Err(ProgramError::Custom(18))); - return result; - } else { - if mint_new.mint_authority().is_some() { - // Validate Owner - inner_test_validate_owner( - mint_new.mint_authority().unwrap(), // expected_owner - &accounts[2], // owner_account_info - &accounts[3..], // tx_signers - maybe_multisig_is_initialised, - result.clone(), - )?; - } else { - assert_eq!(result, Err(ProgramError::Custom(5))); - return result; - } - - let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; - - if amount == 0 && owner!(accounts[0]) != &PROGRAM_ID { - assert_eq!(result, Err(ProgramError::IncorrectProgramId)); - return result; - } else if amount == 0 && owner!(accounts[1]) != &PROGRAM_ID { - assert_eq!(result, Err(ProgramError::IncorrectProgramId)); - return result; - } else if amount != 0 && initial_supply.checked_add(amount).is_none() { - assert_eq!(result, Err(ProgramError::Custom(14))); - return result; - } - - assert_eq!(mint_new.supply(), initial_supply + amount); - assert_eq!(dst_new.amount(), initial_amount + amount); - assert!(result.is_ok()); - } - - // Ensure instruction_data was not mutated - assert_eq!(*instruction_data, instr_with_disc[1..]); - - result -} +include!("../../shared/test_process_mint_to_checked.rs"); /// program_id // Token Program ID /// accounts[0] // Source Info diff --git a/shared/test_process_mint_to_checked.rs b/shared/test_process_mint_to_checked.rs new file mode 100644 index 00000000..dca0fe7b --- /dev/null +++ b/shared/test_process_mint_to_checked.rs @@ -0,0 +1,117 @@ +/// accounts[0] // Mint Info +/// accounts[1] // Destination Info +/// accounts[2] // Owner Info +/// instruction_data[0..9] // Little Endian Bytes of u64 amount, and decimals +#[inline(never)] +fn test_process_mint_to_checked( + accounts: &[AccountInfo; 3], + instruction_data: &[u8; 9], +) -> ProgramResult { + cheatcode_mint!(&accounts[0]); + cheatcode_account!(&accounts[1]); + cheatcode_account!(&accounts[2]); // Excluding the multisig case + + //-Initial State----------------------------------------------------------- + let mint_old = get_mint(&accounts[0]); + let dst_old = get_account(&accounts[1]); + let initial_supply = mint_old.supply(); + let initial_amount = dst_old.amount(); + let mint_initialised = mint_old.is_initialized(); + let dst_initialised = dst_old.is_initialized(); + let dst_init_state = dst_old.account_state(); + let maybe_multisig_is_initialised = None; // Value set to `None` since authority is an account + + #[cfg(feature = "assumptions")] + { + // Do not execute if adding to the account balance would overflow. + // shared::mint_to.rs,L68 is based on the assumption that initial_amount <= + // mint.supply and therefore cannot overflow because the minting itself + // would already error out. + let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; + if initial_amount.checked_add(amount).is_none() { + return Err(ProgramError::Custom(99)); + } + } + + //-Process Instruction----------------------------------------------------- + let result = call_process_mint_to_checked!(accounts, instruction_data); + + //-Assert Postconditions--------------------------------------------------- + let mint_new = get_mint(&accounts[0]); + let dst_new = get_account(&accounts[1]); + + if instruction_data.len() < 9 { + assert_eq!(result, Err(ProgramError::Custom(12))); + return result; + } else if accounts.len() < 3 { + assert_eq!(result, Err(ProgramError::NotEnoughAccountKeys)); + return result; + } else if accounts[1].data_len() != Account::LEN { + // TODO Daniel: is it possible for something to be provided that has the same + // len but is not an account? + assert_eq!(result, Err(ProgramError::InvalidAccountData)); + return result; + } else if dst_initialised.is_err() { + assert_eq!(result, Err(ProgramError::InvalidAccountData)); + return result; + } else if !dst_initialised.unwrap() { + assert_eq!(result, Err(ProgramError::UninitializedAccount)); + return result; + } else if dst_init_state.unwrap() == AccountState::Frozen { + // unwrap must succeed due to dst_initialised not being err + assert_eq!(result, Err(ProgramError::Custom(17))); + return result; + } else if dst_new.is_native() { + assert_eq!(result, Err(ProgramError::Custom(10))); + return result; + } else if key!(accounts[0]) != &mint!(dst_new) { + assert_eq!(result, Err(ProgramError::Custom(3))); + return result; + } else if accounts[0].data_len() != Mint::LEN { + // Not sure if this is even possible if we get past the case above + assert_eq!(result, Err(ProgramError::InvalidAccountData)); + return result; + } else if mint_initialised.is_err() { + assert_eq!(result, Err(ProgramError::InvalidAccountData)); + return result; + } else if !mint_initialised.unwrap() { + assert_eq!(result, Err(ProgramError::UninitializedAccount)); + return result; + } else if instruction_data[8] != decimals!(mint_new) { + assert_eq!(result, Err(ProgramError::Custom(18))); + return result; + } else { + if mint_new.mint_authority().is_some() { + // Validate Owner + inner_test_validate_owner( + mint_new.mint_authority().unwrap(), // expected_owner + &accounts[2], // owner_account_info + &accounts[3..], // tx_signers + maybe_multisig_is_initialised, + result.clone(), + )?; + } else { + assert_eq!(result, Err(ProgramError::Custom(5))); + return result; + } + + let amount = unsafe { u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; 8])) }; + + if amount == 0 && owner!(accounts[0]) != &PROGRAM_ID { + assert_eq!(result, Err(ProgramError::IncorrectProgramId)); + return result; + } else if amount == 0 && owner!(accounts[1]) != &PROGRAM_ID { + assert_eq!(result, Err(ProgramError::IncorrectProgramId)); + return result; + } else if amount != 0 && initial_supply.checked_add(amount).is_none() { + assert_eq!(result, Err(ProgramError::Custom(14))); + return result; + } + + assert_eq!(mint_new.supply(), initial_supply + amount); + assert_eq!(dst_new.amount(), initial_amount + amount); + assert!(result.is_ok()); + } + + result +}