Every thing in Solana is a Swap Program ~ Random Solana Dev
If Alice and Bob want to swap there USDC and SOL, there is an trust issue, what if one sends and other doesn’t, so we’ve a Escrow/Swap Program(Escrow means holding something on behalf on someone)
A Solana escrow is not a swap
It is a state machine controlling authority over assets
Tokens don’t move because users trust each other
Tokens move because the runtime enforces rules that no one can break
Escrow Algorithm:
Our goal is to transfer 10 USDC from Alice to Bob for 2 SOL
There are three users to deal with:
- Alice has two accounts - gives authority only to the escrow PDA
- Alice’s USDC ATA - owned by Token Program - Seeds: USDC Mint Address + Alice’s Address
- Alice’s SOL wallet - system account - Seeds: SOL Mint Address + Alice’s Address
- Bob has two accounts - only interacts with the escrow program
- Bob’s USDC ATA - owned by Token Program - Seeds: USDC Mint Address + Bob’s Address
- Bob’s SOL wallet - system account - Seeds: SOL Mint Address + Bob’s Address
- Swap Program
- The program never owns tokens directly
- It controls them through PDAs that can sign only under program rules
- It has two accounts and one program
- Swap Program, which also have a public key and three functions
make_offer()- Locking Value and frizzing the funds- Offer PDA is created
- Vault PDA is created
- Alice’s USDC is transferred into the vault
- Offer state is written
take_offer()- Atomic Swap Execution - Everything happens in one step- Bob sends SOL → Alice
- Vault sends USDC → Bob
- Offer is marked completed
- Vault is drained
- Offer account is closed (optional but recommended)
refund_offer()- Safety and Liveness- The refund happens when
- Offer is not taken
- Offer has expired
- Maker explicitly cancels
- The refund happens when
- Offer account (State)
- Seeds:
["offer", maker_pubkey, offer_id]- One offer = one PDA
- No collisions
- Deterministic lookup
- No need for external indexing
- It contains two fields
wanted_token:addresswanted_amount:address
- It defines
- Who created the offer (maker)
- What token is deposited
- How much is deposited
- What token is expected
- How much is expected
- Whether the offer is
open,taken,cancelled,expired
- Seeds:
- Vault account (Custody)
- It’s a PDA, but why ?
- Vault has no private key
- No one can sign for it directly
- Only the program can move funds using signer seeds
- Seeds: USDC Mint Address + Offer Address
- It’s a PDA, but why ?
- Swap Program, which also have a public key and three functions
- Alice calls the
make_offer()function and move hers USDC to Vault Account - Bob calls the
take_offer()function this moves his SOL to Alice’s account and USDC in swap program vault to his by signing the transaction on Alice behalf, by taking a small transaction fee as a cut
Escrow Codes
make.rs
pub struct Make<'info> {
#[account(mut)]
pub maker: Signer<'info>,
#[account(
mint::token_program = token_program
)]
pub mint_a: InterfaceAccount<'info, Mint>,
#[account(
mint::token_program = token_program
)]
pub mint_b: InterfaceAccount<'info, Mint>,
#[account(
mut,
associated_token::mint = mint_a,
associated_token::authority = maker,
associated_token::token_program = token_program
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
#[account(
init,
payer = maker,
seeds = [
b"escrow",
maker.key().as_ref(),
seed.to_le_bytes().as_ref()
],
space = Escrow::DISCRIMINATOR.len() + Escrow::INIT_SPACE,
bump
)]
pub escrow: Account<'info, Escrow>,
#[account(
init,
payer = maker,
associated_token::mint = mint_a,
associated_token::authority = escrow,
associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
impl<'info> Make<'info> {
pub fn init_escrow(
&mut self,
seed: u64,
receive: u64,
bumps: &MakeBumps,
) -> Result<()> {
self.escrow.set_inner(Escrow {
seed,
maker: self.maker.key(),
mint_a: self.mint_a.key(),
mint_b: self.mint_b.key(),
receive,
bump: bumps.escrow,
});
Ok(())
}
pub fn deposit(&mut self, deposit: u64) -> Result<()> {
let transfer_accounts = TransferChecked {
from: self.maker_ata_a.to_account_info(),
mint: self.mint_a.to_account_info(),
to: self.vault.to_account_info(),
authority: self.maker.to_account_info(),
};
let cpi_ctx =
CpiContext::new(self.token_program.to_account_info(), transfer_accounts);
transfer_checked(cpi_ctx, deposit, self.mint_a.decimals)
}
}It performs three irreversible actions:
- Creates the escrow PDA (state)
- Creates a vault ATA owned by the escrow PDA
- Moves tokens from maker → vault
After this:
- Maker cannot access funds
- Taker cannot access funds
- Only the program can move funds
Make Account Context
#[account(mut)]
pub maker: Signer<'info>,- Maker must sign
- Maker pays for everything
- Maker is the only trusted human at this stage
Mint Accounts
pub mint_a: InterfaceAccount<'info, Mint>,
pub mint_b: InterfaceAccount<'info, Mint>,These are token-agnostic mint definitions:
- Could be Token-2022
- Could be future token programs
- Decouples escrow logic from token implementation
Maker ATA (Token A)
associated_token::mint = mint_a,
associated_token::authority = maker,Anchor ensures:
- Correct ATA address
- Correct mint
- Correct authority
- Correct token program
This eliminates:
- Fake token accounts
- Mint mismatches
- Authority spoofing
Escrow PDA (State)
seeds = [b"escrow", maker.key().as_ref(), seed.to_le_bytes().as_ref()]This design guarantees:
- One maker → many offers
- Each offer uniquely addressable
- No global counter needed
- Deterministic discovery
The escrow PDA is not optional.
It is the sole authority for vault custody.
Vault ATA
associated_token::authority = escrowThe vault:
- Is an ATA
- Holds token A
- Is owned by the escrow PDA
- Has no private key
No user can ever sign for this account
init_escrow() — Writing Immutable Contract Terms
self.escrow.set_inner(Escrow {
seed,
maker,
mint_a,
mint_b,
receive,
bump,
});This struct is the contract.
After this:
- Maker cannot change terms
- Taker reads terms on-chain
- Program enforces terms blindly
There is no negotiation at execution time
deposit() — Moving Funds Into Neutral Custody
transfer_checked(cpi_ctx, deposit, self.mint_a.decimals)Why transfer_checked?
- Verifies mint decimals
- Prevents precision mismatch
- Prevents wrong-mint transfers
This CPI:
- Uses maker as signer
- Uses SPL Token Program
- Executes before escrow is usable
If this fails → escrow is unusable → transaction reverts.
take.rs
#[derive(Accounts)]
pub struct Take<'info> {
#[account(mut)]
pub taker: Signer<'info>,
/// CHECK: Validated by escrow has_one constraint
#[account(mut)]
pub maker: UncheckedAccount<'info>,
pub mint_a: InterfaceAccount<'info, Mint>,
pub mint_b: InterfaceAccount<'info, Mint>,
#[account(
init_if_needed,
payer = taker,
associated_token::mint = mint_a,
associated_token::authority = taker,
associated_token::token_program = token_program
)]
pub taker_ata_a: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = mint_b,
associated_token::authority = taker,
associated_token::token_program = token_program
)]
pub taker_ata_b: InterfaceAccount<'info, TokenAccount>,
#[account(
init_if_needed,
payer = taker,
associated_token::mint = mint_b,
associated_token::authority = maker,
associated_token::token_program = token_program
)]
pub maker_ata_b: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
close = maker,
has_one = mint_a,
has_one = mint_b,
has_one = maker,
seeds = [
b"escrow",
maker.key().as_ref(),
&escrow.seed.to_le_bytes()
],
bump = escrow.bump
)]
pub escrow: Account<'info, Escrow>,
#[account(
mut,
associated_token::mint = mint_a,
associated_token::authority = escrow,
associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
impl<'info> Take<'info> {
pub fn deposit(&mut self) -> Result<()> {
let transfer_accounts = TransferChecked {
from: self.taker_ata_b.to_account_info(),
mint: self.mint_b.to_account_info(),
to: self.maker_ata_b.to_account_info(),
authority: self.taker.to_account_info(),
};
let cpi_ctx =
CpiContext::new(self.token_program.to_account_info(), transfer_accounts);
transfer_checked(
cpi_ctx,
self.escrow.receive,
self.mint_b.decimals,
)
}
pub fn withdraw_and_close_vault(&mut self) -> Result<()> {
let signer_seeds: &[&[&[u8]]] = &[&[
b"escrow",
self.maker.to_account_info().key.as_ref(),
&self.escrow.seed.to_le_bytes(),
&[self.escrow.bump],
]];
let transfer_accounts = TransferChecked {
from: self.vault.to_account_info(),
mint: self.mint_a.to_account_info(),
to: self.taker_ata_a.to_account_info(),
authority: self.escrow.to_account_info(),
};
let transfer_cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
transfer_checked(
transfer_cpi_ctx,
self.vault.amount,
self.mint_a.decimals,
)?;
let close_accounts = CloseAccount {
account: self.vault.to_account_info(),
destination: self.maker.to_account_info(),
authority: self.escrow.to_account_info(),
};
let close_cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
close_accounts,
signer_seeds,
);
close_account(close_cpi_ctx)
}
}If take is correct:
- No partial execution
- No reentrancy
- No race conditions
- No stolen funds
Take Context
pub taker: Signer<'info>,Taker signs only for their own funds
/// CHECK: Validated by escrow has_one constraint
pub maker: UncheckedAccount<'info>,This is intentional.
Why unchecked?
- Maker does not sign
- Maker is validated via escrow state
- Avoids unnecessary constraints
- Keeps CPI minimal
The real validation is here:
has_one = makerWhich binds the escrow to the original maker cryptographically
Escrow State Validation
has_one = mint_a,
has_one = mint_b,
has_one = maker,This ensures:
- No mint substitution
- No maker spoofing
- No cross-offer confusion
This is runtime-enforced immutability
Taker Deposit (Token B → Maker)
transfer_checked(cpi_ctx, self.escrow.receive, self.mint_b.decimals)This happens first Why?
- If taker fails, nothing else happens
- Vault funds remain locked
- No partial execution
Order matters.
Vault Withdrawal (Token A → Taker)
authority: self.escrow.to_account_info(),Now the escrow PDA must sign
Signer Seeds
[
b"escrow",
maker_pubkey,
seed,
bump
]This proves:
- PDA identity
- Program authority
- Offer uniqueness
The runtime verifies:
“This program is allowed to sign for this account”
No signature exists.
No private key exists.
This is pure math + runtime enforcement.
Vault Closure
close_account(close_cpi_ctx)This does three things atomically:
- Transfers rent lamports
- Deallocates vault
- Prevents reuse
Leaving vaults open is a security and economic bug
Escrow Account Closure
#[account(close = maker)]Anchor ensures:
- Escrow state is destroyed
- Rent returned
- Offer becomes impossible to reuse
This prevents:
- Replay attacks
- Double fills
- State resurrection
refund.rs
#[derive(Accounts)]
pub struct Refund<'info> {
#[account(mut)]
pub maker: Signer<'info>,
pub mint_a: InterfaceAccount<'info, Mint>,
#[account(
mut,
associated_token::mint = mint_a,
associated_token::authority = maker,
associated_token::token_program = token_program
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
close = maker,
has_one = mint_a,
has_one = maker,
seeds = [
b"escrow",
maker.key().as_ref(),
&escrow.seed.to_le_bytes()
],
bump = escrow.bump
)]
pub escrow: Account<'info, Escrow>,
#[account(
mut,
associated_token::mint = mint_a,
associated_token::authority = escrow,
associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
impl<'info> Refund<'info> {
pub fn refund_and_close_vault(&mut self) -> Result<()> {
let signer_seeds: &[&[&[u8]]] = &[&[
b"escrow",
self.maker.to_account_info().key.as_ref(),
&self.escrow.seed.to_le_bytes(),
&[self.escrow.bump],
]];
let transfer_accounts = TransferChecked {
from: self.vault.to_account_info(),
mint: self.mint_a.to_account_info(),
to: self.maker_ata_a.to_account_info(),
authority: self.escrow.to_account_info(),
};
let transfer_cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
transfer_accounts,
signer_seeds,
);
transfer_checked(
transfer_cpi_ctx,
self.vault.amount,
self.mint_a.decimals,
)?;
let close_accounts = CloseAccount {
account: self.vault.to_account_info(),
destination: self.maker.to_account_info(),
authority: self.escrow.to_account_info(),
};
let close_cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
close_accounts,
signer_seeds,
);
close_account(close_cpi_ctx)
}
}Refund is not a feature, it is safety infrastructure
Refund Context
pub maker: Signer<'info>,Only maker can refund.
No taker involvement.
has_one = mint_a,
has_one = maker,This ensures:
- Correct escrow
- Correct authority
- No malicious refund attempts
Refund Execution.
The logic mirrors take:
- Escrow PDA signs
- Vault → maker
- Vault closed
- Escrow closed
This symmetry is intentional.
Why Refund Must Exist ?
Without refund:
- Funds can be grief-locked forever
- Escrow becomes a DoS vector
- Maker risk becomes unbounded
A production escrow must always have a liveness path