feat(token-2022/transfer-hook/allow-block-list-token): add pinocchio example - #717
Conversation
Greptile SummaryThe PR adds a Pinocchio implementation of the Token-2022 allow/block-list transfer hook alongside the existing Anchor example.
Confidence Score: 5/5The 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
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.
|
@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 |
|
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) |
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:
Eight instructions:
InitConfig,InitMint,AttachToMint,InitWallet,RemoveWallet,ChangeMode,ResizeMetaList, and the interface'sExecute.Reading the mode out of the mint
The mode lives in the mint's
TokenMetadataextension under anABkey, with an optionalthreshold— 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, sometadata.rsparses 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 againstspl-transfer-hook-interface:execute, which reproduced the value already shipping in the sibling hook examples.init_mintalso builds a mint carrying three extensions —PermanentDelegate,TransferHookandMetadataPointer— which have to be initialized beforeInitializeMint2, 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
TxHookstruct declares every accountUncheckedAccountand 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'sTransferHookextension 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
Configand eachABWalletare 33 bytes.Modetravels as au8in instruction data and as the same string as Anchor'sDisplayin metadata.RemoveWalletmoves the rent to the authority before closing — pinocchio'sclose()zeroes the lamports field outright, so closing first destroys them and unbalances the instruction.ResizeMetaListis 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.