实战场景:空投与锁仓

实战场景

学会了基础指令之后,让我们看看如何将它们组合起来解决真实世界的问题。本章将介绍两个在实际项目中非常常见的场景:代币空投和代币锁仓(Vesting)。

这些场景的共同特点是:单个指令无法完成,需要多个操作协同工作;涉及到时间、数量等业务逻辑的处理;需要考虑 gas 优化和用户体验。

代币空投实现

空投是代币分发的常见方式,通常用于奖励早期用户、建立社区、或作为营销手段。一个好的空投系统需要考虑以下因素:

  1. 批量处理:可能需要向成千上万个地址发送代币
  2. 账户创建:接收者可能还没有对应的 Token 账户
  3. Gas 优化:如何在有限的交易大小内处理尽可能多的接收者
  4. 防重复领取:确保每个地址只能领取一次

下面实现一个支持批量空投的功能:

rust
use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{mint_to, Mint, MintTo, Token, TokenAccount}, }; #[derive(Accounts)] pub struct Airdrop<'info> { #[account(mut)] pub authority: Signer<'info>, #[account( mut, mint::authority = authority, )] pub mint: Account<'info, Mint>, /// CHECK: 空投接收者 pub recipient: AccountInfo<'info>, #[account( init_if_needed, payer = authority, associated_token::mint = mint, associated_token::authority = recipient, )] pub recipient_ata: Account<'info, TokenAccount>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, } pub fn airdrop(ctx: Context<Airdrop>, 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.recipient_ata.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; msg!( "空投 {} 代币到 {}", amount, ctx.accounts.recipient.key() ); Ok(()) }

客户端批量调用:

