Skip to content

feat(token-2022/transfer-hook/allow-block-list-token): add pinocchio example - #717

Closed
MarkFeder wants to merge 3 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-2022-transfer-hook-allow-block-list-token-pinocchio
Closed

feat(token-2022/transfer-hook/allow-block-list-token): add pinocchio example#717
MarkFeder wants to merge 3 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-2022-transfer-hook-allow-block-list-token-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio implementation of allow-block-list-token, alongside the existing Anchor one. This is the last of the six transfer-hook variants.

What it does

A transfer hook enforcing an allow/block list, with the policy stored in the mint's own token metadata so it travels with the token:

  • Allow — only wallets explicitly on the list may receive.
  • Block — anyone may transact except wallets explicitly blocked.
  • Mixed — like Block, but transfers at or above a threshold need an allowed receiver.

Eight instructions: InitConfig, InitMint, AttachToMint, InitWallet, RemoveWallet, ChangeMode, ResizeMetaList, and the interface's Execute.

Reading the mode out of the mint

The mode lives in the mint's TokenMetadata extension under an AB key, with an optional threshold — exactly where the Anchor version puts it, so both implementations read each other's mints. There is no Pinocchio crate for Token-2022 or the token-metadata interface, so metadata.rs parses the variable-length TLV by hand (update_authority | mint | name | symbol | uri | additional[], all borsh strings, every read bounds-checked) and builds the two interface instructions itself.

Their discriminators are the first eight bytes of sha256("spl_token_metadata_interface:initialize_account") and …:updating_field. I computed those from the preimages rather than copying them from anywhere, and cross-checked the method against spl-transfer-hook-interface:execute, which reproduced the value already shipping in the sibling hook examples.

init_mint also builds a mint carrying three extensions — PermanentDelegate, TransferHook and MetadataPointer — which have to be initialized before InitializeMint2, since Token-2022 refuses extension setup afterwards.

The decision logic

decide() is a pure function of the decoded mint mode, both wallet states and the amount, so it is unit-testable without building accounts. It carries 8 #[cfg(test)] tests, mirroring the Anchor version's — including the one guarding that a blocked sender is rejected in every mint mode, which is the side that is easy to omit.

Validation this port adds

The Anchor TxHook struct declares every account UncheckedAccount and validates nothing. That is defensible there — the hook only reads and returns a verdict — but it means a direct call is answered on whatever accounts the caller supplies. This port refuses one: the metas list must be the mint's PDA, the mint's TransferHook extension must name this program, the source must be a Token-2022 account for that mint and mid-transfer, and both wallet records must be the PDAs derived from the owners recorded in the source and destination token accounts.

That last one matters most: the records are what the verdict is read from, so if a caller could nominate them the answer would be theirs to choose.

Differences from the Anchor version

  • No 8-byte account discriminators, so Config and each ABWallet are 33 bytes.
  • Mode travels as a u8 in instruction data and as the same string as Anchor's Display in metadata.
  • RemoveWallet moves the rent to the authority before closing — pinocchio's close() zeroes the lamports field outright, so closing first destroys them and unbalances the instruction.
  • ResizeMetaList is permissionless, as in the reference: the content is fully determined by the mint and this program's fixed list, so gating it would strand mints whose hook authority was revoked.

Tests

13 LiteSVM tests covering all three modes end to end (including the blocked-sender case and the mixed-mode threshold on both sides), plus non-authority, wrong-mint and direct-call rejections — and 8 unit tests on decide(). Verified locally: cargo test, tsc --noEmit, pnpm test, prettier --check, cargo fmt --check, cargo clippy -D warnings.

@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner August 31, 2026 19:11
@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a Pinocchio implementation of the Token-2022 allow/block-list transfer hook alongside the existing Anchor example.

  • Implements mint initialization, policy metadata, wallet records, hook execution, and meta-list management.
  • Adds unit and LiteSVM coverage for all policy modes, authorization boundaries, malformed attachment metadata, and direct-call rejection.
  • Integrates the example into the Cargo workspace, package tooling, CI-compatible scripts, and repository documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the current attachment guard rejects both missing policy metadata and malformed thresholds before activating the hook.

Important Files Changed

Filename Overview
tokens/token-2022/transfer-hook/allow-block-list-token/pinocchio/program/src/instructions/mint.rs Implements extended-mint creation, policy updates, hook attachment, and meta-list resizing; current attachment validation fixes both previously reported malformed-policy paths.
tokens/token-2022/transfer-hook/allow-block-list-token/pinocchio/program/src/metadata.rs Adds bounds-checked TokenMetadata parsing and manual metadata-interface instruction construction used consistently by attachment and execution.
tokens/token-2022/transfer-hook/allow-block-list-token/pinocchio/program/src/instructions/tx_hook.rs Validates the Token-2022 transfer context and evaluates mint and wallet policy during hook execution.
tokens/token-2022/transfer-hook/allow-block-list-token/pinocchio/program/src/decide.rs Encapsulates allow, block, and threshold policy decisions as a unit-tested pure function.
tokens/token-2022/transfer-hook/allow-block-list-token/pinocchio/tests/test.ts Covers end-to-end policy behavior and verifies rejection of missing or malformed policy metadata before hook attachment.

Reviews (3): Last reviewed commit: "abl-token: validate the threshold too be..." | Re-trigger Greptile

attach_to_mint switched the hook on without checking that the mint
carries an AB policy Execute can read. A mint with no TokenMetadata was
left with every transfer failing and no way back, since ChangeMode can
only update metadata that already exists.

Require a parseable AB mode before attaching. Set the mode with
ChangeMode first, then attach. Covered by a test verified to attach
successfully without the check.
The attach guard checked the AB mode but not the threshold, so a mint
with a valid mode and a non-decimal threshold still bricked: Execute
parses both, and fails on either.

Validate everything Execute will later read. Covered by a test that
writes a malformed threshold with a raw UpdateField and is verified to
attach successfully without the check.
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@amilz could you take a look at this one when you get a chance?

No open review threads left on it, so it is ready for maintainer review. It is one of 23 open Pinocchio ports I have up — they are independent and self-contained, so they can be reviewed and merged in any order: https://github.com/solana-developers/program-examples/pulls/MarkFeder

@dev-jodee

Copy link
Copy Markdown
Collaborator

Hey, in order to not overflow our reviewers with PRs, we'll limit the number of opened PRs to 3 per contributor for now. Thanks for the work! (You can re-open those whenever your other PRs get merged in)

@dev-jodee dev-jodee closed this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants