Non-custodial EARNFI token staking program for Solana, built with the Anchor framework.
Each lock pool has its own pool_reward_per_token_stored[4] and pool_weighted_stake[4]. When add_rewards runs, the reward is split by fixed pool shares (10% / 15% / 25% / 50%), then each pool’s accumulator updates independently.
earned = weighted_amount × (pool_rpt[lock_tier] − reward_per_token_paid) / 1e12 + pending_rewards
weighted_amount = amount × amount_tier_multiplier_bps / 10_000 (size band only — lock length selects the pool, not a lock multiplier).
| Tier (u8) | Duration | Share of each add_rewards distribution |
|---|---|---|
| 0 | 14 days | 10% |
| 1 | 30 days | 15% |
| 2 | 90 days | 25% |
| 3 | 180 days | 50% |
| Band | Min stake (EARNFI) | Weight multiplier |
|---|---|---|
| Standard | 500,000 | 1.00× |
| Supporter | 3,000,000 | 1.10× |
| Champion | 10,000,000 | 1.20× |
Breaking change: v2 uses a larger StakingState account layout. Deploy a new program ID and migrate; legacy v1 positions cannot share state with v2 without a cutoff/migration plan.
| Account | PDA Seeds | Description |
|---|---|---|
StakingState |
["staking_state"] |
Global singleton — total stake, reward accumulator, epoch counter |
StakeVault |
["stake_vault"] |
SPL token account holding all staked EARNFI (authority = StakingState PDA) |
RewardVault |
["reward_vault"] |
Reserve vault (authority = StakingState PDA) |
StakePosition |
["stake_position", user, nonce] |
Per-user stake position |
EpochRecord |
["epoch_record", epoch_number] |
Immutable record of each completed epoch |
| Instruction | Who | Description |
|---|---|---|
initialize(epoch_duration_days) |
Admin (once) | Deploy global state and vaults |
stake(position_nonce, lock_tier, amount) |
User | Deposit EARNFI and open a position |
unstake(position_nonce) |
User | Withdraw principal + pending rewards after lock expires |
claim(position_nonce) |
User | Harvest rewards without unstaking |
add_rewards(reward_amount) |
Admin | Split reward across four lock pools, update per-pool accumulators, advance epoch |
set_paused(paused) |
Admin | Emergency pause/unpause |
update_admin() |
Admin | Transfer admin authority |
update_epoch_duration(days) |
Admin | Change epoch duration |
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install Solana CLI (1.18+)
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
# Install Anchor CLI (0.30.x)
cargo install --git https://github.com/coral-xyz/anchor avm --locked
avm install 0.30.1
avm use 0.30.1
# Install JS deps
yarn installanchor buildAfter the first build, grab the real program ID:
solana-keygen pubkey target/deploy/earnfi_staking-keypair.jsonUpdate declare_id!() in programs/earnfi-staking/src/lib.rs and all entries in Anchor.toml, then rebuild:
anchor buildanchor testanchor deploy --provider.cluster devnet
anchor migrate --provider.cluster devnetanchor deploy --provider.cluster mainnet
anchor migrate --provider.cluster mainnetAfter deployment:
- Set the deployed program ID in your backend signer and app configuration.
- At each epoch end, the backend swaps USDC fees to EARNFI and calls
add_rewards(reward_amount)via an admin signer (STAKING_SKin the node signer service). - Users sign
stake,unstake, andclaimin the browser wallet — user keys are never held by the server. - The app database mirrors on-chain state for fast UI reads (positions, pending rewards).
| Layer | Role |
|---|---|
| Anchor on Solana | Source of truth. Staked EARNFI sits in StakeVault (authority = StakingState PDA). Users sign stake / unstake / claim in the browser. |
| Backend app | Epoch ledger, USDC treasury, Jupiter swap, add_rewards signing. Does not custody user keys. |
| Browser UI | Wallet-connected staking page; reads APY and positions from the backend API. |
Use non-interactive Rust and put tools on PATH in the same script session.
# Rust (non-interactive)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env"
# Solana CLI (path printed by installer — typical below)
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
# Anchor version manager (needs cargo on PATH first)
cargo install --git https://github.com/coral-xyz/anchor avm --locked --no-track
export PATH="$HOME/.cargo/bin:$PATH"
avm install 0.30.1 && avm use 0.30.1
# JS: prefer npm if yarn is not installed (Ubuntu `apt install yarn` often installs wrong `cmdtest`)
cd "$(dirname "$0")"
npm install
anchor buildIf cargo, solana, or anchor is “not found” after install, your shell never sourced ~/.cargo/env — add . "$HOME/.cargo/env" to ~/.bashrc or prefix deploy scripts with it.
The Solana installer puts binaries under ~/.local/share/solana/install/active_release/bin, and Rust/Cargo put tools (including avm / anchor) under ~/.cargo/bin. Those directories are not on PATH by default. The export PATH=... lines prepend those folders so the current shell can find solana, cargo, anchor, etc. If you open a new SSH session, you must either run those export lines again, add them to ~/.bashrc / ~/.profile, or rely on source ~/.cargo/env (Rust) plus a permanent Solana PATH entry.
# Typical permanent additions (adjust if your install paths differ)
echo '. "$HOME/.cargo/env"' >> ~/.bashrc
echo 'export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"' >> ~/.bashrc
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc- The stake vault authority is the
StakingStatePDA — only the program can move tokens out. add_rewardsis admin-only (checked via constraint).- Arithmetic uses
checked_*throughout; overflows returnStakingError::Overflow. - Emergency
set_paused(true)blocks all user operations. unstakeenforces the lock period viaClock::get().unix_timestamp.- The reward accumulator uses
u128to handle large reward-per-token values without precision loss.
- Mainnet mint:
FradHppu1s67CegxxSARmQmyJ26yCF8soUZaSE9qpump - Decimals: 9
- Token standard: SPL Token (Token-2022 not yet supported — the program uses
anchor_spl::token, nottoken_2022)