Solana developers have long enjoyed a smug immunity to reentrancy — the vulnerability class that drained $60M from The DAO and has haunted Ethereum ever since. Solana's runtime explicitly prevents a program from being re-invoked while it's already on the call stack. Case closed, right?
Not anymore. Token-2022 Transfer Hooks have quietly reopened Pandora's box.
What Are Transfer Hooks?
Token-2022 (Token Extensions) is Solana's next-generation token standard. One of its most powerful features is the Transfer Hook — a mint-level extension that triggers a Cross-Program Invocation (CPI) to a designated program every time a token transfer occurs.
User initiates transfer → Token-2022 program → CPI to Transfer Hook program
↓
Custom logic executes
Use cases include enforcing NFT royalties, implementing transfer taxes, maintaining on-chain whitelists, and tracking transfer statistics. The hook program receives all the accounts involved in the transfer, plus any additional accounts specified via an ExtraAccountMetaList.
Sounds great. Here's where it gets dangerous.
The Reentrancy Illusion
Solana's runtime prevents direct reentrancy — you can't CPI back into the same instruction that's currently executing. The Token-2022 program enforces additional protections:
- All original transfer accounts become read-only inside the hook
- Sender signer privileges don't propagate to the hook program
-
Direct CPI back to
transfer_checkedis blocked
These mitigations are real, but they create a false sense of security. The actual attack surface is more subtle.
Attack Vector 1: State Manipulation via Hook-Owned Accounts
The hook program can define its own mutable accounts in the ExtraAccountMetaList. While the transfer accounts are read-only, these hook-owned accounts are fully writable.
Consider a lending protocol that uses Token-2022 tokens as collateral:
// Vulnerable lending protocol's deposit handler
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
// Step 1: Transfer collateral tokens (triggers transfer hook)
token_2022::transfer_checked(
/* ... */
amount,
)?;
// Step 2: Update user's collateral balance
ctx.accounts.user_position.collateral += amount;
// Step 3: Calculate borrowing power
let borrow_limit = ctx.accounts.user_position.collateral *
ctx.accounts.price_oracle.price / COLLATERAL_RATIO;
ctx.accounts.user_position.borrow_limit = borrow_limit;
Ok(())
}
If the transfer hook is controlled by a malicious mint authority, it can:
- Write to its own accounts during the hook execution
- CPI to other programs (not the original transfer instruction)
- Manipulate oracle-adjacent state that the lending protocol reads after the transfer
The classic checks-effects-interactions pattern violation — but hidden behind a token transfer that looks atomic.
Attack Vector 2: ExtraAccountMetaList Injection
The ExtraAccountMetaList is stored on-chain in a PDA derived from the mint and the hook program. But here's the critical detail: the hook program's authority controls what accounts appear in this list.
// Hook program can specify arbitrary additional accounts
pub fn initialize_extra_account_meta_list(
ctx: Context<InitializeExtraAccountMetaList>,
) -> Result<()> {
let extra_metas = vec![
// Legitimate: a fee vault
ExtraAccountMeta::new_with_pubkey(&fee_vault, false, true)?,
// Suspicious: someone's token account
ExtraAccountMeta::new_with_pubkey(&target_account, false, true)?,
// Dangerous: a program to CPI into
ExtraAccountMeta::new_with_pubkey(&malicious_program, false, false)?,
];
ExtraAccountMetaList::init::<ExecuteInstruction>(
&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
&extra_metas,
)?;
Ok(())
}
Any protocol integrating Token-2022 tokens with transfer hooks must now answer: "Do I trust the hook program and every account it demands?"
Most protocols don't even ask the question.
Attack Vector 3: Cross-Program Reentrancy Through Hooks
While you can't re-enter the same instruction, a transfer hook can CPI to a completely different program — including the calling program's other instructions:
Protocol.deposit()
→ token_2022::transfer_checked()
→ HookProgram.execute()
→ Protocol.liquidate() // Different instruction, same program!
This is cross-function reentrancy, the same class of bug that hit Curve's Vyper pools. Solana's runtime only blocks same-instruction reentrancy, not same-program cross-instruction calls.
Attack Vector 4: Compute Budget Exhaustion
Transfer hooks consume compute units from the caller's budget. A malicious hook can deliberately burn CUs:
pub fn execute(ctx: Context<Execute>, amount: u64) -> Result<()> {
// Burn compute units to cause caller's transaction to fail
for _ in 0..10000 {
msg!("burning CUs");
}
Ok(())
}
This creates a griefing vector: any protocol that integrates a Token-2022 token is at the mercy of the hook program's compute consumption. In a lending protocol, this could block liquidations — the attacker takes a leveraged position, then the hook prevents liquidation transactions from completing.
Real-World Impact: Who's Exposed?
Any Solana protocol that:
- Accepts arbitrary Token-2022 mints as collateral or liquidity
- Doesn't verify whether a mint has a transfer hook before integration
- Performs state updates after token transfers (most of them)
- Assumes token transfers are side-effect-free
This includes the majority of Solana DEXes, lending protocols, and yield aggregators that have added Token-2022 support.
Defensive Patterns
1. Check for Transfer Hooks Before Integration
use spl_token_2022::extension::transfer_hook::TransferHook;
pub fn verify_mint(mint_info: &AccountInfo) -> Result<bool> {
let mint_data = mint_info.try_borrow_data()?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
if let Ok(hook) = mint.get_extension::<TransferHook>() {
let hook_program_id = OptionalNonZeroPubkey::try_from(hook.program_id)?;
if hook_program_id.is_some() {
return Ok(true);
}
}
Ok(false)
}
2. Effects Before Interactions (Adapted for Solana)
pub fn deposit_safe(ctx: Context<Deposit>, amount: u64) -> Result<()> {
// Step 1: Update state FIRST
ctx.accounts.user_position.collateral += amount;
ctx.accounts.user_position.borrow_limit = calculate_limit(
ctx.accounts.user_position.collateral,
ctx.accounts.price_oracle.price,
);
// Step 2: THEN transfer (hook executes after state is consistent)
token_2022::transfer_checked(/* ... */ amount)?;
// Step 3: Verify transfer succeeded
ctx.accounts.token_account.reload()?;
require!(
ctx.accounts.token_account.amount >= expected_balance,
ErrorCode::TransferFailed
);
Ok(())
}
3. Whitelist Approved Hook Programs
const APPROVED_HOOKS: &[Pubkey] = &[
pubkey!("Hook1111111111111111111111111111111111111"),
pubkey!("Hook2222222222222222222222222222222222222"),
];
pub fn validate_hook(mint_info: &AccountInfo) -> Result<()> {
let mint_data = mint_info.try_borrow_data()?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
if let Ok(hook) = mint.get_extension::<TransferHook>() {
let hook_id = Pubkey::try_from(hook.program_id)?;
require!(
APPROVED_HOOKS.contains(&hook_id),
ErrorCode::UnapprovedHook
);
}
Ok(())
}
4. Budget Isolation
// Set a compute budget limit for the transfer CPI
invoke_with_compute_budget(
&token_2022::transfer_checked_instruction(/* ... */),
MAX_HOOK_CU_BUDGET,
)?;
The Audit Checklist
If you're auditing a Solana protocol that supports Token-2022:
- [ ] Does the protocol check for transfer hooks on accepted mints?
- [ ] Are state updates performed before or after token transfers?
- [ ] Does the protocol whitelist approved hook programs?
- [ ] Is there compute budget isolation for transfer CPIs?
- [ ] Can a malicious hook block critical operations (liquidations, withdrawals)?
- [ ] Does the protocol validate ExtraAccountMetaList contents?
- [ ] Are there cross-instruction reentrancy guards?
Conclusion
Token-2022 Transfer Hooks are a powerful primitive, but they fundamentally change Solana's security model. The assumption that "Solana doesn't have reentrancy" is now outdated. Every token transfer is potentially an arbitrary program invocation with attacker-controlled inputs.
The protocols that survive will be the ones that treat Token-2022 transfers like they treat cross-chain messages: untrusted by default, validated exhaustively.
The security community needs to update its mental models. Solana auditors who skip reentrancy analysis are leaving critical vulnerabilities on the table.
WebPulse Security researches smart contract vulnerabilities across Solana and EVM ecosystems. Follow for weekly deep dives into DeFi security.
Top comments (0)