核心指令详解
Token Program 提供了一组精心设计的指令,覆盖了代币生命周期的各个阶段。理解这些指令不仅要知道"怎么用",更要理解"为什么这样设计"。这种深层理解将帮助你在面对复杂业务需求时做出正确的架构决策。
在 Anchor 中,调用这些指令通常通过 CPI(Cross-Program Invocation,跨程序调用)实现。CPI 是 Solana 程序组合性的核心机制,它允许一个程序调用另一个程序的指令,就像函数调用一样。
Mint To(铸造代币)
铸造是创造新代币的唯一方式,也是代币经济模型的起点。只有持有 mint_authority 的账户才能执行此操作。这个权限的管理直接关系到代币的价值和用户信任。
在真实项目中,mint 权限的管理策略有几种常见模式:
- 项目方持有:最简单的模式,项目方可以根据需要铸造新代币。适合有持续增发需求的项目,如游戏内货币
- 多签钱包控制:需要多个签名才能铸造,增加了安全性。适合 DAO 治理的代币
- 智能合约控制:将权限转移给一个程序 PDA,由合约逻辑决定何时可以铸造。适合算法稳定币等场景
- 放弃权限:将权限设为 None,永久锁定供应量。适合追求去中心化的社区代币
rustuse anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount}; #[derive(Accounts)] pub struct MintTokens<'info> { #[account(mut)] pub mint: Account<'info, Mint>, #[account(mut)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> { mint_to( CpiContext::new( ctx.accounts.token_program.to_account_info(), MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; msg!("铸造了 {} 个代币(最小单位)", amount); Ok(()) }
关于数量的计算:如果你的代币有 6 位小数,想铸造 100 个代币给用户,实际传入的 amount 应该是 100 * 10^6 = 100_000_000。
Transfer(转账)
转账是最常用的代币操作,也是用户感知最直接的功能。发送方需要签名授权,目标账户必须已经存在且关联同一个 Mint。这个"目标账户必须存在"的要求是新手常遇到的坑——如果接收方还没有对应代币的 Token 账户,转账会失败。
在实际应用中,有几种处理方式:
- 要求用户先创建 Token 账户(用户体验差)
- 在转账前检查并创建账户(推荐做法,使用
init_if_needed) - 使用专门的"转账并创建账户"组合指令
rustuse anchor_spl::token::{transfer, Token, TokenAccount, Transfer}; #[derive(Accounts)] pub struct TransferTokens<'info> { #[account(mut)] pub from: Account<'info, TokenAccount>, #[account(mut)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; Ok(()) }
如果转账的权限来自于 PDA(程序派生地址),你需要使用 CpiContext::new_with_signer:
rustpub fn transfer_from_pda(ctx: Context<TransferFromPda>, amount: u64) -> Result<()> { let seeds = &[ b"vault", ctx.accounts.mint.key().as_ref(), &[ctx.bumps.vault_authority], ]; let signer_seeds = &[&seeds[..]]; transfer( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.user_token_account.to_account_info(), authority: ctx.accounts.vault_authority.to_account_info(), }, signer_seeds, ), amount, )?; Ok(()) }
Burn(销毁代币)
销毁会永久减少代币的流通供应量。这在实现通缩代币经济模型时很有用,也是某些业务场景的必要功能。
通缩代币经济学
许多成功的代币项目都采用了通缩模型。例如:
- 每笔交易销毁一定比例的手续费
- 回购并销毁(类似于股票回购)
- 用户主动销毁以换取某种权益
销毁机制的存在让代币持有者知道,流通量只会减少不会增加(如果同时放弃了铸造权限),这有助于建立价值预期。
谁可以执行销毁?
与铸造不同,销毁不需要特殊的权限。任何 Token 账户的所有者都可以销毁自己持有的代币。这是因为销毁是一种"自愿放弃财产"的行为,不需要第三方授权。
rustuse anchor_spl::token::{burn, Burn, Mint, Token, TokenAccount}; #[derive(Accounts)] pub struct BurnTokens<'info> { #[account(mut)] pub mint: Account<'info, Mint>, #[account(mut)] pub from: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn burn_tokens(ctx: Context<BurnTokens>, amount: u64) -> Result<()> { burn( CpiContext::new( ctx.accounts.token_program.to_account_info(), Burn { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.from.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; msg!("销毁了 {} 个代币,当前供应量: {}", amount, ctx.accounts.mint.supply); Ok(()) }
Approve 与 Revoke(授权与撤销)
approve 允许你授权一个代理人(delegate)代表你转移一定数量的代币。这在实现限价单、自动化策略等场景中非常有用。理解授权机制对于构建 DeFi 应用至关重要。
为什么需要授权机制?
想象一下这个场景:你想在一个 DEX 上挂一个限价单,当价格达到某个点位时自动执行交易。但区块链上的程序不能主动执行操作,它们只能响应用户发起的交易。
授权机制解决了这个问题。你可以提前授权 DEX 合约代表你转移一定数量的代币。当条件满足时,任何人(通常是套利者或做市商)都可以触发这笔交易,DEX 合约将使用你的授权完成代币转移。
授权的安全考量
授权是一种信任行为,你实际上是在说:"我信任这个地址,允许它动用我的资产。"因此:
- 只授权给你信任的合约或地址
- 设置合理的授权额度,不要授权超过必要的数量
- 定期检查和清理不再需要的授权
- 在完成操作后主动撤销授权
rustuse anchor_spl::token::{approve, Approve, Token, TokenAccount}; #[derive(Accounts)] pub struct ApproveDelegate<'info> { #[account(mut)] pub token_account: Account<'info, TokenAccount>, /// CHECK: 被授权的代理人 pub delegate: AccountInfo<'info>, pub owner: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn approve_delegate(ctx: Context<ApproveDelegate>, amount: u64) -> Result<()> { approve( CpiContext::new( ctx.accounts.token_program.to_account_info(), Approve { to: ctx.accounts.token_account.to_account_info(), delegate: ctx.accounts.delegate.to_account_info(), authority: ctx.accounts.owner.to_account_info(), }, ), amount, )?; Ok(()) }
撤销授权同样重要,它会立即取消代理人的所有权限:
rustuse anchor_spl::token::{revoke, Revoke, Token, TokenAccount}; #[derive(Accounts)] pub struct RevokeDelegate<'info> { #[account(mut)] pub token_account: Account<'info, TokenAccount>, pub owner: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn revoke_delegate(ctx: Context<RevokeDelegate>) -> Result<()> { revoke( CpiContext::new( ctx.accounts.token_program.to_account_info(), Revoke { source: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.owner.to_account_info(), }, ), )?; Ok(()) }
Freeze 与 Thaw(冻结与解冻)
冻结功能允许 freeze_authority 禁止特定账户进行任何代币操作。这在合规场景(如冻结被盗资金)或游戏场景(如锁定游戏内资产)中很有用。
冻结功能的争议
冻结功能是区块链世界中最具争议的特性之一。一方面,它为合规和安全提供了必要的工具;另一方面,它违背了"代码即法律"的去中心化精神。
支持者认为:
- 可以冻结被盗或用于非法活动的资金
- 满足监管要求,使代币可以在受监管的交易所上市
- 保护用户免受某些类型的攻击
反对者认为:
- 创造了中心化的控制点
- 可能被滥用来审查合法用户
- 与区块链的去中心化理念相悖
实践中的权衡
大多数项目会根据自身定位做出选择:
- 追求完全去中心化的项目会放弃冻结权限
- 稳定币等需要合规的项目通常保留冻结权限
- 游戏代币可能使用冻结来实现特定的游戏机制
被冻结的账户无法进行任何代币操作:不能转账、不能被授权、不能销毁。只有持有冻结权限的账户才能解冻。
rustuse anchor_spl::token::{freeze_account, FreezeAccount, Mint, Token, TokenAccount}; #[derive(Accounts)] pub struct FreezeTokenAccount<'info> { #[account(mut)] pub token_account: Account<'info, TokenAccount>, pub mint: Account<'info, Mint>, pub freeze_authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn freeze_token_account(ctx: Context<FreezeTokenAccount>) -> Result<()> { freeze_account( CpiContext::new( ctx.accounts.token_program.to_account_info(), FreezeAccount { account: ctx.accounts.token_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), authority: ctx.accounts.freeze_authority.to_account_info(), }, ), )?; Ok(()) }
解冻操作的代码结构类似:
rustuse anchor_spl::token::{thaw_account, ThawAccount, Mint, Token, TokenAccount}; pub fn thaw_token_account(ctx: Context<ThawTokenAccount>) -> Result<()> { thaw_account( CpiContext::new( ctx.accounts.token_program.to_account_info(), ThawAccount { account: ctx.accounts.token_account.to_account_info(), mint: ctx.accounts.mint.to_account_info(), authority: ctx.accounts.freeze_authority.to_account_info(), }, ), )?; Ok(()) }
Set Authority(设置权限)
权限转移是代币管理的重要操作。你可以转移或放弃 mint_authority、freeze_authority 等权限。这个指令的正确使用对代币的去中心化程度和安全性有重大影响。
权限类型详解
Token Program 定义了几种不同的权限类型,每种权限控制不同的操作:
- MintTokens:铸造新代币的权限。这可能是最重要的权限,因为它直接影响代币的供应量。放弃这个权限意味着代币将永远不会增发。
- FreezeAccount:冻结 Token 账户的权限。前面已经详细讨论过这个权限的意义和争议。
- AccountOwner:Token 账户的所有权。转移这个权限意味着将账户(及其中的代币)的控制权完全交给另一个地址。这在实现某些高级功能时会用到。
- CloseAccount:关闭 Token 账户的权限。通常账户所有者可以关闭自己的账户,但在某些场景下可能需要将这个权限委托给其他地址。
不可逆的决定
将任何权限设置为 None 是一个不可逆的操作。一旦执行,即使是原来的权限持有者也无法恢复这个权限。因此,在执行这类操作之前,请确保:
- 你完全理解这个操作的后果
- 这是项目路线图中计划好的步骤
- 已经与团队和社区充分沟通
rustuse anchor_spl::token::{set_authority, SetAuthority, Token}; use spl_token::instruction::AuthorityType; #[derive(Accounts)] pub struct ChangeAuthority<'info> { #[account(mut)] pub account_or_mint: AccountInfo<'info>, pub current_authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn change_mint_authority( ctx: Context<ChangeAuthority>, new_authority: Option<Pubkey>, ) -> Result<()> { set_authority( CpiContext::new( ctx.accounts.token_program.to_account_info(), SetAuthority { account_or_mint: ctx.accounts.account_or_mint.to_account_info(), current_authority: ctx.accounts.current_authority.to_account_info(), }, ), AuthorityType::MintTokens, new_authority, )?; Ok(()) }
AuthorityType 枚举定义了可以修改的权限类型:
MintTokens:铸币权限FreezeAccount:冻结权限AccountOwner:Token 账户所有权CloseAccount:关闭账户权限
将 new_authority 设置为 None 会永久放弃该权限,这是不可逆的操作。
Close Account(关闭账户)
关闭账户可以回收租金。Token 账户余额必须为 0 才能关闭。这是 Solana 账户模型中一个重要但经常被忽视的操作。
为什么要关闭账户?
在 Solana 上,每个账户都需要支付"租金"来维持其存在。虽然对于单个账户来说租金很小(约 0.002 SOL),但对于需要创建大量临时账户的应用来说,这些租金会累积成可观的金额。
关闭账户可以:
- 回收租金,返还给指定账户
- 减少链上状态膨胀
- 清理不再需要的账户
关闭账户的前提条件
- 余额为零:Token 账户必须先将所有代币转出或销毁
- 账户未被冻结:被冻结的账户无法关闭
- 有正确的权限:通常是账户所有者或被授权的关闭权限持有者
最佳实践
在设计程序时,考虑账户的生命周期是很重要的:
- 临时账户(如交易中介账户)应该在使用后立即关闭
- 为用户提供关闭不需要的账户的功能
- 在合约中实现关闭账户的指令,回收租金给用户
rustuse anchor_spl::token::{close_account, CloseAccount, Token, TokenAccount}; #[derive(Accounts)] pub struct CloseTokenAccount<'info> { #[account(mut)] pub token_account: Account<'info, TokenAccount>, /// CHECK: 接收租金的账户 #[account(mut)] pub destination: AccountInfo<'info>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, } pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> { close_account( CpiContext::new( ctx.accounts.token_program.to_account_info(), CloseAccount { account: ctx.accounts.token_account.to_account_info(), destination: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; Ok(()) }