Week 3 • Escrow Mechanism Project
Project: Escrow with PDA Vault
Build an Anchor escrow flow with deterministic vault PDAs, explicit state transitions, cancellation paths, and safer release logic.
Escrow is a perfect intermediate Anchor project because it combines:
- PDA state accounts
- Token CPI transfers
- Clear state machine design
Problem Statement
A maker deposits tokens into escrow. A taker can fulfill conditions. Then funds release according to program rules.
Minimal Escrow State
#[account]
#[derive(InitSpace)]
pub struct Escrow {
pub maker: Pubkey,
pub taker: Pubkey,
pub mint: Pubkey,
pub amount: u64,
pub status: EscrowStatus,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)]
pub enum EscrowStatus {
Initialized,
Funded,
Completed,
Cancelled,
}PDA Design
- Escrow PDA:
b"escrow" + maker + nonce - Vault authority PDA:
b"vault-authority" + escrow_pubkey - Vault ATA owned by vault authority PDA
Keep seed formulas fixed and documented.
Instruction Flow
initialize_escrow- writes escrow configfund_escrow- maker transfers tokens into vaultcomplete_escrow- releases vault tokens to takercancel_escrow- returns funds to maker if rules permit
Release Guardrails
- Only valid actor can transition current state
statusmust match expected prior state- Transfer amount must match escrow agreement
- All token accounts must be validated against expected mint
require!(escrow.status == EscrowStatus::Funded, EscrowError::InvalidState);
require_keys_eq!(escrow.taker, taker.key(), EscrowError::Unauthorized);Common Escrow Bugs
- Missing state transition checks (double release)
- Using client-side booleans instead of on-chain status
- Forgetting to close escrow account and reclaim rent
- Not testing race-like repeated calls
If an instruction can be called twice, assume someone will call it twice. Protect every state transition.
Test Matrix
- Maker initializes + funds successfully
- Unauthorized taker cannot complete
- Correct taker completes once
- Second complete attempt fails
- Cancel path only works in valid states
Try This Next
- Add expiry timestamp and allow timeout-based cancellation.
- Support partial fills with remaining balance tracking.
- Emit events for each state transition and index them in your backend.