referrerpolicy=no-referrer-when-downgrade

pallet_staking_async/
ledger.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 Ledger implementation for stakers.
19//!
20//! A [`StakingLedger`] encapsulates all the state and logic related to the stake of bonded
21//! stakers, namely, it handles the following storage items:
22//! * [`Bonded`]: mutates and reads the state of the controller <> stash bond map (to be deprecated
23//! soon);
24//! * [`Ledger`]: mutates and reads the state of all the stakers. The [`Ledger`] storage item stores
25//!   instances of [`StakingLedger`] keyed by the staker's controller account and should be mutated
26//!   and read through the [`StakingLedger`] API;
27//! * [`Payee`]: mutates and reads the reward destination preferences for a bonded stash.
28//! * Staking locks: mutates the locks for staking.
29//!
30//! NOTE: All the storage operations related to the staking ledger (both reads and writes) *MUST* be
31//! performed through the methods exposed by the [`StakingLedger`] implementation in order to ensure
32//! state consistency.
33
34use crate::{
35	asset, log, BalanceOf, Bonded, Config, DecodeWithMemTracking, Error, Ledger, Pallet, Payee,
36	RewardDestination, Vec, VirtualStakers,
37};
38use alloc::{collections::BTreeMap, fmt::Debug};
39use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
40use frame_support::{
41	defensive, ensure,
42	traits::{Defensive, DefensiveSaturating, Get},
43	BoundedVec, CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound,
44};
45use scale_info::TypeInfo;
46use sp_runtime::{traits::Zero, DispatchResult, Perquintill, Rounding, Saturating};
47use sp_staking::{EraIndex, OnStakingUpdate, StakingAccount, StakingInterface};
48
49/// Just a Balance/BlockNumber tuple to encode when a chunk of funds will be unlocked.
50#[derive(
51	PartialEq, Eq, Clone, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo, MaxEncodedLen,
52)]
53pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> {
54	/// Amount of funds to be unlocked.
55	#[codec(compact)]
56	pub value: Balance,
57	/// Era number at which point it'll be unlocked.
58	#[codec(compact)]
59	pub era: EraIndex,
60}
61
62/// The ledger of a (bonded) stash.
63///
64/// Note: All the reads and mutations to the [`Ledger`], [`Bonded`] and [`Payee`] storage items
65/// *MUST* be performed through the methods exposed by this struct, to ensure the consistency of
66/// ledger's data and corresponding staking lock
67///
68/// TODO: move struct definition and full implementation into `/src/ledger.rs`. Currently
69/// leaving here to enforce a clean PR diff, given how critical this logic is. Tracking issue
70/// <https://github.com/paritytech/substrate/issues/14749>.
71#[derive(
72	PartialEqNoBound,
73	EqNoBound,
74	CloneNoBound,
75	Encode,
76	Decode,
77	DebugNoBound,
78	TypeInfo,
79	MaxEncodedLen,
80	DecodeWithMemTracking,
81)]
82#[scale_info(skip_type_params(T))]
83pub struct StakingLedger<T: Config> {
84	/// The stash account whose balance is actually locked and at stake.
85	pub stash: T::AccountId,
86
87	/// The total amount of the stash's balance that we are currently accounting for.
88	/// It's just `active` plus all the `unlocking` balances.
89	#[codec(compact)]
90	pub total: BalanceOf<T>,
91
92	/// The total amount of the stash's balance that will be at stake in any forthcoming
93	/// rounds.
94	#[codec(compact)]
95	pub active: BalanceOf<T>,
96
97	/// Any balance that is becoming free, which may eventually be transferred out of the stash
98	/// (assuming it doesn't get slashed first). It is assumed that this will be treated as a first
99	/// in, first out queue where the new (higher value) eras get pushed on the back.
100	pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
101
102	/// The controller associated with this ledger's stash.
103	///
104	/// This is not stored on-chain, and is only bundled when the ledger is read from storage.
105	/// Use [`Self::controller()`] function to get the controller associated with the ledger.
106	#[codec(skip)]
107	pub controller: Option<T::AccountId>,
108}
109
110impl<T: Config> StakingLedger<T> {
111	#[cfg(any(feature = "runtime-benchmarks", test))]
112	pub fn default_from(stash: T::AccountId) -> Self {
113		Self {
114			stash: stash.clone(),
115			total: Zero::zero(),
116			active: Zero::zero(),
117			unlocking: Default::default(),
118			controller: Some(stash),
119		}
120	}
121
122	/// Returns a new instance of a staking ledger.
123	///
124	/// The [`Ledger`] storage is not mutated. In order to store, `StakingLedger::update` must be
125	/// called on the returned staking ledger.
126	///
127	/// Note: as the controller accounts are being deprecated, the stash account is the same as the
128	/// controller account.
129	pub fn new(stash: T::AccountId, stake: BalanceOf<T>) -> Self {
130		Self {
131			stash: stash.clone(),
132			active: stake,
133			total: stake,
134			unlocking: Default::default(),
135			// controllers are deprecated and mapped 1-1 to stashes.
136			controller: Some(stash),
137		}
138	}
139
140	/// Returns the paired account, if any.
141	///
142	/// A "pair" refers to the tuple (stash, controller). If the input is a
143	/// [`StakingAccount::Stash`] variant, its pair account will be of type
144	/// [`StakingAccount::Controller`] and vice-versa.
145	///
146	/// This method is meant to abstract from the runtime development the difference between stash
147	/// and controller. This will be deprecated once the controller is fully deprecated as well.
148	pub(crate) fn paired_account(account: StakingAccount<T::AccountId>) -> Option<T::AccountId> {
149		match account {
150			StakingAccount::Stash(stash) => <Bonded<T>>::get(stash),
151			StakingAccount::Controller(controller) => {
152				<Ledger<T>>::get(&controller).map(|ledger| ledger.stash)
153			},
154		}
155	}
156
157	/// Returns whether a given account is bonded.
158	pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool {
159		match account {
160			StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash),
161			StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller),
162		}
163	}
164
165	/// Returns a staking ledger, if it is bonded and it exists in storage.
166	///
167	/// This getter can be called with either a controller or stash account, provided that the
168	/// account is properly wrapped in the respective [`StakingAccount`] variant. This is meant to
169	/// abstract the concept of controller/stash accounts from the caller.
170	///
171	/// Returns [`Error::BadState`] when a bond is in "bad state". A bond is in a bad state when a
172	/// stash has a controller which is bonding a ledger associated with another stash.
173	pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
174		let (stash, controller) = match account {
175			StakingAccount::Stash(stash) => {
176				(stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?)
177			},
178			StakingAccount::Controller(controller) => (
179				Ledger::<T>::get(&controller)
180					.map(|l| l.stash)
181					.ok_or(Error::<T>::NotController)?,
182				controller,
183			),
184		};
185
186		let ledger = <Ledger<T>>::get(&controller)
187			.map(|mut ledger| {
188				ledger.controller = Some(controller.clone());
189				ledger
190			})
191			.ok_or(Error::<T>::NotController)?;
192
193		// if ledger bond is in a bad state, return error to prevent applying operations that may
194		// further spoil the ledger's state. A bond is in bad state when the bonded controller is
195		// associated with a different ledger (i.e. a ledger with a different stash).
196		//
197		// See <https://github.com/paritytech/polkadot-sdk/issues/3245> for more details.
198		ensure!(
199			Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
200			Error::<T>::BadState
201		);
202
203		Ok(ledger)
204	}
205
206	/// Returns the reward destination of a staking ledger, stored in [`Payee`].
207	///
208	/// Note: if the stash is not bonded and/or does not have an entry in [`Payee`], it returns the
209	/// default reward destination.
210	pub(crate) fn reward_destination(
211		account: StakingAccount<T::AccountId>,
212	) -> Option<RewardDestination<T::AccountId>> {
213		let stash = match account {
214			StakingAccount::Stash(stash) => Some(stash),
215			StakingAccount::Controller(controller) => {
216				Self::paired_account(StakingAccount::Controller(controller))
217			},
218		};
219
220		if let Some(stash) = stash {
221			<Payee<T>>::get(stash)
222		} else {
223			defensive!("fetched reward destination from unbonded stash {}", stash);
224			None
225		}
226	}
227
228	/// Returns the controller account of a staking ledger.
229	///
230	/// Note: it will fallback into querying the [`Bonded`] storage with the ledger stash if the
231	/// controller is not set in `self`, which most likely means that self was fetched directly from
232	/// [`Ledger`] instead of through the methods exposed in [`StakingLedger`]. If the ledger does
233	/// not exist in storage, it returns `None`.
234	pub fn controller(&self) -> Option<T::AccountId> {
235		self.controller.clone().or_else(|| {
236			defensive!("fetched a controller on a ledger instance without it.");
237			Self::paired_account(StakingAccount::Stash(self.stash.clone()))
238		})
239	}
240
241	/// Inserts/updates a staking ledger account.
242	///
243	/// Bonds the ledger if it is not bonded yet, signalling that this is a new ledger. The staking
244	/// lock/hold of the stash account are updated accordingly.
245	///
246	/// Note: To ensure lock consistency, all the [`Ledger`] storage updates should be made through
247	/// this helper function.
248	pub(crate) fn update(self) -> Result<(), Error<T>> {
249		if !<Bonded<T>>::contains_key(&self.stash) {
250			return Err(Error::<T>::NotStash);
251		}
252
253		// We skip locking virtual stakers.
254		if !Pallet::<T>::is_virtual_staker(&self.stash) {
255			// for direct stakers, update lock on stash based on ledger.
256			asset::update_stake::<T>(&self.stash, self.total)
257				.map_err(|_| Error::<T>::NotEnoughFunds)?;
258		}
259
260		Ledger::<T>::insert(
261			&self.controller().ok_or_else(|| {
262				defensive!("update called on a ledger that is not bonded.");
263				Error::<T>::NotController
264			})?,
265			&self,
266		);
267
268		Ok(())
269	}
270
271	/// Bonds a ledger.
272	///
273	/// It sets the reward preferences for the bonded stash.
274	pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
275		if <Bonded<T>>::contains_key(&self.stash) {
276			return Err(Error::<T>::AlreadyBonded);
277		}
278
279		<Payee<T>>::insert(&self.stash, payee);
280		<Bonded<T>>::insert(&self.stash, &self.stash);
281		self.update()
282	}
283
284	/// Sets the ledger Payee.
285	pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
286		if !<Bonded<T>>::contains_key(&self.stash) {
287			return Err(Error::<T>::NotStash);
288		}
289
290		<Payee<T>>::insert(&self.stash, payee);
291		Ok(())
292	}
293
294	/// Sets the ledger controller to its stash.
295	pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> {
296		let controller = self.controller.as_ref()
297            .defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.")
298            .ok_or(Error::<T>::NotController)?;
299
300		ensure!(self.stash != *controller, Error::<T>::AlreadyPaired);
301
302		// check if the ledger's stash is a controller of another ledger.
303		if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
304			// there is a ledger bonded by the stash. In this case, the stash of the bonded ledger
305			// should be the same as the ledger's stash. Otherwise fail to prevent data
306			// inconsistencies. See <https://github.com/paritytech/polkadot-sdk/pull/3639> for more
307			// details.
308			ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState);
309		}
310
311		<Ledger<T>>::remove(&controller);
312		<Ledger<T>>::insert(&self.stash, &self);
313		<Bonded<T>>::insert(&self.stash, &self.stash);
314
315		Ok(())
316	}
317
318	/// Clears all data related to a staking ledger and its bond in both [`Ledger`] and [`Bonded`]
319	/// storage items and updates the stash staking lock.
320	pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult {
321		let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
322
323		<Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
324			Ledger::<T>::remove(controller);
325			<Bonded<T>>::remove(&stash);
326			<Payee<T>>::remove(&stash);
327
328			// kill virtual staker if it exists.
329			if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
330				// if not virtual staker, clear locks.
331				asset::kill_stake::<T>(&ledger.stash)?;
332			}
333			Pallet::<T>::deposit_event(crate::Event::<T>::StakerRemoved {
334				stash: ledger.stash.clone(),
335			});
336			Ok(())
337		})?
338	}
339
340	#[cfg(test)]
341	pub(crate) fn assert_stash_killed(stash: T::AccountId) {
342		assert!(!Ledger::<T>::contains_key(&stash));
343		assert!(!Bonded::<T>::contains_key(&stash));
344		assert!(!Payee::<T>::contains_key(&stash));
345		assert!(!VirtualStakers::<T>::contains_key(&stash));
346	}
347
348	/// Remove entries from `unlocking` that are sufficiently old and reduce the
349	/// total by the sum of their balances.
350	pub(crate) fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
351		let mut total = self.total;
352		let unlocking: BoundedVec<_, _> = self
353			.unlocking
354			.into_iter()
355			.filter(|chunk| {
356				if chunk.era > current_era {
357					true
358				} else {
359					total = total.saturating_sub(chunk.value);
360					false
361				}
362			})
363			.collect::<Vec<_>>()
364			.try_into()
365			.expect(
366				"filtering items from a bounded vec always leaves length less than bounds. qed",
367			);
368
369		Self {
370			stash: self.stash,
371			total,
372			active: self.active,
373			unlocking,
374			controller: self.controller,
375		}
376	}
377
378	/// Re-bond funds that were scheduled for unlocking.
379	///
380	/// Returns the updated ledger, and the amount actually rebonded.
381	pub(crate) fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
382		let mut unlocking_balance = BalanceOf::<T>::zero();
383
384		while let Some(last) = self.unlocking.last_mut() {
385			if unlocking_balance.defensive_saturating_add(last.value) <= value {
386				unlocking_balance += last.value;
387				self.active += last.value;
388				self.unlocking.pop();
389			} else {
390				let diff = value.defensive_saturating_sub(unlocking_balance);
391
392				unlocking_balance += diff;
393				self.active += diff;
394				last.value -= diff;
395			}
396
397			if unlocking_balance >= value {
398				break;
399			}
400		}
401
402		(self, unlocking_balance)
403	}
404
405	/// Slash the staker for a given amount of balance.
406	///
407	/// This implements a proportional slashing system, whereby we set our preference to slash as
408	/// such:
409	///
410	/// - If any unlocking chunks exist that are scheduled to be unlocked at `offence_era +
411	///   bonding_duration` and onwards, the slash is divided equally between the active ledger and
412	///   the unlocking chunks.
413	/// - If no such chunks exist, then only the active balance is slashed.
414	///
415	/// Note that the above is only a *preference*. If for any reason the active ledger, with or
416	/// without some portion of the unlocking chunks that are more justified to be slashed are not
417	/// enough, then the slashing will continue and will consume as much of the active and unlocking
418	/// chunks as needed.
419	///
420	/// This will never slash more than the given amount. If any of the chunks become dusted, the
421	/// last chunk is slashed slightly less to compensate. Returns the amount of funds actually
422	/// slashed.
423	///
424	/// `offence_era` is the era in which the offence occurred (not the era it is enacted in).
425	///
426	/// This calls `Config::OnStakingUpdate::on_slash` with information as to how the slash was
427	/// applied.
428	pub(crate) fn slash(
429		&mut self,
430		slash_amount: BalanceOf<T>,
431		minimum_balance: BalanceOf<T>,
432		offence_era: EraIndex,
433	) -> BalanceOf<T> {
434		if slash_amount.is_zero() {
435			return Zero::zero();
436		}
437
438		use sp_runtime::PerThing as _;
439		let mut remaining_slash = slash_amount;
440		let pre_slash_total = self.total;
441
442		// for an `offence_era = x`, any chunk that is scheduled to be unlocked at era `x + 28`
443		// (assuming 28 is the bonding duration) onwards should be slashed.
444		let slashable_chunks_start = offence_era.saturating_add(T::BondingDuration::get());
445
446		// `Some(ratio)` if this is proportional, with `ratio`, `None` otherwise. In both cases, we
447		// slash first the active chunk, and then `slash_chunks_priority`.
448		let (maybe_proportional, slash_chunks_priority) = {
449			if let Some(first_slashable_index) =
450				self.unlocking.iter().position(|c| c.era >= slashable_chunks_start)
451			{
452				// If there exists a chunk who's after the first_slashable_start, then this is a
453				// proportional slash, because we want to slash active and these chunks
454				// proportionally.
455
456				// The indices of the first chunk after the slash up through the most recent chunk.
457				// (The most recent chunk is at greatest from this era)
458				let affected_indices = first_slashable_index..self.unlocking.len();
459				let unbonding_affected_balance =
460					affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
461						if let Some(chunk) = self.unlocking.get(i).defensive() {
462							sum.saturating_add(chunk.value)
463						} else {
464							sum
465						}
466					});
467				let affected_balance = self.active.saturating_add(unbonding_affected_balance);
468				let ratio = Perquintill::from_rational_with_rounding(
469					slash_amount,
470					affected_balance,
471					Rounding::Up,
472				)
473				.unwrap_or_else(|_| Perquintill::one());
474				(
475					Some(ratio),
476					affected_indices.chain((0..first_slashable_index).rev()).collect::<Vec<_>>(),
477				)
478			} else {
479				// We just slash from the last chunk to the most recent one, if need be.
480				(None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
481			}
482		};
483
484		// Helper to update `target` and the ledgers total after accounting for slashing `target`.
485		log!(
486			trace,
487			"slashing {:?} for offence era {:?} out of {:?}, priority: {:?}, proportional = {:?}",
488			slash_amount,
489			offence_era,
490			self,
491			slash_chunks_priority,
492			maybe_proportional,
493		);
494
495		let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
496			let mut slash_from_target = if let Some(ratio) = maybe_proportional {
497				ratio.mul_ceil(*target)
498			} else {
499				*slash_remaining
500			}
501			// this is the total that that the slash target has. We can't slash more than
502			// this anyhow!
503			.min(*target)
504			// this is the total amount that we would have wanted to slash
505			// non-proportionally, a proportional slash should never exceed this either!
506			.min(*slash_remaining);
507
508			// slash out from *target exactly `slash_from_target`.
509			*target = *target - slash_from_target;
510			if *target < minimum_balance {
511				// Slash the rest of the target if it's dust. This might cause the last chunk to be
512				// slightly under-slashed, by at most `MaxUnlockingChunks * ED`, which is not a big
513				// deal.
514				slash_from_target =
515					core::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
516			}
517
518			self.total = self.total.saturating_sub(slash_from_target);
519			*slash_remaining = slash_remaining.saturating_sub(slash_from_target);
520		};
521
522		// If this is *not* a proportional slash, the active will always wiped to 0.
523		slash_out_of(&mut self.active, &mut remaining_slash);
524
525		let mut slashed_unlocking = BTreeMap::<_, _>::new();
526		for i in slash_chunks_priority {
527			if remaining_slash.is_zero() {
528				break;
529			}
530
531			if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
532				slash_out_of(&mut chunk.value, &mut remaining_slash);
533				// write the new slashed value of this chunk to the map.
534				slashed_unlocking.insert(chunk.era, chunk.value);
535			} else {
536				break;
537			}
538		}
539
540		// clean unlocking chunks that are set to zero.
541		self.unlocking.retain(|c| !c.value.is_zero());
542
543		let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
544		T::EventListeners::on_slash(
545			&self.stash,
546			self.active,
547			&slashed_unlocking,
548			final_slashed_amount,
549		);
550		final_slashed_amount
551	}
552}
553
554/// State of a ledger with regards with its data and metadata integrity.
555#[derive(PartialEq, Debug)]
556pub(crate) enum LedgerIntegrityState {
557	/// Ledger, bond and corresponding staking lock is OK.
558	Ok,
559	/// Ledger and/or bond is corrupted. This means that the bond has a ledger with a different
560	/// stash than the bonded stash.
561	Corrupted,
562	/// Ledger was corrupted and it has been killed.
563	CorruptedKilled,
564	/// Ledger and bond are OK, however the ledger's stash lock is out of sync.
565	LockCorrupted,
566}
567
568// This structs makes it easy to write tests to compare staking ledgers fetched from storage. This
569// is required because the controller field is not stored in storage and it is private.
570#[cfg(test)]
571#[derive(frame_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
572pub struct StakingLedgerInspect<T: Config> {
573	pub stash: T::AccountId,
574	#[codec(compact)]
575	pub total: BalanceOf<T>,
576	#[codec(compact)]
577	pub active: BalanceOf<T>,
578	pub unlocking:
579		frame_support::BoundedVec<crate::UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
580}
581
582#[cfg(test)]
583impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> {
584	fn eq(&self, other: &StakingLedgerInspect<T>) -> bool {
585		self.stash == other.stash &&
586			self.total == other.total &&
587			self.active == other.active &&
588			self.unlocking == other.unlocking
589	}
590}
591
592#[cfg(test)]
593impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}