All articles
solanastakingstake-programvalidators

Solana staking: the Stake program, account states, and warmup math

Solana staking via the Stake program — delegate, warmup, active, deactivate, cooldown, withdraw. The state machine, account layout, and the math.

Solana's Stake program is one of three core natives (alongside System and Vote). It owns every stake account on the network — the accounts that hold delegated SOL, accrue validator rewards each epoch, and gate the unstaking flow with a cooldown.

The Stake program lives at Stake11111111111111111111111111111111111111. Every liquid-staking protocol, every validator dashboard, every delegation flow ultimately calls it. Here's how it works.

The stake account states

text
Uninitialized   →  freshly-created, no metadata yet
Initialized     →  has Meta but no Stake delegation
Stake           →  delegated, may be in warmup/active/cooldown/inactive
RewardsPool     →  internal use, system-only

Most accounts you'll touch are in the Stake variant, which carries:

rust
pub struct StakeStateV2 {
    pub meta: Meta {
        rent_exempt_reserve: u64,        // can't be unstaked, ~0.00228 SOL
        authorized: Authorized {
            staker:     Pubkey,           // can delegate/deactivate
            withdrawer: Pubkey,           // can withdraw (and reassign authorities)
        },
        lockup: Lockup {
            unix_timestamp: i64,
            epoch:          u64,
            custodian:      Pubkey,       // for time-locked stake
        },
    },
    pub stake: Stake {
        delegation: Delegation {
            voter_pubkey:    Pubkey,      // the vote account this is delegated to
            stake:           u64,          // amount being staked
            activation_epoch: Epoch,
            deactivation_epoch: Epoch,    // u64::MAX = active (not deactivated)
            warmup_cooldown_rate: f64,    // deprecated, retained for layout
        },
        credits_observed: u64,            // for rewards accounting
    },
    pub flags: StakeFlags,
}

Standard stake account size: 200 bytes. Rent- exempt reserve at modern lamports-per-byte: ~0.00228 SOL.

The lifecycle (and the warmup math)

Delegated stake doesn't become "active" immediately. It enters warmup for one or more epochs while the network gradually increases its effective stake. Same on the way out: cooldown ramps it down.

text
Per-epoch warmup/cooldown rate cap:
  9% of the current total active stake

If your stake is < 9% of total active stake (which it almost certainly is),
your stake activates fully in one epoch. Always.

Only relevant if a single delegation is >9% of total stake — historically
this has only mattered for foundation-scale delegations during testnet ramps.

Epoch length: ~2 days at current parameters (432,000 slots × ~400ms slot time)

Practical implication: stake activates in one epoch (~2 days), deactivates in one epoch (~2 days). The old "multi-epoch warmup" from Solana's early years effectively never triggers anymore at normal delegation sizes.

The instructions

rust
pub enum StakeInstruction {
    Initialize(Authorized, Lockup),               // 0
    Authorize(Pubkey, StakeAuthorize),            // 1  — deprecated
    DelegateStake,                                // 2
    Split(u64),                                   // 3  — split stake into two accounts
    Withdraw(u64),                                // 4  — must be deactivated + cooled
    Deactivate,                                   // 5
    SetLockup(LockupArgs),                        // 6
    Merge,                                        // 7  — combine two compatible stake accounts
    AuthorizeWithSeed(AuthorizeWithSeedArgs),     // 8
    InitializeChecked,                            // 9
    AuthorizeChecked(StakeAuthorize),             // 10
    AuthorizeCheckedWithSeed(...),                // 11
    SetLockupChecked(LockupCheckedArgs),          // 12
    GetMinimumDelegation,                         // 13 — returns minimum stake size
    DeactivateDelinquent,                         // 14
    Redelegate (deprecated),                      // 15
    MoveStake(u64),                               // 16 — newer, atomic stake-account split+merge
    MoveLamports(u64),                            // 17
}

Common flows

Stake N SOL to validator V:

  1. Create a system account, allocate 200 bytes, transfer N+rent lamports in
  2. Call Initialize to set up Meta with your staker/withdrawer keys
  3. Call DelegateStake pointing at V's vote account
typescript
import {
  Connection, Keypair, PublicKey, SystemProgram, StakeProgram,
  LAMPORTS_PER_SOL, Authorized, Lockup, Transaction, sendAndConfirmTransaction,
} from "@solana/web3.js"

const stakeAccount = Keypair.generate()
const amount = 1 * LAMPORTS_PER_SOL
const rentExempt = await connection.getMinimumBalanceForRentExemption(StakeProgram.space)

const tx = new Transaction().add(
  // 1. Create + initialize the stake account
  StakeProgram.createAccount({
    fromPubkey: payer.publicKey,
    stakePubkey: stakeAccount.publicKey,
    lamports: amount + rentExempt,
    authorized: new Authorized(payer.publicKey, payer.publicKey),
    lockup: new Lockup(0, 0, payer.publicKey),
  }),
  // 2. Delegate to validator V's vote account
  StakeProgram.delegate({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: payer.publicKey,
    votePubkey: validatorVoteAccount,
  }),
)

await sendAndConfirmTransaction(connection, tx, [payer, stakeAccount])

Unstake (start the cooldown):

typescript
const tx = new Transaction().add(
  StakeProgram.deactivate({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: payer.publicKey,
  }),
)
await sendAndConfirmTransaction(connection, tx, [payer])

Withdraw after cooldown ends:

typescript
// Wait at least one epoch boundary, then:
const tx = new Transaction().add(
  StakeProgram.withdraw({
    stakePubkey: stakeAccount.publicKey,
    authorizedPubkey: payer.publicKey,
    toPubkey: payer.publicKey,
    lamports: amount,  // up to (account.lamports - rent_exempt_reserve)
  }),
)

Reading effective stake

Status (active / activating / deactivating / inactive) and effective amount come from connection.getStakeActivation(stakeAccount) on the client side — the runtime computes this from epoch deltas and the warmup/cooldown rate:

typescript
const activation = await connection.getStakeActivation(stakeAccount.publicKey)
// { state: "active" | "activating" | "deactivating" | "inactive",
//   active: <effective active lamports this epoch>,
//   inactive: <not-yet-active or already-cooled lamports> }

Rewards

At each epoch boundary the network distributes inflation + priority fee + Jito MEV rewards to stake accounts based on their delegation to vote accounts that earned credits. The Stake program updates credits_observed per stake account and credits the additional lamports directly to the stake account's balance — your effective stake grows in place, compounding.

Validator commission is deducted before this credit, so your stake account's growth reflects post-commission yield. To actually receive the rewards as liquid SOL you deactivate + wait + withdraw, same as the original principal.

Why LSTs exist

The Stake program's flow above is the "native" path: deactivate, wait ~2 days, withdraw. Liquid Staking Tokens (mSOL, JitoSOL, INF, etc) wrap this so you can hold a transferable SPL token that represents your share of a pool of staked SOL, and exchange that token back to SOL instantly via an LST liquidity layer — at the cost of a small fee and trust in the LST issuer's pool management.

References

Solana staking: the Stake program, account states, and warmup math | devrels.xyz