solana-foundation / solana-foundation/program-examples
tokens/nft-staking: propose an NFT staking example (delegate + freeze, checkpointed rewards)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 543
- Avg merge
- 21h 7m
- Merged PRs (30d)
- 14
Description
Proposal
I'd like to add tokens/nft-staking/anchor: stake an NFT to earn reward tokens over time, and claim those rewards without unstaking.
The NFT never leaves the owner's wallet. Instead of moving it into a vault, the program takes delegate authority over the owner's token account with approve and freezes it in place via Metaplex FreezeDelegatedAccount, reversing with ThawDelegatedAccount + revoke on unstake. That is how NFT staking usually works in production, and it is a deliberate contrast with the vault custody tokens/escrow already teaches.
Raising this first per CONTRIBUTING, since a new example is a substantial change.
What it teaches
- Delegate-and-freeze custody —
approve+FreezeDelegatedAccountappears nowhere today. The only other "delegate" matches are Token-2022'spermanent-delegate, a mint-level admin override rather than a revocable per-account approval. - Reward accrual against a checkpoint — every current
Clockuse (token-fundraiser,games/gacha,games/world-cup) is a one-shot deadline check. None settle a value repeatedly across an account's life, which is the mechanism under every yield-bearing protocol on Solana and has its own failure mode: settle without recording that you settled, and the next call pays for the same span again. emit!events — not used in any example today.
Design
| Account | Seeds | Holds |
|---|---|---|
StakeConfig |
["config"] |
Collection, reward rate, stake cap, freeze period |
UserAccount |
["user", user] |
Lifetime points, currently staked count |
StakeAccount |
["stake", nft_mint, config] |
Owner, mint, staked_at, last_claimed_at |
Instructions: initialize_config, initialize_user, stake, claim, unstake. StakeAccount doubles as the SPL delegate for the staked NFT, and the reward mint's authority is the config PDA so only this program can mint.
Two design notes worth flagging, since both are easy to miss and the example is partly there to teach them:
claimadvanceslast_claimed_atby exactly the whole days it pays for, in the same instruction as the payout. Snapping it tonowinstead would silently swallow the part-day remainder.approveand the Metaplex freeze both succeed on a zero-balance token account, and anyone can open an ATA for any mint — sostakehas to check the balance explicitly, or a caller can farm rewards from an NFT they never held.
Scope
Anchor only to start, on the current pins (anchor-lang/anchor-spl 1.0.2, freeze/thaw through anchor-spl's metadata feature with no direct mpl-token-metadata dependency). Tests on LiteSVM with clock warping, against the real Token Metadata program from the prepare.mjs fixture.
Prior art: #58 and #118 both built NFT staking and neither landed
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing tokens/escrow example and the proposed tokens/nft-staking/anchor layout, then inspect prepare.mjs and the LiteSVM test setup. The example is done when it covers config and user initialization, delegate-and-freeze staking, checkpointed claims, thaw-and-revoke unstaking, and clock-warped tests against the real Token Metadata program.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, rust
- Domain
- blockchain, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100