This an SDK for the Staking Protocol. It allows user to stake some tokens in a Pool and claim rewards depending on the duration and weight of their stake.
The Protocol itself creates and interacts with several accounts that refer to different entities used in Staking:
StakePool
- account that represents a single Stake Pool to which users will be staking tokens of the specifiedmint
;RewardPool
- account that represents a single Pool that stores Reward tokens for a givenStakePool
;StakeEntry
- account that represents a deposit in aStakePool
, every stake of tokens creates a newStakeEntry
;
Features and Limitations
StakePool
supports weight and duration configuration:weight of the stake is a multiplier that will be applied to Rewards being given to a user when they claim.
weight of an individual stake can be increased if user stakes tokens for a longer duration (if
StakePool
configuration allows it);if
maxWeight
of theStakePool
is set to1_000_000_000
(equals 1 on-chain) it means that all Stakes regardless of their duration will have the same weight;
user can not unstake tokens before stake duration has passed;
a single
StakePool
can have up to 10 Reward Pools, each Pool represents a separate mint for tokens that can be claimed as rewards for staking:on every claim user will claim all rewards available to them in the
RewardPool
(accounting for reward distribution if there are multiple stakers), meaning that the period of claiming should be controlled by periodic top up for eachRewardPool
;whenever user claims tokens, the current state of each
RewardPool
will be written toclaimedAmounts
of theStakeEntry
, so user won't be able to claim more tokens unless Reward Pool is topped up with more funds;
for every
StakePool
there is astakeMint
that is a PDA and represents a staking token that will be minted when user stakes their tokens into the Pool:minted tokens will account for user's weight of the stake;
these tokens may be used in SPL governance;
these tokens will be burnt when user calls
unstake
;
Usage
Initiate the client
Every interaction with a Staking Protocol should be done via StakingClient that provides higher level abstraction on top of anchor client generated by anchor-client-gen
.
import { Keypair, PublicKey, Connection } from "@solana/web3.js"; import { checkOrCreateAtaBatch, createAtaBatch, getStakeEntryPda, getStakePoolPda, signAndExecuteTransaction, sleep, STAKING_PROGRAM_ID, StakingClient } from "staking"; const url = 'CLUSTER_URL' const client = new StakingClient( { clusterUrl: url, cluster: "devnet" } );
Create Staking Pool
Use create
method to create a staking pool:
const { stakePool, txId } = await client.create( { mint: MINT, nonce: NONCE, maxWeight: new BN(1_000_000_000), minDuration: new BN(3600), maxDuration: new BN(3600) }, { invoker: sender } );
Where:
mint
- mint address of tokens to be staked;nonce
- nonce value, it will be used to derive Staking Pool PDA, PDA consists of invoker+nonce+mint, so for every new Staking Pool created from the same wallet for the same mint should be done with a differentnonce
value;maxWeight
- max weight of the Stake, weight is increased depending on the duration of the vesting, should be more than1_000_000_000
-1_000_000_000
stands for 1, we use extra 9 zeroes for precision for on-chain calculations;minDuration
/maxDuration
- min and max duration of Staking in seconds;when staking tokens user will be able to set
duration
of the staking, it should be inmin/max
range;users won't be able to unstake until
duration
has passed;depending on weight/duration configuration (if
maxWeight
is more than1
andminDuration
!==maxDuration
) user may receive more rewards if they stake for a longer time;
invoker
- Keypair/Wallet of the sender
Add Reward Pool
Reward Pool stores reward tokens that users will be able to claim if they stake tokens. Each StakePool
can have up to 10 Reward Pools.
// In case you didn't store stakePool address you can derive it const stakePoolPubKey = getStakePoolPda(new PublicKey(STAKING_PROGRAM_ID["devnet"]), new PublicKey(MINT), sender.publicKey, NONCE); const { txId } = await client.addRewardPool({ stakePool: stakePoolPubKey.toString(), mint: MINT }, { invoker: sender });
Where:
stakePool
- address of the stake pool;mint
- reward mint token address;invoker
- Keypair/Wallet of the authority;
Top up a Reward Pool
Reward Pool points to an ATA of the StakePool
for the RewardPool
mint account, so you can top up a Reward Pool by transferring tokens like so:
import { TOKEN_PROGRAM_ID, createTransferCheckedInstruction, getMint, Mint, getAssociatedTokenAddressSync, getMultipleAccounts } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; const mintPubKey = new PublicKey(MINT); const stakePoolPubKey = getStakePoolPda(new PublicKey(STAKING_PROGRAM_ID["devnet"]), new PublicKey(MINT), sender.publicKey, NONCE); // As owner of the ATA is a PDA, we need to pass `true` flag to allow offCurve address deriving const recipientAta = getAssociatedTokenAddressSync(mintPubKey, stakePoolPubKey, true); let ixs = [ ...(await checkOrCreateAtaBatch(connection, [stakePoolPubKey], mintPubKey, sender)), createTransferCheckedInstruction( stakePoolPubKey, mintPubKey, recipientAta, sender.publicKey, BigInt(amount.toString()), MINT_DECIMALS ) ]; await prepareAndExecuteTransaction(connection, ixs, { invoker: sender }, {}, {});
Stake
const { txId } = await client.stake({ stakePool, amount: new BN(1_000_000_000), duration: new BN(3600), nonce: 0 }, { invoker: sender });
Where:
stakePool
- address of the stake pool;amount
- amount of the tokens to stake, RAW AMOUNT, meaning that it should account for how many decimals the MINT has;duration
- duration of the staking in seconds;nonce
- nonce value, it will be used to derive Deposit Receipt PDA, sender+stakePool+nonce should be unique;
Claim
Claiming is possible anytime after deposit has been made
await client.claim({ stakePool, stakeEntry: getStakeEntryPda(PROGRAM_ID, stakePoolPubKey, sender.publicKey, 0).toString() }, { invoker: sender });
Where:
stakePool
- address of the stake pool;stakeEntry
address of the account that stores metadata for the individual Stake, can be derived withgetStakeEntryPda
function that acceptsPublicKey of the Program
PublicKey of the StakePool
PublicKey of the Staker
Nonce value
Unstake
Unstaking is possible only after staking duration passed
await client.unstake({ stakePool, stakeEntry: getStakeEntryPda(PROGRAM_ID, stakePoolPubKey, sender.publicKey, 0).toString() }, { invoker: sender });
Where:
stakePool
- address of the stake pool;stakeEntry
address of the account that stores metadata for the individual Stake, can be derived withgetStakeEntryPda
function that acceptsPublicKey of the Program
PublicKey of the StakePool
PublicKey of the Staker
Nonce value
ATAs
Both Claim and Unstake expect all ATAs of the invoker
to be populated for Reward Pool distribution. ATAs can be created like so:
const stakePoolPubKey = getStakePoolPda(new PublicKey(STAKING_PROGRAM_ID["devnet"]), new PublicKey(MINT), sender.publicKey, NONCE); const stakePool = await client.getStakePool(stakePoolPubKey.toString()); const rewardPools = stakePool.rewardPools .map(({ rewardVault }) => rewardVault) .filter((rewardVault) => !rewardVault.equals(PublicKey.default)); const rewardPoolAccounts = await getMultipleAccounts(client.connection, rewardPools); await createAtaBatch(client.connection, rewardPoolAccounts.map((item) => ({ mint: item!.mint, owner: sender.publicKey })), { invoker: sender }, {}, {});
Querying
StakingClient
also exposes several methods to query on-chain data, stake pools and entries.
use
getStakePool
to fetch individualStakePool
by its address;use
getStakeEntries
to fetch entries for a specific pool owner by a specificowner
;
const stakePool = await client.getStakePool(stakePoolPubKey.toString()); const stakeEntries = await client.getStakeEntries({ stakePool: stakePoolPubKey.toString(), owner: sender.publicKey.toString() });
Something we didn't cover?
We've tried to cover as much as possible in this guide, but there is always room for improvement. If we missed something, or you'd like to simply share your ideas, love, and support, email us at [email protected]