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:

  1. 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
  2. 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
  3. 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
      • 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: address
          • wanted_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
      • 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
  • 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:

  1. Creates the escrow PDA (state)
  2. Creates a vault ATA owned by the escrow PDA
  3. 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 = escrow

The 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 = maker

Which 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:

  1. Transfers rent lamports
  2. Deallocates vault
  3. 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:

  1. Escrow PDA signs
  2. Vault → maker
  3. Vault closed
  4. 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