返回 SC 笔记
SC Day 78

Solana/Anchor 高级特性 — 零拷贝/剩余账户/自定义错误 + 增强 Escrow

### 1. #[zero_copy] 大账户优化

2026-06-27
第四阶段:综合实战 (73-80)
solanaanchorzero-copyremaining-accountscustom-errorsescrowproduction

日期: 2026-06-27 方向: Solana / Anchor 阶段: 第四阶段:综合实战 (73-80) 标签: #solana #anchor #zero-copy #remaining-accounts #custom-errors #escrow #production


今日目标

类型内容
学习Anchor 高级特性:零拷贝、剩余账户、自定义错误码、事件日志
实操增强 Escrow 程序:添加超时取消、自定义错误、事件记录
产出生产级 Escrow 程序 + Anchor 高级模式速查表

核心概念

1. #[zero_copy] 大账户优化

在 Day 76 已经介绍了零拷贝的基本原理。这里深入介绍 Anchor 中的高级用法:

use anchor_lang::prelude::*;

/// 带有复杂嵌套结构的零拷贝账户
#[account(zero_copy)]
#[repr(C)]
pub struct GameState {
    pub authority: Pubkey,       // 32
    pub game_id: u64,            // 8
    pub status: u8,              // 1 (0=Pending, 1=Active, 2=Finished, 3=Cancelled)
    pub player_count: u8,        // 1
    pub _padding: [u8; 6],       // 6 (对齐到 8 字节边界)
    pub players: [PlayerInfo; 8], // 8 * 80 = 640
    pub scores: [u32; 8],        // 8 * 4 = 32
    pub created_at: i64,         // 8
    pub finished_at: i64,        // 8
}
// 总计: 32 + 8 + 1 + 1 + 6 + 640 + 32 + 8 + 8 = 736 bytes

#[zero_copy]
#[repr(C)]
pub struct PlayerInfo {
    pub pubkey: Pubkey,          // 32
    pub name: [u8; 32],         // 32(固定长度字符串)
    pub joined_at: i64,          // 8
    pub is_active: u8,           // 1
    pub _padding: [u8; 7],       // 7
}
// 每个: 80 bytes

/// 零拷贝账户的初始化
#[derive(Accounts)]
pub struct CreateGame<'info> {
    #[account(
        init,
        seeds = [b"game", authority.key().as_ref(), &game_id.to_le_bytes()],
        bump,
        payer = authority,
        space = 8 + std::mem::size_of::<GameState>(),
    )]
    pub game: AccountLoader<'info, GameState>,

    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

pub fn create_game(ctx: Context<CreateGame>, game_id: u64) -> Result<()> {
    // load_init() 用于初始化零拷贝账户
    let game = &mut ctx.accounts.game.load_init()?;
    game.authority = ctx.accounts.authority.key();
    game.game_id = game_id;
    game.status = 0; // Pending
    game.player_count = 0;
    game.created_at = Clock::get()?.unix_timestamp;
    game.finished_at = 0;
    Ok(())
}

#[repr(C)] vs #[repr(packed)]

// #[repr(C)] - C 语言内存对齐规则
// 优点:跨语言兼容,可预测的布局
// 缺点:可能有填充字节(浪费空间)
#[zero_copy]
#[repr(C)]
pub struct AlignedStruct {
    pub a: u8,       // 1 byte
    pub _pad1: [u8; 7], // 7 bytes padding (对齐到 8)
    pub b: u64,      // 8 bytes
    pub c: u8,       // 1 byte
    pub _pad2: [u8; 7], // 7 bytes padding
}
// 总计: 24 bytes

// #[repr(packed)] - 无填充,紧凑布局
// 优点:节省空间
// 缺点:未对齐的访问可能更慢(某些平台会 panic)
// Anchor 推荐使用 #[repr(C)] + 显式 padding

2. remaining_accounts — 可变长度账户列表

当指令需要处理不确定数量的账户时,使用 remaining_accounts

use anchor_lang::prelude::*;

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

    /// 批量转账:向多个接收者转账
    /// remaining_accounts 中传入 [recipient1, recipient2, ...]
    pub fn batch_airdrop(
        ctx: Context<BatchAirdrop>,
        amounts: Vec<u64>,
    ) -> Result<()> {
        let recipients = &ctx.remaining_accounts;

        require!(
            recipients.len() == amounts.len(),
            CustomError::MismatchedLengths
        );
        require!(recipients.len() <= 20, CustomError::TooManyRecipients);

        let vault = &ctx.accounts.vault;
        let authority_seeds = &[
            b"vault".as_ref(),
            vault.authority.as_ref(),
            &[vault.bump],
        ];
        let signer_seeds = &[&authority_seeds[..]];

        for (i, recipient) in recipients.iter().enumerate() {
            // 验证每个接收者账户
            require!(
                recipient.is_writable,
                CustomError::AccountNotWritable
            );

            // 使用 CPI 转账
            let transfer_ix = anchor_spl::token::Transfer {
                from: ctx.accounts.vault_token.to_account_info(),
                to: recipient.to_account_info(),
                authority: ctx.accounts.vault.to_account_info(),
            };

            anchor_spl::token::transfer(
                CpiContext::new_with_signer(
                    ctx.accounts.token_program.to_account_info(),
                    transfer_ix,
                    signer_seeds,
                ),
                amounts[i],
            )?;
        }

        emit!(AirdropCompleted {
            recipient_count: recipients.len() as u32,
            total_amount: amounts.iter().sum(),
        });

        Ok(())
    }

    /// 使用 remaining_accounts 实现动态多资产存款
    pub fn multi_asset_deposit(ctx: Context<MultiDeposit>) -> Result<()> {
        let remaining = &ctx.remaining_accounts;

        // 每 3 个账户为一组: [user_token, vault_token, mint]
        require!(remaining.len() % 3 == 0, CustomError::InvalidAccountCount);

        let asset_count = remaining.len() / 3;
        for i in 0..asset_count {
            let user_token = &remaining[i * 3];
            let vault_token = &remaining[i * 3 + 1];
            let mint = &remaining[i * 3 + 2];

            // 反序列化并验证
            let user_token_account: Account<TokenAccount> =
                Account::try_from(user_token)?;
            let vault_token_account: Account<TokenAccount> =
                Account::try_from(vault_token)?;

            require!(
                user_token_account.mint == vault_token_account.mint,
                CustomError::MintMismatch
            );

            // 转账逻辑...
        }

        Ok(())
    }
}

#[derive(Accounts)]
pub struct BatchAirdrop<'info> {
    #[account(
        seeds = [b"vault", vault.authority.as_ref()],
        bump = vault.bump,
    )]
    pub vault: Account<'info, Vault>,

    #[account(mut)]
    pub vault_token: Account<'info, TokenAccount>,

    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,
    // remaining_accounts: 接收者 token 账户列表
}

3. 自定义错误码 (#[error_code])

/// 自定义错误 - 清晰的错误信息和唯一的错误码
#[error_code]
pub enum EscrowError {
    /// 错误码从 6000 开始(Anchor 约定)
    #[msg("Escrow has already been completed")]
    AlreadyCompleted, // 6000

    #[msg("Escrow has been cancelled")]
    AlreadyCancelled, // 6001

    #[msg("Escrow timeout has not been reached")]
    TimeoutNotReached, // 6002

    #[msg("Escrow has expired and can no longer be completed")]
    EscrowExpired, // 6003

    #[msg("Invalid escrow state for this operation")]
    InvalidState, // 6004

    #[msg("Deposit amount must be greater than zero")]
    ZeroDeposit, // 6005

    #[msg("Unauthorized: not the escrow maker or taker")]
    Unauthorized, // 6006

    #[msg("Amount exceeds escrow balance")]
    InsufficientBalance, // 6007
}

// 使用自定义错误
pub fn complete_escrow(ctx: Context<CompleteEscrow>) -> Result<()> {
    let escrow = &ctx.accounts.escrow;

    // 使用 require! 宏 + 自定义错误
    require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);
    require!(
        Clock::get()?.unix_timestamp <= escrow.expiry_time,
        EscrowError::EscrowExpired
    );

    // 或使用 err! 宏
    if ctx.accounts.taker.key() != escrow.taker {
        return err!(EscrowError::Unauthorized);
    }

    Ok(())
}

// 客户端解析错误
// TypeScript:
// try {
//     await program.methods.completeEscrow().rpc();
// } catch (err) {
//     if (err instanceof anchor.AnchorError) {
//         console.log("Error code:", err.error.errorCode.number); // 6003
//         console.log("Error msg:", err.error.errorMessage); // "Escrow has expired..."
//     }
// }

4. 事件日志 (emit!)

/// 事件定义
#[event]
pub struct EscrowCreated {
    pub escrow_id: u64,
    pub maker: Pubkey,
    pub taker: Pubkey,
    pub amount: u64,
    pub token_mint: Pubkey,
    pub expiry_time: i64,
    pub timestamp: i64,
}

#[event]
pub struct EscrowCompleted {
    pub escrow_id: u64,
    pub maker: Pubkey,
    pub taker: Pubkey,
    pub amount: u64,
    pub timestamp: i64,
}

#[event]
pub struct EscrowCancelled {
    pub escrow_id: u64,
    pub cancelled_by: Pubkey,
    pub reason: String, // 注意:String 会增加序列化开销
    pub timestamp: i64,
}

// 使用
pub fn create_escrow(ctx: Context<CreateEscrow>, amount: u64, expiry: i64) -> Result<()> {
    // ... 业务逻辑 ...

    emit!(EscrowCreated {
        escrow_id: escrow.escrow_id,
        maker: ctx.accounts.maker.key(),
        taker: ctx.accounts.taker.key(),
        amount,
        token_mint: ctx.accounts.token_mint.key(),
        expiry_time: expiry,
        timestamp: Clock::get()?.unix_timestamp,
    });

    Ok(())
}

// TypeScript 监听事件:
// program.addEventListener("EscrowCreated", (event, slot) => {
//     console.log("New escrow:", event.escrowId.toString());
//     console.log("Amount:", event.amount.toString());
// });

代码实战

增强版 Escrow 程序

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

