A Guide to Solana Token2022 (Token Extensions)
Solana Token2022 enables transfer hooks, interest bearing tokens, confidential transfers, fees, and more. Let's look at how it all works and the data behind it.
Solana Learning Resources:
The Solana Token program (spl_token) has existed for years, and I’ve covered before how the token/account structure works with fungible tokens and NFTs. It will be helpful background context, but is not required knowledge to follow this guide.
Token2022 is the newest iteration of that program and enables much more flexible token mechanics through “extensions”. It’s often referred to now as the “Token Extension Program”. The program is already being used by companies like PayPal for their PyUSD stablecoin.
I’ll walk through the following concepts:
Differences between spl_token and token2022
Understanding the instruction and enum patterns
The new transfer lifecycle
Tables to query on Dune
ZK and Compressed Tokens
Below are some important links that I will refer to:
Big shoutout to Jon Wong for answering all my questions, be sure to follow him to keep up with all Solana developments.
token2022 versus spl_token
The overall account structure has not changed with token2022, a single token mint account is still created per token and an associated account is created using the same associated token (atoken) program. Remember that wallets can be assigned as the own of an associated account, and each associated account can hold SOL (for rent) and one token mint balance.
Token2022 also retains the same transferChecked() instruction and arguments, as to be easily backwards compatible (the “transfer()” instruction is gone though). You can also still create metadata and NFT edition accounts using the same old Metaplex token metadata and master edition programs.
However, a lot of new functionality has been added. Let’s take a look at all token extensions (official docs):
✅ = live, ⏲ = in development
Mint token account extensions (these affect ALL created associated accounts):
✅ transfer fees: set a percentage fee to take on all transfers. You can set a maximum fee to take as well.
✅ closing mint: set an owner who can delete the token mint forever.
✅ interest-bearing tokens: set an interest rate for ALL tokens to accrue at. Can change the rate at any time.
✅ transfer hook: assign a program id, where after every transfer a CPI to that program will call an arbitrary “execute()” instruction on the assigned program.
✅ non-transferable tokens: tokens cannot be transferred after mint. Can still be burned.
✅ permanent delegate: approve a delegate who can control transfers of any amount forever
✅ default account state: sets default account data
✅ immutable ownership: account owner cannot be changed (this is different from the program account owner)
✅ metadata pointer: pointer to metadata program id
✅ metadata: token metadata (basic program contains name, symbol, uri)
⏲ group pointer: like collection mint id from metaplex standard, which groups tokens together (useful for NFTs).
⏲ group metadata: metadata of the collection
⏲ member pointer: id for a set of tokens within a group
⏲ member metadata: metadata/permissions for the set of tokens
⚠ IMPORTANT NOTE: All of these extensions must be initialized before the mint account is created. You can’t add or remove extensions after creating the token.
You can also combine most of these extensions, with obvious caveats around non-transferable tokens.
Associated token account extensions (set per account):
✅ approve delegate (same as in spl_token): set a delegate who can control transfers up to a certain amount
✅ freeze (same as in spl_token): set an authority who can freeze and thaw a token at will (make non-transferrable).
✅ memo: call the memo program with some text string, after a transfer. requirement is set on a per-account level.
⏲ confidential transfers: keep transfer values private but source and destination accounts are public, account owner must opt-in and create an encryption key. Can deposit and withdraw from the private account.
⏲ confidential transfer fees: take fees on private transfers, this is set on ALL accounts (no opt out).
⏲ CPI guard: Disable certain cross program invocations (CPIs) to the token2022 program. This intends to allow for wallet to wallet transfers only.
If we look at all the token2022 mints created so far, fee tokens have been the main extension utilized in these new tokens.
Breaking Down Instructions and the Enum Pattern
If you’ve looked into Solana data before, you should be familiar with “discriminators” which are the signatures of specific functions on a program. If not, I’ve created this dashboard for quickly breaking down any decoded program in Dune and giving you some data intuition. One of the queries I constantly use for identifying discriminators is this sleuther query (this one works even if the program isn’t decoded).
Read this guide if you want to learn more about discriminators and program types
I’ve hardcoded the token2022 program into that sleuther query, showing the instructions and call stats over the last 365 days:
As it is a native program, the discriminators are 1 byte and follow a nice and orderly number system. There are 41 different instructions - it’s worth looking at each instruction’s example tx and then also its respective instruction code and logic in the processor file.
Most extensions use an extra enum on top of the discriminator to decide which extension instruction to invoke. Let’s take a look at the transfer extension, which is enum number 26 (0x1a). It contains the following nested enum instructions:
You can see there are actually 6 instructions nested within this extension, numbering 0-5 respectively. So if I’m filtering for the InitializeTransferFeeConfig instruction, I would check that the first 2 bytes of the call data are equal to 0x1a00. Unfortunately, this means you’ll need to do manual decoding of any call data variables since Dune does not handle this nested function format. You can look at my authority tracker query below for some examples.
On Authorities
Authorities for who can freeze accounts, change fees and interest rates, add hooks, and more can be changed after initialization. This honestly does not matter much today, but as people build more programmatic ways of changing these variables it will matter more. This can be changed with setAuthority() instruction, where an enum in the call data decides which authority is being changed:
I’ve created this query that reconstructs all authorities for a token mint, you could adjust any subquery here to get historical authorities at any given block slot for a given authority type.
Note that you could set authorities to 0x0000… (32 bytes of 0x00) to immutably “turn off changes” to an extension. The becomes 11111111111111111111111111111111 in base58, which is the system program id.
Transfer Lifecycle
In the original spl_token, the transfer instruction is about 100 lines of code. In the token2022 program, the transfer instruction is about 250 lines of code. The main reason for this is all of the new checks that occur (on both mint and account level), you can ctrl + F for “get_extension” to see when each extension is referenced.
Let’s go over how transfer fees, interest accrual, and transfer hooks work in this lifecycle.
If a memo extension is active on the token account, then the memo program will be called in an instruction BEFORE the transfer happens.
Transfer fees are set as a percentage of the amount transferred, and there is a max fee that can be taken (both are set by the transfer fee config authority). What’s confusing here is that users can call transferChecked() or transferCheckedWithFee() as 0x1a01. The latter clearly defines the fee amount, the former does not. You’ll soon notice that in a transfer transaction of a fee token, there is no transfer of that fee amount to some treasury (in an instruction or account activity/balances). That’s because the fee amount is held on the destination account, which can then be withdrawn from at some future date using HarvestWithheldTokensToMint() and then taken out to the treasury with WithdrawWithheldTokensFromMint(). I’ve captured the correct fee amount in the tokens_solana.transfer table in the “fee” column by calculating the set fee percentage at the time of the transfer.
Interest bearing tokens are a bit harder to intuitively understand. No matter when the tokens were minted, interest accrual on them is counted starting from the time when the token mint was first created. Logically, you can see this in the amount_to_ui_amount function that just takes the cumulative compounding interest of token amount scaled by total time passed. So if there was a token with a 5% interest rate created 1 year ago, and I minted you 100 tokens today, then in actuality you would have received roughly 105 tokens instead. Onchain in both the transaction and the balances you would only see 100 tokens though. It’s not intuitive, and the data work to get the right balance is also difficult. The Solana team is working on a few helper functions to correctly give you the interest accrued balance in an RPC call, but that is not ready yet.
Transfer hooks are called at the end of the transfer function, and will look for a generic “execute()” function on the assigned hook program id. The hook authority can assign any one program id to be hooked onto the token. If no execute function is found, it will continue normally with the transfer. You can revert a transfer by throwing an error in the hook, and you could have a generic hook chaining program if you wanted to string together multiple hooks to a single token. Some interesting applications include identity hooks by Civic, but it is largely an unexplored feature.
Finding the Data Tables in Dune
If you’ve clicked through the linked queries and dashboard, then you have likely already spotted the logic and tables I use for token2022. I’ll list them below:
tokens_solana.fungible: get all token ids, symbols, and deciamls. I added a token_version column, model logic
tokens_solana.transfers: get all token mint, burn, and transfers. I added a token_version column and a fee column, model logic
solana_utils.daily_balances (or latest_balances): you can still use “address” as the associated account address and filter for token2022 mint ids, model logic
tokens_solana.fees_history: reconstructed fee history for tokens, model logic
all authorities: see this query for latest authorities and how to construct historical authorities
interest bearing accruals: I’ve calculated current, average, 1 year forward looking return rate, and total effective historical return rate in this query.
Note that I have not included token2022 in tokens_solana.nft yet, because NFT standards on Solana are a hot mess that I’m not motivated to touch right now.
Overall, you’ll see that token2022 has not really hit developer adoption yet besides a small blip in Feb 2024. Only 70,000 fungible tokens have been created using this standard as of July 2024.
This is partially because it’s not yet supported by most DEXs or defi protocols yet, besides some limited usage in Fluxbeam, Printdex, and Orca. You can see some of the top protocols using token2022 below:
ZK and Compressed Tokens
If you’ve been following along for a while, you have likely realized that Solana has many competing token standards - even I don’t understand all of them. I want to make a quick note to differentiate between confidential tokens, compressed tokens, and ZK tokens; as those are going to potentially be very important but also very confusing.
Confidential transfers with private transfer amounts is an extension on token2022, where you can deposit into and withdraw with a hidden amount. The source and destination of funds are still public. A user can have confidential and non-confidential balances of the same token, at will.
Compressed token accounts are an old concept already seen with encoding accounts into leaves of a merkle tree (because call data is basically free and not stored on the node). You typically have at least three accounts (the mint, the associated, and the metadata accounts) so avoiding creating them saves a lot of SOL. This was created for compressed NFTs, which saw great adoption last year. Metaplex built a program on top called “Bubblegum” which allowed you to transfer/mint/burn these leaves of the tree. The NFT version does not have an option to transition between normal accounts and compressed accounts.
ZK compression tokens goes further to include proofs for token actions like transfers directly in the data, versus using an onchain component like bubblegum. You’re essentially updating your leaf in the merkle tree each time you transact. All transaction data is still written to the mainnet chain, just in call data. Nothing is saved in the state of the blockchain (i.e. in other accounts or balances), so this is considered stateless transactions. Think of the proof verification into a merkle tree as a Verkle tree, and the fact you can update individual leaves as a Reckle tree (recursive proof). What’s cool here is you can actually compress and decompress this token into a normal token account at will. Note that these tokens are NOT privacy preserving.
Stateless transactions, compression of data, and privacy preservation are three key parts to the end state of blockchains, so while these are more technically complex they are worth reading about to keep up with. I’ll come out with more in depth guides for confidential transfers and ZK compression tokens when they are fully live on mainnet.
In Conclusion
Tokens on Solana are fully in experimentation mode, with different teams opting for varying amounts of data and programmatic flexibility. While the many standards can be confusing at times, it’s exciting to see most of these theoretical constructs finally live. While usage of the new token2022 extensions is still low right now, I’m excited to revisit it in half a year to see what exciting applications have been built.