The SPL Stake Pool program allows users to delegate stake to multiple validators while maintaining liquidity through pool tokens. Thorough documentation of the SPL Stake Pool program can be found here. This is meant to be a quick primer, and the Advanced Concepts section contains nuances relevant to the Steward program.
Update Validator List Balances:
Update Stake Pool Balance:
Cleanup Removed Validator Entries:
The pool uses Solana's Stake program for core staking operations:
These operations are performed via Cross-Program Invocation (CPI) calls from the Stake Pool program to the Stake program.
Stake accounts in the pool must maintain minimum balances that cover rent and stake minimums:
The pool uses the minimum_stake_lamports
function to calculate this:
pub fn minimum_stake_lamports(meta: &Meta, stake_program_minimum_delegation: u64) -> u64 {
meta.rent_exempt_reserve
.saturating_add(minimum_delegation(stake_program_minimum_delegation))
}
pub fn minimum_delegation(stake_program_minimum_delegation: u64) -> u64 {
std::cmp::max(stake_program_minimum_delegation, MINIMUM_ACTIVE_STAKE)
}
This ensures that stake accounts always have enough lamports to remain valid and delegated. You cannot decrease or increase a validator account's stake with fewer lamports than the minimum_delegation, because the transient stake account would not have enough lamports to remain valid.
As of August 2024, the minimum lamports for a stake account is 3_282_880 lamports.
When keeping track of all lamports in the pool, it's important to note the rent for stake accounts in the Stake Pool comes from the reserve account.
Validator Addition: When adding a new validator, the rent for the validator's stake account is funded from the pool's reserve account.
let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation);
Self::stake_split(
stake_pool_info.key,
reserve_stake_info.clone(),
withdraw_authority_info.clone(),
AUTHORITY_WITHDRAW,
stake_pool.stake_withdraw_bump_seed,
required_lamports,
stake_info.clone(),
)?;
Transient Stake Accounts: When creating a transient stake account (used for rebalancing), the rent is funded from the pool's reserve account.
let required_lamports = stake_rent.saturating_add(lamports);
Self::stake_split(
stake_pool_info.key,
reserve_stake_info.clone(),
withdraw_authority_info.clone(),
AUTHORITY_WITHDRAW,
stake_pool.stake_withdraw_bump_seed,
required_lamports,
transient_stake_account_info.clone(),
)?;
The Stake Pool always ensures that any stake account it creates or manages has sufficient lamports to remain rent-exempt.
When a validator is removed from the stake pool, the process involves several steps and may take multiple epochs to complete. The exact path depends on the current state of the validator's stake accounts and when the UpdateValidatorListBalance
instruction is called.
RemoveValidatorFromPool
instruction is called by the stake pool's staker.StakeStatus
is changed to DeactivatingValidator
.The completion of the removal process depends on when UpdateValidatorListBalance
is called and the state of the validator's stake accounts. This instruction can be called in the same epoch as the removal or in later epochs.
If the validator has no transient stake when removed:
First UpdateValidatorListBalance
call:
StakeStatus
changes to ReadyForRemoval
.StakeStatus
remains DeactivatingValidator
.Subsequent UpdateValidatorListBalance
calls:
DeactivatingValidator
:ReadyForRemoval
:If the validator has transient stake when removed:
First UpdateValidatorListBalance
call:
StakeStatus
changes to DeactivatingAll
.Subsequent UpdateValidatorListBalance
calls:
StakeStatus
changes to DeactivatingValidator
.StakeStatus
changes to DeactivatingTransient
.Final UpdateValidatorListBalance
call:
StakeStatus
changes to ReadyForRemoval
.ReadyForRemoval
:UpdateValidatorListBalance
must be called regularly to progress the removal process.DeactivatingValidator
, DeactivatingAll
, DeactivatingTransient
) cannot receive new stake.