From 35d944483a38f81b0ee43d7546fd5314d45aad57 Mon Sep 17 00:00:00 2001 From: Jon C Date: Thu, 26 Feb 2026 20:10:42 +0100 Subject: [PATCH 1/4] p-token: Sync native updates rent exempt reserve #### Problem This is the p-token version of https://github.com/solana-program/token-2022/pull/1006. The current rent-exempt reserve at the time of account creation is stored in the `is_native` field of a token account. When rent is decreased, or ever increased, this value will be incorrect, leading to an incorrect `amount` stored in the token account. #### Summary of changes Update `sync_native` to use the current rent-exempt reserve, and allow for the amount to potentially decrease. --- pinocchio/program/src/processor/sync_native.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pinocchio/program/src/processor/sync_native.rs b/pinocchio/program/src/processor/sync_native.rs index 0ec210d1..caef9377 100644 --- a/pinocchio/program/src/processor/sync_native.rs +++ b/pinocchio/program/src/processor/sync_native.rs @@ -1,6 +1,11 @@ use { super::check_account_owner, - pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}, + pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, + }, pinocchio_token_interface::{ error::TokenError, state::{account::Account, load_mut}, @@ -13,20 +18,20 @@ pub fn process_sync_native(accounts: &[AccountInfo]) -> ProgramResult { check_account_owner(native_account_info)?; + let rent = Rent::get()?; + let rent_exempt_reserve = rent.minimum_balance(native_account_info.data_len()); + // 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::(native_account_info.borrow_mut_data_unchecked())? }; - if let Option::Some(rent_exempt_reserve) = native_account.native_amount() { + if native_account.is_native() { let new_amount = native_account_info .lamports() .checked_sub(rent_exempt_reserve) .ok_or(TokenError::Overflow)?; - - if new_amount < native_account.amount() { - return Err(TokenError::InvalidState.into()); - } + native_account.set_native_amount(rent_exempt_reserve); native_account.set_amount(new_amount); } else { return Err(TokenError::NonNativeNotSupported.into()); From 50ad24ffb553353685cf370c8865b2a20ec64758 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 27 Feb 2026 23:07:31 +0100 Subject: [PATCH 2/4] Update old program to make accurate fixtures --- program/src/processor.rs | 9 +++++---- program/tests/processor.rs | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 9eb198de..9180213c 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -759,16 +759,17 @@ impl Processor { let native_account_info = next_account_info(account_info_iter)?; Self::check_account_owner(program_id, native_account_info)?; + let rent = Rent::get()?; + let rent_exempt_reserve = rent.minimum_balance(native_account_info.data_len()); + let mut native_account = Account::unpack(&native_account_info.data.borrow())?; - if let COption::Some(rent_exempt_reserve) = native_account.is_native { + if native_account.is_native.is_some() { let new_amount = native_account_info .lamports() .checked_sub(rent_exempt_reserve) .ok_or(TokenError::Overflow)?; - if new_amount < native_account.amount { - return Err(TokenError::InvalidState.into()); - } + native_account.is_native = COption::Some(rent_exempt_reserve); native_account.amount = new_amount; } else { return Err(TokenError::NonNativeNotSupported.into()); diff --git a/program/tests/processor.rs b/program/tests/processor.rs index dbfb8ca9..4bae59b5 100644 --- a/program/tests/processor.rs +++ b/program/tests/processor.rs @@ -5986,15 +5986,20 @@ fn test_sync_native() { // reduce sol native_account.lamports -= 1; - // fail sync - assert_eq!( - Err(TokenError::InvalidState.into()), - do_process_instruction( - sync_native(&program_id, &native_account_key,).unwrap(), - vec![&mut native_account], - &[Check::err(TokenError::InvalidState.into())], - ) - ); + // succeed sync with reduced value + do_process_instruction( + sync_native(&program_id, &native_account_key).unwrap(), + vec![&mut native_account], + &[ + Check::success(), + Check::account(&native_account_key) + .data_slice(64, &(new_lamports - 1).to_le_bytes()) + .build(), + ], + ) + .unwrap(); + let account = Account::unpack_unchecked(&native_account.data).unwrap(); + assert_eq!(account.amount, new_lamports - 1); } #[test] From 07047b8c446c7164bbf98968c77340a4498b20ff Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 27 Feb 2026 23:15:16 +0100 Subject: [PATCH 3/4] Move outdated fixture --- pinocchio/program/README.md | 4 ++++ ...6ZgZKYD34Q2CUc7siyErKzwoSsE4G8Pv44AsLnoYAuxf.fix | Bin 2 files changed, 4 insertions(+) rename pinocchio/program/fuzz/{blob => blob-old}/instr-6ZgZKYD34Q2CUc7siyErKzwoSsE4G8Pv44AsLnoYAuxf.fix (100%) diff --git a/pinocchio/program/README.md b/pinocchio/program/README.md index a7e6022a..3a46d1bb 100644 --- a/pinocchio/program/README.md +++ b/pinocchio/program/README.md @@ -23,3 +23,7 @@ was built against tag [`program@3.5.0`](https://github.com/solana-program/token/releases/tag/program%40v3.5.0), commit [`4d5ff3015ae5ad3316f2d2efdde6ab9f7a50716c`](https://github.com/solana-program/token/tree/4d5ff3015ae5ad3316f2d2efdde6ab9f7a50716c). + +One fixture was removed due to a change in the `sync-native` behavior to allow +for the `amount` to decrease, due to potential changes in the `Rent` sysvar. +This fixture is still available at `fuzz/blob-old/`. diff --git a/pinocchio/program/fuzz/blob/instr-6ZgZKYD34Q2CUc7siyErKzwoSsE4G8Pv44AsLnoYAuxf.fix b/pinocchio/program/fuzz/blob-old/instr-6ZgZKYD34Q2CUc7siyErKzwoSsE4G8Pv44AsLnoYAuxf.fix similarity index 100% rename from pinocchio/program/fuzz/blob/instr-6ZgZKYD34Q2CUc7siyErKzwoSsE4G8Pv44AsLnoYAuxf.fix rename to pinocchio/program/fuzz/blob-old/instr-6ZgZKYD34Q2CUc7siyErKzwoSsE4G8Pv44AsLnoYAuxf.fix From b93971ef10d45f756790cfb21b68fe675755fa00 Mon Sep 17 00:00:00 2001 From: Jon C Date: Sat, 28 Feb 2026 00:28:39 +0100 Subject: [PATCH 4/4] Add a test for increasing rent --- pinocchio/program/tests/sync_native.rs | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/pinocchio/program/tests/sync_native.rs b/pinocchio/program/tests/sync_native.rs index 4a1be066..31d9b6da 100644 --- a/pinocchio/program/tests/sync_native.rs +++ b/pinocchio/program/tests/sync_native.rs @@ -7,6 +7,7 @@ use { native_mint, state::{ account::Account as TokenAccount, account_state::AccountState, load_mut_unchecked, + load_unchecked, }, }, solana_account::Account, @@ -92,3 +93,51 @@ async fn sync_native() { } }); } + +#[test] +fn sync_native_with_rent_change() { + let native_mint = Pubkey::new_from_array(native_mint::ID); + let authority_key = Pubkey::new_unique(); + + // native account + // - amount: 1_000_000_000 + // - lamports: 2_000_000_000 + let source_account_key = Pubkey::new_unique(); + let lamports = 2_000_000_000; + let source_account = create_token_account( + &native_mint, + &authority_key, + true, + lamports, + &TOKEN_PROGRAM_ID, + ); + + let instruction = + spl_token_interface::instruction::sync_native(&TOKEN_PROGRAM_ID, &source_account_key) + .unwrap(); + + // Executes the sync_native instruction. + + let mut rent = Rent::default(); + rent.lamports_per_byte_year *= 2; + + let space = size_of::(); + let new_rent_exempt_reserve = rent.minimum_balance(space); + let old_rent_exempt_reserve = source_account.lamports - lamports; + let rent_difference = new_rent_exempt_reserve - old_rent_exempt_reserve; + + let mut test_mollusk = mollusk(); + test_mollusk.sysvars.rent = rent; + let result = test_mollusk.process_and_validate_instruction_chain( + &[(&instruction, &[Check::success()])], + &[(source_account_key, source_account)], + ); + + result.resulting_accounts.iter().for_each(|(key, account)| { + if *key == source_account_key { + let token_account = unsafe { load_unchecked::(&account.data).unwrap() }; + assert_eq!(token_account.amount(), lamports - rent_difference); + assert_eq!(token_account.native_amount(), Some(new_rent_exempt_reserve)); + } + }); +}