typescript
async function batchAirdrop( program: Program<SplTokenManager>, mint: PublicKey, recipients: PublicKey[], amountPerRecipient: number ) { const authority = program.provider.publicKey; // 构建所有空投指令 const instructions = await Promise.all( recipients.map(async (recipient) => { const recipientAta = getAssociatedTokenAddressSync(mint, recipient); return program.methods .airdrop(new anchor.BN(amountPerRecipient)) .accounts({ authority, mint, recipient, recipientAta, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, }) .instruction(); }) ); // 将指令分批发送(每批最多 5 个,避免交易过大) const batchSize = 5; for (let i = 0; i < instructions.length; i += batchSize) { const batch = instructions.slice(i, i + batchSize); const tx = new Transaction().add(...batch); const sig = await program.provider.sendAndConfirm(tx); console.log(`批次 ${Math.floor(i / batchSize) + 1} 完成: ${sig}`); } }

代币锁仓(Vesting)

代币锁仓是项目方常用的代币分发机制,通常用于团队代币、投资人代币的分期释放。锁仓机制的目的是:

  1. 对齐利益:确保团队成员长期参与项目,而不是拿到代币后立即离开
  2. 减少抛压:避免大量代币同时进入市场造成价格崩盘
  3. 建立信任:向社区证明项目方对长期发展的承诺

一个标准的 Vesting 计划通常包含以下要素:

  • 总锁仓量:将要释放的代币总数
  • 锁定期(Cliff):在此期间代币完全不可领取
  • 释放期(Vesting Duration):锁定期结束后,代币逐渐释放的时间跨度
  • 释放曲线:线性释放是最常见的,也有项目采用阶梯式或指数曲线

例如,一个典型的团队 Vesting 计划可能是:1 年锁定期 + 3 年线性释放。这意味着第一年团队成员无法获得任何代币,从第二年开始,每月可以领取总量的 1/36。

rust
use anchor_lang::prelude::*; use anchor_spl::token::{transfer, Token, TokenAccount, Transfer}; #[account] pub struct VestingSchedule { pub beneficiary: Pubkey, pub mint: Pubkey, pub total_amount: u64, pub released_amount: u64, pub start_time: i64, pub cliff_duration: i64, // 锁定期 pub vesting_duration: i64, // 总释放期 pub bump: u8, } impl VestingSchedule { pub const LEN: usize = 8 + 32 + 32 + 8 + 8 + 8 + 8 + 8 + 1; pub fn releasable_amount(&self, current_time: i64) -> u64 { if current_time < self.start_time + self.cliff_duration { return 0; } let elapsed = current_time - self.start_time; if elapsed >= self.vesting_duration { // 全部可释放 self.total_amount - self.released_amount } else { // 按比例计算 let vested = (self.total_amount as u128) .checked_mul(elapsed as u128) .unwrap() .checked_div(self.vesting_duration as u128) .unwrap() as u64; vested.saturating_sub(self.released_amount) } } } #[derive(Accounts)] pub struct CreateVesting<'info> { #[account(mut)] pub creator: Signer<'info>, /// CHECK: 受益人 pub beneficiary: AccountInfo<'info>, pub mint: Account<'info, Mint>, #[account( init, payer = creator, space = VestingSchedule::LEN, seeds = [b"vesting", beneficiary.key().as_ref(), mint.key().as_ref()], bump, )] pub vesting_schedule: Account<'info, VestingSchedule>, #[account( init, payer = creator, token::mint = mint, token::authority = vesting_schedule, seeds = [b"vault", vesting_schedule.key().as_ref()], bump, )] pub vault: Account<'info, TokenAccount>, #[account( mut, token::mint = mint, token::authority = creator, )] pub creator_token_account: Account<'info, TokenAccount>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } pub fn create_vesting( ctx: Context<CreateVesting>, total_amount: u64, cliff_duration: i64, vesting_duration: i64, ) -> Result<()> { let clock = Clock::get()?; let vesting = &mut ctx.accounts.vesting_schedule; vesting.beneficiary = ctx.accounts.beneficiary.key(); vesting.mint = ctx.accounts.mint.key(); vesting.total_amount = total_amount; vesting.released_amount = 0; vesting.start_time = clock.unix_timestamp; vesting.cliff_duration = cliff_duration; vesting.vesting_duration = vesting_duration; vesting.bump = ctx.bumps.vesting_schedule; // 将代币转入锁仓账户 transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.creator_token_account.to_account_info(), to: ctx.accounts.vault.to_account_info(), authority: ctx.accounts.creator.to_account_info(), }, ), total_amount, )?; Ok(()) } #[derive(Accounts)] pub struct ReleaseVesting<'info> { pub beneficiary: Signer<'info>, #[account( mut, seeds = [b"vesting", beneficiary.key().as_ref(), vesting_schedule.mint.as_ref()], bump = vesting_schedule.bump, has_one = beneficiary, )] pub vesting_schedule: Account<'info, VestingSchedule>, #[account( mut, seeds = [b"vault", vesting_schedule.key().as_ref()], bump, )] pub vault: Account<'info, TokenAccount>, #[account(mut)] pub beneficiary_token_account: Account<'info, TokenAccount>, pub token_program: Program<'info, Token>, } pub fn release_vesting(ctx: Context<ReleaseVesting>) -> Result<()> { let clock = Clock::get()?; let vesting = &mut ctx.accounts.vesting_schedule; let releasable = vesting.releasable_amount(clock.unix_timestamp); require!(releasable > 0, VestingError::NothingToRelease); // 使用 PDA 签名释放代币 let vesting_key = ctx.accounts.vesting_schedule.key(); let seeds = &[ b"vault", vesting_key.as_ref(), &[ctx.bumps.vault], ]; 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.beneficiary_token_account.to_account_info(), authority: ctx.accounts.vault.to_account_info(), }, signer_seeds, ), releasable, )?; vesting.released_amount += releasable; msg!("释放 {} 代币", releasable); Ok(()) }
RUSTPlayground
EDITOR ACTIVE
Initializing RUST Environment...

批量空投模拟

Task: Send 100 Tokens to 15 Users.
Strategy: Batch 5 instructions per transaction.
User1
User2
User3
User4
User5
User6
User7
User8
User9
User10
User11
User12
User13
User14
User15
Ready to broadcast transactions...