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>,
}
-
can you share the full error logs– JimiiCommented 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"– demirtasarkinbarisCommented Oct 6 at 12:54
Add a comment
|
1 Answer
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.