Browse Source

Initial version

febo 1 year ago
parent
commit
ee0052c266

+ 29 - 0
p-token/Cargo.toml

@@ -0,0 +1,29 @@
+[package]
+name = "token-program"
+version = "0.0.0"
+edition = "2021"
+readme = "./README.md"
+license-file = "../LICENSE"
+publish = false
+
+[package.metadata.solana]
+program-id = "TokenLight111111111111111111111111111111111"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[features]
+logging = []
+test-sbf = []
+
+[dependencies]
+bytemuck = { version="1.18.0", features=["derive"] }
+pinocchio = { version="0.2", path="/Users/febo/Developer/febo/pinocchio/sdk/pinocchio" }
+pinocchio-pubkey = "0.1"
+
+[dev-dependencies]
+assert_matches = "1.5.0"
+solana-program-test = "~1.18"
+solana-sdk = "~1.18"
+spl-token = { version="^4", features=["no-entrypoint"] }
+test-case = "3.3.1"

+ 3 - 0
p-token/README.md

@@ -0,0 +1,3 @@
+# Token
+
+Your generated Solana program. Have fun!

+ 1 - 0
p-token/keypair.json

@@ -0,0 +1 @@
+[178,215,114,55,146,0,60,153,90,63,112,26,148,148,111,230,196,181,5,124,14,237,142,43,207,114,102,60,145,103,53,23,249,192,123,198,160,247,138,44,243,38,29,240,233,86,143,132,170,26,154,207,174,195,147,223,12,231,253,195,118,55,207,100]

+ 63 - 0
p-token/src/entrypoint.rs