declare_id!("Escrw111111111111111111111111111111111111111");

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

    /// 创建 Escrow:maker 存入 token_a,等待 taker 用 token_b 交换
    pub fn create(
        ctx: Context<Create>,
        escrow_id: u64,
        deposit_amount: u64,
        expected_amount: u64,
        timeout_seconds: i64,
    ) -> Result<()> {
        require!(deposit_amount > 0, EscrowError::ZeroDeposit);
        require!(expected_amount > 0, EscrowError::ZeroDeposit);
        require!(timeout_seconds > 0 && timeout_seconds <= 30 * 24 * 3600,
            EscrowError::InvalidTimeout);

        let now = Clock::get()?.unix_timestamp;

        // 初始化 Escrow 账户
        let escrow = &mut ctx.accounts.escrow;
        escrow.escrow_id = escrow_id;
        escrow.maker = ctx.accounts.maker.key();
        escrow.taker = Pubkey::default(); // 任何人都可以接受
        escrow.mint_a = ctx.accounts.mint_a.key();
        escrow.mint_b = ctx.accounts.mint_b.key();
        escrow.deposit_amount = deposit_amount;
        escrow.expected_amount = expected_amount;
        escrow.status = EscrowStatus::Active as u8;
        escrow.created_at = now;
        escrow.expiry_time = now + timeout_seconds;
        escrow.bump = ctx.bumps.escrow;
        escrow.vault_bump = ctx.bumps.vault;

        // 将 token_a 从 maker 转到 vault
        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.maker_token_a.to_account_info(),
                    to: ctx.accounts.vault.to_account_info(),
                    authority: ctx.accounts.maker.to_account_info(),
                },
            ),
            deposit_amount,
        )?;

        emit!(EscrowCreated {
            escrow_id,
            maker: ctx.accounts.maker.key(),
            mint_a: ctx.accounts.mint_a.key(),
            mint_b: ctx.accounts.mint_b.key(),
            deposit_amount,
            expected_amount,
            expiry_time: escrow.expiry_time,
            timestamp: now,
        });

        Ok(())
    }

    /// 完成交换:taker 发送 token_b 给 maker,获得 vault 中的 token_a
    pub fn exchange(ctx: Context<Exchange>) -> Result<()> {
        let escrow = &ctx.accounts.escrow;

        // 状态检查
        require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);

        // 超时检查
        let now = Clock::get()?.unix_timestamp;
        require!(now <= escrow.expiry_time, EscrowError::EscrowExpired);

        // 步骤 1:taker 发送 token_b 给 maker
        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.taker_token_b.to_account_info(),
                    to: ctx.accounts.maker_token_b.to_account_info(),
                    authority: ctx.accounts.taker.to_account_info(),
                },
            ),
            escrow.expected_amount,
        )?;

        // 步骤 2:vault 中的 token_a 发送给 taker(PDA 签名)
        let maker_key = escrow.maker;
        let escrow_id_bytes = escrow.escrow_id.to_le_bytes();
        let seeds = &[
            b"escrow".as_ref(),
            maker_key.as_ref(),
            escrow_id_bytes.as_ref(),
            &[escrow.bump],
        ];
        let signer_seeds = &[&seeds[..]];

        let vault_seeds = &[
            b"vault".as_ref(),
            ctx.accounts.escrow.to_account_info().key.as_ref(),
            &[escrow.vault_bump],
        ];
        let vault_signer = &[&vault_seeds[..]];

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault.to_account_info(),
                    to: ctx.accounts.taker_token_a.to_account_info(),
                    authority: ctx.accounts.vault_authority.to_account_info(),
                },
                vault_signer,
            ),
            escrow.deposit_amount,
        )?;

        // 更新状态
        let escrow = &mut ctx.accounts.escrow;
        escrow.status = EscrowStatus::Completed as u8;

        emit!(EscrowCompleted {
            escrow_id: escrow.escrow_id,
            maker: escrow.maker,
            taker: ctx.accounts.taker.key(),
            deposit_amount: escrow.deposit_amount,
            expected_amount: escrow.expected_amount,
            timestamp: now,
        });

        Ok(())
    }

    /// 超时取消:maker 在超时后可以取回存款
    pub fn cancel_timeout(ctx: Context<CancelTimeout>) -> Result<()> {
        let escrow = &ctx.accounts.escrow;

        require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);

        let now = Clock::get()?.unix_timestamp;
        require!(now > escrow.expiry_time, EscrowError::TimeoutNotReached);

        // 从 vault 返还 token_a 给 maker
        let vault_seeds = &[
            b"vault".as_ref(),
            ctx.accounts.escrow.to_account_info().key.as_ref(),
            &[escrow.vault_bump],
        ];
        let vault_signer = &[&vault_seeds[..]];

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault.to_account_info(),
                    to: ctx.accounts.maker_token_a.to_account_info(),
                    authority: ctx.accounts.vault_authority.to_account_info(),
                },
                vault_signer,
            ),
            escrow.deposit_amount,
        )?;

        // 更新状态
        let escrow = &mut ctx.accounts.escrow;
        escrow.status = EscrowStatus::Cancelled as u8;

        emit!(EscrowCancelled {
            escrow_id: escrow.escrow_id,
            cancelled_by: ctx.accounts.maker.key(),
            timestamp: now,
        });

        Ok(())
    }

    /// Maker 主动取消(超时前也可以,但可能需要额外条件)
    pub fn cancel_by_maker(ctx: Context<CancelByMaker>) -> Result<()> {
        let escrow = &ctx.accounts.escrow;

        require!(escrow.status == EscrowStatus::Active as u8, EscrowError::InvalidState);

        // 返还资金(与 cancel_timeout 类似)
        let vault_seeds = &[
            b"vault".as_ref(),
            ctx.accounts.escrow.to_account_info().key.as_ref(),
            &[escrow.vault_bump],
        ];
        let vault_signer = &[&vault_seeds[..]];

        token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault.to_account_info(),
                    to: ctx.accounts.maker_token_a.to_account_info(),
                    authority: ctx.accounts.vault_authority.to_account_info(),
                },
                vault_signer,
            ),
            escrow.deposit_amount,
        )?;

        let escrow = &mut ctx.accounts.escrow;
        escrow.status = EscrowStatus::Cancelled as u8;

        emit!(EscrowCancelled {
            escrow_id: escrow.escrow_id,
            cancelled_by: ctx.accounts.maker.key(),
            timestamp: Clock::get()?.unix_timestamp,
        });

        Ok(())
    }
}

