Skip to content

About

On-chain SPL token staking on Solana (Anchor) with Synthetix-style rewards, lock periods, and emergency unstake.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

SPL Token Staking Program

An on-chain SPL token staking program for Solana, written in Anchor 1.0 with a Synthetix-style reward distribution model.

Users lock their SPL tokens into a pool and earn reward tokens proportional to their share of the total stake and the time elapsed. A configurable lock period prevents early withdrawal, while an emergency unstake path allows exiting early at a penalty.


Features

  • Pool initialization — admin creates a staking pool with separate stake and reward token mints, a reward rate (tokens/sec), lock period, and early-withdraw penalty (basis points).
  • Stake — users deposit stake tokens into the vault; a StakeAccount PDA tracks their position. Re-staking accrues pending rewards without losing them.
  • Unstake — after the lock period expires, the user withdraws all staked tokens plus accrued rewards. The stake account is closed and rent refunded.
  • Claim rewards — claim accumulated rewards without unstaking.
  • Fund rewards — admin tops up the reward vault (admin-only, enforced via has_one).
  • Emergency unstake — exit before the lock period with a penalty (early_withdraw_penalty bps deducted from the returned amount; penalty stays in the stake vault).
  • Sweep penalty — admin collects accumulated penalties (everything in the stake vault above total_staked) into their own token account.

Reward Model (Synthetix-style)

A global accumulator reward_per_token (u128, scaled by 1e9) is updated before every action:

elapsed = current_time - last_update_time
reward_per_token += (elapsed * reward_rate * 1e9) / total_staked
last_update_time = current_time

A user's pending rewards:

pending = (stake_amount * (reward_per_token - reward_debt)) / 1e9

After any settle (stake, unstake, claim), reward_debt is advanced to the current reward_per_token. This correctly distributes rewards across multiple stakers with different entry times.

Account Layout

Account Seeds Description
StakingPool ["staking_pool", admin, stake_mint] Global pool state
StakeAccount ["stake_account", pool, user] Per-user position
StakeVault ATA of pool PDA, stake mint Holds staked tokens
RewardVault ATA of pool PDA, reward mint Holds reward tokens

Instructions

Instruction Args Access
initialize_pool reward_rate, lock_period, penalty admin
stake amount user
unstake — user
claim_rewards — user
fund_rewards amount admin
emergency_unstake — user
sweep_penalty — admin

Tech Stack

Component Technology
Program Anchor 1.0.2, anchor-spl 1.0.2
Language Rust (edition 2021)
Testing LiteSVM 0.10 (no solana-test-validator)
Client Codama / @solana/kit (generated, clients/ts/)

Project Structure

programs/magican-spl-staking/
├── src/
│   ├── lib.rs                  # Program entry, instruction dispatch
│   ├── state.rs                # StakingPool, StakeAccount, reward math
│   ├── constants.rs            # Seeds, REWARD_PRECISION, MAX_PENALTY_BPS
│   ├── error.rs                # StakingError enum
│   └── instructions/
│       ├── initialize_pool.rs
│       ├── stake.rs
│       ├── unstake.rs
│       ├── claim_rewards.rs
│       ├── fund_rewards.rs
│       ├── emergency_unstake.rs
│       └── sweep_penalty.rs
├── tests/
│   ├── litesvm.rs              # 14 integration tests
│   └── common/mod.rs           # LiteSVM test harness & helpers
└── Cargo.toml
clients/
└── ts/                         # Generated TypeScript client (Codama + @solana/kit)
docs/
├── IDEA.md                     # Original specification
└── PROGRESS.md                 # Stage-by-stage progress tracker

Testing

Tests run entirely in-process via LiteSVM — no local validator required. The test harness deploys the SBF-compiled program, seeds SPL Token mints and ATAs directly, and exercises every instruction against the real SPL Token builtin.

# Build the SBF binary
cargo build-sbf

# Run the LiteSVM test suite
cargo test --test litesvm

Test Coverage

Test Verifies
test_initialize_pool Pool fields set correctly
test_stake Tokens move to vault, stake account created
test_stake_twice Re-staking preserves accumulated rewards
test_claim_rewards Rewards proportional to time staked
test_unstake_after_lock Full withdrawal + rewards after lock expires
test_unstake_before_lock Rejected with LockPeriodNotExpired
test_emergency_unstake Penalty deducted, balance correct
test_sweep_penalty Admin collects vault surplus after emergency unstake
test_sweep_penalty_non_admin Non-admin rejected with Unauthorized
test_sweep_penalty_nothing_to_sweep Rejected when vault holds only staked funds
test_fund_rewards_admin Reward vault topped up
test_fund_rewards_non_admin Non-admin rejected with Unauthorized
test_reward_math_single_staker Single staker receives 100% of rewards
test_reward_math_two_stakers Two stakers split rewards proportionally

All 14 tests pass.

Build & Lint

cargo build-sbf          # Compile SBF binary
cargo check              # Type-check host code
cargo test --test litesvm # Run integration tests

TypeScript Client

A TypeScript client generated from the program IDL via Codama, targeting @solana/kit, lives in clients/ts/. It provides typed instruction builders for all seven instructions, PDA helpers, account decoders (StakingPool, StakeAccount), and typed program errors.

cd clients/ts
npm install
npm run generate   # regenerate from target/idl/*.json
npm run typecheck
npm run smoke      # read-only devnet check

Deployment

Deployed on Solana devnet:

B5ZofijbzwTpCtjLr6SeUKSKoTJSXKaC5vZqyBfBBryC

View on Solana Explorer

About

On-chain SPL token staking on Solana (Anchor) with Synthetix-style rewards, lock periods, and emergency unstake.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages