The Escrow
An escrow is a powerful financial tool that enables secure token swaps between two parties.
Think of it as a digital safe deposit box where one user can lock up Token A, waiting for another user to deposit Token B before the swap is completed.
This creates a trustless environment where neither party needs to worry about the other backing out of the deal.
In this challenge, we're going to implement this concept through three simple but powerful instructions:
- Make: The maker (first user) defines the trade terms and deposits the agreed amount of Token A into a secure vault. This is like putting your item in the safe deposit box and setting the terms of the exchange.
- Take: The taker (second user) accepts the offer by transferring the promised amount of Token B to the maker, and in return, receives the locked Token A. This is the moment when both parties complete their side of the deal.
- Refund: If the maker changes their mind or no suitable taker is found, they can cancel the offer and retrieve their Token A. This is like getting your item back from the safe deposit box if the deal falls through.
Note: 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
Let's start by creating a fresh Rust environment:
# create workspace
cargo new blueshift_escrow --lib --edition 2021
cd blueshift_escrow
Add pinocchio, pinocchio-system, pinocchio-token and pinocchio-associated-token:
cargo add pinocchio pinocchio-system pinocchio-token pinocchio-associated-token-account
Declare the crate types in Cargo.toml
to generate deployment artifacts in target/deploy
:
[lib]
crate-type = ["lib", "cdylib"]
You're now ready to write your escrow program.
Template
This time we'll split the program into small, focused modules instead of cramming everything into the lib.rs
. The folder tree will look roughly like this:
src
├── instructions
│ ├── make.rs
│ ├── helpers.rs
│ ├── mod.rs
│ ├── refund.rs
│ └── take.rs
├── errors.rs
├── lib.rs
└── state.rs
The entrypoint, that lives in the lib.rs
looks very similar to what we did in the last lessons so we're going to go over it very quickly:
use pinocchio::{account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, ProgramResult};
entrypoint!(process_instruction);
pub mod instructions;
pub use instructions::*;
pub mod state;
pub use state::*;
// 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((Make::DISCRIMINATOR, data)) => Make::try_from((data, accounts))?.process(),
Some((Take::DISCRIMINATOR, _)) => Take::try_from(accounts)?.process(),
Some((Refund::DISCRIMINATOR, _)) => Refund::try_from(accounts)?.process(),
_ => Err(ProgramError::InvalidInstructionData)
}
}
State
We're going to move into the state.rs
where all the data for our Escrow
lives. Let's break this down into two parts: the struct definition and its implementation.
First, let's look at the struct definition:
use pinocchio::{account_info::{AccountInfo, Ref}, program_error::ProgramError, pubkey::Pubkey};
use core::mem::size_of;
#[repr(C)]
pub struct Escrow {
pub seed: u64, // Random seed for PDA derivation
pub maker: Pubkey, // Creator of the escrow
pub mint_a: Pubkey, // Token being deposited
pub mint_b: Pubkey, // Token being requested
pub receive: u64, // Amount of token B wanted
pub bump: [u8;1] // PDA bump seed
}
The #[repr(C)]
attribute ensures our struct has a predictable memory layout, which is crucial for on-chain data. Each field serves a specific purpose:
- seed: A random number that allows one maker to create multiple escrows with the same token pair
- maker: The wallet address that created the escrow and will receive the tokens
- mint_a: The SPL token mint address for the token being deposited
- mint_b: The SPL token mint address for the token being requested
- receive: The exact amount of token B that the maker wants to receive
- bump: A single byte used in PDA derivation to ensure the address is on the ed25519 curve
Now, let's look at the implementation with all its helper methods:
impl Escrow {
pub const LEN: usize = size_of::<u64>()
+ size_of::<Pubkey>()
+ size_of::<Pubkey>()
+ size_of::<Pubkey>()
+ size_of::<u64>()
+ size_of::<[u8;1]>();
#[inline(always)]
pub fn load_mut(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> {
if bytes.len() != Escrow::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(unsafe { &mut *core::mem::transmute::<*mut u8, *mut Self>(bytes.as_mut_ptr()) })
}
#[inline(always)]
pub fn load(bytes: &[u8]) -> Result<&Self, ProgramError> {
if bytes.len() != Escrow::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(unsafe { &*core::mem::transmute::<*const u8, *const Self>(bytes.as_ptr()) })
}
#[inline(always)]
pub fn set_seed(&mut self, seed: u64) {
self.seed = seed;
}
#[inline(always)]
pub fn set_maker(&mut self, maker: Pubkey) {
self.maker = maker;
}
#[inline(always)]
pub fn set_mint_a(&mut self, mint_a: Pubkey) {
self.mint_a = mint_a;
}
#[inline(always)]
pub fn set_mint_b(&mut self, mint_b: Pubkey) {
self.mint_b = mint_b;
}
#[inline(always)]
pub fn set_receive(&mut self, receive: u64) {
self.receive = receive;
}
#[inline(always)]
pub fn set_bump(&mut self, bump: [u8;1]) {
self.bump = bump;
}
#[inline(always)]
pub fn set_inner(&mut self, seed: u64, maker: Pubkey, mint_a: Pubkey, mint_b: Pubkey, receive: u64, bump: [u8;1]) {
self.seed = seed;
self.maker = maker;
self.mint_a = mint_a;
self.mint_b = mint_b;
self.receive = receive;
self.bump = bump;
}
}
The implementation provides several key features:
- Exact Size Calculation:
LEN
precisely calculates the account size by summing each field's size - Safe Loading:
from_account_info
provides a safe way to load and validate escrow data - Performance Optimizations:
#[inline(always)]
on getters for maximum performance- Unsafe methods for when we know the borrow is safe
- Efficient field setting with
set_inner
- Memory Safety: Proper validation of account data length and ownership
- Documentation: Clear comments explaining the purpose and safety considerations of each method
This implementation ensures our escrow state is both safe and efficient, with proper validation and performance optimizations where appropriate.