// ============ 账户定义 ============

#[derive(Accounts)]
#[instruction(escrow_id: u64)]
pub struct Create<'info> {
    #[account(
        init,
        seeds = [b"escrow", maker.key().as_ref(), &escrow_id.to_le_bytes()],
        bump,
        payer = maker,
        space = 8 + EscrowState::INIT_SPACE,
    )]
    pub escrow: Account<'info, EscrowState>,

    #[account(
        init,
        seeds = [b"vault", escrow.key().as_ref()],
        bump,
        payer = maker,
        token::mint = mint_a,
        token::authority = vault_authority,
    )]
    pub vault: Account<'info, TokenAccount>,

    /// CHECK: PDA 作为 vault 的 authority
    #[account(
        seeds = [b"vault", escrow.key().as_ref()],
        bump,
    )]
    pub vault_authority: AccountInfo<'info>,

    pub mint_a: Account<'info, Mint>,
    pub mint_b: Account<'info, Mint>,

    #[account(
        mut,
        constraint = maker_token_a.mint == mint_a.key(),
        constraint = maker_token_a.owner == maker.key(),
    )]
    pub maker_token_a: Account<'info, TokenAccount>,

    #[account(mut)]
    pub maker: Signer<'info>,

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

#[derive(Accounts)]
pub struct Exchange<'info> {
    #[account(
        mut,
        seeds = [b"escrow", escrow.maker.as_ref(), &escrow.escrow_id.to_le_bytes()],
        bump = escrow.bump,
    )]
    pub escrow: Account<'info, EscrowState>,

    #[account(
        mut,
        seeds = [b"vault", escrow.key().as_ref()],
        bump = escrow.vault_bump,
    )]
    pub vault: Account<'info, TokenAccount>,

    /// CHECK: vault authority PDA
    #[account(
        seeds = [b"vault", escrow.key().as_ref()],
        bump = escrow.vault_bump,
    )]
    pub vault_authority: AccountInfo<'info>,

    #[account(
        mut,
        constraint = taker_token_a.mint == escrow.mint_a,
    )]
    pub taker_token_a: Account<'info, TokenAccount>,

    #[account(
        mut,
        constraint = taker_token_b.mint == escrow.mint_b,
        constraint = taker_token_b.owner == taker.key(),
    )]
    pub taker_token_b: Account<'info, TokenAccount>,

    #[account(
        mut,
        constraint = maker_token_b.mint == escrow.mint_b,
        constraint = maker_token_b.owner == escrow.maker,
    )]
    pub maker_token_b: Account<'info, TokenAccount>,

    #[account(mut)]
    pub taker: Signer<'info>,

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

#[derive(Accounts)]
pub struct CancelTimeout<'info> {
    #[account(
        mut,
        seeds = [b"escrow", escrow.maker.as_ref(), &escrow.escrow_id.to_le_bytes()],
        bump = escrow.bump,
        has_one = maker,
    )]
    pub escrow: Account<'info, EscrowState>,

    #[account(mut)]
    pub vault: Account<'info, TokenAccount>,

    /// CHECK: vault authority PDA
    pub vault_authority: AccountInfo<'info>,

    #[account(
        mut,
        constraint = maker_token_a.mint == escrow.mint_a,
    )]
    pub maker_token_a: Account<'info, TokenAccount>,

    pub maker: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

#[derive(Accounts)]
pub struct CancelByMaker<'info> {
    #[account(
        mut,
        has_one = maker,
    )]
    pub escrow: Account<'info, EscrowState>,

    #[account(mut)]
    pub vault: Account<'info, TokenAccount>,

    /// CHECK: vault authority PDA
    pub vault_authority: AccountInfo<'info>,

    #[account(mut)]
    pub maker_token_a: Account<'info, TokenAccount>,

    pub maker: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

