Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

13 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ” Anchor Escrow β€” Solana Smart Contract

A trustless token swap program built on Solana using the Anchor framework.
Completed as part of the Blueshift Anchor Escrow Challenge.


πŸ† Challenge Completed

Blueshift NFT Achievement

All 3/3 tests passed on Blueshift. NFT reward unlocked.

Verify on-chain:
πŸ”— View wallet on Solscan


πŸ€” What Is An Escrow?

Imagine you want to trade your Apple Watch for someone's PlayStation.
You don't want to hand over your watch first β€” what if they run away?
They don't want to hand their PS5 first either β€” for the same reason.

Escrow solves this.

You both put your items in a locked box.
Once both items are inside, the box opens automatically and gives each person the other's item.
If no one shows up to trade, you just open the box and get your item back.

That's exactly what this program does β€” but with SPL tokens on Solana, and instead of a locked box, it's a vault owned by a smart contract (PDA).


🧠 Core Concept β€” What Is a PDA?

A PDA (Program Derived Address) is a special type of account on Solana.

  • It looks like a wallet address
  • But it has no private key β€” no human can sign for it
  • Only the program itself can control it
  • It's derived from seeds (like a password made from known inputs)
PDA = hash("escrow" + maker_wallet + seed_number)

This is how the vault is controlled β€” the escrow PDA owns the vault, and only the program logic can move funds out of it.


πŸ“ Project Structure

src
β”œβ”€β”€ instructions
β”‚   β”œβ”€β”€ make.rs       ← Instruction 1: Create escrow & deposit Token A
β”‚   β”œβ”€β”€ take.rs       ← Instruction 2: Accept deal & swap tokens
β”‚   β”œβ”€β”€ refund.rs     ← Instruction 3: Cancel & get Token A back
β”‚   └── mod.rs
β”œβ”€β”€ state.rs          ← Escrow account data structure
β”œβ”€β”€ error.rs          ← Custom error messages
└── lib.rs            ← Program entry point with discriminators

πŸ—‚οΈ The Escrow State Account

This is the data stored on-chain for each escrow deal:

pub struct Escrow {
    pub seed: u64,      // Random number β€” lets one maker open many escrows
    pub maker: Pubkey,  // Who created the deal
    pub mint_a: Pubkey, // Token being offered (Token A)
    pub mint_b: Pubkey, // Token being requested (Token B)
    pub receive: u64,   // How much Token B the maker wants
    pub bump: u8,       // PDA bump β€” cached to save compute
}

The vault's balance tells us how much Token A is deposited.
We only need to store how much Token B we want in return.


πŸ”„ The 3 Instructions

1️⃣ MAKE β€” Create the Escrow Deal

The maker decides the terms and deposits Token A into a vault.

Maker Wallet ──[Token A]──► Vault (owned by Escrow PDA)
                             + Escrow account created on-chain
                               storing: maker, mint_a, mint_b, receive amount

What happens in code:

  1. Anchor creates the Escrow account (PDA) with the trade terms
  2. Anchor creates the Vault (an ATA owned by the Escrow PDA)
  3. Token A is transferred from maker_ata_a β†’ vault via CPI
sequenceDiagram
    participant Maker
    participant Program
    participant Escrow PDA
    participant Vault

    Maker->>Program: make(seed, amount_of_A, want_B)
    Program->>Escrow PDA: Create & store deal terms
    Program->>Vault: Create vault (ATA owned by Escrow PDA)
    Maker->>Vault: Transfer Token A (via CPI)
    Note over Vault: Token A locked here
Loading

2️⃣ TAKE β€” Accept the Deal

The taker sends Token B to the maker and receives Token A from the vault.
Both transfers happen atomically β€” either both succeed or nothing happens.

Taker Wallet ──[Token B]──► Maker Wallet
Vault        ──[Token A]──► Taker Wallet
Vault closed, Escrow PDA closed (rent returned to Maker)

What happens in code:

  1. Taker sends escrow.receive amount of Token B β†’ directly to maker
  2. Escrow PDA signs (using signer seeds) β†’ vault sends Token A β†’ taker
  3. Vault account is closed (rent goes back to maker)
  4. Escrow account is closed (rent goes back to maker)
