Skip to main content

On-chain Program Guide

Supporting Token and Token-2022 Together In Your Programโ€‹

This guide is meant for on-chain program / dapp developers who want to support Token and Token-2022 concurrently.

Prerequisitesโ€‹

This guide requires the Solana CLI tool suite, minimum version 1.10.33 in order to support all Token-2022 features.

Motivationโ€‹

On-chain program developers are accustomed to only including one token program, to be used for all tokens in the application.

With the addition of Token-2022, developers must update on-chain programs. This guide walks through the steps required to support both.

Important note: if you do not wish to support Token-2022, there is nothing to do. Your existing on-chain program will loudly fail if an instruction includes any Token-2022 mints / accounts.

Most likely, your program will fail with ProgramError::IncorrectProgramId while trying to create a CPI instruction into the Token program, providing the Token-2022 program id.

Structure of this Guideโ€‹

To safely code the transition, we'll follow a test-driven development approach:

  • add a dependency to spl-token-2022
  • change tests to use spl_token::id() or spl_token_2022::id(), see that all tests fail with Token-2022
  • update on-chain program code to always use the instruction and deserializers from spl_token_2022, make all tests pass

Optionally, if an instruction uses more than one token mint, common to most DeFi, you must add an input token program account for each additional mint. Since it's possible to swap all types of tokens, we need to either invoke the correct token program.

Everything here will reference real commits to the token-swap program, so feel free to follow along and make the changes to your program.

Part I: Support both token programs in single-token use casesโ€‹

Step 1: Update dependenciesโ€‹

In your Cargo.toml, add the latest spl-token-2022 to your dependencies. Check for the latest version of spl-token-2022 in crates.io, since that will typically be the version deployed to mainnet-beta.

Step 2: Add test cases for Token and Token-2022โ€‹

Using the test-case crate, you can update all tests to use both Token and Token-2022. For example, a test defined as:

#[tokio::test]
async fn test_swap() {
...
}

Will become:

#[test_case(spl_token::id() ; "Token Program")]
#[test_case(spl_token_2022::id() ; "Token-2022 Program")]
#[tokio::test]
async fn test_swap(token_program_id: Pubkey) {
...
}

In your program-test setup, you must include spl_token_2022.so at the correct address. You can add it as normal to tests/fixtures/ after downloading it using:

$ solana program dump TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb spl_token_2022.so

If you're using solana-test-validator for your tests, you can include it using:

$ solana-test-validator -c TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 

Note: This step is temporary, until Token-2022 is included by default in program-test and solana-test-validator.

The token-swap does not use program-test, so there's a bit more boilerplate, but the same principle applies.

Step 3: Replace instruction creatorsโ€‹

Everywhere in the code that uses spl_token::instruction must now use spl_token_2022::instruction. The "Token-2022 Program" tests will still fail, but importantly, the "Token Program" tests will pass using the new instruction creators.

If your program uses unchecked transfers, you'll see a deprecation warning:

warning: use of deprecated function `spl_token_2022::instruction::transfer`: please use `transfer_checked` or `transfer_checked_with_fee` instead

If a token has a transfer fee, an unchecked transfer will fail. We'll fix that later. If you want, in the meantime, feel free to add an #[allow(deprecated)] to pass CI, with a TODO or issue to transition to transfer_checked everywhere.

Step 4: Replace spl_token::id() with a parameterโ€‹

Step 2 started the transition away from a fixed program id by adding token_program_id as a parameter to the test function, but now you'll go through your program and tests to use it everywhere.

Whenever spl_token::id() appears in the code, use a parameter corresponding either to spl_token::id() or spl_token_2022::id().

After this, all of your tests should pass! Not so fast though, there's one more step needed to ensure compatibility.

Step 5: Add Extensions to Testsโ€‹

Although all of your tests are passing, you still need to account for differences in accounts in token-2022.

Account extensions are stored after the first 165 bytes of the account, and the normal Account::unpack and Mint::unpack will fail if the size of the account is not exactly 165 and 82, respectively.

