A Solana smart contract (written in Rust using Anchor) for staking NFTs and earning fungible token rewards. This repository enables users to stake their NFTs (SPL token standard), accrue rewards over time, and claim or unstake them according to configurable parameters. TypeScript-based integration tests are included for simulation and validation.
- Overview
- Features
- Architecture
- Directory Structure
- Getting Started
- Testing
- Deployment
- Smart Contract API Reference
- Security Considerations
- Troubleshooting
- Contributing
- License
- Acknowledgements
- Contact
This project enables a staking system for NFTs (Non-Fungible Tokens) on Solana:
- Users stake their NFTs in a vault and accrue reward tokens over time.
- Rewards are distributed as fungible SPL tokens minted by the program.
- Unstaking is subject to a configurable freeze period and max unstake count.
- Fully decentralized, secure, and extensible for NFT-based reward systems.
- Configurable Staking Parameters: Admin sets reward rate, freeze period, and max unstake per user.
- NFT Vault: Staked NFTs are held in secure vault accounts (PDAs).
- Reward Mint: Fungible tokens minted as staking rewards.
- Stake/Unstake/Claim: Users can stake NFTs, wait for freeze period, and claim rewards or unstake NFTs.
- Per-User Accounts: Tracks staking state and rewards for each user.
- Multiple NFT Support: Users can stake multiple NFTs independently.
- Entrypoint:
programs/nft-staking/src/lib.rs
- Instructions:
initialize_config
: Admin sets up global staking config and creates reward mint.initialize_user
: User creates their personal staking account.stake
: Stake an NFT (moves NFT to vault, creates stake record).claim_rewards
: Mint and distribute reward tokens to user.unstake
: Withdraw staked NFT after freeze period.
- State:
- Config PDA: Stores staking parameters (reward rate, freeze, max unstake).
- Reward Mint PDA: Mint for fungible reward tokens, owned by config PDA.
- Vault PDAs: One per staked NFT, stores NFT while staked.
- User Account PDA: Tracks user’s staking status and rewards.
- Stake Record PDA: Tracks each staked NFT by user.
- Config:
PDA("config")
- Reward Mint:
PDA("rewards", config)
- Vault:
PDA("vault", nft_mint)
- User Account:
PDA("user", user_pubkey)
- Stake Record:
PDA("stake", user_pubkey, nft_mint)
nft-staking/
├── programs/
│ └── nft-staking/
│ ├── src/
│ │ ├── lib.rs # Main entrypoint
│ │ ├── instructions/ # Instruction handlers
│ │ ├── state/ # Account state definitions
│ │ ├── errors.rs # Custom error codes
│ │ └── constants.rs
│ └── Cargo.toml
├── tests/
│ └── nft-staking.ts # TypeScript integration tests
├── migrations/
│ └── deploy.ts # Anchor deployment script
├── Anchor.toml # Anchor config
└── README.md # This file
Clone and install dependencies:
git clone https://github.com/Swarnim-Chandve/nft-staking.git
cd nft-staking
anchor install
npm install
anchor build
Run localnet tests (Mocha/TypeScript):
anchor test
- Setup: Creates admin and user accounts, mints NFTs and reward tokens.
- Config: Initializes staking config with reward parameters.
- User: Initializes user account.
- Staking: Stakes NFTs and verifies vault/account state.
- Unstaking: Waits for freeze period and unstakes NFTs.
- Claiming: Claims rewards and verifies token balances.
Test logs show transaction signatures, balances, and on-chain state after each operation.
Set cluster and wallet in Anchor.toml
as desired.
Deploy to cluster:
anchor deploy
Custom deployment logic can be added in migrations/deploy.ts
.
- Admin only. Creates global config and reward mint.
- Parameters:
points_per_stake
(u8): Reward tokens per stake.max_unstake
(u8): Maximum NFTs a user can unstake at once.freeze_period
(u32): Minimum seconds before unstake allowed.
- Accounts:
- Admin (Signer)
- Config PDA
- Reward Mint PDA
- Creates a staking user account.
- Accounts:
- User (Signer)
- User Account PDA
- Stake an NFT, moving it to vault and creating a stake record.
- Accounts:
- User (Signer)
- User Account PDA
- Config PDA
- NFT Mint, User NFT ATA, Vault ATA, Stake Record PDA
- Unstake NFT after freeze period, returning NFT to user.
- Accounts:
- User (Signer)
- User Account PDA
- Config PDA
- NFT Mint, User NFT ATA, Vault ATA, Stake Record PDA
- Mint reward tokens to user for staked NFTs.
- Accounts:
- User (Signer)
- User Account PDA
- Config PDA
- Reward Mint PDA, User Reward ATA
- PDAs: Vaults and mints are owned by PDAs to prevent unauthorized withdrawals/minting.
- Freeze Period: Prevents instant unstake, reducing reward gaming.
- Max Unstake: Limits batch unstake to prevent abuse.
- Checked Transfers: All token operations use SPL checked instructions.
- Rent Exemption: All accounts created are rent-exempt.
- Insufficient SOL: Use airdrop in localnet for test accounts.
- Account Already Exists: Anchor handles
init_if_needed
for PDAs. - Unstake Fails: Ensure freeze period has elapsed and you haven't exceeded max unstake.
- Transaction Fails: Confirm all PDAs and ATAs are correct and funded.
- Fork and submit PRs!
- Open issues for bugs or features.
- Please add tests for new logic.
MIT License
Open a GitHub issue for questions or support.