@@ -0,0 +1,63 @@
+use pinocchio::{
+    account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, program_error::ProgramError,
+    pubkey::Pubkey,
+};
+
+use crate::processor::{
+    initialize_account::process_initialize_account,
+    initialize_mint::{process_initialize_mint, InitializeMint},
+    mint_to::process_mint_to,
+    transfer::process_transfer,
+};
+
+entrypoint!(process_instruction);
+
+#[inline(always)]
+pub fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    instruction_data: &[u8],
+) -> ProgramResult {
+    match instruction_data.split_first() {
+        // 0 - InitializeMint
+        Some((&0, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: InitializeMint");
+
+            let instruction = InitializeMint::try_from_bytes(data)?;
+            process_initialize_mint(accounts, &instruction, true)
+        }
+        // 1 - InitializeAccount
+        Some((&1, _)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: InitializeAccount");
+
+            process_initialize_account(program_id, accounts, None, true)
+        }
+        // 3 - Transfer
+        Some((&3, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: Transfer");
+
+            let amount = u64::from_le_bytes(
+                data.try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            );
+
+            process_transfer(program_id, accounts, amount, None)
+        }
+        // 7 - InitializeMint
+        Some((&7, data)) => {
+            #[cfg(feature = "logging")]
+            pinocchio::msg!("Instruction: MintTo");
+
+            let amount = u64::from_le_bytes(
+                data.try_into()
+                    .map_err(|_error| ProgramError::InvalidInstructionData)?,
+            );
+
+            process_mint_to(program_id, accounts, amount, None)
+        }
+        _ => Err(ProgramError::InvalidInstructionData),
+    }
+}

+ 61 - 0
p-token/src/error.rs

@@ -0,0 +1,61 @@
+//! Error types
+
+use pinocchio::program_error::ProgramError;
+
+/// Errors that may be returned by the Token program.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum TokenError {
+    // 0
+    /// Lamport balance below rent-exempt threshold.
+    NotRentExempt,
+    /// Insufficient funds for the operation requested.
+    InsufficientFunds,
+    /// Invalid Mint.
+    InvalidMint,
+    /// Account not associated with this Mint.
+    MintMismatch,
+    /// Owner does not match.
+    OwnerMismatch,
+
+    // 5
+    /// This token's supply is fixed and new tokens cannot be minted.
+    FixedSupply,
+    /// The account cannot be initialized because it is already being used.
+    AlreadyInUse,
+    /// Invalid number of provided signers.
+    InvalidNumberOfProvidedSigners,
+    /// Invalid number of required signers.
+    InvalidNumberOfRequiredSigners,
+    /// State is uninitialized.
+    UninitializedState,
+
+    // 10
+    /// Instruction does not support native tokens
+    NativeNotSupported,
+    /// Non-native account can only be closed if its balance is zero
+    NonNativeHasBalance,
+    /// Invalid instruction
+    InvalidInstruction,
+    /// State is invalid for requested operation.
+    InvalidState,
+    /// Operation overflowed
+    Overflow,
+
+    // 15
+    /// Account does not support specified authority type.
+    AuthorityTypeNotSupported,
+    /// This token mint cannot freeze accounts.
+    MintCannotFreeze,
+    /// Account is frozen; all account operations will fail
+    AccountFrozen,
+    /// Mint decimals mismatch between the client and mint
+    MintDecimalsMismatch,
+    /// Instruction does not support non-native tokens
+    NonNativeNotSupported,
+}
+
+impl From<TokenError> for ProgramError {
+    fn from(e: TokenError) -> Self {
+        ProgramError::Custom(e as u32)
+    }
+}

+ 9 - 0
p-token/src/lib.rs

@@ -0,0 +1,9 @@
+//! A lighter Token program for SVM.
+
+mod entrypoint;
+pub mod error;
+pub mod native_mint;
+mod processor;
+pub mod state;
+
+pinocchio_pubkey::declare_id!("TokenLight111111111111111111111111111111111");

+ 15 - 0
p-token/src/native_mint.rs

@@ -0,0 +1,15 @@
+//! The Mint that represents the native token.
+
+use pinocchio::pubkey::Pubkey;
+
+/// There are 10^9 lamports in one SOL
+pub const DECIMALS: u8 = 9;
+
+// The Mint for native SOL Token accounts
+pub const ID: Pubkey =
+    pinocchio_pubkey::declare_pubkey!("So11111111111111111111111111111111111111112");
+
+#[inline(always)]
+pub fn is_native_mint(mint: &Pubkey) -> bool {
+    mint == &ID
+}

+ 112 - 0
p-token/src/processor/initialize_account.rs

@@ -0,0 +1,112 @@
+use std::mem::size_of;
+
+use bytemuck::{Pod, Zeroable};
+use pinocchio::{
+    account_info::AccountInfo,
+    entrypoint::ProgramResult,
+    get_account_info,
+    program_error::ProgramError,
+    pubkey::{self, Pubkey},
+    sysvars::{rent::Rent, Sysvar},
+};
+
+use crate::{
+    error::TokenError,
+    native_mint::is_native_mint,
+    state::{
+        account::{Account, AccountState},
+        mint::Mint,
+        PodCOption,
+    },
+};
+
+use super::check_account_owner;
+
+pub fn process_initialize_account(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    args: Option<&InitializeAccount>,
+    _rent_sysvar_account: bool,
+) -> ProgramResult {
+    let [new_account_info, mint_info, _remaning @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let owner = if let Some(InitializeAccount { owner }) = args {
+        owner
+    } else {
+        get_account_info!(accounts, 2).key()
+    };
+
+    // FEBO: ~408 CU can be saved by removing the rent check (is_exempt seems to
+    // be very expensive).
+    //
+    // The transaction will naturally fail if the account is not rent exempt with
+    // a TransactionError::InsufficientFundsForRent error.
+    /*
+    let rent = Rent::get()?;
+
+    if !rent.is_exempt(
+        unsafe { *new_account_info.unchecked_borrow_lamports() },
+        size_of::<Account>(),
+    ) {
+        return Err(Token::NotRentExempt);
+    }
+    */
+
+    let account_data = unsafe { new_account_info.unchecked_borrow_mut_data() };
+    let account = bytemuck::try_from_bytes_mut::<Account>(account_data)
+        .map_err(|_error| ProgramError::InvalidAccountData)?;
+
+    if account.is_initialized() {
+        return Err(TokenError::AlreadyInUse.into());
+    }
+
+    let is_native_mint = is_native_mint(mint_info.key());
+
+    if !is_native_mint {
+        check_account_owner(program_id, mint_info)?;
+
+        let mint_data = unsafe { mint_info.unchecked_borrow_data() };
+        let mint = bytemuck::from_bytes::<Mint>(mint_data);
+
+        let initialized: bool = mint.is_initialized.into();
+        if !initialized {
+            return Err(TokenError::InvalidMint.into());
+        }
+    }
+
+    pubkey::copy(&mut account.mint, mint_info.key());
+    pubkey::copy(&mut account.owner, owner);
+    account.close_authority = PodCOption::from(None);
+    account.delegate = PodCOption::from(None);
+    account.delegated_amount = 0u64.to_le_bytes();
+    account.state = AccountState::Initialized as u8;
+
+    if is_native_mint {
+        let rent = Rent::get()?;
+        let rent_exempt_reserve = rent.minimum_balance(size_of::<Account>());
+
+        account.is_native = PodCOption::from(Some(rent_exempt_reserve.to_le_bytes()));
+        unsafe {
+            account.amount = new_account_info
+                .unchecked_borrow_lamports()
+                .checked_sub(rent_exempt_reserve)
+                .ok_or(TokenError::Overflow)?
+                .to_le_bytes()
+        }
+    } else {
+        account.is_native = PodCOption::from(None);
+        account.amount = 0u64.to_le_bytes();
+    };
+
+    Ok(())
+}
+
+/// Instruction data for the `InitializeAccount` instruction.
+#[repr(C)]
+#[derive(Clone, Copy, Default, Pod, Zeroable)]
+pub struct InitializeAccount {
+    /// The new account's owner/multisignature.
+    pub owner: Pubkey,
+}

+ 101 - 0
p-token/src/processor/initialize_mint.rs

@@ -0,0 +1,101 @@
+use std::mem::size_of;
+
+use bytemuck::{Pod, Zeroable};
+use pinocchio::{
+    account_info::AccountInfo,
+    entrypoint::ProgramResult,
+    program_error::ProgramError,
+    pubkey::{Pubkey, PUBKEY_BYTES},
+};
+
+use crate::{
+    error::TokenError,
+    state::{mint::Mint, PodCOption},
+};
+
+pub fn process_initialize_mint(
+    accounts: &[AccountInfo],
+    args: &InitializeMint,
+    _rent_sysvar_account: bool,
+) -> ProgramResult {
+    let [mint_info, _remaining @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    let mint_data = &mut mint_info.try_borrow_mut_data()?;
+    let mint = bytemuck::from_bytes_mut::<Mint>(mint_data);
+
+    if mint.is_initialized.into() {
+        return Err(TokenError::AlreadyInUse.into());
+    }
+
+    // FEBO: ~408 CU can be saved by removing the rent check (is_exempt seems to
+    // be very expensive).
+    //
+    // The transaction will naturally fail if the account is not rent exempt with
+    // a TransactionError::InsufficientFundsForRent error.
+    /*
+    let rent = Rent::get()?;
+
+    if !rent.is_exempt(
+        unsafe { *mint_info.unchecked_borrow_lamports() },
+        size_of::<Mint>(),
+    ) {
+        return Err(TokenError::NotRentExempt);
+    }
+    */
+
+    mint.mint_authority = PodCOption::from(Some(args.data.mint_authority));
+    mint.decimals = args.data.decimals;
+    mint.is_initialized = true.into();
+
+    if let Some(freeze_authority) = args.freeze_authority {
+        mint.freeze_authority = PodCOption::from(Some(*freeze_authority));
+    }
+
+    Ok(())
+}
+
+/// Instruction data for the `InitializeMint` instruction.
+pub struct InitializeMint<'a> {
+    pub data: &'a MintData,
+
+    /// The freeze authority/multisignature of the mint.
+    pub freeze_authority: Option<&'a Pubkey>,
+}
+
+impl<'a> InitializeMint<'a> {
+    pub fn try_from_bytes(data: &'a [u8]) -> Result<Self, ProgramError> {
+        // We expect the data to be at least the size of the MintInput struct
+        // plus one byte for the freeze_authority option.
+        if data.len() <= size_of::<MintData>() {
+            return Err(ProgramError::InvalidInstructionData);
+        }
+
+        let (data, remaining) = data.split_at(size_of::<MintData>());
+        let data = bytemuck::from_bytes::<MintData>(data);
+
+        let freeze_authority = match remaining.split_first() {
+            Some((&0, _)) => None,
+            Some((&1, pubkey)) if pubkey.len() == PUBKEY_BYTES => {
+                Some(bytemuck::from_bytes::<Pubkey>(pubkey))
+            }
+            _ => return Err(ProgramError::InvalidInstructionData),
+        };
+
+        Ok(Self {
+            data,
+            freeze_authority,
+        })
+    }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Default, Pod, Zeroable)]
+pub struct MintData {
+    /// Number of base 10 digits to the right of the decimal place.
+    pub decimals: u8,
+
+    /// The authority/multisignature to mint tokens.
+    pub mint_authority: Pubkey,
+}

+ 71 - 0
p-token/src/processor/mint_to.rs

@@ -0,0 +1,71 @@
+use pinocchio::{
+    account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
+    pubkey::Pubkey,
+};
+
+use crate::{
+    error::TokenError,
+    native_mint::is_native_mint,
+    state::{account::Account, mint::Mint},
+};
+
+use super::{check_account_owner, validate_owner};
+
+pub fn process_mint_to(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    amount: u64,
+    expected_decimals: Option<u8>,
+) -> ProgramResult {
+    let [mint_info, destination_account_info, owner_info, remaining @ ..] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+
+    // destination account
+
+    let account_data = unsafe { destination_account_info.unchecked_borrow_mut_data() };
+    let destination_account = bytemuck::from_bytes_mut::<Account>(account_data);
+
+    if destination_account.is_frozen() {
+        return Err(TokenError::AccountFrozen.into());
+    }
+
+    if is_native_mint(mint_info.key()) {
+        return Err(TokenError::NativeNotSupported.into());
+    }
+
+    if mint_info.key() != &destination_account.mint {
+        return Err(TokenError::MintMismatch.into());
+    }
+
+    let mint_data = unsafe { mint_info.unchecked_borrow_mut_data() };
+    let mint = bytemuck::from_bytes_mut::<Mint>(mint_data);
+
+    if let Some(expected_decimals) = expected_decimals {
+        if expected_decimals != mint.decimals {
+            return Err(TokenError::MintDecimalsMismatch.into());
+        }
+    }
+
+    match mint.mint_authority.get() {
+        Some(mint_authority) => validate_owner(program_id, &mint_authority, owner_info, remaining)?,
+        None => return Err(TokenError::FixedSupply.into()),
+    }
+
+    if amount == 0 {
+        check_account_owner(program_id, mint_info)?;
+        check_account_owner(program_id, destination_account_info)?;
+    }
+
+    let destination_amount = u64::from_le_bytes(destination_account.amount)
+        .checked_add(amount)
+        .ok_or(ProgramError::InvalidAccountData)?;
+    destination_account.amount = destination_amount.to_le_bytes();
+
+    let mint_supply = u64::from_le_bytes(mint.supply)
+        .checked_add(amount)
+        .ok_or(ProgramError::InvalidAccountData)?;
+    mint.supply = mint_supply.to_le_bytes();
+
+    Ok(())
+}

+ 66 - 0
p-token/src/processor/mod.rs

@@ -0,0 +1,66 @@
+use pinocchio::{
+    account_info::AccountInfo,
+    entrypoint::ProgramResult,
+    program_error::ProgramError,
+    pubkey::{self, Pubkey},
+};
+
+use crate::{
+    error::TokenError,
+    state::multisignature::{Multisig, MAX_SIGNERS},
+};
+
+pub mod initialize_account;
+pub mod initialize_mint;
+pub mod mint_to;
+pub mod transfer;
+
+/// Checks that the account is owned by the expected program.
+#[inline(always)]
+pub fn check_account_owner(program_id: &Pubkey, account_info: &AccountInfo) -> ProgramResult {
+    if program_id != account_info.owner() {
+        Err(ProgramError::IncorrectProgramId)
+    } else {
+        Ok(())
+    }
+}
+
+/// Validates owner(s) are present
+#[inline(always)]
+pub fn validate_owner(
+    program_id: &Pubkey,
+    expected_owner: &Pubkey,
+    owner_account_info: &AccountInfo,
+    signers: &[AccountInfo],
+) -> ProgramResult {
+    if expected_owner != owner_account_info.key() {
+        return Err(TokenError::OwnerMismatch.into());
+    }
+
+    if owner_account_info.data_len() == Multisig::LEN && program_id != owner_account_info.owner() {
+        let multisig_data = owner_account_info.try_borrow_data()?;
+        let multisig = bytemuck::from_bytes::<Multisig>(&multisig_data);
+
+        let mut num_signers = 0;
+        let mut matched = [false; MAX_SIGNERS];
+
+        for signer in signers.iter() {
+            for (position, key) in multisig.signers[0..multisig.n as usize].iter().enumerate() {
+                if pubkey::compare(key, signer.key()) && !matched[position] {
+                    if !signer.is_signer() {
+                        return Err(ProgramError::MissingRequiredSignature);
+                    }
+                    matched[position] = true;
+                    num_signers += 1;
+                }
+            }
+        }
+        if num_signers < multisig.m {
+            return Err(ProgramError::MissingRequiredSignature);
+        }
+    } else if !owner_account_info.is_signer() {
+        return Err(ProgramError::MissingRequiredSignature);
+    }
+
+    Ok(())
+}

+ 158 - 0
p-token/src/processor/transfer.rs

@@ -0,0 +1,158 @@
+use pinocchio::{
+    account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
+    pubkey::Pubkey,
+};
+
+use crate::{
+    error::TokenError,
+    native_mint::is_native_mint,
+    state::{account::Account, mint::Mint, PodCOption},
+};
+
+use super::{check_account_owner, validate_owner};
+
+pub fn process_transfer(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    amount: u64,
+    expected_decimals: Option<u8>,
+) -> ProgramResult {
+    // Accounts expected depends on whether we have the mint `decimals` or not; when we have the
+    // mint `decimals`, we expect the mint account to be present.
+
+    let (
+        source_account_info,
+        expected_mint_info,
+        destination_account_info,
+        authority_info,
+        remaning,
+    ) = if let Some(decimals) = expected_decimals {
+        let [source_account_info, mint_info, destination_account_info, authority_info, remaning @ ..] =
+            accounts
+        else {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        };
+        (
+            source_account_info,
+            Some((mint_info, decimals)),
+            destination_account_info,
+            authority_info,
+            remaning,
+        )
+    } else {
+        let [source_account_info, destination_account_info, authority_info, remaning @ ..] =
+            accounts
+        else {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        };
+        (
+            source_account_info,
+            None,
+            destination_account_info,
+            authority_info,
+            remaning,
+        )
+    };
+
+    // Validates source and destination accounts.
+
+    let source_account_data = unsafe { source_account_info.unchecked_borrow_mut_data() };
+    let source_account = bytemuck::from_bytes_mut::<Account>(source_account_data);
+
+    let destination_account_data = unsafe { destination_account_info.unchecked_borrow_mut_data() };
+    let destination_account = bytemuck::from_bytes_mut::<Account>(destination_account_data);
+
+    if source_account.is_frozen() || destination_account.is_frozen() {
+        return Err(TokenError::AccountFrozen.into());
+    }
+
+    // FEBO: Implicitly validates that the account has enough tokens by calculating the
+    // remaining amount. The amount is only updated on the account if the transfer
+    // is successful.
+    let remaining_amount = u64::from_le_bytes(source_account.amount)
+        .checked_sub(amount)
+        .ok_or(TokenError::InsufficientFunds)?;
+
+    if source_account.mint != destination_account.mint {
+        return Err(TokenError::MintMismatch.into());
+    }
+
+    // Validates the mint information.
+
+    if let Some((mint_info, decimals)) = expected_mint_info {
+        if mint_info.key() != &source_account.mint {
+            return Err(TokenError::MintMismatch.into());
+        }
+
+        let mint_data = mint_info.try_borrow_data()?;
+        let mint = bytemuck::from_bytes::<Mint>(&mint_data);
+
+        if decimals != mint.decimals {
+            return Err(TokenError::MintDecimalsMismatch.into());
+        }
+    }
+
+    let self_transfer = source_account_info.key() == destination_account_info.key();
+
+    // Validates the authority (delegate or owner).
+
+    if source_account.delegate.as_ref() == Some(authority_info.key()) {
+        validate_owner(program_id, authority_info.key(), authority_info, remaning)?;
+
+        let delegated_amount = u64::from_le_bytes(source_account.delegated_amount)
+            .checked_sub(amount)
+            .ok_or(TokenError::InsufficientFunds)?;
+
+        if !self_transfer {
+            source_account.delegated_amount = delegated_amount.to_le_bytes();
+
+            if delegated_amount == 0 {
+                source_account.delegate = PodCOption::from(None);
+            }
+        }
+    } else {
+        validate_owner(program_id, &source_account.owner, authority_info, remaning)?;
+    }
+
+    if self_transfer || amount == 0 {
+        check_account_owner(program_id, source_account_info)?;
+        check_account_owner(program_id, destination_account_info)?;
+
+        // No need to move tokens around.
+        return Ok(());
+    }
+
+    // FEBO: This was moved to the if statement above since we can skip the amount
+    // manipulation if it is a self-transfer or the amount is zero.
+    //
+    // This check MUST occur just before the amounts are manipulated
+    // to ensure self-transfers are fully validated
+    /*
+    if self_transfer {
+        return Ok(());
+    }
+    */
+
+    // Moves the tokens.
+
+    source_account.amount = remaining_amount.to_le_bytes();
+
+    let destination_amount = u64::from_le_bytes(destination_account.amount)
+        .checked_add(amount)
+        .ok_or(TokenError::Overflow)?;
+    destination_account.amount = destination_amount.to_le_bytes();
+
+    if is_native_mint(&source_account.mint) {
+        let mut source_lamports = source_account_info.try_borrow_mut_lamports()?;
+        *source_lamports = source_lamports
+            .checked_sub(amount)
+            .ok_or(TokenError::Overflow)?;
+
+        let mut destination_lamports = destination_account_info.try_borrow_mut_lamports()?;
+        *destination_lamports = destination_lamports
+            .checked_add(amount)
+            .ok_or(TokenError::Overflow)?;
+    }
+
+    Ok(())
+}

+ 88 - 0
p-token/src/state/account.rs

@@ -0,0 +1,88 @@
+use bytemuck::{Pod, Zeroable};
+use pinocchio::pubkey::Pubkey;
+
+use super::PodCOption;
+
+/// Account data.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
+pub struct Account {
+    /// The mint associated with this account
+    pub mint: Pubkey,
+
+    /// The owner of this account.
+    pub owner: Pubkey,
+
+    /// The amount of tokens this account holds.
+    pub amount: [u8; 8],
+
+    /// If `delegate` is `Some` then `delegated_amount` represents
+    /// the amount authorized by the delegate
+    pub delegate: PodCOption<Pubkey>,
+
+    /// The account's state
+    pub state: u8,
+
+    /// If is_native.is_some, this is a native token, and the value logs the
+    /// rent-exempt reserve. An Account is required to be rent-exempt, so
+    /// the value is used by the Processor to ensure that wrapped SOL
+    /// accounts do not drop below this threshold.
+    pub is_native: PodCOption<[u8; 8]>,
+
+    /// The amount delegated
+    pub delegated_amount: [u8; 8],
+
+    /// Optional authority to close the account.
+    pub close_authority: PodCOption<Pubkey>,
+}
+
+impl Account {
+    #[inline]
+    pub fn is_initialized(&self) -> bool {
+        self.state != AccountState::Uninitialized as u8
+    }
+
+    #[inline]
+    pub fn is_frozen(&self) -> bool {
+        self.state == AccountState::Frozen as u8
+    }
+}
+
+/// Account state.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub enum AccountState {
+    /// Account is not yet initialized
+    #[default]
+    Uninitialized,
+
+    /// Account is initialized; the account owner and/or delegate may perform
+    /// permitted operations on this account
+    Initialized,
+
+    /// Account has been frozen by the mint freeze authority. Neither the
+    /// account owner nor the delegate are able to perform operations on
+    /// this account.
+    Frozen,
+}
+
+impl From<u8> for AccountState {
+    fn from(value: u8) -> Self {
+        match value {
+            0 => AccountState::Uninitialized,
+            1 => AccountState::Initialized,
+            2 => AccountState::Frozen,
+            _ => panic!("invalid account state value: {value}"),
+        }
+    }
+}
+
+impl From<AccountState> for u8 {
+    fn from(value: AccountState) -> Self {
+        match value {
+            AccountState::Uninitialized => 0,
+            AccountState::Initialized => 1,
+            AccountState::Frozen => 2,
+        }
+    }
+}

