Rust
Pinocchio Vault

Pinocchio Vault

56 Graduates

Pinocchio Vault

Pinocchio Vault Challenge

The Vault

A vault allows users to securely store their assets. A vault is a fundamental building block in DeFi that, at its core, allows users to securely store their assets (lamports in this case) that only that same user can withdraw later.

In this challenge, we'll build a simple lamport vault that demonstrates how to work with basic accounts, Program Derived Addresses (PDAs), and Cross-Program Invocation (CPI). If you're not familiar with Pinocchio, you should start by reading the Introduction to Pinocchio to familiarize with the core concepts that we're going to use in this program.

Installation

Before you begin, be sure Rust and Pinocchio are installed. Then in your terminal run:

# create workspace
cargo new blueshift_vault --lib --edition 2021
cd blueshift_vault

Add Pinocchio:

cargo add pinocchio pinocchio-system

Declare the crate types in Cargo.toml to generate deployment artifacts in target/deploy:

[lib]
crate-type = ["lib", "cdylib"]

Template

Let's start with the basic program structure. We'll implement everything in lib.rs since this is a straightforward program. Here's the initial template with the core components we'll need:

rust
#![no_std]
 
use pinocchio::{account_info::AccountInfo, entrypoint, nostd_panic_handler, program_error::ProgramError, pubkey::Pubkey, ProgramResult};
 
entrypoint!(process_instruction);
nostd_panic_handler!();
 
pub mod instructions;
pub use instructions::*;
 
// 22222222222222222222222222222222222222222222
pub const ID: Pubkey = [
    0x0f, 0x1e, 0x6b, 0x14, 0x21, 0xc0, 0x4a, 0x07,
    0x04, 0x31, 0x26, 0x5c, 0x19, 0xc5, 0xbb, 0xee,
    0x19, 0x92, 0xba, 0xe8, 0xaf, 0xd1, 0xcd, 0x07,
    0x8e, 0xf8, 0xaf, 0x70, 0x47, 0xdc, 0x11, 0xf7,
];
 
fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    match instruction_data.split_first() {
        Some((Deposit::DISCRIMINATOR, data)) => Deposit::try_from((data, accounts))?.process(),
        Some((Withdraw::DISCRIMINATOR, _)) => Withdraw::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

Deposit

The deposit instruction performs the following steps:

  1. Verifies the vault is empty (has zero lamports) to prevent double deposits
  2. Ensures the deposit amount exceeds the rent-exempt minimum for a basic account
  3. Transfers lamports from the owner to the vault using a CPI to the System Program

First, let's define the account struct for deposit:

rust
pub struct DepositAccounts<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
}
 
impl<'a> TryFrom<&'a [AccountInfo]> for DepositAccounts<'a> {
    type Error = ProgramError;
 
    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let [owner, vault, _] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };
 
        // Accounts Checks
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }
 
        if !vault.is_owned_by(&pinocchio_system::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }
 
        if vault.lamports().ne(&0) {
            return Err(ProgramError::InvalidAccountData);
        }
 
        let (vault_key, _) = find_program_address(&[b"vault", owner.key()], &crate::ID);
        if vault.key().ne(&vault_key) {
            return Err(ProgramError::InvalidAccountOwner);
        }
 
        // Return the accounts
        Ok(Self { owner, vault })
    }
}

Let's break down each account check:

  1. owner: Must be a signer since they need to authorize the transaction
  2. vault:
    • Must be owned by the System Program
    • Must have zero lamports (ensures "fresh" deposit)
    • Must be derived from the correct seeds
    • Must match the expected PDA address

Now let's implement the instruction data struct:

rust
pub struct DepositInstructionData {
    pub amount: u64,
}
 
impl<'a> TryFrom<&'a [u8]> for DepositInstructionData {
    type Error = ProgramError;
 
    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() != size_of::<u64>() {
            return Err(ProgramError::InvalidInstructionData);
        }
 
        let amount = u64::from_le_bytes(data.try_into().unwrap());
 
