Skip to content

SGDlab67/summer-transfer-hook-challenge

Repository files navigation

Solana Token-2022 Rate Limit Transfer Hook (Completed Solutions)

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.


Overview & Key Features

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) ExtraAccountMetaList to 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.

Evolving the Architecture: Completed Challenges

This repository solves all four guided architectural challenges from the Solana Summer School curriculum:

1. Validate the Mint (Challenge 1)

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>,

2. Store the Mint on the Account (Challenge 2)

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,
}

3. Per-Mint, Per-Owner PDA Seeds (Challenge 3)

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
        )?,
    ])
}

4. In-Program Transfer & Re-entrancy Architecture (Challenge 4)

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:

A. Anchor Struct Persistence (UncheckedAccount)

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.

B. Solana SVM Indirect Re-entrancy Security Boundary

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.


Project Structure

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

Getting Started

Prerequisites

Build the Program

Compile the Anchor program and generate the IDL:

anchor build

Run the Test Suite

The repository includes a comprehensive test suite covering mint validation, TLV metadata resolution, per-owner rate limiting, window resets, and SVM re-entrancy boundaries:

cargo test

Test Summary:

  • test_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 with RateLimitExceeded.
  • test_in_program_transfer_hits_reentrancy_guard: Demonstrates and asserts the Solana SVM indirect re-entrancy security guard (ReentrancyNotAllowed).

License

This project is open-source and available under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors