Raj

@rajgoesout

Token Vault Example

PDA-controlled token custody with deposits and withdrawals.

Anchor Equivalent

#[program]
pub mod vault {
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.vault.authority = ctx.accounts.payer.key();
        ctx.accounts.vault.mint = ctx.accounts.mint.key();
        ctx.accounts.vault.total = 0;
        ctx.accounts.vault.bump = ctx.bumps.vault;
        Ok(())
    }

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        token::transfer(ctx.accounts.transfer_ctx(), amount)?;
        ctx.accounts.vault.total += amount;
        Ok(())
    }

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        token::transfer(ctx.accounts.transfer_ctx_with_signer(), amount)?;
        ctx.accounts.vault.total -= amount;
        Ok(())
    }
}

State

use pinocchio::{AccountView, account::RefMut, error::ProgramError};

#[repr(C)]
pub struct Vault {
    pub authority: [u8; 32],
    pub mint: [u8; 32],
    pub total: u64,
    pub bump: u8,
}

impl Vault {
    pub const LEN: usize = 73;

    pub fn from_account_mut(account: &AccountView) -> Result<RefMut<Self>, ProgramError> {
        let data = account.try_borrow_mut()?;
        Ok(RefMut::map(data, |data| unsafe {
            &mut *(data.as_mut_ptr() as *mut Self)
        }))
    }
}

Program

#![no_std]

use pinocchio::{AccountView, Address, ProgramResult, entrypoint, error::ProgramError};
use pinocchio::cpi::{Signer, Seed};
use pinocchio::sysvars::Rent;
use pinocchio_system::instructions::CreateAccount;
use pinocchio_token::instructions::{InitializeAccount3, Transfer};

pub const ID: Address = pinocchio::address!("Vault11111111111111111111111111111111111111");

entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Address,
    accounts: &[AccountView],
    instruction_data: &[u8],
) -> ProgramResult {
    match instruction_data.split_first() {
        Some((&0, _)) => initialize(program_id, accounts),
        Some((&1, rest)) => {
            let amount = u64::from_le_bytes(rest[0..8].try_into().unwrap());
            deposit(accounts, amount)
        }
        Some((&2, rest)) => {
            let amount = u64::from_le_bytes(rest[0..8].try_into().unwrap());
            withdraw(accounts, amount)
        }
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

Initialize

fn initialize(program_id: &Address, accounts: &[AccountView]) -> ProgramResult {
    let [payer, vault, vault_token, mint, _token_program, _system_program, ..] = accounts else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    if !payer.is_signer() {
        return Err(ProgramError::MissingRequiredSignature);
    }

    if _token_program.address() != &pinocchio_token::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    if _system_program.address() != &pinocchio_system::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    let (vault_pda, vault_bump) = Address::find_program_address(
        &[b"vault", mint.address().as_ref()],
        program_id,
    );
    if vault.address() != &vault_pda {
        return Err(ProgramError::InvalidAccountData);
    }

    let vault_bump_bytes = [vault_bump];
    let vault_seeds: [Seed; 3] = [
        Seed::from(b"vault"),
        Seed::from(mint.address().as_ref()),
        Seed::from(&vault_bump_bytes),
    ];

    let rent = Rent::get()?;

    // Create vault PDA
    CreateAccount {
        from: payer,
        to: vault,
        lamports: rent.try_minimum_balance(Vault::LEN)?,
        space: Vault::LEN as u64,
        owner: program_id,
    }.invoke_signed(&[Signer::from(&vault_seeds)])?;

    // Create vault token account
    let (token_pda, token_bump) = Address::find_program_address(
        &[b"vault_token", vault.address().as_ref()],
        program_id,
    );
    if vault_token.address() != &token_pda {
        return Err(ProgramError::InvalidAccountData);
    }
    let token_bump_bytes = [token_bump];
    let token_seeds: [Seed; 3] = [
        Seed::from(b"vault_token"),
        Seed::from(vault.address().as_ref()),
        Seed::from(&token_bump_bytes),
    ];

    CreateAccount {
        from: payer,
        to: vault_token,
        lamports: rent.try_minimum_balance(165)?,
        space: 165,
        owner: &pinocchio_token::ID,
    }.invoke_signed(&[Signer::from(&token_seeds)])?;

    InitializeAccount3 {
        account: vault_token,
        mint,
        owner: vault.address(),
    }.invoke()?;

    // Init state
    let state = Vault::from_account_mut(vault)?;
    state.authority.copy_from_slice(payer.address().as_ref());
    state.mint.copy_from_slice(mint.address().as_ref());
    state.total = 0;
    state.bump = vault_bump;

    Ok(())
}

Deposit

fn deposit(accounts: &[AccountView], amount: u64) -> ProgramResult {
    let [vault, vault_token, user, user_token, _token_program, ..] = accounts else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    if _token_program.address() != &pinocchio_token::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    if !vault_token.owned_by(&pinocchio_token::ID) || !user_token.owned_by(&pinocchio_token::ID) {
        return Err(ProgramError::InvalidAccountOwner);
    }

    if !user.is_signer() {
        return Err(ProgramError::MissingRequiredSignature);
    }

    Transfer {
        from: user_token,
        to: vault_token,
        authority: user,
        amount,
    }.invoke()?;

    let mut state = Vault::from_account_mut(vault)?;
    state.total = state
        .total
        .checked_add(amount)
        .ok_or(ProgramError::ArithmeticOverflow)?;

    Ok(())
}

Withdraw

fn withdraw(accounts: &[AccountView], amount: u64) -> ProgramResult {
    let [vault, vault_token, user, user_token, _token_program, ..] = accounts else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    if _token_program.address() != &pinocchio_token::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    if !vault_token.owned_by(&pinocchio_token::ID) || !user_token.owned_by(&pinocchio_token::ID) {
        return Err(ProgramError::InvalidAccountOwner);
    }

    if !user.is_signer() {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let mut state = Vault::from_account_mut(vault)?;

    if state.authority != *user.address().as_ref() {
        return Err(ProgramError::InvalidAccountData);
    }

    // Extract values before creating seeds to avoid borrow issues
    let bump = state.bump;
    let mint = state.mint;

    let bump_bytes = [bump];
    let seeds: [Seed; 3] = [
        Seed::from(b"vault"),
        Seed::from(mint.as_slice()),
        Seed::from(&bump_bytes),
    ];

    Transfer {
        from: vault_token,
        to: user_token,
        authority: vault,
        amount,
    }.invoke_signed(&[Signer::from(&seeds)])?;

    state.total = state
        .total
        .checked_sub(amount)
        .ok_or(ProgramError::ArithmeticOverflow)?;

    Ok(())
}

Client (TypeScript)

const PROGRAM_ID = new PublicKey('Vault11111111111111111111111111111111111111');

function getVaultPDA(mint: PublicKey) {
  return PublicKey.findProgramAddressSync(
    [Buffer.from('vault'), mint.toBuffer()],
    PROGRAM_ID
  )[0];
}

function getVaultTokenPDA(vault: PublicKey) {
  return PublicKey.findProgramAddressSync(
    [Buffer.from('vault_token'), vault.toBuffer()],
    PROGRAM_ID
  )[0];
}

function initializeIx(payer: PublicKey, mint: PublicKey) {
  const vault = getVaultPDA(mint);
  const vaultToken = getVaultTokenPDA(vault);

  return new TransactionInstruction({
    programId: PROGRAM_ID,
    keys: [
      { pubkey: payer, isSigner: true, isWritable: true },
      { pubkey: vault, isSigner: false, isWritable: true },
      { pubkey: vaultToken, isSigner: false, isWritable: true },
      { pubkey: mint, isSigner: false, isWritable: false },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
    ],
    data: Buffer.from([0]),
  });
}

function depositIx(vault: PublicKey, user: PublicKey, userToken: PublicKey, amount: bigint) {
  const vaultToken = getVaultTokenPDA(vault);
  const data = Buffer.alloc(9);
  data.writeUInt8(1, 0);
  data.writeBigUInt64LE(amount, 1);

  return new TransactionInstruction({
    programId: PROGRAM_ID,
    keys: [
      { pubkey: vault, isSigner: false, isWritable: true },
      { pubkey: vaultToken, isSigner: false, isWritable: true },
      { pubkey: user, isSigner: true, isWritable: false },
      { pubkey: userToken, isSigner: false, isWritable: true },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
    ],
    data,
  });
}

function withdrawIx(vault: PublicKey, user: PublicKey, userToken: PublicKey, amount: bigint) {
  const vaultToken = getVaultTokenPDA(vault);
  const data = Buffer.alloc(9);
  data.writeUInt8(2, 0);
  data.writeBigUInt64LE(amount, 1);

  return new TransactionInstruction({
    programId: PROGRAM_ID,
    keys: [
      { pubkey: vault, isSigner: false, isWritable: true },
      { pubkey: vaultToken, isSigner: false, isWritable: true },
      { pubkey: user, isSigner: true, isWritable: false },
      { pubkey: userToken, isSigner: false, isWritable: true },
      { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
    ],
    data,
  });
}