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.