Skip to content

Conversation

@Stevengre
Copy link

@Stevengre Stevengre commented Jan 12, 2026

The refactoring makes it straightforward to compare and verify that both token implementations (spl-token and p-token) follow the same specifications. The key changes are:

  1. Extracted shared specs - Moved 43 test specification files to a new specs/shared/ directory, covering all major token operations.
  2. Created separate preludes - Added prelude-p-token.rs and prelude-spl-token.rs to handle implementation-specific setup while sharing common test logic.
  3. Massive deduplication - Removed ~4,200 lines from p-token and ~4,500 lines from spl-token entrypoint files by extracting common code.
  4. Added documentation - New specs/README.md explaining the structure.

@Stevengre Stevengre self-assigned this Jan 12, 2026
@Stevengre Stevengre changed the base branch from proofs to jh/fix_mint_to_checked_remote January 12, 2026 06:31
@Stevengre Stevengre changed the base branch from jh/fix_mint_to_checked_remote to proofs January 14, 2026 07:59
@Stevengre Stevengre marked this pull request as ready for review January 16, 2026 03:54
@Stevengre Stevengre requested a review from dkcumming January 16, 2026 03:54
@Stevengre
Copy link
Author

SPL-Token Spec Refactoring Guide

This document describes how the shared spec files in specs/shared/ are designed to work with both spl-token and p-token implementations through a common abstraction layer defined in the prelude files.

Architecture Overview

specs/shared/*.rs          <- Shared spec files (implementation-agnostic)
    |
    +-- prelude-spl-token.rs   <- Abstractions for spl-token
    +-- prelude-p-token.rs     <- Abstractions for p-token

The shared specs use macros and helper functions that are defined differently in each prelude, allowing the same spec logic to work with both implementations.


Key Abstraction Mechanisms

1. Deref Pattern via Wrappers (spl-token only)

Problem: In spl-token, Account::unpack_unchecked() returns Result<Account, ProgramError>, not a direct reference. The specs need to access account fields like account.amount directly.

Solution: AccountWrapper, MintWrapper, and MultisigWrapper structs wrap the Result and implement Deref:

// prelude-spl-token.rs
struct AccountWrapper(Result<Account, ProgramError>);

impl core::ops::Deref for AccountWrapper {
    type Target = Account;
    fn deref(&self) -> &Self::Target {
        self.0.as_ref().expect("AccountWrapper: underlying account missing")
    }
}

This allows specs to write:

let account = get_account(&accounts[0]);
let amt = account.amount;  // Deref makes this work

p-token difference: In p-token, get_account() returns &Account directly (via unsafe load), so no wrapper is needed:

// prelude-p-token.rs
fn get_account(account_info: &AccountInfo) -> &Account {
    unsafe {
        let byte_ptr = account_info.borrow_data_unchecked();
        load_unchecked::<Account>(byte_ptr).unwrap()
    }
}

2. Helper Methods on Wrappers

The wrappers provide additional helper methods to handle the Result semantics:

Method spl-token (wrapper) p-token (trait/direct)
is_initialized() Returns Result<bool, ProgramError> Direct field access
delegate() Converts COption to Option<&Pubkey> Direct field access
account_state() Returns Result<AccountState, ProgramError> Direct field access
is_native() Checks is_native.is_some() Direct field access
amount() Unwraps and returns u64 Direct field access

3. Call Function Macros

Problem: spl-token and p-token have different function signatures for processor calls.

spl-token uses static methods with program_id:

Processor::process_transfer(&PROGRAM_ID, $accounts, amount, None)

p-token uses standalone functions:

process_transfer($accounts, $instruction_data)

Solution: Macros abstract this difference:

// prelude-spl-token.rs
macro_rules! call_process_transfer {
    ($accounts:expr, $instruction_data:expr) => {{
        let data = $instruction_data;
        let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
        Processor::process_transfer(&PROGRAM_ID, $accounts, amount, None)
    }};
}

// prelude-p-token.rs
macro_rules! call_process_transfer {
    ($accounts:expr, $instruction_data:expr) => {
        process_transfer($accounts, $instruction_data)
    };
}

4. AccountInfo Access Macros

Problem: spl-token's AccountInfo has public fields, while p-token uses getter methods.

Operation spl-token p-token
Get key $acc.key $acc.key()
Get owner $acc.owner $acc.owner()
Is signer $acc.is_signer $acc.is_signer()

Solution: Macros provide a unified interface:

// prelude-spl-token.rs
macro_rules! key {
    ($acc:expr) => { $acc.key };
}

// prelude-p-token.rs
macro_rules! key {
    ($acc:expr) => { $acc.key() };
}

5. Same Account Comparison

spl-token: Compare keys via macro

macro_rules! same_account {
    ($acc1:expr, $acc2:expr) => { key!($acc1) == key!($acc2) };
}

p-token: Direct equality (AccountInfo implements PartialEq)

macro_rules! same_account {
    ($acc1:expr, $acc2:expr) => { $acc1 == $acc2 };
}

6. Pubkey Assertion Macros

Handle differences in how pubkeys are compared:

// prelude-spl-token.rs - needs dereference for reference types
macro_rules! assert_pubkey_from_slice {
    ($actual:expr, $slice:expr) => {{
        let expected_pubkey = Pubkey::new_from_array($slice.try_into().unwrap());
        assert_eq!(*$actual, expected_pubkey);
    }};
}

// prelude-p-token.rs - direct comparison
macro_rules! assert_pubkey_from_slice {
    ($actual:expr, $slice:expr) => {{
        assert_eq!($actual, $slice);
    }};
}

Cheatcode Abstraction

Cheatcodes for symbolic execution are also abstracted:

// prelude-spl-token.rs
fn cheatcode_is_spl_account(_: &AccountInfo) {}
macro_rules! cheatcode_account {
    ($acc:expr) => { cheatcode_is_spl_account($acc) };
}

// prelude-p-token.rs
fn cheatcode_is_account(_: &AccountInfo) {}
macro_rules! cheatcode_account {
    ($acc:expr) => { cheatcode_is_account($acc) };
}

Import Aliases

Each prelude defines consistent aliases for types that may have different paths:

Alias spl-token p-token
PROGRAM_ID crate::ID pinocchio_token_interface::program::ID
Rent solana_rent::Rent pinocchio::sysvars::rent::Rent
RENT_ID solana_sysvar::rent::ID pinocchio::sysvars::rent::RENT_ID
NATIVE_MINT_ID spl_token_interface::native_mint::ID pinocchio_token_interface::native_mint::ID
AccountState Already imported pinocchio_token_interface::state::account_state::AccountState

Writing New Shared Specs

When writing a new shared spec:

  1. Use macros for all AccountInfo access: key!(), owner!(), is_signer!(), same_account!()

  2. Use get_* functions: get_account(), get_mint(), get_rent(), get_multisig()

  3. Use call_process_* macros: For invoking processor functions

  4. Use cheatcode macros: cheatcode_account!(), cheatcode_mint!(), etc.

  5. Access wrapper methods through the abstracted interface: The wrapper's helper methods (is_initialized(), delegate(), etc.) work identically in both implementations

  6. Direct field access via Deref: After calling get_account(), you can access fields like .amount, .owner, .mint directly due to Deref implementation


Summary Table

Feature spl-token p-token
Account unpacking Result<Account, ProgramError> wrapped &Account direct
Field access Via Deref on wrapper Direct on reference
AccountInfo fields Public fields (acc.key) Getter methods (acc.key())
Processor calls Processor::method(&PROGRAM_ID, ...) function(accounts, data)
Same account check Compare keys PartialEq on AccountInfo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants