返回 SC 笔记
SC Day 47

PDA进阶与Escrow托管程序

### 1. PDA 回顾与进阶

2026-05-27
第二阶段:框架实战
solanaanchorpdaescrow

日期: 2026-05-27 方向: Solana 阶段: 第二阶段:框架实战 标签: #solana #anchor #pda #escrow


今日目标

类型内容
学习深入理解多 seed PDA、PDA 作为 CPI signer 的原理,掌握 Escrow 托管模式
实操实现完整的 Escrow 程序:initialize / accept / cancel 三个指令,含 SPL Token 转移
产出Escrow 程序完整代码、PDA 进阶笔记、DeFi 基础构建块分析

核心概念

1. PDA 回顾与进阶

PDA 的本质

PDA(Program Derived Address)是 Solana 中由程序派生的特殊地址。它不在 Ed25519 曲线上,因此没有私钥,只能由创建它的程序来"签名"。

普通账户: 有私钥 → 可以直接签名交易
PDA:      无私钥 → 只能通过 invoke_signed 由程序签名

PDA 的生成公式:

PDA = hash(seeds, program_id, bump)

其中 bump 从 255 开始递减,直到找到一个不在 Ed25519 曲线上的地址

多 Seed PDA

单个 seed 的 PDA 只能唯一对应一个账户。但在实际应用中,我们经常需要为不同实体创建不同的 PDA。这就需要多 seed 组合。

// 单 seed: 整个程序只有一个
#[account(
    seeds = [b"global_config"],
    bump
)]
pub config: Account<'info, Config>,

// 双 seed: 每个用户一个
#[account(
    seeds = [b"user_profile", user.key().as_ref()],
    bump
)]
pub profile: Account<'info, UserProfile>,

// 三 seed: 每个用户对每个 token 一个
#[account(
    seeds = [b"vault", user.key().as_ref(), token_mint.key().as_ref()],
    bump
)]
pub vault: Account<'info, TokenVault>,

// 四 seed: 用字符串 ID 区分
#[account(
    seeds = [b"escrow", maker.key().as_ref(), escrow_id.to_le_bytes().as_ref()],
    bump
)]
pub escrow: Account<'info, EscrowState>,

Seed 设计原则

原则说明示例
唯一性seed 组合必须能唯一标识一个账户[b"escrow", maker, id]
可推导性客户端能根据已知信息推导出地址用 user pubkey 做 seed
前缀命名第一个 seed 用字面量标识类型b"vault", b"escrow"
长度限制总 seed 长度不能超过 32 字节(每个 seed)注意 string seed 的长度

PDA 作为 CPI Signer

PDA 最强大的特性是它可以在 CPI(Cross-Program Invocation)中充当签名者。当程序 A 需要调用程序 B,而程序 B 要求某个账户签名时,如果该账户是程序 A 的 PDA,程序 A 可以通过 invoke_signed 来"代签"。

┌──────────────┐          CPI            ┌──────────────┐
│  Escrow 程序  │ ────────────────────>  │  Token 程序   │
│              │  invoke_signed          │              │
│  知道 PDA 的  │  (seeds + bump)        │  验证 PDA 是  │
│  seeds 和 bump│                        │  合法 signer  │
└──────────────┘                         └──────────────┘

在 Anchor 中,通过 seedsbump 约束自动处理这个过程:

// Anchor 自动将 vault_authority (PDA) 作为 signer
#[account(
    seeds = [b"authority", escrow.key().as_ref()],
    bump = escrow.authority_bump,
)]
pub vault_authority: SystemAccount<'info>,

2. Escrow 模式 — DeFi 的基础构建块

什么是 Escrow?

Escrow(托管)是一种中间人模式:双方的资产先存入一个中立的托管账户,条件满足时自动完成交换。

在传统金融中,Escrow 需要银行或律所作为可信第三方。在区块链上,智能合约/程序就是那个可信的第三方 —— 代码就是法律

Escrow 的工作流程

步骤 1: Maker 创建 Escrow
┌──────────┐                    ┌──────────────┐
│  Maker   │ ── 100 USDC ──>  │  Escrow Vault │  (PDA 控制的 token account)
│          │                    │  100 USDC     │
│  想用 100 │                    │              │
│  USDC 换  │                    │  条件:        │
│  50 SOL   │                    │  50 SOL      │
└──────────┘                    └──────────────┘

步骤 2: Taker 接受交易
┌──────────┐                    ┌──────────────┐
│  Taker   │ ── 50 SOL ──>    │  Maker       │  Taker 直接给 Maker
│          │                    │  收到 50 SOL  │
│  有 50 SOL│ <── 100 USDC ──  │              │
│  想要 USDC│                    │  Escrow Vault │  Vault 的 USDC 给 Taker
└──────────┘                    └──────────────┘

步骤 2b (可选): Maker 取消
┌──────────┐                    ┌──────────────┐
│  Maker   │ <── 100 USDC ──  │  Escrow Vault │  Vault 返还给 Maker
│          │                    │  (关闭)       │
└──────────┘                    └──────────────┘

为什么 Escrow 是 DeFi 的基础?

DeFi 应用Escrow 的应用方式
DEX (AMM)LP 将代币存入 Pool(Escrow),交易时从 Pool 中swap
借贷借款人将抵押品存入合约(Escrow),还款后取回
NFT 交易卖家 NFT 锁定在 Escrow,买家付款后自动转移
OTC 交易双方代币各自存入 Escrow,原子化交换
期权/期货保证金锁定在 Escrow,结算时按盈亏分配

3. SPL Token 在 Escrow 中的角色

Solana 的代币标准是 SPL Token。每个用户对每个代币都有一个独立的 Token Account(ATA, Associated Token Account)。

┌─────────────────────────────────────────┐
│              SPL Token 架构              │
│                                         │
│  Mint Account (代币定义)                 │
│  ├── decimals: 6                        │
│  ├── supply: 1,000,000                  │
│  └── mint_authority: ...                │
│                                         │
│  Token Account (用户持仓)                │
│  ├── mint: <Mint 地址>                  │
│  ├── owner: <用户地址>                   │
│  ├── amount: 500                        │
│  └── ...                                │
│                                         │
│  ATA = findProgramAddress(              │
│    [owner, TOKEN_PROGRAM, mint],        │
│    ATA_PROGRAM                          │
│  )                                      │
└─────────────────────────────────────────┘

在 Escrow 中,我们需要创建一个 由 PDA 控制的 Token Account 作为 Vault:

// Vault 的 owner 是 PDA,不是任何用户
// 这样只有我们的程序能从 vault 中转出代币
#[account(
    init,
    payer = maker,
    token::mint = token_mint_a,
    token::authority = escrow,  // PDA 是 token account 的 authority
)]
pub vault: Account<'info, TokenAccount>,

代码实战

完整 Escrow 程序

项目结构

escrow/
├── programs/escrow/src/
│   ├── lib.rs           # 程序入口
│   ├── state.rs         # 状态定义
│   ├── instructions/
│   │   ├── mod.rs
│   │   ├── initialize.rs  # 创建 Escrow
│   │   ├── accept.rs      # 接受交易
│   │   └── cancel.rs      # 取消交易
│   └── error.rs         # 自定义错误
└── tests/
    └── escrow.ts

state.rs — Escrow 状态

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct EscrowState {
    /// Maker 的公钥
    pub maker: Pubkey,
    /// Maker 存入的代币 Mint
    pub token_mint_a: Pubkey,
    /// Maker 想要的代币 Mint
    pub token_mint_b: Pubkey,
    /// Maker 存入的数量
    pub amount_a: u64,
    /// Maker 期望获得的数量
    pub amount_b: u64,
    /// Escrow 的 bump(用于 PDA 签名)
    pub bump: u8,
}

error.rs — 自定义错误

use anchor_lang::prelude::*;

#[error_code]
pub enum EscrowError {
    #[msg("Insufficient token amount for the trade")]
    InsufficientAmount,
    #[msg("Invalid token mint provided")]
    InvalidMint,
}

instructions/initialize.rs — 创建 Escrow

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
use crate::state::EscrowState;

#[derive(Accounts)]
#[instruction(escrow_id: u64)]
pub struct Initialize<'info> {
    /// Maker: 发起交易的人
    #[account(mut)]
    pub maker: Signer<'info>,

    /// Maker 持有的 Token A 账户(将从中转出代币)
    #[account(
        mut,
        constraint = maker_token_a.mint == token_mint_a.key(),
        constraint = maker_token_a.owner == maker.key(),
    )]
    pub maker_token_a: Account<'info, TokenAccount>,

    /// Token A 的 Mint
    pub token_mint_a: Account<'info, Mint>,

    /// Token B 的 Mint(Maker 想要的)
    pub token_mint_b: Account<'info, Mint>,

    /// Escrow 状态账户(PDA)
    #[account(
        init,
        payer = maker,
        space = 8 + EscrowState::INIT_SPACE,
        seeds = [b"escrow", maker.key().as_ref(), escrow_id.to_le_bytes().as_ref()],
        bump,
    )]
    pub escrow: Account<'info, EscrowState>,

    /// Vault: 托管 Token A 的 token account,authority 是 escrow PDA
    #[account(
        init,
        payer = maker,
        token::mint = token_mint_a,
        token::authority = escrow,
    )]
    pub vault: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

impl<'info> Initialize<'info> {
    pub fn initialize(
        &mut self,
        escrow_id: u64,
        amount_a: u64,
        amount_b: u64,
        bumps: &InitializeBumps,
    ) -> Result<()> {
        // 保存 Escrow 状态
        self.escrow.set_inner(EscrowState {
            maker: self.maker.key(),
            token_mint_a: self.token_mint_a.key(),
            token_mint_b: self.token_mint_b.key(),
            amount_a,
            amount_b,
            bump: bumps.escrow,
        });

        // 将 Maker 的 Token A 转入 Vault
        let cpi_accounts = Transfer {
            from: self.maker_token_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(),
            cpi_accounts,
        );
        token::transfer(cpi_ctx, amount_a)?;

        msg!("Escrow initialized: {} Token A for {} Token B", amount_a, amount_b);
        Ok(())
    }
}

instructions/accept.rs — 接受交易

use anchor_lang::prelude::*;
use anchor_spl::token::{self, CloseAccount, Mint, Token, TokenAccount, Transfer};
use crate::state::EscrowState;
use crate::error::EscrowError;

#[derive(Accounts)]
pub struct Accept<'info> {
    /// Taker: 接受交易的人
    #[account(mut)]
    pub taker: Signer<'info>,

    /// Maker: 原始发起者(接收 Token B)
    /// CHECK: 只用来接收 Token B,通过 escrow.maker 验证
    #[account(mut)]
    pub maker: SystemAccount<'info>,

    /// Taker 的 Token B 账户(将 Token B 发给 Maker)
    #[account(
        mut,
        constraint = taker_token_b.mint == escrow.token_mint_b @ EscrowError::InvalidMint,
        constraint = taker_token_b.owner == taker.key(),
        constraint = taker_token_b.amount >= escrow.amount_b @ EscrowError::InsufficientAmount,
    )]
    pub taker_token_b: Account<'info, TokenAccount>,

    /// Taker 的 Token A 账户(接收 Vault 中的 Token A)
    #[account(
        mut,
        constraint = taker_token_a.mint == escrow.token_mint_a @ EscrowError::InvalidMint,
        constraint = taker_token_a.owner == taker.key(),
    )]
    pub taker_token_a: Account<'info, TokenAccount>,

    /// Maker 的 Token B 账户(接收 Taker 的 Token B)
    #[account(
        mut,
        constraint = maker_token_b.mint == escrow.token_mint_b @ EscrowError::InvalidMint,
        constraint = maker_token_b.owner == escrow.maker,
    )]
    pub maker_token_b: Account<'info, TokenAccount>,

    /// Escrow 状态
    #[account(
        mut,
        has_one = maker,
        seeds = [b"escrow", maker.key().as_ref(), escrow.key().as_ref()],
        bump = escrow.bump,
        close = maker,  // 关闭 escrow 账户,返还租金给 maker
    )]
    pub escrow: Account<'info, EscrowState>,

    /// Vault: 持有 Maker 的 Token A
    #[account(
        mut,
        constraint = vault.mint == escrow.token_mint_a,
        constraint = vault.owner == escrow.key(),
        constraint = vault.amount >= escrow.amount_a,
    )]
    pub vault: Account<'info, TokenAccount>,

    pub token_mint_a: Account<'info, Mint>,
    pub token_mint_b: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

impl<'info> Accept<'info> {
    pub fn accept(&mut self) -> Result<()> {
        // 1. Taker 将 Token B 发给 Maker
        let cpi_accounts = Transfer {
            from: self.taker_token_b.to_account_info(),
            to: self.maker_token_b.to_account_info(),
            authority: self.taker.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(
            self.token_program.to_account_info(),
            cpi_accounts,
        );
        token::transfer(cpi_ctx, self.escrow.amount_b)?;

        // 2. Vault 中的 Token A 发给 Taker(需要 PDA 签名)
        let maker_key = self.maker.key();
        let escrow_key = self.escrow.key();
        let signer_seeds: &[&[&[u8]]] = &[&[
            b"escrow",
            maker_key.as_ref(),
            escrow_key.as_ref(),
            &[self.escrow.bump],
        ]];

        let cpi_accounts = Transfer {
            from: self.vault.to_account_info(),
            to: self.taker_token_a.to_account_info(),
            authority: self.escrow.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            cpi_accounts,
            signer_seeds,
        );
        token::transfer(cpi_ctx, self.escrow.amount_a)?;

        // 3. 关闭 Vault token account,返还租金给 Maker
        let cpi_accounts = CloseAccount {
            account: self.vault.to_account_info(),
            destination: self.maker.to_account_info(),
            authority: self.escrow.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            cpi_accounts,
            signer_seeds,
        );
        token::close_account(cpi_ctx)?;

        msg!("Escrow accepted! Trade completed successfully.");
        Ok(())
    }
}

instructions/cancel.rs — 取消交易

use anchor_lang::prelude::*;
use anchor_spl::token::{self, CloseAccount, Mint, Token, TokenAccount, Transfer};
use crate::state::EscrowState;

#[derive(Accounts)]
pub struct Cancel<'info> {
    /// 只有 Maker 可以取消
    #[account(mut)]
    pub maker: Signer<'info>,

    /// Maker 的 Token A 账户(Vault 退回到这里)
    #[account(
        mut,
        constraint = maker_token_a.mint == escrow.token_mint_a,
        constraint = maker_token_a.owner == maker.key(),
    )]
    pub maker_token_a: Account<'info, TokenAccount>,

    /// Escrow 状态
    #[account(
        mut,
        has_one = maker,
        seeds = [b"escrow", maker.key().as_ref(), escrow.key().as_ref()],
        bump = escrow.bump,
        close = maker,
    )]
    pub escrow: Account<'info, EscrowState>,

    /// Vault
    #[account(
        mut,
        constraint = vault.owner == escrow.key(),
    )]
    pub vault: Account<'info, TokenAccount>,

    pub token_mint_a: Account<'info, Mint>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

impl<'info> Cancel<'info> {
    pub fn cancel(&mut self) -> Result<()> {
        let maker_key = self.maker.key();
        let escrow_key = self.escrow.key();
        let signer_seeds: &[&[&[u8]]] = &[&[
            b"escrow",
            maker_key.as_ref(),
            escrow_key.as_ref(),
            &[self.escrow.bump],
        ]];

        // 1. 将 Vault 中的 Token A 退还给 Maker
        let cpi_accounts = Transfer {
            from: self.vault.to_account_info(),
            to: self.maker_token_a.to_account_info(),
            authority: self.escrow.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            cpi_accounts,
            signer_seeds,
        );
        token::transfer(cpi_ctx, self.vault.amount)?;

        // 2. 关闭 Vault 账户
        let cpi_accounts = CloseAccount {
            account: self.vault.to_account_info(),
            destination: self.maker.to_account_info(),
            authority: self.escrow.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            cpi_accounts,
            signer_seeds,
        );
        token::close_account(cpi_ctx)?;

        msg!("Escrow cancelled. Tokens returned to maker.");
        Ok(())
    }
}

lib.rs — 程序入口

use anchor_lang::prelude::*;

mod state;
mod error;
mod instructions;

use instructions::*;

declare_id!("ESCRoWxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

#[program]
pub mod escrow {
    use super::*;

    /// Maker 创建一个新的 Escrow
    /// amount_a: 存入的 Token A 数量
    /// amount_b: 期望获得的 Token B 数量
    pub fn initialize_escrow(
        ctx: Context<Initialize>,
        escrow_id: u64,
        amount_a: u64,
        amount_b: u64,
    ) -> Result<()> {
        ctx.accounts.initialize(escrow_id, amount_a, amount_b, &ctx.bumps)
    }

    /// Taker 接受 Escrow,完成原子化交换
    pub fn accept_escrow(ctx: Context<Accept>) -> Result<()> {
        ctx.accounts.accept()
    }

    /// Maker 取消 Escrow,取回存入的 Token A
    pub fn cancel_escrow(ctx: Context<Cancel>) -> Result<()> {
        ctx.accounts.cancel()
    }
}

instructions/mod.rs

pub mod initialize;
pub mod accept;
pub mod cancel;

pub use initialize::*;
pub use accept::*;
pub use cancel::*;

TypeScript 测试

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Escrow } from "../target/types/escrow";
import {
  createMint,
  createAccount,
  mintTo,
  getAccount,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { assert } from "chai";

describe("escrow", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Escrow as Program<Escrow>;

  // 参与者
  const maker = anchor.web3.Keypair.generate();
  const taker = anchor.web3.Keypair.generate();

  // 代币
  let mintA: anchor.web3.PublicKey;
  let mintB: anchor.web3.PublicKey;

  // Token Accounts
  let makerTokenA: anchor.web3.PublicKey;
  let makerTokenB: anchor.web3.PublicKey;
  let takerTokenA: anchor.web3.PublicKey;
  let takerTokenB: anchor.web3.PublicKey;

  // Escrow 相关
  let escrowPda: anchor.web3.PublicKey;
  let vault: anchor.web3.Keypair;

  const escrowId = new anchor.BN(1);
  const amountA = new anchor.BN(100_000_000); // 100 Token A (6 decimals)
  const amountB = new anchor.BN(50_000_000);  // 50 Token B (6 decimals)

  before(async () => {
    // 给 maker 和 taker 空投 SOL
    await provider.connection.requestAirdrop(
      maker.publicKey,
      2 * anchor.web3.LAMPORTS_PER_SOL
    );
    await provider.connection.requestAirdrop(
      taker.publicKey,
      2 * anchor.web3.LAMPORTS_PER_SOL
    );

    // 创建 Token A 和 Token B
    mintA = await createMint(
      provider.connection,
      maker,
      maker.publicKey,
      null,
      6
    );
    mintB = await createMint(
      provider.connection,
      taker,
      taker.publicKey,
      null,
      6
    );

    // 创建 Token Accounts
    makerTokenA = await createAccount(
      provider.connection, maker, mintA, maker.publicKey
    );
    makerTokenB = await createAccount(
      provider.connection, maker, mintB, maker.publicKey
    );
    takerTokenA = await createAccount(
      provider.connection, taker, mintA, taker.publicKey
    );
    takerTokenB = await createAccount(
      provider.connection, taker, mintB, taker.publicKey
    );

    // Mint tokens
    await mintTo(
      provider.connection, maker, mintA, makerTokenA, maker, 1_000_000_000
    );
    await mintTo(
      provider.connection, taker, mintB, takerTokenB, taker, 1_000_000_000
    );

    // 计算 Escrow PDA
    [escrowPda] = anchor.web3.PublicKey.findProgramAddressSync(
      [
        Buffer.from("escrow"),
        maker.publicKey.toBuffer(),
        escrowId.toArrayLike(Buffer, "le", 8),
      ],
      program.programId
    );

    vault = anchor.web3.Keypair.generate();
  });

  it("initializes escrow", async () => {
    await program.methods
      .initializeEscrow(escrowId, amountA, amountB)
      .accounts({
        maker: maker.publicKey,
        makerTokenA: makerTokenA,
        tokenMintA: mintA,
        tokenMintB: mintB,
        escrow: escrowPda,
        vault: vault.publicKey,
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .signers([maker, vault])
      .rpc();

    // 验证 Escrow 状态
    const escrowState = await program.account.escrowState.fetch(escrowPda);
    assert.ok(escrowState.maker.equals(maker.publicKey));
    assert.ok(escrowState.amountA.eq(amountA));
    assert.ok(escrowState.amountB.eq(amountB));

    // 验证 Vault 余额
    const vaultAccount = await getAccount(provider.connection, vault.publicKey);
    assert.equal(Number(vaultAccount.amount), 100_000_000);
  });

  it("accepts escrow - atomic swap", async () => {
    const makerBalanceBefore = (
      await getAccount(provider.connection, makerTokenB)
    ).amount;

    await program.methods
      .acceptEscrow()
      .accounts({
        taker: taker.publicKey,
        maker: maker.publicKey,
        takerTokenB: takerTokenB,
        takerTokenA: takerTokenA,
        makerTokenB: makerTokenB,
        escrow: escrowPda,
        vault: vault.publicKey,
        tokenMintA: mintA,
        tokenMintB: mintB,
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .signers([taker])
      .rpc();

    // 验证 Taker 收到了 Token A
    const takerAAccount = await getAccount(provider.connection, takerTokenA);
    assert.equal(Number(takerAAccount.amount), 100_000_000);

    // 验证 Maker 收到了 Token B
    const makerBAccount = await getAccount(provider.connection, makerTokenB);
    assert.equal(
      Number(makerBAccount.amount) - Number(makerBalanceBefore),
      50_000_000
    );
  });
});

关键要点总结

  1. 多 seed PDA 实现一对多映射:通过组合多个 seed(如 [b"escrow", maker, id]),为不同实体创建唯一的 PDA 地址
  2. PDA 作为 token authority 是 Escrow 的核心:Vault 的 authority 设为 PDA,只有程序通过 invoke_signed 才能转出代币
  3. Escrow 的三个生命周期状态:Initialize(创建并锁定代币)→ Accept(原子交换)或 Cancel(退还代币)
  4. close = maker 自动回收账户租金:当 Escrow 完成或取消时,关闭 PDA 账户并将 SOL 租金退还给 Maker
  5. CPI with signer 需要完整的 seeds + bumpCpiContext::new_with_signer 需要传入与 PDA 派生时相同的 seeds 和 bump
  6. 先验证再转账:Anchor 的 constraint 在指令执行前自动检查,确保传入的账户满足所有条件
  7. Escrow 是 DeFi 的元模式:几乎所有 DeFi 应用(AMM、借贷、NFT 交易)都是 Escrow 的变体

常见误区

误区 1: "PDA 有私钥,只是程序帮忙保管"

PDA 从数学上就没有私钥。它被刻意生成在 Ed25519 曲线之外,这意味着不存在一个标量能映射到这个点。invoke_signed 不是"用私钥签名",而是 Solana runtime 验证 "这个地址确实可以从这个程序 + 这些 seeds 派生出来"。

误区 2: "Escrow 完成后不需要关闭 Vault 账户"

不关闭 Vault 会导致租金被永久锁定。Solana 的 Token Account 需要约 0.002 SOL 的租金。一个活跃的 Escrow 程序可能创建成千上万的 Vault 账户,如果不清理,这些 SOL 就浪费了。CloseAccount CPI 将剩余 lamports 返还给指定地址。

误区 3: "Anchor 的 constraint 和程序逻辑中的 require 一样"

Anchor 的 constraint反序列化阶段执行,比指令逻辑更早。如果 constraint 失败,指令函数不会被调用,Gas 消耗更少。而指令中的 require! 在逻辑执行过程中检查。建议尽量用 constraint 做前置验证。

误区 4: "bump 可以随便传一个值"

bump 必须与 PDA 派生时找到的那个值完全一致。Anchor 在 init 时自动计算 bump 并存储在 bumps 结构中,后续使用时通过 bump = escrow.bump 来引用。传错 bump 会导致地址不匹配,交易失败。


面试关联

Q1: "Solana 的 Escrow 和 Ethereum 的 Escrow 有什么区别?"

关键差异

维度EthereumSolana
代币持有方式合约直接持有 ERC20(mapping)需要创建独立的 Token Account(Vault)
权限控制合约地址就是权限需要 PDA 作为 token authority
原子性天然原子(单个交易)同样原子(单个 transaction 多个 instruction)
账户模型无需预创建需要预创建/初始化 Token Account
资源回收无(storage refund 较少)关闭账户回收租金

Q2: "为什么 DeFi 协议本质上都是 Escrow?"

所有 DeFi 都涉及"资产的暂时转移控制权":

  • AMM: LP 把代币存入 Pool(Escrow),按算法交换
  • 借贷: 抵押品存入合约(Escrow),满足条件后解锁
  • 衍生品: 保证金存入合约(Escrow),结算时分配
  • 桥: 源链锁定(Escrow),目标链铸造

理解 Escrow 模式就理解了 DeFi 的底层逻辑。

Q3: "如果 Escrow 创建后 Token 价格大幅变动,Taker 不接受怎么办?"

这是 OTC 交易的固有问题。改进方案:

  1. 设置过期时间:Escrow 超过 N 天后自动可取消
  2. 部分成交:允许 Taker 只交换一部分
  3. 价格保护:集成 Oracle,当价格偏离超过阈值时拒绝交易
  4. 荷兰拍模式:随时间推移逐渐降低要价

参考资源