pallet_staking_async/
slashing.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! A slashing implementation for NPoS systems.
19//!
20//! For the purposes of the economic model, it is easiest to think of each validator as a nominator
21//! which nominates only its own identity.
22//!
23//! The act of nomination signals intent to unify economic identity with the validator - to take
24//! part in the rewards of a job well done, and to take part in the punishment of a job done badly.
25//!
26//! There are 3 main difficulties to account for with slashing in NPoS:
27//!   - A nominator can nominate multiple validators and be slashed via any of them.
28//!   - Until slashed, stake is reused from era to era. Nominating with N coins for E eras in a row
29//!     does not mean you have N*E coins to be slashed - you've only ever had N.
30//!   - Slashable offences can be found after the fact and out of order.
31//!
32//! We only slash participants for the _maximum_ slash they receive in some time period (era),
33//! rather than the sum. This ensures a protection from overslashing.
34//!
35//! In most of the cases, thanks to validator disabling, an offender won't be able to commit more
36//! than one offence. An exception is the case when the number of offenders reaches the
37//! Byzantine threshold. In that case one or more offenders with the smallest offence will be
38//! re-enabled and they can commit another offence. But as noted previously, even in this case we
39//! slash the offender only for the biggest offence committed within an era.
40//!
41//! Based on research at <https://research.web3.foundation/Polkadot/security/slashing/npos>
42
43use crate::{
44	asset, log, session_rotation::Eras, BalanceOf, Config, ErasStakersOverview,
45	NegativeImbalanceOf, OffenceQueue, OffenceQueueEras, PagedExposure, Pallet, Perbill,
46	ProcessingOffence, SlashRewardFraction, UnappliedSlash, UnappliedSlashes, WeightInfo,
47};
48use alloc::{vec, vec::Vec};
49use codec::{Decode, Encode, MaxEncodedLen};
50use frame_support::traits::{Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced};
51use scale_info::TypeInfo;
52use sp_runtime::{
53	traits::{Saturating, Zero},
54	RuntimeDebug, WeakBoundedVec, Weight,
55};
56use sp_staking::{EraIndex, StakingInterface};
57
58/// Parameters for performing a slash.
59#[derive(Clone)]
60pub(crate) struct SlashParams<'a, T: 'a + Config> {
61	/// The stash account being slashed.
62	pub(crate) stash: &'a T::AccountId,
63	/// The proportion of the slash.
64	pub(crate) slash: Perbill,
65	/// The prior slash proportion of the validator if the validator has been reported multiple
66	/// times in the same era, and a new greater slash replaces the old one.
67	/// Invariant: slash > prior_slash
68	pub(crate) prior_slash: Perbill,
69	/// The exposure of the stash and all nominators.
70	pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
71	/// The era where the offence occurred.
72	pub(crate) slash_era: EraIndex,
73	/// The maximum percentage of a slash that ever gets paid out.
74	/// This is f_inf in the paper.
75	pub(crate) reward_proportion: Perbill,
76}
77
78/// Represents an offence record within the staking system, capturing details about a slashing
79/// event.
80#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebug)]
81pub struct OffenceRecord<AccountId> {
82	/// The account ID of the entity that reported the offence.
83	pub reporter: Option<AccountId>,
84
85	/// Era at which the offence was reported.
86	pub reported_era: EraIndex,
87
88	/// The specific page of the validator's exposure currently being processed.
89	///
90	/// Since a validator's total exposure can span multiple pages, this field serves as a pointer
91	/// to the current page being evaluated. The processing order starts from the last page
92	/// and moves backward, decrementing this value with each processed page.
93	///
94	/// This ensures that all pages are systematically handled, and it helps track when
95	/// the entire exposure has been processed.
96	pub exposure_page: u32,
97
98	/// The fraction of the validator's stake to be slashed for this offence.
99	pub slash_fraction: Perbill,
100
101	/// The previous slash fraction of the validator's stake before being updated.
102	/// If a new, higher slash fraction is reported, this field stores the prior fraction
103	/// that was overwritten. This helps in tracking changes in slashes across multiple reports for
104	/// the same era.
105	pub prior_slash_fraction: Perbill,
106}
107
108/// Loads next offence in the processing offence and returns the offense record to be processed.
109///
110/// Note: this can mutate the following storage
111/// - `ProcessingOffence`
112/// - `OffenceQueue`
113/// - `OffenceQueueEras`
114fn next_offence<T: Config>() -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
115	let maybe_processing_offence = ProcessingOffence::<T>::get();
116
117	if let Some((offence_era, offender, offence_record)) = maybe_processing_offence {
118		// If the exposure page is 0, then the offence has been processed.
119		if offence_record.exposure_page == 0 {
120			ProcessingOffence::<T>::kill();
121			return Some((offence_era, offender, offence_record))
122		}
123
124		// Update the next page.
125		ProcessingOffence::<T>::put((
126			offence_era,
127			&offender,
128			OffenceRecord {
129				// decrement the page index.
130				exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
131				..offence_record.clone()
132			},
133		));
134
135		return Some((offence_era, offender, offence_record))
136	}
137
138	// Nothing in processing offence. Try to enqueue the next offence.
139	let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
140	let Some(&oldest_era) = eras.first() else { return None };
141
142	let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
143	let next_offence = offence_iter.next();
144
145	if let Some((ref validator, ref offence_record)) = next_offence {
146		// Update the processing offence if the offence is multi-page.
147		if offence_record.exposure_page > 0 {
148			// update processing offence with the next page.
149			ProcessingOffence::<T>::put((
150				oldest_era,
151				validator.clone(),
152				OffenceRecord {
153					exposure_page: offence_record.exposure_page.defensive_saturating_sub(1),
154					..offence_record.clone()
155				},
156			));
157		}
158
159		// Remove from `OffenceQueue`
160		OffenceQueue::<T>::remove(oldest_era, &validator);
161	}
162
163	// If there are no offences left for the era, remove the era from `OffenceQueueEras`.
164	if offence_iter.next().is_none() {
165		if eras.len() == 1 {
166			// If there is only one era left, remove the entire queue.
167			OffenceQueueEras::<T>::kill();
168		} else {
169			// Remove the oldest era
170			eras.remove(0);
171			OffenceQueueEras::<T>::put(eras);
172		}
173	}
174
175	next_offence.map(|(v, o)| (oldest_era, v, o))
176}
177
178/// Infallible function to process an offence.
179pub(crate) fn process_offence<T: Config>() -> Weight {
180	// We do manual weight tracking for early-returns, and use benchmarks for the final two
181	// branches.
182	let mut incomplete_consumed_weight = Weight::from_parts(0, 0);
183	let mut add_db_reads_writes = |reads, writes| {
184		incomplete_consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
185	};
186
187	add_db_reads_writes(3, 4);
188	let Some((offence_era, offender, offence_record)) = next_offence::<T>() else {
189		return incomplete_consumed_weight
190	};
191
192	log!(
193		debug,
194		"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
195		offender,
196		offence_era,
197		offence_record.slash_fraction,
198	);
199
200	add_db_reads_writes(1, 0);
201	let reward_proportion = SlashRewardFraction::<T>::get();
202
203	add_db_reads_writes(2, 0);
204	let Some(exposure) =
205		Eras::<T>::get_paged_exposure(offence_era, &offender, offence_record.exposure_page)
206	else {
207		// this can only happen if the offence was valid at the time of reporting but became too old
208		// at the time of computing and should be discarded.
209		return incomplete_consumed_weight
210	};
211
212	let slash_page = offence_record.exposure_page;
213	let slash_defer_duration = T::SlashDeferDuration::get();
214	let slash_era = offence_era.saturating_add(slash_defer_duration);
215
216	add_db_reads_writes(3, 3);
217	let Some(mut unapplied) = compute_slash::<T>(SlashParams {
218		stash: &offender,
219		slash: offence_record.slash_fraction,
220		prior_slash: offence_record.prior_slash_fraction,
221		exposure: &exposure,
222		slash_era: offence_era,
223		reward_proportion,
224	}) else {
225		log!(
226			debug,
227			"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
228			offence_record.slash_fraction,
229			offence_era,
230			offence_record.reported_era,
231		);
232		// No slash to apply. Discard.
233		return incomplete_consumed_weight
234	};
235
236	<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
237		offence_era,
238		slash_era,
239		offender: offender.clone(),
240		page: slash_page,
241	});
242
243	log!(
244		debug,
245		"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
246		offence_record.slash_fraction,
247		offence_era,
248		offence_record.reported_era,
249	);
250
251	// add the reporter to the unapplied slash.
252	unapplied.reporter = offence_record.reporter;
253
254	if slash_defer_duration == 0 {
255		// Apply right away.
256		log!(
257			debug,
258			"🦹 applying slash instantly of {:?} happened in {:?} (reported in {:?}) to {:?}",
259			offence_record.slash_fraction,
260			offence_era,
261			offence_record.reported_era,
262			offender,
263		);
264
265		let nominators_slashed = unapplied.others.len() as u32;
266		apply_slash::<T>(unapplied, offence_era);
267		T::WeightInfo::apply_slash(nominators_slashed)
268			.saturating_add(T::WeightInfo::process_offence_queue())
269	} else {
270		// Historical Note: Previously, with BondingDuration = 28 and SlashDeferDuration = 27,
271		// slashes were applied at the start of the 28th era from `offence_era`.
272		// However, with paged slashing, applying slashes now takes multiple blocks.
273		// To account for this delay, slashes are now applied at the start of the 27th era from
274		// `offence_era`.
275		log!(
276			debug,
277			"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
278			offence_record.slash_fraction,
279			offence_era,
280			offence_record.reported_era,
281			slash_era,
282		);
283		UnappliedSlashes::<T>::insert(
284			slash_era,
285			(offender, offence_record.slash_fraction, slash_page),
286			unapplied,
287		);
288		T::WeightInfo::process_offence_queue()
289	}
290}
291
292/// Loads next offence in the processing offence and returns the offense record to be processed.
293///
294/// Note: this can mutate the following storage
295/// - `ProcessingOffence`
296/// - `OffenceQueue`
297/// - `OffenceQueueEras`
298///
299/// This is called when only validators are slashed.
300/// No nominators exposed to the offending validator are slashed.
301fn next_offence_validator_only<T: Config>(
302) -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
303	// Try enqueue the next offence
304	let Some(mut eras) = OffenceQueueEras::<T>::get() else { return None };
305	let Some(&oldest_era) = eras.first() else { return None };
306
307	let mut offence_iter = OffenceQueue::<T>::iter_prefix(oldest_era);
308	let next_offence = offence_iter.next();
309
310	if let Some((ref validator, ref _offence_record)) = next_offence {
311		// Remove from `OffenceQueue`
312		OffenceQueue::<T>::remove(oldest_era, &validator);
313	}
314
315	// If there are no offences left for the era, remove the era from `OffenceQueueEras`.
316	if offence_iter.next().is_none() {
317		if eras.len() == 1 {
318			// If there is only one era left, remove the entire queue.
319			OffenceQueueEras::<T>::kill();
320		} else {
321			// Remove the oldest era
322			eras.remove(0);
323			OffenceQueueEras::<T>::put(eras);
324		}
325	}
326
327	next_offence.map(|(v, o)| (oldest_era, v, o))
328}
329
330pub(crate) fn process_offence_validator_only<T: Config>() -> Weight {
331	// We do manual weight tracking for early-returns, and use benchmarks for the final two
332	// branches.
333	let mut incomplete_consumed_weight = Weight::from_parts(0, 0);
334	let mut add_db_reads_writes = |reads, writes| {
335		incomplete_consumed_weight += T::DbWeight::get().reads_writes(reads, writes);
336	};
337
338	add_db_reads_writes(2, 2);
339	let Some((offence_era, offender, offence_record)) = next_offence_validator_only::<T>() else {
340		return incomplete_consumed_weight
341	};
342
343	log!(
344		debug,
345		"🦹 Processing offence for {:?} in era {:?} with slash fraction {:?}",
346		offender,
347		offence_era,
348		offence_record.slash_fraction,
349	);
350
351	add_db_reads_writes(1, 0);
352	let reward_proportion = SlashRewardFraction::<T>::get();
353
354	add_db_reads_writes(2, 0);
355	let Some(validator_exposure) = <ErasStakersOverview<T>>::get(&offence_era, &offender) else {
356		// this can only happen if the offence was valid at the time of reporting but became too old
357		// at the time of computing and should be discarded.
358		return incomplete_consumed_weight
359	};
360
361	let slash_defer_duration = T::SlashDeferDuration::get();
362	let slash_era = offence_era.saturating_add(slash_defer_duration);
363
364	add_db_reads_writes(3, 3);
365	// For validator-only slashing, call slash_validator directly instead of compute_slash
366	// to avoid unnecessarily calling slash_nominators (which would be a no-op anyway).
367	let params = SlashParams {
368		stash: &offender,
369		slash: offence_record.slash_fraction,
370		prior_slash: offence_record.prior_slash_fraction,
371		// create exposure only from validator state from the overview
372		exposure: &PagedExposure::from_overview(validator_exposure),
373		slash_era: offence_era,
374		reward_proportion,
375	};
376
377	let (val_slashed, reward_payout) = slash_validator::<T>(params);
378
379	// Build UnappliedSlash for validator-only slashing
380	let Some(mut unapplied) = (val_slashed > Zero::zero()).then_some(UnappliedSlash {
381		validator: offender.clone(),
382		own: val_slashed,
383		others: WeakBoundedVec::force_from(vec![], None),
384		reporter: None,
385		payout: reward_payout,
386	}) else {
387		log!(
388			debug,
389			"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is discarded, as could not compute slash",
390			offence_record.slash_fraction,
391			offence_era,
392			offence_record.reported_era,
393		);
394		// No slash to apply. Discard.
395		return incomplete_consumed_weight
396	};
397
398	<Pallet<T>>::deposit_event(super::Event::<T>::SlashComputed {
399		offence_era,
400		slash_era,
401		offender: offender.clone(),
402		// validator only slashes are always single paged
403		page: 0,
404	});
405
406	log!(
407		debug,
408		"🦹 Slash of {:?}% happened in {:?} (reported in {:?}) is computed",
409		offence_record.slash_fraction,
410		offence_era,
411		offence_record.reported_era,
412	);
413
414	// add the reporter to the unapplied slash.
415	unapplied.reporter = offence_record.reporter;
416
417	if slash_defer_duration == 0 {
418		// Apply right away.
419		log!(
420			debug,
421			"🦹 applying slash instantly of {:?} happened in {:?} (reported in {:?}) to {:?}",
422			offence_record.slash_fraction,
423			offence_era,
424			offence_record.reported_era,
425			offender,
426		);
427
428		// Validator-only slash, so no nominators (n=0).
429		apply_slash::<T>(unapplied, offence_era);
430		T::WeightInfo::apply_slash(0).saturating_add(T::WeightInfo::process_offence_queue())
431	} else {
432		// Historical Note: Previously, with BondingDuration = 28 and SlashDeferDuration = 27,
433		// slashes were applied at the start of the 28th era from `offence_era`.
434		// However, with paged slashing, applying slashes now takes multiple blocks.
435		// To account for this delay, slashes are now applied at the start of the 27th era from
436		// `offence_era`.
437		log!(
438			debug,
439			"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
440			offence_record.slash_fraction,
441			offence_era,
442			offence_record.reported_era,
443			slash_era,
444		);
445		UnappliedSlashes::<T>::insert(
446			slash_era,
447			(offender, offence_record.slash_fraction, 0),
448			unapplied,
449		);
450		T::WeightInfo::process_offence_queue()
451	}
452}
453
454/// Process the next offence in the queue, checking the era-specific nominators slashable setting.
455///
456/// This function decides whether to slash nominators based on the [`ErasNominatorsSlashable`]
457/// value for the offence era, ensuring that the rules at the time of the offence are applied.
458///
459/// If there's an in-progress multi-page offence (in `ProcessingOffence`), it continues processing
460/// that offence with full exposure (nominators were already decided to be slashable).
461pub(crate) fn process_offence_for_era<T: Config>() -> Weight {
462	// Check if there's an in-progress multi-page offence
463	if ProcessingOffence::<T>::exists() {
464		// Continue processing the existing multi-page offence with full exposure
465		return process_offence::<T>();
466	}
467
468	// No in-progress offence - peek at the queue to determine the offence era
469	let Some(eras) = OffenceQueueEras::<T>::get() else {
470		return T::DbWeight::get().reads(2); // ProcessingOffence + OffenceQueueEras
471	};
472	let Some(&oldest_era) = eras.first() else { return T::DbWeight::get().reads(2) };
473
474	// Check the era-specific nominators slashable setting
475	// Additional read for ErasNominatorsSlashable
476	if Eras::<T>::are_nominators_slashable(oldest_era) {
477		process_offence::<T>()
478	} else {
479		process_offence_validator_only::<T>()
480	}
481}
482
483/// Computes a slash of a validator and nominators. It returns an unapplied
484/// record to be applied at some later point. Slashing metadata is updated in storage,
485/// since unapplied records are only rarely intended to be dropped.
486///
487/// The pending slash record returned does not have initialized reporters. Those have
488/// to be set at a higher level, if any.
489///
490/// If `nomintors_only` is set to `true`, only the nominator slashes will be computed.
491pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> {
492	let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone());
493
494	let mut nominators_slashed = Vec::new();
495	let (nom_slashed, nom_reward_payout) =
496		slash_nominators::<T>(params.clone(), &mut nominators_slashed);
497	reward_payout += nom_reward_payout;
498
499	// If nominators are not slashable for this era, the list must be empty
500	// (because we use `from_overview` which creates empty `others`).
501	debug_assert!(Eras::<T>::are_nominators_slashable(params.slash_era));
502
503	(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
504		validator: params.stash.clone(),
505		own: val_slashed,
506		others: WeakBoundedVec::force_from(
507			nominators_slashed,
508			Some("slashed nominators not expected to be larger than the bounds"),
509		),
510		reporter: None,
511		payout: reward_payout,
512	})
513}
514
515/// Compute the slash for a validator. Returns the amount slashed and the reward payout.
516fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceOf<T>) {
517	let own_stake = params.exposure.exposure_metadata.own;
518	let prior_slashed = params.prior_slash * own_stake;
519	let new_total_slash = params.slash * own_stake;
520
521	let slash_due = new_total_slash.saturating_sub(prior_slashed);
522	// Audit Note: Previously, each repeated slash reduced the reward by 50% (e.g., 50% × 50% for
523	// two offences). Since repeat offences in the same era are discarded unless the new slash is
524	// higher, this reduction logic was unnecessary and removed.
525	let reward_due = params.reward_proportion * slash_due;
526	log!(
527		warn,
528		"🦹 slashing validator {:?} of stake: {:?} for {:?} in era {:?}",
529		params.stash,
530		own_stake,
531		slash_due,
532		params.slash_era,
533	);
534
535	(slash_due, reward_due)
536}
537
538/// Slash nominators. Accepts general parameters and the prior slash percentage of the validator.
539///
540/// Returns the total amount slashed and amount of reward to pay out.
541fn slash_nominators<T: Config>(
542	params: SlashParams<T>,
543	nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
544) -> (BalanceOf<T>, BalanceOf<T>) {
545	let mut reward_payout = BalanceOf::<T>::zero();
546	let mut total_slashed = BalanceOf::<T>::zero();
547
548	nominators_slashed.reserve(params.exposure.exposure_page.others.len());
549	for nominator in &params.exposure.exposure_page.others {
550		let stash = &nominator.who;
551		let prior_slashed = params.prior_slash * nominator.value;
552		let new_slash = params.slash * nominator.value;
553		// this should always be positive since prior slash is always less than the new slash or
554		// filtered out when offence is reported (`Pallet::on_new_offences`).
555		let slash_diff = new_slash.defensive_saturating_sub(prior_slashed);
556
557		if slash_diff == Zero::zero() {
558			// nothing to do
559			continue
560		}
561
562		log!(
563			debug,
564			"🦹 slashing nominator {:?} of stake: {:?} for {:?} in era {:?}. Prior Slash: {:?}, New Slash: {:?}",
565			stash,
566			nominator.value,
567			slash_diff,
568			params.slash_era,
569			params.prior_slash,
570			params.slash,
571		);
572
573		nominators_slashed.push((stash.clone(), slash_diff));
574		total_slashed.saturating_accrue(slash_diff);
575		reward_payout.saturating_accrue(params.reward_proportion * slash_diff);
576	}
577
578	(total_slashed, reward_payout)
579}
580
581// apply the slash to a stash account, deducting any missing funds from the reward
582// payout, saturating at 0. this is mildly unfair but also an edge-case that
583// can only occur when overlapping locked funds have been slashed.
584pub fn do_slash<T: Config>(
585	stash: &T::AccountId,
586	value: BalanceOf<T>,
587	reward_payout: &mut BalanceOf<T>,
588	slashed_imbalance: &mut NegativeImbalanceOf<T>,
589	slash_era: EraIndex,
590) {
591	let mut ledger =
592		match Pallet::<T>::ledger(sp_staking::StakingAccount::Stash(stash.clone())).defensive() {
593			Ok(ledger) => ledger,
594			Err(_) => return, // nothing to do.
595		};
596
597	let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
598	if value.is_zero() {
599		// nothing to do
600		return
601	}
602
603	// Skip slashing for virtual stakers. The pallets managing them should handle the slashing.
604	if !Pallet::<T>::is_virtual_staker(stash) {
605		let (imbalance, missing) = asset::slash::<T>(stash, value);
606		slashed_imbalance.subsume(imbalance);
607
608		if !missing.is_zero() {
609			// deduct overslash from the reward payout
610			*reward_payout = reward_payout.saturating_sub(missing);
611		}
612	}
613
614	let _ = ledger
615		.update()
616		.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
617
618	// trigger the event
619	<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
620}
621
622/// Apply a previously-unapplied slash.
623pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
624	let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
625	let mut reward_payout = unapplied_slash.payout;
626
627	if unapplied_slash.own > Zero::zero() {
628		do_slash::<T>(
629			&unapplied_slash.validator,
630			unapplied_slash.own,
631			&mut reward_payout,
632			&mut slashed_imbalance,
633			slash_era,
634		);
635	}
636
637	for &(ref nominator, nominator_slash) in &unapplied_slash.others {
638		if nominator_slash.is_zero() {
639			continue
640		}
641
642		do_slash::<T>(
643			nominator,
644			nominator_slash,
645			&mut reward_payout,
646			&mut slashed_imbalance,
647			slash_era,
648		);
649	}
650
651	pay_reporters::<T>(
652		reward_payout,
653		slashed_imbalance,
654		&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
655	);
656}
657
658/// Apply a reward payout to some reporters, paying the rewards out of the slashed imbalance.
659fn pay_reporters<T: Config>(
660	reward_payout: BalanceOf<T>,
661	slashed_imbalance: NegativeImbalanceOf<T>,
662	reporters: &[T::AccountId],
663) {
664	if reward_payout.is_zero() || reporters.is_empty() {
665		// nobody to pay out to or nothing to pay;
666		// just treat the whole value as slashed.
667		T::Slash::on_unbalanced(slashed_imbalance);
668		return
669	}
670
671	// take rewards out of the slashed imbalance.
672	let reward_payout = reward_payout.min(slashed_imbalance.peek());
673	let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
674
675	let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
676	for reporter in reporters {
677		let (reporter_reward, rest) = reward_payout.split(per_reporter);
678		reward_payout = rest;
679
680		// this cancels out the reporter reward imbalance internally, leading
681		// to no change in total issuance.
682		asset::deposit_slashed::<T>(reporter, reporter_reward);
683	}
684
685	// the rest goes to the on-slash imbalance handler (e.g. treasury)
686	value_slashed.subsume(reward_payout); // remainder of reward division remains.
687	T::Slash::on_unbalanced(value_slashed);
688}