我是靠谱客的博主 贪玩绿茶,最近开发中收集的这篇文章主要介绍写一个Solana版的Sushi Masterchef1、项目结构2、Cargo.toml3、lib.rs4、entrypoint.rs5、error.rs6、instruction.rs7、state.rs8、processor.rs9、声明,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

对于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、声明所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部