Skip to main content
Staking SDK
Updated over 3 months ago

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 specified mint;

  • RewardPool - account that represents a single Pool that stores Reward tokens for a given StakePool;

  • StakeEntry - account that represents a deposit in a StakePool, every stake of tokens creates a new StakeEntry;

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 the StakePool is set to 1_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 each RewardPool;

    • whenever user claims tokens, the current state of each RewardPool will be written to claimedAmounts of the StakeEntry, so user won't be able to claim more tokens unless Reward Pool is topped up with more funds;

  • for every StakePool there is a stakeMint 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 different nonce value;

  • maxWeight - max weight of the Stake, weight is increased depending on the duration of the vesting, should be more than 1_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 in min/max range;

    • users won't be able to unstake until duration has passed;

    • depending on weight/duration configuration (if maxWeight is more than 1 and minDuration !== 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 with getStakeEntryPda function that accepts

    • PublicKey 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 with getStakeEntryPda function that accepts

    • PublicKey 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 individual StakePool by its address;

  • use getStakeEntries to fetch entries for a specific pool owner by a specific owner;

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]

Did this answer your question?