Solana 中的 Vault

本文介绍了Solana区块链上的Vault Program的概念,它是一种安全存储和控制数字资产(如tokens或NFTs)的机制。文章详细解释了Vault Program的用途,包括安全资产存储、条件访问、自动化和无需信任的系统。此外,文章还通过Anchor框架,一步步地构建了一个基本的token vault,实现了初始化、存款、取款、锁定和解锁等核心功能。

Solana 中的 Vaults

Solana Vault Program 是一种用于在 Solana 区块链上安全地存储和控制数字资产(如 tokens 或 NFTs)的概念。

Vault Program 用于为区块链上的资产存储和处理带来安全性、控制和自动化。

以下是使用它的原因:

安全的资产存储

  • Vaults 通过将 tokens/NFTs 置于智能合约控制之下而不是用户的钱包来保护它们
  • 这降低了用户端黑客或错误的风险

有条件访问

  • 只有在满足特定条件时才能移动资产,例如:
  • 已经过了特定的时间
  • 交易获得批准
  • 一定数量的签名者批准(多重签名)

自动化

  • Vaults 能够实现自动化的金融逻辑,例如:
  • 在销售期间锁定 tokens
  • 在归属期结束后解锁它们
  • 随着时间的推移释放奖励

无需信任的系统

  • Vault 确保规则由代码而不是人来执行
  • 你不需要信任个人,只需要信任程序的逻辑

为什么我们要研究 Vault Programs?

我们研究它们是因为它们是在 Solana 上构建安全、现实世界应用程序的基础工具。

以下是它重要的原因:

DeFi 和 NFTs 中的核心概念

Vaults 是许多去中心化应用程序的核心:

  • Token 质押
  • 归属计划
  • DAO 金库
  • 游戏或借贷的 NFT 托管

了解程序所有权

Vaults 教你 Solana 如何通过程序和 PDAs 管理所有权。

现实世界的用例

了解 vaults 可以帮助你了解真实的项目如何安全地管理资产——这是区块链开发的关键部分。

智能合约逻辑

研究 vaults 是理解链上逻辑、安全性和账户管理的一个切入点,这些对于 Solana 开发者来说都是必不可少的。

误解

Vaults 仅仅是钱包

Vaults 不是常规钱包。它们是程序控制的账户——用户无法直接访问它们。只有程序逻辑可以根据条件移动资金。

你可以随时提款

从 vault 提款是有条件的。除非满足特定规则,否则资产将保持锁定。

Vaults 对于小型项目来说太复杂了

即使是简单的应用程序也可以从 vaults 中受益,尤其是在资产安全、无需信任或自动化很重要的情况下。

Vaults 可以满足所有安全需求

Vaults 是一层保护。开发人员仍然需要在访问控制、指令验证和代码审计方面遵循最佳实践。

使用 Anchor 构建你的第一个 Solana Vault

现在我们了解了 vaults 是什么以及它们为什么重要,让我们深入研究实际实现。我们将构建一个全面的 token vault,通过五个关键指令演示所有核心概念:初始化,存款,取款,锁定和解锁。

前提条件

在我们开始编码之前,请确保你已具备:

  • Rust 和 Cargo 已安装
  • Solana CLI 工具
  • Anchor 框架 (cargo install --git https://github.com/coral-xyz/anchor avm --locked --force)
  • 对 Solana 账户和 PDAs 的基本了解

项目设置

首先,让我们创建一个新的 Anchor 项目:

anchor init token_vault
cd token_vault

定义我们的 Vault 结构

让我们首先定义我们的 vault 需要的核心账户结构。打开 programs/token_vault/src/lib.rs 并添加:

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer, Mint};
declare_id!("YourProgramIdHere");
##[account]
##[derive(InitSpace)]
pub struct Vault {
    pub authority: Pubkey,        // 谁可以控制这个 vault
    pub token_account: Pubkey,    // 持有资金的 token 账户
    pub bump: u8,                 // vault 账户的 PDA bump
    pub authority_bump: u8,       // vault 授权机构的 PDA bump
    pub is_locked: bool,          // vault 是否被锁定
    pub unlock_timestamp: i64,    // 何时可以解锁 vault (0 = 没有时间锁)
}

##[error_code]
pub enum VaultError {
    #[msg("Vault is still locked")]
    VaultStillLocked,
    #[msg("Insufficient funds in vault")]
    InsufficientFunds,
    #[msg("Unauthorized access")]
    UnauthorizedAccess,
}

#[derive(InitSpace)] 宏会自动计算我们的账户所需的空间,从而更容易管理账户大小。

指令 1:初始化 Vault

我们的第一个指令创建 vault 账户并设置将持有我们资产的 token 账户。

##[derive(Accounts)]
##[instruction(bump: u8, authority_bump: u8)]
pub struct InitializeVault<'info> {
    #[account(
        init,
        payer = payer,
        seeds = [b"vault", payer.key().as_ref()],
        bump,
        space = 8 + Vault::INIT_SPACE
    )]
    pub vault: Account<'info, Vault>,

    /// CHECK: Safe because we derive it with PDA and just store it
    #[account(
        seeds = [b"authority", vault.key().as_ref()],
        bump = authority_bump
    )]
    pub vault_authority: UncheckedAccount<'info>,
    #[account(
        init,
        payer = payer,
        token::mint = mint,
        token::authority = vault_authority,
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub mint: Account<'info, Mint>,

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

pub fn initialize_vault(
    ctx: Context<InitializeVault>,
    bump: u8,
    authority_bump: u8,
) -> Result<()> {
    ctx.accounts.vault.set_inner(Vault {
        authority: ctx.accounts.payer.key(),
        token_account: ctx.accounts.token_account.key(),
        bump,
        authority_bump,
        is_locked: false,
        unlock_timestamp: 0,
    });
    Ok(())
}

要点:

  • 我们使用 set_inner() 一次初始化所有账户字段 - 它比单独分配更干净、更安全
  • vault 授权机构是一个 PDA,它将拥有 token 账户,确保只有我们的程序可以移动 tokens
  • 我们存储 vault bump 和授权机构 bump 以供以后使用

指令 2:存入 Tokens

现在让我们添加将 tokens 存入我们的 vault 的功能:

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

    #[account(
        mut,
        token::authority = authority,
    )]
    pub user_token_account: Account<'info, TokenAccount>,
    #[account(
        mut,
        address = vault.token_account
    )]
    pub vault_token_account: Account<'info, TokenAccount>,
    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    let cpi_accounts = Transfer {
        from: ctx.accounts.user_token_account.to_account_info(),
        to: ctx.accounts.vault_token_account.to_account_info(),
        authority: ctx.accounts.authority.to_account_info(),
    };
    let cpi_program = ctx.accounts.token_program.to_account_info();
    let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
    token::transfer(cpi_ctx, amount)?;
    Ok(())
}

要点:

  • has_one = authority 确保只有 vault 的所有者才能存入
  • address = vault.token_account 验证我们是否正在存入正确的 token 账户
  • 我们使用跨程序调用 (CPI) 来调用 SPL Token 程序

指令 3:提取 Tokens

提取更复杂,因为我们需要检查 vault 是否已锁定:

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

    /// CHECK: Safe because we derive it with PDA
    #[account(
        seeds = [b"authority", vault.key().as_ref()],
        bump = vault.authority_bump
    )]
    pub vault_authority: UncheckedAccount<'info>,
    #[account(
        mut,
        token::authority = authority,
    )]
    pub user_token_account: Account<'info, TokenAccount>,
    #[account(
        mut,
        address = vault.token_account
    )]
    pub vault_token_account: Account<'info, TokenAccount>,
    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let vault = &ctx.accounts.vault;

    // 检查 vault 是否已锁定
    require!(!vault.is_locked, VaultError::VaultStillLocked);

    // 检查我们是否有足够的 tokens
    require!(
        ctx.accounts.vault_token_account.amount >= amount,
        VaultError::InsufficientFunds
    );
    // 创建 PDA 签名者 seeds
    let authority_seed = &[
        b"authority",
        vault.key().as_ref(),
        &[vault.authority_bump],
    ];
    let signer = &[&authority_seed[..]];
    let cpi_accounts = Transfer {
        from: ctx.accounts.vault_token_account.to_account_info(),
        to: ctx.accounts.user_token_account.to_account_info(),
        authority: ctx.accounts.vault_authority.to_account_info(),
    };
    let cpi_program = ctx.accounts.token_program.to_account_info();
    let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
    token::transfer(cpi_ctx, amount)?;
    Ok(())
}

要点:

  • 我们在允许提取之前检查 vault 是否已锁定
  • 我们验证 vault 中是否存在足够的资金
  • 我们使用 CpiContext::new_with_signer(),因为我们的 PDA 需要签署交易
  • 签名者 seeds 允许我们的程序代表 vault 授权机构 PDA 行事