+ 27 - 0
p-token/src/state/mint.rs

@@ -0,0 +1,27 @@
+use bytemuck::{Pod, Zeroable};
+use pinocchio::pubkey::Pubkey;
+
+use super::{PodBool, PodCOption};
+
+/// Mint data.
+#[repr(C)]
+#[derive(Clone, Copy, Default, Pod, Zeroable)]
+pub struct Mint {
+    /// Optional authority used to mint new tokens. The mint authority may only
+    /// be provided during mint creation. If no mint authority is present
+    /// then the mint has a fixed supply and no further tokens may be
+    /// minted.
+    pub mint_authority: PodCOption<Pubkey>,
+
+    /// Total supply of tokens.
+    pub supply: [u8; 8],
+
+    /// Number of base 10 digits to the right of the decimal place.
+    pub decimals: u8,
+
+    /// Is `true` if this structure has been initialized
+    pub is_initialized: PodBool,
+
+    /// Optional authority to freeze token accounts.
+    pub freeze_authority: PodCOption<Pubkey>,
+}

+ 118 - 0
p-token/src/state/mod.rs

@@ -0,0 +1,118 @@
+use std::mem::align_of;
+
+use bytemuck::{Pod, Zeroable};
+
+pub mod account;
+pub mod mint;
+pub mod multisignature;
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct PodCOption<T: Default + PartialEq + Pod + Sized> {
+    /// Indicates if the option is `Some` or `None`.
+    tag: [u8; 4],
+
+    /// The value of the option.
+    value: T,
+}
+
+impl<T: Default + PartialEq + Pod + Sized> From<Option<T>> for PodCOption<T> {
+    fn from(value: Option<T>) -> Self {
+        if align_of::<T>() != 1 {
+            panic!("PodCOption only supports Pod types with alignment 1");
+        }
+
+        match value {
+            Some(value) => Self {
+                tag: [1, 0, 0, 0],
+                value,
+            },
+            None => Self {
+                tag: [0, 0, 0, 0],
+                value: T::default(),
+            },
+        }
+    }
+}
+
+impl<T: Default + PartialEq + Pod + Sized> PodCOption<T> {
+    /// Returns `true` if the option is a `None` value.
+    #[inline]
+    pub fn is_none(&self) -> bool {
+        self.tag == [0, 0, 0, 0]
+    }
+
+    /// Returns `true` if the option is a `Some` value.
+    #[inline]
+    pub fn is_some(&self) -> bool {
+        !self.is_none()
+    }
+
+    /// Returns the contained value as an `Option`.
+    #[inline]
+    pub fn get(self) -> Option<T> {
+        if self.is_none() {
+            None
+        } else {
+            Some(self.value)
+        }
+    }
+
+    /// Returns the contained value as an `Option`.
+    #[inline]
+    pub fn as_ref(&self) -> Option<&T> {
+        if self.is_none() {
+            None
+        } else {
+            Some(&self.value)
+        }
+    }
+
+    /// Returns the contained value as a mutable `Option`.
+    #[inline]
+    pub fn as_mut(&mut self) -> Option<&mut T> {
+        if self.is_none() {
+            None
+        } else {
+            Some(&mut self.value)
+        }
+    }
+}
+
+/// ## Safety
+///
+/// `PodCOption` requires a `Pod` type `T` with alignment of 1.
+unsafe impl<T: Default + PartialEq + Pod + Sized> Pod for PodCOption<T> {}
+
+/// ## Safety
+///
+/// `PodCOption` requires a `Pod` type `T` with alignment of 1.
+unsafe impl<T: Default + PartialEq + Pod + Sized> Zeroable for PodCOption<T> {}
+
+#[repr(C)]
+#[derive(Copy, Clone, Default, Pod, Zeroable)]
+pub struct PodBool(u8);
+
+impl From<bool> for PodBool {
+    fn from(b: bool) -> Self {
+        Self(b.into())
+    }
+}
+
+impl From<&bool> for PodBool {
+    fn from(b: &bool) -> Self {
+        Self((*b).into())
+    }
+}
+
+impl From<&PodBool> for bool {
+    fn from(b: &PodBool) -> Self {
+        b.0 != 0
+    }
+}
+
+impl From<PodBool> for bool {
+    fn from(b: PodBool) -> Self {
+        b.0 != 0
+    }
+}

+ 27 - 0
p-token/src/state/multisignature.rs

@@ -0,0 +1,27 @@
+use bytemuck::{Pod, Zeroable};
+use pinocchio::pubkey::Pubkey;
+
+use super::PodBool;
+
+/// Minimum number of multisignature signers (min N)
+pub const MIN_SIGNERS: usize = 1;
+/// Maximum number of multisignature signers (max N)
+pub const MAX_SIGNERS: usize = 11;
+
+/// Multisignature data.
+#[repr(C)]
+#[derive(Clone, Copy, Default, Pod, Zeroable)]
+pub struct Multisig {
+    /// Number of signers required
+    pub m: u8,
+    /// Number of valid signers
+    pub n: u8,
+    /// Is `true` if this structure has been initialized
+    pub is_initialized: PodBool,
+    /// Signer public keys
+    pub signers: [Pubkey; MAX_SIGNERS],
+}
+
+impl Multisig {
+    pub const LEN: usize = core::mem::size_of::<Multisig>();
+}

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

@@ -0,0 +1,93 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::mint;
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    system_instruction,
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "token-light")]
+#[tokio::test]
+async fn initialize_account(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Pubkey::new_unique();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority,
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // Given a mint authority, freeze authority and an account keypair.
+
+    let owner = Pubkey::new_unique();
+    let account = Keypair::new();
+
+    let account_size = 165;
+    let rent = context.banks_client.get_rent().await.unwrap();
+
+    let mut initialize_ix = spl_token::instruction::initialize_account(
+        &spl_token::ID,
+        &account.pubkey(),
+        &mint,
+        &owner,
+    )
+    .unwrap();
+    // Switches the program id to the token program.
+    initialize_ix.program_id = token_program;
+
+    // When a new mint account is created and initialized.
+
+    let instructions = vec![
+        system_instruction::create_account(
+            &context.payer.pubkey(),
+            &account.pubkey(),
+            rent.minimum_balance(account_size),
+            account_size as u64,
+            &token_program,
+        ),
+        initialize_ix,
+    ];
+
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &account],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then an account has the correct data.
+
+    let account = context
+        .banks_client
+        .get_account(account.pubkey())
+        .await
+        .unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let account = spl_token::state::Account::unpack(&account.data).unwrap();
+
+    assert!(!account.is_frozen());
+    assert!(account.owner == owner);
+    assert!(account.mint == mint);
+}

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

