Skip to main content

pallet_staking/
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 frame_support::{defensive, ensure, traits::Defensive};
35use sp_runtime::DispatchResult;
36use sp_staking::{StakingAccount, StakingInterface};
37
38use crate::{
39	asset, BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination,
40	StakingLedger, VirtualStakers,
41};
42
43#[cfg(any(feature = "runtime-benchmarks", test))]
44use sp_runtime::traits::Zero;
45
46impl<T: Config> StakingLedger<T> {
47	#[cfg(any(feature = "runtime-benchmarks", test))]
48	pub fn default_from(stash: T::AccountId) -> Self {
49		Self {
50			stash: stash.clone(),
51			total: Zero::zero(),
52			active: Zero::zero(),
53			unlocking: Default::default(),
54			legacy_claimed_rewards: Default::default(),
55			controller: Some(stash),
56		}
57	}
58
59	/// Returns a new instance of a staking ledger.
60	///
61	/// The [`Ledger`] storage is not mutated. In order to store, `StakingLedger::update` must be
62	/// called on the returned staking ledger.
63	///
64	/// Note: as the controller accounts are being deprecated, the stash account is the same as the
65	/// controller account.
66	pub fn new(stash: T::AccountId, stake: BalanceOf<T>) -> Self {
67		Self {
68			stash: stash.clone(),
69			active: stake,
70			total: stake,
71			unlocking: Default::default(),
72			legacy_claimed_rewards: Default::default(),
73			// controllers are deprecated and mapped 1-1 to stashes.
74			controller: Some(stash),
75		}
76	}
77
78	/// Returns the paired account, if any.
79	///
80	/// A "pair" refers to the tuple (stash, controller). If the input is a
81	/// [`StakingAccount::Stash`] variant, its pair account will be of type
82	/// [`StakingAccount::Controller`] and vice-versa.
83	///
84	/// This method is meant to abstract from the runtime development the difference between stash
85	/// and controller. This will be deprecated once the controller is fully deprecated as well.
86	pub(crate) fn paired_account(account: StakingAccount<T::AccountId>) -> Option<T::AccountId> {
87		match account {
88			StakingAccount::Stash(stash) => <Bonded<T>>::get(stash),
89			StakingAccount::Controller(controller) => {
90				<Ledger<T>>::get(&controller).map(|ledger| ledger.stash)
91			},
92		}
93	}
94
95	/// Returns whether a given account is bonded.
96	pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool {
97		match account {
98			StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash),
99			StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller),
100		}
101	}
102
103	/// Returns a staking ledger, if it is bonded and it exists in storage.
104	///
105	/// This getter can be called with either a controller or stash account, provided that the
106	/// account is properly wrapped in the respective [`StakingAccount`] variant. This is meant to
107	/// abstract the concept of controller/stash accounts from the caller.
108	///
109	/// Returns [`Error::BadState`] when a bond is in "bad state". A bond is in a bad state when a
110	/// stash has a controller which is bonding a ledger associated with another stash.
111	pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
112		let (stash, controller) = match account.clone() {
113			StakingAccount::Stash(stash) => {
114				(stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?)
115			},
116			StakingAccount::Controller(controller) => (
117				Ledger::<T>::get(&controller)
118					.map(|l| l.stash)
119					.ok_or(Error::<T>::NotController)?,
120				controller,
121			),
122		};
123
124		let ledger = <Ledger<T>>::get(&controller)
125			.map(|mut ledger| {
126				ledger.controller = Some(controller.clone());
127				ledger
128			})
129			.ok_or(Error::<T>::NotController)?;
130
131		// if ledger bond is in a bad state, return error to prevent applying operations that may
132		// further spoil the ledger's state. A bond is in bad state when the bonded controller is
133		// associated with a different ledger (i.e. a ledger with a different stash).
134		//
135		// See <https://github.com/paritytech/polkadot-sdk/issues/3245> for more details.
136		ensure!(
137			Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
138			Error::<T>::BadState
139		);
140
141		Ok(ledger)
142	}
143
144	/// Returns the reward destination of a staking ledger, stored in [`Payee`].
145	///
146	/// Note: if the stash is not bonded and/or does not have an entry in [`Payee`], it returns the
147	/// default reward destination.
148	pub(crate) fn reward_destination(
149		account: StakingAccount<T::AccountId>,
150	) -> Option<RewardDestination<T::AccountId>> {
151		let stash = match account {
152			StakingAccount::Stash(stash) => Some(stash),
153			StakingAccount::Controller(controller) => {
154				Self::paired_account(StakingAccount::Controller(controller))
155			},
156		};
157
158		if let Some(stash) = stash {
159			<Payee<T>>::get(stash)
160		} else {
161			defensive!("fetched reward destination from unbonded stash {}", stash);
162			None
163		}
164	}
165
166	/// Returns the controller account of a staking ledger.
167	///
168	/// Note: it will fallback into querying the [`Bonded`] storage with the ledger stash if the
169	/// controller is not set in `self`, which most likely means that self was fetched directly from
170	/// [`Ledger`] instead of through the methods exposed in [`StakingLedger`]. If the ledger does
171	/// not exist in storage, it returns `None`.
172	pub fn controller(&self) -> Option<T::AccountId> {
173		self.controller.clone().or_else(|| {
174			defensive!("fetched a controller on a ledger instance without it.");
175			Self::paired_account(StakingAccount::Stash(self.stash.clone()))
176		})
177	}
178
179	/// Inserts/updates a staking ledger account.
180	///
181	/// Bonds the ledger if it is not bonded yet, signalling that this is a new ledger. The staking
182	/// locks of the stash account are updated accordingly.
183	///
184	/// Note: To ensure lock consistency, all the [`Ledger`] storage updates should be made through
185	/// this helper function.
186	pub(crate) fn update(self) -> Result<(), Error<T>> {
187		if !<Bonded<T>>::contains_key(&self.stash) {
188			return Err(Error::<T>::NotStash);
189		}
190
191		// We skip locking virtual stakers.
192		if !Pallet::<T>::is_virtual_staker(&self.stash) {
193			// for direct stakers, update lock on stash based on ledger.
194			asset::update_stake::<T>(&self.stash, self.total)
195				.map_err(|_| Error::<T>::NotEnoughFunds)?;
196		}
197
198		Ledger::<T>::insert(
199			&self.controller().ok_or_else(|| {
200				defensive!("update called on a ledger that is not bonded.");
201				Error::<T>::NotController
202			})?,
203			&self,
204		);
205
206		Ok(())
207	}
208
209	/// Bonds a ledger.
210	///
211	/// It sets the reward preferences for the bonded stash.
212	pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
213		if <Bonded<T>>::contains_key(&self.stash) {
214			return Err(Error::<T>::AlreadyBonded);
215		}
216
217		<Payee<T>>::insert(&self.stash, payee);
218		<Bonded<T>>::insert(&self.stash, &self.stash);
219		self.update()
220	}
221
222	/// Sets the ledger Payee.
223	pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
224		if !<Bonded<T>>::contains_key(&self.stash) {
225			return Err(Error::<T>::NotStash);
226		}
227
228		<Payee<T>>::insert(&self.stash, payee);
229		Ok(())
230	}
231
232	/// Sets the ledger controller to its stash.
233	pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> {
234		let controller = self.controller.as_ref()
235            .defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.")
236            .ok_or(Error::<T>::NotController)?;
237
238		ensure!(self.stash != *controller, Error::<T>::AlreadyPaired);
239
240		// check if the ledger's stash is a controller of another ledger.
241		if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
242			// there is a ledger bonded by the stash. In this case, the stash of the bonded ledger
243			// should be the same as the ledger's stash. Otherwise fail to prevent data
244			// inconsistencies. See <https://github.com/paritytech/polkadot-sdk/pull/3639> for more
245			// details.
246			ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState);
247		}
248
249		<Ledger<T>>::remove(&controller);
250		<Ledger<T>>::insert(&self.stash, &self);
251		<Bonded<T>>::insert(&self.stash, &self.stash);
252
253		Ok(())
254	}
255
256	/// Clears all data related to a staking ledger and its bond in both [`Ledger`] and [`Bonded`]
257	/// storage items and updates the stash staking lock.
258	pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult {
259		let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
260
261		<Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
262			Ledger::<T>::remove(controller);
263			<Bonded<T>>::remove(&stash);
264			<Payee<T>>::remove(&stash);
265
266			// kill virtual staker if it exists.
267			if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
268				// if not virtual staker, clear locks.
269				asset::kill_stake::<T>(&ledger.stash)?;
270			}
271
272			Ok(())
273		})?
274	}
275}
276
277#[cfg(test)]
278use {
279	crate::UnlockChunk,
280	codec::{Decode, Encode, MaxEncodedLen},
281	scale_info::TypeInfo,
282};
283
284// This structs makes it easy to write tests to compare staking ledgers fetched from storage. This
285// is required because the controller field is not stored in storage and it is private.
286#[cfg(test)]
287#[derive(frame_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
288pub struct StakingLedgerInspect<T: Config> {
289	pub stash: T::AccountId,
290	#[codec(compact)]
291	pub total: BalanceOf<T>,
292	#[codec(compact)]
293	pub active: BalanceOf<T>,
294	pub unlocking: frame_support::BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
295	pub legacy_claimed_rewards: frame_support::BoundedVec<sp_staking::EraIndex, T::HistoryDepth>,
296}
297
298#[cfg(test)]
299impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> {
300	fn eq(&self, other: &StakingLedgerInspect<T>) -> bool {
301		self.stash == other.stash &&
302			self.total == other.total &&
303			self.active == other.active &&
304			self.unlocking == other.unlocking &&
305			self.legacy_claimed_rewards == other.legacy_claimed_rewards
306	}
307}
308
309#[cfg(test)]
310impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}