Skip to content

Commit 3f3b51f

Browse files
committed
Staking Program For Graduated Tokens
1 parent c4a5880 commit 3f3b51f

File tree

17 files changed

+868
-0
lines changed

17 files changed

+868
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "staking_rewards"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "staking"
10+
11+
[features]
12+
default = []
13+
cpi = ["no-entrypoint"]
14+
no-entrypoint = []
15+
no-idl = []
16+
no-log-ix-name = []
17+
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
18+
19+
[dependencies]
20+
anchor-lang = {version = "0.30.1", features=["init-if-needed"]}
21+
anchor-spl = "0.30.1"
22+
solana-program = "=2.0.3"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use anchor_lang::prelude::*;
2+
3+
// SEEDS -> ALWAYS PREFIX WITH POOL KEY
4+
// Reward = reward + mint.key
5+
// User = signer.key
6+
// UserReward = reward + mint.key + user.key
7+
pub const SEED_POOL: &[u8] = b"pool";
8+
pub const SEED_STAKING_TOKEN_ACCOUNT: &[u8] = b"staking_token_account";
9+
pub const SEED_REWARD_TOKEN_ACCOUNT: &[u8] = b"reward_token_account";
10+
pub const SEED_USER: &[u8] = b"user";
11+
12+
#[constant]
13+
pub const PRECISION: u64 = 1_000_000_000;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use anchor_lang::prelude::*;
2+
3+
#[error_code]
4+
pub enum ErrorCode {
5+
#[msg("Not Distributor")]
6+
NotDistributor,
7+
#[msg("Zero Amount Added")]
8+
ZeroAmount,
9+
#[msg("Insufficient Funds")]
10+
InsufficientFunds,
11+
#[msg("Not Reward Register")]
12+
NotRewardRegister,
13+
#[msg("Reward Already Exists")]
14+
RewardAlreadyExists,
15+
#[msg("Pool Already Initialized")]
16+
AlreadyInitialized,
17+
#[msg("Invalid Rewards Distributor")]
18+
InvalidDistributor,
19+
#[msg("Math Overflow")]
20+
Overflow,
21+
#[msg("User Account Not Provided")]
22+
UserAccountNotProvided,
23+
#[msg("User Reward Account Not Provided")]
24+
UserRewardNotProvided,
25+
#[msg("User Reward Accounts Have Different Length to Reward Accounts")]
26+
RewardsUnalignedWithUserRewards,
27+
#[msg("Rewards Passed Do Not Equal the Amount of Rewards in the Pool")]
28+
PoolRewardsUnequalToRewardsPassed,
29+
#[msg("Order of Rewards Not Same As Pool")]
30+
MismatchedReward,
31+
#[msg("Mint doesn't Match Reward Mint")]
32+
InvalidMint,
33+
#[msg("Reward Period Is Still Active")]
34+
RewardsStillActive,
35+
#[msg("Pool Does Not Match User Account")]
36+
MismatchedUserPool,
37+
#[msg("Zero Reward Rate")]
38+
ZeroRewardRate,
39+
#[msg("Invalid Duration")]
40+
InvalidDuration,
41+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use anchor_lang::prelude::*;
2+
3+
#[event]
4+
pub struct InitializedPoolEvent {
5+
pub pool: Pubkey,
6+
pub mint: Pubkey,
7+
pub reward: Pubkey,
8+
pub distributor: Pubkey,
9+
pub staking_token_program: Pubkey,
10+
pub reward_token_program: Pubkey,
11+
pub duration: u64,
12+
pub timestamp: u64,
13+
pub mint_decimals: u8,
14+
pub reward_mint_decimals: u8,
15+
}
16+
17+
#[event]
18+
pub struct AddedRewardEvent {
19+
pub pool: Pubkey,
20+
pub contributor: Pubkey,
21+
pub reward_token_program: Pubkey,
22+
pub amount: u64,
23+
pub new_pool_reward_amount: u64,
24+
pub timestamp: u64,
25+
}
26+
27+
#[event]
28+
pub struct DepositEvent {
29+
pub pool: Pubkey,
30+
pub depositor: Pubkey,
31+
pub amount: u64,
32+
pub timestamp: u64,
33+
}
34+
35+
#[event]
36+
pub struct WithdrawEvent {
37+
pub pool: Pubkey,
38+
pub withdrawer: Pubkey,
39+
pub amount: u64,
40+
pub timestamp: u64,
41+
}
42+
43+
#[event]
44+
pub struct ClaimEvent {
45+
pub pool: Pubkey,
46+
pub recipient: Pubkey,
47+
pub amount: u64,
48+
pub timestamp: u64,
49+
}
50+
51+
#[event]
52+
pub struct SetDistributorEvent {
53+
pub pool: Pubkey,
54+
pub new_distributor: Pubkey,
55+
pub timestamp: u64,
56+
}
57+
58+
#[event]
59+
pub struct SetDurationEvent {
60+
pub pool: Pubkey,
61+
pub duration: u64,
62+
pub timestamp: u64,
63+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked, transfer_checked};
3+
use crate::{constants::{SEED_POOL, SEED_REWARD_TOKEN_ACCOUNT}, events::AddedRewardEvent, state::Pool, utils::update_reward};
4+
use crate::errors::ErrorCode;
5+
6+
#[derive(Accounts)]
7+
pub struct AddReward<'info> {
8+
#[account(mut)]
9+
signer: Signer<'info>,
10+
11+
pub mint: InterfaceAccount<'info, Mint>,
12+
#[account(
13+
mint::token_program = reward_token_program,
14+
)]
15+
pub reward_mint: InterfaceAccount<'info, Mint>,
16+
17+
#[account(
18+
mut,
19+
seeds = [SEED_POOL, mint.key().as_ref(), reward_mint.key().as_ref()],
20+
bump = pool.bump,
21+
)]
22+
pub pool: Account<'info, Pool>,
23+
24+
#[account(
25+
init_if_needed,
26+
payer = signer,
27+
seeds = [SEED_REWARD_TOKEN_ACCOUNT, pool.key().as_ref()],
28+
token::mint = reward_mint,
29+
token::authority = pool_reward_token_account,
30+
token::token_program = reward_token_program,
31+
bump,
32+
)]
33+
pub pool_reward_token_account: InterfaceAccount<'info, TokenAccount>,
34+
35+
#[account(
36+
mut,
37+
associated_token::mint = reward_mint,
38+
associated_token::authority = signer,
39+
associated_token::token_program = reward_token_program,
40+
)]
41+
pub user_reward_token_account: InterfaceAccount<'info, TokenAccount>,
42+
43+
pub reward_token_program: Interface<'info, TokenInterface>,
44+
pub system_program: Program<'info, System>,
45+
}
46+
47+
pub fn process_add_reward(ctx: Context<AddReward>, amount: u64) -> Result<()> {
48+
require!(amount > 0, ErrorCode::ZeroAmount);
49+
50+
let pool = &mut ctx.accounts.pool;
51+
52+
require!(pool.mint == ctx.accounts.mint.key(), ErrorCode::InvalidMint);
53+
54+
let current_timestamp = Clock::get()?.unix_timestamp as u64;
55+
56+
require!( ctx.accounts.signer.key() == pool.distributor, ErrorCode::NotDistributor);
57+
58+
// Initializing pool_reward_token_account if not initialized
59+
if pool.reward_token_account == Pubkey::default() {
60+
pool.reward_token_account = ctx.accounts.pool_reward_token_account.key();
61+
pool.bump_reward_token_account = ctx.bumps.pool_reward_token_account
62+
}
63+
64+
update_reward(pool, None)?;
65+
66+
if current_timestamp >= pool.period_finish {
67+
pool.reward_rate = amount.saturating_div(pool.duration);
68+
} else {
69+
let remaining: u64 = pool.period_finish.saturating_sub(current_timestamp);
70+
let leftover: u64 = remaining.saturating_mul(pool.reward_rate);
71+
pool.reward_rate = (amount.saturating_add(leftover)).saturating_div(pool.duration);
72+
}
73+
74+
require!(pool.reward_rate > 0, ErrorCode::ZeroRewardRate);
75+
76+
pool.last_updated = current_timestamp;
77+
pool.period_finish = current_timestamp.saturating_add(pool.duration);
78+
79+
// Do Transfer
80+
let transfer_cpi_accounts = TransferChecked {
81+
from: ctx.accounts.user_reward_token_account.to_account_info(),
82+
to: ctx.accounts.pool_reward_token_account.to_account_info(),
83+
authority: ctx.accounts.signer.to_account_info(),
84+
mint: ctx.accounts.reward_mint.to_account_info(),
85+
};
86+
87+
let cpi_program = ctx.accounts.reward_token_program.to_account_info();
88+
let cpi_ctx = CpiContext::new(cpi_program, transfer_cpi_accounts);
89+
90+
transfer_checked(cpi_ctx, amount, ctx.accounts.reward_mint.decimals)?;
91+
92+
let new_pool_reward_amount = pool.duration.saturating_mul(pool.reward_rate);
93+
94+
emit!(AddedRewardEvent { pool: pool.key(), contributor: ctx.accounts.signer.key(), reward_token_program: ctx.accounts.reward_token_program.key(), amount: amount, new_pool_reward_amount: new_pool_reward_amount, timestamp: current_timestamp });
95+
96+
Ok(())
97+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked, transfer_checked};
3+
use crate::{constants::{SEED_POOL, SEED_REWARD_TOKEN_ACCOUNT, SEED_USER}, events::ClaimEvent, state::{Pool, User}, utils::update_reward};
4+
use crate::errors::ErrorCode;
5+
6+
#[derive(Accounts)]
7+
pub struct Claim<'info> {
8+
#[account(mut)]
9+
pub signer: Signer<'info>,
10+
11+
pub mint: InterfaceAccount<'info, Mint>,
12+
pub reward_mint: InterfaceAccount<'info, Mint>,
13+
14+
#[account(
15+
mut,
16+
seeds = [SEED_POOL, mint.key().as_ref(), reward_mint.key().as_ref()],
17+
bump = pool.bump,
18+
)]
19+
pub pool: Account<'info, Pool>,
20+
21+
#[account(
22+
mut,
23+
seeds = [SEED_REWARD_TOKEN_ACCOUNT, pool.key().as_ref()],
24+
bump = pool.bump_reward_token_account,
25+
)]
26+
pub pool_reward_token_account: InterfaceAccount<'info, TokenAccount>,
27+
28+
#[account(
29+
mut,
30+
seeds = [SEED_USER, pool.key().as_ref(), signer.key().as_ref()],
31+
bump,
32+
)]
33+
pub user: Account<'info, User>,
34+
35+
#[account(
36+
mut, // Don't need to init as this is done during deposit
37+
associated_token::mint = reward_mint,
38+
associated_token::authority = signer,
39+
associated_token::token_program = reward_token_program,
40+
)]
41+
pub user_reward_token_account: InterfaceAccount<'info, TokenAccount>,
42+
43+
pub reward_token_program: Interface<'info, TokenInterface>,
44+
}
45+
46+
pub fn process_claim(ctx: Context<Claim>) -> Result<()> {
47+
let pool = &mut ctx.accounts.pool;
48+
let user = &mut ctx.accounts.user;
49+
50+
require!(pool.mint == ctx.accounts.mint.key(), ErrorCode::InvalidMint);
51+
52+
update_reward(pool, Some(user))?;
53+
54+
let cpi_program = ctx.accounts.reward_token_program.to_account_info();
55+
56+
let cpi_accounts = TransferChecked {
57+
from: ctx.accounts.pool_reward_token_account.to_account_info(),
58+
to: ctx.accounts.user_reward_token_account.to_account_info(),
59+
authority: ctx.accounts.pool_reward_token_account.to_account_info(),
60+
mint: ctx.accounts.reward_mint.to_account_info()
61+
};
62+
63+
let pool_key = pool.key();
64+
65+
// from account seeds + from account bump
66+
let signer_seeds: &[&[&[u8]]] = &[
67+
&[
68+
SEED_REWARD_TOKEN_ACCOUNT,
69+
pool_key.as_ref(),
70+
&[pool.bump_reward_token_account],
71+
]
72+
];
73+
74+
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
75+
76+
let amount = user.pending_payout;
77+
78+
transfer_checked(cpi_ctx, amount, ctx.accounts.reward_mint.decimals)?;
79+
80+
user.pending_payout = 0;
81+
82+
let timestamp = Clock::get()?.unix_timestamp as u64;
83+
84+
emit!(ClaimEvent { pool: pool.key(), recipient: ctx.accounts.signer.key(), amount: amount, timestamp: timestamp });
85+
86+
Ok(())
87+
}

0 commit comments

Comments
 (0)