use crate::{
asset, log, session_rotation::Eras, BalanceOf, Config, ErasStakersOverview,
NegativeImbalanceOf, OffenceQueue, OffenceQueueEras, PagedExposure, Pallet, Perbill,
ProcessingOffence, SlashRewardFraction, UnappliedSlash, UnappliedSlashes, WeightInfo,
};
use alloc::{vec, vec::Vec};
use codec::{Decode, Encode, MaxEncodedLen};
use frame_support::traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{Saturating, Zero},
Debug, WeakBoundedVec, Weight,
};
use sp_staking::{EraIndex, StakingInterface};
#[derive(Clone)]
pub(crate) struct SlashParams<'a, T: 'a + Config> {
pub(crate) stash: &'a T::AccountId,
pub(crate) slash: Perbill,
pub(crate) prior_slash: Perbill,
pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
pub(crate) slash_era: EraIndex,
pub(crate) reward_proportion: Perbill,
}
#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, Debug)]
pub struct OffenceRecord<AccountId> {
pub reporter: Option<AccountId>,
pub reported_era: EraIndex,
pub exposure_page: u32,
pub slash_fraction: Perbill,
pub prior_slash_fraction: Perbill,
}
fn next_offence<T: Config>() -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
let maybe_processing_offence = ProcessingOffence::<T>::get();
if let Some((offence_era, offender, offence_record)) = maybe_processing_offence {
if offence_record.exposure_page == 0 {
ProcessingOffence::<T>::kill();
return Some((offence_era, offender, offence_record));
}
ProcessingOffence::<T>::put((
offence_era,
&offender,
OffenceRecord {
exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
..offence_record.clone()
},
));
return Some((offence_era, offender, offence_record));
}
let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
let Some(&oldest_era) = eras.first() else { return None };
let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
let next_offence = offence_iter.next();
if let Some((ref validator, ref offence_record)) = next_offence {
if offence_record.exposure_page > 0 {
ProcessingOffence::<T>::put((
oldest_era,
validator.clone(),
OffenceRecord {
exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
..offence_record.clone()
},
));
}
OffenceQueue::<T>::remove(oldest_era, &validator);
}
if offence_iter.next().is_none() {
if eras.len() == 1 {
OffenceQueueEras::<T>::kill();
} else {
eras.remove(0);
OffenceQueueEras::<T>::put(eras);
}
}
next_offence.map(|(v, o)| (oldest_era, v, o))
}
pub(crate) fn process_offence<T: Config>() -> Weight {
let mut incomplete_consumed_weight = Weight::from_parts(0, 0);
let mut add_db_reads_writes = |reads, writes| {
incomplete_consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
};
add_db_reads_writes(3, 4);
let Some((offence_era, offender, offence_record)) = next_offence::<T>() else {
return incomplete_consumed_weight;
};
log!(
debug,
"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
offender,
offence_era,
offence_record.slash_fraction,
);
add_db_reads_writes(1, 0);
let reward_proportion = SlashRewardFraction::<T>::get();
add_db_reads_writes(2, 0);
let Some(exposure) =
Eras::<T>::get_paged_exposure(offence_era, &offender, offence_record.exposure_page)
else {
return incomplete_consumed_weight;
};
let slash_page = offence_record.exposure_page;
let slash_defer_duration = T::SlashDeferDuration::get();
let slash_era = offence_era.saturating_add(slash_defer_duration);
add_db_reads_writes(3, 3);
let Some(mut unapplied) = compute_slash::<T>(SlashParams {
stash: &offender,
slash: offence_record.slash_fraction,
prior_slash: offence_record.prior_slash_fraction,
exposure: &exposure,
slash_era: offence_era,
reward_proportion,
}) else {
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
return incomplete_consumed_weight;
};
<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
offence_era,
slash_era,
offender: offender.clone(),
page: slash_page,
});
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
unapplied.reporter = offence_record.reporter;
if slash_defer_duration == 0 {
log!(
debug,
"🦹 applying slash instantly of {:?} happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
offender,
);
let nominators_slashed = unapplied.others.len() as u32;
apply_slash::<T>(unapplied, offence_era);
T::WeightInfo::apply_slash(nominators_slashed)
.saturating_add(T::WeightInfo::process_offence_queue())
} else {
log!(
debug,
"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
slash_era,
);
UnappliedSlashes::<T>::insert(
slash_era,
(offender, offence_record.slash_fraction, slash_page),
unapplied,
);
T::WeightInfo::process_offence_queue()
}
}
fn next_offence_validator_only<T: Config>(
) -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
let Some(&oldest_era) = eras.first() else { return None };
let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
let next_offence = offence_iter.next();
if let Some((ref validator, ref _offence_record)) = next_offence {
OffenceQueue::<T>::remove(oldest_era, &validator);
}
if offence_iter.next().is_none() {
if eras.len() == 1 {
OffenceQueueEras::<T>::kill();
} else {
eras.remove(0);
OffenceQueueEras::<T>::put(eras);
}
}
next_offence.map(|(v, o)| (oldest_era, v, o))
}
pub(crate) fn process_offence_validator_only<T: Config>() -> Weight {
let mut incomplete_consumed_weight = Weight::from_parts(0, 0);
let mut add_db_reads_writes = |reads, writes| {
incomplete_consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
};
add_db_reads_writes(2, 2);
let Some((offence_era, offender, offence_record)) = next_offence_validator_only::<T>() else {
return incomplete_consumed_weight;
};
log!(
debug,
"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
offender,
offence_era,
offence_record.slash_fraction,
);
add_db_reads_writes(1, 0);
let reward_proportion = SlashRewardFraction::<T>::get();
add_db_reads_writes(2, 0);
let Some(validator_exposure) = <ErasStakersOverview<T>>::get(&offence_era, &offender) else {
return incomplete_consumed_weight;
};
let slash_defer_duration = T::SlashDeferDuration::get();
let slash_era = offence_era.saturating_add(slash_defer_duration);
add_db_reads_writes(3, 3);
let params = SlashParams {
stash: &offender,
slash: offence_record.slash_fraction,
prior_slash: offence_record.prior_slash_fraction,
exposure: &PagedExposure::from_overview(validator_exposure),
slash_era: offence_era,
reward_proportion,
};
let (val_slashed, reward_payout) = slash_validator::<T>(params);
let Some(mut unapplied) = (val_slashed > Zero::zero()).then_some(UnappliedSlash {
validator: offender.clone(),
own: val_slashed,
others: WeakBoundedVec::force_from(vec![], None),
reporter: None,
payout: reward_payout,
}) else {
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
return incomplete_consumed_weight;
};
<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
offence_era,
slash_era,
offender: offender.clone(),
page: 0,
});
log!(
debug,
"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
);
unapplied.reporter = offence_record.reporter;
if slash_defer_duration == 0 {
log!(
debug,
"🦹 applying slash instantly of {:?} happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
offender,
);
apply_slash::<T>(unapplied, offence_era);
T::WeightInfo::apply_slash(0).saturating_add(T::WeightInfo::process_offence_queue())
} else {
log!(
debug,
"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
offence_record.slash_fraction,
offence_era,
offence_record.reported_era,
slash_era,
);
UnappliedSlashes::<T>::insert(
slash_era,
(offender, offence_record.slash_fraction, 0),
unapplied,
);
T::WeightInfo::process_offence_queue()
}
}
pub(crate) fn process_offence_for_era<T: Config>() -> Weight {
if ProcessingOffence::<T>::exists() {
return process_offence::<T>();
}
let Some(eras) = OffenceQueueEras::<T>::get() else {
return T::DbWeight::get().reads(2); };
let Some(&oldest_era) = eras.first() else { return T::DbWeight::get().reads(2) };
if Eras::<T>::are_nominators_slashable(oldest_era) {
process_offence::<T>()
} else {
process_offence_validator_only::<T>()
}
}
pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> {
let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone());
let mut nominators_slashed = Vec::new();
let (nom_slashed, nom_reward_payout) =
slash_nominators::<T>(params.clone(), &mut nominators_slashed);
reward_payout += nom_reward_payout;
debug_assert!(Eras::<T>::are_nominators_slashable(params.slash_era));
(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
validator: params.stash.clone(),
own: val_slashed,
others: WeakBoundedVec::force_from(
nominators_slashed,
Some("slashed nominators not expected to be larger than the bounds"),
),
reporter: None,
payout: reward_payout,
})
}
fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceOf<T>) {
let own_stake = params.exposure.exposure_metadata.own;
let prior_slashed = params.prior_slash * own_stake;
let new_total_slash = params.slash * own_stake;
let slash_due = new_total_slash.saturating_sub(prior_slashed);
let reward_due = params.reward_proportion * slash_due;
log!(
warn,
"🦹 slashing validator {:?} of stake: {:?} for {:?} in era {:?}",
params.stash,
own_stake,
slash_due,
params.slash_era,
);
(slash_due, reward_due)
}
fn slash_nominators<T: Config>(
params: SlashParams<T>,
nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
) -> (BalanceOf<T>, BalanceOf<T>) {
let mut reward_payout = BalanceOf::<T>::zero();
let mut total_slashed = BalanceOf::<T>::zero();
nominators_slashed.reserve(params.exposure.exposure_page.others.len());
for nominator in ¶ms.exposure.exposure_page.others {
let stash = &nominator.who;
let prior_slashed = params.prior_slash * nominator.value;
let new_slash = params.slash * nominator.value;
let slash_diff = new_slash.defensive_saturating_sub(prior_slashed);
if slash_diff == Zero::zero() {
continue;
}
log!(
debug,
"🦹 slashing nominator {:?} of stake: {:?} for {:?} in era {:?}. Prior Slash: {:?}, New Slash: {:?}",
stash,
nominator.value,
slash_diff,
params.slash_era,
params.prior_slash,
params.slash,
);
nominators_slashed.push((stash.clone(), slash_diff));
total_slashed.saturating_accrue(slash_diff);
reward_payout.saturating_accrue(params.reward_proportion * slash_diff);
}
(total_slashed, reward_payout)
}
pub fn do_slash<T: Config>(
stash: &T::AccountId,
value: BalanceOf<T>,
reward_payout: &mut BalanceOf<T>,
slashed_imbalance: &mut NegativeImbalanceOf<T>,
slash_era: EraIndex,
) {
let mut ledger =
match Pallet::<T>::ledger(sp_staking::StakingAccount::Stash(stash.clone())).defensive() {
Ok(ledger) => ledger,
Err(_) => return, };
let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
if value.is_zero() {
return;
}
if !Pallet::<T>::is_virtual_staker(stash) {
let (imbalance, missing) = asset::slash::<T>(stash, value);
slashed_imbalance.subsume(imbalance);
if !missing.is_zero() {
*reward_payout = reward_payout.saturating_sub(missing);
}
}
let _ = ledger
.update()
.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
}
pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
let mut reward_payout = unapplied_slash.payout;
if unapplied_slash.own > Zero::zero() {
do_slash::<T>(
&unapplied_slash.validator,
unapplied_slash.own,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
for &(ref nominator, nominator_slash) in &unapplied_slash.others {
if nominator_slash.is_zero() {
continue;
}
do_slash::<T>(
nominator,
nominator_slash,
&mut reward_payout,
&mut slashed_imbalance,
slash_era,
);
}
pay_reporters::<T>(
reward_payout,
slashed_imbalance,
&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
);
}
fn pay_reporters<T: Config>(
reward_payout: BalanceOf<T>,
slashed_imbalance: NegativeImbalanceOf<T>,
reporters: &[T::AccountId],
) {
if reward_payout.is_zero() || reporters.is_empty() {
T::Slash::on_unbalanced(slashed_imbalance);
return;
}
let reward_payout = reward_payout.min(slashed_imbalance.peek());
let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
for reporter in reporters {
let (reporter_reward, rest) = reward_payout.split(per_reporter);
reward_payout = rest;
asset::deposit_slashed::<T>(reporter, reporter_reward);
}
value_slashed.subsume(reward_payout); T::Slash::on_unbalanced(value_slashed);
}