指令 4:锁定 Vault

让我们添加使用基于时间的条件锁定 vault 的功能:

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

    pub authority: Signer<'info>,
}

pub fn lock_vault(ctx: Context<LockVault>, unlock_timestamp: i64) -> Result<()> {
    let vault = &mut ctx.accounts.vault;
    vault.is_locked = true;
    vault.unlock_timestamp = unlock_timestamp;

    msg!("Vault locked until timestamp: {}", unlock_timestamp);
    Ok(())
}

要点:

  • 只有 vault 授权机构可以锁定 vault
  • 我们同时设置锁定状态和解锁时间戳
  • msg! 宏记录可以在交易日志中看到的信息

指令 5:解锁 Vault

最后,让我们添加解锁功能:

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

    pub authority: Signer<'info>,
}

pub fn unlock_vault(ctx: Context<UnlockVault>) -> Result<()> {
    let vault = &mut ctx.accounts.vault;
    let clock = Clock::get()?;
    require!(
        clock.unix_timestamp >= vault.unlock_timestamp,
        VaultError::VaultStillLocked
    );
    vault.is_locked = false;
    vault.unlock_timestamp = 0;

    msg!("Vault unlocked successfully");
    Ok(())
}

要点:

  • 我们使用 Clock::get()? 获取当前的区块链时间
  • 我们验证是否已经过了足够的时间才允许解锁
  • 我们重置锁定状态和时间戳

完整的程序结构

你的最终 lib.rs 应该具有以下结构:

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

    pub fn initialize_vault(/* ... */) -> Result<()> { /* ... */ }
    pub fn deposit(/* ... */) -> Result<()> { /* ... */ }
    pub fn withdraw(/* ... */) -> Result<()> { /* ... */ }
    pub fn lock_vault(/* ... */) -> Result<()> { /* ... */ }
    pub fn unlock_vault(/* ... */) -> Result<()> { /* ... */ }
}

测试我们的 Vault

现在让我们创建全面的测试。创建 tests/token_vault.ts

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { TokenVault } from "../target/types/token_vault";
import {
  createMint,
  createAccount,
  mintTo,
  getAccount
} from "@solana/spl-token";
import { expect } from "chai";

