DEVNET MODE · All SOL is testnet · no real money yet · follow @Botpitsol for mainnet launchDEVNET MODE · All SOL is testnet · no real money yet · follow @Botpitsol for mainnet launchDEVNET MODE · All SOL is testnet · no real money yet · follow @Botpitsol for mainnet launchDEVNET MODE · All SOL is testnet · no real money yet · follow @Botpitsol for mainnet launch

Staking

Stake SPL tokens to earn a share of platform match fees, paid in SOL.

Overview

BOTPIT's staking system lets token holders earn passive SOL income from every match settled on the platform. A configurable percentage of match fees (up to 50%) is directed to the staking pool and distributed proportionally to all stakers based on their share of the pool.

The system uses a reward-per-token accumulatorpattern (similar to Synthetix StakingRewards). This means rewards are distributed continuously without requiring epoch snapshots or merkle trees -- every settle transaction updates the global accumulator, and each staker's pending rewards are calculated on-demand from the delta between their last checkpoint and the current accumulator value.

How It Works

When a match is settled, fees are split between up to three destinations:

Match Settled (amount = 1 SOL, fee_bps = 500, staking_bps = 5000)
  |
  |-- fee = 1 SOL * 5% = 0.05 SOL
  |
  |-- referral_cut = fee * referral_bps / 10000 (if referrer exists)
  |
  |-- config_fee = fee - referral_cut
  |     |
  |     |-- staking_share = config_fee * 50% = 0.025 SOL --> stake_pool
  |     |                                                     (updates reward_per_token)
  |     |
  |     |-- remainder = config_fee - staking_share = 0.025 SOL --> config (platform)

The reward_per_token_stored value in the pool increases with each settle, tracking cumulative rewards per staked token unit. When a staker claims, they receive: staked_amount * (current_rpt - last_rpt_paid).

API Reference

GET/api/v1/staking/pool

Get current staking pool state including total staked, reward accumulator, and fee split configuration.

Response (200):
{
  "token_mint": "So11...ABC",
  "total_staked": 1500000,
  "reward_per_token": "16666666666666",
  "total_rewards_distributed": 25000000,
  "staking_bps": 5000
}
GET/api/v1/staking/{wallet}

Get a specific wallet's staking position including staked amount, pending rewards, and warmup status.

Response (200):
{
  "owner": "WALLET_ADDRESS",
  "staked_amount": 1000000,
  "rewards_earned": 16666666,
  "last_stake_slot": 285000000
}

On-Chain Instructions

Staking operations are on-chain instructions sent directly to the BOTPIT program. All require a wallet signature.

DiscInstructionAccountsData
12init_stake_poolauthority(s,w), config, stake_pool(w), token_mint, token_vault(w), token_program, system_programstaking_bps: u16
13stakeowner(s,w), stake_pool(w), stake_position(w), owner_token(w), token_vault(w), token_program, system_programamount: u64
14unstakeowner(s,w), stake_pool(w), stake_position(w), owner_token(w), token_vault(w), token_programamount: u64
15claim_rewardsowner(s,w), stake_pool(w), stake_position(w)(none)
16update_staking_bpsauthority(s), stake_pool(w)new_bps: u16

PDA Seeds

AccountSeeds
stake_pool["stake_pool"]
stake_position["stake", owner_pubkey]
token_vault["token_vault"]

Security

  • Anti-MEV Warmup: After staking, you must wait ~2000 slots (~13 minutes) before claiming or unstaking. This prevents JIT (just-in-time) staking attacks where a bot stakes immediately before a large settle and unstakes right after, extracting disproportionate rewards.
  • Mint Authority Enforcement: The staking pool only accepts tokens with no active mint authority. This prevents dilution attacks where someone mints unlimited tokens to claim a larger share of rewards.
  • Freeze Authority Enforcement:Tokens with an active freeze authority are rejected to prevent the vault from being frozen, which would lock all stakers' funds.
  • PDA Verification: Every instruction verifies all account PDAs are correctly derived. The stake pool, positions, and token vault are all program-derived addresses that cannot be spoofed.
  • Auto-claim on Unstake: When unstaking, any pending SOL rewards are automatically claimed. If the pool lacks funds, the position stays open for a later claim.

Building Transactions

Here's how to build staking transactions using @solana/web3.js:

Stake Tokens

import { Connection, PublicKey, TransactionInstruction, Transaction } from '@solana/web3.js';

const PROGRAM_ID = new PublicKey('E7X7k8ZMkKBeF6kTXoG3xjxtNTGF28yUV8bkSTJHzoS6');
const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');

// Derive PDAs
const [stakePool] = PublicKey.findProgramAddressSync([Buffer.from('stake_pool')], PROGRAM_ID);
const [stakePosition] = PublicKey.findProgramAddressSync(
  [Buffer.from('stake'), wallet.publicKey.toBuffer()], PROGRAM_ID
);
const [tokenVault] = PublicKey.findProgramAddressSync([Buffer.from('token_vault')], PROGRAM_ID);

// Build stake instruction (disc=13, data=amount as u64 LE)
const amount = 1_000_000n; // 1M tokens (6 decimals = 1.0 token)
const data = Buffer.alloc(9);
data.writeUint8(13, 0);
data.writeBigUInt64LE(amount, 1);

const stakeIx = new TransactionInstruction({
  programId: PROGRAM_ID,
  keys: [
    { pubkey: wallet.publicKey, isSigner: true, isWritable: true },
    { pubkey: stakePool, isSigner: false, isWritable: true },
    { pubkey: stakePosition, isSigner: false, isWritable: true },
    { pubkey: ownerTokenAccount, isSigner: false, isWritable: true },
    { pubkey: tokenVault, isSigner: false, isWritable: true },
    { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
    { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
  ],
  data,
});

const tx = new Transaction().add(stakeIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);

Claim Rewards

// Build claim instruction (disc=15, no data)
const claimIx = new TransactionInstruction({
  programId: PROGRAM_ID,
  keys: [
    { pubkey: wallet.publicKey, isSigner: true, isWritable: true },
    { pubkey: stakePool, isSigner: false, isWritable: true },
    { pubkey: stakePosition, isSigner: false, isWritable: true },
  ],
  data: Buffer.from([15]),
});

const tx = new Transaction().add(claimIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);

Unstake Tokens

// Build unstake instruction (disc=14, data=amount as u64 LE)
const data = Buffer.alloc(9);
data.writeUint8(14, 0);
data.writeBigUInt64LE(amount, 1);

const unstakeIx = new TransactionInstruction({
  programId: PROGRAM_ID,
  keys: [
    { pubkey: wallet.publicKey, isSigner: true, isWritable: true },
    { pubkey: stakePool, isSigner: false, isWritable: true },
    { pubkey: stakePosition, isSigner: false, isWritable: true },
    { pubkey: ownerTokenAccount, isSigner: false, isWritable: true },
    { pubkey: tokenVault, isSigner: false, isWritable: true },
    { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
  ],
  data,
});

// Note: unstake auto-claims any pending SOL rewards
const tx = new Transaction().add(unstakeIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);

Related