feat(token-2022/transfer-hook/account-data-as-seed): add pinocchio example - #711
Conversation
Greptile SummaryAdds a Pinocchio implementation of the Token-2022 account-data-as-seed transfer-hook example.
Confidence Score: 3/5The PR is not safe to merge until initialization of the canonical per-mint meta-list is restricted to the mint's configured transfer-hook authority. The reply from the unnamed author says the prior issue was fixed, but the change only reuses an existing owner counter across multiple mints; any unrelated signer can still create the mint's one-time meta-list first, preventing legitimate counter setup and causing subsequent owner transfers to fail. Files Needing Attention: tokens/token-2022/transfer-hook/account-data-as-seed/pinocchio/program/src/instructions/initialize_extra_account_meta_list.rs Important Files Changed
Reviews (3): Last reviewed commit: "token-2022 transfer-hook account-data-as..." | Re-trigger Greptile |
…nter when 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
account-data-as-seedtransfer hook, alongside the existing Anchor one.What the example demonstrates
The counter PDA is keyed by the token owner, and nobody ever passes the owner in. The
ExtraAccountMetaListrecords anAccountDataseed, so Token-2022 reads 32 bytes at offset 32 of the source token account — its owner field — derives[b"counter", owner]against this program, and hands the resulting account toExecute.That is the only difference from the sibling
counterhook, and it lives entirely in the 51-byte list encoding:[1, 7, b"counter"]isSeed::Literal;[4, 0, 32, 32]isSeed::AccountData { account_index: 0, data_index: 32, length: 32 }. There is no Pinocchio crate for Token-2022, so this is a documented constant rather than a dependency on the TLV encoder — and it is validated end-to-end, since a wrong encoding makes Token-2022 resolve a different account and the transfer fails.The program rederives the same owner bytes from the source account rather than trusting account index 3. Index 3 is the transfer authority, which may be a delegate and would then key a different counter than the one Token-2022 resolved.
Deliberate differences from the Anchor version
counter.checked_add(1)but never assigns it back (counter_accountis notmut), so its counter reports1on every transfer. This port stores it — covered by a test that transfers twice and asserts the counter reaches 2.[b"counter", payer]while itsTransferHookstruct declares[b"counter", owner]; those coincide only because the test's payer is the token owner. This port derives from the source account's owner in the hook, matching what the resolver actually does.Known limitation, inherited from the reference
InitializeExtraAccountMetaListcreates exactly one counter — the payer's — and cannot be called twice for the same mint, so a second token owner has no counter and cannot transfer. The Anchor version has the same single-counter setup instruction. I kept parity rather than adding an instruction the reference does not have; the last test documents the behaviour explicitly.Security checks on
ExecuteExecuteis a public entrypoint, so it does not trust its accounts: the source must be a Token-2022 account naming the invoked mint, the mint'sTransferHookextension must name this program, the counter must be the expected PDA and owned by this program, and thetransferringflag must be set.I did not port Anchor's
token::authority = ownerconstraint: Token-2022 passes the transfer's authority, which may be a delegate, so it would reject legitimate delegated transfers.Tests
9 LiteSVM tests. Verified locally:
tsc --noEmit,pnpm test,prettier --check,cargo fmt --check,cargo clippy -D warnings.