1use 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#[derive(Clone)]
60pub(crate) struct SlashParams<'a, T: 'a + Config> {
61 pub(crate) stash: &'a T::AccountId,
63 pub(crate) slash: Perbill,
65 pub(crate) prior_slash: Perbill,
69 pub(crate) exposure: &'a PagedExposure<T::AccountId, BalanceOf<T>>,
71 pub(crate) slash_era: EraIndex,
73 pub(crate) reward_proportion: Perbill,
76}
77
78#[derive(Clone, Encode, Decode, TypeInfo, MaxEncodedLen, PartialEq, RuntimeDebug)]
81pub struct OffenceRecord<AccountId> {
82 pub reporter: Option<AccountId>,
84
85 pub reported_era: EraIndex,
87
88 pub exposure_page: u32,
97
98 pub slash_fraction: Perbill,
100
101 pub prior_slash_fraction: Perbill,
106}
107
108fn 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 offence_record.exposure_page == 0 {
120 ProcessingOffence::<T>::kill();
121 return Some((offence_era, offender, offence_record))
122 }
123
124 ProcessingOffence::<T>::put((
126 offence_era,
127 &offender,
128 OffenceRecord {
129 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 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 if offence_record.exposure_page > 0 {
148 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 OffenceQueue::<T>::remove(oldest_era, &validator);
161 }
162
163 if offence_iter.next().is_none() {
165 if eras.len() == 1 {
166 OffenceQueueEras::<T>::kill();
168 } else {
169 eras.remove(0);
171 OffenceQueueEras::<T>::put(eras);
172 }
173 }
174
175 next_offence.map(|(v, o)| (oldest_era, v, o))
176}
177
178pub(crate) fn process_offence<T: Config>() -> Weight {
180 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 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 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 unapplied.reporter = offence_record.reporter;
253
254 if slash_defer_duration == 0 {
255 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 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
292fn next_offence_validator_only<T: Config>(
302) -> Option<(EraIndex, T::AccountId, OffenceRecord<T::AccountId>)> {
303 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 OffenceQueue::<T>::remove(oldest_era, &validator);
313 }
314
315 if offence_iter.next().is_none() {
317 if eras.len() == 1 {
318 OffenceQueueEras::<T>::kill();
320 } else {
321 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 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 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 let params = SlashParams {
368 stash: &offender,
369 slash: offence_record.slash_fraction,
370 prior_slash: offence_record.prior_slash_fraction,
371 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 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 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 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 unapplied.reporter = offence_record.reporter;
416
417 if slash_defer_duration == 0 {
418 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 apply_slash::<T>(unapplied, offence_era);
430 T::WeightInfo::apply_slash(0).saturating_add(T::WeightInfo::process_offence_queue())
431 } else {
432 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
454pub(crate) fn process_offence_for_era<T: Config>() -> Weight {
462 if ProcessingOffence::<T>::exists() {
464 return process_offence::<T>();
466 }
467
468 let Some(eras) = OffenceQueueEras::<T>::get() else {
470 return T::DbWeight::get().reads(2); };
472 let Some(&oldest_era) = eras.first() else { return T::DbWeight::get().reads(2) };
473
474 if Eras::<T>::are_nominators_slashable(oldest_era) {
477 process_offence::<T>()
478 } else {
479 process_offence_validator_only::<T>()
480 }
481}
482
483pub(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 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
515fn 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 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
538fn 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 ¶ms.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 let slash_diff = new_slash.defensive_saturating_sub(prior_slashed);
556
557 if slash_diff == Zero::zero() {
558 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
581pub 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, };
596
597 let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
598 if value.is_zero() {
599 return
601 }
602
603 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 *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 <Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
620}
621
622pub(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
658fn 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 T::Slash::on_unbalanced(slashed_imbalance);
668 return
669 }
670
671 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 asset::deposit_slashed::<T>(reporter, reporter_reward);
683 }
684
685 value_slashed.subsume(reward_payout); T::Slash::on_unbalanced(value_slashed);
688}