Transfer tokens between accounts on Solana

Learn how to create a smart contract instruction on Solana that deposits tokens from one account to another

0xksure
12 min readMar 11, 2022
Photo by Ferdinand Stöhr on Unsplash

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

Figure 1: Account ownerships

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

Figure 2: token flow

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

  1. Get AccountInfo
  2. Check privileges of accounts
  3. Check that the correct pda (program derived addresses) are submitted
  4. Amount sanity checks
  5. 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

AccountInfo

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.

--

--