A Solana program (Anchor) that implements a whitelist-gated vault using SPL Token-2022 and the transfer hook extension. Only whitelisted users can receive or send the vault’s token via the hook.
- Goal: Build a vault that holds Token-2022 tokens and only allows transfers involving whitelisted users.
- Mechanism: Use the Token-2022 transfer hook so that every transfer is validated by our program; the hook checks a per-user whitelist PDA before allowing the transfer.
- Flow: Users are added to a whitelist → a Token-2022 mint is created with the transfer hook pointing to our program → an ExtraAccountMetaList PDA (per mint) is initialized with the whitelist and other accounts → a single vault PDA holds tokens; deposit (user → vault) and withdraw (vault → user) go through our program via CPI to Token-2022, which invokes the hook.
-
Whitelist
initialize_whitelist: create a PDA per user (seeds = [b"whitelist", user_key]).add_to_whitelist/remove_from_whitelist: manage who is allowed to use the vault/hook.
-
Token-2022 mint with transfer hook
init_mint_token(TokenFactory): create a mint withextensions::transfer_hook(authority = user, program_id = our program). Requires an existing whitelist PDA for the user and an ExtraAccountMetaList PDA (created later).
-
ExtraAccountMetaList (per mint)
initialize_transfer_hook(InitializeExtraAccountMetaList): create PDA[b"extra-account-metas", mint]and write the list of extra accounts the hook needs (e.g. whitelist PDA via seeds). Token-2022 uses this when invoking the hook.
-
Vault
initialize_vault: create vault PDA (seeds = [b"vault"]) and set mint + bump; vault ATA must already exist (created by client with Token-2022).
-
Deposit & withdraw
deposit: user signs; CPItransfer_checkedfrom user_ata → vault_ata (Token-2022 runs the transfer hook).withdraw: vault PDA as signer via CPI; CPItransfer_checkedfrom vault_ata → user_ata. Both use Token-2022 and the same hook.
-
Transfer hook
transfer_hook: invoked by Token-2022 on every transfer of this mint. Validates that the source token owner has a whitelist PDA and that it matches the account list (e.g.whitelistfrom seeds[b"whitelist", owner.key()]). Rejects if not whitelisted.
- Single vault: One global vault PDA per program; vault ATA holds the Token-2022 tokens.
- Whitelist: One whitelist PDA per user; the transfer hook only allows transfers if the source owner’s whitelist exists and is passed in the hook’s extra accounts.
- Token-2022 transfer hook: The mint is created with
transfer_hook::program_id = our_program. On any transfer, Token-2022 CPIs into ourtransfer_hookwith the extra accounts from the ExtraAccountMetaList; we check the owner’s whitelist and allow or deny. - Deposit/withdraw: Implemented as CPIs to Token-2022’s
transfer_checked(deposit: user → vault; withdraw: vault → user with vault PDA signer). The same hook runs on those transfers, so only whitelisted users can participate.
anchor build anchor test # runs: cargo test -- --nocapture