Skip to content

Detailed Analysis of Key Staking Pallet Functions

1. Staking Ledger Management

1.1 Getting and Updating Ledger

pub fn ledger(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
    StakingLedger::<T>::get(account)
}

pub fn bonded(stash: &T::AccountId) -> Option<T::AccountId> {
    StakingLedger::<T>::paired_account(Stash(stash.clone()))
}

Purpose: Managing relationships between stash and controller accounts, retrieving staking information.

1.2 Slashable Balance Calculation

pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
    Self::ledger(Stash(stash.clone())).map(|l| l.active).unwrap_or_default()
}

pub fn slashable_balance_of_vote_weight(
    stash: &T::AccountId,
    issuance: BalanceOf<T>,
) -> VoteWeight {
    T::CurrencyToVote::to_vote(Self::slashable_balance_of(stash), issuance)
}

Purpose: Determining the amount that can be slashed and converting to votes for elections.

2. Interest and Reward System

2.1 Interest Calculation

fn calculate_interest(era: EraIndex, opt_vivid_staking: &Option<VividStakingState>) -> Perbill {
    if let Some(vivid_staking) = opt_vivid_staking {
        if vivid_staking.starting_era <= era && vivid_staking.ending_era >= era {
            let eras_count = vivid_staking
                .ending_era
                .saturating_sub(vivid_staking.starting_era.saturating_sub(1));
            let months_count = eras_count / 28;

            return T::StandartStakingInterest::get()
                + Perbill::from_parts(
                    T::VividStakingInterestPerMonth::get().deconstruct() * months_count,
                );
        }
    }
    T::StandartStakingInterest::get()
}

Calculation Logic: 1. Check vivid staking activity in current era 2. Count number of locked months 3. Calculation: base_rate + additional_rate * months

2.2 Reward Payout

pub(super) fn do_payout_stakers(
    validator_stash: T::AccountId,
    era: EraIndex,
) -> DispatchResultWithPostInfo {
    // Input data validation
    let current_era = CurrentEra::<T>::get().ok_or_else(|| {
        Error::<T>::InvalidEraToReward
            .with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
    })?;

    // Era history check
    let history_depth = T::HistoryDepth::get();
    ensure!(
        era <= current_era && era >= current_era.saturating_sub(history_depth),
        Error::<T>::InvalidEraToReward
    );

    // Get ledger and check for already claimed rewards
    let mut ledger = Self::ledger(account.clone())?;
    match ledger.claimed_rewards.binary_search(&era) {
        Ok(_) => return Err(Error::<T>::AlreadyClaimed),
        Err(pos) => ledger.claimed_rewards.try_insert(pos, era)?,
    }

    // Get era exposure
    let exposure = <ErasStakersClipped<T>>::get(&era, &stash);

    // Calculate rewards for nominators
    for nominator in exposure.others.iter() {
        let interest = Self::calculate_interest(era, &nominator.vivid_staking);
        let mut nominator_reward: BalanceOf<T> = portion * interest * nominator.value;

        // Deduct validator commission
        validator_payment_commissions += validator_commission * nominator_reward;
        nominator_reward -= validator_commission * nominator_reward;

        // Create payout
        if let Some((imbalance, dest)) = Self::make_payout(&nominator.who, nominator_reward) {
            nominator_payout_count += 1;
            Self::deposit_event(Event::<T>::Rewarded {
                stash: nominator.who.clone(),
                dest,
                amount: imbalance.peek(),
            });
            total_imbalance.subsume(imbalance);
        }
    }

    // Calculate rewards for validator
    let validator_own_interest = Self::calculate_interest(era, &exposure.vivid_staking);
    let validator_own_reward = portion * validator_own_interest * exposure.own;
    let validator_reward = validator_own_reward + validator_payment_commissions;

    // Payout to validator
    if let Some((imbalance, dest)) = Self::make_payout(&stash, validator_reward) {
        Self::deposit_event(Event::<T>::Rewarded {
            stash: stash.clone(),
            dest,
            amount: imbalance.peek(),
        });
        total_imbalance.subsume(imbalance);
    }

    T::Reward::on_unbalanced(total_imbalance);
    Ok(Some(T::WeightInfo::payout_stakers_alive_staked(nominator_payout_count)).into())
}

Key Steps: 1. Era validation and payout permission check 2. Get exposure (nominators and their stakes) 3. Calculate interest considering vivid staking 4. Deduct validator commission 5. Create payouts via make_payout()

2.3 Creating Payouts

fn make_payout(
    stash: &T::AccountId,
    amount: BalanceOf<T>,
) -> Option<(PositiveImbalanceOf<T>, RewardDestination<T::AccountId>)> {
    let dest = Self::payee(StakingAccount::Stash(stash.clone()));
    let maybe_imbalance = match dest {
        RewardDestination::Controller => Self::bonded(stash)
            .map(|controller| T::Currency::deposit_creating(&controller, amount)),
        RewardDestination::Stash => T::Currency::deposit_into_existing(stash, amount).ok(),
        RewardDestination::Staked => Self::ledger(Stash(stash.clone()))
            .and_then(|mut ledger| {
                ledger.active += amount;
                ledger.total += amount;
                let r = T::Currency::deposit_into_existing(stash, amount).ok();
                let _ = ledger.update().defensive_proof("ledger fetched from storage, so it exists; qed.");
                Ok(r)
            })
            .unwrap_or_default(),
        RewardDestination::Account(dest_account) => {
            Some(T::Currency::deposit_creating(&dest_account, amount))
        },
        RewardDestination::None => None,
    };
    maybe_imbalance.map(|imbalance| (imbalance, Self::payee(StakingAccount::Stash(stash.clone()))))
}

Reward Recipient Types: - Controller: To controller account - Stash: To stash account - Staked: Automatic addition to stake - Account: To specified account - None: No payout

3. Era Management

3.1 Planning New Session

fn new_session(
    session_index: SessionIndex,
    is_genesis: bool,
) -> Option<BoundedVec<T::AccountId, MaxWinnersOf<T>>> {
    if let Some(current_era) = Self::current_era() {
        let current_era_start_session_index = Self::eras_start_session_index(current_era)
            .unwrap_or_else(|| {
                frame_support::print("Error: start_session_index must be set for current_era");
                0
            });

        let era_length = session_index.saturating_sub(current_era_start_session_index);

        match ForceEra::<T>::get() {
            Forcing::ForceNew => (),
            Forcing::ForceAlways => (),
            Forcing::NotForcing if era_length >= T::SessionsPerEra::get() => (),
            _ => return None,
        }

        let maybe_new_era_validators = Self::try_trigger_new_era(session_index, is_genesis);
        if maybe_new_era_validators.is_some() && matches!(ForceEra::<T>::get(), Forcing::ForceNew) {
            Self::set_force_era(Forcing::NotForcing);
        }

        maybe_new_era_validators
    } else {
        Self::try_trigger_new_era(session_index, is_genesis)
    }
}

Planning Logic: 1. Check current era 2. Analyze forcing modes (ForceNew, ForceAlways) 3. Check if session limit in era is reached 4. Trigger new era if necessary

3.2 Starting New Era

fn start_era(start_session: SessionIndex) {
    let active_era = ActiveEra::<T>::mutate(|active_era| {
        let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0);
        *active_era = Some(ActiveEraInfo {
            index: new_index,
            start: None,
        });
        new_index
    });

    let bonding_duration = T::BondingDuration::get();

    BondedEras::<T>::mutate(|bonded| {
        bonded.push((active_era, start_session));

        if active_era > bonding_duration {
            let first_kept = active_era - bonding_duration;
            let n_to_prune = bonded.iter().take_while(|&&(era_idx, _)| era_idx < first_kept).count();

            for (pruned_era, _) in bonded.drain(..n_to_prune) {
                slashing::clear_era_metadata::<T>(pruned_era);
            }

            if let Some(&(_, first_session)) = bonded.first() {
                T::SessionInterface::prune_historical_up_to(first_session);
            }
        }
    });

    Self::apply_unapplied_slashes(active_era);
}

Actions on Era Start: 1. Increment active era index 2. Update BondedEras 3. Clean old data (slashing metadata) 4. Apply deferred slashes

3.3 Ending Era

fn end_era(active_era: ActiveEraInfo, _session_index: SessionIndex) {
    if let Some(active_era_start) = active_era.start {
        let now_as_millis_u64 = T::UnixTime::now().as_millis().saturated_into::<u64>();
        let era_duration = (now_as_millis_u64 - active_era_start).saturated_into::<u64>();
        let staked = Self::eras_total_stake(&active_era.index);
        let issuance = T::Currency::total_issuance();

        let portion = Perbill::from_rational(era_duration as u64, MILLISECONDS_PER_YEAR);
        let estimated_interest = T::StandartStakingInterest::get()
            + Perbill::from_parts(T::VividStakingInterestPerMonth::get().deconstruct() * 12);

        let estimated_payout = estimated_interest * portion * staked;

        if (issuance + estimated_payout) <= T::IssuanceLimit::get() && <StakingActive<T>>::get() {
            Self::deposit_event(Event::<T>::EraPaid { era_index: active_era.index });
            <ErasDuration<T>>::insert(&active_era.index, era_duration);
        } else {
            <StakingActive<T>>::set(false);
        }

        <OffendingValidators<T>>::kill();
    }
}

Ending Logic: 1. Calculate era duration 2. Estimate payouts considering issuance limit 3. Stop staking when limit is exceeded 4. Clean offender data

4. Election Provider Implementation

4.1 Getting Voters

pub fn get_npos_voters(bounds: DataProviderBounds) -> Vec<VoterOf<Self>> {
    let mut voters_size_tracker: StaticTracker<Self> = StaticTracker::default();
    let final_predicted_len = {
        let all_voter_count = T::VoterList::count();
        bounds.count.unwrap_or(all_voter_count.into()).min(all_voter_count.into()).0
    };

    let mut all_voters = Vec::<_>::with_capacity(final_predicted_len as usize);
    let weight_of = Self::weight_of_fn();

    let mut voters_seen = 0u32;
    let mut validators_taken = 0u32;
    let mut nominators_taken = 0u32;
    let mut min_active_stake = u64::MAX;

    let mut sorted_voters = T::VoterList::iter();
    while all_voters.len() < final_predicted_len as usize
        && voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
    {
        let voter = match sorted_voters.next() {
            Some(voter) => {
                voters_seen.saturating_inc();
                voter
            },
            None => break,
        };

        let voter_weight = weight_of(&voter);
        if voter_weight.is_zero() {
            continue;
        }

        if let Some(Nominations { targets, .. }) = <Nominators<T>>::get(&voter) {
            if !targets.is_empty() {
                let voter = (voter, voter_weight, targets);
                if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() {
                    Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
                        size: voters_size_tracker.size as u32,
                    });
                    break;
                }
                all_voters.push(voter);
                nominators_taken.saturating_inc();
            }
        } else if Validators::<T>::contains_key(&voter) {
            let self_vote = (
                voter.clone(),
                voter_weight,
                vec![voter.clone()].try_into().expect("MaxVotesPerVoter must be >= 1"),
            );

            if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() {
                Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
                    size: voters_size_tracker.size as u32,
                });
                break;
            }
            all_voters.push(self_vote);
            validators_taken.saturating_inc();
        }

        min_active_stake = if voter_weight < min_active_stake { voter_weight } else { min_active_stake };
    }

    MinimumActiveStake::<T>::put(min_active_stake);
    all_voters
}

Voter Retrieval Algorithm: 1. Iterate through VoterList 2. Filter by weight (exclude zero weights) 3. Separate nominators and validators 4. Apply bounds constraints 5. Track minimum active stake

4.2 Getting Targets

pub fn get_npos_targets(bounds: DataProviderBounds) -> Vec<T::AccountId> {
    let mut targets_size_tracker: StaticTracker<Self> = StaticTracker::default();
    let final_predicted_len = {
        let all_target_count = T::TargetList::count();
        bounds.count.unwrap_or(all_target_count.into()).min(all_target_count.into()).0
    };

    let mut all_targets = Vec::<T::AccountId>::with_capacity(final_predicted_len as usize);
    let mut targets_seen = 0;

    let mut targets_iter = T::TargetList::iter();
    while all_targets.len() < final_predicted_len as usize
        && targets_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32)
    {
        let target = match targets_iter.next() {
            Some(target) => {
                targets_seen.saturating_inc();
                target
            },
            None => break,
        };

        if targets_size_tracker.try_register_target(target.clone(), &bounds).is_err() {
            Self::deposit_event(Event::<T>::SnapshotTargetsSizeExceeded {
                size: targets_size_tracker.size as u32,
            });
            break;
        }

        if Validators::<T>::contains_key(&target) {
            all_targets.push(target);
        }
    }

    all_targets
}

5. Runtime API

5.1 Nominations Quota

pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
    T::NominationsQuota::get_quota(balance)
}

5.2 Payout Estimation

pub fn api_estimate_staking_payout(account: T::AccountId) -> BalanceOf<T> {
    let Some(active_era) = Self::active_era() else { return Zero::zero() };

    let era_duration: u64 = T::SessionsPerEra::get()
        .saturating_mul(T::NextNewSession::average_session_length().saturated_into())
        .saturating_mul(T::ExpectedBlockTime::get().saturated_into())
        .try_into().unwrap_or(0);

    let portion = Perbill::from_rational(era_duration, MILLISECONDS_PER_YEAR);

    let Some(Nominations { targets, .. }) = Self::nominators(&account) else {
        return Zero::zero();
    };

    targets.iter().fold(Zero::zero(), |acc, validator| {
        let exposure = Self::eras_stakers_clipped(active_era.index, validator);
        let Some(nominator_exposure) = exposure.others.iter().find(|&x| x.who == account) else {
            return acc;
        };

        let validator_prefs = Self::eras_validator_prefs(&active_era.index, &validator);
        let interest = Self::calculate_interest(active_era.index, &nominator_exposure.vivid_staking);

        let nominator_reward: BalanceOf<T> = portion
            * interest * Perbill::one()
            .saturating_sub(validator_prefs.commission)
            * nominator_exposure.value;

        acc + nominator_reward
    })
}

Estimation Algorithm: 1. Get active era 2. Calculate era duration 3. Iterate through nominated validators 4. Calculate rewards considering vivid staking and commissions 5. Sum all potential payouts

Conclusion

The Staking pallet implements a complex economic model with support for: - Flexible interest system (base + vivid) - Issuance limits - Efficient election algorithms - Runtime API for external integrations - Protection against abuse

The architecture provides high performance and security for the Proof-of-Stake system.