@@ -0,0 +1,83 @@
+#![cfg(feature = "test-sbf")]
+
+use std::mem::size_of;
+
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_option::COption,
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    system_instruction,
+    transaction::Transaction,
+};
+use token_program::state::mint::Mint;
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "token-light")]
+#[tokio::test]
+async fn initialize_mint(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint authority, freeze authority and an account keypair.
+
+    let mint_authority = Pubkey::new_unique();
+    let freeze_authority = Pubkey::new_unique();
+    let account = Keypair::new();
+
+    let account_size = size_of::<Mint>();
+    let rent = context.banks_client.get_rent().await.unwrap();
+
+    let mut initialize_ix = spl_token::instruction::initialize_mint(
+        &spl_token::ID,
+        &account.pubkey(),
+        &mint_authority,
+        Some(&freeze_authority),
+        0,
+    )
+    .unwrap();
+    // Switches the program id to the token program.
+    initialize_ix.program_id = token_program;
+
+    // When a new mint account is created and initialized.
+
+    let instructions = vec![
+        system_instruction::create_account(
+            &context.payer.pubkey(),
+            &account.pubkey(),
+            rent.minimum_balance(account_size),
+            account_size as u64,
+            &token_program,
+        ),
+        initialize_ix,
+    ];
+
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &account],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then an account has the correct data.
+
+    let account = context
+        .banks_client
+        .get_account(account.pubkey())
+        .await
+        .unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let mint = spl_token::state::Mint::unpack(&account.data).unwrap();
+
+    assert!(mint.is_initialized);
+    assert!(mint.mint_authority == COption::Some(mint_authority));
+    assert!(mint.freeze_authority == COption::Some(freeze_authority));
+    assert!(mint.decimals == 0)
+}

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

