SOLSTAKE Docs
Solana SPL Token Staking Protocol
Overview
SOLSTAKE is a non-custodial SPL token staking protocol built on Solana using the Anchor framework. It allows any SPL token holder to stake their tokens and earn yield at a fixed APY set by the pool authority.
Fixed APY
APY is set at pool creation and can never be changed — fully transparent and predictable.
Non-Custodial
All funds are held in PDA-controlled vaults. The authority cannot withdraw user deposits.
Pausable
The authority can pause new stakes, but users can always unstake and claim — even when paused.
Any SPL Token
Works with any existing SPL token mint. Just provide the mint address when initializing.
Architecture
The protocol uses two on-chain account types and PDA-controlled token vaults.
StakePool
StakeAccount
PDA Seeds
| Account | Seeds |
|---|---|
| StakePool | "stake_pool" + mint.key() |
| StakeVault | "stake_vault" + pool.key() |
| RewardVault | "reward_vault" + pool.key() |
| StakeAccount | "stake_account" + pool.key() + owner.key() |
Instructions
The program exposes 6 instructions — 3 admin-only and 3 user-facing.
initialize_pool
AdminCreates a new staking pool for a given SPL token mint. Sets the immutable APY (in basis points) and initializes the stake and reward vaults as PDA-controlled token accounts.
deposit_rewards
AdminTransfers tokens from the authority's wallet into the reward vault. These tokens are distributed to stakers when they claim rewards.
toggle_pause
AdminToggles the pool's paused state. When paused, new stakes are blocked but users can still unstake and claim existing rewards.
stake
UserStakes a specified amount of tokens into the pool. Creates a StakeAccount if one doesn't exist for the user, or adds to the existing stake. Records the timestamp for reward calculation.
unstake
UserWithdraws a specified amount of staked tokens back to the user's wallet. Automatically claims any pending rewards before unstaking.
claim_rewards
UserClaims accumulated staking rewards based on the pool's APY and time elapsed since last stake. Rewards are transferred from the reward vault to the user's token account.
Deployment Guide
Requirements
- Rust — latest stable via
rustup - Solana CLI — v1.18+ (
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)") - Anchor CLI — v0.30+ (
cargo install --git https://github.com/coral-xyz/anchor avm && avm install latest && avm use latest) - Node.js — v18+ with yarn or npm
Build the Program
Install dependencies
yarn install
Build the Anchor program
anchor build
This compiles the program and generates the IDL and keypair in target/.
Get your program ID
solana address -k target/deploy/sol_stake-keypair.json
Update your program ID in four places:
programs/sol-stake/src/lib.rs— insidedeclare_id!("...")Anchor.toml— under[programs.devnet]and[programs.localnet]app/src/lib/constants.ts— thePROGRAM_IDexportapp/src/lib/idl.json— the"address"field on line 2
Rebuild with the correct program ID
anchor build
Configure Your Wallet
The default wallet path in Anchor.toml is .keys/deployer.json. Create this keypair or update the path:
# Option A: Create a new deployer keypair
mkdir -p .keys
solana-keygen new -o .keys/deployer.json
# Option B: Use your existing Solana CLI keypair
# Edit Anchor.toml → wallet = "~/.config/solana/id.json"
Deploy to Devnet
# Configure CLI for devnet
solana config set --url devnet
# Airdrop some SOL for deployment fees
solana airdrop 2
# Deploy
anchor deploy --provider.cluster devnet
# Copy the updated IDL to the frontend
cp target/idl/sol_stake.json app/src/lib/idl.json
Deploy to Mainnet
# Configure CLI for mainnet
solana config set --url mainnet-beta
# Ensure your wallet has enough SOL for deployment (~3-5 SOL)
solana balance
# Deploy
anchor deploy --provider.cluster mainnet
Testing on Devnet
Follow this walkthrough to test the full staking flow on Solana devnet. No real tokens are needed — everything uses free devnet SOL and a test SPL token created by the built-in faucet.
Prerequisites
- The program is deployed to devnet (see Deployment Guide)
- The frontend is running locally (
cd app && yarn dev) - A Solana wallet browser extension (Phantom, Solflare, or Backpack) set to Devnet
Get Devnet SOL
You need a small amount of devnet SOL to pay for transaction fees. Use either method:
# Via Solana CLI
solana airdrop 2 YOUR_WALLET_ADDRESS --url devnet
# Or visit the web faucet
# https://faucet.solana.com
Full Demo Walkthrough
Connect your wallet
Open http://localhost:3000 and click Select Wallet in the navbar. Choose your wallet and approve the connection. The network badge should show "Devnet".
Create a test token & pool (Admin)
Navigate to /admin. Expand the Test Faucet section and click Create Test Token + Pool. This mints a brand-new SPL token, airdrops 1,000 tokens to your wallet, and initializes a staking pool at 12% APY — all in one transaction.
Deposit rewards (Admin)
Still on the Admin page, scroll to Deposit Rewards. Enter an amount (e.g., 100) and click Deposit. This funds the reward vault so stakers can claim yield.
Stake tokens (User)
Go back to the Dashboard (/). Enter an amount to stake (e.g., 50) and click Stake. Your tokens are transferred to the PDA-controlled stake vault.
Watch rewards accrue
The dashboard shows a live "Pending Rewards" counter. Wait a minute or two and you'll see rewards ticking up based on the pool's 12% APY.
Claim rewards
Click Claim Rewards to transfer your accrued rewards from the reward vault to your wallet. Your token balance increases by the claimed amount.
Unstake tokens
Enter an amount and click Unstake. Your staked tokens are returned to your wallet, and any remaining pending rewards are automatically claimed at the same time.
Frontend Configuration
RPC Endpoint (Environment Variable)
The app reads the Solana RPC endpoint from the NEXT_PUBLIC_RPC_ENDPOINT
environment variable. If not set, it falls back to the public Solana devnet RPC.
Create a file called app/.env.local (this file is gitignored and
excluded from the CodeCanyon zip — your API key stays private):
# app/.env.local
# Option 1 — Public devnet (free, rate-limited, fine for quick testing)
NEXT_PUBLIC_RPC_ENDPOINT=https://api.devnet.solana.com
# Option 2 — Helius devnet (recommended, free tier, much higher limits)
NEXT_PUBLIC_RPC_ENDPOINT=https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY
# Option 3 — Helius mainnet (for production)
NEXT_PUBLIC_RPC_ENDPOINT=https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY
# Option 4 — QuickNode
NEXT_PUBLIC_RPC_ENDPOINT=https://your-endpoint.solana-devnet.quiknode.pro/YOUR_TOKEN/
# Option 5 — Any other RPC provider
NEXT_PUBLIC_RPC_ENDPOINT=https://your-rpc-url-here
After creating or editing .env.local, restart the dev server for changes to take effect.
Constants
Other settings are in app/src/lib/constants.ts:
| Constant | Description |
|---|---|
PROGRAM_ID | Your deployed program address. Update after deploying to a new cluster. |
RPC_ENDPOINT | Fallback RPC URL (used only when NEXT_PUBLIC_RPC_ENDPOINT env var is not set). |
TOKEN_DECIMALS | SPL token decimal places (default 9). |
NETWORK_LABEL | Auto-detected from the active RPC URL. Shown in the navbar badge. |
POLL_INTERVAL_MS | How often pool & account data refreshes (default 30000 = 30 seconds). |
HISTORY_POLL_INTERVAL_MS | How often transaction history refreshes (default 60000 = 60 seconds). |
Running the Frontend
cd app
yarn install
yarn dev
The app runs at http://localhost:3000 by default.
RPC Options
Your choice of RPC endpoint affects performance, reliability, and rate limits.
Switching providers is a one-line change in app/.env.local — no code changes needed.
Quick Setup (Recommended)
Sign up for Helius (free)
Go to dev.helius.xyz and create a free account. Copy your API key from the dashboard.
Create app/.env.local
# For devnet
NEXT_PUBLIC_RPC_ENDPOINT=https://devnet.helius-rpc.com/?api-key=YOUR_API_KEY
# For mainnet
NEXT_PUBLIC_RPC_ENDPOINT=https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY
Restart the dev server
cd app && yarn dev
That's it — the app now uses your dedicated RPC with much higher rate limits.
Public Endpoints (Free)
| Network | URL | Notes |
|---|---|---|
| Devnet | https://api.devnet.solana.com | Free, rate-limited. OK for quick testing. |
| Mainnet | https://api.mainnet-beta.solana.com | Free, heavily rate-limited. Not recommended for production. |
429 Too Many Requests errors under normal usage. Always use a dedicated RPC provider for production.
Recommended RPC Providers
Helius ★
Solana-native. Generous free tier (50k credits/day). Best choice for getting started quickly.
QuickNode
Multi-chain. Fast endpoints, add-on marketplace, websocket support.
Triton (RPC Pool)
Solana-focused. High throughput, global edge network.
Alchemy
Multi-chain. Enhanced APIs, debugging tools, generous free tier.
Rate Limiting & Caching
The app includes built-in optimizations to minimize RPC calls:
- Polling intervals — Pool data refreshes every 30s, transaction history every 60s (configurable in
constants.ts). - Transaction caching — Previously fetched transactions are cached in memory. Subsequent polls only fetch new, unseen transactions.
- Batch fetching — Transaction history is fetched in small batches (5 at a time) to stay within rate limits.
- On-demand refresh — Data is also refreshed immediately after user actions (stake, unstake, claim) so you see updates without waiting for the next poll.
Switching to Mainnet
Follow these steps to move from devnet to mainnet-beta.
Update Anchor.toml
Change the cluster from devnet to mainnet-beta:
[provider]
cluster = "mainnet-beta"
wallet = "~/.config/solana/id.json"
Deploy to mainnet
anchor deploy --provider.cluster mainnet
Note the deployed program ID.
Update program ID & RPC
Set PROGRAM_ID in app/src/lib/constants.ts to your mainnet program address:
export const PROGRAM_ID = new PublicKey("YOUR_MAINNET_PROGRAM_ID");
Then set your mainnet RPC in app/.env.local:
NEXT_PUBLIC_RPC_ENDPOINT=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY
Initialize the pool with your token
Go to the Admin panel and paste your existing token's mint address into the "Initialize Pool" section. Do not use the Test Faucet — that creates a throwaway test mint.
constants.ts.
Admin Guide
The admin panel (/admin) lets the pool authority manage the staking pool.
Initialize a Pool
On the admin page, paste your SPL token's mint address and set the APY in basis points
(e.g., 1200 = 12%). Click Launch Pool. The APY is immutable
after creation.
Test Faucet (Demo Only)
For testing purposes, expand the "Test Faucet" section. This creates a brand-new SPL token, airdrops 1,000 tokens to your wallet, and initializes a pool with that test mint. This is only useful for demos on devnet.
Deposit Rewards
The reward vault must be funded for users to claim rewards. Use the "Deposit Rewards" section to transfer tokens from your wallet into the reward vault. You can deposit additional rewards at any time.
Pause & Resume
Toggle the pool's paused state. When paused, new stakes are blocked. Users can always unstake and claim rewards regardless of pause state — the protocol never locks user funds.
Security Model
Immutable APY
Set once at pool creation. The authority cannot change the rate after initialization — what users see is what they get.
PDA-Controlled Vaults
Both stake and reward vaults are owned by program-derived addresses. The authority cannot withdraw user-deposited funds.
Always Unstakeable
Users can unstake and claim rewards even when the pool is paused. The pause only prevents new stakes.
Preserved Rewards
If the reward vault is underfunded, unclaimed rewards are tracked on-chain and can be claimed once the vault is replenished.
Trust Assumptions
- The pool authority is trusted to fund the reward vault. If they don't, rewards accrue but can't be claimed until funded.
- The authority can pause new stakes but cannot prevent withdrawals or claim user funds.
- The program is upgradeable by default (standard Anchor deploy). To make it immutable, use
solana program set-upgrade-authority <PROGRAM_ID> --final.