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
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-onlyMost accounts you'll touch are in the Stake variant, which carries:
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.
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
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:
- Create a system account, allocate 200 bytes, transfer N+rent lamports in
- Call
Initializeto set up Meta with your staker/withdrawer keys - Call
DelegateStakepointing at V's vote account
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):
const tx = new Transaction().add(
StakeProgram.deactivate({
stakePubkey: stakeAccount.publicKey,
authorizedPubkey: payer.publicKey,
}),
)
await sendAndConfirmTransaction(connection, tx, [payer])Withdraw after cooldown ends:
// 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:
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.