        // Instruction Checks
        if amount.eq(&0) {
            return Err(ProgramError::InvalidInstructionData);
        }
 
        Ok(Self { amount })
    }
}

Here we just check that the amount is different from zero.

Finally, let's implement the deposit instruction:

rust
pub struct Deposit<'a> {
    pub accounts: DepositAccounts<'a>,
    pub instruction_data: DepositInstructionData,
}
 
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
    type Error = ProgramError;
 
    fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
        let accounts = DepositAccounts::try_from(accounts)?;
        let instruction_data = DepositInstructionData::try_from(data)?;
 
        Ok(Self {
            accounts,
            instruction_data,
        })
    }
}
 
impl<'a> Deposit<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;
 
    pub fn process(&mut self) -> ProgramResult {
        Transfer {
            from: self.accounts.owner,
            to: self.accounts.vault,
            lamports: self.instruction_data.amount,
        }
        .invoke()?;
 
        Ok(())
    }
}

Withdraw

The withdraw instruction performs the following steps:

  1. Verifies the vault contains lamports (is not empty)
  2. Uses the vault's PDA to sign the transfer on its own behalf
  3. Transfers all lamports from the vault back to the owner

First, let's define the account struct for withdraw:

rust
pub struct WithdrawAccounts<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
    pub bumps: [u8; 1],
}
 
impl<'a> TryFrom<&'a [AccountInfo]> for WithdrawAccounts<'a> {
    type Error = ProgramError;
 
    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let [owner, vault, _] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };
 
        // Basic Accounts Checks
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }
 
        if !vault.is_owned_by(&pinocchio_system::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }
 
        if self.accounts.vault.lamports().eq(&0) {
            return Err(ProgramError::InvalidAccountData);
        }
 
        let (vault_key, bump) = find_program_address(&[b"vault", owner.key().as_ref()], &crate::ID);
        if &vault_key != vault.key() {
            return Err(ProgramError::InvalidAccountOwner);
        }
 
        Ok(Self { owner, vault, bumps: [bump] })
    }
}

Let's break down each account check:

  1. owner: Must be a signer since they need to authorize the transaction
  2. vault:
    • Must be owned by the System Program
    • Must be derived from the correct seeds
    • Must match the expected PDA address
  3. bumps: We store the bump seed for use in the PDA signing

Now let's implement the withdraw instruction:

rust
pub struct Withdraw<'a> {
    pub accounts: WithdrawAccounts<'a>,
}
 
impl<'a> TryFrom<&'a [AccountInfo]> for Withdraw<'a> {
    type Error = ProgramError;
 
    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        let accounts = WithdrawAccounts::try_from(accounts)?;
 
        Ok(Self { accounts })
    }
}
 
impl<'a> Withdraw<'a> {
    pub const DISCRIMINATOR: &'a u8 = &1;
 
    pub fn process(&mut self) -> ProgramResult {
        // Create PDA signer seeds
        let seeds = [
            Seed::from(b"vault"),
            Seed::from(self.accounts.owner.key().as_ref()),
            Seed::from(&self.accounts.bumps),
        ];
        let signers = [Signer::from(&seeds)];
 
        // Transfer all lamports from vault to owner
        Transfer {
            from: self.accounts.vault,
            to: self.accounts.owner,
            lamports: self.accounts.vault.lamports(),
        }
        .invoke_signed(&signers)?;
 
        Ok(())
    }
}

The security of this withdrawal is guaranteed by two factors:

  1. The vault's PDA is derived using the owner's public key, ensuring only the original depositor can withdraw
  2. The PDA's ability to sign the transfer is verified through the seeds we provide to invoke_signed

Conclusion

You can now test your program against our unit tests and claim your NFTs!

Start by building your program using the following command in your terminal:

cargo build-sbf

This generated a .so file directly in your target/deploy folder.

Now click on the take challenge button and drop the file there!

Ready to take the challenge?
Contents
View Source
Blueshift © 2025Commit: cc5f933