The Transfer Hook extension and Transfer Hook Interface introduce the ability to create Mint Accounts that execute custom instruction logic on every token transfer.
This unlocks many new use cases for token transfers, such as:
- Enforcing NFT royalties
- Black or white list wallets that can receive tokens
- Implementing custom fees on token transfers
- Creating custom token transfer events
- Track statistics over your token transfers
- And many more
To achieve this, developers must build a program that implements the Transfer Hook Interface and initialize a Mint Account with the Transfer Hook extension enabled.
For every token transfer involving tokens from the Mint Account, the Token Extensions program makes a Cross Program Invocation (CPI) to execute an instruction on the Transfer Hook program.
When the Token Extensions program CPIs to a Transfer Hook program, all accounts from the initial transfer are converted to read-only accounts. This means the signer privileges of the sender do not extend to the Transfer Hook program.
This design decision is made to prevent malicious use of Transfer Hook programs.
In this guide, we will create a Transfer Hook program using the Anchor framework however, it is possible to implement the Transfer Hook Interface using a native program as well. Learn more about Anchor framework here: Anchor Framework
Transfer Hook Interface Overview
The Transfer Hook Interface provides a way for developers to implement custom instruction logic that is executed on every token transfer for a specific Mint Account.
The Transfer Hook Interface specifies the following instructions:
Execute
: An instruction that the Token Extension program invokes on every token transfer.InitializeExtraAccountMetaList
(optional): Creates an account that stores a list of additional accounts required by the customExecute
instruction.UpdateExtraAccountMetaList
(optional): Updates the list of additional accounts by overwriting the existing list.
It is technically not required to implement the InitializeExtraAccountMetaList
instruction using the interface. The account can be created by any instruction
on a Transfer Hook program.
However, the Program Derived Address (PDA) for the account must be derived using the following seeds:
- The hard coded string "extra-account-metas"
- The Mint Account address
- The Transfer Hook program ID
By storing the extra accounts required by the Execute
instruction in the
predefined PDA, these accounts can be automatically added to a token transfer
instruction from the client.
Hello-world Transfer hook
This example is the hello world of transfer hooks. It is a simple transfer hook that will just print a message on every token transfer. We start by opening the example in Solana Playground, an online tool to build and deploy solana programs: link
The example consists of an anchor program which implements the transfer hook interface and a test file to test the program.
This program will only include 3 instructions:
initialize_extra_account_meta_list
: Creates an account that stores a list of extra accounts required by thetransfer_hook
instruction. In the hello world we leave this empty.transfer_hook
: This instruction is invoked via CPI on every token transfer to perform a wrapped SOL token transfer.fallback
: Because we are using Anchor and the token program is a native program we need to add a fallback instruction to manually match the instruction discriminator and invoke our customtransfer_hook
instruction. You don't need to change this function.
Every time the token gets transferred this transfer_hook
function will be
called by the token program.
In this function you can now add your additional logic. For example, you could let the transfer fail whenever an amount is transferred that is bigger than 50 like so:
To run the example in Solana Playground follow this link: link
In Playground's terminal, run the build
command which will update the value of
declare_id
in the lib.rs
file with a newly generated program ID. Then run
the deploy
command to deploy your program to devnet. When when the program is
deployed you can run the test file by using the test
command in the terminal.
This will then give you the output similar to this:
If you do not want to use javascript to create your token, you can also use the
spl-token
command from the Solana CLI after you deployed your program:
Counter Transfer hook
The next example will show you how you can increase a counter every time your token has been transferred. link
If you want to add logic to your transfer hook that needs additional accounts you need to add them to the ExtraAccountMetaList account. In our case here we want a PDA which saves the amount how often the token has been transferred.
This can be done by adding the following code to the
initialize_extra_account_meta_list
instruction:
And we also need to create this account when we initialize the new mint account and we need to pass it in every time we transfer the token.
And the account will hold a u64
counter variable:
Now in our transfer hook function we can just increase this counter by one every time it gets called:
In the client these additional accounts are added automatically by the helper function createTransferCheckedWithTransferHookInstruction:
To run the example in Solana Playground follow this link: link
And then in there type build
which will update the value of declare_id
in
the lib.rs
file with a newly generated program ID. Then type deploy
to
deploy your program to devnet. When when the program is deployed you can run the
test file by typing test
in the terminal.
This will then give you the following output. In last transaction you will then able to see how often your token has been transferred:
Since here we are increasing a counter whenever the token is transferred we need to make sure that the transfer hook instruction can only be called during a transfer, otherwise someone could just call the transfer hook instruction directly and mess up our counter. This is a check you should add to any of your transfer hooks.
You can add the check like this:
And then call it at the start of your transfer_hook
function:
Transfer Hook with wSOl Transfer fee (advanced example)
In the next part of this guide, we will build a more advanced Transfer Hook program using the Anchor framework. This program will require the sender to pay a wSOL fee for every token transfer.
The wSOL transfers will be executed using a delegate that is a PDA derived from the Transfer Hook program. This is necessary because the signature from the initial sender of the token transfer instruction is not accessible in the Transfer Hook program.
This program will only include 3 instructions:
initialize_extra_account_meta_list
: Creates an account that stores a list of extra accounts required by thetransfer_hook
instruction.transfer_hook
: This instruction is invoked via CPI on every token transfer to perform a wrapped SOL token transfer.fallback
: The transfer hook interface instructions have specific discriminators (instruction identifiers). In an Anchor program, we can use a fallback instruction to manually match the instruction discriminator and invoke our customtransfer_hook
instruction.
This program will require the sender to pay a fee in wrapped SOL (wSOL) on every token transfer. Here is the final program.
Getting Started
Start by opening this Solana Playground link and then click the "Import" button to copy the project.
The starter code includes a lib.rs
and transfer-hook.test.ts
file which are
scaffolded for the program we will be creating. In the lib.rs
file you should
see the following code:
Once you've imported the project, build the program by using the build
command
in the Playground terminal.
This will update the value of declare_id
in the lib.rs
file with a newly
generated program ID.
Initialize ExtraAccountMetas Account Instruction
In this step, we will implement the initialize_extra_account_meta_list
instruction for our Transfer Hook program. This instruction creates an
ExtraAccountMetas account, which will store the additional accounts required by
our transfer_hook
instruction.
In this example, the initialize_extra_account_meta_list
instruction requires 7
accounts:
payer
: The account used to pay for the creation of the ExtraAccountMetas account.extra_account_meta_list
: The ExtraAccountMetas account created to store the list of accounts required by ourtransfer_hook
instruction.mint
: The Mint Account that points to this Transfer Hook program. The mint address is a required seed for deriving theextra_account_meta_list
PDA.wsol_mint
: The wrapped SOL mint.token_program
: The original Token program IDassociated_token_program
: The Associated Token program ID.system_program
: The system program, which is a required account when creating new accounts.
The addresses for wsol_mint
, wsol_mint
, and associated_token_program
will
be used to derive the addresses for the wSOL Associated Token Accounts. These
accounts are required by the transfer_hook
instruction and will be stored on
the ExtraAccountMetas account.
Update the InitializeExtraAccountMetaList
struct by replacing the following
starter code:
With the code provided below:
Next, update the initialize_extra_account_meta_list
instruction by replacing
the following starter code:
With the code below:
Let's walk through the updated instruction logic. We begin by listing the additional accounts that need to be stored on the ExtraAccountMetas account.
There are three methods for storing these accounts:
- Directly store the account address:
- Wrapped SOL mint address
- Token Program ID
- Associated Token Program ID
- Store the seeds to derive a PDA for the Transfer Hook program:
- Delegate PDA
- Store the seeds to derive a PDA for a program other than the Transfer Hook
program:
- Delegate wSOL Associated Token Account
- Sender wSOL Associated Token Account
Next, we calculate the size and rent required to store the list of ExtraAccountMetas.
Next, we make a CPI to the System Program to create an account and set the Transfer Hook Program as the owner. The PDA seeds are included as signer seeds on the CPI because we are using the PDA as the address of the new account.
Once we've created the account, we initialize the account data to store the list of ExtraAccountMetas.
In this example, we are not using the Transfer Hook interface to create the ExtraAccountMetas account.
Custom Transfer Hook Instruction
Next, let's implement the custom transfer_hook
instruction. This is the
instruction the Token Extension program will invoke on every token transfer.
In this example, we will require a fee paid in wSOL for every token transfer. For simplicity, the fee amount equals the token transfer amount.
Update the TransferHook
struct by replacing the following starter code:
With the updated code below:
Note that the order of accounts in this struct matters. This is the order in which the Token Extensions program provides these accounts when it CPIs to this Transfer Hook program.
The first 4 accounts are the accounts required by the initial token transfer.
The 5th account is the address of the ExtraAccountMeta account that stores the
list of extra accounts required by our transfer_hook
instruction.
The remaining accounts are the accounts listed in the ExtraAccountMetas account
in the order we defined them in the initialize_extra_account_meta_list
instruction.
Next, update the transfer_hook
instruction by replacing the following starter
code:
With the updated code below:
Within the instruction logic, we make a CPI to transfer wSOL from the sender's wSOL token account. This transfer is signed for using the delegate PDA. For every token transfer, the sender must first approve the delegate for the transfer amount.
Fallback Instruction
Lastly, we need to add a fallback instruction to the Anchor program to handle the CPI from the Token Extensions program.
This step is required due to the difference in the way Anchor generates
instruction discriminators compared to the ones used in Transfer Hook interface
instructions. The instruction discriminator for the transfer_hook
instruction
will not match the one for the Transfer Hook interface.
Update the fallback
instruction by replacing the following starter code:
With the updated code below:
The fallback instruction checks if the instruction discriminator for an incoming
instruction matches the Execute
instruction from the Transfer Hook interface.
If there is a successful match, it invokes the transfer_hook
instruction in
our Anchor program.
Currently, there is an unreleased Anchor feature that simplifies this process. It would remove the need for the fallback instruction.
Build and Deploy Program
The Transfer Hook program is now complete. Ensure that you have enough Devnet SOL in your Playground wallet to deploy the program.
To build the program, use the following command:
Next, deploy the program using the command:
Test File Overview
Next, let's test the program. Open the transfer-hook.test.ts
file, and you
should see the following starter code:
First, we generate a keypair to use as the address for a new Mint Account. Using the mint address, we derive the Associated Token Account (ATA) addresses that we will use for the token transfer.
Next, we derive the PDA for the ExtraAccountMetas account. This account is created to store the additional accounts required by the custom transfer hook instruction.
We also derive the PDA that will be used as the delegate. The sender must approve this address as a delegate for their wSOL token account. This delegate PDA is used to "sign" for the wSOL transfer in the custom transfer hook instruction.
Additionally, we derive the addresses for the wSOL token accounts. The first address is for the sender's wSOL token account, which needs to be funded to pay for the transfer fee required by the transfer hook instruction. The second address is for the wSOL token account owned by the delegate PDA. In this example, all wSOL fees are sent to this account.
Finally, as part of the setup, we create the wSOL token accounts.
Create Mint Account
To begin, build a transaction to create a new Mint Account with the Transfer Hook extension enabled. In this transaction, make sure to specify our program as the Transfer Hook program stored on the extension.
Enabling the Transfer Hook extension allows the Transfer Extension program to determine which program to invoke on every token transfer.
Replace the placeholder test:
With the updated test below:
Creating Token Accounts
Next, as part of the setup, create the Associated Token Accounts for both the sender and recipient. Also, fund the sender's account with some tokens.
Replace the placeholder test:
With the updated test below:
Create ExtraAccountMeta Account
Before sending a token transfer, we need to create the ExtraAccountMetas account to store all the additional accounts required by the transfer hook instruction.
To create this account, we invoke the instruction from our program.
Replace the placeholder test:
With the updated test below:
Transfer Tokens
Finally, we are ready to send a token transfer. In addition to the transfer instruction, there are a few additional instructions that need to be included.
- The sender must transfer SOL to their wSOL token account to cover the fee required by the transfer hook instruction.
- The sender must approve the delegate PDA for the amount of the wSOL fee.
- Include an instruction to sync the wSOL balance.
- The token transfer instruction must include all the extra accounts required by the transfer hook instruction.
Replace the placeholder test:
With the updated test below:
The transfer instruction must include all additional AccountMetas, the address of the ExtraAccountMetas account, and the address of the Transfer Hook program.
Run Test File
Once you have updated all the tests, the final step is to run the test.
To run the test file, use the following command in the terminal:
You should see output similar to the following:
Using token account data in transfer hook
Sometimes you may want to use account data to derive additional accounts in the extra account metas. This is useful if, for example, you want to use the token account's owner as a seed for a PDA.
When creating the ExtraAccountMeta you can use the data of any account as an extra seed. In this case we want to derive a counter account from the token account owner and the string 'counter'. This means we will be always able to see how often that token account owner has transferred tokens.
This is how you set it up in the extra_account_metas()
function.
Let's look at the token account struct to understand how the account data is stored. Below is an example of a token account structure. So we can take 32 bytes at position 32 to 64 as the owner of the token account, which is at 'account_index: 0'. 'account_index` refers to the index of the account in the accounts array. In the case of a transfer hook, the owner token account is the first entry in the accounts array. The second account is always the mint and the third account is the destination token account. This account order is the same as in the old token program.
In our case, we want to derive a counter account from the owner of the sender
token account so when we create the ExtraAccountMeta accounts we init
this PDA
counter account that is derived from the sender token account owner and the
string 'counter'. When the PDA counter account is initialized we will be able to
use it within the transfer hook to increment the value in every transfer.
We also need to define this extra counter account in the TransferHook struct. These are the accounts that are passed to our TransferHook program every time a transfer is done. The client gets these additional accounts from the ExtraAccountsMetaList PDA and includes them in token transfer instruction, but here in the program we still need to define it.
In the client this account is auto generated and you can use it as follows.
The helper function is resolving the account automatically from the ExtraAccounts data account. How the account would be resolved in the client is like this:
Note that the counter account is derived from the owner of the token account and needs to be initialized before doing a transfer. In the case of this example we initialize the counter account when we initialize the extra account metas. So we will only have a counter PDA for the owner of the token account that called that function. If you want to have a counter account for every token account for your mint out there you will need to have some functionality to create these PDAs before hand. There could be a button on your dapp to sign up for a counter that creates this PDA account and from then on the users can use this counter token.
Conclusion
The Transfer Hook extension and Transfer Hook Interface allow for the creation of Mint Accounts that execute custom instruction logic on every token transfer. This guide serves as a reference to help you create your own Transfer Hook programs. Feel free to be creative and explore the capabilities of this new functionality!