Skip to content

earnfidotfun/earnfi-staking

Repository files navigation

EarnFi Staking — Anchor Smart Contract

Non-custodial EARNFI token staking program for Solana, built with the Anchor framework.

Architecture

Reward mechanism: per-pool Synthetix-style accumulators (v2)

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).

Lock pools (v2)

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%

Amount size bands

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.

Program accounts

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

Instructions

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

Getting started

Prerequisites

# 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 install

Build

anchor build

After the first build, grab the real program ID:

solana-keygen pubkey target/deploy/earnfi_staking-keypair.json

Update declare_id!() in programs/earnfi-staking/src/lib.rs and all entries in Anchor.toml, then rebuild:

anchor build

Test (localnet)

anchor test

Deploy to devnet

anchor deploy --provider.cluster devnet
anchor migrate --provider.cluster devnet

Deploy to mainnet

anchor deploy --provider.cluster mainnet
anchor migrate --provider.cluster mainnet

Integration with the EarnFi backend

After deployment:

  1. Set the deployed program ID in your backend signer and app configuration.
  2. At each epoch end, the backend swaps USDC fees to EARNFI and calls add_rewards(reward_amount) via an admin signer (STAKING_SK in the node signer service).
  3. Users sign stake, unstake, and claim in the browser wallet — user keys are never held by the server.
  4. The app database mirrors on-chain state for fast UI reads (positions, pending rewards).

EarnFi stack: on-chain vs backend vs browser

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.

VPS / headless install (no TTY)

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 build

If 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.

Why export PATH=... lines matter

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

Security notes

  • The stake vault authority is the StakingState PDA — only the program can move tokens out.
  • add_rewards is admin-only (checked via constraint).
  • Arithmetic uses checked_* throughout; overflows return StakingError::Overflow.
  • Emergency set_paused(true) blocks all user operations.
  • unstake enforces the lock period via Clock::get().unix_timestamp.
  • The reward accumulator uses u128 to handle large reward-per-token values without precision loss.

EARNFI Token

  • Mainnet mint: FradHppu1s67CegxxSARmQmyJ26yCF8soUZaSE9qpump
  • Decimals: 9
  • Token standard: SPL Token (Token-2022 not yet supported — the program uses anchor_spl::token, not token_2022)

About

EarnFi Staking — Solana Anchor ProgramOpen-source staking program for **EARNFI** on Solana. Staked tokens are held in a program-controlled vault PDA; rewards use per-pool accumulators with fixed lock-pool shares.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors