This repository contains the fully completed implementation of the Solana Summer School Transfer Hook program. It demonstrates how to implement a custom transfer hook using the SPL Token-2022 Transfer Hook Interface to enforce automatic, on-chain rate limiting on token transfers.
In this program, every token transfer is automatically validated by an on-chain transfer hook against a rate limit window:
- Per-Mint & Per-Owner Rate Limiting: Unlike basic implementations that share a single global rate limit, this program derives unique rate limit buckets for every user and token mint using Program Derived Addresses (PDAs).
- Fixed-Window Throttling: The rate limiter enforces a fixed time window (1 hour). Once the window expires, the next transfer opens a fresh window with a zeroed total. This prevents steady traffic from indefinitely delaying window resets.
- On-Chain TLV Account Resolution: Leverages Token-2022 Type-Length-Value (TLV)
ExtraAccountMetaListto dynamically resolve per-user PDAs during transfers without requiring client-side intervention. - In-Program Transfers & Re-entrancy Protection: Demonstrates how to perform Token-2022 CPI transfers from within an Anchor program while handling Anchor struct deserialization quirks and Solana SVM re-entrancy boundaries.
This repository solves all four guided architectural challenges from the Solana Summer School curriculum:
During rate limit initialization (Initialize), the program explicitly verifies that the provided mint account is owned by the official Token-2022 program (token_2022::ID) before initializing state:
#[account(
constraint = mint.to_account_info().owner == &token_2022::ID @ ErrorCode::InvalidMint,
)]
pub mint: InterfaceAccount<'info, Mint>,The RateLimit state account explicitly stores the associated mint public key alongside the authority, maximum transfer amount, window start timestamp, and transferred amount:
#[account]
#[derive(InitSpace)]
pub struct RateLimit {
pub authority: Pubkey,
pub mint: Pubkey,
pub max_amount: u64,
pub window_start: i64,
pub amount_transferred: u64,
}To ensure each user gets their own independent rate limit bucket per mint, the PDA seed derivation was upgraded across all four coordinated locations in the architecture:
- PDA Seed Structure:
[b"rate_limit", mint.key().as_ref(), owner.key().as_ref()] - Dynamic TLV Account Resolution: In
extra_account_metas(), the required extra accounts are referenced by their index in the transfer hook's execute instruction:- Index 1: The Token Mint
- Index 3: The Source Account Owner (Authority)
pub fn extra_account_metas() -> Result<Vec<ExtraAccountMeta>> {
Ok(vec![
ExtraAccountMeta::new_with_seeds(
&[
Seed::Literal { bytes: b"rate_limit".to_vec() },
Seed::AccountKey { index: 1 }, // mint
Seed::AccountKey { index: 3 }, // owner
],
false, // is_signer
true, // is_writable
)?,
])
}We implemented an in-program transfer_checked instruction (instructions/transfer.rs) that invokes Token-2022 via Cross-Program Invocation (CPI), uncovering two critical Solana development patterns:
In Anchor, if an instruction deserializes an account struct (Account<'info, RateLimit>), mutates it during an outgoing CPI call, and then exits, Anchor will serialize the outer instruction's stale struct back into account memory upon exit—silently erasing updates made by the CPI hook.
To prevent this, rate_limit is declared as an UncheckedAccount<'info> with explicit seed constraints in Transfer<'info>. This ensures Anchor never overwrites the account memory upon instruction exit, preserving the rate limit increments recorded by transfer_hook.
When testing an in-program transfer where our program (J3xE...) initiates the transfer AND acts as the transfer hook for the mint, the Solana SVM runtime returns:
Cross-program invocation reentrancy not allowed for this instruction
Why this happens: The Solana runtime enforces a fundamental security rule: A program may only re-enter itself if the immediate caller (the program currently at the top of the instruction stack) is the same program.
- Stack Frame 1: Our Program (
transfer_checked) - Stack Frame 2: Token-2022 (
transfer_checked) - Stack Frame 3: Our Program (
transfer_hook) — BLOCKED
Because the immediate caller at Frame 2 is Token-2022 (a different program ID), the SVM blocks the indirect re-entry at Frame 3 with InstructionError::ReentrancyNotAllowed.
Important
Real-World Architectural Takeaway: This confirms why in production Solana DeFi protocols, Transfer Hook programs must be deployed as standalone, distinct program IDs separate from vaults, pools, or token-transferring programs. Keeping the hook program distinct prevents indirect re-entrancy lockouts when protocol programs initiate CPI token transfers.
programs/solana-summer-transfer-hook/src/
├── instructions/
│ ├── init_extra_account_meta.rs # TLV ExtraAccountMetaList initialization & seed indices
│ ├── initialize.rs # RateLimit account creation (per-mint, per-owner)
│ ├── initialize_mint.rs # Token-2022 mint initialization with hook extension
│ ├── transfer.rs # In-program CPI transfer_checked implementation
│ └── transfer_hook.rs # The hook execution logic & rate limit enforcement
├── state/
│ └── rate_limit.rs # RateLimit struct definition & window calculations
├── error.rs # Custom Anchor error codes
└── lib.rs # Program entrypoint & instruction dispatch
- Rust & Cargo
- Solana CLI (v1.18+)
- Anchor CLI (v0.30+)
Compile the Anchor program and generate the IDL:
anchor buildThe repository includes a comprehensive test suite covering mint validation, TLV metadata resolution, per-owner rate limiting, window resets, and SVM re-entrancy boundaries:
cargo testtest_id: Verifies program ID consistency.test_initialize_extra_account_meta_list: Validates TLV metadata account creation and seed configurations.test_initialize: Verifies per-mint/per-owner rate limit account creation and Token-2022 mint ownership constraints.test_transfer_hook: Verifies successful token transfers within the rate limit and automatic window resets upon timestamp expiration.test_transfer_hook_rate_limit_exceeded: Verifies that transfers exceeding the threshold are cleanly rejected withRateLimitExceeded.test_in_program_transfer_hits_reentrancy_guard: Demonstrates and asserts the Solana SVM indirect re-entrancy security guard (ReentrancyNotAllowed).
This project is open-source and available under the MIT License.