@@ -0,0 +1,77 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "token-light")]
+#[tokio::test]
+async fn mint_to(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program)
+        .await
+        .unwrap();
+
+    // When we mint tokens to it.
+
+    let mut mint_ix = spl_token::instruction::mint_to(
+        &spl_token::ID,
+        &mint,
+        &account,
+        &mint_authority.pubkey(),
+        &[],
+        100,
+    )
+    .unwrap();
+    // Switches the program id to the token program.
+    mint_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[mint_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &mint_authority],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then an account has the correct data.
+
+    let account = context.banks_client.get_account(account).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let account = spl_token::state::Account::unpack(&account.data).unwrap();
+
+    assert!(account.amount == 100);
+}

+ 43 - 0
p-token/tests/setup/account.rs

@@ -0,0 +1,43 @@
+use solana_program_test::ProgramTestContext;
+use solana_sdk::{
+    program_error::ProgramError, pubkey::Pubkey, signature::Keypair, signer::Signer,
+    system_instruction, transaction::Transaction,
+};
+
+pub async fn initialize(
+    context: &mut ProgramTestContext,
+    mint: &Pubkey,
+    owner: &Pubkey,
+    program_id: &Pubkey,
+) -> Result<Pubkey, ProgramError> {
+    let account = Keypair::new();
+
+    let account_size = 165;
+    let rent = context.banks_client.get_rent().await.unwrap();
+
+    let mut initialize_ix =
+        spl_token::instruction::initialize_account(&spl_token::ID, &account.pubkey(), mint, owner)
+            .unwrap();
+    initialize_ix.program_id = *program_id;
+
+    let instructions = vec![
+        system_instruction::create_account(
+            &context.payer.pubkey(),
+            &account.pubkey(),
+            rent.minimum_balance(account_size),
+            account_size as u64,
+            program_id,
+        ),
+        initialize_ix,
+    ];
+
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &account],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    Ok(account.pubkey())
+}

