Use Token Extensions in onchain programs
Summary
- The
Token Extensions Program
is a superset of theToken Program
with a different program id token_program
is an Anchor account constraint allowing you to verify an account belongs to a specific token program- Anchor introduced the concept of Interfaces to easily allow for programs to
support interaction with both
Token Program
andToken Extensions Program
Overview
The Token Extensions Program
is a program on Solana mainnet that provides
additional functionality to Solana tokens and mints. The
Token Extensions Program
is a superset of the Token Program
. Essentially it
is a byte for byte recreation with additional functionality tagged on at the
end. However they are sill separate programs. With two types of Token Programs,
we must anticipate being sent the program type in instructions.
In this lesson, you'll learn how to design your program to accept
Token Program
and Token Extensions Program
accounts using Anchor. You will
also learn how to interact with Token Extensions Program
accounts, identifying
which token program an account belongs to, and some differences between
Token Program
and the Token Extensions Program
onchain.
Difference between legacy Token Program and Token Extensions Program
We must clarify that the Token Extensions Program
is separate from the
original Token Program
. The Token Extensions Program
is a superset of the
original Token Program
, meaning all the instructions and functionality in the
original Token Program
come with the Token Extensions Program
.
Previously, one primary program (the Token Program
) was in charge of creating
accounts. As more and more use cases came to Solana, there was a need for new
token functionality. Historically, only way to add new token functionality was
to create a new type of token. A new token required its own program, and any
wallet or client that wanted to use this new token had to add specific logic to
support it. Fortunately the headache of supporting different types of tokens,
made this option not very popular. However, new functionality was still very
much needed, and the Token Extensions Program
was built to address this.
As mentioned before, the Token Extensions Program
is a strict superset of the
original token program and comes with all the previous functionality. The
Token Extensions Program
development team chose this approach to ensure
minimal disruption to users, wallets, and dApps while adding new functionality.
The Token Extensions Program
supports the same instruction set as the Token
program and is the same byte-for-byte throughout the very last instruction,
allowing existing programs to support Token Extensions
out of the box. However
this does not mean that Token Extensions Program
tokens and Token Program
tokens are interoperable - they are not. We'll have to handle each separately.
How to determine which program owns a particular token
With Anchor managing the two different token programs is pretty straight
forward. Now when we work with tokens within our programs we'll check the
token_program
constraint.
The two token programs ID
are as follows:
use spl_token::ID; // Token Program
use anchor_spl::token_2022::ID; // Token Extensions Program
To check for the regular Token Program
you'd use the following:
use spl_token::ID;
// verify given token/mint accounts belong to the spl-token program
#[account(
mint::token_program = ID,
)]
pub token_a_mint: Box>,
#[account(
token::token_program = ID,
)]
pub token_a_account: Box>,
You can do the same thing for the Token Extensions Program
, just with a
different ID.
use anchor_spl::token_2022::ID;
// verify given token/mint accounts belong to the Token Extension program
#[account(
mint::token_program = ID,
)]
pub token_a_mint: Box>,
#[account(
token::token_program = ID,
)]
pub token_a_account: Box>,
If a client passed in the wrong token program account, the instruction would
fail. However, this raises a problem, what if we want to support both
Token Program
and Token Extensions Program
? If we hardcode the check for the
program ID
, we'd need twice as many instructions. Fortunately, you can verify
that the token accounts passed into your program belong to a particular token
program. You would do this similarly to the previous examples. Instead of
passing in the static ID
of the token program, you check the given
token_program
.
// verify the given token and mint accounts match the given token_program
#[account(
mint::token_program = token_program,
)]
pub token_a_mint: Box>,
#[account(
token::token_program = token_program,
)]
pub token_a_account: Box>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
You can do the same thing with an associated token account by supplying a specific token program.
#[account(
associated_token::token_program = token_program
)]
pub associated_token: Box>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
If you'd like to check which token program a token account and mint belongs to
in your program logic, you can refer to the owner field on the AccountInfo
struct. The following code will log the owning program's ID. You could use this
field in a conditional to execute different logic for spl-token
and
Token Extensions Program
accounts.
msg!("Token Program Owner: {}", ctx.accounts.token_account.to_account_info().owner);
Anchor Interfaces
Interfaces are Anchor's newest feature that simplifies working with
Token Extensions
in a program. There are two relevant interface wrapper types
from the anchor_lang
crate:
And three corresponding Account Types from the anchor_spl
crate:
In the previous section, we defined the token_program
in our example as:
pub token_program: Interface<'info, token_interface::TokenInterface>,
This code makes use of Interface
and token_interface::TokenInterface
.
Interface
is a wrapper over the original Program
type, allowing multiple
possible program IDs. It's a type validating that the account is one of a set of
given programs. The Interface
type checks the following:
- If the given account is executable
- If the given account is one of a set of expected accounts from the given interface type
You must use the Interface
wrapper with a specific interface type. The
anchor_lang
and anchor_spl
crates provide the following Interface
type of
out the box:
TokenInterface
provides an interface type that expects the pubkey of the
account passed in to match either spl_token::ID
or spl_token_2022::ID
. These
program IDs are hard coded on the TokenInterface
type in Anchor.
static IDS: [Pubkey; 2] = [spl_token::ID, spl_token_2022::ID];
#[derive(Clone)]
pub struct TokenInterface;
impl anchor_lang::Ids for TokenInterface {
fn ids() -> &'static [Pubkey] {
&IDS
}
}
Anchor checks that the ID of the account passed in matches one of the two IDs
above. If the given account does not match either of these two, Anchor will
throw an InvalidProgramId
error and prevent the transaction from executing.
impl<T: Ids> CheckId for T {
fn check_id(id: &Pubkey) -> Result<()> {
if !Self::ids().contains(id) {
Err(error::Error::from(error::ErrorCode::InvalidProgramId).with_account_name(*id))
} else {
Ok(())
}
}
}
.
.
.
impl<'a, T: CheckId> TryFrom<&'a AccountInfo<'a>> for Interface<'a, T> {
type Error = Error;
/// Deserializes the given `info` into a `Program`.
fn try_from(info: &'a AccountInfo<'a>) -> Result<Self> {
T::check_id(info.key)?;
if !info.executable {
return Err(ErrorCode::InvalidProgramExecutable.into());
}
Ok(Self::new(info))
}
}
The InterfaceAccount
type is similar to the Interface
type in that it is
also a wrapper, this time around AccountInfo
. InterfaceAccount
is used on
accounts; it verifies program ownership and deserializes the underlying data
into a Rust type. This lesson will focus on using the InterfaceAccount
on
token and mint accounts. We can use the InterfaceAccount
wrapper with the
Mint
or TokenAccount
types from the anchor_spl::token_interface
crate we
mentioned. Here is an example:
use {
anchor_lang::prelude::*,
anchor_spl::{token_interface},
};
#[derive(Accounts)]
pub struct Example<'info>{
// Token account
#[account(
token::token_program = token_program
)]
pub token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
// Mint account
#[account(
mut,
mint::token_program = token_program
)]
pub mint_account: InterfaceAccount<'info, token_interface::Mint>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
}
If you're familiar with Anchor, then you may notice the TokenAccount
and
Mint
account types are not new. Although what is new is how they work with the
InterfaceAccount
wrapper. The InterfaceAccount
wrapper allows for either
Token Program
or Token Extensions Program
accounts to be passed in and
deserialized, just like the Interface
and the TokenInterface
types. These
wrappers and account types work together to provide a smooth and
straight-forward experience for developers, giving you the flexibility to
interact with both Token Program
and the Token Extensions Program
in your
program.
However, you cannot use any of these types from the token_interface
module
with the regular Anchor Program
and Account
wrappers. These new types are
used with either the Interface
or InterfaceAccount
wrappers. For example,
the following would not be valid, and any transactions sent to an instruction
using this account deserialization would return an error.
// This is invalid, using as an example.
// Cannot wrap Account over a token_interface::* type.
pub token_account: Account<'info, token_interface::TokenAccount>
Lab
Now let's get some hands-on experience with the Token Extensions Program
onchain by implementing a generalized token staking program that will accept
both Token Program
and Token Extensions Program
accounts. As far as staking
programs go, this will be a simple implementation with the following design:
- We'll create a stake pool account to hold all the staked tokens. There will only be one staking pool for a given token. The program will own the account.
- Every stake pool will have a state account that will hold information regarding the amount of tokens staked in the pool, etc.
- Users can stake as many tokens as they like, transferring them from their token account to the stake pool.
- Each user will have a state account created for each pool they stake in. This state account will keep track of how many tokens they have staked in this pool, when they last staked, etc.
- Users will be minted staking reward tokens upon unstaking. There is no separate claim process required.
- We'll determine a user's staking rewards using a simple algorithm.
- The program will accept both
Token Program
andToken Extensions Program
accounts.
The program will have four instructions: init_pool
, init_stake_entry
,
stake
, unstake
.
This lab will utilize a lot of Anchor and Solana APIs that have been covered previously in this course. We will not spend time explaining some of the concepts we expect you to know. With that said, let's get started.
1. Verify Solana/Anchor/Rust Versions
We will be interacting with the Token Extension
program in this lab and that
requires you have solana cli version ≥ 1.18.0
.
To check your version run:
solana --version
If the version printed out after running solana --version
is less than
1.18.0
then you can update the
cli version manually. Note, at the
time of writing this, you cannot simply run the solana-install update
command.
This command will not update the CLI to the correct version for us, so we have
to explicitly download version 1.18.0
. You can do so with the following
command:
solana-install init 1.18.0
If you run into the following error at any point attempting to build the program, that likely means you do not have the correct version of the Solana CLI installed.
anchor build
error: package `solana-program v1.18.0` cannot be built because it requires rustc 1.72.0 or newer, while the currently active rustc version is 1.68.0-dev
Either upgrade to rustc 1.72.0 or newer, or use
cargo update -p [email protected] --precise ver
where `ver` is the latest version of `solana-program` supporting rustc 1.68.0-dev
You will also want the latest version of the Anchor CLI installed. You can follow along the steps listed here to update via avm https://www.anchor-lang.com/docs/avm or simply run:
avm install latest
avm use latest
At the time of writing, the latest version of the Anchor CLI is 0.29.0
Now, we can check our Rust version.
rustc --version
At the time of writing, version 1.26.0
was used for the Rust compiler. If you
would like to update, you can do so via rustup
https://doc.rust-lang.org/book/ch01-01-installation.html
rustup update
Now, we should have all the correct versions installed.
2. Get starter code and add dependencies
Let's grab the starter branch.
git clone https://github.com/Unboxed-Software/token22-staking
cd token22-staking
git checkout starter
3. Update Program ID and Anchor Keypair
Once in the starter branch, run anchor keys list
to get your program ID.
Copy and paste this program ID in the Anchor.toml
file:
// in Anchor.toml
[programs.localnet]
token_22_staking = "<YOUR-PROGRAM-ID-HERE>"
And in the programs/token-22-staking/src/lib.rs
file:
declare_id!("<YOUR-PROGRAM-ID-HERE>");
Lastly set your developer keypair path in Anchor.toml
.
[provider]
cluster = "Localnet"
wallet = "/YOUR/PATH/HERE/id.json"
If you don't know what your current keypair path is you can always run the Solana cli to find out.
solana config get
4. Confirm the program builds
Let's build the starter code to confirm we have everything configured correctly. If it does not build, please revisit the steps above.
anchor build
You can safely ignore the warnings of the build script, these will go away as we add in the necessary code.
Feel free to run the provided tests to make sure the rest of the development
environment is set up correctly. You'll have to install the node dependencies
using npm
or yarn
. The tests should run, but they'll all fail until we have
completed our program.
yarn install
anchor test
5. Explore program design
Now that we have confirmed the program builds, let's take a look at the layout
of the program. You'll notice inside /programs/token22-staking/src
there are a
few different files:
lib.rs
error.rs
state.rs
utils.rs
The errors.rs
and utils.rs
files are already filled out for you. errors.rs
is where we have defined our custom errors for our program. To do this, you just
have to create a public enum
and define each error.
utils.rs
is a file that only contains one function called
check_token_program
. This is just a file where you can write helper functions
if you have the need. This function was written ahead of time and will be used
in our program to simply log the specific token program that was passed in the
instruction. We will be using both Token Extensions Program
and spl-token
in
this program, so this function will help clarify that distinction.
lib.rs
is the entrypoint to our program, as is the common practice in all
Solana programs. Here we define our program ID using the declare_id
Anchor
macro and the public token_22_staking
module. This module is where we define
our publicly callable instructions, these can be thought of as our program's
API.
We have four separate instructions defined here:
init_pool
init_stake_entry
stake
unstake
Each of these instructions makes a call to a handler
method that is defined
elsewhere. We do this to modularize the program, which helps keep the program
organized. This is generally a good idea when working with larger programs.
Each of these specific handler
methods are defined in their own file in the
instructions
directory. You'll notice there is a file corresponding to each
instruction, as well as an additional mod.rs
file. Each of these instruction
files is where we will write the logic for each individual instruction. The
mod.rs
file is what makes these handler
methods callable from the lib.rs
file.
6. Implement state.rs
Open up the /src/state.rs
file. Here, we will define some state data
structures and a few constants that we will need throughout our program. Let's
start by bringing in the packages we'll need here.
use {
anchor_lang::prelude::*,
solana_program::{pubkey::Pubkey},
};
Next, we we will need a handful of seeds defined that will be referenced throughout the program. These seeds will be used to derive different PDAs our program will expect to receive.
pub const STAKE_POOL_STATE_SEED: &str = "state";
pub const VAULT_SEED: &str = "vault";
pub const VAULT_AUTH_SEED: &str = "vault_authority";
pub const STAKE_ENTRY_SEED: &str = "stake_entry";
Now, we'll define two data structs that will define the data of two different
accounts our program will use to hold state. The PoolState
and StakeEntry
accounts.
The PoolState
account is meant to hold information about a specific staking
pool.
#[account]
pub struct PoolState {
pub bump: u8,
pub amount: u64,
pub token_mint: Pubkey,
pub staking_token_mint: Pubkey,
pub staking_token_mint_bump: u8,
pub vault_bump: u8,
pub vault_auth_bump: u8,
pub vault_authority: Pubkey,
}
The StakeEntry
account will hold information about a specific user's stake in
that pool.
#[account]
pub struct StakeEntry {
pub user: Pubkey,
pub user_stake_token_account: Pubkey,
pub bump: u8,
pub balance: u64,
pub last_staked: i64,
}
7. init_pool
Instruction
Now that we understand our program's architecture, let's get started with the
first instruction init_pool
.
Open init_pool.rs
and you should see the following:
use {
anchor_lang::prelude::*,
crate::{state::*, utils::*},
anchor_spl::{token_interface},
std::mem::size_of
};
pub fn handler(ctx: Context<InitializePool>) -> Result <()> {
check_token_program(ctx.accounts.token_program.key());
Ok(())
}
#[derive(Accounts)]
pub struct InitializePool<'info> {
pub token_program: Interface<'info, token_interface::TokenInterface>,
}
The handler
method is defined and so is the InitializePool
accounts struct.
The accounts struct simply expects to receive a token_program
account and
that's it. The handler
method calls the check_token_program
method that is
defined in the utils.rs
file. As it stands, this instruction does not really
do a whole lot.
To get started implementing the logic of this instruction, let's first think about the accounts that will be required. We will need the following to initialize a staking pool:
pool_authority
- PDA that is the authority over all staking pools. This will be a PDA derived with a specific seed.pool_state
- State account created in this instruction at a PDA. This account will hold state regarding this specific staking pool like the amount of tokens staked, how many users have staked, etc.token_mint
- The mint of tokens expected to be staked in this staking pool. There will be a unique staking pool for each token.token_vault
- Token account of the same mint astoken_mint
at a PDA. This is a token account with thepool_authority
PDA as the authority. This gives the program control over the token account. All tokens staked in this pool will be held in this token account.staking_token_mint
- The reward token mint for staking in this pool.payer
- Account responsible for paying for the creation of the staking pool.token_program
- The token program associated with the given token and mint accounts. Should work for either the Token Extension or the Token program.system_program
- System program.rent
- Rent program.
Let's implement this accounts struct starting with the pool_authority
account
and its constraints.
The pool_authority
account is a PDA derived with the VAULT_AUTH_SEED
that we
defined in the state.rs
file. This account does not hold any state, so we do
not need to deserialize it into any specific account structure. For this reason,
we use the UncheckedAccount
Anchor account type.
#[derive(Accounts)]
pub struct InitializePool<'info> {
/// CHECK: PDA, auth over all token vaults
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub pool_authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
}
Note that the UncheckedAccount
is considered unsafe by Anchor because Anchor
does not do any additional verification under the hood. However, this is okay
here because we do verify that the account is the expected PDA and we do not
read or write from the account. However, the /// CHECK:
comment is required
above an account utilizing the UncheckedAccount
or AccountInfo
structs.
Without that annotation, your program will throw the following error while
building:
Struct field "pool_authority" is unsafe, but is not documented.
Please add a `/// CHECK:` doc comment explaining why no checks through types are necessary.
See https://www.anchor-lang.com/docs/the-accounts-struct#safety-checks for more information.
Next, we'll define the pool_state
account.
This account utilizes the init
constraint, which indicates to Anchor that we
need to create the account. The account is expected to be a PDA derived with the
token_mint
account key and STAKE_POOL_STATE_SEED
as keys. payer
is
required to pay the rent required to create this account. We allocate enough
space for the account to store the PoolState
data struct that we defined in
the state.rs
file. Lastly, we use the Account
wrapper to deserialize the
given account into the PoolState
struct.
// pool state account
#[account(
init,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump,
payer = payer,
space = 8 + size_of::<PoolState>()
)]
pub pool_state: Account<'info, PoolState>,
Moving on to the token_mint
account.
We make use of two account constraints on this token_mint
account.
mint::token_program = <token_program>
verifies that the given account is a
mint created from the given <token_program>
. Before the Token Extensions
Program, this was not really a concern as there was only one token program. Now,
there are two! The reason we verify the token_mint
account belongs to the
given token_program
is because token accounts and mints of one program are not
compatible with token accounts and mints from the other program. So, for every
instruction in our program, we will be verifying that all the given token
accounts and mints belong to the same token_program
.
The second constraint mint::authority = payer
verifies that the authority over
the mint passed in is the payer
account, which will also be required to be a
signer. This may seem counterintuitive, but we do this because at the moment we
are inherently restricting the program to one staking pool per token due to the
PDA seeds we use for the pool_state
account. We also allow the creator of the
pool to define what the reward token mint is for staking in that pool. Because
the program currently limits one pool per token, we wouldn't want to allow just
anybody to create a staking pool for a token. This gives the creator of the pool
control over what the reward is for staking here. Imagine if we did not require
the mint::authority
, this would allow anyone to create the staking pool for
Token X
and define what the reward is for everyone that stakes Token X
with
this staking program. If they decide to define the reward token as the meme coin
FooBar
, then everyone would be stuck with that staking pool in this program.
For this reason, we will only allow the token_mint
authority to create a
staking pool for said token_mint
. This program design would probably not be a
good choice for the real world, it does not scale very well. But, it serves as a
great example to help get the points across in this lesson while keeping things
relatively simple. This can also serve as a good exercise in program design. How
would you design this program to make it more scalable for mainnet?
Lastly, we utilize the InterfaceAccount
struct to deserialize the given
account into token_interface::Mint
. The InterfaceAccount
type is a wrapper
around AccountInfo
that verifies program ownership and deserializes underlying
data into a given Rust type. Used with the token_interface::Mint
struct,
Anchor knows to deserialize this into a Mint account. The
token_interface::Mint
struct provides support for both Token Program
and
Token Extensions Program
mints out of the box! This interface concept was
created specifically for this use case. You can read more about the
InterfaceAccount
in the
anchor_lang
docs.
// Mint of token
#[account(
mint::token_program = token_program,
mint::authority = payer
)]
pub token_mint: InterfaceAccount<'info, token_interface::Mint>,
Looking at the pool_token_vault
where the tokens staked in this pool will be
held.
We initialize the token account with the init
constraint, create the token
account with mint = token_mint
, authority = pool_authority
, and
token_program
. This token account is created at a PDA using the token_mint
,
pool_authority
, and VAULT_SEED
as seeds. pool_authority
is assigned as
authority over this token account so that the program has control over it.
// pool token account for Token Mint
#[account(
init,
token::mint = token_mint,
token::authority = pool_authority,
token::token_program = token_program,
// use token_mint, pool auth, and constant as seeds for token a vault
seeds = [token_mint.key().as_ref(), pool_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump,
payer = payer,
)]
pub token_vault: InterfaceAccount<'info, token_interface::TokenAccount>,
Moving on to staking_token_mint
We just verify the mint belongs to the given token_program
. Again, we are
using InterfaceAccount
and token_interface::Mint
here.
// Mint of staking token
#[account(
mut,
mint::token_program = token_program
)]
pub staking_token_mint: InterfaceAccount<'info, token_interface::Mint>,
Lastly, we have a few familiar accounts.
// payer, will pay for creation of pool vault
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>
Take a look at the token_program
. This account uses the Interface
and
token_interface::TokenInterface
structs similar to the TokenInterface
and
mint/token structs we used earlier. This follows the same idea as those, the
Interface
and token_interface::TokenInterface
structs allow for either token
program to be passed in here. This is why we must verify that all of the token
and mint accounts passed in belong to the given token_program
.
Our accounts struct should look like this now:
#[derive(Accounts)]
pub struct InitializePool<'info> {
/// CHECK: PDA, auth over all token vaults
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub pool_authority: UncheckedAccount<'info>,
// pool state account
#[account(
init,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump,
payer = payer,
space = 8 + size_of::<PoolState>()
)]
pub pool_state: Account<'info, PoolState>,
// Mint of token
#[account(
mint::token_program = token_program,
mint::authority = payer
)]
pub token_mint: InterfaceAccount<'info, token_interface::Mint>,
// pool token account for Token Mint
#[account(
init,
token::mint = token_mint,
token::authority = pool_authority,
token::token_program = token_program,
// use token_mint, pool auth, and constant as seeds for token a vault
seeds = [token_mint.key().as_ref(), pool_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump,
payer = payer,
)]
pub token_vault: InterfaceAccount<'info, token_interface::TokenAccount>,
// Mint of staking token
#[account(
mut,
mint::token_program = token_program
)]
pub staking_token_mint: InterfaceAccount<'info, token_interface::Mint>,
// payer, will pay for creation of pool vault
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>
}
Setting up the account struct is the bulk of the logic for this instruction. All
we have to do inside the handler
function, is to initialize all of the
pool_state
fields.
The handler
function should be:
pub fn handler(ctx: Context<InitializePool>) -> Result <()> {
check_token_program(ctx.accounts.token_program.key());
// initialize pool state
let pool_state = &mut ctx.accounts.pool_state;
pool_state.bump = ctx.bumps.pool_state;
pool_state.amount = 0;
pool_state.vault_bump = ctx.bumps.token_vault;
pool_state.vault_auth_bump = ctx.bumps.pool_authority;
pool_state.token_mint = ctx.accounts.token_mint.key();
pool_state.staking_token_mint = ctx.accounts.staking_token_mint.key();
pool_state.vault_authority = ctx.accounts.pool_authority.key();
msg!("Staking pool created!");
Ok(())
}
After that, save your work and build to make sure there are no issues with your program at this point.
anchor build
8. init_stake_entry
Instruction
Now we can move on to the init_stake_entry.rs
file. This instruction creates a
staking account for a user to keep track of some state while they stake their
tokens. The StakeEntry
account is required to exist before a user can stake
tokens. The StakeEntry
account struct was defined in the state.rs
file
earlier.
Let's get started with the accounts required for this instruction. We will need the following:
user
- The user that is creating thestake_entry
account. This account must sign the transaction and will need to pay for the rent required to create thestake_entry
account.user_stake_entry
- State account that will be created at a PDA derived from the user, mint the staking pool was created for, and theSTAKE_ENTRY_SEED
as seeds.user_stake_token_account
- User's associated token account for the staking reward token.staking_token_mint
- Mint of the staking reward token of this pool.pool_state
-PoolState
account for this staking pool.token_program
- Token Program.associated_token_program
- Associated token program.system_program
- System Program.
Let's start by adding in the user
account to the InitializeStakeEntry
account struct.
It's necessary to verify that the user account has the authority to sign, indicating ownership, and is also changeable, as they are the payer of the transaction (which will mutate their balance).
#[derive(Accounts)]
pub struct InitializeStakeEntry<'info> {
#[account(mut)]
pub user: Signer<'info>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
}
The user_stake_entry
account requires a few more constraints. We need to
initialize the account, derive the address using the expected seeds, define who
is paying for the creation of the account, and allocate enough space for the
StakeEntry
data struct. We deserialize the given account into the StakeEntry
account.
#[account(
init,
seeds = [user.key().as_ref(), pool_state.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump,
payer = user,
space = 8 + size_of::<StakeEntry>()
)]
pub user_stake_entry: Account<'info, StakeEntry>,
The user_stake_token_account
is, again, the account where the user's staking
rewards will eventually be sent. We create the account in this instruction so we
don't have to worry about it later on when it's time to dole out the staking
rewards. Because we initialize this account in this instruction, it puts a limit
on the number of pools a user can stake in with the same reward token. This
current design would prevent a user from creating another user_stake_entry
account for another pool with the same staking_token_mint
. This is another
design choice that probably would not scale in production. Think about how else
this could be designed.
We use some similar Anchor SPL constraints as in the previous instruction, this
time targeting the associated token program. With the init
constraint, these
tell Anchor what mint, authority, and token program to use while initializing
this associated token account.
#[account(
init,
associated_token::mint = staking_token_mint,
associated_token::authority = user,
associated_token::token_program = token_program,
payer = user,
)]
pub user_stake_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
InterfaceAccount
and
token_interface::TokenAccount
types here. The token_interface::TokenAccount
type can only be used in conjunction with InterfaceAccount
.
Next, we add the staking_token_mint
account. Notice we are using our first
custom error here. This constraint verifies that the pubkey on the
staking_token_mint
account is equal to the pubkey stored in the
staking_token_mint
field of the given PoolState
account. This field was
initialized in the handler
method of the inti_pool
instruction in the
previous step.
#[account(
constraint = staking_token_mint.key() == pool_state.staking_token_mint
@ StakeError::InvalidStakingTokenMint,
mint::token_program = token_program
)]
pub staking_token_mint: InterfaceAccount<'info, token_interface::Mint>,
The pool_state
account is pretty much the same here as in the init_pool
instruction. However, in the init_pool
instruction we saved the bump used to
derive this account so we don't actually have to re-calculate it every time we
want to verify the PDA. We can conveniently call bump = pool_state.bump
and
this will use the bump stored in this account.
#[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>,
The remaining accounts are ones that we are familiar with already and there are not any special constraints on them.
pub token_program: Interface<'info, token_interface::TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>
The final InitializeStakeEntry
account struct should be:
#[derive(Accounts)]
pub struct InitializeStakeEntry<'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 = 8 + size_of::<StakeEntry>()
)]
pub user_stake_entry: Account<'info, StakeEntry>,
#[account(
init,
associated_token::mint = staking_token_mint,
associated_token::authority = user,
associated_token::token_program = token_program,
payer = user,
)]
pub user_stake_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
#[account(
constraint = staking_token_mint.key() == pool_state.staking_token_mint
@ StakeError::InvalidStakingTokenMint,
mint::token_program = token_program
)]
pub staking_token_mint: InterfaceAccount<'info, token_interface::Mint>,
#[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 token_program: Interface<'info, token_interface::TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>
}
The handler
method is also very straight-forward in this instruction. All we
need to is initialize the state of the newly created user_stake_entry
account.
pub fn handler(ctx: Context<InitializeStakeEntry>) -> Result<()> {
check_token_program(ctx.accounts.token_program.key());
// initialize user stake entry state
let user_entry = &mut ctx.accounts.user_stake_entry;
user_entry.user = ctx.accounts.user.key();
user_entry.user_stake_token_account = ctx.accounts.user_stake_token_account.key();
user_entry.bump = ctx.bumps.user_stake_entry;
user_entry.balance = 0;
Ok(())
}
Save your work and build to verify there are no compilation errors.
anchor build
9. stake
Instruction
The stake
instruction is what is called when users actually want to stake
their tokens. This instruction should transfer the amount of tokens the user
wants to stake from their token account to the pool vault account that is owned
by the program. There's a lot of validation in this instruction to prevent any
potentially malicious transactions from succeeding.
The accounts required are:
pool_state
- State account of the staking pool.token_mint
- Mint of the token being staked. This is required for the transfer.pool_authority
- PDA given authority over all staking pools.token_vault
- Token vault account where the tokens staked in this pool are held.user
- User attempting to stake tokens.user_token_account
- User owned token account where the tokens they would like to stake will be transferred from.user_stake_entry
- UserStakeEntry
account created in the previous instructiontoken_program
system_program
Again, let's build the Stake
account struct first.
First taking a look at the pool_state
account. This is the same account we
have used in previous instructions, derived with the same seeds and bump.
#[derive(Accounts)]
pub struct Stake<'info> {
// pool state account
#[account(
mut,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool_state.bump,
)]
pub pool_state: Account<'info, PoolState>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
}
Next, is the token_mint
which is required for the transfer CPI in this
instruction. This is the mint of the token that is being staked. We verify that
the given mint is of the given token_program
to make sure we are not mixing
any spl-token
and Token Extensions Program
accounts.
// Mint of token to stake
#[account(
mut,
mint::token_program = token_program
)]
pub token_mint: InterfaceAccount<'info, token_interface::Mint>,
The pool_authority
account is again the PDA that is the authority over all of
the staking pools.
/// CHECK: PDA, auth over all token vaults
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub pool_authority: UncheckedAccount<'info>,
Now we have the token_vault
which is where the tokens will be held while they
are staked. This account MUST be verified since this is where the tokens are
transferred to. Here, we verify the given account is the expected PDA derived
from the token_mint
, pool_authority
, and VAULT_SEED
seeds. We also verify
the token account belongs to the given token_program
. We use
InterfaceAccount
and token_interface::TokenAccount
here again to support
either spl-token
or Token Extensions Program
accounts.
// pool token account for Token Mint
#[account(
mut,
// use token_mint, pool auth, and constant as seeds for token a vault
seeds = [token_mint.key().as_ref(), pool_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool_state.vault_bump,
token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, token_interface::TokenAccount>,
The user
account is marked as mutable and must sign the transaction. They are
the ones initiating the transfer and they are the owner of the tokens being
transferred, so their signature is a requirement for the transfer to take place.
#[account(
mut,
constraint = user.key() == user_stake_entry.user
@ StakeError::InvalidUser
)]
pub user: Signer<'info>,
user_stake_entry
account. If it is not, our program will
throw the InvalidUser
custom error.
The user_token_account
is the token account where the tokens being transferred
to be staked should be currently held. The mint of this token account must match
the mint of the staking pool. If it does not, a custom InvalidMint
error will
be thrown. We also verify the given token account matches the given
token_program
.
#[account(
mut,
constraint = user_token_account.mint == pool_state.token_mint
@ StakeError::InvalidMint,
token::token_program = token_program
)]
pub user_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
The last three accounts are ones we are familiar with by now.
#[account(
mut,
seeds = [user.key().as_ref(), pool_state.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump = user_stake_entry.bump,
)]
pub user_stake_entry: Account<'info, StakeEntry>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
pub system_program: Program<'info, System>
The full Stake
accounts struct should look like:
#[derive(Accounts)]
pub struct Stake<'info> {
// pool state account
#[account(
mut,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool_state.bump,
)]
pub pool_state: Account<'info, PoolState>,
// Mint of token to stake
#[account(
mut,
mint::token_program = token_program
)]
pub token_mint: InterfaceAccount<'info, token_interface::Mint>,
/// CHECK: PDA, auth over all token vaults
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub pool_authority: UncheckedAccount<'info>,
// pool token account for Token Mint
#[account(
mut,
// use token_mint, pool auth, and constant as seeds for token a vault
seeds = [token_mint.key().as_ref(), pool_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool_state.vault_bump,
token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, token_interface::TokenAccount>,
#[account(
mut,
constraint = user.key() == user_stake_entry.user
@ StakeError::InvalidUser
)]
pub user: Signer<'info>,
#[account(
mut,
constraint = user_token_account.mint == pool_state.token_mint
@ StakeError::InvalidMint,
token::token_program = token_program
)]
pub user_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
#[account(
mut,
seeds = [user.key().as_ref(), pool_state.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump = user_stake_entry.bump,
)]
pub user_stake_entry: Account<'info, StakeEntry>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
pub system_program: Program<'info, System>
}
That is it for the accounts struct. Save your work and verify your program still compiles.
anchor build
Next, we are going to implement a helper function to assist with the transfer
CPI that we will have to make. We'll add the skeleton for the implementation of
a transfer_checked_ctx
method on our Stake
data struct. Below the Stake
accounts struct we just built, add the following:
impl<'info> Stake <'info> {
// transfer_checked for Token2022
pub fn transfer_checked_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
}
}
This method takes &self
as an argument, which gives us access to members of
the Stake
struct inside of the method by calling self
. This method is
expected to return a CpiContext
,
which is an Anchor primitive.
A CpiContext
is defined as:
pub struct CpiContext<'a, 'b, 'c, 'info, T>
where
T: ToAccountMetas + ToAccountInfos<'info>,
{
pub accounts: T,
pub remaining_accounts: Vec<AccountInfo<'info>>,
pub program: AccountInfo<'info>,
pub signer_seeds: &'a [&'b [&'c [u8]]],
}
Where T
is the accounts struct for the instruction you are invoking.
This is very similar to the Context
object that traditional Anchor
instructions expect as input (i.e. ctx: Context<Stake>
). This is the same
concept here, except we are defining one for a Cross-Program Invocation instead!
In our case, we will be invoking the transfer_checked
instruction in either
token programs, hence the transfer_checked_ctx
method name and the
TransferChecked
type in the returned CpiContext
. The regular transfer
instruction has been deprecated in the Token Extensions Program
and it is
suggested you use transfer_checked
going forward.
Now that we know what the goal of this method is, we can implement it! First, we
will need to define the program we will be invoking. This should be the
token_program
that was passed into our accounts struct.
impl<'info> Stake <'info> {
// transfer_checked for spl-token or Token2022
pub fn transfer_checked_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
let cpi_program = self.token_program.to_account_info();
}
}
Notice how we are simply able to reference the accounts in the Stake
data
struct by calling self
.
Then, we need to define the accounts we'll be passing in the CPI. We can do this
via the TransferChecked
data type, which we are importing from the
anchor_spl::token_2022
crate
at the top of our file. This data type is defined as:
pub struct TransferChecked<'info> {
pub from: AccountInfo<'info>,
pub mint: AccountInfo<'info>,
pub to: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}
This data type expects four different AccountInfo
objects, all of which should
have been passed into our program. Just like with the cpi_program
, we can
build this TransferChecked
data struct by referencing self
which gives us
access to all of the accounts defined in the Stake
data structure. Note, this
is only possible because transfer_checked_ctx
is being implemented on the
Stake
data type with this line impl<'info> Stake <'info>
. Without it, there
is no self to reference.
impl<'info> Stake <'info> {
// transfer_checked for spl-token or Token2022
pub fn transfer_checked_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked {
from: self.user_token_account.to_account_info(),
to: self.token_vault.to_account_info(),
authority: self.user.to_account_info(),
mint: self.token_mint.to_account_info()
};
}
}
So we have our cpi_program
and cpi_accounts
defined, but this method is
supposed to return a CpiContext
object. To do that, we simply need to pass
these two into the CpiContext
constructor CpiContext::new
.
impl<'info> Stake <'info> {
// transfer_checked for Token2022
pub fn transfer_checked_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked {
from: self.user_token_account.to_account_info(),
to: self.token_vault.to_account_info(),
authority: self.user.to_account_info(),
mint: self.token_mint.to_account_info()
};
CpiContext::new(cpi_program, cpi_accounts)
}
}
With this defined, we can call transfer_checked_ctx
at any point in our
handler
method and it will return a CpiContext
object that we can use to
execute a CPI.
Moving on to the handler
function, we'll need to do a couple of things here.
First, we need to use our transfer_checked_ctx
method to create the correct
CpiContext
and make the CPI. Then, we have some critical updates to make to
our two state accounts. As a reminder, we have two state accounts PoolState
and StakeEntry
. The former holds information regarding current state of the
overall staking pool, while the latter is in charge of keeping an accurate
recording of the a specific user's stake in a pool. With that in mind, any time
there is an update to the staking pool we should be updating both the
PoolState
and a given user's StakeEntry
accounts in some way.
For starters, let's implement the actual CPI. Since we defined the program and
accounts required for the CPI ahead of time in the transfer_checked_ctx()
method, the actual CPI is very straight-forward. We'll make use of another
helper function from the anchor_spl::token_2022
crate, specifically the
transfer_checked
function. This is
defined as the following:
pub fn transfer_checked<'info>(
ctx: CpiContext<'_, '_, '_, 'info, TransferChecked<'info>>,
amount: u64,
decimals: u8
) -> Result<()>
It takes three input parameters:
CpiContext
- amount
- decimals
The CpiContext
is exactly what is returned in our transfer_checked_ctx()
method, so for this first argument we can simply call the method with
ctx.accounts.transfer_checked_ctx()
.
The amount is simply the amount of tokens to transfer, which our handler
method expects as an input parameter.
Lastly, the decimals
argument is the amount of decimals on the token mint of
what is being transferred. This is a requirement of the transfer checked
instruction. Since the token_mint
account is passed in, you can actually fetch
the decimals on the token mint in this instruction. Then, we just pass that in
as the third argument.
All in all, it should look something like this:
pub fn handler(ctx: Context<Stake>, stake_amount: u64) -> Result <()> {
check_token_program(ctx.accounts.token_program.key());
msg!("Pool initial total: {}", ctx.accounts.pool_state.amount);
msg!("User entry initial balance: {}", ctx.accounts.user_stake_entry.balance);
let decimals = ctx.accounts.token_mint.decimals;
// transfer_checked for either spl-token or the Token Extension program
transfer_checked(ctx.accounts.transfer_checked_ctx(), stake_amount, decimals)?;
Ok(())
}
The transfer_checked
method builds a transfer_checked
instruction object and
actually invokes the program in the CpiContext
under the hood. We are just
utilizing Anchor's wrapper over the top of this process. If you're curious,
here is the source code.
pub fn transfer_checked<'info>(
ctx: CpiContext<'_, '_, '_, 'info, TransferChecked<'info>>,
amount: u64,
decimals: u8,
) -> Result<()> {
let ix = spl_token_2022::instruction::transfer_checked(
ctx.program.key,
ctx.accounts.from.key,
ctx.accounts.mint.key,
ctx.accounts.to.key,
ctx.accounts.authority.key,
&[],
amount,
decimals,
)?;
solana_program::program::invoke_signed(
&ix,
&[
ctx.accounts.from,
ctx.accounts.mint,
ctx.accounts.to,
ctx.accounts.authority,
],
ctx.signer_seeds,
)
.map_err(Into::into)
}
Using Anchor's CpiContext
wrapper is much cleaner and it abstracts a lot away,
but it's important you understand what's going on under the hood.
Once the transfer_checked
function has completed, we can start updating our
state accounts because that means the transfer has taken place. The two accounts
we'll want to update are the pool_state
and user_entry
accounts, which
represent the overall staking pool data and this specific user's data regarding
their stake in this pool.
Since this is the stake
instruction and the user is transferring tokens into
the pool, both values representing the amount the user has staked and the total
amount staked in the pool should increase by the stake_amount
.
To do this, we will deserialize the pool_state
and user_entry
accounts as
mutable and increase the pool_state.amount
and user_enry.balance
fields by
the stake_amount
using checked_add()
. CheckedAdd
is a Rust feature that
allows you to safely perform mathematical operations without worrying about
buffer overflow. checked_add()
adds two numbers, checking for overflow. If
overflow happens, None
is returned.
Lastly, we'll also update the user_entry.last_staked
field with the current
unix timestamp from the Clock
. This is just meant to keep track of the most
recent time a specific user staked tokens.
Add this after transfer_checked
and before Ok(())
in the handler
function.
let pool_state = &mut ctx.accounts.pool_state;
let user_entry = &mut ctx.accounts.user_stake_entry;
// update pool state amount
pool_state.amount = pool_state.amount.checked_add(stake_amount).unwrap();
msg!("Current pool stake total: {}", pool_state.amount);
// update user stake entry
user_entry.balance = user_entry.balance.checked_add(stake_amount).unwrap();
msg!("User stake balance: {}", user_entry.balance);
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
Now that was a lot and we covered some new stuff, so feel free to go back through and make sure it all makes sense. Check out all of the external resources that are linked for any of the new topics. Once you're ready to move on, save your work and verify the program still builds!
anchor build
10. unstake
Instruction
Lastly, the unstake
transaction will be pretty similar to the stake
transaction. We'll need to transfer tokens out of the stake pool to the user,
this is also when the user will receive their staking rewards. Their staking
rewards will be minted to the user in this same transaction.
Something to note here, we are not going to allow the user to determine how many
tokens are unstaked, we will simply unstake all of the tokens that they
currently have staked. Additionally, we are not going to implement a very
realistic algorithm to determine how many reward tokens they have accrued. We'll
simply take their stake balance and multiply by 10 to get the amount of reward
tokens to mint them. We do this again to simplify the program and remain focused
on the goal of the lesson, the Token Extensions Program
.
The account structure will be very similar to the stake
instruction, but there
are a few differences. We'll need:
pool_state
token_mint
pool_authority
token_vault
user
user_token_account
user_stake_entry
staking_token_mint
user_stake_token_account
token_program
system_program
The main difference between the required accounts in stake
and unstake
is
that we need the staking_token_mint
and user_stake_token_account
for this
instruction to mint the user their staking rewards. We won't cover each account
individually because the struct is the exact same as the previous instruction,
just with the addition of these two new accounts.
First, the staking_token_mint
account is the mint of the staking reward token.
The mint authority must be the pool_authority
PDA so that the program has the
ability to mint tokens to users. The given staking_token_mint
account also
must match the given token_program
. We'll add a custom constraint verifying
that this account matches the pubkey stored in the staking_token_mint
field of
the pool_state
account, if not we will return the custom
InvalidStakingTokenMint
error.
// Mint of staking token
#[account(
mut,
mint::authority = pool_authority,
mint::token_program = token_program,
constraint = staking_token_mint.key() == pool_state.staking_token_mint
@ StakeError::InvalidStakingTokenMint
)]
pub staking_token_mint: InterfaceAccount<'info, token_interface::Mint>,
The user_stake_token_account
follows a similar vein. It must match the mint
staking_token_mint
, the user
must be the authority since these are their
staking rewards, and this account must match what we have stored on the
user_stake_entry
account as their stake token account.
#[account(
mut,
token::mint = staking_token_mint,
token::authority = user,
token::token_program = token_program,
constraint = user_stake_token_account.key() == user_stake_entry.user_stake_token_account
@ StakeError::InvalidUserStakeTokenAccount
)]
pub user_stake_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
Here is what the final Unstake
struct should look like:
#[derive(Accounts)]
pub struct Unstake<'info> {
// pool state account
#[account(
mut,
seeds = [token_mint.key().as_ref(), STAKE_POOL_STATE_SEED.as_bytes()],
bump = pool_state.bump,
)]
pub pool_state: Account<'info, PoolState>,
// Mint of token
#[account(
mut,
mint::token_program = token_program
)]
pub token_mint: InterfaceAccount<'info, token_interface::Mint>,
/// CHECK: PDA, auth over all token vaults
#[account(
seeds = [VAULT_AUTH_SEED.as_bytes()],
bump
)]
pub pool_authority: UncheckedAccount<'info>,
// pool token account for Token Mint
#[account(
mut,
// use token_mint, pool auth, and constant as seeds for token a vault
seeds = [token_mint.key().as_ref(), pool_authority.key().as_ref(), VAULT_SEED.as_bytes()],
bump = pool_state.vault_bump,
token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, token_interface::TokenAccount>,
// require a signature because only the user should be able to unstake their tokens
#[account(
mut,
constraint = user.key() == user_stake_entry.user
@ StakeError::InvalidUser
)]
pub user: Signer<'info>,
#[account(
mut,
constraint = user_token_account.mint == pool_state.token_mint
@ StakeError::InvalidMint,
token::token_program = token_program
)]
pub user_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
#[account(
mut,
seeds = [user.key().as_ref(), pool_state.token_mint.key().as_ref(), STAKE_ENTRY_SEED.as_bytes()],
bump = user_stake_entry.bump,
)]
pub user_stake_entry: Account<'info, StakeEntry>,
// Mint of staking token
#[account(
mut,
mint::authority = pool_authority,
mint::token_program = token_program,
constraint = staking_token_mint.key() == pool_state.staking_token_mint
@ StakeError::InvalidStakingTokenMint
)]
pub staking_token_mint: InterfaceAccount<'info, token_interface::Mint>,
#[account(
mut,
token::mint = staking_token_mint,
token::authority = user,
token::token_program = token_program,
constraint = user_stake_token_account.key() == user_stake_entry.user_stake_token_account
@ StakeError::InvalidUserStakeTokenAccount
)]
pub user_stake_token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
pub system_program: Program<'info, System>
}
Now, we have two different CPIs to make in this instruction - a transfer and a
mint. We are going to be using a CpiContext
for both in this instruction as
well. There is a catch however, in the stake
instruction we did not require a
"signature" from a PDA but in this instruction we do. So, we cannot follow the
exact same pattern as before but we can do something very similar.
Again, let's create two skeleton helper functions implemented on the Unstake
data struct: transfer_checked_ctx
and mint_to_ctx
.
impl<'info> Unstake <'info> {
// transfer_checked for Token2022
pub fn transfer_checked_ctx<'a>(&'a self, seeds: &'a [&[&[u8]]]) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
}
// mint_to
pub fn mint_to_ctx<'a>(&'a self, seeds: &'a [&[&[u8]]]) -> CpiContext<'_, '_, '_, 'info, MintTo<'info>> {
}
}
We'll work on transfer_checked_ctx
first, the implementation of this method is
almost exactly the same as in the stake
instruction. The main difference is
here we have two arguments: self
and seeds
. The second argument will be the
vector of PDA signature seeds that we would normally pass into invoke_signed
ourselves. Since we need to sign with a PDA, instead of calling the
CpiContext::new
constructor, we'll call CpiContext::new_with_signer
instead.
new_with_signer
is defined as:
pub fn new_with_signer(
program: AccountInfo<'info>,
accounts: T,
signer_seeds: &'a [&'b [&'c [u8]]]
) -> Self
Additionally, the from
and to
accounts in our TransferChecked
struct will
be reversed from before.
// transfer_checked for spl-token or Token2022
pub fn transfer_checked_ctx<'a>(&'a self, seeds: &'a [&[&[u8]]]) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = TransferChecked {
from: self.token_vault.to_account_info(),
to: self.user_token_account.to_account_info(),
authority: self.pool_authority.to_account_info(),
mint: self.token_mint.to_account_info()
};
CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)
}
Check out the
anchor_lang
crate docs to learn more about CpiContext
.
Moving on to the mint_to_ctx
function, we need to do the exact same thing we
just did with transfer_checked_ctx
but target the mint_to
instruction
instead! To do this, we'll need to use the MintTo
struct instead of
TransferChecked
. MintTo
is defined as:
pub struct MintTo<'info> {
pub mint: AccountInfo<'info>,
pub to: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}
anchor_spl::token_2022::MintTo
rust crate docs.
With this in mind, we can implement mint_to_ctx
the same exact way we did
transfer_checked_ctx
. We'll be targeting the exact same token_program
with
this CPI, so cpi_program
should be the same as before. We construct the
MinTo
struct the same as we did the TransferChecked
struct, just passing the
appropriate accounts here. The mint
is the staking_token_mint
because that
is the mint we will be minting to the user, to
is the user's
user_stake_token_account
, and authority
is the pool_authority
because this
PDA should have sole authority over this mint.
Lastly, the function returns a CpiContext
object constructed using the signer
seeds passed into it.
// mint_to
pub fn mint_to_ctx<'a>(&'a self, seeds: &'a [&[&[u8]]]) -> CpiContext<'_, '_, '_, 'info, MintTo<'info>> {
let cpi_program = self.token_program.to_account_info();
let cpi_accounts = MintTo {
mint: self.staking_token_mint.to_account_info(),
to: self.user_stake_token_account.to_account_info(),
authority: self.pool_authority.to_account_info()
};
CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)
}
Now we can move on to the logic of our handler
function. This instruction will
need to update both the pool and user state accounts, transfer all of the user's
staked tokens, and mint the user their reward tokens. To get started, we are
going to log some info and determine how many tokens to transfer to the user.
We have kept track of the user's stake amount in the user_stake_entry
account,
so we know exactly how many tokens this user has staked at this point in time.
We can fetch this amount from the user_entry.balance
field. Then, we'll log
some information so that we can inspect this later. We'll also verify that the
amount to transfer out is not greater than the amount that is stored in the
pool as an extra safety measure. If so, we will return a custom OverdrawError
and prevent the user from draining the pool.
pub fn handler(ctx: Context<Unstake>) -> Result <()> {
check_token_program(ctx.accounts.token_program.key());
let user_entry = &ctx.accounts.user_stake_entry;
let amount = user_entry.balance;
let decimals = ctx.accounts.token_mint.decimals;
msg!("User stake balance: {}", user_entry.balance);
msg!("Withdrawing all of users stake balance. Tokens to withdraw: {}", amount);
msg!("Total staked before withdrawal: {}", ctx.accounts.pool_state.amount);
// verify user and pool have >= requested amount of tokens staked
if amount > ctx.accounts.pool_state.amount {
return Err(StakeError::OverdrawError.into())
}
// More code to come
Ok(())
}
Next, we will fetch the signer seeds needed for the PDA signature. The
pool_authority
is what will be required to sign in these CPIs, so we use that
account's seeds.
// 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[..]];
Once we have those seeds stored in the signer
variable, we can easily pass it
into the transfer_checked_ctx()
method. At the same time, we'll call the
transfer_checked
helper function from the Anchor crate to acually invoke the
CPI behind the scenes.
// transfer staked tokens
transfer_checked(ctx.accounts.transfer_checked_ctx(signer), amount, decimals)?;
Next, we'll calculate how many reward tokens to mint the user and invoke the
mint_to
instruction using our mint_to_ctx
function. Remember, we are just
taking the amount of tokens the user has staked and multiplying it by 10 to get
their reward amount. This is a very simple algorithm that would not make sense
to use in production, but it works here as an example.
Notice we use checked_mul()
here, similar to how we used checked_add
in the
stake
instruction. Again, this is to prevent buffer overflow.
// mint users staking rewards, 10x amount of staked tokens
let stake_rewards = amount.checked_mul(10).unwrap();
// mint rewards to user
mint_to(ctx.accounts.mint_to_ctx(signer), stake_rewards)?;
Lastly, we will need to update our state accounts by subtracting the amount that
was unstaked from both the pool and user's balances. We'll be using
checked_sub()
for this.
// borrow mutable references
let pool_state = &mut ctx.accounts.pool_state;
let user_entry = &mut ctx.accounts.user_stake_entry;
// subtract transferred amount from pool total
pool_state.amount = pool_state.amount.checked_sub(amount).unwrap();
msg!("Total staked after withdrawal: {}", pool_state.amount);
// update user stake entry
user_entry.balance = user_entry.balance.checked_sub(amount).unwrap();
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
Putting that all together gives us our final handler
function:
pub fn handler(ctx: Context<Unstake>) -> Result <()> {
check_token_program(ctx.accounts.token_program.key());
let user_entry = &ctx.accounts.user_stake_entry;
let amount = user_entry.balance;
let decimals = ctx.accounts.token_mint.decimals;
msg!("User stake balance: {}", user_entry.balance);
msg!("Withdrawing all of users stake balance. Tokens to withdraw: {}", amount);
msg!("Total staked before withdrawal: {}", ctx.accounts.pool_state.amount);
// verify user and pool have >= requested amount of tokens staked
if amount > ctx.accounts.pool_state.amount {
return Err(StakeError::OverdrawError.into())
}
// 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[..]];
// transfer staked tokens
transfer_checked(ctx.accounts.transfer_checked_ctx(signer), amount, decimals)?;
// mint users staking rewards, 10x amount of staked tokens
let stake_rewards = amount.checked_mul(10).unwrap();
// mint rewards to user
mint_to(ctx.accounts.mint_to_ctx(signer), stake_rewards)?;
// borrow mutable references
let pool_state = &mut ctx.accounts.pool_state;
let user_entry = &mut ctx.accounts.user_stake_entry;
// subtract transferred amount from pool total
pool_state.amount = pool_state.amount.checked_sub(amount).unwrap();
msg!("Total staked after withdrawal: {}", pool_state.amount);
// update user stake entry
user_entry.balance = user_entry.balance.checked_sub(amount).unwrap();
user_entry.last_staked = Clock::get().unwrap().unix_timestamp;
Ok(())
}
That is it for our staking program! There has been an entire test suite written ahead of time for you to run against this program. Go ahead and install the needed packages for testing and run the tests:
npm install
anchor test
If you run into problems feel free to checkout the solution branch.
Challenge
Create your own program that is Token Program and Token Extensions Program agnostic.