7 min read
Overview
The Solana Token Program is a standard for creating and managing tokens on the Solana blockchain. In 2022, Solana Labs released a new feature set, Token Extensions, which allows for customization in how any specific token works.
One of these extensions is the Transfer Hook, which allows developers to add custom logic to token transfers. In this guide, we will discuss the Transfer Hook, how it works, and how you might use it in your Solana token program.
Prerequisites
Before reading this guide, we recommend having some familiarity with the following topics:
What is a Transfer Hook?
Transfer hooks require custom logic to be executed when a token transfer occurs. For example, you might want to:
- verify that a sender and receiver account are both KYC verified before allowing a transfer to occur,
- implement a tax or royalty structure that varies based on the amount of tokens being transferred, or
- dynamically update the metadata of an NFT (check out our guide on How to Mint a Solana NFT with the Metadata Extension)
There are endless opportunities for what you can do with transfer hooks--it is really up to your imagination and the needs of your project. A token with a transfer hook has three main elements:
- A token mint with the token hook extension
- An account metas list that specifies the accounts that the transfer hook program instruction will operate on
- A transfer hook program instruction to specify the custom logic that should be executed when a token transfer occurs. If using older versions of Anchor (before v. 0.30), you will also need a
fallback
function to handle the transfer hook.
Let's take a closer look at each of these elements.
Token Mint with the Token Hook Extension
To use transfer hooks, you must create an SPL token using the TOKEN_2022_PROGRAM_ID
(as extensions do not work on previous versions of the token program). Like all extensions, the transfer hook extension must be initialized when the token is created.
The transfer hook extension itself is pretty simple, it consists of two parts:
- A program ID that will handle the custom logic and
- An authority for making changes to this extension
pub struct InitializeInstructionData {
/// The public key for the account that can update the program id
pub authority: OptionalNonZeroPubkey,
/// The program id that performs logic during transfers
pub program_id: OptionalNonZeroPubkey,
}
Source: GitHub - Solana SPL Token Program
The program passed must include a transfer hook instruction that will be called when a token transfer occurs. The complexity and fun come from the custom logic you inside the hook itself. We will look at that in a moment, but first, we need to think about the accounts that the transfer hook will require. Let's take a look at that next.
Transfer Hook Struct
Every token transfer (regardless of whether you are using transfer hooks or not) requires a few accounts:
- The source token account from which the tokens are being transferred
- The token mint of the tokens being transferred
- The destination token account to which the tokens are being transferred
- The owner of the source token account
Transfer hooks require at least one additional account, an ExtraAccountMetaList
account. This account stores information about any additional accounts that must be passed into the transaction at runtime to execute the transfer hook. This account is seeded on the string literal "extra-account-metas" and the mint's public key and uses the program ID of the specific transfer hook's program:
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
program.programId, // transfer hook program ID
);
Together, these accounts create a TransferHook
context that is passed into the transfer hook program instruction. For example:
#[derive(Accounts)]
pub struct TransferHook<'info> {
#[account(
token::mint = mint,
token::authority = owner,
)]
pub source_token: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(
token::mint = mint,
)]
pub destination_token: InterfaceAccount<'info, TokenAccount>,
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
pub owner: UncheckedAccount<'info>,
/// CHECK: ExtraAccountMetaList Account,
#[account(
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: UncheckedAccount<'info>,
// Add any additional accounts below 👇
}
Transfer Hook Instruction
The transfer hook is a program instruction called when a token transfer occurs. This instruction is called the "hook," and it is executed atomically with the token transfer. This means the transfer will also fail if the hook fails. Transfer hooks are capable of any number of operations to enforce business logic, such as requiring that a user has performed KYC they can transfer tokens, or to add additional functionality to a token transfer, such as updating a seperate account to log the transfer details. Let's look at a couple of simple examples to get a feel for how transfer hooks work.
Here's an example that checks if a user is authorized to make a transfer:
#[error_code]
pub enum MyError {
#[msg("The user is not authorized")]
Unauthorized,
}
#[interface(spl_transfer_hook_interface::execute)]
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
let authorized = ctx.accounts.user.authorized;
if !authorized {
return err!(MyError::Unauthorized);
}
Ok(())
}
Here's another example that checks if the transfer is happening during a predefined set of trading hours:
#[error_code]
pub enum MyError {
#[msg("Outside of allowable trading hours")]
OutsideTradingHours,
}
#[interface(spl_transfer_hook_interface::execute)]
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
let timestamp = Clock::get()?.unix_timestamp;
let day = Clock::get()?.days_from_unix_epoch % 7;
let hour = (timestamp % 86400) / 3600;
let is_trading_hours = day < 5 && hour >= 5 && hour < 17;
if !is_trading_hours {
return err!(MyError::OutsideTradingHours);
}
Ok(())
}
In each of the examples above, if the condition is not met, the transfer will fail. This is the power of transfer hooks--they allow you to enforce custom logic on token transfers. Note the #[interface]
macro was added to Anchor 0.30.0 and is used override Anchor's default instruction discriminator to use the interface instruction's discriminator (so that you do not need to use a fallback
function).
Fallback
If you are using older versions of Anchor (before 0.30.0 release), you will also need to include a fallback
function in your program to handle the transfer hook. This function is required due to differences in how the native Solana program and Anchor programs handle instruction discriminators.
// fallback instruction handler as workaround to anchor instruction discriminator check
pub fn fallback<'info>(
program_id: &Pubkey,
accounts: &'info [AccountInfo<'info>],
data: &[u8],
) -> Result<()> {
let instruction = TransferHookInstruction::unpack(data)?;
// match instruction discriminator to transfer hook interface execute instruction
// token2022 program CPIs this instruction on token transfer
match instruction {
TransferHookInstruction::Execute { amount } => {
let amount_bytes = amount.to_le_bytes();
// invoke custom transfer hook instruction on our program
__private::__global::transfer_hook(program_id, accounts, &amount_bytes)
}
_ => return Err(ProgramError::InvalidInstructionData.into()),
}
}
Hooked yet?
Transfer hooks are a powerful way to add custom logic to your token transfers. They allow you to enforce business logic, add additional functionality, and create a more secure and robust token program. If you are looking to add custom logic to your token transfers, transfer hooks are a great place to start. In our next guide, we will create an example transfer hook program to demonstrate how they work in practice. Stay tuned!
If you have any questions, feel free to use our dedicated channel on Discord or provide feedback using the form below. Stay up to date with the latest by following us on Twitter and our Telegram announcement channel.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.