feat(token-2022/transfer-hook/transfer-cost): add pinocchio example - #712
Conversation
Greptile SummaryAdds a Pinocchio implementation of the Token-2022 transfer-cost hook alongside the existing Anchor example.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported initialization failures are addressed by the current account-creation and counter-reuse logic. Important Files Changed
Reviews (3): Last reviewed commit: "token-2022 transfer-hook transfer-cost: ..." | Re-trigger Greptile |
…en configuring another mint
|
Audit follow-up from #714: every PDA this example creates has a publicly derivable address, and Fixed here too. PDA creation now goes through a Covered by pre-funding each derivable address with one lamport in the setup test before the creating instruction runs. |
|
@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 |
Adds a Pinocchio implementation of the Token-2022
transfer-costtransfer hook, alongside the existing Anchor one.What it does
Every transfer of the hooked mint charges a fee in wrapped SOL equal to the token amount, moving it from the sender's wSOL account to a delegate's, and bumps a transfer counter.
The fee cannot be signed for by whoever signed the transfer — Token-2022 CPIs into
Executewithout forwarding signer privileges, so nothing in the hook's account list is a signer. The sender instead approves a PDA of this program as a delegate on their wSOL account beforehand, and the hook signs as that PDA. This is also why the fee is wrapped SOL rather than lamports: a delegate can move tokens, not a wallet's SOL.The ExtraAccountMetaList
This is the involved part, and the reason this example is worth having in Pinocchio. Seven accounts are resolved, and Token-2022 derives all of them itself from a 261-byte list written at setup:
There is no Pinocchio crate for Token-2022, so the list is encoded by hand (
build_extra_account_metas). Two encoding details are worth recording, since neither is obvious and both are silent when wrong:0for a literal address,1for a PDA of the hook program, and for a PDA of another program it is0x80 | index, where the index points at that program in theExecuteaccount list. It is notindex + 2—2is a distinct kind that reads an address out of another account's data. Getting this wrong makes Token-2022 fail the whole transfer with a bareInvalidAccountData, before the hook is ever invoked.Seed::Literal → [1, len, ...bytes]andSeed::AccountKey → [3, index], zero-padded to 32 bytes. Accounts 9 and 10 are ATAs, so their seeds are[owner, token program, mint]given as account-key references — which is how the sender's wSOL account is found without any caller naming it.The test rebuilds the same 261 bytes independently and compares them to what the program wrote, so an encoding change fails loudly and locally rather than as an opaque transfer failure.
Security checks on
ExecuteExecuteis a public entrypoint and this one moves money, so it rederives everything Token-2022 resolved rather than trusting the account list: the source must be a Token-2022 account naming the invoked mint and mid-transfer, the mint'sTransferHookextension must name this program, the wSOL mint and both programs must be the expected addresses, the delegate and counter must be this program's PDAs, and both wSOL accounts must be the ATAs derived from the delegate and the transfer authority.Without the mid-transfer check in particular, anyone could call
Executedirectly and drain the approved allowance one fee at a time; there are tests for that and for a substituted fee destination.I did not port Anchor's
token::authority = ownerconstraint on the source account: Token-2022 passes the transfer's authority, which may itself be a delegate, so that constraint would reject legitimate delegated transfers.Differences from the Anchor version
u64rather than au8. This crate builds withoverflow-checks, so au8counter would start aborting transfers after 255 of them.Tests
10 LiteSVM tests covering the fee path, repeat transfers, and four rejection cases. Note the suite creates the native mint itself — LiteSVM does not seed
So111...112. Verified locally:tsc --noEmit,pnpm test,prettier --check,cargo fmt --check,cargo clippy -D warnings.