Transfer tokens between accounts on Solana
Learn how to create a smart contract instruction on Solana that deposits tokens from one account to another
gm gm, I’m 0xksure and I write mostly about rust development and solana (usually with the anchor framework) development. I have a lot of experience in backend development, but have also done my fair share of frontend work and general sdk work using typescript. If you are interested in these topics then please check some of my previous writings
Transfer tokens between accounts on Solana
Transfer tokens between accounts on Solana
Mint tokens on Solana using the Rust SDK
Also, check out my daily takes on twitter
This post is basically a continuation of the previous post on how to mint tokens on Solana using the rust SDK. Please check it out if you want to set up tests using the solana_program_test crate.
If you want to jump straight into the code before reading the post the referenced github repo can be found here https://github.com/kristohberg/xbooth
Introduction
As part of the Solana Chicago bootcamp there were two projects.
One, was an echo program where the main goal was to store arbitrary data in an account thus effectively treating it as a data buffer.
The second was to create an exchange booth that would allow the owner of the booth to deposit/withdraw money between two vaults (accounts). At the same time it would also allow any user to use the booth to exchange between two tokens one from each of the vaults. In order to determine the exchange rate a Pyth oracle was to be used, as it was Pyth who held the workshop. The goal of the project was to explore the complexities of exchange protocols within DeFI.
Before we start here are some quick notes on the Solana programming model that is great to have in mind
- Everything on Solana are accounts with different flags that indicate if it’s a signer (is_signer: true), can it store data (is_writable: true) or if its a smart contract (is_executable: true). We can control the two first flags.
- One or more instructions makes up a transaction.
For this specific post I will dig into the deposit part of the Exchange Booth Spec which is stated as
This instruction should be callable by the booth admin and facilitate a token transfer for one of the booth’s two tokens from an admin-controlled token account into a programcontrolled vault.
Accounts
First time reading it could make you a bit confused if you haven’t read through the spec in its entirety. However, a sketch with the required accounts won’t hurt
So these are the required accounts:
- Admin/Authority/Owner: This is the account that owns the Exchange Booth and controls the Token Accounts.
- Mint A: this is the account that has Authority to mint new tokens. It is assumed that this account mints into Token Account A
- Token Account A: this account contains Tokens of type A and is owned by the Authority. It has a public private key pair.
- Mint B: Account with the authority to mint new tokens of type B. This account is used to mint into token account B.
- Token account B: account holds token of type B and is controlled by the Authority. It has a public private key pair.
- Exchange Booth: This is the exchange booth that contains information about the owner, vaults and other potential data. It is a program derived account (pda) that is the authority for vault A and vault. By being a PDA it does not have a private key but a key derived deterministically based on seeds.
- Vault A: a token account for holding tokens of type A. Controlled by the exchange booth.
- Vault B: token account for holding tokens of type B. Controlled by the exchange booth.
Wow, that’s a lot of accounts to keep track of.
Now let’s take a look at how we would deposit money from our token account to the correct vault.
Data flow
The flow of data is basically
So it is pretty simple. We just want to transfer from token account A to vault A. However, things become more complicated when we have to validate the accounts and make sure the correct accounts have the correct privileges.
Setting up the project
I will assume that you have set up your rust project by running
>cargo init — lib
and created the expected Solana smart contract folder structure and updated the Cargo.toml file to reflect the necessary Solana settings. If you haven’t or are stuck please refer to the Solana development guide or my previous post on setting up a test environment.
The folder structure should look something like this
Although it is not necessary to split the instructions into seperate files under the processor folder.
Finally, let’s work out the instruction specification. The accounts needed in order to transfer the tokens are
- Exchange Booth PDA: it is the Authority of both vaults so it is needed when signing the transfer
- Authority: This is the owner/signer of the Token Account that we are depositing from.
- Token Account: this is the account we are deposit from.
- Vault A: this is the account we are depositing into.
- mint_a: this is the original mint account for token A.
- mint_b: this is the mint account for token b. In this context it is mainly used to verify the correctness of inputted accounts.
- token_program: this is the spl_token program that was responsible for creating the mints and creating the token Account before assigning ownership to us.
Programmatically this results in the following instruction.rs file
As you can see the accounts are specified from lines 37–57. Before digging into the privileges of each account I should mention how to deal with amounts in Solana programs
A note on amount handling
This part is a bit tricky and a typical gotcha when working with amounts. When we send instructions we expect to send instructions of the type
Send 14.2 tokens from Account A to Account B.
However, looking at the spl_token::instruction::transfer function we see the following spec
as you can see the amount is u64 (64bit unsigned integer). This is because the method expects to see an integer. If we move one step backwards and take a look at the spl_token::instruction::initialize_mint instruction
you can see that it expects a field decimals: u8. This is so that Solana don’t have to store amount as a float and thus not having to experience floating point errors. So say we set decimals to 9 this would mean we expect amounts to look like this
14.200000000
but we would store it as
14,200,000,000
which is a u64.
The max number that can be stored as a u64 is
18,446,744,073,709,551,615
which would be
18 446 744 073,709551615
tokens.
In order to convert between the f64 amount and u64 amount format we can use the one liner
let amount_long = (amount * f64::powf(10., mint_decimals.into())) as u64;
Where mint_decimals is 9 in the above example.
Instruction / processor
Finally, we are ready to write our deposit instruction in the processor.rs file. I will assume that you have initialized all the necessary accounts shown in Figure 1. If not then again refer to my previous article on how to set up a test environment where most of these accounts are created.
I will divide the instruction method into 5parts for clarity
- Get AccountInfo
- Check privileges of accounts
- Check that the correct pda (program derived addresses) are submitted
- Amount sanity checks
- Transfer instruction and execution of transaction
1. Get AccountInfo
This part should become trivial to you over time as it is the exact same pattern every time
These are the same accounts described earlier in the instructions.rs file. It is very important for documentation and your own mental model to have it match the comments in instruction.rs file.
According to solana_program crate the AccountInfo struct is
So you can see that the AccountInfo struct contains information on whether the account is a signer, writable or even executable.
2. Check Privileges
We need to make sure that the accounts sent to the program has the right privileges in order to reduce potential errors. So we need to think about what kind of privileges each account needs.
The first inputted account is the exchange_booth_account which is the authority of the vaults. The account can store various data related to the booth like the owner of the booth and its related vaults. As it is a PDA it does not have a private key and can therefore not sign anything. As a consequence the exchange_booth_account should have the following privileges
- is_signer: false
- is_writable: true
The next account is the authority_account which is the owner of the exchange booth and the token accounts. We don’t write to this account as it is meant to hold SOL and sign transactions. So the privileges should be
- is_signer: true
- is_writable: false
The token_account is the account we transfer from and which is used to hold tokens of a certain mint (type). Therefore, we need to update the amount field and thus the account is writable. Since the account has an authority it will not need to be a signer. Thus the privileges are
- is_signer: false
- is_writable: true
The vault is the account that we are transferring to. Thus it needs to be writable but it will not sign anything for two reasons: 1. it is a PDA and 2. it is on the receiving end of the transaction. So the privileges should be
- is_signer: false
- is_writable: false
The next account up is mint_a or the mint account for token a. This account is only being used in the seeds of the PDAs so we will not write to it nor would it sign anything. Hence, the privileges are
- is_signer: false
- is_writable: false
The next account mint_b has the same privileges at mint_a
- is_signer: false
- is_writable: false
Last but not least the token_program_account for which the public key is the spl token program id. This account is used for referencing the spl token program when making the transfer transaction. Thus its privileges are
- is_signer: false
- is_writable: false
We should also check if the token_account is of mint a or mint b. More specifically, whether the token_account hold tokens of type A or B. Thus we would have to unpack the token_account data into the spl_token::state::Account struct
by doing this we get access to the mint pubkey and we can check if that pubkey is equal to the mint_a or mint_b public key. The result of this comparison decides how we should continue with the validation.
Additionally, we should do a sanity check of whether the vault exists or not. Now, the vault is an account holding a token of a certain mint and is created using the spl token program. So the account structure is that of an spl account.
In order to verify that the account is initialized we would have to unpack the data of the vault account into spl_token::state::account which has the following structure
Now we call the is_initialized() method on the account object to check if it is actually initialized. We also have to make sure that the mint key for the token_account and vault is equal. If they are not then it is not possible to transfer tokens between them.
Finally, if we store the owner in the data field of the exchange_booth_account we should probably check that this matches the public key of the inputted authority account.
These checks should result in something like this
- lines 29–47: we check the privileges of the input accounts as described earlier
- lines 49–50: unpack the token account data into the spl token account
- lines 52: are we depositing a token of type A?
- lines 54–59: unpack the vault data. Note: this is done in a different way than for the token_account just to show that it is possible to do it in at least two ways.
- lines 61–64: check that the vault is actually initialized
- lines 66–69: check that the vault_account and token_account have the same mint i.e. that they can hold and transfer the same kind of tokens.
- lines: 72–77: unpack the exchange_booth_account data and check if the admin field matches the authority.
PDA Validation
As we are dealing with two program derived addresses (PDAs) in the case of the exchange_booth_account and the vault we should make sure that accounts matches that of the inputted accounts. This is done using the solana_program::pubkey::find_program_address method.
So we end up with the code
Note: in the github repo this part is abstracted away into separate functions for reusability across instructions.
- lines 82–92: find the vault_pda key by calling the find_program_address
- lines 94–97: check that the generated vault_pda public key matches that of the inputted vault account
- lines 99–107: find the pda public key for the exchange booth account
- lines 110–113: check that the exchange booth account public key matches that of the generated pda key.
4. Balance sanity check
In order to reduce the potential unexpected error we should also check if the token account that we are transferring from has enough funds, obviously.
- lines 100–102: unpack the mint with the sole goal of getting the decimals
- lines 103: get the decimals
- lines 105: this is basically he calculation we discussed under A note on amount handling earlier.
- lines 107–110: if the amount in the token_account is less than the amount to be deposited then throw an error.
5. Create the transfer instruction and make the transaction
Finally after making a ton of checks and unpacking we are ready to get to the real meat of the instruction. Namely the depositing of funds into the vault from the token account.
This is basically the code needed to do the transfer
It’s not much but enough to get the work done.
Ok, let’s start with explaining the spl_token::instruction::transfer method. It is an instruction that calls the spl token program with token_program as the id in order to transfer from the token_account to the vault.
Now, in order to do so we need to specify who owns or is the authority of the account that we are transferring from. Earlier in figure 1 this was clearly the owner of the exchange_booth_account namely the authority account.
Additionally we need at least one signer. In this case the only signer that we need is the owner of the token_account which is the authority account. Lastly we specify the amount in u64 that we wish to transfer.
Next, we are ready to invoke the instruction. More specifically we need to make the Cross program invocation (CPI) on lines 121–130. We need to specify the accounts that we are required to make the transaction. These are:
- token_program: This is the spl token program which actually updates the data in the accounts.
- token_account: this is the account we are transferring from
- vault: this is the account we are transferring to
- authority: this is the signer account or more specifically the owner of the token_account.
Great, I think that is everything we need. It might be the case that I have missed something so please tell me so I can update the code and examples.
Next steps
The next steps are to built out the tests in order to check that the deposit instruction actually does what it is supposed to do.
Additionally, there are other instructions specified in the spec that would be interested to build out as well.
Conclusion
Congratulations on learning
- How to think about architecting a Solana instruction
- How to unpack accounts sent from a client
- How to do different types of validation on the accounts
- Do sanity checks
- Make a transfer from one token account to another using the spl token program and its sdk
Github
You can find the github repo that I referenced here https://github.com/kristohberg/xbooth . It is still a work in progress as some parts of the spec if not finished.
Social Media
Please follow me on twitter https://twitter.com/kristohberg for more compact comments, thoughts and insight into programming, blockchain and general development.
Hope you learned a lot and if there are things that you think is confusing or that you want to learn more about please shoot me a dm.