feat(token-2022/nft-meta-data-pointer): add pinocchio example - #718
feat(token-2022/nft-meta-data-pointer): add pinocchio example#718MarkFeder wants to merge 2 commits into
Conversation
Greptile SummaryThe PR adds a Pinocchio implementation of the Token-2022 metadata-pointer NFT game.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (2): Last reviewed commit: "nft-meta-data-pointer: bind the mint to ..." | Re-trigger Greptile |
| if !mint.owned_by(&TOKEN_2022_PROGRAM_ID) { | ||
| return Err(GameError::InvalidAccountData.into()); | ||
| } | ||
|
|
||
| metadata_update_field( | ||
| token_program.address(), |
There was a problem hiding this comment.
When a valid player supplies another program-created NFT, chop_tree accepts it because every NFT shares the same metadata authority and the mint is checked only for Token-2022 ownership, causing the victim NFT's wood metadata to be overwritten with the caller's total.
How this was verified: The authenticated player path reaches a caller-selected mint that the shared authority can update without any player-to-mint binding.
Knowledge Base Used: Token-2022 extension patterns
There was a problem hiding this comment.
Good catch — fixed in f676d789.
chop_tree now takes the signer's token account for the mint and requires it to name that mint, be owned by the signer, and hold a non-zero balance. Since every NFT here answers to the same nft_authority PDA, holding the token is the only thing that distinguishes a player's own NFT from anyone else's. The reference's mint account carries a CHECK: Make sure the ata to the mint is actually owned by the signer comment describing exactly this constraint — it just never enforces it, so this closes the gap rather than diverging.
Verified rather than assumed: the new test Refuses to rewrite another player's NFT has a second funded player mint their own NFT, then chop while passing the victim's mint. Against the pre-fix program that transaction succeeds — the assertion fails with expected TransactionMetadata{} to be an instance of FailedTransactionMetadata, i.e. the overwrite went through. With the guard it is rejected with WrongAuthority and the victim's metadata is asserted unchanged. 10 tests passing.
| metadata_update_field( | ||
| token_program.address(), | ||
| mint, | ||
| nft_authority, | ||
| WOOD_KEY, | ||
| &u64_to_decimal(wood), | ||
| &[Signer::from(&seeds)], | ||
| )?; |
There was a problem hiding this comment.
Metadata CPI target is untrusted
When a valid player supplies another executable program in the token-program slot, the metadata CPI invokes that program instead of Token-2022, allowing the game state to advance successfully while the real mint's wood metadata remains stale.
| metadata_update_field( | |
| token_program.address(), | |
| mint, | |
| nft_authority, | |
| WOOD_KEY, | |
| &u64_to_decimal(wood), | |
| &[Signer::from(&seeds)], | |
| )?; | |
| metadata_update_field( | |
| &TOKEN_2022_PROGRAM_ID, | |
| mint, | |
| nft_authority, | |
| WOOD_KEY, | |
| &u64_to_decimal(wood), | |
| &[Signer::from(&seeds)], | |
| )?; |
Knowledge Base Used: Token-2022 extension patterns
There was a problem hiding this comment.
Right — fixed in f676d789, and your suggestion is what I applied.
Both metadata CPIs now target &TOKEN_2022_PROGRAM_ID directly, and chop_tree rejects a token-program slot that is not Token-2022 up front. mint_nft had the same two problems, so it got the same treatment — its metadata calls were also going through the caller-supplied slot, and it hands that account to the ATA Create CPI as well.
This matches the reference on both counts: it declares the slot as Program<'info, Token2022> and builds the instruction against spl_token_2022::id(), so the target was never caller-controlled there.
Verified: the new test Refuses a token program that is not Token-2022 asserts on incorrect program id specifically, not merely that the transaction failed. That distinction mattered — my first version of the test passed against the unguarded program, because a bogus slot fails inside the CPI anyway; asserting the error code is what makes it actually exercise the guard. 10 tests passing.
…program chop_tree accepted any Token-2022 mint, and every NFT this program mints shares one metadata authority, so a valid player could rewrite another player's wood. It also invoked the metadata CPI against the caller-supplied program slot. The reference pins that slot with Program<'info, Token2022> and builds the instruction against spl_token_2022::id(); its mint account documents the holder check in a CHECK comment but never enforces it.
Ports
tokens/token-2022/nft-meta-data-pointerto Pinocchio.An NFT whose metadata lives in the mint itself via the Token-2022
MetadataPointerextension (pointing at the mint), so there is no separate metadata account. A small chop-the-tree game writes the player's bankedwoodonto that metadata as they play, which is what makes the pointer worth demonstrating: the token's own data tracks progress, no indexer involved.init_player— creates the player PDA and the shared level PDA (only the first player pays for the level)mint_nft— 234-byte mint,MetadataPointer→ mint,InitializeMint2(0 decimals), metadata initialize +level=1, ATA, mint 1, thenSetAuthority(MintTokens, None)so the supply is fixed at one. The metadata authority stays with the program PDA, which is what letschop_treekeep updating it.chop_tree— spends energy, banks wood, rewriteswoodon the mint's metadataThere is no pinocchio crate for Token-2022 or the token-metadata interface, so those CPIs are built by hand; the discriminators are documented from their preimages rather than pasted.
One deliberate omission: the Anchor example routes
chop_treethrough gum session keys, which have no Pinocchio equivalent and are orthogonal to what this example teaches. Herechop_treerequires the player's own signature (player.authority == signer). Everything else matches the reference, includingon_tree_chopped's pre-add level comparison.Tests use LiteSVM +
@solana/kit; 8 tests cover both instructions plus the energy refill and the authority check. Rent is topped up after metadata writes, sinceUpdateFieldgrows the mint.