anchor 智能合约案例5 之 vesting
前言:
前面我们介绍了 solana 关于 anchor 智能合约的环境搭建 配置,简单编写 发布 部署 调用 的一些落地。等等。接下来我们借着案例。详细剖析下智能合约编写。
案例 vesting 介绍:
这是一个关于 释放 spl token 的案例。老板将 token 作为激励 发放给员工。包含 锁定期, 受益人 开始兑换时间,结束兑换时间, 解锁账户 等等,
主要功能步骤:
- 雇主先创建一个 以公司名称作为派生种子的账户,用于存放 待释放 SPL Token,
- 雇主再为 每个员工创建一个 pad 账户,记录员工待解锁 账户的信息,(token总量,锁定期限,开始兑换期限,员工接收token地址 等等, )
- 员工领取 token
智能合约的分析:
智能合约 编写包含很多固定写法。这里就不赘述了。可以先看前面的章节内容。我们这里主要介绍下 学到的新姿势:
- 雇主创建 账户
/** 创建 老板解锁 账户:主要就是 给 vesting_account 赋值 **/ pub fn create_vesting_account(ctx: Context<CreateVestingAccount>,company_name:String ) -> Result<()> {*ctx.accounts.vesting_account = VestingAccount {owner: ctx.accounts.signer.key(),mint: ctx.accounts.mint.key(),treasury_token_account: ctx.accounts.treasury_token_account.key(),company_name,treasury_bump: ctx.bumps.treasury_token_account,bump: ctx.bumps.treasury_token_account,};Ok(()) }#[derive(Accounts)] #[instruction(company_name:String)] pub struct CreateVestingAccount<'info>{#[account(mut)]pub signer: Signer<'info>,#[account(init,space = 8 + VestingAccount::INIT_SPACE,payer = signer,seeds = [company_name.as_ref()],bump,)]pub vesting_account: Account<'info, VestingAccount>,pub mint:InterfaceAccount<'info, Mint>,/**1. 需要创建2. 指定 spl mint3. 授权账户就是自己4. 支付的费用5. 生成 pda 账户种子6. pda bump**/#[account(init,token::mint = mint,token::authority = treasury_token_account,payer = signer,seeds = [b"vesting_treasury".as_ref(),company_name.as_ref()],bump,)]pub treasury_token_account:InterfaceAccount<'info,TokenAccount>,pub system_program: Program<'info, System>,pub token_program: Interface<'info, TokenInterface>, }/** 解锁账户 雇主操作的账户 用于公司员工管理,存储需要解锁的 spl token1.owner 归属人 觉得谁可以更新2.spl 发放 spl mint3.用于保存 spl 账户信息,雇主 用于分配 所有要发放的 SPL4.公司名称 标识, 用于派生 PDA5.保存 分配 SPL 账户的 bump,6.保存 归属账号的 bump宏 :#[account] 用于构造数据 指定改结构体为一个 solana 账户#[derive(InitSpace)] 是对数据结构的实现, 指定我们想要使用 InitSpace,会让 anchor 自动计算 结构体大小#[max_len(50)] 字符串类型没有固定大小,这里指定为 固定大小 才能使用 InitSpace 推导出结构体大小 **/ #[account] #[derive(InitSpace)] pub struct VestingAccount {pub owner: Pubkey,pub mint: Pubkey,pub treasury_token_account: Pubkey,#[max_len(50)]pub company_name:String,pub treasury_bump:u8,pub bump:u8, }
雇主为每个员工 创建账号
/** 创建雇员 待解锁的账号: 结构体包含 解锁 token 的 所有信息 **/ pub fn create_employee_account(ctx: Context<CreateEmployeeAccount>,start_time:i64,end_time:i64,total_amount:u64,cliff_time:i64, ) -> Result<()> {*ctx.accounts.employee_account = EmployeeAccount {beneficiary: ctx.accounts.beneficiary.key(),start_time,end_time,total_amount,total_withdrawn:0,cliff_time,vesting_account: ctx.accounts.vesting_account.key(),bump:ctx.bumps.employee_account,};Ok(()) }/**1.支付费用的账户2.受益人账户,领取 token 的 账户地址3.操作该账户的权限4.员工账户5.指定系统程序 **/ #[derive(Accounts)] pub struct CreateEmployeeAccount<'info>{#[account(mut)]pub owner:Signer<'info>,pub beneficiary:SystemAccount<'info>,//has_one 解锁 账户的所有者 是刺指令的签名者 owner#[account(has_one = owner,)]pub vesting_account:Account<'info, VestingAccount>,//创建员工账户#[account(init,space = 8 + EmployeeAccount::INIT_SPACE,payer = owner,seeds = [b"employee_vesting", beneficiary.key().as_ref(), vesting_account.key().as_ref()],bump,)]pub employee_account: Account<'info, EmployeeAccount>,pub system_program: Program<'info, System>, }/**1.受益人 接收token 的雇员 钱包 (ATA账号)2.开始兑换时间 unix 时间3.截止的兑换时间4.锁定期 员工需要等待多久才能解锁(类似期权,几年不能交易)5.解锁账户 雇主创建的放 spl token 的账户6.分配给该员工的 总 spl 数量7.记录该员工 解锁的数量,用于后期计算 **/ #[account] #[derive(InitSpace)] pub struct EmployeeAccount {pub beneficiary: Pubkey,pub start_time:i64,pub end_time:i64,pub cliff_time:i64,pub vesting_account: Pubkey,pub total_amount:u64,pub total_withdrawn:u64,pub bump:u8, }
员工释放 token
/** 员工认领 Token,保证员工在正确的时间里 领取 token**/ pub fn claim_tokens(ctx:Context<ClaimTokens>,company_name:String,) -> Result<()>{//获取员工账户(有老板已经创建好的)let employee_account =&mut ctx.accounts.employee_account;//获取系统时间let now = Clock::get()?.unix_timestamp;//当前时间小于 锁定期则 不能释放if now < employee_account.cliff_time {return Err(ErrorCode::ClaimNotAvailableYet.into())}//已经开放的时间, saturating_sub 防止数组越界let time_since_start = now.saturating_sub(employee_account.start_time);//总释放时间let total_vesting_time = employee_account.end_time.saturating_sub(employee_account.start_time);if total_vesting_time == 0 {return Err(ErrorCode::InvalidVestingPeriod.into())}//解锁数量//如果当前时间 大于 释放结束时间。那可以全部都 解锁掉let vested_amount = if now >= employee_account.end_time {employee_account.total_amount}else {//checked_mul 防止 乘法的溢出//数量*总的开放时间 / 当前已经开放的时间 = 当前开放时间可以获取的数量match employee_account.total_amount.checked_mul(time_since_start as u64) {Some(product) => product / (total_vesting_time as u64),None =>{return Err(ErrorCode::CalculationOverflow.into())}}};//可申领金额let claimable_amount = vested_amount.saturating_sub(employee_account.total_withdrawn);if claimable_amount == 0 {return Err(ErrorCode::NothingToClaim.into())}//从老板设立的账号中 转账 到员工let transfer_cpi_accounts = TransferChecked {from: ctx.accounts.treasury_token_account.to_account_info(),mint: ctx.accounts.mint.to_account_info(),//to: ctx.accounts.employee_account.to_account_info(),to: employee_account.to_account_info(),//已经将权限设置为自己了。所有这里是可以调用的authority: ctx.accounts.treasury_token_account.to_account_info(),};let cip_program = ctx.accounts.token_program.to_account_info();let signer_seeds:&[&[&[u8]]] = &[&[b"vesting_treasury",ctx.accounts.vesting_account.company_name.as_ref(),&[ctx.accounts.vesting_account.treasury_bump],]];let cpi_context = CpiContext::new(cip_program, transfer_cpi_accounts).with_signer(signer_seeds);let decimals = ctx.accounts.mint.decimals;token_interface::transfer_checked(cpi_context, claimable_amount, decimals)?;employee_account.total_withdrawn += claimable_amount;Ok(()) }}
依赖 Cargo.toml:
[package]
name = "tokenvesting"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"[lib]
crate-type = ["cdylib", "lib"]
name = "tokenvesting"[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build","anchor-spl/idl-build"][dependencies]
#cargo add anchor-lang --features init-if-needed
anchor-lang = { version = "0.31.1", features = ["init-if-needed"] }
anchor-spl = "0.31.1"
solana-program = "2.3.0"
完整脚本:
#![allow(clippy::result_large_err)]use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked};declare_id!("JAVuBXeBZqXNtS73azhBDAoYaaAFfo4gWXoZe2e7Jf8H");/**
(智能合约 和 http 请求类似都是 无状态的)
智能合约主要功能:1.雇主先创建一个 以公司名称作为派生种子的账户,用于存放 待释放 SPL Token,2.雇主再为 每个员工创建一个 pad 庄户,记录员工带解锁庄户的信息,(token总量,锁定期限,开始兑换期限,员工接收token地址 等等, )3.员工领取 token**/
#[program]
pub mod vesting {use super::*;use anchor_spl::token_interface;/**创建 老板解锁 账户:主要就是 给 vesting_account 赋值**/pub fn create_vesting_account(ctx: Context<CreateVestingAccount>,company_name: String,) -> Result<()> {*ctx.accounts.vesting_account = VestingAccount {owner: ctx.accounts.signer.key(),mint: ctx.accounts.mint.key(),treasury_token_account: ctx.accounts.treasury_token_account.key(),company_name,treasury_bump: ctx.bumps.treasury_token_account,bump: ctx.bumps.treasury_token_account,};Ok(())}/**创建雇员 待解锁的账号:结构体包含 解锁 token 的 所有信息**/pub fn create_employee_account(ctx: Context<CreateEmployeeAccount>,start_time: i64,end_time: i64,total_amount: u64,cliff_time: i64,) -> Result<()> {*ctx.accounts.employee_account = EmployeeAccount {beneficiary: ctx.accounts.beneficiary.key(),start_time,end_time,total_amount,total_withdrawn: 0,cliff_time,vesting_account: ctx.accounts.vesting_account.key(),bump: ctx.bumps.employee_account,};Ok(())}/**员工认领 Token,保证员工在正确的时间里 领取 token**/pub fn claim_tokens(ctx: Context<ClaimTokens>, company_name: String) -> Result<()> {//获取员工账户(有老板已经创建好的)let employee_account = &mut ctx.accounts.employee_account;//获取系统时间let now = Clock::get()?.unix_timestamp;//当前时间小于 锁定期则 不能释放if now < employee_account.cliff_time {return Err(ErrorCode::ClaimNotAvailableYet.into());}//已经开放的时间, saturating_sub 防止数组越界let time_since_start = now.saturating_sub(employee_account.start_time);//总释放时间let total_vesting_time = employee_account.end_time.saturating_sub(employee_account.start_time);if total_vesting_time == 0 {return Err(ErrorCode::InvalidVestingPeriod.into());}//解锁数量//如果当前时间 大于 释放结束时间。那可以全部都 解锁掉let vested_amount = if now >= employee_account.end_time {employee_account.total_amount} else {//checked_mul 防止 乘法的溢出//数量*总的开放时间 / 当前已经开放的时间 = 当前开放时间可以获取的数量match employee_account.total_amount.checked_mul(time_since_start as u64){Some(product) => product / (total_vesting_time as u64),None => return Err(ErrorCode::CalculationOverflow.into()),}};//可申领金额let claimable_amount = vested_amount.saturating_sub(employee_account.total_withdrawn);if claimable_amount == 0 {return Err(ErrorCode::NothingToClaim.into());}//从老板设立的账号中 转账 到员工let transfer_cpi_accounts = TransferChecked {from: ctx.accounts.treasury_token_account.to_account_info(),mint: ctx.accounts.mint.to_account_info(),//to: ctx.accounts.employee_account.to_account_info(),to: employee_account.to_account_info(),//已经将权限设置为自己了。所有这里是可以调用的authority: ctx.accounts.treasury_token_account.to_account_info(),};let cip_program = ctx.accounts.token_program.to_account_info();let signer_seeds: &[&[&[u8]]] = &[&[b"vesting_treasury",ctx.accounts.vesting_account.company_name.as_ref(),&[ctx.accounts.vesting_account.treasury_bump],]];let cpi_context =CpiContext::new(cip_program, transfer_cpi_accounts).with_signer(signer_seeds);let decimals = ctx.accounts.mint.decimals;token_interface::transfer_checked(cpi_context, claimable_amount, decimals)?;employee_account.total_withdrawn += claimable_amount;Ok(())}
}/**
创建雇主账号1.signer mut 签名者,支付手续费,应为需要支付手续费 所以需要是 mut2.指定 解锁账号 派生信息3.spl mint 地址4.存储 激励 spl 的 ATA 地址,(即将分配给 员工的 Token)5.指定使用到的系统程序 System6.指定使用到的系统程序 TokenInterface宏:#[instruction(company_name:String)] 是对数据结构的实现 (创建), 指定入参 company_name**/#[derive(Accounts)]
#[instruction(company_name:String)]
pub struct CreateVestingAccount<'info> {#[account(mut)]pub signer: Signer<'info>,#[account(init,space = 8 + VestingAccount::INIT_SPACE,payer = signer,seeds = [company_name.as_ref()],bump,)]pub vesting_account: Account<'info, VestingAccount>,pub mint: InterfaceAccount<'info, Mint>,/**1. 需要创建2. 指定 spl mint3. 授权账户就是自己4. 支付的费用5. 生成 pda 账户种子6. pda bump**/#[account(init,token::mint = mint,token::authority = treasury_token_account,payer = signer,seeds = [b"vesting_treasury".as_ref(),company_name.as_ref()],bump,)]pub treasury_token_account: InterfaceAccount<'info, TokenAccount>,pub system_program: Program<'info, System>,pub token_program: Interface<'info, TokenInterface>,
}/**
解锁账户 雇主操作的账户 用于公司员工管理,存储需要解锁的 spl token1.owner 归属人 觉得谁可以更新2.spl 发放 spl mint3.用于保存 spl 账户信息,雇主 用于分配 所有要发放的 SPL4.公司名称 标识, 用于派生 PDA5.保存 分配 SPL 账户的 bump,6.保存 归属账号的 bump宏 :#[account] 用于构造数据 指定改结构体为一个 solana 账户#[derive(InitSpace)] 是对数据结构的实现, 指定我们想要使用 InitSpace,会让 anchor 自动计算 结构体大小#[max_len(50)] 字符串类型没有固定大小,这里指定为 固定大小 才能使用 InitSpace 推导出结构体大小
**/
#[account]
#[derive(InitSpace)]
pub struct VestingAccount {pub owner: Pubkey,pub mint: Pubkey,pub treasury_token_account: Pubkey,#[max_len(50)]pub company_name: String,pub treasury_bump: u8,pub bump: u8,
}/**1.支付费用的账户2.受益人账户,领取 token 的 账户地址3.操作该账户的权限4.员工账户5.指定系统程序
**/
#[derive(Accounts)]
pub struct CreateEmployeeAccount<'info> {#[account(mut)]pub owner: Signer<'info>,pub beneficiary: SystemAccount<'info>,//has_one 解锁 账户的所有者 是刺指令的签名者 owner#[account(has_one = owner,)]pub vesting_account: Account<'info, VestingAccount>,//创建员工账户#[account(init,space = 8 + EmployeeAccount::INIT_SPACE,payer = owner,seeds = [b"employee_vesting", beneficiary.key().as_ref(), vesting_account.key().as_ref()],bump,)]pub employee_account: Account<'info, EmployeeAccount>,pub system_program: Program<'info, System>,
}/**1.受益人 接收token 的雇员 钱包 (ATA账号)2.开始兑换时间 unix 时间3.截止的兑换时间4.锁定期 员工需要等待多久才能解锁(类似期权,几年不能交易)5.解锁账户 雇主创建的放 spl token 的账户6.分配给该员工的 总 spl 数量7.记录该员工 解锁的数量,用于后期计算
**/
#[account]
#[derive(InitSpace)]
pub struct EmployeeAccount {pub beneficiary: Pubkey,pub start_time: i64,pub end_time: i64,pub cliff_time: i64,pub vesting_account: Pubkey,pub total_amount: u64,pub total_withdrawn: u64,pub bump: u8,
}/**
员工认领 token1.交易的签名2.查找 待认领的 员工账户信息,雇主创建好的, mut 可编辑, has_one 约束验证,结构体中 EmployeeAccount 的两个字段与 当前约束值 是否一直3.查找 存放 解锁的 spl token 账户, mut 可编 has_one = treasury_token_account,has_one = mint 共同约束 vesting_account pda 账户4.spl mint 信息, 确定 vesting_account5.待 解锁的 老板庄户6.员工 待接收的 token account 账户
**/
#[derive(Accounts)]
#[instruction(company_name:String)]
pub struct ClaimTokens<'info> {#[account(mut)]pub beneficiary: Signer<'info>,#[account(mut,seeds = [b"employee_vesting",beneficiary.key().as_ref(),vesting_account.key().as_ref()],bump = employee_account.bump,has_one = beneficiary,has_one = vesting_account,)]pub employee_account: Account<'info, EmployeeAccount>,//宏 根据 传入的公司名称处理,是否存在 对应的pad#[account(mut,seeds = [company_name.as_ref()],bump = vesting_account.bump,has_one = treasury_token_account,has_one = mint,)]pub vesting_account: Account<'info, VestingAccount>,pub mint: InterfaceAccount<'info, Mint>,#[account(mut)]pub treasury_token_account: InterfaceAccount<'info, TokenAccount>,#[account(init_if_needed,payer = beneficiary,associated_token::mint = mint,associated_token::authority = beneficiary,associated_token::token_program = token_program,)]pub employee_token_account: InterfaceAccount<'info, TokenAccount>,pub token_program: Interface<'info, TokenInterface>,pub associated_token_program: Program<'info, AssociatedToken>,pub system_program: Program<'info, System>,
}#[error_code]
pub enum ErrorCode {#[msg("Claim not available yet")]ClaimNotAvailableYet,#[msg("Invalid Vesting Period")]InvalidVestingPeriod,#[msg("Calculation Overflow")]CalculationOverflow,#[msg("Nothing To Claim")]NothingToClaim,
}