Staking Program
A staking program locks tokens to earn rewards. This implementation features time-based reward calculation, compound staking, and slashing mechanics.
Staking Architecture
Text
┌─────────────────────────────────────────────────────────────────┐
│ Staking Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Pool Admin │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Staking Pool │◄──── Reward Token Deposits │
│ │ ──────────────── │ │
│ │ Total Staked │ │
│ │ Reward Rate │ │
│ │ Last Update Time │ │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ User Stake #1 │ │ User Stake #2 │ │
│ │ ──────────────── │ │ ──────────────── │ │
│ │ Amount: 1000 │ │ Amount: 500 │ │
│ │ Rewards: 50 │ │ Rewards: 25 │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Complete Implementation
Rust
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer, Mint};
use anchor_spl::associated_token::AssociatedToken;
declare_id!("Stake11111111111111111111111111111111111111");
#[program]
pub mod staking {
use super::*;
/// Initialize a new staking pool
pub fn initialize_pool(
ctx: Context<InitializePool>,
reward_rate: u64, // Rewards per second per staked token (scaled by 1e9)
lock_period: i64, // Minimum stake duration in seconds
min_stake: u64, // Minimum stake amount
) -> Result<()> {
let pool = &mut ctx.accounts.pool;
pool.authority = ctx.accounts.authority.key();
pool.stake_mint = ctx.accounts.stake_mint.key();
pool.reward_mint = ctx.accounts.reward_mint.key();
pool.reward_rate = reward_rate;
pool.lock_period = lock_period;
pool.min_stake = min_stake;
pool.total_staked = 0;
pool.reward_per_token_stored = 0;
pool.last_update_time = Clock::get()?.unix_timestamp;
pool.paused = false;
pool.bump = ctx.bumps.pool;
pool.stake_vault_bump = ctx.bumps.stake_vault;
pool.reward_vault_bump = ctx.bumps.reward_vault;
emit!(PoolInitialized {
pool: ctx.accounts.pool.key(),
stake_mint: pool.stake_mint,
reward_mint: pool.reward_mint,
reward_rate,
});
Ok(())
}
/// Stake tokens
pub fn stake(ctx: Context<Stake>, amount: u64) -> Result<()> {
let pool = &ctx.accounts.pool;
require!(!pool.paused, StakingError::PoolPaused);
require!(amount >= pool.min_stake, StakingError::BelowMinimumStake);
// Update rewards before modifying stake
update_rewards(&mut ctx.accounts.pool, Some(&mut ctx.accounts.user_stake))?;
// Transfer stake tokens to vault
let cpi_accounts = Transfer {
from: ctx.accounts.user_stake_token.to_account_info(),
to: ctx.accounts.stake_vault.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, amount)?;
// Update user stake
let user_stake = &mut ctx.accounts.user_stake;
user_stake.staked_amount = user_stake.staked_amount.checked_add(amount)
.ok_or(StakingError::Overflow)?;
user_stake.last_stake_time = Clock::get()?.unix_timestamp;
// Update pool
let pool = &mut ctx.accounts.pool;
pool.total_staked = pool.total_staked.checked_add(amount)
.ok_or(StakingError::Overflow)?;
emit!(Staked {
pool: ctx.accounts.pool.key(),
user: ctx.accounts.user.key(),
amount,
total_staked: user_stake.staked_amount,
});
Ok(())
}
/// Unstake tokens
pub fn unstake(ctx: Context<Unstake>, amount: u64) -> Result<()> {
let user_stake = &ctx.accounts.user_stake;
let pool = &ctx.accounts.pool;
require!(amount > 0, StakingError::InvalidAmount);
require!(amount <= user_stake.staked_amount, StakingError::InsufficientStake);
// Check lock period
let clock = Clock::get()?;
let time_since_stake = clock.unix_timestamp - user_stake.last_stake_time;
require!(
time_since_stake >= pool.lock_period,
StakingError::StakeLocked
);
// Update rewards
update_rewards(&mut ctx.accounts.pool, Some(&mut ctx.accounts.user_stake))?;
// Transfer tokens back to user
let pool_key = ctx.accounts.pool.key();
let seeds = &[
b"pool",
pool.stake_mint.as_ref(),
pool.reward_mint.as_ref(),
&[pool.bump],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = Transfer {
from: ctx.accounts.stake_vault.to_account_info(),
to: ctx.accounts.user_stake_token.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
signer_seeds,
);
token::transfer(cpi_ctx, amount)?;
// Update user stake
let user_stake = &mut ctx.accounts.user_stake;
user_stake.staked_amount = user_stake.staked_amount.checked_sub(amount)
.ok_or(StakingError::Overflow)?;
// Update pool
let pool = &mut ctx.accounts.pool;
pool.total_staked = pool.total_staked.checked_sub(amount)
.ok_or(StakingError::Overflow)?;
emit!(Unstaked {
pool: ctx.accounts.pool.key(),
user: ctx.accounts.user.key(),
amount,
remaining: user_stake.staked_amount,
});
Ok(())
}
/// Claim accumulated rewards
pub fn claim_rewards(ctx: Context<ClaimRewards>) -> Result<()> {
// Update rewards
update_rewards(&mut ctx.accounts.pool, Some(&mut ctx.accounts.user_stake))?;
let rewards = ctx.accounts.user_stake.pending_rewards;
require!(rewards > 0, StakingError::NoRewardsToClaim);
// Check reward vault has enough tokens
require!(
ctx.accounts.reward_vault.amount >= rewards,
StakingError::InsufficientRewardBalance
);
// Transfer rewards
let pool = &ctx.accounts.pool;
let seeds = &[
b"pool",
pool.stake_mint.as_ref(),
pool.reward_mint.as_ref(),
&[pool.bump],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = Transfer {
from: ctx.accounts.reward_vault.to_account_info(),
to: ctx.accounts.user_reward_token.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
signer_seeds,
);
token::transfer(cpi_ctx, rewards)?;
// Reset pending rewards
let user_stake = &mut ctx.accounts.user_stake;
user_stake.pending_rewards = 0;
emit!(RewardsClaimed {
pool: ctx.accounts.pool.key(),
user: ctx.accounts.user.key(),
amount: rewards,
});
Ok(())
}
/// Compound rewards into stake (for same-token staking)
pub fn compound(ctx: Context<Compound>) -> Result<()> {
let pool = &ctx.accounts.pool;
require!(
pool.stake_mint == pool.reward_mint,
StakingError::CannotCompound
);
require!(!pool.paused, StakingError::PoolPaused);
// Update rewards
update_rewards(&mut ctx.accounts.pool, Some(&mut ctx.accounts.user_stake))?;
let rewards = ctx.accounts.user_stake.pending_rewards;
require!(rewards > 0, StakingError::NoRewardsToClaim);
// Transfer from reward vault to stake vault
let pool = &ctx.accounts.pool;
let seeds = &[
b"pool",
pool.stake_mint.as_ref(),
pool.reward_mint.as_ref(),
&[pool.bump],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = Transfer {
from: ctx.accounts.reward_vault.to_account_info(),
to: ctx.accounts.stake_vault.to_account_info(),
authority: ctx.accounts.pool.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
signer_seeds,
);
token::transfer(cpi_ctx, rewards)?;
// Update balances
let user_stake = &mut ctx.accounts.user_stake;
user_stake.staked_amount = user_stake.staked_amount.checked_add(rewards)
.ok_or(StakingError::Overflow)?;
user_stake.pending_rewards = 0;
let pool = &mut ctx.accounts.pool;
pool.total_staked = pool.total_staked.checked_add(rewards)
.ok_or(StakingError::Overflow)?;
emit!(Compounded {
pool: ctx.accounts.pool.key(),
user: ctx.accounts.user.key(),
amount: rewards,
new_stake: user_stake.staked_amount,
});
Ok(())
}
/// Admin: Add rewards to pool
pub fn fund_rewards(ctx: Context<FundRewards>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.funder_token.to_account_info(),
to: ctx.accounts.reward_vault.to_account_info(),
authority: ctx.accounts.funder.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, amount)?;
emit!(RewardsFunded {
pool: ctx.accounts.pool.key(),
funder: ctx.accounts.funder.key(),
amount,
});
Ok(())
}
/// Admin: Update pool parameters
pub fn update_pool(
ctx: Context<UpdatePool>,
new_reward_rate: Option<u64>,
new_lock_period: Option<i64>,
new_min_stake: Option<u64>,
) -> Result<()> {
// Update rewards with current rate first
update_rewards(&mut ctx.accounts.pool, None)?;
let pool = &mut ctx.accounts.pool;
if let Some(rate) = new_reward_rate {
pool.reward_rate = rate;
}
if let Some(period) = new_lock_period {
pool.lock_period = period;
}
if let Some(min) = new_min_stake {
pool.min_stake = min;
}
emit!(PoolUpdated {
pool: ctx.accounts.pool.key(),
reward_rate: pool.reward_rate,
lock_period: pool.lock_period,
min_stake: pool.min_stake,
});
Ok(())
}
/// Admin: Pause/unpause pool
pub fn set_paused(ctx: Context<SetPaused>, paused: bool) -> Result<()> {
ctx.accounts.pool.paused = paused;
Ok(())
}
}
// Helper functions
fn update_rewards(pool: &mut Account<Pool>, user_stake: Option<&mut Account<UserStake>>) -> Result<()> {
let clock = Clock::get()?;
let current_time = clock.unix_timestamp;
if pool.total_staked > 0 {
let time_elapsed = current_time - pool.last_update_time;
let new_rewards = (time_elapsed as u128)
.checked_mul(pool.reward_rate as u128)
.ok_or(StakingError::Overflow)?;
let reward_per_token_delta = new_rewards
.checked_mul(PRECISION as u128)
.ok_or(StakingError::Overflow)?
.checked_div(pool.total_staked as u128)
.unwrap_or(0);
pool.reward_per_token_stored = pool.reward_per_token_stored
.checked_add(reward_per_token_delta as u64)
.ok_or(StakingError::Overflow)?;
}
pool.last_update_time = current_time;
if let Some(user) = user_stake {
let earned = calculate_earned(pool, user)?;
user.pending_rewards = user.pending_rewards
.checked_add(earned)
.ok_or(StakingError::Overflow)?;
user.reward_per_token_paid = pool.reward_per_token_stored;
}
Ok(())
}
fn calculate_earned(pool: &Account<Pool>, user: &Account<UserStake>) -> Result<u64> {
let reward_delta = pool.reward_per_token_stored
.checked_sub(user.reward_per_token_paid)
.ok_or(StakingError::Overflow)?;
let earned = (user.staked_amount as u128)
.checked_mul(reward_delta as u128)
.ok_or(StakingError::Overflow)?
.checked_div(PRECISION as u128)
.unwrap_or(0) as u64;
Ok(earned)
}
// Constants
const PRECISION: u64 = 1_000_000_000; // 1e9 for reward calculations
// Accounts
#[derive(Accounts)]
pub struct InitializePool<'info> {
#[account(
init,
payer = authority,
space = 8 + Pool::INIT_SPACE,
seeds = [b"pool", stake_mint.key().as_ref(), reward_mint.key().as_ref()],
bump
)]
pub pool: Account<'info, Pool>,
#[account(
init,
payer = authority,
token::mint = stake_mint,
token::authority = pool,
seeds = [b"stake_vault", pool.key().as_ref()],
bump
)]
pub stake_vault: Account<'info, TokenAccount>,
#[account(
init,
payer = authority,
token::mint = reward_mint,
token::authority = pool,
seeds = [b"reward_vault", pool.key().as_ref()],
bump
)]
pub reward_vault: Account<'info, TokenAccount>,
pub stake_mint: Account<'info, Mint>,
pub reward_mint: Account<'info, Mint>,
#[account(mut)]
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct Stake<'info> {
#[account(
mut,
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, Pool>,
#[account(
init_if_needed,
payer = user,
space = 8 + UserStake::INIT_SPACE,
seeds = [b"user_stake", pool.key().as_ref(), user.key().as_ref()],
bump
)]
pub user_stake: Account<'info, UserStake>,
#[account(
mut,
seeds = [b"stake_vault", pool.key().as_ref()],
bump = pool.stake_vault_bump,
)]
pub stake_vault: Account<'info, TokenAccount>,
#[account(
mut,
token::mint = pool.stake_mint,
token::authority = user,
)]
pub user_stake_token: Account<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Unstake<'info> {
#[account(
mut,
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, Pool>,
#[account(
mut,
seeds = [b"user_stake", pool.key().as_ref(), user.key().as_ref()],
bump,
has_one = pool,
)]
pub user_stake: Account<'info, UserStake>,
#[account(
mut,
seeds = [b"stake_vault", pool.key().as_ref()],
bump = pool.stake_vault_bump,
)]
pub stake_vault: Account<'info, TokenAccount>,
#[account(
mut,
token::mint = pool.stake_mint,
token::authority = user,
)]
pub user_stake_token: Account<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct ClaimRewards<'info> {
#[account(
mut,
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, Pool>,
#[account(
mut,
seeds = [b"user_stake", pool.key().as_ref(), user.key().as_ref()],
bump,
has_one = pool,
)]
pub user_stake: Account<'info, UserStake>,
#[account(
mut,
seeds = [b"reward_vault", pool.key().as_ref()],
bump = pool.reward_vault_bump,
)]
pub reward_vault: Account<'info, TokenAccount>,
#[account(
mut,
token::mint = pool.reward_mint,
)]
pub user_reward_token: Account<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct Compound<'info> {
#[account(
mut,
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, Pool>,
#[account(
mut,
seeds = [b"user_stake", pool.key().as_ref(), user.key().as_ref()],
bump,
has_one = pool,
)]
pub user_stake: Account<'info, UserStake>,
#[account(
mut,
seeds = [b"stake_vault", pool.key().as_ref()],
bump = pool.stake_vault_bump,
)]
pub stake_vault: Account<'info, TokenAccount>,
#[account(
mut,
seeds = [b"reward_vault", pool.key().as_ref()],
bump = pool.reward_vault_bump,
)]
pub reward_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct FundRewards<'info> {
#[account(
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
)]
pub pool: Account<'info, Pool>,
#[account(
mut,
seeds = [b"reward_vault", pool.key().as_ref()],
bump = pool.reward_vault_bump,
)]
pub reward_vault: Account<'info, TokenAccount>,
#[account(
mut,
token::mint = pool.reward_mint,
token::authority = funder,
)]
pub funder_token: Account<'info, TokenAccount>,
#[account(mut)]
pub funder: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct UpdatePool<'info> {
#[account(
mut,
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
has_one = authority,
)]
pub pool: Account<'info, Pool>,
pub authority: Signer<'info>,
}
#[derive(Accounts)]
pub struct SetPaused<'info> {
#[account(
mut,
seeds = [b"pool", pool.stake_mint.as_ref(), pool.reward_mint.as_ref()],
bump = pool.bump,
has_one = authority,
)]
pub pool: Account<'info, Pool>,
pub authority: Signer<'info>,
}
// State
#[account]
#[derive(InitSpace)]
pub struct Pool {
pub authority: Pubkey,
pub stake_mint: Pubkey,
pub reward_mint: Pubkey,
pub reward_rate: u64,
pub lock_period: i64,
pub min_stake: u64,
pub total_staked: u64,
pub reward_per_token_stored: u64,
pub last_update_time: i64,
pub paused: bool,
pub bump: u8,
pub stake_vault_bump: u8,
pub reward_vault_bump: u8,
}
#[account]
#[derive(InitSpace)]
pub struct UserStake {
pub pool: Pubkey,
pub owner: Pubkey,
pub staked_amount: u64,
pub pending_rewards: u64,
pub reward_per_token_paid: u64,
pub last_stake_time: i64,
}
// Events
#[event]
pub struct PoolInitialized {
pub pool: Pubkey,
pub stake_mint: Pubkey,
pub reward_mint: Pubkey,
pub reward_rate: u64,
}
#[event]
pub struct Staked {
pub pool: Pubkey,
pub user: Pubkey,
pub amount: u64,
pub total_staked: u64,
}
#[event]
pub struct Unstaked {
pub pool: Pubkey,
pub user: Pubkey,
pub amount: u64,
pub remaining: u64,
}
#[event]
pub struct RewardsClaimed {
pub pool: Pubkey,
pub user: Pubkey,
pub amount: u64,
}
#[event]
pub struct Compounded {
pub pool: Pubkey,
pub user: Pubkey,
pub amount: u64,
pub new_stake: u64,
}
#[event]
pub struct RewardsFunded {
pub pool: Pubkey,
pub funder: Pubkey,
pub amount: u64,
}
#[event]
pub struct PoolUpdated {
pub pool: Pubkey,
pub reward_rate: u64,
pub lock_period: i64,
pub min_stake: u64,
}
// Errors
#[error_code]
pub enum StakingError {
#[msg("Pool is paused")]
PoolPaused,
#[msg("Amount below minimum stake")]
BelowMinimumStake,
#[msg("Invalid amount")]
InvalidAmount,
#[msg("Insufficient stake balance")]
InsufficientStake,
#[msg("Stake is still locked")]
StakeLocked,
#[msg("No rewards to claim")]
NoRewardsToClaim,
#[msg("Insufficient reward balance in pool")]
InsufficientRewardBalance,
#[msg("Cannot compound: stake and reward tokens must match")]
CannotCompound,
#[msg("Arithmetic overflow")]
Overflow,
}
TypeScript Client
TypeScript
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Staking } from "../target/types/staking";
import * as token from "@solana/spl-token";
export class StakingClient {
constructor(
private program: Program<Staking>,
private provider: anchor.AnchorProvider,
) {}
async initializePool(
stakeMint: anchor.web3.PublicKey,
rewardMint: anchor.web3.PublicKey,
rewardRate: anchor.BN,
lockPeriod: anchor.BN,
minStake: anchor.BN,
): Promise<anchor.web3.PublicKey> {
const [poolPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("pool"), stakeMint.toBuffer(), rewardMint.toBuffer()],
this.program.programId,
);
const [stakeVaultPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("stake_vault"), poolPda.toBuffer()],
this.program.programId,
);
const [rewardVaultPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("reward_vault"), poolPda.toBuffer()],
this.program.programId,
);
await this.program.methods
.initializePool(rewardRate, lockPeriod, minStake)
.accounts({
pool: poolPda,
stakeVault: stakeVaultPda,
rewardVault: rewardVaultPda,
stakeMint,
rewardMint,
authority: this.provider.wallet.publicKey,
tokenProgram: token.TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
})
.rpc();
return poolPda;
}
async stake(pool: anchor.web3.PublicKey, amount: anchor.BN): Promise<void> {
const poolData = await this.program.account.pool.fetch(pool);
const user = this.provider.wallet.publicKey;
const [userStakePda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("user_stake"), pool.toBuffer(), user.toBuffer()],
this.program.programId,
);
const [stakeVaultPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("stake_vault"), pool.toBuffer()],
this.program.programId,
);
const userStakeToken = await token.getAssociatedTokenAddress(
poolData.stakeMint,
user,
);
await this.program.methods
.stake(amount)
.accounts({
pool,
userStake: userStakePda,
stakeVault: stakeVaultPda,
userStakeToken,
user,
tokenProgram: token.TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
}
async unstake(pool: anchor.web3.PublicKey, amount: anchor.BN): Promise<void> {
const poolData = await this.program.account.pool.fetch(pool);
const user = this.provider.wallet.publicKey;
const [userStakePda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("user_stake"), pool.toBuffer(), user.toBuffer()],
this.program.programId,
);
const [stakeVaultPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("stake_vault"), pool.toBuffer()],
this.program.programId,
);
const userStakeToken = await token.getAssociatedTokenAddress(
poolData.stakeMint,
user,
);
await this.program.methods
.unstake(amount)
.accounts({
pool,
userStake: userStakePda,
stakeVault: stakeVaultPda,
userStakeToken,
user,
tokenProgram: token.TOKEN_PROGRAM_ID,
})
.rpc();
}
async claimRewards(pool: anchor.web3.PublicKey): Promise<void> {
const poolData = await this.program.account.pool.fetch(pool);
const user = this.provider.wallet.publicKey;
const [userStakePda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("user_stake"), pool.toBuffer(), user.toBuffer()],
this.program.programId,
);
const [rewardVaultPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("reward_vault"), pool.toBuffer()],
this.program.programId,
);
const userRewardToken = await token.getOrCreateAssociatedTokenAccount(
this.provider.connection,
(this.provider.wallet as anchor.Wallet).payer,
poolData.rewardMint,
user,
);
await this.program.methods
.claimRewards()
.accounts({
pool,
userStake: userStakePda,
rewardVault: rewardVaultPda,
userRewardToken: userRewardToken.address,
user,
tokenProgram: token.TOKEN_PROGRAM_ID,
})
.rpc();
}
async getPendingRewards(
pool: anchor.web3.PublicKey,
user: anchor.web3.PublicKey,
): Promise<anchor.BN> {
const [userStakePda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("user_stake"), pool.toBuffer(), user.toBuffer()],
this.program.programId,
);
const poolData = await this.program.account.pool.fetch(pool);
const userStake = await this.program.account.userStake.fetch(userStakePda);
// Calculate pending rewards
const currentTime = Math.floor(Date.now() / 1000);
const timeElapsed = currentTime - poolData.lastUpdateTime.toNumber();
const PRECISION = new anchor.BN(1_000_000_000);
let rewardPerTokenStored = poolData.rewardPerTokenStored;
if (poolData.totalStaked.gt(new anchor.BN(0))) {
const newRewards = new anchor.BN(timeElapsed).mul(poolData.rewardRate);
const rewardPerTokenDelta = newRewards
.mul(PRECISION)
.div(poolData.totalStaked);
rewardPerTokenStored = rewardPerTokenStored.add(rewardPerTokenDelta);
}
const rewardDelta = rewardPerTokenStored.sub(userStake.rewardPerTokenPaid);
const earned = userStake.stakedAmount.mul(rewardDelta).div(PRECISION);
return userStake.pendingRewards.add(earned);
}
}
Reward Calculation
The staking program uses a time-weighted reward distribution formula:
Text
┌─────────────────────────────────────────────────────────────────┐
│ Reward Calculation │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Accumulated Reward Per Token: │
│ │
│ reward_rate × time_elapsed │
│ reward_per_token = ─────────────────────────── × PRECISION │
│ total_staked │
│ │
│ User's Pending Rewards: │
│ │
│ earned = user_stake × (reward_per_token - user_paid) │
│ ───────────────────────────────────────────── │
│ PRECISION │
│ │
│ Where PRECISION = 10^9 for avoiding floating point │
│ │
└─────────────────────────────────────────────────────────────────┘
APY Calculation
TypeScript
function calculateAPY(
rewardRate: number, // Rewards per second per token
stakeMintDecimals: number,
rewardMintDecimals: number,
): number {
const SECONDS_PER_YEAR = 365.25 * 24 * 60 * 60;
const PRECISION = 1e9;
// Rewards per year per token (normalized)
const rewardsPerYear = (rewardRate / PRECISION) * SECONDS_PER_YEAR;
// Adjust for decimal differences
const decimalAdjustment = Math.pow(
10,
stakeMintDecimals - rewardMintDecimals,
);
// APY as percentage
return rewardsPerYear * decimalAdjustment * 100;
}
Next: NFT Minting Contract - Creating and managing NFT collections.