fix(nft-meta-data-pointer): require the player's NFT token account in chop_tree and reset the tree when it reaches MAX_WOOD_PER_TREE - #5
Open
SwineCoder101 wants to merge 2 commits into
Conversation
… mint and the tree counter resets one chop late
… chop_tree and reset the tree when it reaches MAX_WOOD_PER_TREE
SwineCoder101
force-pushed
the
fix/nft-meta-data-pointer-chop-tree-mint-check
branch
from
August 27, 2026 12:23
d2b7511 to
1351b32
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug:
chop_treerewrites thewoodmetadata of any NFT, and the tree counter resets one chop latechop_treetakes the NFTmintas an uncheckedAccountInfoand never verifies that the player holds that NFT, although its/// CHECKcomment claims the ATA is validated. Because the program-ownednft_authorityPDA is the metadata update authority of every NFT minted bymint_nft, any player (or any session key) can pass another player's mint and overwrite that NFT's on-chainwoodfield with their own count. This is a medium-severity authorization bug, not a style issue: the on-chain game state stored in a stranger's NFT can be tampered with by anyone who has a player account. A second, low-severity off-by-one inGameData::on_tree_choppedlets the shared tree counter reachMAX_WOOD_PER_TREEand only reset on the following chop.Affected
anchor/(programextension_nft)anchor/programs/extension_nft/src/instructions/chop_tree.rsanchor/programs/extension_nft/src/state/game_data.rsanchor/programs/extension_nft/src/errors.rs(new error code)anchor/programs/extension_nft/src/instructions/mint_nft.rs(AccountInfo->UncheckedAccount, needed forclippy -D warnings)app/idl/extension_nft.json,app/components/ChopTreeButton.tsx(client passes the new account)anchor/tests/lumberjack.tsFunctionality
mint_nftcreates a Token-2022 NFT whose mint carries the metadata-pointer + token-metadata extensions, sets the program PDAnft_authorityas metadata update authority, and mints the single token to the caller's associated token account.chop_treespends one energy, adds one wood to the caller'sPlayerData, adds one to the sharedGameData.total_wood_collected(which is supposed to reset to 0 once a tree ofMAX_WOOD_PER_TREEwood has been fully chopped), and mirrors the player's wood count into thewoodfield of the metadata of the NFT passed asmint, signing withnft_authority.The bug
anchor/programs/extension_nft/src/instructions/chop_tree.rs:87-89(before the fix):No ATA is in the context and nothing ties
minttoplayer; the comment describes a check that does not exist.chop_tree.rs:35-48then callsupdate_field(wood = player.wood)on whatever mint was passed, signed bynft_authority, which is the update authority of every NFT the program has ever minted (mint_nft.rs:98-112). Scenario: player A mints an NFT and chops twice (wood = "2"). Player B (ownPlayerData, no relation to A's NFT) callschop_treewith A's mint. The transaction succeeds and A's NFT metadata now readswood = "1". B can equally do this through a session key.anchor/programs/extension_nft/src/state/game_data.rs:14(before the fix) compares the old value:With
total_wood_collected = MAX_WOOD_PER_TREE - 1a chop storesMAX_WOOD_PER_TREEinstead of resetting; the "New Tree coming up" reset only happens on the next chop, so every tree yieldsMAX_WOOD_PER_TREE + 1wood.Reproduce
(If port 8899 is busy, start
solana-test-validator --reset --rpc-port 9099 ... --bpf-program H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3 target/deploy/extension_nft.soand runANCHOR_PROVIDER_URL=http://127.0.0.1:9099 ANCHOR_WALLET=~/.config/solana/id.json pnpm mocha --import=tsx -t 1000000 'tests/**/*.ts'.)Tests:
Chop tree with another player's NFT is rejected(tests/lumberjack.ts) andstate::game_data::tests::resets_when_total_reaches_max(Rust unit test). Against the unmodified program:Fix
chop_tree.rs:mintis nowInterfaceAccount<'info, Mint>(must be a real token mint) and the context gainsplayer_token_account: InterfaceAccount<'info, TokenAccount>constrained withassociated_token::mint = mint,associated_token::authority = player.authority,associated_token::token_program = token_programandamount == 1(new errorGameErrorCode::NftNotOwned). The ATA is keyed onplayer.authority, notsigner, so the session-key flow keeps working while a session key can only update the NFT of the player it was issued for. A foreign mint now fails at account validation (AccountNotInitializedwhen the ATA does not exist,NftNotOwnedwhen it exists with balance 0) before any metadata is written.game_data.rs: compare the new total (v >= MAX_WOOD_PER_TREE) so the counter resets on the chop that completes the tree. The reset semantics were ambiguous in the README, so "reset when the new total reachesMAX_WOOD_PER_TREE" was chosen; the tree then yields exactlyMAX_WOOD_PER_TREEwood.mint_nft.rs:token_accounttyped asUncheckedAccountinstead of the deprecatedAccountInfo, socargo clippy -- -D warningspasses on the crate.app/idl/extension_nft.jsonandChopTreeButton.tsxpasses the player's Token-2022 ATA (findAssociatedTokenPda) asplayerTokenAccountin both the session-key and main-wallet paths. The Unity client (unity/) is generated from the IDL and already did not passmint; it was not updated.Verification