PDA进阶与Escrow托管程序
### 1. PDA 回顾与进阶
日期: 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 中,通过 seeds 和 bump 约束自动处理这个过程:
// 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
);
});
});
关键要点总结
- 多 seed PDA 实现一对多映射:通过组合多个 seed(如
[b"escrow", maker, id]),为不同实体创建唯一的 PDA 地址 - PDA 作为 token authority 是 Escrow 的核心:Vault 的 authority 设为 PDA,只有程序通过
invoke_signed才能转出代币 - Escrow 的三个生命周期状态:Initialize(创建并锁定代币)→ Accept(原子交换)或 Cancel(退还代币)
close = maker自动回收账户租金:当 Escrow 完成或取消时,关闭 PDA 账户并将 SOL 租金退还给 Maker- CPI with signer 需要完整的 seeds + bump:
CpiContext::new_with_signer需要传入与 PDA 派生时相同的 seeds 和 bump - 先验证再转账:Anchor 的
constraint在指令执行前自动检查,确保传入的账户满足所有条件 - 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 有什么区别?"
关键差异:
| 维度 | Ethereum | Solana |
|---|---|---|
| 代币持有方式 | 合约直接持有 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 交易的固有问题。改进方案:
- 设置过期时间:Escrow 超过 N 天后自动可取消
- 部分成交:允许 Taker 只交换一部分
- 价格保护:集成 Oracle,当价格偏离超过阈值时拒绝交易
- 荷兰拍模式:随时间推移逐渐降低要价
参考资源
| 资源 | 链接 |
|---|---|
| Anchor Escrow 官方示例 | https://github.com/coral-xyz/anchor/tree/master/examples/escrow |
| Solana Cookbook: PDA | https://solanacookbook.com/core-concepts/pdas.html |
| SPL Token 文档 | https://spl.solana.com/token |
| Anchor Book: CPI | https://www.anchor-lang.com/docs/cross-program-invocations |
| Paulx Escrow 教程(经典) | https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/ |
| Solana 账户模型深度 | https://docs.solana.com/developing/programming-model/accounts |