describe("token_vault", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.TokenVault as Program<TokenVault>;
  const payer = provider.wallet.publicKey;

  let mint: anchor.web3.PublicKey;
  let userTokenAccount: anchor.web3.PublicKey;
  let vault: anchor.web3.PublicKey;
  let vaultAuthority: anchor.web3.PublicKey;
  let vaultTokenAccount: anchor.web3.PublicKey;
  let vaultBump: number;
  let authorityBump: number;
  before(async () => {
    // 创建 mint
    mint = await createMint(
      provider.connection,
      provider.wallet.payer,
      payer,
      payer,
      9
    );
    // 创建用户 token 账户
    userTokenAccount = await createAccount(
      provider.connection,
      provider.wallet.payer,
      mint,
      payer
    );
    // 将 tokens 铸造给用户
    await mintTo(
      provider.connection,
      provider.wallet.payer,
      mint,
      userTokenAccount,
      payer,
      1000 * 10**9 // 1000 个 tokens
    );
    // 查找 PDAs
    [vault, vaultBump] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("vault"), payer.toBuffer()],
      program.programId
    );
    [vaultAuthority, authorityBump] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("authority"), vault.toBuffer()],
      program.programId
    );
  });
  it("初始化 vault", async () => {
    await program.methods
      .initializeVault(vaultBump, authorityBump)
      .accounts({
        vault,
        vaultAuthority,
        mint,
        payer,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .rpc();
    const vaultAccount = await program.account.vault.fetch(vault);
    expect(vaultAccount.authority.toString()).to.equal(payer.toString());
    expect(vaultAccount.isLocked).to.be.false;

    // 获取创建的 token 账户
    const vaultData = await program.account.vault.fetch(vault);
    vaultTokenAccount = vaultData.tokenAccount;

    console.log("✅ Vault 初始化成功");
  });
  it("存入 tokens", async () => {
    const depositAmount = 500 * 10**9; // 500 个 tokens

    await program.methods
      .deposit(new anchor.BN(depositAmount))
      .accounts({
        vault,
        userTokenAccount,
        vaultTokenAccount,
        authority: payer,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
      })
      .rpc();
    const vaultBalance = await getAccount(provider.connection, vaultTokenAccount);
    expect(vaultBalance.amount.toString()).to.equal(depositAmount.toString());

    console.log("✅ Tokens 存入成功");
  });
  it("解锁后提取 tokens", async () => {
    const withdrawAmount = 100 * 10**9; // 100 个 tokens

    await program.methods
      .withdraw(new anchor.BN(withdrawAmount))
      .accounts({
        vault,
        vaultAuthority,
        userTokenAccount,
        vaultTokenAccount,
        authority: payer,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
      })
      .rpc();
    const vaultBalance = await getAccount(provider.connection, vaultTokenAccount);
    expect(vaultBalance.amount.toString()).to.equal((400 * 10**9).toString());

    console.log("✅ Tokens 提取成功");
  });
  it("使用时间戳锁定 vault", async () => {
    const unlockTime = Math.floor(Date.now() / 1000) + 2; // 从现在开始 2 秒

    await program.methods
      .lockVault(new anchor.BN(unlockTime))
      .accounts({
        vault,
        authority: payer,
      })
      .rpc();
    const vaultAccount = await program.account.vault.fetch(vault);
    expect(vaultAccount.isLocked).to.be.true;
    expect(vaultAccount.unlockTimestamp.toString()).to.equal(unlockTime.toString());

    console.log("✅ Vault 锁定成功");
  });
  it("锁定时无法提取", async () => {
    try {
      await program.methods
        .withdraw(new anchor.BN(50 * 10**9))
        .accounts({
          vault,
          vaultAuthority,
          userTokenAccount,
          vaultTokenAccount,
          authority: payer,
          tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
        })
        .rpc();

      expect.fail("应该失败");
    } catch (error) {
      expect(error.error.errorMessage).to.include("Vault is still locked");
      console.log("✅ 当 vault 锁定时,提取被正确阻止");
    }
  });
  it("时间过去后解锁 vault", async () => {
    // 等待锁定时间过去
    await new Promise(resolve => setTimeout(resolve, 3000));

    await program.methods
      .unlockVault()
      .accounts({
        vault,
        authority: payer,
      })
      .rpc();
    const vaultAccount = await program.account.vault.fetch(vault);
    expect(vaultAccount.isLocked).to.be.false;

    console.log("✅ Vault 解锁成功");
  });
  it("解锁后提取", async () => {
    const withdrawAmount = 50 * 10**9; // 50 个 tokens

    await program.methods
      .withdraw(new anchor.BN(withdrawAmount))
      .accounts({
        vault,
        vaultAuthority,
        userTokenAccount,
        vaultTokenAccount,
        authority: payer,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
      })
      .rpc();
    const vaultBalance = await getAccount(provider.connection, vaultTokenAccount);
    expect(vaultBalance.amount.toString()).to.equal((350 * 10**9).toString());

    console.log("✅ 解锁后提取成功");
  });
});

运行测试

  • 构建程序:
anchor build
  • 运行测试:
anchor test

你应该看到类似以下的输出:

✅ Vault 初始化成功
✅ Tokens 存入成功
✅ Tokens 提取成功
✅ Vault 锁定成功
✅ 当 vault 锁定时,提取被正确阻止
✅ Vault 解锁成功
✅ 解锁后提取成功

主要收获

这个 vault 实现演示了几个关键的 Solana 概念:

  1. PDA 使用: 我们将 PDAs 用于 vault 账户和 vault 授权机构,确保确定性地址和程序控制
  2. 跨程序调用: 我们与 SPL Token 程序交互以移动 tokens
  3. 账户验证: Anchor 的约束系统确保我们正在使用正确的账户
  4. 基于时间的条件: 我们使用 Solana 的 Clock 系统变量实现时间锁
  5. 错误处理: 当操作失败时,自定义错误提供清晰的反馈

结论

Vaults 是在 Solana 上安全和自动化资产处理的基础模式。我们已经探索了如何使用 Anchor 构建一个基本的 token vault,其中包含存款、取款和时间锁功能。

从这里开始,你可以扩展到更高级的用例,例如收费提款、限时 SOL 处理、白名单访问、托管逻辑和多用户流程。这些模式使 vaults 成为 DeFi、NFTs 及其他领域中现实世界应用程序的强大工具。

如果你对去中心化基础设施、链上数据系统或使用预言机网络构建真实世界的项目感兴趣,请继续关注:

  • 原文链接: blog.blockmagnates.com/v...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block