Table of contents
๐บ Previously In Hell's Kitchen
This is part 2 of the staking program. We are still trying to achieve the core staking feature. By trying to implement the minting part, we learned how to chop and slice token mints, token bags, CPI signing, etc... Thanks to that, we will be able to move much faster now!
Remember, if you are an Etherean, ex etherean, or cyborg etherean-solanian ๐, please jump to the EVM Comparison and copy-paste the section somewhere, and keep it along with you as you follow the article. That section was separated on purpose as I am not sure how many of you have the double skills.
Let's do the second part of the staking. We now need to transfer $beef tokens from the user.
๐ DO THIS FIRST: Zoom In
Please, please CMD + +
, or view -> zoom in
at least two or three times in your browser. Unfortunately, hashnode keeps the reading area ridiculously small, even on huge screens, making the screenshots unreadable.
Or, right-click on the screenshots and open in the new tab.
I had previously added the full code in text directly in the article, but it was just impossible to follow given the length of the code! So I decided to go for side-by-side screenshots instead. Also because of accounts, it's way better to see side by side, what your program API expects and what accounts you need to prepare on the client side.
Maybe in the future, I will consider using another platform. I am thinking about a two-view or three-view side by side to look at rust, js, and diagram at the same time.
๐จ๐ปโ๐ณ Tonight The Chef Propose
Humans have two faces, and Solanians have three.
Remember, to fully understand how a Solana program works. We better try to look at it from different glasses ๐:
- The deployment part.
- The client-side.
- The program itself.
1 Pair-Progra-Cooking The Minting Feature:
This was done in part 1.
2 Completing the staking with the $beef transfer:
In this episode, we will make users pay $beef!
3. Unstake:
Finally, we will learn how to do the inverse operation of staking.
๐ญ TL;DR - Github
We are still looking at the same repo, the code in this article is not exhaustive. Instead, it will illustrate the important pieces so that you develop the mental model to build a DeFi program yourself in the future.
Please don't try to copy-paste any of the code here. It probably won't compile. I have reduced the noise on purpose. However, the complete code is available here. Feel free to look at it along with the article or clone it locally and try it.
๐ฎ Transfering Beef From Users
๐ฅ Fourth Ingredient - Airdrop ๐ง
Got some beef?
Previously we have already created the๐ฎ token mint. Our tokens now exist in the blockchain (at least in our local ledger). It is not time to work on our program yet. Why? Our staker program takes $beef tokens and rewards our users with stake tokens. But how do users get $beef in the first place?
Have you ever noticed that almost all DeFi apps have a swap feature on the home page? So if we were to build a complete DeFi app, users would start by swapping their $sol for ๐ฎ tokens. Then they would be able to stake their ๐ฎ tokens.
Airdrop
Since we are in Solana, users usually start with $sol, so when they go to our Staking or DEX application, users would first need to swap their $sol for another crypto token. To simplify this article and make testing easier, we will just airdrop the ๐ฎ tokens directly to users.
For our tests, we are using ourselves as guinea pig users. So you or I will be the one receiving the ๐ฎ tokens.
This part is not crucial to the article. However, if you want, look at
scripts/airdrop-beef.ts
to understand how to do it.
So, we usually would airdrop $beef to users before they arrive in our application. So, let's do that in the tests:
import { airdropBeef } from "../scripts/airdrop-beef";
describe("staker program", () => {
before(async () => {
await createMints();
await airdropBeef();
});
it('Swap $๐ฎ for $๐ฅฉ', async () => {
...
}
}
๐ Achievement: Airdroping ๐ฎ
- Users now have ๐ฎ in their wallets.
- All the preparation to finish the stake function is now done!
๐ฎ๐ฐ Your Program Also Wants Gucci
๐ช From now on, we don't need to do additional deployment stuff. We can solely focus on our staker program.
One more thing before we let users send ๐ฎ to us, we need a beef token bag for our program. As users need token bags to hold tokens, programs also need token bags. So let's create one for our program. Since the program will own the token bag account, we will be using a Program Derived Address mapped to the address of the beef mint.
Rust
On the left side, the implementation is virtually empty, because thanks to Anchor we can do all the work with the macros when defining Context<CreateBeefTokenBag
.
The program token bag will be created with the CreateBeefTokenBag
instruction:
- We are creating an account from a PDA, you know this already!
- This time, the
bump
is not necessary.
Deployment - Creating The Bag Account
- Left side: remember, with Solana, we need to prepare the addresses of the accounts ahead of time and feed them to our program. PDAs are "found" instead of created, so let's find that address.
- Right side: we create an account with that program-derived address, all the other accounts are just dependencies of that
program_beef_token_bag
.
Let's look at the left side:
- This time, the
bump
is not necessary. payer
: Solana wonders: " and who is gonna pay for that token bag account space?"The rest are required by Token Program, as we saw when we defined the
Context<CreateBeefTokenBag>.
In real life (what is real life?), you would actually do this in an Anchor deployment script.
Run the test๐ anchor test
:
โ It creates the program ๐ฎ๐ฐ beef token bag (615ms)
โ Swap $๐ฎ for $๐ฅฉ (1701ms)
2 passing (7s)
๐ Achievement:
- We created a token bag for our program to receive ๐ฎ from the user.
- Our program can now receive and store ๐ฎ beef tokens.
๐ An Attempt To Transfer
Similarly to what we did for the mint instruction with the token program, let's see what the transfer looks like:
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: // from which token bag?
authority: // do you have the authority to withdraw from โฌ๏ธ ?
to: // to which token bag?
}
);
token::transfer(cpi_ctx, beef_amount)?;
from
: is the token bag to withdraw from, meaning the user ๐ฎ token bag.authority
: is the authority forfrom.
Solana wants to make sure we are not stealing from the users without their consent!to
: is the program token bag we will need to create below.
๐ No need for a checklist this time. We already have all the ingredients necessary. We will complete all these arguments at once!
๐ฎ Withdrawing $Beef From Users
Rust - token::Transfer
The left side is our implementation and the right side is the Context<Stake>
accounts we need to define:
program_beef_token_bag
: you know the dance now, it's a PDA seeded with a mint address.- (right), note the additional
#[instruction(stake_mint_authority_bump: u8, program_beef_bag_bump: u8)]
. - (left), and also in the function:
stake(ctx: Context<Stake>, stake_mint_authority_bump: u8, program_beef_bag_bump: u8)
Shall We... Test?
*Later, if you are curious about the helpers, look at the
scripts
folder.
Run the anchor test
:
๐ฎ beef Mint Address: AXyTBL1C48WEdpzpY4bc...
๐ฅฉ๏ธ stake Mint Address: 9FgzyMYYiQew42BdVjs...
๐ฎ Token Account ๐ฐ'8rn1qnW1QivKinta8rmDHsyV...' balance: 1000000000
โ It creates the program ๐ฎ๐ฐ beef token bag (552ms)
โ Swap $๐ฎ for $๐ฅฉ (2280ms)
2 passing (8s)
Checkpoint code in this branch: github.com/mwrites/solana-staker/tree/featu...
๐ Achievement: Transfer
๐๐๐ Phew... We finally put all the pieces together, the staking feature finally works!!!
- We had to get a little help from the airdrop function to get users some ๐ฎ.
- Once users had ๐ฎ, we noticed that the program also needed a ๐ฎ token bag to store SPL tokens.
- After that, we were already familiar with all the ๐ฅ previous ingredients, mint, PDA, and token bags, so we could finish it in one straight line.
๐๐๐ Huge job on getting to this checkpoint!!! We are basically done. There are no more ingredients or detours to learn about. The rest is just finishing the job.
๐ Final Lap - Unstake / Redeem
Now, we need to do all of this but in reverse. So what does the Unstake / Redeem of ๐ฅฉ actually do?
It should not mint but burn the received ๐ฅฉ.
It should transfer back ๐ฎ to users.
๐ An Attempt to UnStake
Here's what the token::Burn
instruction for the Token Program looks like:
token::Burn {
mint: // what type of token is this?
to: // who is burning token?
authority: // who get the right to burn these?
},
๐ฅ Burning Users' $Stakes
- Left side: implementation.
- Right side: what kind of accounts the API expects.
Let's discuss token::Burn
(left side):
to
: I would have called it afrom
as in "token bag to burn from" instead, but basically, that's the token bag we want to burn.authority
: Solana wants to make sure the person who is unstaking also controls that token bag.
๐ค Refunding $Beef To Users
Rust-Side
For the transfer, it's pretty much the same thing we did for the stake but inversing the recipient and the destination. On the left side, we do the implementation and on the right side we define the Context<UnStake>
:
- The
Context<UnStake>
, is a little similar to the Stake's one but we are mostly interested about beef mint and beef bags this time.
Let's zoom in, on the signing, it's quite similar to what we did in fn stake()
just using beef_mint
and beef_token_bag
instead:
// PDA Signing: same as how we did in `fn stake()`
let stake_mint_address= ctx.accounts.beef_mint.key();
let seeds = &[beef_mint_address.as_ref(), &[program_beef_bag_bump]];
let signer = [&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer( // NEW
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.program_beef_token_bag.to_account_info(),
authority: ctx.accounts.program_beef_token_bag.to_account_info(),
to: ctx.accounts.user_beef_token_bag.to_account_info()
},
&signer
);
CpiContext::new_with_signer
: when we did the transfer call instake,
we needed the user's signature. Since the token comes from the vault, we need the program to sign this time.
Client-Side
We can look at the right side, to see what are the expected accounts. So that, on the left side, we prepare the addresses of the accounts and feed them to the program:
anchor tests
:
๐ฎ beef Mint Address: AXyTBL1C48WEdpzpY...
๐ฅฉ๏ธ stake Mint Address: 9FgzyMYYiQew42BdVjsK...
๐ฎ Token Account ๐ฐ'8rn1qnW1QivKinta8rmDH...' balance: 1000000000
โ It creates the program ๐ฎ๐ฐ beef token bag (531ms)
โ Swaps $๐ฎ for $๐ฅฉ (2090ms)
โ It redeems ๐ฅฉ for ๐ฎ (1557ms)
3 passing (8s)
Checkpoint code is in this branch: github.com/mwrites/solana-staker/tree/featu...
๐ฌ And Cut
Tremendous job on making it! ๐ช
Users can now stake and unstake tokens. The only remaining part is the math on how to distribute tokens. One easy solution is to just divide by the total supply, but other ways exist. I will let you figure out this part.
We started from a draft of the minting which led us to learn about several ingredients that we needed:
- Creating a Mint.
- Signing with a PDA.
- Associated Token Accounts.
These ๐ฅ ingredients will be the foundation of your core skills which you can use to make new recipes apps!
Learning how to prepare and chop these ingredients was the most challenging part, but after mastering these, we were able to quickly unroll the rest, the transfer, and the redeeming feature.
๐ Going further, the front-end is basically done. You just have to take the js code from the tests and let users connect their wallets with a wallet-adapter. Not sure how to do it?
- Take a look at this front-end walkthrough. You also might want to add the swap feature and avoid the awkward $beef airdrop we did.
- Or you can try to let users stake $sol instead of ๐ฎ, try to implement it, and see what's different about staking $sol.
- Also, you might want to name your token by adding token, Jacob Creech explains how to use the metaplex token metadata standard
Grab a coffee, a beer, water, look at the sunshine take a breath, pat yourself, look at how handsome or pretty you are in the mirror ๐คฉ. Then, come back for the next sections below!
๐ Review & EVM Comparison
The Consequence Of Accounts
Comparing with the solidity version. You might notice that the Solana version is much more involved. If we can resume it in one word, that word is Accounts. You might have seen the phrase " Solana programs are stateless". It took me a while to really, I mean, really understand what this involves. Basically, it means programs are dumb!
So, programs don't know anything. They are just machine processing data. So when you want to talk to programs, you want them to process something. But they have no idea what data you are talking about, so because of that, you need to always provide everything to these processors:
- The first consequence of this is that data (accounts) need to be provided with each instruction, which makes the code longer to write.
- The second consequence is that because accounts are independent of programs, they need to be signed for access control, which again makes the code longer to write.
It's not a program->accounts, it's program->accounts->signer
Because of these two reasons, accounts introduce a new depth. For example, when you want to talk to a program, you want to give an account and not only the account but also the account's signer. So whenever you want to do something, you first need to get the accounts and make sure you have the appropriate signing in place. Then, finally, you can do something with the account.
ERC20 Contracts
The equivalent to ERC20 contracts in Solana is SPL Tokens. However, SPL Tokens are not smart contracts but accounts. So instead of creating a new smart contract (program), we register a new account that defines our token with the SPL Token Program, the centralized authority for managing tokens.
Associated Token Accounts or Token Bags
While in EVM, token balance is handled by the ERC20 smart contract, it is not managed by a program in Solana. Indeed, the token balance lives in something like a token bag, and that token bag is owned by the user, not the system or your smart contract!
PDA Signing
Since accounts live outside programs, signing is used to determine who has control of an account. Sometimes though. You want only your program to own such an account. This is achieved by PDA Signing, it is pretty finicky, but you will get used to it with time.
Rent
Finally, we need to pay rent for the space accounts occupied in Solana. The rent is usually paid by the signer of the transaction. Because space needs to be paid, we are incentivized as developers to make accounts are small and granular as possible.
Going Further - How Does A Swap work?
By looking at the transaction scan, we can understand what is happening without even looking at the code. Here's an example of how ORCA does it:
explorer.solana.com/tx/3KzBwqLYRwxafSzB8ewD..
We see that for a swap, we also need to interact with the Token Program:
- Token Program
- Transfer
- Mint
- Transfer
As you now know, to receive tokens, you need to have the corresponding token bags first. In some cases, you will see that before the swap, there is the token bag creation:
- Associated Token Account Program - Create Associated Account
- SOL Transfer
- Allocate
- Assign
- Initialize account
- Token Program
- Transfer
- Mint
- Transfer
The swap code for orca is public and can be found here: github.com/orca-so/solana-program-library/b...
Going Further - Different Staking Model
Depending on the project, the staking mode might differ in how they structure the tokens.
- Input: step
- Output: xstep
- Reward = none
- Input: CRP
- Output: sCRP
- Reward: CRP
- Input: Ray
- Output: none
- Reward: Ray
Open Source Champions
This article would have never seen the light without these beautiful projects:
- The Solana Cookbook
- The Anchor Book
- Step Finance - Single Token Staking Github
- Project Serum Stake Example
References
- Intro to blockchains programming with Solana
- What is a Program Derived Address
- Solana Doc - Token Program
- Solana Doc - Associated Token Account Program
- Anchor Book - CPI
- Anchor Book - PDA Signing
- How to use the Metaplex Token Metadata Standard