diff --git a/content/guides/token-extensions/transfer-hook.md b/content/guides/token-extensions/transfer-hook.md new file mode 100644 index 000000000..e6f0f6d6c --- /dev/null +++ b/content/guides/token-extensions/transfer-hook.md @@ -0,0 +1,1299 @@ +--- +date: Jan 08, 2023 +seoTitle: "Token Extensions: Transfer Hook" +title: How to use the Transfer Hook extension +description: "The Transfer Hook extension and Transfer Hook Interface introduce the ability to +create Mint Accounts that execute custom instruction logic on every token +transfer." +keywords: + - token 2022 + - token extensions + - token program +difficulty: intermediate +tags: + - token 2022 + - token extensions +--- + +The Transfer Hook extension and Transfer Hook Interface introduce the ability to +create Mint Accounts that execute custom instruction logic on every token +transfer. + +To achieve this, developers must build a program that implements the +[Transfer Hook Interface](https://github.com/solana-labs/solana-program-library/tree/master/token/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. This program will require the sender to pay a fee in wrapped SOL +(wSOL) on every token transfer. Here is the +[final program](https://beta.solpg.io/https://github.com/solana-developers/anchor-transfer-hook/tree/main). + +## 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](https://github.com/solana-labs/solana-program-library/blob/master/token/transfer-hook/interface/src/instruction.rs): + +- `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 custom `Execute` 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 + +```js +const [pda] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId, // 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. + +## Program Overview + +In this guide, we will build a 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: + +1. `initialize_extra_account_meta_list`: Creates an account that stores a list + of extra accounts required by the `transfer_hook` instruction. +2. `transfer_hook`: This instruction is invoked via CPI on every token transfer + to perform a wrapped SOL token transfer. +3. `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 custom `transfer_hook` instruction. + +## Getting Started + +Start by opening this Solana Playground +[link](https://beta.solpg.io/https://github.com/solana-developers/anchor-transfer-hook/tree/starter) +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: + +```rust +use anchor_lang::{ + prelude::*, + system_program::{create_account, CreateAccount}, +}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked}, +}; +use spl_tlv_account_resolution::{ + account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, +}; +use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}; + +declare_id!("E6wu6Nykdra8gXs57Zqo7hY6DLaWugTmD3uuuBmX2Vxt"); + +#[program] +pub mod transfer_hook { + use super::*; + + pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + Ok(()) + } + + pub fn transfer_hook(ctx: Context, amount: u64) -> Result<()> { + Ok(()) + } + + pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], + ) -> Result<()> { + Ok(()) + } +} + +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList {} + +#[derive(Accounts)] +pub struct TransferHook {} +``` + +Once you've imported the project, build the program by using the `build` command +in the Playground terminal. + +``` +build +``` + +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 our `transfer_hook` instruction. +- `mint`: The Mint Account that points to this Transfer Hook program. The mint + address is a required seed for deriving the `extra_account_meta_list` PDA. +- `wsol_mint`: The wrapped SOL mint. +- `token_program`: The original Token program ID +- `associated_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: + +```rust +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList {} +``` + +With the code provided below: + +```rust +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList Account, must use these seeds + #[account( + mut, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + pub wsol_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} +``` + +Next, update the `initialize_extra_account_meta_list`instruction by replacing +the following starter code: + +```rust +pub fn initialize_extra_account_meta_list( + ctx: Context, +) -> Result<()> { + Ok(()) +} +``` + +With the code below: + +```rust +pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + // index 0-3 are the accounts required for token transfer (source, mint, destination, owner) + // index 4 is address of ExtraAccountMetaList account + // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly + let account_metas = vec![ + // index 5, wrapped SOL mint + ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?, + // index 6, token program + ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?, + // index 7, associated token program + ExtraAccountMeta::new_with_pubkey( + &ctx.accounts.associated_token_program.key(), + false, + false, + )?, + // index 8, delegate PDA + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "delegate".as_bytes().to_vec(), + }], + false, // is_signer + false, // is_writable + )?, + // index 9, delegate wrapped SOL token account + ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 8 }, // owner index (delegate PDA) + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable + )?, + // index 10, sender wrapped SOL token account + ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 3 }, // owner index + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable + )?, + ]; + + // calculate account size + let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; + // calculate minimum required lamports + let lamports = Rent::get()?.minimum_balance(account_size as usize); + + let mint = ctx.accounts.mint.key(); + let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + &mint.as_ref(), + &[ctx.bumps.extra_account_meta_list], + ]]; + + // create ExtraAccountMetaList account + create_account( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + CreateAccount { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.extra_account_meta_list.to_account_info(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + ctx.program_id, + )?; + + // initialize ExtraAccountMetaList account with extra accounts + ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, + )?; + + Ok(()) + } +``` + +Let's walk through the updated instruction logic. We begin by listing the +additional accounts that need to be stored on the ExtraAccountMetas account. + +```rust +// index 0-3 are the accounts required for token transfer (source, mint, destination, owner) +// index 4 is address of ExtraAccountMetaList account +// The `addExtraAccountsToInstruction` JS helper function resolving incorrectly +let account_metas = vec![ + // index 5, wrapped SOL mint + ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?, + // index 6, token program + ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?, + // index 7, associated token program + ExtraAccountMeta::new_with_pubkey( + &ctx.accounts.associated_token_program.key(), + false, + false, + )?, + // index 8, delegate PDA + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "delegate".as_bytes().to_vec(), + }], + false, // is_signer + true, // is_writable + )?, + // index 9, delegate wrapped SOL token account + ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 8 }, // owner index (delegate PDA) + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable + )?, + // index 10, sender wrapped SOL token account + ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 3 }, // owner index + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable + )?, +]; +``` + +There are three methods for storing these accounts: + +1. Directly store the account address: + - Wrapped SOL mint address + - Token Program ID + - Associated Token Program ID + +```rust +// index 5, wrapped SOL mint +ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?, +// index 6, token program +ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?, +// index 7, associated token program +ExtraAccountMeta::new_with_pubkey( + &ctx.accounts.associated_token_program.key(), + false, + false, +)?, + +``` + +2. Store the seeds to derive a PDA for the Transfer Hook program: + - Delegate PDA + +```rust +// index 8, delegate PDA +ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "delegate".as_bytes().to_vec(), + }], + false, // is_signer + false, // is_writable +)?, + +``` + +3. 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 + +```rust +// index 9, delegate wrapped SOL token account +ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 8 }, // owner index (delegate PDA) + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable +)?, +// index 10, sender wrapped SOL token account +ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 3 }, // owner index + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable +)?, +``` + +Next, we calculate the size and rent required to store the list of +ExtraAccountMetas. + +```rust +// calculate account size +let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; +// calculate minimum required lamports +let lamports = Rent::get()?.minimum_balance(account_size as usize); +``` + +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. + +```rust +let mint = ctx.accounts.mint.key(); +let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + &mint.as_ref(), + &[ctx.bumps.extra_account_meta_list], +]]; + +// create ExtraAccountMetaList account +create_account( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + CreateAccount { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.extra_account_meta_list.to_account_info(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + ctx.program_id, +)?; +``` + +Once we’ve created the account, we initialize the account data to store the list +of ExtraAccountMetas. + +```rust +// initialize ExtraAccountMetaList account with extra accounts +ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, +)?; +``` + + + +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: + +```rust +#[derive(Accounts)] +pub struct TransferHook {} +``` + +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. + + + +```rust +// Order of accounts matters for this struct. +// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner) +// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account +// These accounts are provided via CPI to this program from the token2022 program +#[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>, + pub wsol_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + #[account( + seeds = [b"delegate"], + bump + )] + pub delegate: SystemAccount<'info>, + #[account( + mut, + token::mint = wsol_mint, + token::authority = delegate, + )] + pub delegate_wsol_token_account: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + token::mint = wsol_mint, + token::authority = owner, + )] + pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>, +} +``` + +The first 4 accounts are the accounts required by the initial token transfer. + +```rust +#[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>, +``` + +The 5th account is the address of the ExtraAccountMeta account that stores the +list of extra accounts required by our `transfer_hook` instruction. + +```rust +/// CHECK: ExtraAccountMetaList Account +#[account( + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump +)] +pub extra_account_meta_list: UncheckedAccount<'info>, +``` + +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. + +```rust +pub wsol_mint: InterfaceAccount<'info, Mint>, +pub token_program: Interface<'info, TokenInterface>, +pub associated_token_program: Program<'info, AssociatedToken>, +#[account( + mut, + seeds = [b"delegate"], + bump +)] +pub delegate: SystemAccount<'info>, +#[account( + mut, + token::mint = wsol_mint, + token::authority = delegate, +)] +pub delegate_wsol_token_account: InterfaceAccount<'info, TokenAccount>, +#[account( + mut, + token::mint = wsol_mint, + token::authority = owner, +)] +pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>, +``` + +Next, update the `transfer_hook` instruction by replacing the following starter +code: + +```rust +pub fn transfer_hook(ctx: Context, amount: u64) -> Result<()> { + Ok(()) +} +``` + +With the updated code below: + +```rust +// Require SOL fee on transfer, lamport fee is equal to transfer amount +// If this fails, the initial token transfer fails +pub fn transfer_hook(ctx: Context, amount: u64) -> Result<()> { + msg!("Transfer WSOL using delegate PDA"); + + let signer_seeds: &[&[&[u8]]] = &[&[b"delegate", &[ctx.bumps.delegate]]]; + + // transfer WSOL from sender to delegate token account using delegate PDA + transfer_checked( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + TransferChecked { + from: ctx.accounts.sender_wsol_token_account.to_account_info(), + mint: ctx.accounts.wsol_mint.to_account_info(), + to: ctx.accounts.delegate_wsol_token_account.to_account_info(), + authority: ctx.accounts.delegate.to_account_info(), + }, + ) + .with_signer(signer_seeds), + amount, + ctx.accounts.wsol_mint.decimals, + )?; + Ok(()) +} +``` + +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: + +```rust +pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], +) -> Result<()> { + Ok(()) +} +``` + +With the updated code below: + +```rust +// 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()), + } +} +``` + +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: + +``` +build +``` + +Next, deploy the program using the command: + +``` +deploy +``` + +## Test File Overview + +Next, let's test the program. Open the `transfer-hook.test.ts` file, and you +should see the following starter code: + +```js +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { TransferHook } from "../target/types/transfer_hook"; +import { + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, + Keypair, +} from "@solana/web3.js"; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + getMintLen, + createInitializeMintInstruction, + createInitializeTransferHookInstruction, + addExtraAccountsToInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createMintToInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + createApproveInstruction, + createSyncNativeInstruction, + NATIVE_MINT, + TOKEN_PROGRAM_ID, + getAccount, + getOrCreateAssociatedTokenAccount, +} from "@solana/spl-token"; +import assert from "assert"; + +describe("transfer-hook", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.TransferHook as Program; + const wallet = provider.wallet as anchor.Wallet; + const connection = provider.connection; + + // Generate keypair to use as address for the transfer-hook enabled mint + const mint = new Keypair(); + const decimals = 9; + + // Sender token account address + const sourceTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + wallet.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // Recipient token account address + const recipient = Keypair.generate(); + const destinationTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // ExtraAccountMetaList address + // Store extra accounts required by the custom transfer hook instruction + const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId + ); + + // PDA delegate to transfer wSOL tokens from sender + const [delegatePDA] = PublicKey.findProgramAddressSync( + [Buffer.from("delegate")], + program.programId + ); + + // Sender wSOL token account address + const senderWSolTokenAccount = getAssociatedTokenAddressSync( + NATIVE_MINT, // mint + wallet.publicKey // owner + ); + + // Delegate PDA wSOL token account address, to receive wSOL tokens from sender + const delegateWSolTokenAccount = getAssociatedTokenAddressSync( + NATIVE_MINT, // mint + delegatePDA, // owner + true // allowOwnerOffCurve + ); + + // Create the two WSol token accounts as part of setup + before(async () => { + // WSol Token Account for sender + await getOrCreateAssociatedTokenAccount( + connection, + wallet.payer, + NATIVE_MINT, + wallet.publicKey + ); + + // WSol Token Account for delegate PDA + await getOrCreateAssociatedTokenAccount( + connection, + wallet.payer, + NATIVE_MINT, + delegatePDA, + true + ); + }); + + it("Create Mint Account with Transfer Hook Extension", async () => {}); + + it("Create Token Accounts and Mint Tokens", async () => {}); + + it("Create ExtraAccountMetaList Account", async () => {}); + + it("Transfer Hook with Extra Account Meta", async () => {}); +}); +``` + +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. + +```js +// Generate keypair to use as address for the transfer-hook enabled mint +const mint = new Keypair(); +const decimals = 9; + +// Sender token account address +const sourceTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + wallet.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, +); + +// Recipient token account address +const recipient = Keypair.generate(); +const destinationTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, +); +``` + +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. + +```js +// ExtraAccountMetaList address +// Store extra accounts required by the custom transfer hook instruction +const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId, +); +``` + +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. + +```js +// PDA delegate to transfer wSOL tokens from sender +const [delegatePDA] = PublicKey.findProgramAddressSync( + [Buffer.from("delegate")], + program.programId, +); +``` + +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. + +```js +// Sender wSOL token account address +const senderWSolTokenAccount = getAssociatedTokenAddressSync( + NATIVE_MINT, // mint + wallet.publicKey, // owner +); + +// Delegate PDA wSOL token account address, to receive wSOL tokens from sender +const delegateWSolTokenAccount = getAssociatedTokenAddressSync( + NATIVE_MINT, // mint + delegatePDA, // owner + true, // allowOwnerOffCurve +); +``` + +Finally, as part of the setup, we create the wSOL token accounts. + +```js +// Create the two WSol token accounts as part of setup +before(async () => { + // WSol Token Account for sender + await getOrCreateAssociatedTokenAccount( + connection, + wallet.payer, + NATIVE_MINT, + wallet.publicKey, + ); + + // WSol Token Account for delegate PDA + await getOrCreateAssociatedTokenAccount( + connection, + wallet.payer, + NATIVE_MINT, + delegatePDA, + true, + ); +}); +``` + +## 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: + +```js +it("Create Mint Account with Transfer Hook Extension", async () => {}); +``` + +With the updated test below: + +```js +it("Create Mint Account with Transfer Hook Extension", async () => { + const extensions = [ExtensionType.TransferHook]; + const mintLen = getMintLen(extensions); + const lamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports: lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferHookInstruction( + mint.publicKey, + wallet.publicKey, + program.programId, // Transfer Hook Program ID + TOKEN_2022_PROGRAM_ID, + ), + createInitializeMintInstruction( + mint.publicKey, + decimals, + wallet.publicKey, + null, + TOKEN_2022_PROGRAM_ID, + ), + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer, mint], + ); + console.log(`Transaction Signature: ${txSig}`); +}); +``` + +## 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: + +```js +it("Create Token Accounts and Mint Tokens", async () => {}); +``` + +With the updated test below: + +```js +// Create the two token accounts for the transfer-hook enabled mint +// Fund the sender token account with 100 tokens +it("Create Token Accounts and Mint Tokens", async () => { + // 100 tokens + const amount = 100 * 10 ** decimals; + + const transaction = new Transaction().add( + createAssociatedTokenAccountInstruction( + wallet.publicKey, + sourceTokenAccount, + wallet.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + createAssociatedTokenAccountInstruction( + wallet.publicKey, + destinationTokenAccount, + recipient.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + createMintToInstruction( + mint.publicKey, + sourceTokenAccount, + wallet.publicKey, + amount, + [], + TOKEN_2022_PROGRAM_ID, + ), + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true }, + ); + + console.log(`Transaction Signature: ${txSig}`); +}); +``` + +## 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: + +```js +it("Create ExtraAccountMetaList Account", async () => {}); +``` + +With the updated test below: + +```js +// Account to store extra accounts required by the transfer hook instruction +it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = await program.methods + .initializeExtraAccountMetaList() + .accounts({ + payer: wallet.publicKey, + extraAccountMetaList: extraAccountMetaListPDA, + mint: mint.publicKey, + wsolMint: NATIVE_MINT, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction, + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer], + { skipPreflight: true }, + ); + console.log("Transaction Signature:", txSig); +}); +``` + +## 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: + +```js +it("Transfer Hook with Extra Account Meta", async () => {}); +``` + +With the updated test below: + +```js +it("Transfer Hook with Extra Account Meta", async () => { + // 1 tokens + const amount = 1 * 10 ** decimals; + + // Instruction for sender to fund their WSol token account + const solTransferInstruction = SystemProgram.transfer({ + fromPubkey: wallet.publicKey, + toPubkey: senderWSolTokenAccount, + lamports: amount, + }); + + // Approve delegate PDA to transfer WSol tokens from sender WSol token account + const approveInstruction = createApproveInstruction( + senderWSolTokenAccount, + delegatePDA, + wallet.publicKey, + amount, + [], + TOKEN_PROGRAM_ID, + ); + + // Sync sender WSol token account + const syncWrappedSolInstruction = createSyncNativeInstruction( + senderWSolTokenAccount, + ); + + // Standard token transfer instruction + const transferInstruction = createTransferCheckedInstruction( + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + amount, + decimals, + [], + TOKEN_2022_PROGRAM_ID, + ); + + // Automatic account resolution not working correctly for the WSol PDA + // Manually add all the extra accounts required by the transfer hook instruction + // Also include the address of the ExtraAccountMetaList account and Transfer Hook Program + transferInstruction.keys.push( + { + pubkey: new PublicKey("So11111111111111111111111111111111111111112"), + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + isSigner: false, + isWritable: false, + }, + { + pubkey: new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), + isSigner: false, + isWritable: false, + }, + { + pubkey: delegatePDA, + isSigner: false, + isWritable: false, + }, + { + pubkey: delegateWSolTokenAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: senderWSolTokenAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: program.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: extraAccountMetaListPDA, + isSigner: false, + isWritable: false, + }, + ); + + const transaction = new Transaction().add( + solTransferInstruction, + syncWrappedSolInstruction, + approveInstruction, + transferInstruction, + ); + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true }, + ); + console.log("Transfer Signature:", txSig); +}); +``` + +The transfer instruction must include all additional AccountMetas, the address +of the ExtraAccountMetas account, and the address of the Transfer Hook program. + + + +In this example, we manually add the accounts to the transaction. The +`@solana/spl-token` library provides a helper function to automatically resolve +the necessary accounts. However, this function currently does not work if a seed +for a PDA is also included on the ExtraAccountMetas account. + + + +## 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: + +``` +test +``` + +You should see output similar to the following: + +``` +Running tests... + transfer-hook.test.ts: + transfer-hook + Transaction Signature: 5o12ZTvcSkV8YNqyeQpzRCq4zFSg9VqguQkT9ZSesioj8uzb8dWRheoknuPaRDDqEGdrUBqmRQ2veSUshUicWsqG + ✔ Create Mint Account with Transfer Hook Extension (996ms) + Transaction Signature: 4F4Vhi8s1h2reDr6jecvuQFF5XpoofWPpshgAMnfg7jtNZj4HtxbsksFTh28ZjYTaKFpjeturYZKxk5Cj4gBZoy + ✔ Create Token Accounts and Mint Tokens (716ms) + Transaction Signature: 3s4Nok6H4qexpGXup3AWC4nGuiqy567rm5rTWLFMXYKxZJensBVVZHCwVDzpwD3XtWjMFHm4TrvQXwKSsp47y5jx + ✔ Create ExtraAccountMetaList Account (711ms) + Transfer Signature: 53j9QV5LYUVgV7T7Z99GfYg1Xvp2qbQnHsJbzDK6BR5TPBo9s622KCf3W3BDEL4ECprkZFs5biDRDedfVj6zuDA6 + ✔ Transfer Hook with Extra Account Meta (925ms) + 4 passing (5s) +``` + +## 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!