Skip to content

feat(token-2022/nft-meta-data-pointer): add pinocchio example - #718

Open
MarkFeder wants to merge 2 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-2022-nft-meta-data-pointer-pinocchio
Open

feat(token-2022/nft-meta-data-pointer): add pinocchio example#718
MarkFeder wants to merge 2 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-2022-nft-meta-data-pointer-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Ports tokens/token-2022/nft-meta-data-pointer to Pinocchio.

An NFT whose metadata lives in the mint itself via the Token-2022 MetadataPointer extension (pointing at the mint), so there is no separate metadata account. A small chop-the-tree game writes the player's banked wood onto 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, then SetAuthority(MintTokens, None) so the supply is fixed at one. The metadata authority stays with the program PDA, which is what lets chop_tree keep updating it.
  • chop_tree — spends energy, banks wood, rewrites wood on the mint's metadata

There 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_tree through gum session keys, which have no Pinocchio equivalent and are orthogonal to what this example teaches. Here chop_tree requires the player's own signature (player.authority == signer). Everything else matches the reference, including on_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, since UpdateField grows the mint.

@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner August 31, 2026 20:37
@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a Pinocchio implementation of the Token-2022 metadata-pointer NFT game.

  • Adds player and shared-level initialization, NFT minting, and tree-chopping instructions.
  • Builds Token-2022 metadata CPIs manually and updates the mint’s on-chain wood field.
  • Adds LiteSVM tests, package configuration, workspace registration, and documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
tokens/token-2022/nft-meta-data-pointer/pinocchio/program/src/instructions/chop_tree.rs Validates player authority, NFT possession, and the Token-2022 program before updating game state and mint metadata; the previously reported account-boundary issues are fixed.
tokens/token-2022/nft-meta-data-pointer/pinocchio/program/src/instructions/mint_nft.rs Creates the extended mint, initializes self-referential metadata, mints one token, and revokes mint authority.
tokens/token-2022/nft-meta-data-pointer/pinocchio/program/src/instructions/init_player.rs Initializes the player PDA and lazily creates the shared game-level PDA.
tokens/token-2022/nft-meta-data-pointer/pinocchio/program/src/metadata.rs Encodes and invokes Token-2022 metadata initialization and field-update instructions.
tokens/token-2022/nft-meta-data-pointer/pinocchio/program/src/state.rs Defines and mutates the player and level account layouts used by the game instructions.
tokens/token-2022/nft-meta-data-pointer/pinocchio/tests/test.ts Exercises initialization, minting, chopping, refill behavior, authority validation, NFT ownership validation, and token-program validation.

Reviews (2): Last reviewed commit: "nft-meta-data-pointer: bind the mint to ..." | Re-trigger Greptile

Comment on lines +98 to +103
if !mint.owned_by(&TOKEN_2022_PROGRAM_ID) {
return Err(GameError::InvalidAccountData.into());
}

metadata_update_field(
token_program.address(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Mint ownership is unbound

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +102 to +109
metadata_update_field(
token_program.address(),
mint,
nft_authority,
WOOD_KEY,
&u64_to_decimal(wood),
&[Signer::from(&seeds)],
)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant