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 + FreezeDelegatedAccount appears nowhere today. The only other "delegate" matches are Token-2022's permanent-delegate, a mint-level admin override rather than a revocable per-account approval.
- Reward accrual against a checkpoint — every current
Clock use (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:
claim advances last_claimed_at by exactly the whole days it pays for, in the same instruction as the payout. Snapping it to now instead would silently swallow the part-day remainder.
approve and the Metaplex freeze both succeed on a zero-balance token account, and anyone can open an ATA for any mint — so stake has 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
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
approveand freezes it in place via MetaplexFreezeDelegatedAccount, reversing withThawDelegatedAccount+revokeon unstake. That is how NFT staking usually works in production, and it is a deliberate contrast with the vault custodytokens/escrowalready teaches.Raising this first per CONTRIBUTING, since a new example is a substantial change.
What it teaches
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.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
StakeConfig["config"]UserAccount["user", user]StakeAccount["stake", nft_mint, config]staked_at,last_claimed_atInstructions:
initialize_config,initialize_user,stake,claim,unstake.StakeAccountdoubles 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-spl1.0.2, freeze/thaw through anchor-spl'smetadatafeature with no directmpl-token-metadatadependency). Tests on LiteSVM with clock warping, against the real Token Metadata program from theprepare.mjsfixture.Prior art: #58 and #118 both built NFT staking and neither landed