Anchor
Anchor Vault

Anchor Vault

108 Graduates

Anchor Vault

Anchor 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 Anchor, you should start by reading the Introduction to Anchor to familiarize with the core concept that we're going to use in this program.

Installation

Before you begin, be sure Rust and Anchor are installed (see the official documentation if you need a refresher). Then in your terminal run:

anchor init blueshift_anchor_vault

We don't need any additional crate for this challenge, so you can now open the newly generated folder, and you're ready to start coding!

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
declare_id!("22222222222222222222222222222222222222222222");
 
#[program]
pub mod blueshift_anchor_vault {
  use super::*;
 
  pub fn deposit(ctx: Context<VaultAction>, amount: u64) -> Result<()> {
    // deposit logic
    Ok(())
  }
 
  pub fn withdraw(ctx: Context<VaultAction>) -> Result<()> {
    // withdraw logic
    Ok(())
  }
}
 
#[derive(Accounts)]
pub struct VaultAction<'info> {
  #[account(mut)]
  pub signer: Signer<'info>,
  #[account(
    mut,
    seeds = [b"vault", signer.key().as_ref()],
    bump,
  )]
  pub vault: SystemAccount<'info>,
  pub system_program: Program<'info, System>,
}
 
#[error_code]
pub enum VaultError {
  // error enum
}

Note: remember to chagne the program ID to 22222222222222222222222222222222222222222222 since we use this under the hood to test your program.

Accounts

Since both instruction use the same accounts, to make it easier and more readable, we can just create one context called VaultAction and use it for both deposit and withdraw.

The VaultAction account struct will need to have:

  • signer: this is the owner of the vault, and the only person that can withdraw the lamports after creating the vault.
  • vault: a PDA derived from the following seeds: [b"vault", signer.key().as_ref()] that holds the lamports for the signer.
  • system_program: the system program account that needs to be included since we're going to use the transfer instruction CPI from the system program

Here's how we define the account struct:

rust
#[derive(Accounts)]
pub struct VaultAction<'info> {
  #[account(mut)]
  pub signer: Signer<'info>,
  #[account(
    mut,
    seeds = [b"vault", signer.key().as_ref()],
    bump,
  )]
  pub vault: SystemAccount<'info>,
  pub system_program: Program<'info, System>,
}

Let's break down each account constraint:

  1. signer: The mut constraint is needed because we'll be modifying its lamports during transfers.
  2. vault:
  • mut because we'll be modifying its lamports
  • seeds & bumps defines how to derive a valid PDA from the seeds
  1. system_program: checks if the account is set to executable and that the address is the System Program one

Errors

We don't need a lot of errors for this small programs, so we're just going to create 2 enums:

  • VaultAlreadyExists: that let us know if there are already some lamports in the account since it would mean that the vault exists already.
  • InvalidAmount: we can't deposit an amount that is less than the minimum rent for a basic account, so we check that the amount is greater than that.

It will look something like this:

rust
#[error_code]
pub enum VaultError {
  #[msg("Vault already exists")]
  VaultAlreadyExists,
  #[msg("Invalid amount")]
  InvalidAmount,
}

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 SystemAccount
  3. Transfers lamports from the signer to the vault using a CPI to the System Program

Let's implement these checks first:

rust
// Check if vault is empty
require_eq!(ctx.accounts.vault.lamports(), 0, VaultError::VaultAlreadyExists);
 
// Ensure amount exceeds rent-exempt minimum
require_gt!(amount, Rent::get()?.minimum_balance(0), VaultError::InvalidAmount);

The two require macros act like custom guard clauses:

  • require_eq! confirms the vault is empty (preventing double deposits).
  • require_gt! checks the amount clears the rent-exempt threshold.

Once the checks pass, Anchor's System Program helper calls the Transfer CPI like this:

rust
use anchor_lang::system_program::{transfer, Transfer};
 
transfer(
  CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    Transfer {
      from: ctx.accounts.signer.to_account_info(),
      to: ctx.accounts.vault.to_account_info(),
    }
  ), amount)?;

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 signer

First, let's check if the vault has any lamports to withdraw:

rust
// Check if vault has any lamports
require_neq!(ctx.accounts.vault.lamports(), 0, VaultError::InvalidAmount);

Then, we need to create the PDA signer seeds and perform the transfer:

rust
// Create PDA signer seeds
let signer_key = ctx.accounts.signer.key();
let signer_seeds = &[b"vault", signer_key.as_ref(), &[ctx.bumps.vault]];
 
// Transfer all lamports from vault to signer
transfer(
  CpiContext::new_with_signer(
    ctx.accounts.system_program.to_account_info(),
    Transfer {
      from: ctx.accounts.vault.to_account_info(),
      to: ctx.accounts.signer.to_account_info(),
    },
    &[&signer_seeds[..]]
  ),
  ctx.accounts.vault.lamports()
)?;

The security of this withdrawal is guaranteed by two factors:

  1. The vault's PDA is derived using the signer'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 CpiContext::new_with_signer

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

anchor build

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