本文介绍了Solana区块链上的Vault Program的概念,它是一种安全存储和控制数字资产(如tokens或NFTs)的机制。文章详细解释了Vault Program的用途,包括安全资产存储、条件访问、自动化和无需信任的系统。此外,文章还通过Anchor框架,一步步地构建了一个基本的token vault,实现了初始化、存款、取款、锁定和解锁等核心功能。
Solana Vault Program 是一种用于在 Solana 区块链上安全地存储和控制数字资产(如 tokens 或 NFTs)的概念。
Vault Program 用于为区块链上的资产存储和处理带来安全性、控制和自动化。
以下是使用它的原因:
我们研究它们是因为它们是在 Solana 上构建安全、现实世界应用程序的基础工具。
以下是它重要的原因:
Vaults 是许多去中心化应用程序的核心:
Vaults 教你 Solana 如何通过程序和 PDAs 管理所有权。
了解 vaults 可以帮助你了解真实的项目如何安全地管理资产——这是区块链开发的关键部分。
研究 vaults 是理解链上逻辑、安全性和账户管理的一个切入点,这些对于 Solana 开发者来说都是必不可少的。
Vaults 不是常规钱包。它们是程序控制的账户——用户无法直接访问它们。只有程序逻辑可以根据条件移动资金。
从 vault 提款是有条件的。除非满足特定规则,否则资产将保持锁定。
即使是简单的应用程序也可以从 vaults 中受益,尤其是在资产安全、无需信任或自动化很重要的情况下。
Vaults 是一层保护。开发人员仍然需要在访问控制、指令验证和代码审计方面遵循最佳实践。
现在我们了解了 vaults 是什么以及它们为什么重要,让我们深入研究实际实现。我们将构建一个全面的 token vault,通过五个关键指令演示所有核心概念:初始化,存款,取款,锁定和解锁。
在我们开始编码之前,请确保你已具备:
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
)首先,让我们创建一个新的 Anchor 项目:
anchor init token_vault
cd token_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)]
宏会自动计算我们的账户所需的空间,从而更容易管理账户大小。
我们的第一个指令创建 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()
一次初始化所有账户字段 - 它比单独分配更干净、更安全现在让我们添加将 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 账户提取更复杂,因为我们需要检查 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(())
}
要点:
CpiContext::new_with_signer()
,因为我们的 PDA 需要签署交易让我们添加使用基于时间的条件锁定 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(())
}
要点:
msg!
宏记录可以在交易日志中看到的信息最后,让我们添加解锁功能:
##[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<()> { /* ... */ }
}
现在让我们创建全面的测试。创建 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 概念:
Vaults 是在 Solana 上安全和自动化资产处理的基础模式。我们已经探索了如何使用 Anchor 构建一个基本的 token vault,其中包含存款、取款和时间锁功能。
从这里开始,你可以扩展到更高级的用例,例如收费提款、限时 SOL 处理、白名单访问、托管逻辑和多用户流程。这些模式使 vaults 成为 DeFi、NFTs 及其他领域中现实世界应用程序的强大工具。
如果你对去中心化基础设施、链上数据系统或使用预言机网络构建真实世界的项目感兴趣,请继续关注:
- 原文链接: blog.blockmagnates.com/v...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!