+ 84 - 0
p-token/tests/setup/mint.rs

@@ -0,0 +1,84 @@
+use std::mem::size_of;
+
+use solana_program_test::{BanksClientError, ProgramTestContext};
+use solana_sdk::{
+    program_error::ProgramError, pubkey::Pubkey, signature::Keypair, signer::Signer,
+    system_instruction, transaction::Transaction,
+};
+use token_program::state::mint::Mint;
+
+pub async fn initialize(
+    context: &mut ProgramTestContext,
+    mint_authority: Pubkey,
+    freeze_authority: Option<Pubkey>,
+    program_id: &Pubkey,
+) -> Result<Pubkey, ProgramError> {
+    // Mint account keypair.
+    let account = Keypair::new();
+
+    let account_size = size_of::<Mint>();
+    let rent = context.banks_client.get_rent().await.unwrap();
+
+    let mut initialize_ix = spl_token::instruction::initialize_mint(
+        &spl_token::ID,
+        &account.pubkey(),
+        &mint_authority,
+        freeze_authority.as_ref(),
+        0,
+    )
+    .unwrap();
+    // Switches the program id in case we are using a "custom" one.
+    initialize_ix.program_id = *program_id;
+
+    // Create a new account and initialize as a mint.
+
+    let instructions = vec![
+        system_instruction::create_account(
+            &context.payer.pubkey(),
+            &account.pubkey(),
+            rent.minimum_balance(account_size),
+            account_size as u64,
+            program_id,
+        ),
+        initialize_ix,
+    ];
+
+    let tx = Transaction::new_signed_with_payer(
+        &instructions,
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &account],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    Ok(account.pubkey())
+}
+
+pub async fn mint(
+    context: &mut ProgramTestContext,
+    mint: &Pubkey,
+    account: &Pubkey,
+    mint_authority: &Keypair,
+    amount: u64,
+    program_id: &Pubkey,
+) -> Result<(), BanksClientError> {
+    let mut mint_ix = spl_token::instruction::mint_to(
+        &spl_token::ID,
+        mint,
+        account,
+        &mint_authority.pubkey(),
+        &[],
+        amount,
+    )
+    .unwrap();
+    // Switches the program id to the token program.
+    mint_ix.program_id = *program_id;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[mint_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, mint_authority],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await
+}

