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:
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:
#[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:
signer
: Themut
constraint is needed because we'll be modifying its lamports during transfers.vault
:
mut
because we'll be modifying its lamportsseeds
&bumps
defines how to derive a valid PDA from the seeds
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:
#[error_code]
pub enum VaultError {
#[msg("Vault already exists")]
VaultAlreadyExists,
#[msg("Invalid amount")]
InvalidAmount,
}
Deposit
The deposit instruction performs the following steps:
- Verifies the vault is empty (has zero lamports) to prevent double deposits
- Ensures the deposit amount exceeds the rent-exempt minimum for a
SystemAccount
- Transfers lamports from the signer to the vault using a CPI to the System Program
Let's implement these checks first:
// 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:
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:
- Verifies the vault contains lamports (is not empty)
- Uses the vault's PDA to sign the transfer on its own behalf
- Transfers all lamports from the vault back to the signer
First, let's check if the vault has any lamports to withdraw:
// 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:
// 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:
- The vault's PDA is derived using the signer's public key, ensuring only the original depositor can withdraw
- 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!