从核心逻辑到上链部署:SolanaAnchor托管程序实战全记录托管(Escrow)是去中心化金融的基石。在Solana这种基于账户模型的链上,如何安全地管理互不信任的资产交换?答案在于对PDA(程序派生地址)权限与交易原子性的深度掌握。本文是一份完整的工程实战记录。我们将从Merm
托管(Escrow)是去中心化金融的基石。在 Solana 这种基于账户模型的链上,如何安全地管理互不信任的资产交换?答案在于对 PDA(程序派生地址)权限与交易原子性的深度掌握。
本文是一份完整的工程实战记录。我们将从 Mermaid 逻辑架构图出发,深度拆解 Anchor 框架下托管合约的底层实现——从状态账户定义到 Make、Take、Refund 三大指令的权限约束,再到本地与 Devnet 开发网的自动化部署验证。这不仅是代码的堆砌,更是一次完整的链上资产管理方案闭环。

anchor init blueshift_anchor_escrow
cd blueshift_anchor_escrow
cargo add anchor-lang --features init-if-needed
cargo add anchor-spl
blueshift_anchor_escrow on master [?] via 🦀 1.89.0 took 18.0s
➜ tree . -L 6 -I "docs|target|node_modules"
.
├── Anchor.toml
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── app
├── cliff.toml
├── deny.toml
├── deploy_out
│ └── blueshift_anchor_escrow.so
├── migrations
│ └── deploy.ts
├── package.json
├── pnpm-lock.yaml
├── programs
│ └── blueshift_anchor_escrow
│ ├── Cargo.toml
│ └── src
│ ├── errors.rs
│ ├── instructions
│ │ ├── make.rs
│ │ ├── mod.rs
│ │ ├── refund.rs
│ │ └── take.rs
│ ├── lib.rs
│ └── state.rs
├── rust-toolchain.toml
├── tests
│ └── blueshift_anchor_escrow.ts
└── tsconfig.json
9 directories, 21 files
lib.rs 文件use anchor_lang::prelude::*;
mod errors;
mod instructions;
mod state;
use instructions::*;
declare_id!("22222222222222222222222222222222222222222222");
#[program]
pub mod blueshift_anchor_escrow {
use super::*;
#[instruction(discriminator = 0)]
pub fn make(ctx: Context<Make>, seed: u64, receive: u64, amount: u64) -> Result<()> {
instructions::make::handler(ctx, seed, receive, amount)
}
#[instruction(discriminator = 1)]
pub fn take(ctx: Context<Take>) -> Result<()> {
instructions::take::handler(ctx)
}
#[instruction(discriminator = 2)]
pub fn refund(ctx: Context<Refund>) -> Result<()> {
instructions::refund::handler(ctx)
}
}
state.rs 文件use anchor_lang::prelude::*;
pub const ESCROW_SEED: &[u8] = b"escrow";
#[derive(InitSpace)]
#[account(discriminator = 1)]
pub struct Escrow {
pub seed: u64,
pub maker: Pubkey,
pub mint_a: Pubkey,
pub mint_b: Pubkey,
pub receive: u64,
pub bump: u8,
}
errors.rs 文件use anchor_lang::prelude::*;
#[error_code]
pub enum EscrowError {
#[msg("Invalid amount")]
InvalidAmount,
#[msg("Invalid maker")]
InvalidMaker,
#[msg("Invalid mint a")]
InvalidMintA,
#[msg("Invalid mint b")]
InvalidMintB,
}
instructions/mod.rs 文件pub mod make;
pub mod refund;
pub mod take;
pub use make::*;
pub use refund::*;
pub use take::*;
instructions/make.rs 文件use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},
};
use crate::{
errors::EscrowError,
state::{Escrow, ESCROW_SEED},
};
/// 1. 初始化托管记录并存储所有条款。
/// 2. 创建金库(一个由 escrow 拥有的 mint_a 的关联代币账户 (ATA))。
/// 3. 使用 CPI 调用 SPL-Token 程序,将创建者的 Token A 转移到该金库中。
#[derive(Accounts)]
#[instruction(seed: u64)]
pub struct Make<'info> {
#[account(mut)]
pub maker: Signer<'info>,
#[account(
init,
payer = maker,
space = Escrow::INIT_SPACE + Escrow::DISCRIMINATOR.len(),
seeds = [ESCROW_SEED, maker.key().as_ref(), seed.to_le_bytes().as_ref()],
bump,
)]
pub escrow: Account<'info, Escrow>,
/// Token Accounts
#[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,
associated_token::mint = mint_a,
associated_token::authority = escrow,
associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
/// Programs
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
impl<'info> Make<'info> {
/// Create the Escrow
pub(crate) fn populate_escrow(&mut self, seed: u64, amount: u64, bump: u8) -> Result<()> {
self.escrow.set_inner(Escrow {
seed,
maker: self.maker.key(),
mint_a: self.mint_a.key(),
mint_b: self.mint_b.key(),
receive: amount,
bump,
});
Ok(())
}
/// Deposit the tokens
pub(crate) fn deposit_tokens(&self, amount: u64) -> Result<()> {
let cpi_accounts = TransferChecked {
from: self.maker_ata_a.to_account_info(),
to: self.vault.to_account_info(),
mint: self.mint_a.to_account_info(),
authority: self.maker.to_account_info(),
};
let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), cpi_accounts);
transfer_checked(cpi_ctx, amount, self.mint_a.decimals)
}
}
pub fn handler(ctx: Context<Make>, seed: u64, receive: u64, amount: u64) -> Result<()> {
// Validate the amount
require_gt!(receive, 0, EscrowError::InvalidAmount);
require_gt!(amount, 0, EscrowError::InvalidAmount);
// Save the Escrow Data
ctx.accounts
.populate_escrow(seed, receive, ctx.bumps.escrow)?;
// Deposit Tokens
ctx.accounts.deposit_tokens(amount)?;
Ok(())
}
这段代码实现了 Escrow 合约中 make 指令的完整创建流程:首先通过 PDA(由 ESCROW_SEED + maker + seed 派生)初始化并存储一份托管协议数据,明确交易双方将使用的 mint_a、mint_b 以及期望接收的数量;随后为该 escrow 创建一个由其自身控制的 Token A 金库(vault,对应 escrow 作为 authority 的 ATA);最后通过 CPI 调用 SPL Token Program 的 transfer_checked,将创建者(maker)账户中的 Token A 安全转入金库中。整个过程在账户约束层完成权限与一致性校验,在逻辑层完成状态写入与资产托管,确保 escrow 在创建阶段即具备完整、可信的交易条件。
instructions/take.rs 文件use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{
close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface,
TransferChecked,
},
};
use crate::{
errors::EscrowError,
state::{Escrow, ESCROW_SEED},
};
/// 1. 关闭托管记录,将其租金 lamports 返还给创建者。
/// 2. 将 Token A 从保管库转移到接受者,然后关闭保管库。
/// 3. 将约定数量的 Token B 从接受者转移到创建者。
#[derive(Accounts)]
pub struct Take<'info> {
#[account(mut)]
pub taker: Signer<'info>,
#[account(mut)]
pub maker: SystemAccount<'info>,
#[account(
mut,
close = maker,
seeds = [ESCROW_SEED, maker.key().as_ref(), escrow.seed.to_le_bytes().as_ref()],
bump = escrow.bump,
has_one = maker @ EscrowError::InvalidMaker,
has_one = mint_a @ EscrowError::InvalidMintA,
has_one = mint_b @ EscrowError::InvalidMintB,
)]
pub escrow: Box<Account<'info, Escrow>>,
/// Token Accounts
pub mint_a: Box<InterfaceAccount<'info, Mint>>,
pub mint_b: Box<InterfaceAccount<'info, Mint>>,
#[account(
mut,
associated_token::mint = mint_a,
associated_token::authority = escrow,
associated_token::token_program = token_program
)]
pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
#[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: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(
mut,
associated_token::mint = mint_b,
associated_token::authority = taker,
associated_token::token_program = token_program
)]
pub taker_ata_b: Box<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: Box<InterfaceAccount<'info, TokenAccount>>,
/// Programs
pub associated_token_program: Program<'info, AssociatedToken>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
impl<'info> Take<'info> {
fn transfer_to_maker(&mut self) -> Result<()> {
transfer_checked(
CpiContext::new(
self.token_program.to_account_info(),
TransferChecked {
from: self.taker_ata_b.to_account_info(),
to: self.maker_ata_b.to_account_info(),
mint: self.mint_b.to_account_info(),
authority: self.taker.to_account_info(),
},
),
self.escrow.receive,
self.mint_b.decimals,
)?;
Ok(())
}
fn withdraw_and_close_vault(&mut self) -> Result<()> {
let binding = self.maker.to_account_info().key();
// Create the signer seeds for the vault
let signer_seeds: [&[&[u8]]; 1] = [&[
b"escrow",
binding.as_ref(),
&self.escrow.seed.to_le_bytes()[..],
&[self.escrow.bump],
]];
let amount = self.vault.amount;
require!(amount > 0, EscrowError::InvalidAmount);
// Transfer Token A (Vault -> Taker)
transfer_checked(
CpiContext::new_with_signer(
self.token_program.to_account_info(),
TransferChecked {
from: self.vault.to_account_info(),
to: self.taker_ata_a.to_account_info(),
mint: self.mint_a.to_account_info(),
authority: self.escrow.to_account_info(),
},
&signer_seeds,
),
amount,
self.mint_a.decimals,
)?;
// Close the vault
close_account(CpiContext::new_with_signer(
self.token_program.to_account_info(),
CloseAccount {
account: self.vault.to_account_info(),
authority: self.escrow.to_account_info(),
destination: self.maker.to_account_info(),
},
&signer_seeds,
))?;
Ok(())
}
}
pub fn handler(ctx: Context<Take>) -> Result<()> {
// Transfer Token B to Maker
ctx.accounts.transfer_to_maker()?;
// Withdraw and close the Vault
ctx.accounts.withdraw_and_close_vault()?;
Ok(())
}
这段代码实现了 Escrow 合约中的 take 指令,用于完成托管交易的最终成交与清算流程:在账户约束层首先校验 escrow 的 PDA、创建者、以及参与的两种 Mint 的一致性;在执行逻辑中,先由接受者(taker)将约定数量的 Token B 转移给创建者(maker),随后通过 PDA 签名将托管金库(vault)中的全部 Token A 转移给 taker,并在转移完成后关闭 vault,将其剩余租金返还给 maker;同时 escrow 账户本身也在指令结束时被自动关闭,从而保证资产与状态一次性结算、无残留账户,完成一个完整、原子化的 escrow 成交流程。
instructions/refund.rs 文件use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{
close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface,
TransferChecked,
},
};
use crate::{
errors::EscrowError,
state::{Escrow, ESCROW_SEED},
};
/// 1. 关闭托管 PDA,并将其租金 lamports 返还给创建者。
/// 2. 将金库中的全部 Token A 余额转回创建者,然后关闭金库账户。
/// 只有 Maker 能在没人 Take 的情况下,
/// 把 Vault 里的 Token A 原路拿回,并彻底销毁 Escrow。
/// 它解决的是这个问题:
/// ❓ 如果一直没人来换,我的 Token A 会不会被锁死?
/// 答案:不会,Refund 就是逃生门。
#[derive(Accounts)]
pub struct Refund<'info> {
#[account(mut)]
pub maker: Signer<'info>,
#[account(
mut,
close = maker,
seeds = [ESCROW_SEED, maker.key().as_ref(), escrow.seed.to_le_bytes().as_ref()],
bump = escrow.bump,
has_one = maker @ EscrowError::InvalidMaker, // 校验 maker 与 escrow 一致。
has_one = mint_a @ EscrowError::InvalidMintA
)]
pub escrow: Account<'info, Escrow>,
/// Token Accounts
#[account(mint::token_program = token_program)]
pub mint_a: InterfaceAccount<'info, Mint>,
/// Vault holding Token A
#[account(
mut,
associated_token::mint = mint_a,
associated_token::authority = escrow,
associated_token::token_program = token_program
)]
pub vault: InterfaceAccount<'info, TokenAccount>,
#[account(
init_if_needed,
payer = maker,
associated_token::mint = mint_a,
associated_token::authority = maker,
associated_token::token_program = token_program
)]
pub maker_ata_a: InterfaceAccount<'info, TokenAccount>,
/// Programs
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(&mut self) -> Result<()> {
self.withdraw_and_close_vault()?;
Ok(())
}
fn withdraw_and_close_vault(&self) -> Result<()> {
let escrow = &self.escrow;
let seed_bytes = escrow.seed.to_le_bytes();
let signer_seeds: [&[&[u8]]; 1] = [&[
ESCROW_SEED,
escrow.maker.as_ref(),
seed_bytes.as_ref(),
&[escrow.bump],
]];
let amount = self.vault.amount;
if amount == 0 {
return Ok(());
}
// if amount > 0 {
// 1️⃣ Transfer all Token A from vault → maker
// // Vault -> Maker (Token A)
transfer_checked(
CpiContext::new_with_signer(
self.token_program.to_account_info(),
TransferChecked {
from: self.vault.to_account_info(),
to: self.maker_ata_a.to_account_info(),
mint: self.mint_a.to_account_info(),
authority: self.escrow.to_account_info(),
},
&signer_seeds,
),
amount,
self.mint_a.decimals,
)?;
// }
// 2️⃣ Close vault account, refund rent → maker
close_account(CpiContext::new_with_signer(
self.token_program.to_account_info(),
CloseAccount {
account: self.vault.to_account_info(),
authority: self.escrow.to_account_info(),
destination: self.maker.to_account_info(),
},
&signer_seeds,
))?;
// 3️⃣ Escrow PDA will be closed automatically (close = maker)
Ok(())
}
}
pub fn handler(ctx: Context<Refund>) -> Result<()> {
ctx.accounts.refund()?;
Ok(())
}
| 指令 | Token A 去向 | Token B 去向 | 谁能调用 |
|---|---|---|---|
| Take | Vault → Taker | Taker → Maker | 任意 Taker |
| Refund | Vault → Maker | ❌ 不存在 | 只有 Maker |
👉 Refund 不涉及 Token B
👉 Refund 只关心 Token A + 权限
Refund 必须保证 3 件事:
1️⃣ 只有 Maker 能退
2️⃣ Token A 只能退回 Maker
3️⃣ Escrow 和 Vault 只能被关闭一次
这段代码实现了 Escrow 合约中的
refund指令,用来处理“无人成交时的安全退出”场景:只有创建者(maker)才能调用该指令,通过 PDA 签名从 escrow 控制的 vault 中将全部 Token A 原路转回自己的关联代币账户,并在转移完成后关闭 vault,将其租金返还给 maker;与此同时,close = maker约束会在指令结束时自动销毁 escrow PDA,本次托管状态被彻底清理,从而保证即使一直没有 taker 出现,创建者的资产也不会被锁死,Escrow 始终具备可回退、可清算的“逃生门”机制。

上面一条线:
👉 Maker Token A → Vault → Taker Token A
下面一条线:
👉 Taker Token B → Maker Token B
✔️ Vault 只碰 Token A
✔️ Token B 永远直接给 Maker
✔️ 两条线在一次 take 里 原子完成
Token B 给 Maker 的前提是: 在同一个
take指令里, Token A 必须“同时”从 Vault 转给 Taker。
否则:
import * as anchor from "@coral-xyz/anchor"
import { Program } from "@coral-xyz/anchor"
import type { BlueshiftAnchorEscrow } from "../target/types/blueshift_anchor_escrow.js"
import {
getOrCreateAssociatedTokenAccount,
createMint,
mintTo,
getAssociatedTokenAddressSync,
getAccount,
TOKEN_PROGRAM_ID,
ASSO... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!