feat(token-2022/transfer-hook/transfer-switch): add pinocchio example - #713
Conversation
Greptile SummaryThe PR adds a Pinocchio implementation of the Token-2022 transfer-switch hook alongside the Anchor example.
Confidence Score: 5/5The PR appears safe to merge. The previously reported delegate-authority bypass is fixed by deriving and validating the switch from the source token account owner, and no blocking failure remains. Important Files Changed
Reviews (3): Last reviewed commit: "token-2022 transfer-switch: create PDAs ..." | Re-trigger Greptile |
…e transfer authority The ExtraAccountMetaList resolved the switch from account 3, the transfer authority. That authority may be a delegate or Token-2022's permanent delegate, so a delegate whose own switch was on could move tokens out of a wallet the admin had switched off. Resolve the switch from the source token account's owner instead, via an AccountData seed, and rederive the same owner in the hook.
|
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-switchtransfer hook, alongside the existing Anchor one.What it does
A kill switch per wallet. An admin can turn a wallet's transfers of the hooked mint on or off, and the hook refuses any transfer whose sender is switched off — without touching the mint or the token accounts.
Five instructions:
Initialize— creates a Token-2022 mint with theTransferHookextension pointed at this program. (The Anchor version builds the mint client-side; doing it in-program keeps the example self-contained and matches the sibling Pinocchio hook examples.)ConfigureAdmin— installs the admin. The first call is unchallenged; later calls must be signed by the current admin and cannot reinstall them.InitializeExtraAccountMetaList— writes the list Token-2022 reads before each transfer.Switch— admin-only; creates the wallet's switch account on first use and sets it on or off.Execute— the transfer-hook interface entrypoint.How the switch is found
The
ExtraAccountMetaListholds a single meta: a PDA of this program whose seed config isSeed::AccountData { account_index: 0, data_index: 32, length: 32 }— 32 bytes at offset 32 of the source token account, i.e. its owner. So Token-2022 derives[owner]itself and passes the switch in, and no caller ever names it. That is the whole 51-byte list:There is no Pinocchio crate for Token-2022, so this is a documented constant rather than a dependency on the TLV encoder; the test compares it byte-for-byte against what the program writes.
Why the owner and not account 3. The Anchor version keys the switch on
seeds=[wallet.key().as_ref()]wherewalletis account 3 — the transfer authority. That authority may be a delegate, or Token-2022's permanent delegate, so an enabled delegate can move tokens straight out of a wallet the admin has switched off. Keying on the token account's owner puts the policy where it belongs. There is a test that demonstrates the bypass against the authority-keyed version.Default-deny
A wallet that has never been switched on has no switch account at all. The hook treats that as off rather than as an account it can skip, so a mint is unusable until the admin explicitly enables senders. There is a test for it.
Security checks on
ExecuteExecuteis a public entrypoint, so it does not trust the accounts it is handed: the source must be a Token-2022 account naming the invoked mint and mid-transfer, the mint'sTransferHookextension must name this program, and the switch must be the PDA rederived from the source account's owner and owned by this program. Without that last check, any wallet's switched-on account would authorise any other wallet's transfer — covered by the "substituted switch account" test.The mint check matters here too: a mint hooked to a different program is mid-transfer while that program runs, and that program could otherwise CPI in and get a "transfer allowed" answer from this one.
Differences from the Anchor version
is_initialisedflag becauseinit_if_neededhands it a zeroed account either way and cannot otherwise tell first call from later ones; here the account is created explicitly, so its existence is the flag.Tests
14 LiteSVM tests: the on/off cycle end to end, admin handover with the old admin losing access, the delegate bypass, and five rejection cases. Verified locally:
tsc --noEmit,pnpm test,prettier --check,cargo fmt --check,cargo clippy -D warnings.