sequenceDiagram
    participant Taker
    participant Program
    participant Escrow PDA
    participant Vault
    participant Maker

    Taker->>Program: take()
    Program->>Maker: Transfer Token B (taker β†’ maker directly)
    Escrow PDA->>Taker: Transfer Token A from Vault (PDA signs)
    Program->>Maker: Close Vault (rent returned)
    Program->>Maker: Close Escrow PDA (rent returned)
    Note over Taker,Maker: Deal complete βœ…
Loading

3️⃣ REFUND β€” Cancel the Deal

The maker changes their mind. They get Token A back and everything is closed.

Vault ──[Token A]──► Maker Wallet
Vault closed, Escrow PDA closed (rent returned to Maker)

What happens in code:

  1. Escrow PDA signs β†’ vault sends all Token A back β†’ maker
  2. Vault account is closed
  3. Escrow account is closed
sequenceDiagram
    participant Maker
    participant Program
    participant Escrow PDA
    participant Vault

    Maker->>Program: refund()
    Escrow PDA->>Maker: Transfer all Token A back from vault (PDA signs)
    Program->>Maker: Close Vault (rent returned)
    Program->>Maker: Close Escrow PDA (rent returned)
    Note over Maker: All funds back βœ…
Loading

πŸ” Security β€” How Does the Vault Stay Safe?

The vault is a token account whose authority is the Escrow PDA.

To move funds out of the vault, you need the PDA to sign.
PDAs can't sign on their own β€” only the program can create a PDA signature using signer seeds:

let signer_seeds: [&[&[u8]]; 1] = [&[
    b"escrow",
    maker_key.as_ref(),
    seed_ref.as_ref(),
    &[self.escrow.bump],
]];

CpiContext::new_with_signer(token_program, transfer_accounts, &signer_seeds)

The seeds must match exactly what was used to create the PDA.
If they don't match β†’ wrong PDA β†’ signature fails β†’ transfer blocked.
No one can steal funds.


πŸ“Š Complete Flow Diagram

flowchart TD
    A([Maker has Token A]) --> B[Call MAKE instruction]
    B --> C[Escrow PDA created\nstores deal terms on-chain]
    B --> D[Vault created\nowned by Escrow PDA]
    B --> E[Token A transferred\nMaker β†’ Vault]

    E --> F{What happens next?}

    F -->|Taker accepts| G[Call TAKE instruction]
    G --> H[Token B sent\nTaker β†’ Maker directly]
    G --> I[Token A sent\nVault β†’ Taker\nPDA signs]
    G --> J[Vault closed\nEscrow PDA closed\nRent β†’ Maker]
    H & I & J --> K([Deal Complete βœ…])

    F -->|Maker cancels| L[Call REFUND instruction]
    L --> M[Token A returned\nVault β†’ Maker\nPDA signs]
    L --> N[Vault closed\nEscrow PDA closed\nRent β†’ Maker]
    M & N --> O([Cancelled βœ…])
Loading

βš™οΈ Instruction Discriminators

This program uses custom discriminators (requires Anchor 0.31.0+):

Instruction Discriminator
make 0
take 1
refund 2

The state account Escrow also uses a custom discriminator of 1.


πŸ› οΈ How To Build

# Install dependencies
anchor build

# The compiled .so file will be at:
# target/deploy/blueshift_anchor_escrow.so

Requirements:

  • Anchor 0.31.0+
  • Solana CLI
  • Rust

πŸ“¦ Key Dependencies

anchor-lang = { features = ["init-if-needed"] }
anchor-spl  # for SPL Token and Token-2022 support

The idl-build feature in Cargo.toml:

idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

πŸ’‘ What I Learned β€” Real Bugs I Hit

1. Account ordering matters
Anchor processes accounts positionally in the IDL. Having vault in the wrong slot caused AccountNotInitialized even though the vault existed on-chain. Reordering fixed it.

2. init_if_needed vs mut
For Refund, the maker's Token A ATA (maker_ata_a) might not exist if the test runs in isolation. Using mut (which expects an existing account) causes failure. init_if_needed handles both cases.

3. PDA signing for CPI
Moving tokens out of a vault owned by a PDA requires CpiContext::new_with_signer with the exact seeds used to derive the PDA. Wrong seeds = wrong PDA address = transfer blocked.


πŸ”— Resources

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages