1use crate::{
44	asset, log, session_rotation::Eras, BalanceOf, Config, NegativeImbalanceOf, OffenceQueue,
45	OffenceQueueEras, PagedExposure, Pallet, Perbill, ProcessingOffence, SlashRewardFraction,
46	UnappliedSlash, UnappliedSlashes, ValidatorSlashInEra, WeightInfo,
47};
48use alloc::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(1, 1);
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		apply_slash::<T>(unapplied, offence_era);
266		T::WeightInfo::apply_slash().saturating_add(T::WeightInfo::process_offence_queue())
267	} else {
268		log!(
274			debug,
275			"🦹 deferring slash of {:?}% happened in {:?} (reported in {:?}) to {:?}",
276			offence_record.slash_fraction,
277			offence_era,
278			offence_record.reported_era,
279			slash_era,
280		);
281		UnappliedSlashes::<T>::insert(
282			slash_era,
283			(offender, offence_record.slash_fraction, slash_page),
284			unapplied,
285		);
286		T::WeightInfo::process_offence_queue()
287	}
288}
289
290pub(crate) fn compute_slash<T: Config>(params: SlashParams<T>) -> Option<UnappliedSlash<T>> {
299	let (val_slashed, mut reward_payout) = slash_validator::<T>(params.clone());
300
301	let mut nominators_slashed = Vec::new();
302	let (nom_slashed, nom_reward_payout) =
303		slash_nominators::<T>(params.clone(), &mut nominators_slashed);
304	reward_payout += nom_reward_payout;
305
306	(nom_slashed + val_slashed > Zero::zero()).then_some(UnappliedSlash {
307		validator: params.stash.clone(),
308		own: val_slashed,
309		others: WeakBoundedVec::force_from(
310			nominators_slashed,
311			Some("slashed nominators not expected to be larger than the bounds"),
312		),
313		reporter: None,
314		payout: reward_payout,
315	})
316}
317
318fn slash_validator<T: Config>(params: SlashParams<T>) -> (BalanceOf<T>, BalanceOf<T>) {
320	let own_stake = params.exposure.exposure_metadata.own;
321	let prior_slashed = params.prior_slash * own_stake;
322	let new_total_slash = params.slash * own_stake;
323
324	let slash_due = new_total_slash.saturating_sub(prior_slashed);
325	let reward_due = params.reward_proportion * slash_due;
329	log!(
330		warn,
331		"🦹 slashing validator {:?} of stake: {:?} for {:?} in era {:?}",
332		params.stash,
333		own_stake,
334		slash_due,
335		params.slash_era,
336	);
337
338	(slash_due, reward_due)
339}
340
341fn slash_nominators<T: Config>(
345	params: SlashParams<T>,
346	nominators_slashed: &mut Vec<(T::AccountId, BalanceOf<T>)>,
347) -> (BalanceOf<T>, BalanceOf<T>) {
348	let mut reward_payout = BalanceOf::<T>::zero();
349	let mut total_slashed = BalanceOf::<T>::zero();
350
351	nominators_slashed.reserve(params.exposure.exposure_page.others.len());
352	for nominator in ¶ms.exposure.exposure_page.others {
353		let stash = &nominator.who;
354		let prior_slashed = params.prior_slash * nominator.value;
355		let new_slash = params.slash * nominator.value;
356		let slash_diff = new_slash.defensive_saturating_sub(prior_slashed);
359
360		if slash_diff == Zero::zero() {
361			continue
363		}
364
365		log!(
366			debug,
367			"🦹 slashing nominator {:?} of stake: {:?} for {:?} in era {:?}. Prior Slash: {:?}, New Slash: {:?}",
368			stash,
369			nominator.value,
370			slash_diff,
371			params.slash_era,
372			params.prior_slash,
373			params.slash,
374		);
375
376		nominators_slashed.push((stash.clone(), slash_diff));
377		total_slashed.saturating_accrue(slash_diff);
378		reward_payout.saturating_accrue(params.reward_proportion * slash_diff);
379	}
380
381	(total_slashed, reward_payout)
382}
383
384pub(crate) fn clear_era_metadata<T: Config>(obsolete_era: EraIndex) {
386	#[allow(deprecated)]
387	ValidatorSlashInEra::<T>::remove_prefix(&obsolete_era, None);
388}
389
390pub fn do_slash<T: Config>(
394	stash: &T::AccountId,
395	value: BalanceOf<T>,
396	reward_payout: &mut BalanceOf<T>,
397	slashed_imbalance: &mut NegativeImbalanceOf<T>,
398	slash_era: EraIndex,
399) {
400	let mut ledger =
401		match Pallet::<T>::ledger(sp_staking::StakingAccount::Stash(stash.clone())).defensive() {
402			Ok(ledger) => ledger,
403			Err(_) => return, };
405
406	let value = ledger.slash(value, asset::existential_deposit::<T>(), slash_era);
407	if value.is_zero() {
408		return
410	}
411
412	if !Pallet::<T>::is_virtual_staker(stash) {
414		let (imbalance, missing) = asset::slash::<T>(stash, value);
415		slashed_imbalance.subsume(imbalance);
416
417		if !missing.is_zero() {
418			*reward_payout = reward_payout.saturating_sub(missing);
420		}
421	}
422
423	let _ = ledger
424		.update()
425		.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
426
427	<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
429}
430
431pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T>, slash_era: EraIndex) {
433	let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
434	let mut reward_payout = unapplied_slash.payout;
435
436	if unapplied_slash.own > Zero::zero() {
437		do_slash::<T>(
438			&unapplied_slash.validator,
439			unapplied_slash.own,
440			&mut reward_payout,
441			&mut slashed_imbalance,
442			slash_era,
443		);
444	}
445
446	for &(ref nominator, nominator_slash) in &unapplied_slash.others {
447		if nominator_slash.is_zero() {
448			continue
449		}
450
451		do_slash::<T>(
452			nominator,
453			nominator_slash,
454			&mut reward_payout,
455			&mut slashed_imbalance,
456			slash_era,
457		);
458	}
459
460	pay_reporters::<T>(
461		reward_payout,
462		slashed_imbalance,
463		&unapplied_slash.reporter.map(|v| crate::vec![v]).unwrap_or_default(),
464	);
465}
466
467fn pay_reporters<T: Config>(
469	reward_payout: BalanceOf<T>,
470	slashed_imbalance: NegativeImbalanceOf<T>,
471	reporters: &[T::AccountId],
472) {
473	if reward_payout.is_zero() || reporters.is_empty() {
474		T::Slash::on_unbalanced(slashed_imbalance);
477		return
478	}
479
480	let reward_payout = reward_payout.min(slashed_imbalance.peek());
482	let (mut reward_payout, mut value_slashed) = slashed_imbalance.split(reward_payout);
483
484	let per_reporter = reward_payout.peek() / (reporters.len() as u32).into();
485	for reporter in reporters {
486		let (reporter_reward, rest) = reward_payout.split(per_reporter);
487		reward_payout = rest;
488
489		asset::deposit_slashed::<T>(reporter, reporter_reward);
492	}
493
494	value_slashed.subsume(reward_payout); T::Slash::on_unbalanced(value_slashed);
497}