+ 4 - 0
p-token/tests/setup/mod.rs

@@ -0,0 +1,4 @@
+#[allow(dead_code)]
+pub mod account;
+#[allow(dead_code)]
+pub mod mint;

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

@@ -0,0 +1,94 @@
+#![cfg(feature = "test-sbf")]
+
+mod setup;
+
+use setup::{account, mint};
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{
+    program_pack::Pack,
+    pubkey::Pubkey,
+    signature::{Keypair, Signer},
+    transaction::Transaction,
+};
+
+#[test_case::test_case(spl_token::ID ; "spl-token")]
+#[test_case::test_case(Pubkey::new_from_array(token_program::ID) ; "token-light")]
+#[tokio::test]
+async fn transfer(token_program: Pubkey) {
+    let program_id = Pubkey::new_from_array(token_program::ID);
+    let mut context = ProgramTest::new("token_program", program_id, None)
+        .start_with_context()
+        .await;
+
+    // Given a mint account.
+
+    let mint_authority = Keypair::new();
+    let freeze_authority = Pubkey::new_unique();
+
+    let mint = mint::initialize(
+        &mut context,
+        mint_authority.pubkey(),
+        Some(freeze_authority),
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // And a token account with 100 tokens.
+
+    let owner = Keypair::new();
+
+    let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program)
+        .await
+        .unwrap();
+
+    mint::mint(
+        &mut context,
+        &mint,
+        &account,
+        &mint_authority,
+        100,
+        &token_program,
+    )
+    .await
+    .unwrap();
+
+    // When we transfer the tokens.
+
+    let destination = Pubkey::new_unique();
+
+    let destination_account =
+        account::initialize(&mut context, &mint, &destination, &token_program)
+            .await
+            .unwrap();
+
+    let mut transfer_ix = spl_token::instruction::transfer(
+        &spl_token::ID,
+        &account,
+        &destination_account,
+        &owner.pubkey(),
+        &[],
+        100,
+    )
+    .unwrap();
+    transfer_ix.program_id = token_program;
+
+    let tx = Transaction::new_signed_with_payer(
+        &[transfer_ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &owner],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then an account has the correct data.
+
+    let account = context.banks_client.get_account(account).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    let account = spl_token::state::Account::unpack(&account.data).unwrap();
+
+    assert!(account.amount == 0);
+}