// ============ 数据模型 ============

#[account]
#[derive(InitSpace)]
pub struct EscrowState {
    pub escrow_id: u64,
    pub maker: Pubkey,
    pub taker: Pubkey,
    pub mint_a: Pubkey,
    pub mint_b: Pubkey,
    pub deposit_amount: u64,
    pub expected_amount: u64,
    pub status: u8,
    pub created_at: i64,
    pub expiry_time: i64,
    pub bump: u8,
    pub vault_bump: u8,
}

#[repr(u8)]
pub enum EscrowStatus {
    Active = 0,
    Completed = 1,
    Cancelled = 2,
}

// ============ 错误和事件 ============

#[error_code]
pub enum EscrowError {
    #[msg("Deposit amount must be greater than zero")]
    ZeroDeposit,
    #[msg("Invalid escrow state for this operation")]
    InvalidState,
    #[msg("Escrow has expired")]
    EscrowExpired,
    #[msg("Timeout has not been reached yet")]
    TimeoutNotReached,
    #[msg("Invalid timeout duration")]
    InvalidTimeout,
    #[msg("Unauthorized")]
    Unauthorized,
}

#[event]
pub struct EscrowCreated {
    pub escrow_id: u64,
    pub maker: Pubkey,
    pub mint_a: Pubkey,
    pub mint_b: Pubkey,
    pub deposit_amount: u64,
    pub expected_amount: u64,
    pub expiry_time: i64,
    pub timestamp: i64,
}

#[event]
pub struct EscrowCompleted {
    pub escrow_id: u64,
    pub maker: Pubkey,
    pub taker: Pubkey,
    pub deposit_amount: u64,
    pub expected_amount: u64,
    pub timestamp: i64,
}

#[event]
pub struct EscrowCancelled {
    pub escrow_id: u64,
    pub cancelled_by: Pubkey,
    pub timestamp: i64,
}

关键要点总结

特性用途注意事项
#[zero_copy]大账户高效读写字段必须固定大小,需要 #[repr(C)]
remaining_accounts动态长度账户列表需要手动验证每个账户
#[error_code]清晰的错误反馈错误码从 6000 开始
emit!链上事件日志可被客户端监听和索引
init_if_needed条件创建账户需要谨慎使用,可能有安全风险
has_one字段匹配验证编译时生成约束检查代码

常见误区

  1. "remaining_accounts 自动验证" — 错误!remaining_accounts 没有任何自动验证,必须手动检查
  2. "emit! 和 msg! 等价" — 不对。emit! 产生结构化的事件日志(可索引),msg! 只产生文本日志
  3. "错误码是任意的" — Anchor 错误码从 6000 开始,系统错误和 Anchor 内部错误占用了更低的范围
  4. "超时就能自动取消 Escrow" — Solana 没有自动执行机制,超时只是允许 maker 手动取消
  5. "init_if_needed 没有风险" — 有!如果账户已被其他程序创建(但 owner 不对),可能导致意外行为

面试关联

面试题:Solana Escrow 设计中有哪些关键的安全考虑?

30 秒回答: 核心考虑包括:PDA 签名确保只有程序能控制 vault、超时机制防止资金永久锁定、状态机确保操作原子性、所有传入账户的身份和所有权验证。

2 分钟回答: Escrow 设计的安全性体现在四个层面。第一是资金安全——使用 PDA 控制的 vault 账户存放资金,确保只有程序逻辑能转移资金,没有单一私钥可以直接提取。第二是超时机制——没有超时的 Escrow 意味着资金可能永久锁定,设计合理的超时窗口和取消流程至关重要。第三是状态管理——Escrow 有明确的状态机(Active/Completed/Cancelled),每个操作都检查当前状态,防止重复完成或取消。第四是账户验证——Anchor 的 has_oneconstraint 确保传入的 token 账户、mint 地址都匹配,防止攻击者传入伪造账户窃取资金。


参考资源

资源说明
Anchor BookAnchor 官方教程
Anchor Examples官方示例代码
Solana Cookbook - PDAsPDA 使用指南
Token Swap ExampleAnchor Escrow 测试
Anchor Error Handling错误处理文档