Let's make the tests fail again by adding an extension to all mint and token accounts. We'll add the MintCloseAuthority extension to mints, and the ImmutableOwner extension to accounts.

When creating mint accounts, calculate the space required before allocating, then include an initialize_mint_close_authority instruction before initialize_mint. For example this could be:

use spl_token_2022::{extension::ExtensionType, instruction::*, state::Mint};
use solana_sdk::{system_instruction, transaction::Transaction};

// Calculate the space required using the `ExtensionType`
let space = ExtensionType::get_account_len::<Mint>(&[ExtensionType::MintCloseAuthority]);

// get the Rent object and calculate the rent required
let rent_required = rent.minimum_balance(space);

// and then create the account using those parameters
let create_instruction = system_instruction::create_account(&payer.pubkey(), mint_pubkey, rent_required, space, token_program_id);

// Important: you must initialize the mint close authority *BEFORE* initializing the mint,
// and only when working with Token-2022, since the instruction is unsupported by Token.
let initialize_close_authority_instruction = initialize_mint_close_authority(token_program_id, mint_pubkey, Some(close_authority)).unwrap();
let initialize_mint_instruction = initialize_mint(token_program_id, mint_pubkey, mint_authority_pubkey, freeze_authority, 9).unwrap();

// Make the transaction with all of these instructions
let create_mint_transaction = Transaction::new(&[create_instruction, initialize_close_authority_instruction, initialize_mint_instruction], Some(&payer.pubkey));

// Sign it and send it however you want!

The concept is similar with token accounts, but we'll use the ImmutableOwner extension, which is actually supported by both programs, but Tokenkeg... will no-op.

use spl_token_2022::{extension::ExtensionType, instruction::*, state::Account};
use solana_sdk::{system_instruction, transaction::Transaction};

// Calculate the space required using the `ExtensionType`
let space = ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);

// get the Rent object and calculate the rent required
let rent_required = rent.minimum_balance(space);

// and then create the account using those parameters
let create_instruction = system_instruction::create_account(&payer.pubkey(), account_pubkey, rent_required, space, token_program_id);

// Important: you must initialize immutable owner *BEFORE* initializing the account
let initialize_immutable_owner_instruction = initialize_immutable_owner(token_program_id, account_pubkey).unwrap();
let initialize_account_instruction = initialize_account(token_program_id, account_pubkey, mint_pubkey, owner_pubkey).unwrap();

// Make the transaction with all of these instructions
let create_account_transaction = Transaction::new(&[create_instruction, initialize_immutable_owner_instruction, initialize_account_instruction], Some(&payer.pubkey));

// Sign it and send it however you want!

After making these changes, everything fails again. Well done!

Step 6: Use StateWithExtensions instead of Mint and Accountโ€‹

The test failures happen because the program is trying to deserialize a pure Mint or Account, and failing because there are extensions added to it.

Token-2022 adds a new type called StateWithExtensions, which allows you to deserialize the base type, and then pull out any extensions on the fly. It's very close to the same cost as the normal unpack.

Everywhere in your code, wherever you see Mint::unpack or Account::unpack, you'll have to change that to:

use spl_token_2022::{extension::StateWithExtensions, state::{Account, Mint}};
let account_state = StateWithExtensions::<Account>::unpack(&token_account_info.data.borrow())?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account_info.data.borrow())?;

Anytime you access fields in the state, you'll need to go through the base. For example, to access the amount, you must do:

let token_amount = account_state.base.amount;

So typically, you'll just need to add in .base wherever those fields are accessed.

Once that's done, all of your tests should pass! Congratulations, your program is now compatible with Token-2022!

If your program is using multiple token types at once, however, you will need to do more work.

Part II: Support Mixed Token Programs: trading a Token for a Token-2022โ€‹

Part III: Support Specific Extensionsโ€‹

Update from transfer to transfer_checkedโ€‹

Take fee into account when calculating slippageโ€‹