1
    use anchor_lang::{
    prelude::*,
    system_program::{create_account, CreateAccount},
    };
    use anchor_spl::{
    associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, 
    TokenInterface}
    };
    use spl_tlv_account_resolution::state::ExtraAccountMetaList;
    use spl_transfer_hook_interface::instruction::{ExecuteInstruction, 
    TransferHookInstruction};
    use anchor_spl::token_2022::burn;
    use anchor_spl::token_2022;
    use spl_tlv_account_resolution::account::ExtraAccountMeta;
    use spl_tlv_account_resolution::seeds::Seed;

    declare_id!("AXisjazgSH9etw9bwuyCCrwtTU4556puPB4B8oah3Est");

    #[program]
    pub mod transfer_hook {

    use super::*;

    pub fn initialize_extra_account_meta_list(
        ctx: Context<InitializeExtraAccountMetaList>,
    ) -> Result<()> {
        // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly
        let account_metas = vec![
            ExtraAccountMeta::new_with_pubkey(&token_2022::ID, false, false)?,
            ExtraAccountMeta::new_with_seeds(
                &[Seed::Literal {
                    bytes: "burn".as_bytes().to_vec(),
                }, Seed::AccountKey { index: 1 }],
                false, // is_signer
                false,  // 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::<ExecuteInstruction>(
            &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
            &account_metas,
        )?;

        Ok(())
    }

    pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
        msg!("Hello Transfer Hook Before!");
        msg!("token program: {}", ctx.accounts.token_program.to_account_info().key());
        msg!("burn auth: {}", ctx.accounts.burn_authority.to_account_info().key());
        
        let mint = ctx.accounts.mint.key();
        let signer_seeds: &[&[&[u8]]] = &[&[
            b"burn",
            &mint.as_ref(),
            &[ctx.bumps.burn_authority],
        ]];

        let _ = burn(CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            token_2022::Burn {
                authority: ctx.accounts.burn_authority.to_account_info(),
                from: ctx.accounts.destination_token.to_account_info(),
                mint: ctx.accounts.mint.to_account_info(),
           },
           signer_seeds,
            ), amount / 2).unwrap();

        msg!("Hello Transfer Hook!");
        msg!("Amount: {}", amount);

        Ok(())
    }

    // 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()),
        }
    }
    }

    #[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 token_program: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    }

    // 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 token_program: Interface<'info, TokenInterface>,

    /// CHECK: burn authority Account,
    #[account(
        seeds = [b"burn", mint.key().as_ref()], 
        bump
    )]
    pub burn_authority: UncheckedAccount<'info>,
    }
2
  • can you share the full error logs
    – Jimii
    Commented Oct 6 at 12:23
  • Token-2022ProgramInstruction > Program logged: "Instruction: TransferChecked" > Program invoked: UnknownProgram(9L8PsMa9UU8bGAHP7ueqNn3649bozWyiRy37b2VZwhay) > Program logged: "Instruction: TransferHook" > Program logged: "Hello Transfer Hook Before!" > Program logged: "token program:TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" > Program logged: "burn auth:87JhJkhC4QQWTZuysuqJynhdiAYptvUoFTn9H5ycgiNH" > 8eM9rJtgtNWnubXM8LYEAAPBoa24GH5ES17DM9BJXbPp's writable privilege escalated > Program returned error: "Cross-program invocation with unauthorized signer or writable account" Commented Oct 6 at 12:54

1 Answer 1

1

Your program is being called from token-2022, and then your program is trying to CPI back into token-2022, which isn't possible. Re-entrancy is not allowed on Solana.

You'll need to update your solution to avoid burning in your transfer hook, or you can burn a token that belongs to SPL Token instead of SPL Token-2022.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.