Files
pezkuwi-sdk/bizinikiwi/pezframe/staking-async/src/pezpallet/impls.rs
T
pezkuwichain 5a184fd7dc fix: resolve all broken links for check-links.yml CI
## Changes

### High Impact Fixes (RED)
- Fix radium git URL (https://https:// → github.com/paritytech/radium-0.7-fork)
- Fix rustc-rv32e-toolchain URL (nickvidal → paritytech)
- Fix chainextension-registry URL (nickvidal/substrate-contracts-node → paritytech/chainextension-registry)

### Medium Impact Fixes (YELLOW)
- Fix docs.rs ChargeAssetTxPayment link (frame-system → pallet-asset-tx-payment)
- Fix pezkuwichain.github.io → paritytech.github.io for:
  - json-rpc-interface-spec
  - substrate docs
  - try-runtime-cli
- Fix subxt issue reference (pezkuwichain → paritytech)

### Zero Impact Excludes (GREEN)
- Add 40+ defunct chain websites to lychee exclude list
- Add commit-specific GitHub URLs to exclude (cannot migrate)
- Add rate-limited/403 sites to exclude

### Documentation
- Refactor .claude/domains_repositories.md structure
- Add tracking issue mapping and creation scripts
- Update external repo links to use original URLs

Result: 🔍 9610 Total  6747 OK 🚫 0 Errors
2025-12-23 09:37:12 +03:00

2140 lines
72 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! `pezpallet-staking-async`'s main `impl` blocks.
use crate::{
asset,
election_size_tracker::StaticTracker,
log,
session_rotation::{self, Eras, Rotator},
slashing::OffenceRecord,
weights::WeightInfo,
BalanceOf, Exposure, Forcing, LedgerIntegrityState, MaxNominationsOf, Nominations,
NominationsQuota, PositiveImbalanceOf, RewardDestination, SnapshotStatus, StakingLedger,
ValidatorPrefs, STAKING_ID,
};
use alloc::{boxed::Box, vec, vec::Vec};
use pezframe_election_provider_support::{
bounds::CountBound, data_provider, DataProviderBounds, ElectionDataProvider, ElectionProvider,
PageIndex, ScoreProvider, SortedListProvider, VoteWeight, VoterOf,
};
use pezframe_support::{
defensive,
dispatch::WithPostDispatchInfo,
pezpallet_prelude::*,
traits::{
Defensive, DefensiveSaturating, Get, Imbalance, InspectLockableCurrency, LockableCurrency,
OnUnbalanced,
},
weights::Weight,
StorageDoubleMap,
};
use pezframe_system::{pezpallet_prelude::BlockNumberFor, RawOrigin};
use pezpallet_staking_async_rc_client::{self as rc_client};
use pezsp_runtime::{
traits::{CheckedAdd, Saturating, StaticLookup, Zero},
ArithmeticError, DispatchResult, Perbill,
};
use pezsp_staking::{
currency_to_vote::CurrencyToVote,
EraIndex, OnStakingUpdate, Page, SessionIndex, Stake,
StakingAccount::{self, Controller, Stash},
StakingInterface,
};
use super::pezpallet::*;
#[cfg(feature = "try-runtime")]
use pezframe_support::ensure;
#[cfg(any(test, feature = "try-runtime"))]
use pezsp_runtime::TryRuntimeError;
/// The maximum number of iterations that we do whilst iterating over `T::VoterList` in
/// `get_npos_voters`.
///
/// In most cases, if we want n items, we iterate exactly n times. In rare cases, if a voter is
/// invalid (for any reason) the iteration continues. With this constant, we iterate at most 2 * n
/// times and then give up.
const NPOS_MAX_ITERATIONS_COEFFICIENT: u32 = 2;
impl<T: Config> Pezpallet<T> {
/// Returns the minimum required bond for participation, considering nominators,
/// and the chains existential deposit.
///
/// This function computes the smallest allowed bond among `MinValidatorBond` and
/// `MinNominatorBond`, but ensures it is not below the existential deposit required to keep an
/// account alive.
pub(crate) fn min_chilled_bond() -> BalanceOf<T> {
MinValidatorBond::<T>::get()
.min(MinNominatorBond::<T>::get())
.max(asset::existential_deposit::<T>())
}
/// Returns the minimum required bond for participation in staking as a validator account.
pub(crate) fn min_validator_bond() -> BalanceOf<T> {
MinValidatorBond::<T>::get().max(asset::existential_deposit::<T>())
}
/// Returns the minimum required bond for participation in staking as a nominator account.
pub(crate) fn min_nominator_bond() -> BalanceOf<T> {
MinNominatorBond::<T>::get().max(asset::existential_deposit::<T>())
}
/// Fetches the ledger associated with a controller or stash account, if any.
pub fn ledger(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
StakingLedger::<T>::get(account)
}
pub fn payee(account: StakingAccount<T::AccountId>) -> Option<RewardDestination<T::AccountId>> {
StakingLedger::<T>::reward_destination(account)
}
/// Fetches the controller bonded to a stash account, if any.
pub fn bonded(stash: &T::AccountId) -> Option<T::AccountId> {
StakingLedger::<T>::paired_account(Stash(stash.clone()))
}
/// Inspects and returns the corruption state of a ledger and direct bond, if any.
///
/// Note: all operations in this method access directly the `Bonded` and `Ledger` storage maps
/// instead of using the [`StakingLedger`] API since the bond and/or ledger may be corrupted.
/// It is also meant to check state for direct bonds and may not work as expected for virtual
/// bonds.
pub(crate) fn inspect_bond_state(
stash: &T::AccountId,
) -> Result<LedgerIntegrityState, Error<T>> {
// look at any old unmigrated lock as well.
let hold_or_lock = asset::staked::<T>(&stash)
.max(T::OldCurrency::balance_locked(STAKING_ID, &stash).into());
let controller = <Bonded<T>>::get(stash).ok_or_else(|| {
if hold_or_lock == Zero::zero() {
Error::<T>::NotStash
} else {
Error::<T>::BadState
}
})?;
match Ledger::<T>::get(controller) {
Some(ledger) => {
if ledger.stash != *stash {
Ok(LedgerIntegrityState::Corrupted)
} else {
if hold_or_lock != ledger.total {
Ok(LedgerIntegrityState::LockCorrupted)
} else {
Ok(LedgerIntegrityState::Ok)
}
}
},
None => Ok(LedgerIntegrityState::CorruptedKilled),
}
}
/// The total balance that can be slashed from a stash account as of right now.
pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf<T> {
// Weight note: consider making the stake accessible through stash.
Self::ledger(Stash(stash.clone())).map(|l| l.active).unwrap_or_default()
}
/// Internal impl of [`Self::slashable_balance_of`] that returns [`VoteWeight`].
pub fn slashable_balance_of_vote_weight(
stash: &T::AccountId,
issuance: BalanceOf<T>,
) -> VoteWeight {
T::CurrencyToVote::to_vote(Self::slashable_balance_of(stash), issuance)
}
/// Returns a closure around `slashable_balance_of_vote_weight` that can be passed around.
///
/// This prevents call sites from repeatedly requesting `total_issuance` from backend. But it is
/// important to be only used while the total issuance is not changing.
pub fn weight_of_fn() -> Box<dyn Fn(&T::AccountId) -> VoteWeight> {
// NOTE: changing this to unboxed `impl Fn(..)` return type and the pezpallet will still
// compile, while some types in mock fail to resolve.
let issuance = asset::total_issuance::<T>();
Box::new(move |who: &T::AccountId| -> VoteWeight {
Self::slashable_balance_of_vote_weight(who, issuance)
})
}
/// Same as `weight_of_fn`, but made for one time use.
pub fn weight_of(who: &T::AccountId) -> VoteWeight {
let issuance = asset::total_issuance::<T>();
Self::slashable_balance_of_vote_weight(who, issuance)
}
/// Checks if a slash has been cancelled for the given era and slash parameters.
pub(crate) fn check_slash_cancelled(
era: EraIndex,
validator: &T::AccountId,
slash_fraction: Perbill,
) -> bool {
let cancelled_slashes = CancelledSlashes::<T>::get(&era);
cancelled_slashes.iter().any(|(cancelled_validator, cancel_fraction)| {
*cancelled_validator == *validator && *cancel_fraction >= slash_fraction
})
}
pub(super) fn do_bond_extra(stash: &T::AccountId, additional: BalanceOf<T>) -> DispatchResult {
let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?;
// for virtual stakers, we don't need to check the balance. Since they are only accessed
// via low level apis, we can assume that the caller has done the due diligence.
let extra = if Self::is_virtual_staker(stash) {
additional
} else {
// additional amount or actual balance of stash whichever is lower.
additional.min(asset::free_to_stake::<T>(stash))
};
ledger.total = ledger.total.checked_add(&extra).ok_or(ArithmeticError::Overflow)?;
ledger.active = ledger.active.checked_add(&extra).ok_or(ArithmeticError::Overflow)?;
// last check: the new active amount of ledger must be more than min bond.
ensure!(ledger.active >= Self::min_chilled_bond(), Error::<T>::InsufficientBond);
// NOTE: ledger must be updated prior to calling `Self::weight_of`.
ledger.update()?;
// update this staker in the sorted list, if they exist in it.
if T::VoterList::contains(stash) {
// This might fail if the voter list is locked.
let _ = T::VoterList::on_update(&stash, Self::weight_of(stash));
}
Self::deposit_event(Event::<T>::Bonded { stash: stash.clone(), amount: extra });
Ok(())
}
/// Calculate the earliest era that withdrawals are allowed for, considering:
/// - The current active era
/// - Any unprocessed offences in the queue
fn calculate_earliest_withdrawal_era(active_era: EraIndex) -> EraIndex {
// get lowest era for which all offences are processed and withdrawals can be allowed.
let earliest_unlock_era_by_offence_queue = OffenceQueueEras::<T>::get()
.as_ref()
.and_then(|eras| eras.first())
.copied()
// if nothing in queue, use the active era.
.unwrap_or(active_era)
// above returns earliest era for which offences are NOT processed yet, so we subtract
// one from it which gives us the oldest era for which all offences are processed.
.saturating_sub(1)
// Unlock chunks are keyed by the era they were initiated plus Bonding Duration.
// We do the same to processed offence era so they can be compared.
.saturating_add(T::BondingDuration::get());
// If there are unprocessed offences older than the active era, withdrawals are only
// allowed up to the last era for which offences have been processed.
// Note: This situation is extremely unlikely, since offences have `SlashDeferDuration` eras
// to be processed. If it ever occurs, it likely indicates offence spam and that we're
// struggling to keep up with processing.
active_era.min(earliest_unlock_era_by_offence_queue)
}
pub(super) fn do_withdraw_unbonded(controller: &T::AccountId) -> Result<Weight, DispatchError> {
let mut ledger = Self::ledger(Controller(controller.clone()))?;
let (stash, old_total) = (ledger.stash.clone(), ledger.total);
let active_era = Rotator::<T>::active_era();
// Ensure last era slashes are applied. Else we block the withdrawals.
if active_era > 1 {
Self::ensure_era_slashes_applied(active_era.saturating_sub(1))?;
}
let earliest_era_to_withdraw = Self::calculate_earliest_withdrawal_era(active_era);
log!(
debug,
"Withdrawing unbonded stake. Active_era is: {:?} | \
Earliest era we can allow withdrawing: {:?}",
active_era,
earliest_era_to_withdraw
);
// withdraw unbonded balance from the ledger until earliest_era_to_withdraw.
ledger = ledger.consolidate_unlocked(earliest_era_to_withdraw);
let new_total = ledger.total;
debug_assert!(
new_total <= old_total,
"consolidate_unlocked should never increase the total balance of the ledger"
);
let used_weight = if ledger.unlocking.is_empty()
&& (ledger.active < Self::min_chilled_bond() || ledger.active.is_zero())
{
// This account must have called `unbond()` with some value that caused the active
// portion to fall below existential deposit + will have no more unlocking chunks
// left. We can now safely remove all staking-related information.
Self::kill_stash(&ledger.stash)?;
T::WeightInfo::withdraw_unbonded_kill()
} else {
// This was the consequence of a partial unbond. just update the ledger and move on.
ledger.update()?;
// This is only an update, so we use less overall weight.
T::WeightInfo::withdraw_unbonded_update()
};
// `old_total` should never be less than the new total because
// `consolidate_unlocked` strictly subtracts balance.
if new_total < old_total {
// Already checked that this won't overflow by entry condition.
let value = old_total.defensive_saturating_sub(new_total);
Self::deposit_event(Event::<T>::Withdrawn { stash, amount: value });
// notify listeners.
T::EventListeners::on_withdraw(controller, value);
}
Ok(used_weight)
}
fn ensure_era_slashes_applied(era: EraIndex) -> Result<(), DispatchError> {
ensure!(
!UnappliedSlashes::<T>::contains_prefix(era),
Error::<T>::UnappliedSlashesInPreviousEra
);
Ok(())
}
pub(super) fn do_payout_stakers(
validator_stash: T::AccountId,
era: EraIndex,
) -> DispatchResultWithPostInfo {
let page = Eras::<T>::get_next_claimable_page(era, &validator_stash).ok_or_else(|| {
Error::<T>::AlreadyClaimed.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
Self::do_payout_stakers_by_page(validator_stash, era, page)
}
pub(super) fn do_payout_stakers_by_page(
validator_stash: T::AccountId,
era: EraIndex,
page: Page,
) -> DispatchResultWithPostInfo {
// Validate input data
let current_era = CurrentEra::<T>::get().ok_or_else(|| {
Error::<T>::InvalidEraToReward
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
let history_depth = T::HistoryDepth::get();
ensure!(
era <= current_era && era >= current_era.saturating_sub(history_depth),
Error::<T>::InvalidEraToReward
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
);
ensure!(
page < Eras::<T>::exposure_page_count(era, &validator_stash),
Error::<T>::InvalidPage.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
);
// Note: if era has no reward to be claimed, era may be future.
let era_payout = Eras::<T>::get_validators_reward(era).ok_or_else(|| {
Error::<T>::InvalidEraToReward
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
let account = StakingAccount::Stash(validator_stash.clone());
let ledger = Self::ledger(account.clone()).or_else(|_| {
if StakingLedger::<T>::is_bonded(account) {
Err(Error::<T>::NotController.into())
} else {
Err(Error::<T>::NotStash.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)))
}
})?;
ledger.clone().update()?;
let stash = ledger.stash.clone();
if Eras::<T>::is_rewards_claimed(era, &stash, page) {
return Err(Error::<T>::AlreadyClaimed
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0)));
}
Eras::<T>::set_rewards_as_claimed(era, &stash, page);
let exposure = Eras::<T>::get_paged_exposure(era, &stash, page).ok_or_else(|| {
Error::<T>::InvalidEraToReward
.with_weight(T::WeightInfo::payout_stakers_alive_staked(0))
})?;
// Input data seems good, no errors allowed after this point
// Get Era reward points. It has TOTAL and INDIVIDUAL
// Find the fraction of the era reward that belongs to the validator
// Take that fraction of the eras rewards to split to nominator and validator
//
// Then look at the validator, figure out the proportion of their reward
// which goes to them and each of their nominators.
let era_reward_points = Eras::<T>::get_reward_points(era);
let total_reward_points = era_reward_points.total;
let validator_reward_points =
era_reward_points.individual.get(&stash).copied().unwrap_or_else(Zero::zero);
// Nothing to do if they have no reward points.
if validator_reward_points.is_zero() {
return Ok(Some(T::WeightInfo::payout_stakers_alive_staked(0)).into());
}
// This is the fraction of the total reward that the validator and the
// nominators will get.
let validator_total_reward_part =
Perbill::from_rational(validator_reward_points, total_reward_points);
// This is how much validator + nominators are entitled to.
let validator_total_payout = validator_total_reward_part * era_payout;
let validator_commission = Eras::<T>::get_validator_commission(era, &ledger.stash);
// total commission validator takes across all nominator pages
let validator_total_commission_payout = validator_commission * validator_total_payout;
let validator_leftover_payout =
validator_total_payout.defensive_saturating_sub(validator_total_commission_payout);
// Now let's calculate how this is split to the validator.
let validator_exposure_part = Perbill::from_rational(exposure.own(), exposure.total());
let validator_staking_payout = validator_exposure_part * validator_leftover_payout;
let page_stake_part = Perbill::from_rational(exposure.page_total(), exposure.total());
// validator commission is paid out in fraction across pages proportional to the page stake.
let validator_commission_payout = page_stake_part * validator_total_commission_payout;
Self::deposit_event(Event::<T>::PayoutStarted {
era_index: era,
validator_stash: stash.clone(),
page,
next: Eras::<T>::get_next_claimable_page(era, &stash),
});
let mut total_imbalance = PositiveImbalanceOf::<T>::zero();
// We can now make total validator payout:
if let Some((imbalance, dest)) =
Self::make_payout(&stash, validator_staking_payout + validator_commission_payout)
{
Self::deposit_event(Event::<T>::Rewarded { stash, dest, amount: imbalance.peek() });
total_imbalance.subsume(imbalance);
}
// Track the number of payout ops to nominators. Note:
// `WeightInfo::payout_stakers_alive_staked` always assumes at least a validator is paid
// out, so we do not need to count their payout op.
let mut nominator_payout_count: u32 = 0;
// Lets now calculate how this is split to the nominators.
// Reward only the clipped exposures. Note this is not necessarily sorted.
for nominator in exposure.others().iter() {
let nominator_exposure_part = Perbill::from_rational(nominator.value, exposure.total());
let nominator_reward: BalanceOf<T> =
nominator_exposure_part * validator_leftover_payout;
// We can now make nominator payout:
if let Some((imbalance, dest)) = Self::make_payout(&nominator.who, nominator_reward) {
// Note: this logic does not count payouts for `RewardDestination::None`.
nominator_payout_count += 1;
let e = Event::<T>::Rewarded {
stash: nominator.who.clone(),
dest,
amount: imbalance.peek(),
};
Self::deposit_event(e);
total_imbalance.subsume(imbalance);
}
}
T::Reward::on_unbalanced(total_imbalance);
debug_assert!(nominator_payout_count <= T::MaxExposurePageSize::get());
Ok(Some(T::WeightInfo::payout_stakers_alive_staked(nominator_payout_count)).into())
}
/// Chill a stash account.
pub(crate) fn chill_stash(stash: &T::AccountId) {
let chilled_as_validator = Self::do_remove_validator(stash);
let chilled_as_nominator = Self::do_remove_nominator(stash);
if chilled_as_validator || chilled_as_nominator {
Self::deposit_event(Event::<T>::Chilled { stash: stash.clone() });
}
}
/// Actually make a payment to a staker. This uses the currency's reward function
/// to pay the right payee for the given staker account.
fn make_payout(
stash: &T::AccountId,
amount: BalanceOf<T>,
) -> Option<(PositiveImbalanceOf<T>, RewardDestination<T::AccountId>)> {
// noop if amount is zero
if amount.is_zero() {
return None;
}
let dest = Self::payee(StakingAccount::Stash(stash.clone()))?;
let maybe_imbalance = match dest {
RewardDestination::Stash => asset::mint_into_existing::<T>(stash, amount),
RewardDestination::Staked => Self::ledger(Stash(stash.clone()))
.and_then(|mut ledger| {
ledger.active += amount;
ledger.total += amount;
let r = asset::mint_into_existing::<T>(stash, amount);
let _ = ledger
.update()
.defensive_proof("ledger fetched from storage, so it exists; qed.");
Ok(r)
})
.unwrap_or_default(),
RewardDestination::Account(ref dest_account) =>
Some(asset::mint_creating::<T>(&dest_account, amount)),
RewardDestination::None => None,
#[allow(deprecated)]
RewardDestination::Controller => Self::bonded(stash)
.map(|controller| {
defensive!("Paying out controller as reward destination which is deprecated and should be migrated.");
// This should never happen once payees with a `Controller` variant have been migrated.
// But if it does, just pay the controller account.
asset::mint_creating::<T>(&controller, amount)
}),
};
maybe_imbalance.map(|imbalance| (imbalance, dest))
}
/// Remove all associated data of a stash account from the staking system.
///
/// Assumes storage is upgraded before calling.
///
/// This is called:
/// - after a `withdraw_unbonded()` call that frees all of a stash's bonded balance.
/// - through `reap_stash()` if the balance has fallen to zero (through slashing).
pub(crate) fn kill_stash(stash: &T::AccountId) -> DispatchResult {
// removes controller from `Bonded` and staking ledger from `Ledger`, as well as reward
// setting of the stash in `Payee`.
StakingLedger::<T>::kill(&stash)?;
Self::do_remove_validator(&stash);
Self::do_remove_nominator(&stash);
Ok(())
}
#[cfg(test)]
pub(crate) fn reward_by_ids(validators_points: impl IntoIterator<Item = (T::AccountId, u32)>) {
Eras::<T>::reward_active_era(validators_points)
}
/// Helper to set a new `ForceEra` mode.
pub(crate) fn set_force_era(mode: Forcing) {
log!(info, "Setting force era mode {:?}.", mode);
ForceEra::<T>::put(mode);
Self::deposit_event(Event::<T>::ForceEra { mode });
}
#[cfg(feature = "runtime-benchmarks")]
pub fn add_era_stakers(
current_era: EraIndex,
stash: T::AccountId,
exposure: Exposure<T::AccountId, BalanceOf<T>>,
) {
Eras::<T>::upsert_exposure(current_era, &stash, exposure);
}
#[cfg(feature = "runtime-benchmarks")]
pub fn set_slash_reward_fraction(fraction: Perbill) {
SlashRewardFraction::<T>::put(fraction);
}
/// Get all the voters associated with `page` that are eligible for the npos election.
///
/// `bounds` can impose a cap on the number of voters returned per page.
///
/// Sets `MinimumActiveStake` to the minimum active nominator stake in the returned set of
/// nominators.
///
/// Note: in the context of the multi-page snapshot, we expect the *order* of `VoterList` and
/// `TargetList` not to change while the pages are being processed.
pub(crate) fn get_npos_voters(
bounds: DataProviderBounds,
status: &SnapshotStatus<T::AccountId>,
) -> Vec<VoterOf<Self>> {
let mut voters_size_tracker: StaticTracker<Self> = StaticTracker::default();
let page_len_prediction = {
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(page_len_prediction as usize);
// cache a few things.
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 = match status {
// start the snapshot processing from the beginning.
SnapshotStatus::Waiting => T::VoterList::iter(),
// snapshot continues, start from the last iterated voter in the list.
SnapshotStatus::Ongoing(account_id) => T::VoterList::iter_from(&account_id)
.defensive_unwrap_or(Box::new(vec![].into_iter())),
// all voters have been consumed already, return an empty iterator.
SnapshotStatus::Consumed => Box::new(vec![].into_iter()),
};
while all_voters.len() < page_len_prediction as usize
&& voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * page_len_prediction 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, do not consider this voter for the snapshot.
if voter_weight.is_zero() {
log!(debug, "voter's active balance is 0. skip this voter.");
continue;
}
if let Some(Nominations { targets, .. }) = <Nominators<T>>::get(&voter) {
if !targets.is_empty() {
// Note on lazy nomination quota: we do not check the nomination quota of the
// voter at this point and accept all the current nominations. The nomination
// quota is only enforced at `nominate` time.
let voter = (voter, voter_weight, targets);
if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() {
// no more space left for the election result, stop iterating.
Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
size: voters_size_tracker.size as u32,
});
break;
}
all_voters.push(voter);
nominators_taken.saturating_inc();
} else {
defensive!("non-nominator fetched from voter list: {:?}", voter);
// technically should never happen, but not much we can do about it.
}
min_active_stake =
if voter_weight < min_active_stake { voter_weight } else { min_active_stake };
} else if Validators::<T>::contains_key(&voter) {
// if this voter is a validator:
let self_vote = (
voter.clone(),
voter_weight,
vec![voter.clone()]
.try_into()
.expect("`MaxVotesPerVoter` must be greater than or equal to 1"),
);
if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() {
// no more space left for the election snapshot, stop iterating.
Self::deposit_event(Event::<T>::SnapshotVotersSizeExceeded {
size: voters_size_tracker.size as u32,
});
break;
}
all_voters.push(self_vote);
validators_taken.saturating_inc();
} else {
// this can only happen if: 1. there a bug in the bags-list (or whatever is the
// sorted list) logic and the state of the two pallets is no longer compatible, or
// because the nominators is not decodable since they have more nomination than
// `T::NominationsQuota::get_quota`. The latter can rarely happen, and is not
// really an emergency or bug if it does.
defensive!(
"invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now",
voter,
);
}
}
// all_voters should have not re-allocated.
debug_assert!(all_voters.capacity() == page_len_prediction as usize);
let min_active_stake: T::CurrencyBalance =
if all_voters.is_empty() { Zero::zero() } else { min_active_stake.into() };
MinimumActiveStake::<T>::put(min_active_stake);
all_voters
}
/// Get all the targets associated are eligible for the npos election.
///
/// The target snapshot is *always* single paged.
///
/// This function is self-weighing as [`DispatchClass::Mandatory`].
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() {
// no more space left for the election snapshot, stop iterating.
log!(warn, "npos targets size exceeded, stopping iteration.");
Self::deposit_event(Event::<T>::SnapshotTargetsSizeExceeded {
size: targets_size_tracker.size as u32,
});
break;
}
if Validators::<T>::contains_key(&target) {
all_targets.push(target);
}
}
log!(debug, "[bounds {:?}] generated {} npos targets", bounds, all_targets.len());
all_targets
}
/// This function will add a nominator to the `Nominators` storage map,
/// and `VoterList`.
///
/// If the nominator already exists, their nominations will be updated.
///
/// NOTE: you must ALWAYS use this function to add nominator or update their targets. Any access
/// to `Nominators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_add_nominator(who: &T::AccountId, nominations: Nominations<T>) {
if !Nominators::<T>::contains_key(who) {
// maybe update sorted list.
let _ = T::VoterList::on_insert(who.clone(), Self::weight_of(who))
.defensive_unwrap_or_default();
}
Nominators::<T>::insert(who, nominations);
}
/// This function will remove a nominator from the `Nominators` storage map,
/// and `VoterList`.
///
/// Returns true if `who` was removed from `Nominators`, otherwise false.
///
/// NOTE: you must ALWAYS use this function to remove a nominator from the system. Any access to
/// `Nominators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_remove_nominator(who: &T::AccountId) -> bool {
let outcome = if Nominators::<T>::contains_key(who) {
Nominators::<T>::remove(who);
let _ = T::VoterList::on_remove(who);
true
} else {
false
};
outcome
}
/// This function will add a validator to the `Validators` storage map.
///
/// If the validator already exists, their preferences will be updated.
///
/// NOTE: you must ALWAYS use this function to add a validator to the system. Any access to
/// `Validators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_add_validator(who: &T::AccountId, prefs: ValidatorPrefs) {
if !Validators::<T>::contains_key(who) {
// maybe update sorted list.
let _ = T::VoterList::on_insert(who.clone(), Self::weight_of(who));
}
Validators::<T>::insert(who, prefs);
}
/// This function will remove a validator from the `Validators` storage map.
///
/// Returns true if `who` was removed from `Validators`, otherwise false.
///
/// NOTE: you must ALWAYS use this function to remove a validator from the system. Any access to
/// `Validators` or `VoterList` outside of this function is almost certainly
/// wrong.
pub fn do_remove_validator(who: &T::AccountId) -> bool {
let outcome = if Validators::<T>::contains_key(who) {
Validators::<T>::remove(who);
let _ = T::VoterList::on_remove(who);
true
} else {
false
};
outcome
}
/// Register some amount of weight directly with the system pezpallet.
///
/// This is always mandatory weight.
pub(crate) fn register_weight(weight: Weight) {
<pezframe_system::Pezpallet<T>>::register_extra_weight_unchecked(
weight,
DispatchClass::Mandatory,
);
}
/// Returns full exposure of a validator for a given era.
///
/// History note: This used to be a getter for old storage item `ErasStakers` deprecated in v14
/// and deleted in v17. Since this function is used in the codebase at various places, we kept
/// it as a custom getter that takes care of getting the full exposure of the validator in a
/// backward compatible way.
pub fn eras_stakers(
era: EraIndex,
account: &T::AccountId,
) -> Exposure<T::AccountId, BalanceOf<T>> {
Eras::<T>::get_full_exposure(era, account)
}
pub(super) fn do_migrate_currency(stash: &T::AccountId) -> DispatchResult {
if Self::is_virtual_staker(stash) {
return Self::do_migrate_virtual_staker(stash);
}
let ledger = Self::ledger(Stash(stash.clone()))?;
let staked: BalanceOf<T> = T::OldCurrency::balance_locked(STAKING_ID, stash).into();
ensure!(!staked.is_zero(), Error::<T>::AlreadyMigrated);
ensure!(ledger.total == staked, Error::<T>::BadState);
// remove old staking lock
T::OldCurrency::remove_lock(STAKING_ID, &stash);
// check if we can hold all stake.
let max_hold = asset::free_to_stake::<T>(&stash);
let force_withdraw = if max_hold >= staked {
// this means we can hold all stake. yay!
asset::update_stake::<T>(&stash, staked)?;
Zero::zero()
} else {
// if we are here, it means we cannot hold all user stake. We will do a force withdraw
// from ledger, but that's okay since anyways user do not have funds for it.
let force_withdraw = staked.saturating_sub(max_hold);
// we ignore if active is 0. It implies the locked amount is not actively staked. The
// account can still get away from potential slash but we can't do much better here.
StakingLedger {
total: max_hold,
active: ledger.active.saturating_sub(force_withdraw),
// we are not changing the stash, so we can keep the stash.
..ledger
}
.update()?;
force_withdraw
};
// Get rid of the extra consumer we used to have with OldCurrency.
pezframe_system::Pezpallet::<T>::dec_consumers(&stash);
Self::deposit_event(Event::<T>::CurrencyMigrated { stash: stash.clone(), force_withdraw });
Ok(())
}
fn do_migrate_virtual_staker(stash: &T::AccountId) -> DispatchResult {
// Funds for virtual stakers not managed/held by this pezpallet. We only need to clear
// the extra consumer we used to have with OldCurrency.
pezframe_system::Pezpallet::<T>::dec_consumers(&stash);
// The delegation system that manages the virtual staker needed to increment provider
// previously because of the consumer needed by this pezpallet. In reality, this stash
// is just a key for managing the ledger and the account does not need to hold any
// balance or exist. We decrement this provider.
let actual_providers = pezframe_system::Pezpallet::<T>::providers(stash);
let expected_providers =
// provider is expected to be 1 but someone can always transfer some free funds to
// these accounts, increasing the provider.
if asset::free_to_stake::<T>(&stash) >= asset::existential_deposit::<T>() {
2
} else {
1
};
// We should never have more than expected providers.
ensure!(actual_providers <= expected_providers, Error::<T>::BadState);
// if actual provider is less than expected, it is already migrated.
ensure!(actual_providers == expected_providers, Error::<T>::AlreadyMigrated);
// dec provider
let _ = pezframe_system::Pezpallet::<T>::dec_providers(&stash)?;
return Ok(());
}
}
impl<T: Config> Pezpallet<T> {
/// Returns the current nominations quota for nominators.
///
/// Used by the runtime API.
pub fn api_nominations_quota(balance: BalanceOf<T>) -> u32 {
T::NominationsQuota::get_quota(balance)
}
pub fn api_eras_stakers(
era: EraIndex,
account: T::AccountId,
) -> Exposure<T::AccountId, BalanceOf<T>> {
Self::eras_stakers(era, &account)
}
pub fn api_eras_stakers_page_count(era: EraIndex, account: T::AccountId) -> Page {
Eras::<T>::exposure_page_count(era, &account)
}
pub fn api_pending_rewards(era: EraIndex, account: T::AccountId) -> bool {
Eras::<T>::pending_rewards(era, &account)
}
}
impl<T: Config> ElectionDataProvider for Pezpallet<T> {
type AccountId = T::AccountId;
type BlockNumber = BlockNumberFor<T>;
type MaxVotesPerVoter = MaxNominationsOf<T>;
fn desired_targets() -> data_provider::Result<u32> {
Self::register_weight(T::DbWeight::get().reads(1));
Ok(ValidatorCount::<T>::get())
}
fn electing_voters(
bounds: DataProviderBounds,
page: PageIndex,
) -> data_provider::Result<Vec<VoterOf<Self>>> {
let mut status = VoterSnapshotStatus::<T>::get();
let voters = Self::get_npos_voters(bounds, &status);
// update the voter snapshot status.
match (page, &status) {
// last page, reset status for next round.
(0, _) => status = SnapshotStatus::Waiting,
(_, SnapshotStatus::Waiting) | (_, SnapshotStatus::Ongoing(_)) => {
let maybe_last = voters.last().map(|(x, _, _)| x).cloned();
if let Some(ref last) = maybe_last {
let has_next =
T::VoterList::iter_from(last).ok().and_then(|mut i| i.next()).is_some();
if has_next {
status = SnapshotStatus::Ongoing(last.clone());
} else {
status = SnapshotStatus::Consumed;
}
}
},
// do nothing.
(_, SnapshotStatus::Consumed) => (),
}
log!(
debug,
"[page {}, (next) status {:?}, bounds {:?}] generated {} npos voters [first: {:?}, last: {:?}]",
page,
status,
bounds,
voters.len(),
voters.first().map(|(x, y, _)| (x, y)),
voters.last().map(|(x, y, _)| (x, y)),
);
match status {
SnapshotStatus::Ongoing(_) => T::VoterList::lock(),
_ => T::VoterList::unlock(),
}
VoterSnapshotStatus::<T>::put(status);
debug_assert!(!bounds.slice_exhausted(&voters));
Ok(voters)
}
fn electing_voters_stateless(
bounds: DataProviderBounds,
) -> data_provider::Result<Vec<VoterOf<Self>>> {
let voters = Self::get_npos_voters(bounds, &SnapshotStatus::Waiting);
log!(debug, "[stateless, bounds {:?}] generated {} npos voters", bounds, voters.len(),);
Ok(voters)
}
fn electable_targets(
bounds: DataProviderBounds,
page: PageIndex,
) -> data_provider::Result<Vec<T::AccountId>> {
if page > 0 {
log!(warn, "multi-page target snapshot not supported, returning page 0.");
}
let targets = Self::get_npos_targets(bounds);
if bounds.exhausted(None, CountBound(targets.len() as u32).into()) {
return Err("Target snapshot too big");
}
debug_assert!(!bounds.slice_exhausted(&targets));
Ok(targets)
}
fn next_election_prediction(_: BlockNumberFor<T>) -> BlockNumberFor<T> {
debug_assert!(false, "this is deprecated and not used anymore");
pezsp_runtime::traits::Bounded::max_value()
}
#[cfg(feature = "runtime-benchmarks")]
fn fetch_page(page: PageIndex) {
session_rotation::EraElectionPlanner::<T>::do_elect_paged(page);
}
#[cfg(feature = "runtime-benchmarks")]
fn add_voter(
voter: T::AccountId,
weight: VoteWeight,
targets: BoundedVec<T::AccountId, Self::MaxVotesPerVoter>,
) {
let stake = <BalanceOf<T>>::try_from(weight).unwrap_or_else(|_| {
panic!("cannot convert a VoteWeight into BalanceOf, benchmark needs reconfiguring.")
});
<Bonded<T>>::insert(voter.clone(), voter.clone());
<Ledger<T>>::insert(voter.clone(), StakingLedger::<T>::new(voter.clone(), stake));
Self::do_add_nominator(&voter, Nominations { targets, submitted_in: 0, suppressed: false });
}
#[cfg(feature = "runtime-benchmarks")]
fn add_target(target: T::AccountId) {
let stake = (Self::min_validator_bond() + 1u32.into()) * 100u32.into();
<Bonded<T>>::insert(target.clone(), target.clone());
<Ledger<T>>::insert(target.clone(), StakingLedger::<T>::new(target.clone(), stake));
Self::do_add_validator(
&target,
ValidatorPrefs { commission: Perbill::zero(), blocked: false },
);
}
#[cfg(feature = "runtime-benchmarks")]
fn clear() {
#[allow(deprecated)]
<Bonded<T>>::remove_all(None);
#[allow(deprecated)]
<Ledger<T>>::remove_all(None);
#[allow(deprecated)]
<Validators<T>>::remove_all();
#[allow(deprecated)]
<Nominators<T>>::remove_all();
T::VoterList::unsafe_clear();
}
#[cfg(feature = "runtime-benchmarks")]
fn put_snapshot(
voters: Vec<VoterOf<Self>>,
targets: Vec<T::AccountId>,
target_stake: Option<VoteWeight>,
) {
targets.into_iter().for_each(|v| {
let stake: BalanceOf<T> = target_stake
.and_then(|w| <BalanceOf<T>>::try_from(w).ok())
.unwrap_or_else(|| Self::min_nominator_bond() * 100u32.into());
<Bonded<T>>::insert(v.clone(), v.clone());
<Ledger<T>>::insert(v.clone(), StakingLedger::<T>::new(v.clone(), stake));
Self::do_add_validator(
&v,
ValidatorPrefs { commission: Perbill::zero(), blocked: false },
);
});
voters.into_iter().for_each(|(v, s, t)| {
let stake = <BalanceOf<T>>::try_from(s).unwrap_or_else(|_| {
panic!("cannot convert a VoteWeight into BalanceOf, benchmark needs reconfiguring.")
});
<Bonded<T>>::insert(v.clone(), v.clone());
<Ledger<T>>::insert(v.clone(), StakingLedger::<T>::new(v.clone(), stake));
Self::do_add_nominator(
&v,
Nominations { targets: t, submitted_in: 0, suppressed: false },
);
});
}
#[cfg(feature = "runtime-benchmarks")]
fn set_desired_targets(count: u32) {
ValidatorCount::<T>::put(count);
}
}
impl<T: Config> rc_client::AHStakingInterface for Pezpallet<T> {
type AccountId = T::AccountId;
type MaxValidatorSet = T::MaxValidatorSet;
/// When we receive a session report from the relay chain, it kicks off the next session.
///
/// There are three special types of things we can do in a session:
/// 1. Plan a new era: We do this one session before the expected era rotation.
/// 2. Kick off election: We do this based on the [`Config::PlanningEraOffset`] configuration.
/// 3. Activate Next Era: When we receive an activation timestamp in the session report, it
/// implies a new validator set has been applied, and we must increment the active era to keep
/// the systems in sync.
fn on_relay_session_report(report: rc_client::SessionReport<Self::AccountId>) -> Weight {
log!(debug, "Received session report: {}", report,);
let rc_client::SessionReport {
end_index,
activation_timestamp,
validator_points,
leftover,
} = report;
debug_assert!(!leftover);
// note: weight for `reward_active_era` is taken care of inside `end_session`
Eras::<T>::reward_active_era(validator_points.into_iter());
session_rotation::Rotator::<T>::end_session(end_index, activation_timestamp)
}
fn weigh_on_relay_session_report(
_report: &rc_client::SessionReport<Self::AccountId>,
) -> Weight {
// worst case weight of this is always
T::WeightInfo::rc_on_session_report()
}
/// Accepts offences only if they are from era `active_era - (SlashDeferDuration - 1)` or newer.
///
/// Slashes for offences are applied `SlashDeferDuration` eras after the offence occurred.
/// Accepting offences older than this range would not leave enough time for slashes to be
/// applied.
///
/// Note: The validator set report that we send to the relay chain contains the pruning
/// information for a relay chain, but we conservatively keep some extra sessions, so it is
/// possible that an offence report is created for a session between SlashDeferDuration and
/// BondingDuration eras before the active era. But they will be dropped here.
fn on_new_offences(
slash_session: SessionIndex,
offences: Vec<rc_client::Offence<T::AccountId>>,
) -> Weight {
log!(debug, "🦹 on_new_offences: {:?}", offences);
let weight = T::WeightInfo::rc_on_offence(offences.len() as u32);
// Find the era to which offence belongs.
let Some(active_era) = ActiveEra::<T>::get() else {
log!(warn, "🦹 on_new_offences: no active era; ignoring offence");
return T::WeightInfo::rc_on_offence(0);
};
let active_era_start_session = Rotator::<T>::active_era_start_session_index();
// Fast path for active-era report - most likely.
// `slash_session` cannot be in a future active era. It must be in `active_era` or before.
let offence_era = if slash_session >= active_era_start_session {
active_era.index
} else {
match BondedEras::<T>::get()
.iter()
// Reverse because it's more likely to find reports from recent eras.
.rev()
.find_map(|&(era, sesh)| if sesh <= slash_session { Some(era) } else { None })
{
Some(era) => era,
None => {
// defensive: this implies offence is for a discarded era, and should already be
// filtered out.
log!(warn, "🦹 on_offence: no era found for slash_session; ignoring offence");
return T::WeightInfo::rc_on_offence(0);
},
}
};
let oldest_reportable_offence_era = if T::SlashDeferDuration::get() == 0 {
// this implies that slashes are applied immediately, so we can accept any offence up to
// bonding duration old.
active_era.index.saturating_sub(T::BondingDuration::get())
} else {
// slashes are deffered, so we only accept offences that are not older than the
// defferal duration.
active_era.index.saturating_sub(T::SlashDeferDuration::get().saturating_sub(1))
};
let invulnerables = Invulnerables::<T>::get();
for o in offences {
let slash_fraction = o.slash_fraction;
let validator: <T as pezframe_system::Config>::AccountId = o.offender.into();
// Skip if the validator is invulnerable.
if invulnerables.contains(&validator) {
log!(debug, "🦹 on_offence: {:?} is invulnerable; ignoring offence", validator);
continue;
}
// ignore offence if too old to report.
if offence_era < oldest_reportable_offence_era {
log!(warn, "🦹 on_new_offences: offence era {:?} too old; Can only accept offences from era {:?} or newer", offence_era, oldest_reportable_offence_era);
Self::deposit_event(Event::<T>::OffenceTooOld {
validator: validator.clone(),
fraction: slash_fraction,
offence_era,
});
// will emit an event for each validator in the report.
continue;
}
let Some(exposure_overview) = <ErasStakersOverview<T>>::get(&offence_era, &validator)
else {
// defensive: this implies offence is for a discarded era, and should already be
// filtered out.
log!(
warn,
"🦹 on_offence: no exposure found for {:?} in era {}; ignoring offence",
validator,
offence_era
);
continue;
};
Self::deposit_event(Event::<T>::OffenceReported {
validator: validator.clone(),
fraction: slash_fraction,
offence_era,
});
let prior_slash_fraction = ValidatorSlashInEra::<T>::get(offence_era, &validator)
.map_or(Zero::zero(), |(f, _)| f);
if let Some(existing) = OffenceQueue::<T>::get(offence_era, &validator) {
if slash_fraction.deconstruct() > existing.slash_fraction.deconstruct() {
OffenceQueue::<T>::insert(
offence_era,
&validator,
OffenceRecord {
reporter: o.reporters.first().cloned(),
reported_era: active_era.index,
slash_fraction,
..existing
},
);
// update the slash fraction in the `ValidatorSlashInEra` storage.
ValidatorSlashInEra::<T>::insert(
offence_era,
&validator,
(slash_fraction, exposure_overview.own),
);
log!(
debug,
"🦹 updated slash for {:?}: {:?} (prior: {:?})",
validator,
slash_fraction,
prior_slash_fraction,
);
} else {
log!(
debug,
"🦹 ignored slash for {:?}: {:?} (existing prior is larger: {:?})",
validator,
slash_fraction,
prior_slash_fraction,
);
}
} else if slash_fraction.deconstruct() > prior_slash_fraction.deconstruct() {
ValidatorSlashInEra::<T>::insert(
offence_era,
&validator,
(slash_fraction, exposure_overview.own),
);
OffenceQueue::<T>::insert(
offence_era,
&validator,
OffenceRecord {
reporter: o.reporters.first().cloned(),
reported_era: active_era.index,
// there are cases of validator with no exposure, hence 0 page, so we
// saturate to avoid underflow.
exposure_page: exposure_overview.page_count.saturating_sub(1),
slash_fraction,
prior_slash_fraction,
},
);
OffenceQueueEras::<T>::mutate(|q| {
if let Some(eras) = q {
log!(debug, "🦹 inserting offence era {} into existing queue", offence_era);
eras.binary_search(&offence_era).err().map(|idx| {
eras.try_insert(idx, offence_era).defensive_proof(
"Offence era must be present in the existing queue",
)
});
} else {
let mut eras = WeakBoundedVec::default();
log!(debug, "🦹 inserting offence era {} into empty queue", offence_era);
let _ = eras
.try_push(offence_era)
.defensive_proof("Failed to push offence era into empty queue");
*q = Some(eras);
}
});
log!(
debug,
"🦹 queued slash for {:?}: {:?} (prior: {:?})",
validator,
slash_fraction,
prior_slash_fraction,
);
} else {
log!(
debug,
"🦹 ignored slash for {:?}: {:?} (already slashed in era with prior: {:?})",
validator,
slash_fraction,
prior_slash_fraction,
);
}
}
weight
}
fn weigh_on_new_offences(offence_count: u32) -> Weight {
T::WeightInfo::rc_on_offence(offence_count)
}
}
impl<T: Config> ScoreProvider<T::AccountId> for Pezpallet<T> {
type Score = VoteWeight;
fn score(who: &T::AccountId) -> Option<Self::Score> {
Self::ledger(Stash(who.clone()))
.ok()
.and_then(|l| {
if Nominators::<T>::contains_key(&l.stash)
|| Validators::<T>::contains_key(&l.stash)
{
Some(l.active)
} else {
None
}
})
.map(|a| {
let issuance = asset::total_issuance::<T>();
T::CurrencyToVote::to_vote(a, issuance)
})
}
#[cfg(feature = "runtime-benchmarks")]
fn set_score_of(who: &T::AccountId, weight: Self::Score) {
// this will clearly results in an inconsistent state, but it should not matter for a
// benchmark.
let active: BalanceOf<T> = weight.try_into().map_err(|_| ()).unwrap();
let mut ledger = match Self::ledger(StakingAccount::Stash(who.clone())) {
Ok(l) => l,
Err(_) => StakingLedger::default_from(who.clone()),
};
ledger.active = active;
<Ledger<T>>::insert(who, ledger);
<Bonded<T>>::insert(who, who);
// we also need to appoint this staker to be validator or nominator, such that their score
// is actually there. Note that `fn score` above checks the role.
<Validators<T>>::insert(who, ValidatorPrefs::default());
// also, we play a trick to make sure that a issuance based-`CurrencyToVote` behaves well:
// This will make sure that total issuance is zero, thus the currency to vote will be a 1-1
// conversion.
let imbalance = asset::burn::<T>(asset::total_issuance::<T>());
// kinda ugly, but gets the job done. The fact that this works here is a HUGE exception.
// Don't try this pattern in other places.
core::mem::forget(imbalance);
}
}
/// A simple sorted list implementation that does not require any additional pallets. Note, this
/// does not provide validators in sorted order. If you desire nominators in a sorted order take
/// a look at [`pezpallet-bags-list`].
pub struct UseValidatorsMap<T>(core::marker::PhantomData<T>);
impl<T: Config> SortedListProvider<T::AccountId> for UseValidatorsMap<T> {
type Score = BalanceOf<T>;
type Error = ();
/// Returns iterator over voter list, which can have `take` called on it.
fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
Box::new(Validators::<T>::iter().map(|(v, _)| v))
}
fn iter_from(
start: &T::AccountId,
) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
if Validators::<T>::contains_key(start) {
let start_key = Validators::<T>::hashed_key_for(start);
Ok(Box::new(Validators::<T>::iter_from(start_key).map(|(n, _)| n)))
} else {
Err(())
}
}
fn lock() {}
fn unlock() {}
fn count() -> u32 {
Validators::<T>::count()
}
fn contains(id: &T::AccountId) -> bool {
Validators::<T>::contains_key(id)
}
fn on_insert(_: T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> {
// nothing to do on insert.
Ok(())
}
fn get_score(id: &T::AccountId) -> Result<Self::Score, Self::Error> {
Ok(Pezpallet::<T>::weight_of(id).into())
}
fn on_update(_: &T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> {
// nothing to do on update.
Ok(())
}
fn on_remove(_: &T::AccountId) -> Result<(), Self::Error> {
// nothing to do on remove.
Ok(())
}
fn unsafe_regenerate(
_: impl IntoIterator<Item = T::AccountId>,
_: Box<dyn Fn(&T::AccountId) -> Option<Self::Score>>,
) -> u32 {
// nothing to do upon regenerate.
0
}
#[cfg(feature = "try-runtime")]
fn try_state() -> Result<(), TryRuntimeError> {
Ok(())
}
fn unsafe_clear() {
#[allow(deprecated)]
Validators::<T>::remove_all();
}
#[cfg(feature = "runtime-benchmarks")]
fn score_update_worst_case(_who: &T::AccountId, _is_increase: bool) -> Self::Score {
unimplemented!()
}
}
/// A simple voter list implementation that does not require any additional pallets. Note, this
/// does not provided nominators in sorted ordered. If you desire nominators in a sorted order take
/// a look at [`pezpallet-bags-list].
pub struct UseNominatorsAndValidatorsMap<T>(core::marker::PhantomData<T>);
impl<T: Config> SortedListProvider<T::AccountId> for UseNominatorsAndValidatorsMap<T> {
type Error = ();
type Score = VoteWeight;
fn iter() -> Box<dyn Iterator<Item = T::AccountId>> {
Box::new(
Validators::<T>::iter()
.map(|(v, _)| v)
.chain(Nominators::<T>::iter().map(|(n, _)| n)),
)
}
fn iter_from(
start: &T::AccountId,
) -> Result<Box<dyn Iterator<Item = T::AccountId>>, Self::Error> {
if Validators::<T>::contains_key(start) {
let start_key = Validators::<T>::hashed_key_for(start);
Ok(Box::new(
Validators::<T>::iter_from(start_key)
.map(|(n, _)| n)
.chain(Nominators::<T>::iter().map(|(x, _)| x)),
))
} else if Nominators::<T>::contains_key(start) {
let start_key = Nominators::<T>::hashed_key_for(start);
Ok(Box::new(Nominators::<T>::iter_from(start_key).map(|(n, _)| n)))
} else {
Err(())
}
}
fn lock() {}
fn unlock() {}
fn count() -> u32 {
Nominators::<T>::count().saturating_add(Validators::<T>::count())
}
fn contains(id: &T::AccountId) -> bool {
Nominators::<T>::contains_key(id) || Validators::<T>::contains_key(id)
}
fn on_insert(_: T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> {
// nothing to do on insert.
Ok(())
}
fn get_score(id: &T::AccountId) -> Result<Self::Score, Self::Error> {
Ok(Pezpallet::<T>::weight_of(id))
}
fn on_update(_: &T::AccountId, _weight: Self::Score) -> Result<(), Self::Error> {
// nothing to do on update.
Ok(())
}
fn on_remove(_: &T::AccountId) -> Result<(), Self::Error> {
// nothing to do on remove.
Ok(())
}
fn unsafe_regenerate(
_: impl IntoIterator<Item = T::AccountId>,
_: Box<dyn Fn(&T::AccountId) -> Option<Self::Score>>,
) -> u32 {
// nothing to do upon regenerate.
0
}
#[cfg(feature = "try-runtime")]
fn try_state() -> Result<(), TryRuntimeError> {
Ok(())
}
fn unsafe_clear() {
// NOTE: Caller must ensure this doesn't lead to too many storage accesses. This is a
// condition of SortedListProvider::unsafe_clear.
#[allow(deprecated)]
Nominators::<T>::remove_all();
#[allow(deprecated)]
Validators::<T>::remove_all();
}
#[cfg(feature = "runtime-benchmarks")]
fn score_update_worst_case(_who: &T::AccountId, _is_increase: bool) -> Self::Score {
unimplemented!()
}
}
impl<T: Config> StakingInterface for Pezpallet<T> {
type AccountId = T::AccountId;
type Balance = BalanceOf<T>;
type CurrencyToVote = T::CurrencyToVote;
fn minimum_nominator_bond() -> Self::Balance {
Self::min_nominator_bond()
}
fn minimum_validator_bond() -> Self::Balance {
Self::min_validator_bond()
}
fn stash_by_ctrl(controller: &Self::AccountId) -> Result<Self::AccountId, DispatchError> {
Self::ledger(Controller(controller.clone()))
.map(|l| l.stash)
.map_err(|e| e.into())
}
fn bonding_duration() -> EraIndex {
T::BondingDuration::get()
}
fn current_era() -> EraIndex {
CurrentEra::<T>::get().unwrap_or(Zero::zero())
}
fn stake(who: &Self::AccountId) -> Result<Stake<BalanceOf<T>>, DispatchError> {
Self::ledger(Stash(who.clone()))
.map(|l| Stake { total: l.total, active: l.active })
.map_err(|e| e.into())
}
fn bond_extra(who: &Self::AccountId, extra: Self::Balance) -> DispatchResult {
Self::bond_extra(RawOrigin::Signed(who.clone()).into(), extra)
}
fn unbond(who: &Self::AccountId, value: Self::Balance) -> DispatchResult {
let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?;
Self::unbond(RawOrigin::Signed(ctrl).into(), value)
.map_err(|with_post| with_post.error)
.map(|_| ())
}
fn set_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult {
// Since virtual stakers are not allowed to compound their rewards as this pezpallet does
// not manage their locks, we do not allow reward account to be set same as stash. For
// external pallets that manage the virtual bond, they can claim rewards and re-bond them.
ensure!(
!Self::is_virtual_staker(stash) || stash != reward_acc,
Error::<T>::RewardDestinationRestricted
);
let ledger = Self::ledger(Stash(stash.clone()))?;
let _ = ledger
.set_payee(RewardDestination::Account(reward_acc.clone()))
.defensive_proof("ledger was retrieved from storage, thus its bonded; qed.")?;
Ok(())
}
fn chill(who: &Self::AccountId) -> DispatchResult {
// defensive-only: any account bonded via this interface has the stash set as the
// controller, but we have to be sure. Same comment anywhere else that we read this.
let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?;
Self::chill(RawOrigin::Signed(ctrl).into())
}
fn withdraw_unbonded(
who: Self::AccountId,
_num_slashing_spans: u32,
) -> Result<bool, DispatchError> {
let ctrl = Self::bonded(&who).ok_or(Error::<T>::NotStash)?;
Self::withdraw_unbonded(RawOrigin::Signed(ctrl.clone()).into(), 0)
.map(|_| !StakingLedger::<T>::is_bonded(StakingAccount::Controller(ctrl)))
.map_err(|with_post| with_post.error)
}
fn bond(
who: &Self::AccountId,
value: Self::Balance,
payee: &Self::AccountId,
) -> DispatchResult {
Self::bond(
RawOrigin::Signed(who.clone()).into(),
value,
RewardDestination::Account(payee.clone()),
)
}
fn nominate(who: &Self::AccountId, targets: Vec<Self::AccountId>) -> DispatchResult {
let ctrl = Self::bonded(who).ok_or(Error::<T>::NotStash)?;
let targets = targets.into_iter().map(T::Lookup::unlookup).collect::<Vec<_>>();
Self::nominate(RawOrigin::Signed(ctrl).into(), targets)
}
fn desired_validator_count() -> u32 {
ValidatorCount::<T>::get()
}
fn election_ongoing() -> bool {
<T::ElectionProvider as ElectionProvider>::status().is_ok()
}
fn force_unstake(who: Self::AccountId) -> pezsp_runtime::DispatchResult {
Self::force_unstake(RawOrigin::Root.into(), who.clone(), 0)
}
fn is_exposed_in_era(who: &Self::AccountId, era: &EraIndex) -> bool {
ErasStakersPaged::<T>::iter_prefix((era,)).any(|((validator, _), exposure_page)| {
validator == *who || exposure_page.others.iter().any(|i| i.who == *who)
})
}
fn status(
who: &Self::AccountId,
) -> Result<pezsp_staking::StakerStatus<Self::AccountId>, DispatchError> {
if !StakingLedger::<T>::is_bonded(StakingAccount::Stash(who.clone())) {
return Err(Error::<T>::NotStash.into());
}
let is_validator = Validators::<T>::contains_key(&who);
let is_nominator = Nominators::<T>::get(&who);
use pezsp_staking::StakerStatus;
match (is_validator, is_nominator.is_some()) {
(false, false) => Ok(StakerStatus::Idle),
(true, false) => Ok(StakerStatus::Validator),
(false, true) => Ok(StakerStatus::Nominator(
is_nominator.expect("is checked above; qed").targets.into_inner(),
)),
(true, true) => {
defensive!("cannot be both validators and nominator");
Err(Error::<T>::BadState.into())
},
}
}
/// Whether `who` is a virtual staker whose funds are managed by another pezpallet.
///
/// There is an assumption that, this account is keyless and managed by another pezpallet in the
/// runtime. Hence, it can never sign its own transactions.
fn is_virtual_staker(who: &T::AccountId) -> bool {
pezframe_system::Pezpallet::<T>::account_nonce(who).is_zero()
&& VirtualStakers::<T>::contains_key(who)
}
fn slash_reward_fraction() -> Perbill {
SlashRewardFraction::<T>::get()
}
pezsp_staking::runtime_benchmarks_enabled! {
fn nominations(who: &Self::AccountId) -> Option<Vec<T::AccountId>> {
Nominators::<T>::get(who).map(|n| n.targets.into_inner())
}
fn add_era_stakers(
current_era: &EraIndex,
stash: &T::AccountId,
exposures: Vec<(Self::AccountId, Self::Balance)>,
) {
let others = exposures
.iter()
.map(|(who, value)| crate::IndividualExposure { who: who.clone(), value: *value })
.collect::<Vec<_>>();
let exposure = Exposure { total: Default::default(), own: Default::default(), others };
Eras::<T>::upsert_exposure(*current_era, stash, exposure);
}
fn set_current_era(era: EraIndex) {
CurrentEra::<T>::put(era);
}
fn max_exposure_page_size() -> Page {
T::MaxExposurePageSize::get()
}
}
}
impl<T: Config> pezsp_staking::StakingUnchecked for Pezpallet<T> {
fn migrate_to_virtual_staker(who: &Self::AccountId) -> DispatchResult {
asset::kill_stake::<T>(who)?;
VirtualStakers::<T>::insert(who, ());
Ok(())
}
/// Virtually bonds `keyless_who` to `payee` with `value`.
///
/// The payee must not be the same as the `keyless_who`.
fn virtual_bond(
keyless_who: &Self::AccountId,
value: Self::Balance,
payee: &Self::AccountId,
) -> DispatchResult {
if StakingLedger::<T>::is_bonded(StakingAccount::Stash(keyless_who.clone())) {
return Err(Error::<T>::AlreadyBonded.into());
}
// check if payee not same as who.
ensure!(keyless_who != payee, Error::<T>::RewardDestinationRestricted);
// mark who as a virtual staker.
VirtualStakers::<T>::insert(keyless_who, ());
Self::deposit_event(Event::<T>::Bonded { stash: keyless_who.clone(), amount: value });
let ledger = StakingLedger::<T>::new(keyless_who.clone(), value);
ledger.bond(RewardDestination::Account(payee.clone()))?;
Ok(())
}
/// Only meant to be used in tests.
#[cfg(feature = "runtime-benchmarks")]
fn migrate_to_direct_staker(who: &Self::AccountId) {
assert!(VirtualStakers::<T>::contains_key(who));
let ledger = StakingLedger::<T>::get(Stash(who.clone())).unwrap();
let _ = asset::update_stake::<T>(who, ledger.total)
.expect("funds must be transferred to stash");
VirtualStakers::<T>::remove(who);
}
}
#[cfg(any(test, feature = "try-runtime"))]
impl<T: Config> Pezpallet<T> {
pub(crate) fn do_try_state(_now: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
// If the pezpallet is not initialized (both ActiveEra and CurrentEra are None),
// there's nothing to check, so return early.
if ActiveEra::<T>::get().is_none() && CurrentEra::<T>::get().is_none() {
return Ok(());
}
session_rotation::Rotator::<T>::do_try_state()?;
session_rotation::Eras::<T>::do_try_state()?;
use pezframe_support::traits::fungible::Inspect;
if T::CurrencyToVote::will_downscale(T::Currency::total_issuance()).map_or(false, |x| x) {
log!(warn, "total issuance will cause T::CurrencyToVote to downscale -- report to maintainers.")
}
Self::check_ledgers()?;
Self::check_bonded_consistency()?;
Self::check_payees()?;
Self::check_paged_exposures()?;
Self::check_count()?;
Self::check_slash_health()?;
Ok(())
}
/// Invariants:
/// * A controller should not be associated with more than one ledger.
/// * A bonded (stash, controller) pair should have only one associated ledger. I.e. if the
/// ledger is bonded by stash, the controller account must not bond a different ledger.
/// * A bonded (stash, controller) pair must have an associated ledger.
///
/// NOTE: these checks result in warnings only. Once
/// <https://github.com/pezkuwichain/pezkuwi-sdk/issues/273> is resolved, turn warns into check
/// failures.
fn check_bonded_consistency() -> Result<(), TryRuntimeError> {
use alloc::collections::btree_set::BTreeSet;
let mut count_controller_double = 0;
let mut count_double = 0;
let mut count_none = 0;
// sanity check to ensure that each controller in Bonded storage is associated with only one
// ledger.
let mut controllers = BTreeSet::new();
for (stash, controller) in <Bonded<T>>::iter() {
if !controllers.insert(controller.clone()) {
count_controller_double += 1;
}
match (<Ledger<T>>::get(&stash), <Ledger<T>>::get(&controller)) {
(Some(_), Some(_)) =>
// if stash == controller, it means that the ledger has migrated to
// post-controller. If no migration happened, we expect that the (stash,
// controller) pair has only one associated ledger.
{
if stash != controller {
count_double += 1;
}
},
(None, None) => {
count_none += 1;
},
_ => {},
};
}
if count_controller_double != 0 {
log!(
warn,
"a controller is associated with more than one ledger ({} occurrences)",
count_controller_double
);
};
if count_double != 0 {
log!(warn, "single tuple of (stash, controller) pair bonds more than one ledger ({} occurrences)", count_double);
}
if count_none != 0 {
log!(warn, "inconsistent bonded state: (stash, controller) pair missing associated ledger ({} occurrences)", count_none);
}
Ok(())
}
/// Invariants:
/// * A bonded ledger should always have an assigned `Payee`.
/// * The number of entries in `Payee` and of bonded staking ledgers *must* match.
/// * The stash account in the ledger must match that of the bonded account.
fn check_payees() -> Result<(), TryRuntimeError> {
for (stash, _) in Bonded::<T>::iter() {
ensure!(Payee::<T>::get(&stash).is_some(), "bonded ledger does not have payee set");
}
ensure!(
(Ledger::<T>::iter().count() == Payee::<T>::iter().count())
&& (Ledger::<T>::iter().count() == Bonded::<T>::iter().count()),
"number of entries in payee storage items does not match the number of bonded ledgers",
);
Ok(())
}
/// Invariants:
/// * Number of voters in `VoterList` match that of the number of Nominators and Validators in
/// the system (validator is both voter and target).
/// * Number of targets in `TargetList` matches the number of validators in the system.
/// * Current validator count is bounded by the election provider's max winners.
fn check_count() -> Result<(), TryRuntimeError> {
ensure!(
<T as Config>::VoterList::count()
== Nominators::<T>::count() + Validators::<T>::count(),
"wrong external count"
);
ensure!(
<T as Config>::TargetList::count() == Validators::<T>::count(),
"wrong external count"
);
let max_validators_bound = crate::MaxWinnersOf::<T>::get();
let max_winners_per_page_bound = crate::MaxWinnersPerPageOf::<T::ElectionProvider>::get();
ensure!(
max_validators_bound >= max_winners_per_page_bound,
"max validators should be higher than per page bounds"
);
ensure!(ValidatorCount::<T>::get() <= max_validators_bound, Error::<T>::TooManyValidators);
Ok(())
}
/// Invariants:
/// * Stake consistency: ledger.total == ledger.active + sum(ledger.unlocking).
/// * The ledger's controller and stash matches the associated `Bonded` tuple.
/// * Staking locked funds for every bonded stash (non virtual stakers) should be the same as
/// its ledger's total.
/// * For virtual stakers, locked funds should be zero and payee should be non-stash account.
/// * Staking ledger and bond are not corrupted.
fn check_ledgers() -> Result<(), TryRuntimeError> {
Bonded::<T>::iter()
.map(|(stash, ctrl)| {
// ensure locks consistency.
if VirtualStakers::<T>::contains_key(stash.clone()) {
ensure!(
asset::staked::<T>(&stash) == Zero::zero(),
"virtual stakers should not have any staked balance"
);
ensure!(
<Bonded<T>>::get(stash.clone()).unwrap() == stash.clone(),
"stash and controller should be same"
);
ensure!(
Ledger::<T>::get(stash.clone()).unwrap().stash == stash,
"ledger corrupted for virtual staker"
);
ensure!(
pezframe_system::Pezpallet::<T>::account_nonce(&stash).is_zero(),
"virtual stakers are keyless and should not have any nonce"
);
let reward_destination = <Payee<T>>::get(stash.clone()).unwrap();
if let RewardDestination::Account(payee) = reward_destination {
ensure!(
payee != stash.clone(),
"reward destination should not be same as stash for virtual staker"
);
} else {
return Err(DispatchError::Other(
"reward destination must be of account variant for virtual staker",
));
}
} else {
let integrity = Self::inspect_bond_state(&stash);
if integrity != Ok(LedgerIntegrityState::Ok) {
// NOTE: not using defensive! since we test these cases and it panics them
log!(
error,
"defensive: bonded stash {:?} has inconsistent ledger state: {:?}",
stash,
integrity
);
}
}
Self::ensure_ledger_consistent(&ctrl)?;
Self::ensure_ledger_role_and_min_bond(&ctrl)?;
Ok(())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
/// Invariants:
/// Nothing to do if ActiveEra is not set.
/// For each page in `ErasStakersPaged`, `page_total` must be set.
/// For each metadata:
/// * page_count is correct
/// * nominator_count is correct
/// * total is own + sum of pages
/// `ErasTotalStake`` must be correct
fn check_paged_exposures() -> Result<(), TryRuntimeError> {
let Some(era) = ActiveEra::<T>::get().map(|a| a.index) else { return Ok(()) };
let overview_and_pages = ErasStakersOverview::<T>::iter_prefix(era)
.map(|(validator, metadata)| {
let pages = ErasStakersPaged::<T>::iter_prefix((era, validator))
.map(|(_idx, page)| page)
.collect::<Vec<_>>();
(metadata, pages)
})
.collect::<Vec<_>>();
ensure!(
overview_and_pages.iter().flat_map(|(_m, pages)| pages).all(|page| {
let expected = page
.others
.iter()
.map(|e| e.value)
.fold(BalanceOf::<T>::zero(), |acc, x| acc + x);
page.page_total == expected
}),
"found wrong page_total"
);
ensure!(
overview_and_pages.iter().all(|(metadata, pages)| {
let page_count_good = metadata.page_count == pages.len() as u32;
let nominator_count_good = metadata.nominator_count
== pages.iter().map(|p| p.others.len() as u32).fold(0u32, |acc, x| acc + x);
let total_good = metadata.total
== metadata.own
+ pages
.iter()
.fold(BalanceOf::<T>::zero(), |acc, page| acc + page.page_total);
page_count_good && nominator_count_good && total_good
}),
"found bad metadata"
);
ensure!(
overview_and_pages
.iter()
.map(|(metadata, _pages)| metadata.total)
.fold(BalanceOf::<T>::zero(), |acc, x| acc + x)
== ErasTotalStake::<T>::get(era),
"found bad eras total stake"
);
Ok(())
}
/// Ensures offence pipeline and slashing is in a healthy state.
fn check_slash_health() -> Result<(), TryRuntimeError> {
// (1) Ensure offence queue is sorted
let offence_queue_eras = OffenceQueueEras::<T>::get().unwrap_or_default().into_inner();
let mut sorted_offence_queue_eras = offence_queue_eras.clone();
sorted_offence_queue_eras.sort();
ensure!(
sorted_offence_queue_eras == offence_queue_eras,
"Offence queue eras are not sorted"
);
drop(sorted_offence_queue_eras);
// (2) Ensure oldest offence queue era is old enough.
let active_era = Rotator::<T>::active_era();
let oldest_unprocessed_offence_era =
offence_queue_eras.first().cloned().unwrap_or(active_era);
// how old is the oldest unprocessed offence era?
// given bonding duration = 28, the ideal value is between 0 and 2 eras.
// anything close to bonding duration is terrible.
let oldest_unprocessed_offence_age =
active_era.saturating_sub(oldest_unprocessed_offence_era);
// warn if less than 26 eras old.
if oldest_unprocessed_offence_age > 2.min(T::BondingDuration::get()) {
log!(
warn,
"Offence queue has unprocessed offences from older than 2 eras: oldest offence era in queue {:?} (active era: {:?})",
oldest_unprocessed_offence_era,
active_era
);
}
// error if the oldest unprocessed offence era closer to bonding duration.
ensure!(
oldest_unprocessed_offence_age < T::BondingDuration::get() - 1,
"offences from era less than 3 eras old from active era not processed yet"
);
// (3) Report count of offences in the queue.
for e in offence_queue_eras {
let count = OffenceQueue::<T>::iter_prefix(e).count();
ensure!(count > 0, "Offence queue is empty for era listed in offence queue eras");
log!(info, "Offence queue for era {:?} has {:?} offences queued", e, count);
}
// (4) Ensure all slashes older than (active era - 1) are applied.
// We will look at all eras before the active era as it can take 1 era for slashes
// to be applied.
for era in (active_era.saturating_sub(T::BondingDuration::get()))..(active_era) {
// all unapplied slashes are expected to be applied until the active era. If this is not
// the case, then we need to use a permissionless call to apply all of them.
// See `Call::apply_slash` for more details.
Self::ensure_era_slashes_applied(era)?;
}
// (5) Ensure no canceled slashes exist in the past eras.
for (era, _) in CancelledSlashes::<T>::iter() {
ensure!(era >= active_era, "Found cancelled slashes for era before active era");
}
Ok(())
}
fn ensure_ledger_role_and_min_bond(ctrl: &T::AccountId) -> Result<(), TryRuntimeError> {
let ledger = Self::ledger(StakingAccount::Controller(ctrl.clone()))?;
let stash = ledger.stash;
let is_nominator = Nominators::<T>::contains_key(&stash);
let is_validator = Validators::<T>::contains_key(&stash);
match (is_nominator, is_validator) {
(false, false) => {
if ledger.active < Self::min_chilled_bond() && !ledger.active.is_zero() {
// chilled accounts allow to go to zero and fully unbond ^^^^^^^^^
log!(
warn,
"Chilled stash {:?} has less stake ({:?}) than minimum role bond ({:?})",
stash,
ledger.active,
Self::min_chilled_bond()
);
}
// is chilled
},
(true, false) => {
// Nominators must have a minimum bond.
if ledger.active < Self::min_nominator_bond() {
log!(
warn,
"Nominator {:?} has less stake ({:?}) than minimum role bond ({:?})",
stash,
ledger.active,
Self::min_nominator_bond()
);
}
},
(false, true) => {
// Validators must have a minimum bond.
if ledger.active < Self::min_validator_bond() {
log!(
warn,
"Validator {:?} has less stake ({:?}) than minimum role bond ({:?})",
stash,
ledger.active,
Self::min_validator_bond()
);
}
},
(true, true) => {
ensure!(false, "Stash cannot be both nominator and validator");
},
}
Ok(())
}
fn ensure_ledger_consistent(ctrl: &T::AccountId) -> Result<(), TryRuntimeError> {
// ensures ledger.total == ledger.active + sum(ledger.unlocking).
let ledger = Self::ledger(StakingAccount::Controller(ctrl.clone()))?;
let real_total: BalanceOf<T> =
ledger.unlocking.iter().fold(ledger.active, |a, c| a + c.value);
ensure!(real_total == ledger.total, "ledger.total corrupt");
Ok(())
}
}