🪝 Hooked! The power of leveraging Transfer Hooks on Solana

LEâ—Ž
Turbin3 Research
Published in
7 min readMar 17, 2024

This guide offers a detailed exploration of Transfer Hooks and their potential to transform user interactions with tokens!

The code for this guide can be found in this repository. It’s advisable to follow along and run the examples locally to get a better grasp of Instruction Introspection.

Token2022 Program & Token Extension

Why a New Token Program?

As the Solana ecosystem turns into a hub for developers to trial groundbreaking concepts, there’s been a noticeable spike in the demand for enhanced token features.

Traditionally, expanding the capabilities of tokens required forking the existing Token Program and incorporating new functionalities. While this approach is possible, it introduces significant challenges due to Solana’s architectural requirement for transactions to specify the involved programs and accounts, making it cumbersome to handle transactions involving multiple versions of forked token programs.

Solana needed to add new token functionality, with minimal disruption to users, wallets, and dApps. That’s why the Token-2022 program was created!

Introducing Token Extensions — Enhanced token features on a protocol level

Enabled by the new Token-2022 standard, Token Extensions introduces a suite of program-level enhancements such as confidential transactions, customizable transfer logic, and enriched metadata.

This innovation lays the groundwork for a new wave of features in digital assets and stablecoins on the Solana blockchain, providing them with a robust set of capabilities and a revamped user experience.

Assets minted under this program are poised to realize the full potential of true programmability of money and assets on the blockchain, representing a pivotal advancement in the digital asset landscape.

If you want to learn more about how Token Extensions work, click here!

Transfer Hook

Transfer hooks are a powerful new extension, empowering token issuers to precisely shape the dynamics between users and tokens interaction.

Rather than executing a straightforward transfer, developers now have the freedom to embed custom logic into a program leveraging the transfer hook extension. This capability opens the door to more complex token interactions, enhancing the utility and versatility of tokens.

How does it work?

Whenever a Transfer Instruction for a mint that includes a Transfer Hook extension, the execute instruction is triggered.

Every time a Transfer Instruction is invoked for a mint that has a Transfer Hook extension, the execute instruction is triggered. This execute instruction is where all the custom transfer logic resides. It functions similarly to a standard Anchor Program Instruction but with a distinct set of limitations:

  • The sole accessible variable is the amount of the token being transferred.
  • It’s possible to augment the execute instruction with a predetermined array of accounts, which must be included in the extra-account-meta-list Account and incorporated into the instruction each time it’s executed.
    Note: the account structure has a very specific template. At index 0–3 there are the accounts required for token transfer like source, mint, destination, and owner; at index 4 there is the ExtraAccountMetaList account and following all the accounts in the ExtraAccountMetaList
  • Given that we’re integrating with Anchor and the token program is inherent, a fallback instruction is necessary. This fallback is required to manually align the instruction discriminator and activate our bespoke transfer_hook instruction, ensuring seamless integration and functionality.

Note: It is important to note that while transfer hooks give the capability to insert custom logic within a transfer, all accounts from the initial transfer are converted to read-only accounts. This means that the signer privileges of the sender do not extend to the Transfer Hook program

Extra-Account-Meta-List

As we said before, the extra accounts required by the Execute instruction are stored in the predefined PDA that must be derived using the following seeds:

  • The hard-coded string “extra-account-metas”
  • The Mint Account address
  • The Transfer Hook program ID
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
program.programId, // transfer hook program ID
);

The accounts stored inside the Extra-Account-Meta-List can be automatically added to a token transfer instruction from the client, making it very convenient for protocols to use!

To facilitate the storage of these additional accounts, an InitializeExtraAccountMetaList instruction can be implemented. This initialization leverages the ExtraAccountMeta instruction for creating the address in a deterministic manner, offering several methods for address generation based on the protocol's needs:

  • ExtraAccountMeta::new_with_pubkey(): Allows for the specification of a public key that one wishes to store within the account.
  • ExtraAccountMeta::new_with_seed(): Enables the creation of an address from a specified array of seeds, providing flexibility in address generation.
  • ExtraAccountMeta::new_external_pda_with_seeds(): This method allows specifying an array of seeds and the program ID from which the PDA was generated, offering a tailored approach to PDA creation.

Note: For the generation of the address using seeds, we can pass in both literal string and address that are based on the index of the execute instruction (For example: Index 0 as we said before is the mint).

If you want to learn more about how Transfer Hooks work, click here!

Live Example: Vesting Program

Let’s take a closer look at a practical case: implementing a Vesting Program with Transfer Hooks.

This example showcases a method for users to gradually gain access to tokens. Unlike traditional locking mechanisms, this approach places tokens directly in users’ wallets from the start. The security magic happens through a transfer hook, which restricts how many tokens can be transferred out of the owner’s associated token account (ATA).

Instruction 1: Setting Up a Vesting Account

This instruction was created inside of the Transfer Hook program, for simplicity reasons, to let an admin establish a Vesting Account.

The create_vesting_account instruction

This account is designed to record the amount of tokens vested, check if the airdrop has been claimed, and store vesting specifics. These details include the release percentage in fee basis points and the release schedule, encapsulated as VestingData Type.

VestingData Type

Instruction 2: Claiming Vested Tokens

For users, the next step is to claim their vested tokens in the full amount. In our scenario, claiming directly results in tokens being minted to the user’s wallet. This method was chosen to effectively demonstrate the smart contract’s functionalities and facilitate testing.

Instruction 3: Execute

For the execute instruction we just need an additional account beyond the essential ones: the vesting_account. This account is incorporated as an unchecked account, a decision we’ll clarify shortly.

As we can see, the identification of this account relies on specific seeds: the word “vesting” in byte form, the public key of the mint, and the public key of the source token. To accomplish this, we add the seeds in the ExtraAccountMetaList utilizing this ExtraAccountMeta instruction as follows:

let account_metas = vec![
ExtraAccountMeta::new_with_seeds(
&[
Seed::Literal { bytes: "vesting".as_bytes().to_vec() },
Seed::AccountKey { index: 1 },
Seed::AccountKey { index: 0 }
],
false, // is_signer
true, // is_writable
)?
];

This instruction informs the account that it needs to be created from a string literal (in this case, “vesting”) converted to bytes, alongside two account keys derived from the execute account at indices 1 (mint) and 0 (source_token).

Now let’s move to the logic inside the transfer hook!

We first verify the presence of a vesting schedule for the token sender by attempting to deserialize the vesting_account.

If no errors arise, it confirms the sender’s tokens are subject to a vesting schedule, which must be enforced. Conversely, if an error occurs, we handle it gracefully without failing the transaction, indicating the sender doesn’t have a vesting schedule and can trade their tokens freely.

This is why we passed the vesting_account as UncheckedAccount, we don’t want the transaction to fail if a VestingAccount is not deserialized directly from the Execute Accounts Struct

Once we check that the VestingAccount exists, we need to enforce it!

To enforce the vesting schedule, we calculate the total amount locked by iterating through the VestingData vector, identifying tokens that haven’t been released yet.

The final step ensures the tokens remaining in the source_token account, minus what the owner intends to transfer, equals or exceeds the locked amount, maintaining the integrity of the vesting schedule.

Congratulations! You have now written your first Transfer Hook! I hope this tutorial has been helpful and that you learned something insightful!

A special thanks to Web3 Builder Alliance Institute which always teaches me cutting-edge concepts on Solana.

If you want to follow my journey and you don’t want to miss out on my next tutorial, follow me on Twitter!

--

--