Summary
- The
Token Extensions Programis a superset of theToken Programwith a different program id token_programis 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 ProgramandToken Extensions Program
Overview
TheToken Extensions Program is a program on Nexis Native Chain mainnet that provides
additional functionality to Nexis Native Chain 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 theToken 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 Nexis Native Chain, 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 thetoken_program constraint.
The two token programs ID are as follows:
Token Program you’d use the following:
Token Extensions Program, just with a
different ID.
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.
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.
Anchor Interfaces
Interfaces are Anchor’s newest feature that simplifies working withToken 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:
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
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.
InvalidProgramId error and prevent the transaction from executing.
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:
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.
Lab
Now let’s get some hands-on experience with theToken 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 ProgramandToken Extensions Programaccounts.
init_pool, init_stake_entry,
stake, unstake.
This lab will utilize a lot of Anchor and Nexis Native Chain 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 Nexis Native Chain/Anchor/Rust Versions
We will be interacting with theToken Extension program in this lab and that
requires you have nexis cli version ≥ 1.18.0.
To check your version run:
nexis --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 nexis-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:
0.29.0
Now, we can check our Rust 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
2. Get starter code and add dependencies
Let’s grab the starter branch.3. Update Program ID and Anchor Keypair
Once in the starter branch, runanchor keys list to get your program ID.
Copy and paste this program ID in the Anchor.toml file:
programs/token-22-staking/src/lib.rs file:
Anchor.toml.
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.npm or yarn. The tests should run, but they’ll all fail until we have
completed our program.
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.rserror.rsstate.rsutils.rs
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
Nexis Native Chain 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_poolinit_stake_entrystakeunstake
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.
PoolState and StakeEntry
accounts.
The PoolState account is meant to hold information about a specific staking
pool.
StakeEntry account will hold information about a specific user’s stake in
that pool.
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:
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_mintat a PDA. This is a token account with thepool_authorityPDA 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.
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.
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:
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.
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.
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.
staking_token_mint
We just verify the mint belongs to the given token_program. Again, we are
using InterfaceAccount and token_interface::Mint here.
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:
handler function, is to initialize all of the
pool_state fields.
The handler function should be:
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_entryaccount. This account must sign the transaction and will need to pay for the rent required to create thestake_entryaccount.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_SEEDas 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-PoolStateaccount for this staking pool.token_program- Token Program.associated_token_program- Associated token program.system_program- System Program.
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).
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.
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.
We are using the
InterfaceAccount and
token_interface::TokenAccount types here. The token_interface::TokenAccount
type can only be used in conjunction with InterfaceAccount.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.
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.
InitializeStakeEntry account struct should be:
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.
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- UserStakeEntryaccount created in the previous instructiontoken_programsystem_program
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.
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.
pool_authority account is again the PDA that is the authority over all of
the staking pools.
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.
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.
We also verify that the given user is the same pubkey
stored in the given
user_stake_entry account. If it is not, our program will
throw the InvalidUser custom error. 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.
Stake accounts struct should look like:
transfer_checked_ctx method on our Stake data struct. Below the Stake
accounts struct we just built, add the following:
&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:
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.
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:
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.
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.
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:
CpiContext- amount
- decimals
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:
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.
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.
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_statetoken_mintpool_authoritytoken_vaultuseruser_token_accountuser_stake_entrystaking_token_mintuser_stake_token_accounttoken_programsystem_program
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.
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.
Unstake struct should look like:
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.
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:
from and to accounts in our TransferChecked struct will
be reversed from before.
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:
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.
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.
pool_authority is what will be required to sign in these CPIs, so we use that
account’s seeds.
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.
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.
checked_sub() for this.
handler function: