owned this note
owned this note
Published
Linked with GitHub
# Reward Distribution Program
A while back, I was given a prompt for a program to build as part of the interview process for a Solana DeFi project. I thought it was a pretty interesting and unique prompt, so I decided to make a tutorial out of it. For what it’s worth, I did receive an unofficial offer from the company after this, so this can give any potential devs out there an idea of the quality of work that’s expected from a potential employer.
In this tutorial, we’ll be building a staking contract that allows users to stake their `RND` tokens in a pool. An authorized user can mint more `RND` tokens to the pool that is distributed pro rata to all stakers according to their stake weight. The same authorized user is also able to burn `RND` tokens from the pool, taking tokens away from each staker pro rata.
The program makes use of a pull based system where each user's total rewards gained and tokens burned are derived once the user issues an instruction to unstake their tokens.
The algorithm used in this program borrows heavily from the Scalable Rewards Distribution algorithms described in these two papers for the Ethereum network. The algorithm used in this program is slightly different, but is essentially a version of the methods described there implemented on Solana.
- [Scalable Rewards Distribution on the Ethereum Blockchain](https://uploads-ssl.webflow.com/5ad71ffeb79acc67c8bcdaba/5ad8d1193a40977462982470_scalable-reward-distribution-paper.pdf)
- [Scalable Rewards Distribution with Compounding Stakes](https://github.com/liquity/liquity/blob/master/papers/Scalable_Reward_Distribution_with_Compounding_Stakes.pdf)
I do suggest you read each of these papers thoroughly. All the math required for this program is explained there. Also, this is more of an intermediate tutorial, as such, I will not be explaining some of the fundamental topics of Solana development that I expect you should know already. Although, I will do my best to link to helpful documentation wherever possible for anybody that may not be an intermediate Solana developer yet. With that said, this will probably be hard to follow and truly understand if you have not already done some Solana/Anchor development.
The finished program has been open sourced in GitHub:
[https://github.com/ixmorrow/rnd-staking](https://github.com/ixmorrow/rnd-staking)
## Instructions
Our program will contain the following instructions:
### `init_pool`
Initializes a new staking pool, requires a signature from the `program_authority`. The pool is a pda with the address of the token mint that the pool is intended for and "state" as seeds.
### `init_stake_entry`
Initializes an account to hold state about a user's stake position. PDA with the User's pubkey, mint of token, and "stake_entry" as seeds.
### `stake`
Transfers tokens from a User token account to the program token vault, where they are kept while staked.
### `distribute`
This instruction mints tokens to the staking pool where they are distributed evenly to all stakers in proportion to their stake weight.
Requires a signature from the `program_authority`.
### `burn`
Burns tokens from the staking pool and each staker loses tokens evenly in proportion to their stake weight.
Requires a signature from the `program_authority`.
### `unstake`
Transfers tokens from the staking pool back to a user. The amount of tokens transferred is dependent upon the amount of rewards and burns that have occurred while a user was staked.
User can call this at any time.
Users can only unstake tokens that they have staked themselves.
## Setup
Before we get started, we need to make sure we have the proper dev environment setup. Please follow the instructions in the [Local Setup Lesson of the SolDev course](https://soldev.app/course/local-setup) and then [install Anchor](https://www.anchor-lang.com/docs/installation) if you have not already done so.
## Code
### Program Structure
Now, we can get started writing some code. Let’s create an Anchor project from scratch using the command line.
```bash
anchor init rnd-staking
```
That should create a new directory called `rnd-staking`, open this up in VS Code (or your preferred code editor). Inside `programs/rnd-staking/src` we’ve got the default file Anchor generates.

Before we get started, let’s change the layout of the program. First, create the following new files inside `src`.
- `errors.rs`
- `state.rs`
- `utils.rs`
Then, create a new directory called `instructions` inside `/src`.
Next, change the `lib.rs` file to:
```rust
pub mod errors;
pub mod instructions;
pub mod state;
pub mod utils;
use {anchor_lang::prelude::*, instructions::*};
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod rnd_staking {
use super::*;
}
```
The new file structure and `lib.rs` change should look like this:

The `lib.rs` file is the entrypoint to our program. It’s where we’ll define the publicly callable endpoints, but we don’t want the logic to live there. Modularizing the program like this makes it more organized and easier to read. As we add instructions to the `instructions` directory, we will update the `rnd_staking` module here to define how that instruction logic can be called externally.
Lastly, we need to add some dependencies to our program. Open up the `/src/Cargo.toml` file and add the following to the `[dependencies]` section:
```rust
[dependencies]
anchor-lang = "0.25.0"
anchor-spl = "0.25.0"
solana-program = "~1.10.29"
spl-token = "3.3.0"
```
The appropriate version of each package may vary depending on when you are working through this.
### `init_pool`
Now we’ve got the program setup we can start working on the instruction logic. First, we’ll implement the logic necessary to create a staking pool. We are going to limit *who* can initialize a new staking pool to only the `program_authority`. Other than that, we just need to create a token account that is owned by a PDA of our program, which is where the staked tokens will be held. We will also need a state account to hold information about the staking pool.
If you need a refresher on PDAs, [check this out](https://soldev.app/course/pda).
Create a file called `init_pool.rs` inside the `instructions` directory. The accounts struct for this instruction will be as follows:
```rust
// inside init_pool.rs
#[derive(Accounts)]
pub struct InitializePool<'info> {
#[account(
init,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump,
payer = program_authority,
space = STAKE_POOL_SIZE
)]
pub pool_state: Account<'info, PoolState>,
#[account(
init,
token::mint = token_mint,
token::authority = vault_authority,
seeds = [token_mint.key().as_ref(), vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump,
payer = program_authority
)]
pub token_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub token_mint: Account<'info, Mint>,
#[account(
mut,
constraint = program_authority.key() == PROGRAM_AUTHORITY
@ StakeError::InvalidProgramAuthority
)]
pub program_authority: Signer<'info>,
/// CHECK: This is not dangerous because we're only using this as a program signer
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub vault_authority: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>
}
```
There are a few constants that are referenced here, we’ll be defining these in the `state.rs` file.
- `STAKE_POOL_STATE_SEED`
- `STAKE_POOL_SIZE`
- `VAULT_SEED`
- `PROGRAM_AUTHORITY`
- `VAULT_AUTH_SEED`
Before we do that though, let’s walkthrough what’s happening with the Anchor code we just wrote. Using constraints, we initialize a `PoolState` account paid for by the `program_authority` account. Then, a token account is created for the mint of the `token_mint` that was passed in the instruction. This token account’s authority is `vault_authority`, which is a PDA derived with the `VAULT_AUTH_SEED`. This means the program has full control over the token account. Lastly, a signature is required from the `program_authority` account and we ensure the address of the account matches what the program has stored in the `PROGRAM_AUTHORITY` variable. If it doesn’t then we return a `StakeError::InvalidProgramAuthority` error. This is a custom error type that we have not implemented yet, so let’s do that! In `error.rs` all we need to do is create an enum to represent all the different custom errors we’ll define.
```rust
// inside error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum StakeError {
#[msg("Incorrect program authority")]
InvalidProgramAuthority
}
```
We only have one custom error right now, but as we build our program out we can create more!
The next step would be to actually initialize the state of the `PoolState` account that we just created, but we have to first define what type of information this account will store. Let’s do that in the `state.rs file.
```rust
// inside state.rs
use {
anchor_lang::prelude::*,
solana_program::{pubkey, pubkey::Pubkey},
};
pub const STAKE_POOL_STATE_SEED: &str = "state";
pub const STAKE_POOL_SIZE: usize = 8 + 32 + 32 + 1 + 8 + 32 + 8 + 1 + 1 + 32 + 16 + 8;
pub const VAULT_SEED: &str = "vault";
pub const VAULT_AUTH_SEED: &str = "vault_authority";
pub static PROGRAM_AUTHORITY: Pubkey = pubkey!("9MNHTJJ1wd6uQrZfXk46T24qcWNZYpYfwZKk6zho4poV");
#[account]
pub struct PoolState {
pub authority: Pubkey,
pub bump: u8,
pub amount: u64,
pub token_vault: Pubkey,
pub token_mint: Pubkey,
pub initialized_at: i64,
pub vault_bump: u8,
pub vault_auth_bump: u8,
pub vault_authority: Pubkey,
pub distribution_rate: u128,
pub user_deposit_amt: u64
}
```
Now, see if you can write the code to initialize the `PoolState` account yourself inside `init_pool.rs`.
The full `init_pool.rs` file should look like:
```rust
use {
anchor_lang::prelude::*,
crate::{state::*, errors::*},
anchor_spl::{token::{TokenAccount, Mint, Token}},
};
pub fn handler(ctx: Context<InitializePool>) -> Result<()> {
// initialize pool state
let pool_state = &mut ctx.accounts.pool_state;
pool_state.authority = ctx.accounts.program_authority.key();
pool_state.bump = *ctx.bumps.get("pool_state").unwrap();
pool_state.amount = 0;
pool_state.user_deposit_amt = 0;
pool_state.token_vault = ctx.accounts.token_vault.key();
pool_state.token_mint = ctx.accounts.token_mint.key();
pool_state.initialized_at = Clock::get().unwrap().unix_timestamp;
pool_state.vault_bump = *ctx.bumps.get("token_vault").unwrap();
pool_state.vault_auth_bump = *ctx.bumps.get("vault_authority").unwrap();
pool_state.vault_authority = ctx.accounts.vault_authority.key();
pool_state.distribution_rate = 1;
Ok(())
}
#[derive(Accounts)]
pub struct InitializePool<'info> {
#[account(
init,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump,
payer = program_authority,
space = STAKE_POOL_SIZE
)]
pub pool_state: Account<'info, PoolState>,
#[account(
init,
token::mint = token_mint,
token::authority = vault_authority,
seeds = [token_mint.key().as_ref(), vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump,
payer = program_authority
)]
pub token_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub token_mint: Account<'info, Mint>,
#[account(
mut,
constraint = program_authority.key() == PROGRAM_AUTHORITY
@ StakeError::InvalidProgramAuthority
)]
pub program_authority: Signer<'info>,
/// CHECK: This is not dangerous because we're only using this as a program signer
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub vault_authority: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>
}
```
Congrats, you have just implemented the logic for the first instruction of our program! Now, we need to define how this instruction can be called externally. Before we do that, we need to create a new file called `mod.rs` inside the `instructions` directory. This file just serves as a way for us to create public modules for each instruction that we can then call in the `rnd_staking` module in `lib.rs`. To do that, add the following to the newly created `mod.rs` file.
```rust
// inside src/instructions/mod.rs
pub mod init_pool;
pub use init_pool::*;
```
Then, we can create the public endpoint to the `init_pool` instruction in `lib.rs`:
```rust
// inside lib.rs
#[program]
pub mod rnd_staking {
use super::*;
pub fn init_pool(ctx: Context<InitializePool>) -> Result<()> {
init_pool::handler(ctx)
}
}
```
Awesome! The `init_pool` instruction should be completely functional now. We’ll have to add each instruction we create to the `mod.rs` and `lib.rs` files like that in order for them to be publicly callable by clients. This extra step won’t be explicitly covered for every instruction in this program, it’s expected that you add these on your own.
### `init_stake_entry`
This instruction will create a state account that will hold information about a user’s stake - that’s it. Because this `StakeEntry` account will hold state about a user’s stake, we will require that it be created before someone stakes tokens. You could just create this account on the Stake instruction, but then you’d run into an issue if a user wanted to stake some tokens more than once. There is an Anchor account constraint, `init_if_needed`, you can use to create the account only if it has not been created yet but it’s not the most secure way of doing things (see [the Reinitialization Attacks lesson of the Solana Course](https://soldev.app/course/reinitialization-attacks)).
So, we separate the initialization of the `StakeEntry` account from the stake instruction to avoid that altogether. Before we implement this, let’s first define what this account will look like inside `state.rs`.
```rust
// inside state.rs
...
...
pub const STAKE_ENTRY_SEED: &str = "stake_entry";
pub const STAKE_ENTRY_SIZE: usize = 8 + 32 + 1 + 8 + 8 + 16;
...
...
#[account]
pub struct StakeEntry {
pub user: Pubkey,
pub bump: u8,
pub balance: u64,
pub last_staked: i64,
pub initial_distribution_rate: u128
}
```
The `StakeEntry` account will be a PDA derived with the seed “stake_entry”. This PDA will hold the user’s Pubkey, bump used to derive the `StakeEntry` PDA, how much they have staked, when they last staked in the form of a Unix timestamp, and something called the `initial_distribution_rate` which we’ll explain later on.
We also store the seed and the size of the account in bytes as constants here.
Now, we can create a new file called `init_stake_entry.rs` inside the `instructions` folder. All we’re doing in this instruction is creating the `StakeEntry` account, which is a PDA derived with the following seeds:
- the user’s wallet public key
- the mint address of the token being staked
- the `STAKE_ENTRY_SEED`
Once the account has been created, we just need to initialize the `StakeEntry` account data. Try putting this together yourself.
Here is what the code inside `init_stake_entry.rs` should look like:
```rust
use {
anchor_lang::prelude::*,
crate::{state::*},
};
pub fn handler(ctx: Context<InitEntryCtx>) -> Result<()> {
// initialize user stake entry state
let user_entry = &mut ctx.accounts.user_stake_entry;
user_entry.user = ctx.accounts.user.key();
user_entry.bump = *ctx.bumps.get("user_stake_entry").unwrap();
user_entry.balance = 0;
user_entry.initial_distribution_rate = ctx.accounts.pool_state.distribution_rate;
Ok(())
}
#[derive(Accounts)]
pub struct InitEntryCtx <'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
seeds = [user.key().as_ref(), pool_state.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump,
payer = user,
space = STAKE_ENTRY_SIZE
)]
pub user_stake_entry: Account<'info, StakeEntry>,
#[account(
seeds = [pool_state.token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool_state.bump
)]
pub pool_state: Account<'info, PoolState>,
pub system_program: Program<'info, System>,
}
```
It’s pretty straightforward, but let’s talk through it. First, we derive and then initialize the `StakeEntry` PDA using the seeds previously mentioned. You’ll notice that we do not require the token mint be passed in to this instruction, even though it was a required seed. That’s because we store the token mint on the `PoolState` account and a `StakeEntry` account should be correlated to the pool the tokens are meant to be staked in. So, we require the corresponding `pool_state` account be passed in and then use the mint address stored there as the seed. You could also use the `pool_state` account address as a seed if you wanted to allow for multiple staking pools for a single token.
We also require the `user` sign the transaction.
Inside the `handler` function, we just initialize the state of the newly created entry account. We don’t set the `user_stake_entry.last_staked` field because the user has not staked anything yet, only created the `StakeEntry` account.
Lastly, we need to add this instruction to the `mod.rs` and `lib.rs` files.
```rust
// inside mod.rs
pub mod init_pool;
pub mod init_stake_entry;
pub use init_pool::*;
pub use init_stake_entry::*;
```
```rust
// inside lib.rs
pub fn init_stake_entry(ctx: Context<InitEntryCtx>) -> Result<()> {
init_stake_entry::handler(ctx)
}
```
As a sanity check, you can save all of your work and run `anchor build` in the terminal to make sure the program compiles so far.
### `stake`
This instruction gets a little more complicated, let’s talk through the logic we need.
When a user calls the `stake` instruction, we should transfer the amount of tokens they want to stake from their token account to the staking pool token account. In the same transaction, we need to update the state of the user’s `StakeEntry` account to reflect how many tokens they have staked. We also need to update the state in the `PoolState` account to reflect the newest addition to the staking pool.
Let’s start by creating the `stake.rs` file and defining our accounts struct. Here are the accounts we need:
- pool state account
- token vault where the staked tokens are stored
- user
- user stake entry account, must be initialized already
- user token account
- token program
- system program
```rust
// inside stake.rs
#[derive(Accounts)]
pub struct StakeCtx <'info> {
#[account(
mut,
seeds = [pool.token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool.bump
)]
pub pool: Account<'info, PoolState>,
#[account(
mut,
seeds = [pool.token_mint.key().as_ref(), pool.vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool.vault_bump
)]
pub token_vault: Account<'info, TokenAccount>,
#[account(
mut,
constraint = user.key() == user_stake_entry.user
@ StakeError::InvalidUser
)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [user.key().as_ref(), pool.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump = user_stake_entry.bump
)]
pub user_stake_entry: Account<'info, StakeEntry>,
#[account(
mut,
constraint = user_token_account.mint == pool.token_mint
@ StakeError::InvalidMint
)]
pub user_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>
}
```
All we’re doing here is verifying the accounts passed into the instruction. We have added two new custom errors that we need to define in `error.rs`.
```rust
// inside error.rs
#[error_code]
pub enum StakeError {
#[msg("Incorrect program authority")]
InvalidProgramAuthority,
#[msg("Token mint is invalid")]
InvalidMint,
#[msg("Invalid user provided")]
InvalidUser
}
```
Moving on to the business logic, we need to do these three things:
- transfer the amount of tokens the user wants to stake to the staking vault
- update the pool state account
- update the user’s stake entry account
```rust
pub fn handler(ctx: Context<StakeCtx>, stake_amount: u64) -> Result<()> {
// transfer amount from user token acct to vault
transfer(ctx.accounts.transfer_ctx(), stake_amount)?;
msg!("Pool initial total: {}", ctx.accounts.pool.amount);
msg!("Initial user deposits: {}", ctx.accounts.pool.user_deposit_amt);
msg!("User entry initial balance: {}", ctx.accounts.user_stake_entry.balance);
ctx.accounts.user_stake_entry.initial_distribution_rate = ctx.accounts.pool.distribution_rate;
// update pool state amount
let pool = &mut ctx.accounts.pool;
let user_entry = &mut ctx.accounts.user_stake_entry;
pool.amount = pool.amount.checked_add(stake_amount).unwrap();
pool.user_deposit_amt = pool.user_deposit_amt.checked_add(stake_amount).unwrap();
msg!("Current pool total: {}", pool.amount);
msg!("Amount of tokens deposited by users: {}", pool.user_deposit_amt);
// update user stake entry
user_entry.balance = user_entry.balance.checked_add(stake_amount).unwrap();
msg!("User entry balance: {}", user_entry.balance);
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
Ok(())
}
```
Where the `transfer` method is defined as:
```rust
impl<'info> StakeCtx <'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = Transfer {
from: self.user_token_account.to_account_info(),
to: self.token_vault.to_account_info(),
authority: self.user.to_account_info()
};
CpiContext::new(cpi_program, cpi_accounts)
}
}
```
If Anchor CPIs, or just CPIs (Cross-Program Invocation) in general, are new to you check out the following:
- [https://soldev.app/course/cpi](https://soldev.app/course/cpi)
- [https://soldev.app/course/anchor-cpi](https://soldev.app/course/anchor-cpi)
Let’s talk about this line in the instruction:
```rust
ctx.accounts.user_stake_entry.initial_distribution_rate = ctx.accounts.pool.distribution_rate;
```
We have to set the current distribution rate stored in the pool in the user’s `StakeEntry` account. This value is what’s used to calculate how many tokens a user gets from the rewards distributed/burned over the course of their stake when they finally unstake them. If this is at all confusing you, I urge you to go back and review the two papers linked in the beginning of this tutorial.
There is a specific scenario our current logic doesn’t account for. Right now, the `stake` instruction assumes a user will only stake tokens once. Suppose a user were to stake 10 tokens at t0, then 20 more at t1 where the pool distribution rate has changed from t0 → t1. In this case, the amount of tokens the user would receive upon unstaking would not be accurate. We have to use the current distribution rate to determine how many tokens the user is *actually owed* before adding more tokens to their stake.
Let’s take a look at how we can implement this in the code.
```rust
pub fn handler(ctx: Context<StakeCtx>, stake_amount: u64) -> Result<()> {
// transfer amount from user token acct to vault
transfer(ctx.accounts.transfer_ctx(), stake_amount)?;
msg!("Pool initial total: {}", ctx.accounts.pool.amount);
msg!("Initial user deposits: {}", ctx.accounts.pool.user_deposit_amt);
msg!("User entry initial balance: {}", ctx.accounts.user_stake_entry.balance);
if ctx.accounts.user_stake_entry.balance == 0 {
// if it's a user's first time staking, this is all that needs to be done
ctx.accounts.user_stake_entry.initial_distribution_rate = ctx.accounts.pool.distribution_rate;
}
else {
msg!("User adding to original stake position");
// calculate amount of tokens user is owed after rewards/burns are taken into account
let out_amount: u128 = calculate_out_amount(&ctx.accounts.pool, &ctx.accounts.user_stake_entry);
msg!("Out amount: {}", out_amount);
// create new staking position with rewards/burn amount included
ctx.accounts.pool.user_deposit_amt = ctx.accounts.pool.user_deposit_amt.checked_sub(ctx.accounts.user_stake_entry.balance).unwrap()
.checked_add(out_amount as u64).unwrap();
msg!("Deposit amt: {}", ctx.accounts.pool.user_deposit_amt);
ctx.accounts.user_stake_entry.balance = out_amount as u64;
msg!("User stake balance: {}", ctx.accounts.user_stake_entry.balance);
ctx.accounts.user_stake_entry.initial_distribution_rate = ctx.accounts.pool.distribution_rate;
}
// update pool state amount
let pool = &mut ctx.accounts.pool;
let user_entry = &mut ctx.accounts.user_stake_entry;
pool.amount = pool.amount.checked_add(stake_amount).unwrap();
pool.user_deposit_amt = pool.user_deposit_amt.checked_add(stake_amount).unwrap();
msg!("Current pool total: {}", pool.amount);
msg!("Amount of tokens deposited by users: {}", pool.user_deposit_amt);
// update user stake entry
user_entry.balance = user_entry.balance.checked_add(stake_amount).unwrap();
msg!("User entry balance: {}", user_entry.balance);
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
Ok(())
}
```
If a user’s account balance is 0, then we don’t need to do anything different. If not, we have to calculate how many tokens the user actually has staked after taking into account the distributions/burns that have taken place. That’s what the `calculate_out_amount` function is doing. Then, we update the user’s `StakeEntry` balance to reflect this amount.
Now, we just need to implement the `calculate_out_amount` function.
```rust
// inside state.rs
pub fn calculate_out_amount(pool_state: &PoolState, user_stake_entry: &StakeEntry) -> u128 {
// using a single distribution rate
let distribution_rate: u128;
if user_stake_entry.initial_distribution_rate == 1 {
distribution_rate = pool_state.distribution_rate;
msg!("initial rate == 1");
msg!("Distribution rate: {}", distribution_rate);
} else {
distribution_rate = pool_state.distribution_rate.checked_mul(RATE_MULT).unwrap()
.checked_div(user_stake_entry.initial_distribution_rate).unwrap();
msg!("Distribution rate: {}", distribution_rate);
}
msg!("User staked amount: {}", user_stake_entry.balance);
let amount = user_stake_entry.balance;
let out_amount: u128;
out_amount = (amount as u128).checked_mul(distribution_rate).unwrap().checked_div(RATE_MULT).unwrap();
msg!("Amount after rewards/burn: {}", out_amount);
out_amount
}
```
This might look a little confusing at first, let’s walk through it. The purpose of the function is to determine how many tokens the user is owed based on the amount of tokens they have staked, the number of tokens that have been distributed as rewards, and the number that have burned. This value is determined by taking the ratio of the distribution rate when the user initially staked their tokens and the current distribution rate. This ratio is then multiplied by their initial stake amount to derive how many tokens they’re actually owed.
This is probably the most confusing part of the program. Again I suggest re-reading the papers linked above in the beginning of the tutorial to get a better understanding of the algorithm itself.
The distribution rate is calculated when tokens are distributed/burned from the staking pool, which we will cover later in the tutorial.
### `unstake`
The logic for the `unstake` instruction should go as follows:
1. Calculate amount of tokens user is owed after the rewards/burns are taken into account
2. Transfer this amount from the stake vault to the user token account
3. Update the amount of tokens stored in the `PoolState` account
4. Update the state stored in the user `StakeEntry` account
Before any of this happens, we of course need to ensure signatures are provided for the appropriate accounts, that the account calling this instruction is the owner of these Staked tokens, etc.
Let’s dive into the accounts struct. You should be able to construct this on your own with the proper validations. We will need the following:
- staking pool state account
- token vault, where the staked tokens are held
- the user account
- this account must be a singer on the tx
- user `StakeEntry` account
- user_stake_entry.user == user.key()
- the user token account to transfer to
- must be of the same mint as the staking pool
- token vault authority
- token mint of staked tokens
- token program
- system program
---
All of that together looks something like this:
```rust
//inside unstake.rs
#[derive(Accounts)]
pub struct UnstakeCtx <'info> {
#[account(
mut,
seeds = [pool.token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool.bump
)]
pub pool: Account<'info, PoolState>,
#[account(
mut,
seeds = [pool.token_mint.key().as_ref(), pool.vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool.vault_bump
)]
pub token_vault: Account<'info, TokenAccount>,
#[account(
mut,
constraint = user.key() == user_stake_entry.user
@ StakeError::InvalidUser
)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [user.key().as_ref(), pool.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump = user_stake_entry.bump
)]
pub user_stake_entry: Account<'info, StakeEntry>,
#[account(
mut,
constraint = user_token_account.mint == pool.token_mint
@ StakeError::InvalidMint
)]
pub user_token_account: Account<'info, TokenAccount>,
/// CHECK: This is not dangerous because we're only using this as a program signer
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump = pool.vault_auth_bump
)]
pub vault_authority: AccountInfo<'info>,
#[account(
mut,
constraint = token_mint.key() == pool.token_mint
@ StakeError::InvalidMint
)]
pub token_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>
}
```
There are a couple important validations we’re making here that I want to highlight. First, we use the `Signer` account type on the user account. This is because a signature must be present to unstake some tokens, otherwise anyone can unstake the tokens. The second is we use anchor constraints to verify that the account passed in as the user matches the key stored on the `user_stake_entry` account. Because we require the user account to sign the transaction and verify that it is, in fact, the account associated with this stake, we can safely guarantee that only the original staker can unstake these tokens (or guarantee that only someone with access to this account’s private key can unstake these tokens).
Now that we’ve made these validations, we can move on to the business logic. We don’t need to worry about anymore validation, it’s all handled in the account struct. To start, let’s derive how many tokens the user is actually owed. We can use the same exact `calculate_out_amount` function that we implemented earlier to do this.
```rust
// inside unstake.rs
pub fn handler(ctx: Context<UnstakeCtx>) -> Result<()> {
// calculate amount of tokens user is owed after rewards/burns are taken into account
let out_amount: u128 = calculate_out_amount(&ctx.accounts.pool, &ctx.accounts.user_stake_entry);
msg!("Out amount returned: {}", out_amount);
msg!("Total staked before withdrawal: {}", ctx.accounts.pool.amount);
...
...
Ok(())
}
```
If our algorithm used inside `calculate_out_amount` is correct, then the variable `out_amount` should contain exactly how many tokens the user is owed after taking into account the distributions/burns that have taken place over the course of their stake. Next, we need to transfer `out_amount` of tokens to the user’s token account passed in the instruction.
```rust
// inside unstake.rs
pub fn handler(ctx: Context<UnstakeCtx>) -> Result<()> {
// calculate amount of tokens user is owed after rewards/burns are taken into account
let out_amount: u128 = calculate_out_amount(&ctx.accounts.pool, &ctx.accounts.user_stake_entry);
msg!("Out amount returned: {}", out_amount);
msg!("Total staked before withdrawal: {}", ctx.accounts.pool.amount);
// program signer seeds
let auth_bump = ctx.accounts.pool.vault_auth_bump;
let auth_seeds = &[VAULT_AUTH_SEED.as_bytes(), &[auth_bump]];
let signer = &[&auth_seeds[..]];
// transfer out_amount from stake vault to user
let transfer_ix = transfer_checked(
&ctx.accounts.token_program.key(),
&ctx.accounts.token_vault.key(),
&ctx.accounts.token_mint.key(),
&ctx.accounts.user_token_account.key(),
&ctx.accounts.vault_authority.key(),
&[&ctx.accounts.vault_authority.key()],
out_amount as u64,
6
).unwrap();
invoke_signed(
&transfer_ix,
&[
ctx.accounts.token_program.to_account_info(),
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.token_mint.to_account_info(),
ctx.accounts.user_token_account.to_account_info(),
ctx.accounts.vault_authority.to_account_info()
],
signer
)?;
...
...
Ok(())
}
```
We are transferring from the `token_vault` account which is a program owned token account. That means the authority of the account is a PDA. In order to transfer tokens, we must provide a signature for that PDA. We can only provide signatures for PDAs within the owning program by passing the seeds used to derive the PDA into the `invoke_signed` method.
We also need to construct our transfer instruction. Notice we are using the `transfer_checked` method from the `spl_token` crate instead of the normal `transfer` instruction. This is solely because you are allowed to pass in the amount of decimals the token mint has that you are transferring in this instruction. In my testing, I created a token mint with 6 decimal places instead of the default 9 decimal places, which is why I use this instruction here. You can change this later if you’d like to use the default 9 decimals, but I suggest you leave it as is for the duration of the tutorial.
Calling `invoke_signed` ([Rust docs](https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html)) is what actually sends the tx to be processed by the runtime. After calling this, we need to update the state stored in our pool and user entry accounts.
```rust
let pool = &mut ctx.accounts.pool;
let user_entry = &mut ctx.accounts.user_stake_entry;
// subtract out_amount from pool total
pool.amount = pool.amount.checked_sub(out_amount.try_into().unwrap()).unwrap();
// subtract amount user had staked originally, not the amount they are receiving after rewards/burn
pool.user_deposit_amt = pool.user_deposit_amt.checked_sub(user_entry.balance).unwrap();
msg!("Total staked after withdrawal: {}", pool.amount);
msg!("Amount deposited by users: {}", pool.user_deposit_amt);
// update user stake entry
user_entry.balance = 0;
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
```
The final `[unstake.rs](http://unstake.rs)` file should look like:
```rust
use {
anchor_lang::prelude::*,
crate::{state::*, errors::*},
anchor_spl::{token::{TokenAccount, Token, Mint}},
solana_program::{program::invoke_signed},
spl_token::instruction::transfer_checked,
};
pub fn handler(ctx: Context<UnstakeCtx>) -> Result<()> {
// calculate amount of tokens user is owed after rewards/burns are taken into account
let out_amount: u128 = calculate_out_amount(&ctx.accounts.pool, &ctx.accounts.user_stake_entry);
msg!("Out amount returned: {}", out_amount);
msg!("Total staked before withdrawal: {}", ctx.accounts.pool.amount);
// program signer seeds
let auth_bump = ctx.accounts.pool.vault_auth_bump;
let auth_seeds = &[VAULT_AUTH_SEED.as_bytes(), &[auth_bump]];
let signer = &[&auth_seeds[..]];
// transfer out_amount from stake vault to user
let transfer_ix = transfer_checked(
&ctx.accounts.token_program.key(),
&ctx.accounts.token_vault.key(),
&ctx.accounts.token_mint.key(),
&ctx.accounts.user_token_account.key(),
&ctx.accounts.vault_authority.key(),
&[&ctx.accounts.vault_authority.key()],
out_amount as u64,
6
).unwrap();
invoke_signed(
&transfer_ix,
&[
ctx.accounts.token_program.to_account_info(),
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.token_mint.to_account_info(),
ctx.accounts.user_token_account.to_account_info(),
ctx.accounts.vault_authority.to_account_info()
],
signer
)?;
let pool = &mut ctx.accounts.pool;
let user_entry = &mut ctx.accounts.user_stake_entry;
// subtract out_amount from pool total
pool.amount = pool.amount.checked_sub(out_amount.try_into().unwrap()).unwrap();
// subtract amount user had staked originally, not the amount they are receiving after rewards/burn
pool.user_deposit_amt = pool.user_deposit_amt.checked_sub(user_entry.balance).unwrap();
msg!("Total staked after withdrawal: {}", pool.amount);
msg!("Amount deposited by users: {}", pool.user_deposit_amt);
// update user stake entry
user_entry.balance = 0;
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
Ok(())
}
#[derive(Accounts)]
pub struct UnstakeCtx <'info> {
#[account(
mut,
seeds = [pool.token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool.bump
)]
pub pool: Account<'info, PoolState>,
#[account(
mut,
seeds = [pool.token_mint.key().as_ref(), pool.vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool.vault_bump
)]
pub token_vault: Account<'info, TokenAccount>,
#[account(
mut,
constraint = user.key() == user_stake_entry.user
@ StakeError::InvalidUser
)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [user.key().as_ref(), pool.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump = user_stake_entry.bump
)]
pub user_stake_entry: Account<'info, StakeEntry>,
#[account(
mut,
constraint = user_token_account.mint == pool.token_mint
@ StakeError::InvalidMint
)]
pub user_token_account: Account<'info, TokenAccount>,
/// CHECK: This is not dangerous because we're only using this as a program signer
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump = pool.vault_auth_bump
)]
pub vault_authority: AccountInfo<'info>,
#[account(
mut,
constraint = token_mint.key() == pool.token_mint
@ StakeError::InvalidMint
)]
pub token_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>
}
```
Awesome! At this point, you have written an almost fully functional token staking contract. With some very slight changes, you could actually deploy this program as is and create staking pools, stake/unstake tokens, etc. Users would not get any rewards for staking with this contract, so I don’t think it would be considered very useful, but it’s still really cool to think that you’ve built that.
### `distribute`
Now, on to the rewards portion of our contract. Before we get started, let’s recap the business logic of how this is supposed to work. The idea is, say you have x amount of people staking tokens in a staking pool. We want an instruction that a program authority can call that will mint a given amount of tokens and distribute them pro rata to all the current stakers. Pro rata meaning in proportion to the amount each user has staked.
This is where the distribution rate that we mentioned briefly earlier in the tutorial comes into play. The distribution rate is a value that represents how many reward tokens a user is owed per token they have originally staked. We calculate this value by dividing the number of tokens to distribute by the total tokens currently staked. Every time tokens are distributed or burned, this value is re-calculated.
Hopefully that made sense, if not then maybe walking through the code will help. Let’s put the accounts struct together.
```rust
// inside distribute.rs
use {
anchor_lang::prelude::*,
crate::{state::*, errors::*},
anchor_spl::{token::{TokenAccount, MintTo, Token, Mint, mint_to}},
};
...
...
#[derive(Accounts)]
pub struct DistributeCtx<'info> {
#[account(
constraint = program_authority.key() == PROGRAM_AUTHORITY
@ StakeError::InvalidProgramAuthority
)]
pub program_authority: Signer<'info>,
#[account(
mut,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool_state.bump,
)]
pub pool_state: Account<'info, PoolState>,
#[account(
mut,
seeds = [token_mint.key().as_ref(), pool_state.vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool_state.vault_bump,
)]
pub token_vault: Account<'info, TokenAccount>,
#[account(
mut,
constraint = token_mint.key() == pool_state.token_mint
@ StakeError::InvalidMint
)]
pub token_mint: Account<'info, Mint>,
/// CHECK: This is not dangerous because using as program signer
#[account(
constraint = mint_auth.key() == pool_state.vault_authority
@ StakeError::InvalidMintAuthority
)]
pub mint_auth: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
}
```
You might have noticed I mentioned that only the “program authority” should be able to call this instruction. That’s because we don’t want just anybody to be able to call this instruction since it distributes rewards, that would be a major security flaw. So, the `program_authority` account is required to have signed this transaction. This will just be an account that we have hard coded the public key in the value `PROGRAM_AUTHORITY` in the `state.rs` file. For testing purposes, you should set this to the public key of what ever account you will be calling this instruction with.
The rest of the struct is pretty straightforward. The only account that is new is the `mint_auth`. When a stake pool is created, a PDA is assigned as the authority of the vault token account. This instruction also requires that that same PDA be the mint authority of this token. This is not very realistic for contract on mainnet, but it serves our purposes here. The reason the mint authority of the token mint must be a PDA is because this is a program that distributes staking rewards in the same token that is being staked. Most staking contracts have their own reward token.
Next, we can work on the logic to “distribute” the rewards. I put that in quotes because we are not actually sending tokens out to users in this instruction, or even updating how many tokens each user has staked. All we do is calculate the ratio of the amount of tokens distributed to the total tokens users have staked. This ratio is then used when a user calls the `unstake` instruction to derive how many tokens to actually unstake.
```rust
// inside distribute.rs
pub fn handler(ctx: Context<DistributeCtx>, amount: u64) -> Result<()> {
// program signer seeds
let auth_bump = ctx.accounts.pool_state.vault_auth_bump;
let auth_seeds = &[VAULT_AUTH_SEED.as_bytes(), &[auth_bump]];
let signer = &[&auth_seeds[..]];
// donate RND tokens by minting to vault
mint_to(ctx.accounts.mint_ctx().with_signer(signer), amount)?;
// update state
let pool_state = &mut ctx.accounts.pool_state;
if pool_state.amount != 0 {
// calculate new distribution rate
let new_reward_rate = RATE_MULT.checked_add((amount as u128).checked_mul(RATE_MULT).unwrap()
.checked_div(pool_state.amount as u128).unwrap()).unwrap();
msg!("New rate (to be mult by previous: {}", new_reward_rate);
if pool_state.distribution_rate == 1 {
pool_state.distribution_rate = pool_state.distribution_rate.checked_mul(new_reward_rate).unwrap();
} else {
pool_state.distribution_rate = pool_state.distribution_rate.checked_mul(new_reward_rate).unwrap().checked_div(RATE_MULT).unwrap();
}
msg!("Rewards to distribute: {}", amount);
msg!("Total staked: {}", pool_state.amount);
msg!("User deposits: {}", pool_state.user_deposit_amt);
msg!("Distribution rate: {}", pool_state.distribution_rate);
}
// update pool amount
pool_state.amount = pool_state.amount.checked_add(amount).unwrap();
msg!("Total staked after distribution: {}", pool_state.amount);
Ok(())
}
```
### `burn`
The last instruction is the `burn` instruction. Not only does this contract distribute rewards evenly to stakers pro rata, but it also has the capability to burn tokens from the staking pool in a similar fashion.
The algorithm to burn tokens is the same as the distribution logic. In fact, all we’re going to be doing is changing that distribution rate that’s stored on the `PoolState` account in the same manner. Again, this instruction should only be callable by the `PROGRAM_AUTHORITY` and instead of minting tokens, it will burn them. So, we will need the same PDA mint authority in order to execute this instruction.
The account struct is pretty similar to the `DistributeCtx`
```rust
// inside burn.rs
#[derive(Accounts)]
pub struct BurnCtx<'info> {
#[account(
constraint = program_authority.key() == PROGRAM_AUTHORITY
@ StakeError::InvalidProgramAuthority
)]
pub program_authority: Signer<'info>,
#[account(
mut,
seeds = [pool_state.token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool_state.bump,
)]
pub pool_state: Account<'info, PoolState>,
#[account(
mut,
seeds = [pool_state.token_mint.key().as_ref(), pool_state.vault_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool_state.vault_bump,
)]
pub token_vault: Account<'info, TokenAccount>,
/// CHECK: This is not dangerous because we're only using this as a program signer
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump = pool_state.vault_auth_bump
)]
pub vault_authority: AccountInfo<'info>,
#[account(
mut,
constraint = token_mint.key() == pool_state.token_mint
@ StakeError::InvalidMint
)]
pub token_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>
}
impl<'info> BurnCtx <'info> {
pub fn burn_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Burn<'info>> {
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = Burn {
mint: self.token_mint.to_account_info(),
from: self.token_vault.to_account_info(),
authority: self.vault_authority.to_account_info()
};
CpiContext::new(cpi_program, cpi_accounts)
}
}
```
The business logic is also very similar, the only difference is that we call `burn` instead of `mint`.
```rust
// inside burn.rs
use {
anchor_lang::prelude::*,
crate::{state::*, errors::*},
anchor_spl::{token::{TokenAccount, Token, Mint, Burn, burn}},
};
pub fn handler(ctx: Context<BurnCtx>, amount: u64) -> Result<()> {
// program signer seeds
let auth_bump = ctx.accounts.pool_state.vault_auth_bump;
let auth_seeds = &[VAULT_AUTH_SEED.as_bytes(), &[auth_bump]];
let signer = &[&auth_seeds[..]];
// burn the tokens
burn(ctx.accounts.burn_ctx().with_signer(signer), amount)?;
// calculate new reward rate
let pool_state = &mut ctx.accounts.pool_state;
msg!("Tokens to burn: {}", amount);
msg!("Initial total staked: {}", pool_state.amount);
msg!("Initial distribution rate: {}", pool_state.distribution_rate);
if pool_state.amount != 0 {
// calculate new distribution rate
let new_distribution_rate = RATE_MULT.checked_sub((amount as u128).checked_mul(RATE_MULT).unwrap()
.checked_div(pool_state.amount as u128).unwrap()).unwrap();
msg!("New rate (to be mult by previous: {}", new_distribution_rate);
if pool_state.distribution_rate == 1 {
pool_state.distribution_rate = pool_state.distribution_rate.checked_mul(new_distribution_rate).unwrap();
} else {
pool_state.distribution_rate = pool_state.distribution_rate.checked_mul(new_distribution_rate).unwrap().checked_div(RATE_MULT).unwrap();
}
msg!("User deposits: {}", pool_state.user_deposit_amt);
msg!("Distribution rate: {}", pool_state.distribution_rate);
}
// update state in pool
pool_state.amount = pool_state.amount.checked_sub(amount).unwrap();
msg!("Current total staked: {}", pool_state.amount);
msg!("Amount deposited by Users: {}", pool_state.user_deposit_amt);
msg!("Current distribution rate: {}", pool_state.distribution_rate);
Ok(())
}
```
And that’s it! Please refer to [this repo](https://github.com/ixmorrow/rnd-staking) for the completely finished version of the program. There is an entire integration test and unit tests within the /tests directory of the repo. If you have followed along and implemented everything in this tutorial, you should be able to copy the entire tests folder, npm install any dependencies and then run the tests in the directory against your program.