Procházet zdrojové kódy

Add isTransferring check to transfer hook examples

Jonas Hahn před 1 rokem
rodič
revize
0d61963177

+ 4 - 4
tokens/token-2022/transfer-hook/counter/anchor/programs/transfer-hook/src/lib.rs

@@ -26,7 +26,7 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction;
 declare_id!("1qahDxKHeCLZhbBU2NyMU6vQCQmEUmdeSEBrG5drffK");
 
 #[error_code]
-pub enum MyError {
+pub enum TransferError {
     #[msg("The amount is too big")]
     AmountTooBig,
     #[msg("The token is not currently transferring")]
@@ -60,13 +60,13 @@ pub mod transfer_hook {
         // Check if the amount is too big
         if amount > 50 {
             msg!("The amount is too big: {}", amount);
-            //return err!(MyError::AmountTooBig);
+            //return err!(TransferError::AmountTooBig);
         }
 
         // Increment the transfer count safely
         let count = ctx.accounts.counter_account.counter
             .checked_add(1)
-            .ok_or(MyError::AmountTooBig)?;
+            .ok_or(TransferError::AmountTooBig)?;
 
         msg!("This token has been transferred {} times", count);
 
@@ -81,7 +81,7 @@ fn check_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {
     let account_extension = account.get_extension_mut::<TransferHookAccount>()?;
 
     if !bool::from(account_extension.transferring) {
-        return err!(MyError::IsNotCurrentlyTransferring);
+        return err!(TransferError::IsNotCurrentlyTransferring);
     }
 
     Ok(())

+ 1 - 2
tokens/token-2022/transfer-hook/counter/anchor/tests/transfer-hook.ts

@@ -25,7 +25,6 @@ import { BN } from "bn.js";
 import { expect } from "chai";
 import chai from "chai";
 import chaiAsPromised from "chai-as-promised";
-import { send } from "process";
 
 chai.use(chaiAsPromised);
 
@@ -244,7 +243,7 @@ describe("transfer-hook", () => {
 
     await expect(sendPromise).to.eventually.be.rejectedWith(
       SendTransactionError,
-      /Number: 6001./
+      program.idl.errors[1].msg
     );
   });
 });

+ 3 - 1
tokens/token-2022/transfer-hook/hello-world/anchor/package.json

@@ -9,9 +9,11 @@
   },
   "devDependencies": {
     "@types/bn.js": "^5.1.0",
+    "@types/chai-as-promised": "^7.1.8",
+    "chai-as-promised": "^7.1.2",
     "@types/chai": "^4.3.0",
-    "@types/mocha": "^9.0.0",
     "chai": "^4.3.4",
+    "@types/mocha": "^9.0.0",
     "mocha": "^9.0.3",
     "prettier": "^2.6.2",
     "ts-mocha": "^10.0.0",

+ 18 - 2
tokens/token-2022/transfer-hook/hello-world/anchor/programs/transfer-hook/src/lib.rs

@@ -1,6 +1,16 @@
+use std::cell::RefMut;
+
 use anchor_lang::prelude::*;
 use anchor_spl::{
     associated_token::AssociatedToken,
+    token_2022::spl_token_2022::{
+        extension::{
+            transfer_hook::TransferHookAccount,
+            BaseStateWithExtensionsMut,
+            PodStateWithExtensionsMut,
+        },
+        pod::PodAccount,
+    },
     token_interface::{
         spl_pod::optional_keys::OptionalNonZeroPubkey,
         spl_token_2022::{
@@ -21,6 +31,12 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction;
 
 declare_id!("jY5DfVksJT8Le38LCaQhz5USeiGu4rUeVSS8QRAMoba");
 
+#[error_code]
+pub enum TransferError {
+    #[msg("The token is not currently transferring")]
+    IsNotCurrentlyTransferring,
+}
+
 #[program]
 pub mod transfer_hook {
     use super::*;
@@ -47,7 +63,7 @@ pub mod transfer_hook {
     }
 
     #[interface(spl_transfer_hook_interface::execute)]
-    pub fn transfer_hook(_ctx: Context<TransferHook>, _amount: u64) -> Result<()> {
+    pub fn transfer_hook(ctx: Context<TransferHook>, _amount: u64) -> Result<()> {
         // Fail this instruction if it is not called from within a transfer hook
         check_is_transferring(&ctx)?;
 
@@ -64,7 +80,7 @@ fn check_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {
     let account_extension = account.get_extension_mut::<TransferHookAccount>()?;
 
     if !bool::from(account_extension.transferring) {
-        return err!(MyError::IsNotCurrentlyTransferring);
+        return err!(TransferError::IsNotCurrentlyTransferring);
     }
 
     Ok(())

+ 95 - 34
tokens/token-2022/transfer-hook/hello-world/anchor/tests/transfer-hook.ts

@@ -1,5 +1,5 @@
-import * as anchor from '@coral-xyz/anchor';
-import type { Program } from '@coral-xyz/anchor';
+import * as anchor from "@coral-xyz/anchor";
+import type { Program } from "@coral-xyz/anchor";
 import {
   ASSOCIATED_TOKEN_PROGRAM_ID,
   TOKEN_2022_PROGRAM_ID,
@@ -7,11 +7,21 @@ import {
   createMintToInstruction,
   createTransferCheckedWithTransferHookInstruction,
   getAssociatedTokenAddressSync,
-} from '@solana/spl-token';
-import { Keypair, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
-import type { TransferHook } from '../target/types/transfer_hook';
-
-describe('transfer-hook', () => {
+} from "@solana/spl-token";
+import {
+  Keypair,
+  Transaction,
+  sendAndConfirmTransaction,
+  SendTransactionError,
+} from "@solana/web3.js";
+import type { TransferHook } from "../target/types/transfer_hook";
+import { expect } from "chai";
+import chai from "chai";
+import chaiAsPromised from "chai-as-promised";
+
+chai.use(chaiAsPromised);
+
+describe("transfer-hook", () => {
   // Configure the client to use the local cluster.
   const provider = anchor.AnchorProvider.env();
   anchor.setProvider(provider);
@@ -30,7 +40,7 @@ describe('transfer-hook', () => {
     wallet.publicKey,
     false,
     TOKEN_2022_PROGRAM_ID,
-    ASSOCIATED_TOKEN_PROGRAM_ID,
+    ASSOCIATED_TOKEN_PROGRAM_ID
   );
 
   // Recipient token account address
@@ -40,21 +50,21 @@ describe('transfer-hook', () => {
     recipient.publicKey,
     false,
     TOKEN_2022_PROGRAM_ID,
-    ASSOCIATED_TOKEN_PROGRAM_ID,
+    ASSOCIATED_TOKEN_PROGRAM_ID
   );
 
-  it('Create Mint with Transfer Hook Extension', async () => {
+  it("Create Mint with Transfer Hook Extension", async () => {
     const transactionSignature = await program.methods
       .initialize(decimals)
       .accounts({ mintAccount: mint.publicKey })
       .signers([mint])
       .rpc({ skipPreflight: true });
-    console.log('Your transaction signature', transactionSignature);
+    console.log("Your transaction signature", transactionSignature);
   });
 
   // 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 () => {
+  it("Create Token Accounts and Mint Tokens", async () => {
     // 100 tokens
     const amount = 100 * 10 ** decimals;
 
@@ -65,7 +75,7 @@ describe('transfer-hook', () => {
         wallet.publicKey,
         mint.publicKey,
         TOKEN_2022_PROGRAM_ID,
-        ASSOCIATED_TOKEN_PROGRAM_ID,
+        ASSOCIATED_TOKEN_PROGRAM_ID
       ),
       createAssociatedTokenAccountInstruction(
         wallet.publicKey,
@@ -73,18 +83,30 @@ describe('transfer-hook', () => {
         recipient.publicKey,
         mint.publicKey,
         TOKEN_2022_PROGRAM_ID,
-        ASSOCIATED_TOKEN_PROGRAM_ID,
+        ASSOCIATED_TOKEN_PROGRAM_ID
       ),
-      createMintToInstruction(mint.publicKey, sourceTokenAccount, wallet.publicKey, amount, [], TOKEN_2022_PROGRAM_ID),
+      createMintToInstruction(
+        mint.publicKey,
+        sourceTokenAccount,
+        wallet.publicKey,
+        amount,
+        [],
+        TOKEN_2022_PROGRAM_ID
+      )
     );
 
-    const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true });
+    const txSig = await sendAndConfirmTransaction(
+      connection,
+      transaction,
+      [wallet.payer],
+      { skipPreflight: true }
+    );
 
     console.log(`Transaction Signature: ${txSig}`);
   });
 
   // Account to store extra accounts required by the transfer hook instruction
-  it('Create ExtraAccountMetaList Account', async () => {
+  it("Create ExtraAccountMetaList Account", async () => {
     const initializeExtraAccountMetaListInstruction = await program.methods
       .initializeExtraAccountMetaList()
       .accounts({
@@ -92,34 +114,73 @@ describe('transfer-hook', () => {
       })
       .instruction();
 
-    const transaction = new Transaction().add(initializeExtraAccountMetaListInstruction);
+    const transaction = new Transaction().add(
+      initializeExtraAccountMetaListInstruction
+    );
 
-    const txSig = await sendAndConfirmTransaction(provider.connection, transaction, [wallet.payer], { skipPreflight: true, commitment: 'confirmed' });
-    console.log('Transaction Signature:', txSig);
+    const txSig = await sendAndConfirmTransaction(
+      provider.connection,
+      transaction,
+      [wallet.payer],
+      { skipPreflight: true, commitment: "confirmed" }
+    );
+    console.log("Transaction Signature:", txSig);
   });
 
-  it('Transfer Hook with Extra Account Meta', async () => {
+  it("Transfer Hook with Extra Account Meta", async () => {
     // 1 tokens
     const amount = 1 * 10 ** decimals;
     const bigIntAmount = BigInt(amount);
 
     // Standard token transfer instruction
-    const transferInstruction = await createTransferCheckedWithTransferHookInstruction(
+    const transferInstruction =
+      await createTransferCheckedWithTransferHookInstruction(
+        connection,
+        sourceTokenAccount,
+        mint.publicKey,
+        destinationTokenAccount,
+        wallet.publicKey,
+        bigIntAmount,
+        decimals,
+        [],
+        "confirmed",
+        TOKEN_2022_PROGRAM_ID
+      );
+
+    const transaction = new Transaction().add(transferInstruction);
+
+    const txSig = await sendAndConfirmTransaction(
       connection,
-      sourceTokenAccount,
-      mint.publicKey,
-      destinationTokenAccount,
-      wallet.publicKey,
-      bigIntAmount,
-      decimals,
-      [],
-      'confirmed',
-      TOKEN_2022_PROGRAM_ID,
+      transaction,
+      [wallet.payer],
+      { skipPreflight: true }
     );
+    console.log("Transfer Signature:", txSig);
+  });
 
-    const transaction = new Transaction().add(transferInstruction);
+  it("Try call transfer hook without transfer", async () => {
+    const transferHookIx = await program.methods
+      .transferHook(new anchor.BN(1))
+      .accounts({
+        sourceToken: sourceTokenAccount,
+        mint: mint.publicKey,
+        destinationToken: destinationTokenAccount,
+        owner: wallet.publicKey,
+      })
+      .instruction();
 
-    const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true });
-    console.log('Transfer Signature:', txSig);
+    const transaction = new Transaction().add(transferHookIx);
+
+    const sendPromise = sendAndConfirmTransaction(
+      connection,
+      transaction,
+      [wallet.payer],
+      { skipPreflight: false }
+    );
+
+    await expect(sendPromise).to.eventually.be.rejectedWith(
+      SendTransactionError,
+      program.idl.errors[0].msg
+    );
   });
 });

+ 119 - 98
tokens/token-2022/transfer-hook/transfer-cost/anchor/programs/transfer-hook/src/lib.rs

@@ -1,12 +1,22 @@
-use std::str::FromStr;
-use anchor_lang::{
-    prelude::*, solana_program::pubkey::Pubkey, 
-};
+use std::{ cell::RefMut, str::FromStr };
+use anchor_lang::{ prelude::*, solana_program::pubkey::Pubkey };
 use anchor_spl::{
-    associated_token::AssociatedToken, token::Token, token_interface::{transfer_checked, Mint, TokenAccount, TransferChecked}
+    associated_token::AssociatedToken,
+    token::Token,
+    token_2022::spl_token_2022::{
+        extension::{
+            transfer_hook::TransferHookAccount,
+            BaseStateWithExtensionsMut,
+            PodStateWithExtensionsMut,
+        },
+        pod::PodAccount,
+    },
+    token_interface::{ transfer_checked, Mint, TokenAccount, TransferChecked },
 };
 use spl_tlv_account_resolution::{
-    account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
+    account::ExtraAccountMeta,
+    seeds::Seed,
+    state::ExtraAccountMetaList,
 };
 use spl_transfer_hook_interface::instruction::ExecuteInstruction;
 
@@ -16,9 +26,11 @@ use spl_transfer_hook_interface::instruction::ExecuteInstruction;
 declare_id!("FjcHckEgXcBhFmSGai3FRpDLiT6hbpV893n8iTxVd81g");
 
 #[error_code]
-pub enum MyError {
-   #[msg("Amount Too big")]
-   AmountTooBig,
+pub enum TransferError {
+    #[msg("Amount Too big")]
+    AmountTooBig,
+    #[msg("The token is not currently transferring")]
+    IsNotCurrentlyTransferring,
 }
 
 #[program]
@@ -27,14 +39,14 @@ pub mod transfer_hook {
 
     #[interface(spl_transfer_hook_interface::initialize_extra_account_meta_list)]
     pub fn initialize_extra_account_meta_list(
-        ctx: Context<InitializeExtraAccountMetaList>,
+        ctx: Context<InitializeExtraAccountMetaList>
     ) -> Result<()> {
         let extra_account_metas = InitializeExtraAccountMetaList::extra_account_metas()?;
-      
+
         // initialize ExtraAccountMetaList account with extra accounts
         ExtraAccountMetaList::init::<ExecuteInstruction>(
             &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
-            &extra_account_metas,
+            &extra_account_metas
         )?;
 
         Ok(())
@@ -42,10 +54,12 @@ pub mod transfer_hook {
 
     #[interface(spl_transfer_hook_interface::execute)]
     pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
+        // Fail this instruction if it is not called from within a transfer hook
+        check_is_transferring(&ctx)?;
 
         if amount > 50 {
-            //msg!("The amount is too big {0}", amount);
-            //return err!(MyError::AmountTooBig);
+            msg!("The amount is too big {0}", amount);
+            //return err!(TransferError::AmountTooBig);
         }
 
         ctx.accounts.counter_account.counter += 1;
@@ -54,7 +68,10 @@ pub mod transfer_hook {
 
         // All accounts are non writable so you can not burn any of them for example here
         msg!("Is writable mint {0}", ctx.accounts.mint.to_account_info().is_writable);
-        msg!("Is destination mint {0}", ctx.accounts.destination_token.to_account_info().is_writable);
+        msg!(
+            "Is destination mint {0}",
+            ctx.accounts.destination_token.to_account_info().is_writable
+        );
         msg!("Is source mint {0}", ctx.accounts.source_token.to_account_info().is_writable);
 
         let signer_seeds: &[&[&[u8]]] = &[&[b"delegate", &[ctx.bumps.delegate]]];
@@ -62,23 +79,32 @@ pub mod transfer_hook {
         // Transfer WSOL from sender to delegate token account using delegate PDA
         // transfer lamports amount equal to token transfer amount
         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),
+            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,
+            ctx.accounts.wsol_mint.decimals
         )?;
         Ok(())
     }
 }
 
+fn check_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {
+    let source_token_info = ctx.accounts.source_token.to_account_info();
+    let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?;
+    let mut account = PodStateWithExtensionsMut::<PodAccount>::unpack(*account_data_ref)?;
+    let account_extension = account.get_extension_mut::<TransferHookAccount>()?;
+
+    if !bool::from(account_extension.transferring) {
+        return err!(TransferError::IsNotCurrentlyTransferring);
+    }
+
+    Ok(())
+}
+
 #[derive(Accounts)]
 pub struct InitializeExtraAccountMetaList<'info> {
     #[account(mut)]
@@ -87,20 +113,16 @@ pub struct InitializeExtraAccountMetaList<'info> {
     /// CHECK: ExtraAccountMetaList Account, must use these seeds
     #[account(
         init,
-        seeds = [b"extra-account-metas", mint.key().as_ref()], 
+        seeds = [b"extra-account-metas", mint.key().as_ref()],
         bump,
-        space = ExtraAccountMetaList::size_of(InitializeExtraAccountMetaList::extra_account_metas()?.len())?,
-        payer = payer,
+        space = ExtraAccountMetaList::size_of(
+            InitializeExtraAccountMetaList::extra_account_metas()?.len()
+        )?,
+        payer = payer
     )]
     pub extra_account_meta_list: AccountInfo<'info>,
     pub mint: InterfaceAccount<'info, Mint>,
-    #[account(
-        init,
-        seeds = [b"counter"],
-        bump,
-        payer = payer,
-        space = 9
-    )]
+    #[account(init, seeds = [b"counter"], bump, payer = payer, space = 9)]
     pub counter_account: Account<'info, CounterAccount>,
     pub system_program: Program<'info, System>,
 }
@@ -113,51 +135,61 @@ impl<'info> InitializeExtraAccountMetaList<'info> {
 
         // index 0-3 are the accounts required for token transfer (source, mint, destination, owner)
         // index 4 is address of ExtraAccountMetaList account
-        Ok(vec![
-            // index 5, wrapped SOL mint
-            ExtraAccountMeta::new_with_pubkey(&Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(), false, false)?,
-            // index 6, token program (for wsol token transfer)
-            ExtraAccountMeta::new_with_pubkey(&Token::id(), false, false)?,
-            // index 7, associated token program
-            ExtraAccountMeta::new_with_pubkey(&AssociatedToken::id(), false, false)?,
-            // index 8, delegate PDA
-            ExtraAccountMeta::new_with_seeds(
-                &[Seed::Literal {
-                    bytes: b"delegate".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
-            )?,
-            ExtraAccountMeta::new_with_seeds(
-                &[Seed::Literal {
-                    bytes: b"counter".to_vec(),
-                }],
-                false, // is_signer
-                true,  // is_writable
-            )?,
-        ])
+        Ok(
+            vec![
+                // index 5, wrapped SOL mint
+                ExtraAccountMeta::new_with_pubkey(
+                    &Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(),
+                    false,
+                    false
+                )?,
+                // index 6, token program (for wsol token transfer)
+                ExtraAccountMeta::new_with_pubkey(&Token::id(), false, false)?,
+                // index 7, associated token program
+                ExtraAccountMeta::new_with_pubkey(&AssociatedToken::id(), false, false)?,
+                // index 8, delegate PDA
+                ExtraAccountMeta::new_with_seeds(
+                    &[
+                        Seed::Literal {
+                            bytes: b"delegate".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
+                )?,
+                ExtraAccountMeta::new_with_seeds(
+                    &[
+                        Seed::Literal {
+                            bytes: b"counter".to_vec(),
+                        },
+                    ],
+                    false, // is_signer
+                    true // is_writable
+                )?
+            ]
+        )
     }
 }
 
@@ -167,26 +199,18 @@ impl<'info> InitializeExtraAccountMetaList<'info> {
 // 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,
-    )]
+    #[account(token::mint = mint, token::authority = owner)]
     pub source_token: InterfaceAccount<'info, TokenAccount>,
     pub mint: InterfaceAccount<'info, Mint>,
-    #[account(
-        token::mint = 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
-    )]
+    #[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:Program<'info, Token>,
+    pub token_program: Program<'info, Token>,
     pub associated_token_program: Program<'info, AssociatedToken>,
     #[account(
         mut,
@@ -206,14 +230,11 @@ pub struct TransferHook<'info> {
         token::authority = owner,
     )]
     pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>,
-    #[account(
-        seeds = [b"counter"],
-        bump
-    )]
+    #[account(seeds = [b"counter"], bump)]
     pub counter_account: Account<'info, CounterAccount>,
 }
 
 #[account]
 pub struct CounterAccount {
-    counter: u8
+    counter: u8,
 }

+ 3 - 1
tokens/token-2022/transfer-hook/whitelist/anchor/package.json

@@ -9,9 +9,11 @@
   },
   "devDependencies": {
     "@types/bn.js": "^5.1.0",
+    "@types/chai-as-promised": "^7.1.8",
+    "chai-as-promised": "^7.1.2",
     "@types/chai": "^4.3.0",
-    "@types/mocha": "^9.0.0",
     "chai": "^4.3.4",
+    "@types/mocha": "^9.0.0",
     "mocha": "^9.0.3",
     "prettier": "^2.6.2",
     "ts-mocha": "^10.0.0",

+ 64 - 41
tokens/token-2022/transfer-hook/whitelist/anchor/programs/transfer-hook/src/lib.rs

@@ -1,18 +1,39 @@
+use std::cell::RefMut;
+
 use anchor_lang::prelude::*;
-use anchor_spl::token_interface::{Mint, TokenAccount};
-use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList};
+use anchor_spl::{
+    token_2022::spl_token_2022::{
+        extension::{
+            transfer_hook::TransferHookAccount,
+            BaseStateWithExtensionsMut,
+            PodStateWithExtensionsMut,
+        },
+        pod::PodAccount,
+    },
+    token_interface::{ Mint, TokenAccount },
+};
+use spl_tlv_account_resolution::{
+    account::ExtraAccountMeta,
+    seeds::Seed,
+    state::ExtraAccountMetaList,
+};
 use spl_transfer_hook_interface::instruction::ExecuteInstruction;
 
 declare_id!("DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub");
 
+#[error_code]
+pub enum TransferError {
+    #[msg("The token is not currently transferring")]
+    IsNotCurrentlyTransferring,
+}
+
 #[program]
 pub mod transfer_hook {
-
     use super::*;
 
     #[interface(spl_transfer_hook_interface::initialize_extra_account_meta_list)]
     pub fn initialize_extra_account_meta_list(
-        ctx: Context<InitializeExtraAccountMetaList>,
+        ctx: Context<InitializeExtraAccountMetaList>
     ) -> Result<()> {
         // set authority field on white_list account as payer address
         ctx.accounts.white_list.authority = ctx.accounts.payer.key();
@@ -22,13 +43,15 @@ pub mod transfer_hook {
         // initialize ExtraAccountMetaList account with extra accounts
         ExtraAccountMetaList::init::<ExecuteInstruction>(
             &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
-            &extra_account_metas,
+            &extra_account_metas
         )?;
         Ok(())
     }
 
     #[interface(spl_transfer_hook_interface::execute)]
     pub fn transfer_hook(ctx: Context<TransferHook>, _amount: u64) -> Result<()> {
+        // Fail this instruction if it is not called from within a transfer hook
+        check_is_transferring(&ctx)?;
 
         if !ctx.accounts.white_list.white_list.contains(&ctx.accounts.destination_token.key()) {
             panic!("Account not in white list!");
@@ -40,7 +63,6 @@ pub mod transfer_hook {
     }
 
     pub fn add_to_whitelist(ctx: Context<AddToWhiteList>) -> Result<()> {
-
         if ctx.accounts.white_list.authority != ctx.accounts.signer.key() {
             panic!("Only the authority can add to the white list!");
         }
@@ -51,7 +73,19 @@ pub mod transfer_hook {
 
         Ok(())
     }
+}
+
+fn check_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {
+    let source_token_info = ctx.accounts.source_token.to_account_info();
+    let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?;
+    let mut account = PodStateWithExtensionsMut::<PodAccount>::unpack(*account_data_ref)?;
+    let account_extension = account.get_extension_mut::<TransferHookAccount>()?;
 
+    if !bool::from(account_extension.transferring) {
+        return err!(TransferError::IsNotCurrentlyTransferring);
+    }
+
+    Ok(())
 }
 
 #[derive(Accounts)]
@@ -62,36 +96,36 @@ pub struct InitializeExtraAccountMetaList<'info> {
     /// CHECK: ExtraAccountMetaList Account, must use these seeds
     #[account(
         init,
-        seeds = [b"extra-account-metas", mint.key().as_ref()], 
+        seeds = [b"extra-account-metas", mint.key().as_ref()],
         bump,
-        space = ExtraAccountMetaList::size_of(InitializeExtraAccountMetaList::extra_account_metas()?.len())?,
-        payer = payer,
+        space = ExtraAccountMetaList::size_of(
+            InitializeExtraAccountMetaList::extra_account_metas()?.len()
+        )?,
+        payer = payer
     )]
     pub extra_account_meta_list: AccountInfo<'info>,
     pub mint: InterfaceAccount<'info, Mint>,
     pub system_program: Program<'info, System>,
-    #[account(
-        init_if_needed,
-        seeds = [b"white_list"],
-        bump,
-        payer = payer,
-        space = 400
-    )]
+    #[account(init_if_needed, seeds = [b"white_list"], bump, payer = payer, space = 400)]
     pub white_list: Account<'info, WhiteList>,
 }
 
 // Define extra account metas to store on extra_account_meta_list account
 impl<'info> InitializeExtraAccountMetaList<'info> {
     pub fn extra_account_metas() -> Result<Vec<ExtraAccountMeta>> {
-        Ok(vec![
-            ExtraAccountMeta::new_with_seeds(
-                &[Seed::Literal {
-                    bytes: "white_list".as_bytes().to_vec(),
-                }], 
-                false, // is_signer
-                true,  // is_writable
-            )?,
-        ])
+        Ok(
+            vec![
+                ExtraAccountMeta::new_with_seeds(
+                    &[
+                        Seed::Literal {
+                            bytes: "white_list".as_bytes().to_vec(),
+                        },
+                    ],
+                    false, // is_signer
+                    true // is_writable
+                )?
+            ]
+        )
     }
 }
 
@@ -101,28 +135,17 @@ impl<'info> InitializeExtraAccountMetaList<'info> {
 // 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,
-    )]
+    #[account(token::mint = mint, token::authority = owner)]
     pub source_token: InterfaceAccount<'info, TokenAccount>,
     pub mint: InterfaceAccount<'info, Mint>,
-    #[account(
-        token::mint = 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
-    )]
+    #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)]
     pub extra_account_meta_list: UncheckedAccount<'info>,
-    #[account(
-        seeds = [b"white_list"],
-        bump
-    )]
+    #[account(seeds = [b"white_list"], bump)]
     pub white_list: Account<'info, WhiteList>,
 }
 
@@ -145,4 +168,4 @@ pub struct AddToWhiteList<'info> {
 pub struct WhiteList {
     pub authority: Pubkey,
     pub white_list: Vec<Pubkey>,
-}
+}