概述
对于Masterchef不了解的,可以看我之前写的文章:SushiSwap的MasterChef解读_biakia0610的博客-CSDN博客
1、项目结构
首先我们需要使用cargo新建一个Rust的lib项目,项目结构参考Solana官方例子进行划分:
entrypoint.rs:Solana程序入口,是一些模板代码
error.rs:定义业务错误码
instruction.rs:定义操作指令,这里的操作指令你可以想想成solidity里的公开函数,是可以被外部用户调用的
lib.rs:定义模块,这样各个rs文件直接可以互相引用
processor.rs:这里会对每个操作指令,实现对应的业务逻辑处理
state.rs:内部使用的业务数据结构
2、Cargo.toml
Cargo.toml主要定义了项目信息和项目依赖,具体如下:
[package]
name = "masterchef"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
borsh = { version = "0.9.3", features = [ "const-generics" ] }
borsh-derive = "0.9.3"
solana-program = "= 1.10.1"
spl-token = "3.2.0"
thiserror = "1.0"
[lib]
name = "masterchef"
crate-type = [ "cdylib", "lib" ]
[dev-dependencies]
solana-program-test = "= 1.10.1"
solana-sdk = "= 1.10.1"
[package]下是项目基本信息,name是项目名称,version是当前项目版本号,edition是Rust编译器版本号,一般用cargo生成的就好。
[dependencies]下是你要依赖的各个三方库,borsh是一个序列化和反序列化的库,在Solana中数据传输的都是二进制,所以会有频繁的二进制到结构体的转换,通过borsh可以把这类繁琐的事省掉,borsh-derive是borsh相关的宏命令,下面会具体说明。solana-program是solana官方SDK,必须依赖,spl-token是solana官方开发的类似ERC20的程序,solana和EVM不一样,在solana中只有一个ERC20程序,不同的token通过Mint账户去区分。thiserror是用来简化错误码处理。
[lib]下则是打包相关信息,name表示程序编译后打包成lib的名字,crate-type是打包的格式,具体参考Rust官方文档。
[dev-dependencies]下是测试环境依赖的各个三方库,其中solana-program-test和solana-sdk是写单元测试必须用到的。
3、lib.rs
#![cfg_attr(not(test), forbid(unsafe_code))]
#![feature(const_fn_trait_bound)]
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint;
solana_program::declare_id!("FjbZzxK2TGP9q23mxSAE58nyXu6vW5ZCSCMASsr55CA6");
lib会定义各个模块:error、instruction、processor、state以及entrypoint,可以通过cfg_attr定义全局配置,并且通过solana_program::declare_id!宏来定义当前程序的ID
4、entrypoint.rs
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::processor::Processor;
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Processor::process_instruction(program_id,accounts,instruction_data)?;
Ok(())
}
entrypoint定义了入口,然后把业务处理直接转交给Processor,entrypoint的原理可以看我写的这篇文章:Solana项目学习(一):Hello World_biakia0610的博客-CSDN博客
5、error.rs
use solana_program::{program_error::ProgramError};
use thiserror::Error;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum MasterChefError {
#[error("You are not allowed.")]
NotAllowed,
#[error("Signature verification failed.")]
VerificationFailed,
#[error("Invalid account owner.")]
InvalidAccountOwner,
#[error("Not rent exempt.")]
NotRentExempt,
#[error("Inconsistant account.")]
InconsistantAccount,
#[error("Invalid stake amount.")]
InvalidStakeAmount,
#[error("Invalid stake balance pubkey.")]
InvalidStakeBalancePubkey,
#[error("Fail to get stake balance.")]
FailToGetStakeBalance,
#[error("Conflicted stake status owner.")]
ConflictedStakeStatusOwner,
#[error("Amount over total supply.")]
AmountOverTotalSupply,
#[error("Insufficient balance.")]
InsufficientBalance,
#[error("Invalid stake reward account.")]
InvalidStakeRewardAccount,
#[error("Paused.")]
Paused,
#[error("Period not finished.")]
PeriodNotFinished,
#[error("Too much reward.")]
TooMuchReward,
#[error("Invalid reward duration.")]
InvalidRewardDuration,
#[error("Illegal account.")]
IllegalAccount,
#[error("Pool must be paused.")]
NotPaused,
#[error("Not released yet.")]
NotReleasedYet,
#[error("Not started yet.")]
NotStarted,
#[error("invalid pool")]
InvalidPool,
#[error("invalid user account, please create user account first")]
InvalidUserAccount,
#[error("can't stake and reward the same token")]
MintAccountSame,
}
impl From<MasterChefError> for ProgramError {
fn from(e: MasterChefError) -> ProgramError {
ProgramError::Custom(e as u32)
}
}
这里定义了一个错误码枚举MasterChefError,里面定义各个错误码,并且使用#[error("")]定义错误信息,thiserror库会将错误信息和错误码关联,在发生实际错误的使用输出指定错误信息,如果不用这个库,你需要自己写代码进行错误码和错误信息的匹配。
最后一行为ProgramError实现了From这个trait,目的是将MasterChefError转换成ProgramError的Custom错误,否则Solana是无法识别MasterChefError的。
6、instruction.rs
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)]
pub enum MasterChefInstruction {
/// 0
///
/// Initializes a staking reward pool
///
/// 0.`[writable,signer]` The owner
/// 1.`[writable]` The pool account
/// 2.`[]` The pool authority account
/// 3.`[]` The stake mint account
/// 4.`[]` The stake vault account
/// 5.`[]` The reward mint account
/// 6.`[]` The reward vault account
InitializeStakePool {
pool_authority_nonce: u8,
locked_ts:u64,
},
/// 1
/// create user account
/// 0.`[writable,signer]` The owner
/// 1.`[writable]` The pool account
/// 2.`[writable]` The user account pda
/// 3.`[]` The system program account
CreateUser{
user_nonce:u8,
},
/// 2
/// add rewards to reward vault
/// 0.`[signer]` The owner
/// 1.`[writable]` The pool account
/// 2.`[writable]` The owner's reward ata account
/// 3.`[writable]` The pool's reward vault account
/// 4.`[]` The spl token program account
NotifyRewardAmount {
amount: u64,
start_ts: u64,
},
/// 3
/// pause and unpause
/// 0.`[signer]` The signer
/// 1.`[writable]` The pool account
SetPaused {
paused: bool,
},
/// 4
/// stake to the pool
/// 0.`[signer]` The staker
/// 1.`[writable]` The staker's stake mint ata account
/// 2.`[writable]` The staker's balance account
/// 3.`[writable]` The pool account
/// 4.`[writable]` The pool stake vault
/// 5.`[]` The spl token program account
Stake {
amount: u64,
},
/// 6
/// withdraw from the pool
/// 0.`[signer]` The staker
/// 1.`[writable]` The staker's stake mint ata account
/// 2.`[writable]` The staker's balance account
/// 3.`[writable]` The pool account
/// 4.`[writable]` The pool stake vault
/// 5.`[]` The pool authority
/// 6.`[]` The spl token program account
Withdraw {
amount: u64,
},
/// 7
/// get reward
/// 0.`[signer]` The staker
/// 1.`[writable]` The staker's reward mint ata account
/// 2.`[writable]` The staker's balance account
/// 3.`[writable]` The pool account
/// 4.`[writable]` The pool reward vault account
/// 5.`[]` The pool authority
/// 6.`[]` The spl token program account
GetReward,
/// 8
/// withdraw all tokens and get all rewards
/// 0.`[signer]` The staker
/// 1.`[writable]` The staker's stake mint ata account
/// 2.`[writable]` The staker's reward mint ata account
/// 3.`[writable]` The staker's balance account
/// 4.`[writable]` The pool account
/// 5.`[writable]` The pool stake vault
/// 6.`[writable]` The pool reward vault
/// 7.`[writable]` The pool authority
/// 8.`[]` The spl token program account
Exit,
/// 9
/// change owner
/// 0.`[signer]` The signer
/// 1.`[writable]` The pool account
/// 2.`[]` The new owner account
ChangeOwner,
}
instruction.rs定义了指令MasterChefInstruction,它其实是一个枚举,它继承了BorshSerialize和BorshDeserialize,因此我们不需要关心二进制和枚举之间的转换。里面每个枚举成员都是一个指令,指令里包含了额外的参数。开发者需要为指令写注释,详细描述传入的accounts数组包含哪些成员,方便后续开发。
7、state.rs
use solana_program::{
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
pubkey::Pubkey,
};
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(Debug, Clone, PartialEq, Default, BorshSerialize, BorshDeserialize)]
pub struct StakePool {
pub initialized: bool,
pub owner: Pubkey,
pub nonce: u8,
pub stake_mint_pubkey: Pubkey,
pub reward_mint_pubkey: Pubkey,
pub paused: bool,
pub stake_mint_decimals: u8,
pub total_supply: u64,
pub reward_per_token_stored: u64,
pub stake_vault:Pubkey,
pub reward_vault:Pubkey,
// in slots
pub period_start: u64,
pub period_finish: u64,
pub reward_duration: u64,
pub last_update_slot: u64,
pub reward_rate: u64,
}
impl Sealed for StakePool {}
impl IsInitialized for StakePool {
fn is_initialized(&self) -> bool {
self.initialized
}
}
impl Pack for StakePool {
const LEN: usize = 220;
fn pack_into_slice(&self, dst: &mut [u8]) {
let x = self.try_to_vec().unwrap();
dst.copy_from_slice(&x);
}
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
Ok(Self::try_from_slice(src)?)
}
}
#[derive(Debug, Clone, PartialEq, Default, BorshSerialize, BorshDeserialize)]
pub struct StakeBalance {
pub owner: Pubkey,
pub balance: u64,
pub reward: u64,
pub reward_per_token_paid:u64,
pub nonce: u8,
}
impl Sealed for StakeBalance {}
impl IsInitialized for StakeBalance {
fn is_initialized(&self) -> bool {
self.owner != Pubkey::default()
}
}
impl Pack for StakeBalance {
const LEN: usize = 57;
fn pack_into_slice(&self, dst: &mut [u8]) {
let x = self.try_to_vec().unwrap();
dst.copy_from_slice(&x);
}
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
Ok(Self::try_from_slice(src)?)
}
}
这里定义两个内部的业务结构体,StakePool是流动性挖矿池,内部保存很多池子相关的信息,StakeBalance是用户的账户,内部保存用户相关数据。它们都继承了BorshSerialize和BorshDeserialize,方便序列化和反序列化。它们都实现了Sealed,可以获取二进制数据的总长度。它们都实现了IsInitialized,这个trait用来判断是否初始化。而实现Pack是为了序列化和反序列化数据,其中pack_into_slice是将自身数据转换成二进制,然后写入dst这个数组,这里其实使用的是BorshSerialize的能力,否则需要将结构体成员拆分成一个一个的二进制数据进行写入,而unpack_from_slice也是使用了BorshDeserialize的能力,将二进制轻易转换成了结构体。
8、processor.rs
processor.rs里是针对每个指令的业务处理,我们绝大部分的业务代码都在这里,下面根据每个指令一一介绍。
8.1、定义和依赖
use crate::{
error::MasterChefError,instruction::{MasterChefInstruction},
state::{StakePool,StakeBalance}
};
use borsh::BorshDeserialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
system_instruction,
sysvar::{clock::Clock, rent::Rent, Sysvar},
};
use spl_token;
const SEED_BALANCE_ATA: &'static [u8] = b"balance_ata";
pub struct Processor {}
这里引入的很多依赖,然后定义了一个Processor,下面需要实现不同的指令。实现执行需要使用impl Processor{},下面介绍的所有的执行处理函数都包含在这个大括号内:
impl Processor{
.....
.....
.....
}
8.2、process_instruction
pub fn process_instruction(
program_id:&Pubkey,
accounts:&[AccountInfo],
instruction_data:&[u8],
)-> ProgramResult{
if program_id!=&crate::ID{
return Err(ProgramError::IncorrectProgramId);
}
let instruction = MasterChefInstruction::try_from_slice(instruction_data).map_err(|e|{
msg!(&e.to_string());
ProgramError::InvalidInstructionData})?;
match instruction{
MasterChefInstruction::InitializeStakePool { pool_authority_nonce, locked_ts }=>Self::process_init_stake_pool(program_id,accounts,pool_authority_nonce,locked_ts),
MasterChefInstruction::CreateUser { user_nonce }=>Self::process_create_user(program_id,accounts,user_nonce),
MasterChefInstruction::NotifyRewardAmount { amount, start_ts }=>Self::process_notify_reward(program_id,accounts,amount,start_ts),
MasterChefInstruction::SetPaused { paused }=>Self::process_set_pause(program_id,accounts,paused),
MasterChefInstruction::Stake { amount }=>Self::process_stake(program_id,accounts,amount),
MasterChefInstruction::Withdraw { amount }=>Self::process_withdraw(program_id,accounts,amount),
MasterChefInstruction::GetReward =>Self::process_get_reward(program_id,accounts),
MasterChefInstruction::Exit =>Self::process_exit(program_id,accounts),
MasterChefInstruction::ChangeOwner =>Self::process_change_owner(program_id,accounts)
}
}
process_instruction类似于一个前端控制器,通过MasterChefInstruction::try_from_slice解析不同的指令,然后通过match进行匹配,分发到不同的具体函数去处理。
8.3、ProcessHelper
struct ProcessHelper{}
impl ProcessHelper{
#[inline(always)]
pub(crate) fn assert_signer(account: &AccountInfo) -> ProgramResult {
if !account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
Ok(())
}
#[inline(always)]
pub(crate) fn assert_account_owner(
account: &AccountInfo,
program_id: &Pubkey,
) -> ProgramResult {
if account.owner != program_id {
return Err(MasterChefError::InvalidAccountOwner.into());
}
Ok(())
}
pub(crate) fn assert_account(pubkey: &Pubkey, pubkey_expected: &Pubkey) -> ProgramResult {
if pubkey_expected != pubkey {
return Err(MasterChefError::InconsistantAccount.into());
}
Ok(())
}
pub fn address_staker_balance(
pool_id: &Pubkey,
staker: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
SEED_BALANCE_ATA,
staker.as_ref(),
pool_id.as_ref(),
],
program_id,
)
}
pub fn last_time_reward_applicable(stake_pool: &StakePool) -> u64 {
let current_slot = Clock::get().unwrap().slot;
if current_slot < stake_pool.period_start {
stake_pool.period_start
} else if current_slot < stake_pool.period_finish {
current_slot
} else {
stake_pool.period_finish
}
}
pub fn reward_per_token(stake_pool: &StakePool) -> u64 {
if stake_pool.total_supply == 0 {
return stake_pool.reward_per_token_stored;
} else {
stake_pool
.reward_per_token_stored
.checked_add(
Self::last_time_reward_applicable(stake_pool)
.saturating_sub(stake_pool.last_update_slot)
.checked_mul(stake_pool.reward_rate)
.unwrap()
.checked_mul(u64::pow(10, stake_pool.stake_mint_decimals as u32))
.unwrap()
.checked_div(stake_pool.total_supply)
.unwrap(),
)
.unwrap()
}
}
pub fn earned(
stake_pool: &StakePool,
staker_balance: u64,
staker_reward: u64,
staker_reward_per_token_paid: u64,
) -> u64 {
let reward_per_token = Self::reward_per_token(stake_pool);
staker_balance
.checked_mul(
reward_per_token
.checked_sub(staker_reward_per_token_paid)
.unwrap(),
)
.unwrap()
.checked_div(u64::pow(10, stake_pool.stake_mint_decimals as u32))
.unwrap()
.checked_add(staker_reward)
.unwrap()
}
pub(crate) fn update_staker_reward(
pool: &mut StakePool,
staker_balance: &mut StakeBalance,
) -> ProgramResult {
pool.reward_per_token_stored = Self::reward_per_token(pool);
pool.last_update_slot = Self::last_time_reward_applicable(pool);
let balance = staker_balance.balance;
let reward = staker_balance.reward;
let reward_per_token_paid = staker_balance.reward_per_token_paid;
let earned = Self::earned(
pool,
balance,
reward,
reward_per_token_paid,
);
staker_balance.reward = earned;
Ok(())
}
}
在介绍正式指令前,我们先介绍辅助函数ProcessHelper,这部分是单独的,不在impl Processor{}内部。其中assert_signer函数是用来检验账户是否是签名者,assert_account_owner函数用来校验账户是否属于某个program,assert_account函数用来比较两个账户是否相同,address_staker_balance函数用来生成用户PDA,last_time_reward_applicable用来获取上一次发奖励的时间,reward_per_token用来计算每份质押可以获取的奖励数量,这是个累计值,earned用来计算用户实际可以获取的奖励,是池子最新的reward_per_token减去用户的staker_reward_per_token_paid,然后乘上相应的质押份额,这部分逻辑是Sushi Masterchef中计算奖励的一个变种。update_staker_reward用来更新用户奖励,奖励也是一个累计值。
8.4、process_init_stake_pool
pub fn process_init_stake_pool(
program_id:&Pubkey,
accounts:&[AccountInfo],
nonce:u8,
locked_ts:u64
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let signer_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let pool_authority_ai = next_account_info(accounts_iter)?;
let stake_mint_ai = next_account_info(accounts_iter)?;
let stake_vault_ai = next_account_info(accounts_iter)?;
let reward_mint_ai = next_account_info(accounts_iter)?;
let reward_vault_ai = next_account_info(accounts_iter)?;
//2、校验
//2.1、Mint账户校验
ProcessHelper::assert_account_owner(stake_mint_ai, &spl_token::ID)?;
ProcessHelper::assert_account_owner(reward_mint_ai, &spl_token::ID)?;
let stake_mint = spl_token::state::Mint::unpack(&stake_mint_ai.data.borrow())?;
let reward_mint = spl_token::state::Mint::unpack(&reward_mint_ai.data.borrow())?;
if !stake_mint.is_initialized(){
return Err(MasterChefError::IllegalAccount.into());
}
if !reward_mint.is_initialized(){
return Err(MasterChefError::IllegalAccount.into());
}
if stake_mint_ai.key == reward_mint_ai.key{
return Err(MasterChefError::MintAccountSame.into());
}
//2.2、校验Singer
ProcessHelper::assert_signer(signer_ai)?;
//2.3、校验pool和pool_authority
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let pool_authority_pubkey = Pubkey::create_program_address(
&[b"authority", pool_ai.key.as_ref(), &[nonce]],
program_id,
)?;
ProcessHelper::assert_account(&pool_authority_pubkey, pool_authority_ai.key)?;
//2.4、校验stake vault 和 reward vault
ProcessHelper::assert_account_owner(stake_vault_ai, &spl_token::ID)?;
let stake_vault = spl_token::state::Account::unpack(&stake_vault_ai.data.borrow())?;
ProcessHelper::assert_account(&stake_vault.mint, stake_mint_ai.key)?;
ProcessHelper::assert_account(&stake_vault.owner, &pool_authority_pubkey)?;
if!&stake_vault.close_authority.contains(&pool_authority_pubkey){
return Err(MasterChefError::IllegalAccount.into());
}
ProcessHelper::assert_account_owner(reward_vault_ai, &spl_token::ID)?;
let reward_vault = spl_token::state::Account::unpack(&reward_vault_ai.data.borrow())?;
ProcessHelper::assert_account(&reward_vault.mint, stake_mint_ai.key)?;
ProcessHelper::assert_account(&reward_vault.owner, &pool_authority_pubkey)?;
if!&reward_vault.close_authority.contains(&pool_authority_pubkey){
return Err(MasterChefError::IllegalAccount.into());
}
//3、初始化pool
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
if stake_pool.is_initialized() {
return Err(ProgramError::AccountAlreadyInitialized);
}
stake_pool.initialized = true;
stake_pool.owner = *signer_ai.key;
stake_pool.nonce = nonce;
stake_pool.stake_mint_decimals = stake_mint.decimals;
stake_pool.stake_mint_pubkey = *stake_mint_ai.key;
stake_pool.reward_mint_pubkey = *reward_mint_ai.key;
stake_pool.stake_vault = *stake_vault_ai.key;
stake_pool.reward_vault = *reward_vault_ai.key;
stake_pool.reward_duration = locked_ts * 1000 / clock::DEFAULT_MS_PER_SLOT;
stake_pool.period_start = u64::MAX;
stake_pool.last_update_slot = u64::MAX;
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
//4、发事件
msg!("process_init_stake_pool:{:?},{:?}",nonce,locked_ts);
Ok(())
}
这个函数用来初始化一个流动性矿池。首先获取所有的账户,然后进行账户校验,最后执行初始化
1、Mint账户进行校验,判断是否属于SPL Token,然后unpack出来对应的Mint数据,校验是否合法,然后校验两个Mint是否一样,这里我们选择不支持单token质押的情况,因此两个Mint一样就返回错误。
2、Signer校验,其实就是校验一下flag标记
3、pool校验,校验传进来的pool是属于这个program的,否则当前program是无法操作这个pool账户的,也就是执行数据写入的时候会失败。
4、通过"authority"字符串和pool的地址进行拼接,生成一个PDA,然后和传入的账户地址进行比较,需要保持一致,否则就等于使用错了账户。
5、stake vault和reward vault是这个pool的两个金库,stake vault用来接受用户质押的token,同时可以在用户发起赎回的时候转回token,reward vault是用来保存奖励token,同时可以给用户转奖励token。这两个金库必须是spl_token里的TokenAccount,它们的data可以解析为spl_token::state::Account,这里面有两个属性:mint和owner,其中mint就是Mint账户的地址,标明这个TokenAccount持有的是哪种类型的token,而owner是指这个账户的拥有者。这里会校验两个金库对应的Mint是传入的Mint以及owner是上面生成的PDA。如果Mint不一致,那么后面转账就会有问题,如果owner不是PDA,说明这两个金库可能是个人持有的,会有rug-pull的风险。然后这两个金库的close权限也必须为空,防止被意外关闭导致资金损失。
6、使用StakePool的unpack把数据转成结构体,然后判断是否初始化,防止重复调用,最后设置结构体的各个属性,调用pack写回数据
7、通过msg!宏命令发送事件,记录关键参数。
8.5、process_create_user
pub fn process_create_user(
program_id:&Pubkey,
accounts:&[AccountInfo],
user_nonce:u8,
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let signer_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let user_pda = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
//2、校验
//2.1、校验Singer
ProcessHelper::assert_signer(signer_ai)?;
//2.2、校验pool
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
//3、生成user账户
let (key, _) =
ProcessHelper::address_staker_balance(pool_ai.key, signer_ai.key, program_id);
ProcessHelper::assert_account(&key, user_pda.key)?;
invoke_signed(
&system_instruction::create_account(
signer_ai.key,
&key,
Rent::get()?.minimum_balance(StakeBalance::LEN),
StakeBalance::LEN as u64,
program_id,
),
&[
signer_ai.clone(),
user_pda.clone(),
system_program.clone(),
],
&[&[
SEED_BALANCE_ATA,
signer_ai.key.as_ref(),
pool_ai.key.as_ref(),
&[user_nonce],
]],
)?;
Pack::pack(
StakeBalance {
owner: *signer_ai.key,
reward:0,
reward_per_token_paid:0,
balance: 0,
nonce:user_nonce
},
&mut user_pda.data.borrow_mut(),
)?;
msg!("process_create_user:{:?}",user_nonce);
Ok(())
}
process_create_user用来创建一个流动性矿池账户,首先获取所有账户,然后进行校验,最后调用底层指令生成一个流动性矿池账户,后续所有质押、赎回以及奖励信息都会记录到这个账户中。
1、Signer校验,其实就是校验一下flag标记
2、校验pool,防止pool未初始化
3、使用pool和signer的地址生成一个PDA,然后校验传入的地址是这个PDA,这里之所以用PDA,是为了保证一个用户在一个池子里只能创建一个账户。
4、调用invoke_signed执行system_instruction::create_account指令,其实就是创建一个正在的账户,然后把StakeBalance结构体写入这个账户的data字段,这里我们可以看到owner是我们的signer地址。之前说过通过PDA已经保证一个用户在一个池子里只能创建一个账户,这里的owner保证了只有设置的signer才能操作这个账户。
8.6、process_notify_reward
pub fn process_notify_reward(
program_id:&Pubkey,
accounts:&[AccountInfo],
amount:u64,
start_ts:u64,
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let signer_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let user_reward_account = next_account_info(accounts_iter)?;
let pool_reward_account = next_account_info(accounts_iter)?;
let spl_token_program = next_account_info(accounts_iter)?;
//2、校验
//2.1、校验Singer
ProcessHelper::assert_signer(signer_ai)?;
//2.2、校验pool
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if stake_pool.paused{
return Err(MasterChefError::Paused.into());
}
if &stake_pool.owner != signer_ai.key {
return Err(MasterChefError::NotAllowed.into());
}
ProcessHelper::assert_account(pool_reward_account.key, &stake_pool.stake_vault)?;
//3、更新奖励相关参数
if stake_pool.reward_duration == 0 {
return Err(MasterChefError::InvalidRewardDuration.into());
}
let current_slot = Clock::get()?.slot;
let current_ts = Clock::get()?.unix_timestamp as u64;
let start_slot = if start_ts == 0 {
current_slot
} else {
current_slot + (start_ts.checked_sub(current_ts).unwrap()) * 1000 / clock::DEFAULT_MS_PER_SLOT
};
if current_slot >= stake_pool.period_finish {
stake_pool.reward_rate = amount / stake_pool.reward_duration;
} else {
let remaining = stake_pool.period_finish.checked_sub(current_slot).unwrap();
let leftover = remaining * stake_pool.reward_rate;
stake_pool.reward_rate = (amount + leftover) / stake_pool.reward_duration;
}
stake_pool.period_start = start_slot;
stake_pool.last_update_slot = start_slot;
stake_pool.period_finish = start_slot + stake_pool.reward_duration;
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
//4、添加奖励
invoke(
&spl_token::instruction::transfer(
&spl_token::ID,
user_reward_account.key,
pool_reward_account.key,
signer_ai.key,
&[],
amount,
)?,
&[
spl_token_program.clone(),
user_reward_account.clone(),
pool_reward_account.clone(),
signer_ai.clone(),
],
)?;
//5、发事件
msg!(&format!("RewardAdded({})", amount));
Ok(())
}
process_notify_reward是用来给reward vault进行充值的,也就是说可以延长矿池的挖矿时间。
1、除了常规校验,这里还校验了矿池不能处于暂停状态,然后signer必须是矿池的owner。
2、根据充值的奖励,算出新的结束时间和奖励发放速率,并更新pool
3、调用spl_token::instruction::transfer指令进行转账
8.7、process_set_pause
pub fn process_set_pause(
program_id:&Pubkey,
accounts:&[AccountInfo],
paused:bool,
)-> ProgramResult{
let accounts_iter = &mut accounts.iter();
let admin_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
ProcessHelper::assert_signer(admin_ai)?;
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())?;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if &stake_pool.owner != admin_ai.key {
return Err(MasterChefError::NotAllowed.into());
}
stake_pool.paused = paused;
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
msg!("process_set_pause:{:?}",paused);
Ok(())
}
process_set_pause是用来开关矿池,只有pool的owner才有权限,业务逻辑比较简单,就不细说了。
8.8、process_stake
pub fn process_stake(
program_id:&Pubkey,
accounts:&[AccountInfo],
amount:u64,
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let staker_ai = next_account_info(accounts_iter)?;
let staker_mint_ata_ai = next_account_info(accounts_iter)?;
let staker_balance_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let pool_stake_vault_ai = next_account_info(accounts_iter)?;
let spl_token_program = next_account_info(accounts_iter)?;
//2、校验
//2.1、校验Singer
ProcessHelper::assert_signer(staker_ai)?;
//2.2、校验pool
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if stake_pool.paused{
return Err(MasterChefError::Paused.into());
}
ProcessHelper::assert_account(pool_stake_vault_ai.key, &stake_pool.stake_vault)?;
//2.3、校验staker balance
ProcessHelper::assert_account_owner(staker_balance_ai, program_id)?;
let mut balance = StakeBalance::unpack(&staker_balance_ai.data.borrow())?;
if !balance.is_initialized(){
return Err(MasterChefError::InvalidUserAccount.into());
}
ProcessHelper::assert_account(&balance.owner, staker_ai.key)?;
//3、更新奖励
ProcessHelper::update_staker_reward(&mut stake_pool, &mut balance)?;
//4、更新用户balance数据
stake_pool.total_supply = stake_pool.total_supply.checked_add(amount).unwrap();
balance.balance = balance.balance.checked_add(amount).unwrap();
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
Pack::pack(balance, &mut staker_balance_ai.data.borrow_mut())?;
//5、转账
invoke(
&spl_token::instruction::transfer(
&spl_token::ID,
staker_mint_ata_ai.key,
pool_stake_vault_ai.key,
staker_ai.key,
&[],
amount,
)?,
&[
spl_token_program.clone(),
staker_mint_ata_ai.clone(),
pool_stake_vault_ai.clone(),
staker_ai.clone(),
],
)?;
//6、发事件
msg!(&format!(
"Staked({}, {})",
staker_ai.key.to_string(),
amount
));
Ok(())
}
process_stake是用户质押的逻辑
1、除了做常规的校验,还需要校验传入的pool_stake_vault_ai是否和pool里stake vault的地址一致,防止传错,然后需要校验staker_balance_ai里的owner是不是signer,因为用户只能操作自己的StakeBalance。
2、更新全局奖励,然后更新用户奖励
3、执行转账,把token从用户账户转给stake vault
8.9、process_withdraw
pub fn process_withdraw(
program_id:&Pubkey,
accounts:&[AccountInfo],
amount:u64,
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let staker_ai = next_account_info(accounts_iter)?;
let staker_mint_ata_ai = next_account_info(accounts_iter)?;
let staker_balance_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let pool_stake_vault_ai = next_account_info(accounts_iter)?;
let pool_authority_ai = next_account_info(accounts_iter)?;
let spl_token_program = next_account_info(accounts_iter)?;
//2、校验
//2.1、校验Singer
ProcessHelper::assert_signer(staker_ai)?;
//2.2、校验pool
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
let nonce = stake_pool.nonce;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if stake_pool.paused{
return Err(MasterChefError::Paused.into());
}
ProcessHelper::assert_account(pool_stake_vault_ai.key, &stake_pool.stake_vault)?;
//2.3、校验staker balance
ProcessHelper::assert_account_owner(staker_balance_ai, program_id)?;
let mut balance = StakeBalance::unpack(&staker_balance_ai.data.borrow())?;
if !balance.is_initialized(){
return Err(MasterChefError::InvalidUserAccount.into());
}
ProcessHelper::assert_account(&balance.owner, staker_ai.key)?;
//3、更新奖励
ProcessHelper::update_staker_reward(&mut stake_pool, &mut balance)?;
//4、更新用户balance数据
stake_pool.total_supply = stake_pool.total_supply.checked_sub(amount).unwrap();
balance.balance = balance.balance.checked_sub(amount).unwrap();
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
Pack::pack(balance, &mut staker_balance_ai.data.borrow_mut())?;
//5、转账
let pool_authority_pubkey = Pubkey::create_program_address(
&[b"authority", pool_ai.key.as_ref(), &[nonce]],
program_id,
)?;
ProcessHelper::assert_account(&pool_authority_pubkey, pool_authority_ai.key)?;
invoke_signed(
&spl_token::instruction::transfer(
&spl_token::ID,
pool_stake_vault_ai.key,
staker_mint_ata_ai.key,
&pool_authority_pubkey,
&[],
amount,
)?,
&[
pool_stake_vault_ai.clone(),
staker_mint_ata_ai.clone(),
pool_authority_ai.clone(),
spl_token_program.clone(),
],
&[&[b"authority", pool_ai.key.as_ref(), &[nonce]]],
)?;
//6、发事件
msg!(&format!(
"Withdraw({}, {})",
staker_ai.key.to_string(),
amount
));
Ok(())
}
process_withdraw是赎回质押的token,这里的校验和stake差不多,需要注意的是,赎回是从stake vault转到用户账户,而stake vault是有所有者的,所以这里需要传入一个pool_authority_ai,然后校验是否和stake vault的owner一致。最后转账的时候也是用的invoke_signed而不是invoke,因为stake vault属于PDA控制的账户。
8.10、process_get_reward
pub fn process_get_reward(
program_id:&Pubkey,
accounts:&[AccountInfo],
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let staker_ai = next_account_info(accounts_iter)?;
let staker_reward_ata_ai = next_account_info(accounts_iter)?;
let staker_balance_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let pool_reward_vault_ai = next_account_info(accounts_iter)?;
let pool_authority_ai = next_account_info(accounts_iter)?;
let spl_token_program = next_account_info(accounts_iter)?;
//2、校验
//2.1、校验Singer
ProcessHelper::assert_signer(staker_ai)?;
//2.2、校验pool
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
let nonce = stake_pool.nonce;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if stake_pool.paused{
return Err(MasterChefError::Paused.into());
}
ProcessHelper::assert_account(pool_reward_vault_ai.key, &stake_pool.reward_vault)?;
//2.3、校验staker balance
ProcessHelper::assert_account_owner(staker_balance_ai, program_id)?;
let mut balance = StakeBalance::unpack(&staker_balance_ai.data.borrow())?;
if !balance.is_initialized(){
return Err(MasterChefError::InvalidUserAccount.into());
}
ProcessHelper::assert_account(&balance.owner, staker_ai.key)?;
//3、更新奖励
ProcessHelper::update_staker_reward(&mut stake_pool, &mut balance)?;
//4、更新用户reward数据
let total_reward = balance.reward;
balance.reward = 0;
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
Pack::pack(balance, &mut staker_balance_ai.data.borrow_mut())?;
//5、转账
if total_reward == 0{
msg!(&format!(
"GetReward({}, {})",
staker_ai.key.to_string(),
total_reward
));
return Ok(());
}
let pool_authority_pubkey = Pubkey::create_program_address(
&[b"authority", pool_ai.key.as_ref(), &[nonce]],
program_id,
)?;
ProcessHelper::assert_account(&pool_authority_pubkey, pool_authority_ai.key)?;
invoke_signed(
&spl_token::instruction::transfer(
&spl_token::ID,
pool_reward_vault_ai.key,
staker_reward_ata_ai.key,
&pool_authority_pubkey,
&[],
total_reward,
)?,
&[
pool_reward_vault_ai.clone(),
staker_reward_ata_ai.clone(),
pool_authority_ai.clone(),
spl_token_program.clone(),
],
&[&[b"authority", pool_ai.key.as_ref(), &[nonce]]],
)?;
//6、发事件
msg!(&format!(
"GetReward({}, {})",
staker_ai.key.to_string(),
total_reward
));
Ok(())
}
process_get_reward是获取奖励,这里的校验和withdraw差不多,获取奖励是从reward vault转到用户账户,而reward vault是有所有者的,所以这里需要传入一个pool_authority_ai,然后校验是否和reward vault的owner一致。最后转账的时候也是用的invoke_signed而不是invoke,因为reward vault属于PDA控制的账户。
8.11、process_exit
pub fn process_exit(
program_id:&Pubkey,
accounts:&[AccountInfo],
)-> ProgramResult{
//1、获取所有账户
let accounts_iter = &mut accounts.iter();
let staker_ai = next_account_info(accounts_iter)?;
let staker_stake_ata_ai = next_account_info(accounts_iter)?;
let staker_reward_ata_ai = next_account_info(accounts_iter)?;
let staker_balance_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let pool_stake_vault_ai = next_account_info(accounts_iter)?;
let pool_reward_vault_ai = next_account_info(accounts_iter)?;
let pool_authority_ai = next_account_info(accounts_iter)?;
let spl_token_program = next_account_info(accounts_iter)?;
//2、校验
//2.1、校验Singer
ProcessHelper::assert_signer(staker_ai)?;
//2.2、校验pool
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())
.or::<ProgramError>(Ok(StakePool::default()))?;
let nonce = stake_pool.nonce;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if stake_pool.paused{
return Err(MasterChefError::Paused.into());
}
ProcessHelper::assert_account(pool_reward_vault_ai.key, &stake_pool.reward_vault)?;
ProcessHelper::assert_account(pool_stake_vault_ai.key, &stake_pool.stake_vault)?;
//2.3、校验staker balance
ProcessHelper::assert_account_owner(staker_balance_ai, program_id)?;
let mut balance = StakeBalance::unpack(&staker_balance_ai.data.borrow())?;
if !balance.is_initialized(){
return Err(MasterChefError::InvalidUserAccount.into());
}
ProcessHelper::assert_account(&balance.owner, staker_ai.key)?;
//3、更新奖励
ProcessHelper::update_staker_reward(&mut stake_pool, &mut balance)?;
//4、更新用户balance数据
let stake_amount = balance.balance;
let total_reward = balance.reward;
stake_pool.total_supply = stake_pool.total_supply.checked_sub(stake_amount).unwrap();
balance.balance = 0;
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
Pack::pack(balance, &mut staker_balance_ai.data.borrow_mut())?;
//5、赎回质押的token,并且获取奖励
let pool_authority_pubkey = Pubkey::create_program_address(
&[b"authority", pool_ai.key.as_ref(), &[nonce]],
program_id,
)?;
ProcessHelper::assert_account(&pool_authority_pubkey, pool_authority_ai.key)?;
if stake_amount>0 {
invoke_signed(
&spl_token::instruction::transfer(
&spl_token::ID,
pool_stake_vault_ai.key,
staker_stake_ata_ai.key,
&pool_authority_pubkey,
&[],
stake_amount,
)?,
&[
pool_stake_vault_ai.clone(),
staker_stake_ata_ai.clone(),
pool_authority_ai.clone(),
spl_token_program.clone(),
],
&[&[b"authority", pool_ai.key.as_ref(), &[nonce]]],
)?;
}
if total_reward>0{
invoke_signed(
&spl_token::instruction::transfer(
&spl_token::ID,
pool_reward_vault_ai.key,
staker_reward_ata_ai.key,
&pool_authority_pubkey,
&[],
total_reward,
)?,
&[
pool_reward_vault_ai.clone(),
staker_reward_ata_ai.clone(),
pool_authority_ai.clone(),
spl_token_program.clone(),
],
&[&[b"authority", pool_ai.key.as_ref(), &[nonce]]],
)?;
}
//6、发事件
msg!(&format!(
"Exit({}, {},{})",
staker_ai.key.to_string(),
stake_amount,
total_reward
));
Ok(())
}
process_exit是赎回全部token,并且获取全部奖励,其实就是把withdraw和get_reward的逻辑合在一起了。
8.12、process_change_owner
pub fn process_change_owner(
program_id:&Pubkey,
accounts:&[AccountInfo],
)-> ProgramResult{
let accounts_iter = &mut accounts.iter();
let admin_ai = next_account_info(accounts_iter)?;
let pool_ai = next_account_info(accounts_iter)?;
let new_owner_ai = next_account_info(accounts_iter)?;
ProcessHelper::assert_signer(admin_ai)?;
ProcessHelper::assert_account_owner(pool_ai, program_id)?;
let mut stake_pool = StakePool::unpack(&pool_ai.data.borrow())?;
if !stake_pool.is_initialized() {
return Err(MasterChefError::InvalidPool.into());
}
if &stake_pool.owner != admin_ai.key {
return Err(MasterChefError::NotAllowed.into());
}
stake_pool.owner = *new_owner_ai.key;
Pack::pack(stake_pool, &mut pool_ai.data.borrow_mut())?;
msg!("process_change_owner:{:?}",new_owner_ai.key);
Ok(())
}
修改pool的owner,具体细节参考前面。
9、声明
本文的用意只是尝试写一个solana版本的sushi masterchef,本文的代码没有经过严格测试,可能会有很多bug,不保证代码绝对正确性。
最后
以上就是贪玩绿茶为你收集整理的写一个Solana版的Sushi Masterchef1、项目结构2、Cargo.toml3、lib.rs4、entrypoint.rs5、error.rs6、instruction.rs7、state.rs8、processor.rs9、声明的全部内容,希望文章能够帮你解决写一个Solana版的Sushi Masterchef1、项目结构2、Cargo.toml3、lib.rs4、entrypoint.rs5、error.rs6、instruction.rs7、state.rs8、processor.rs9、声明所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复