// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! A Ledger implementation for stakers. //! //! A [`StakingLedger`] encapsulates all the state and logic related to the stake of bonded //! stakers, namely, it handles the following storage items: //! * [`Bonded`]: mutates and reads the state of the controller <> stash bond map (to be deprecated //! soon); //! * [`Ledger`]: mutates and reads the state of all the stakers. The [`Ledger`] storage item stores //! instances of [`StakingLedger`] keyed by the staker's controller account and should be mutated //! and read through the [`StakingLedger`] API; //! * [`Payee`]: mutates and reads the reward destination preferences for a bonded stash. //! * Staking locks: mutates the locks for staking. //! //! NOTE: All the storage operations related to the staking ledger (both reads and writes) *MUST* be //! performed through the methods exposed by the [`StakingLedger`] implementation in order to ensure //! state consistency. use pezframe_support::{defensive, ensure, traits::Defensive}; use pezsp_runtime::DispatchResult; use pezsp_staking::{StakingAccount, StakingInterface}; use crate::{ asset, BalanceOf, Bonded, Config, Error, Ledger, Payee, Pezpallet, RewardDestination, StakingLedger, VirtualStakers, }; #[cfg(any(feature = "runtime-benchmarks", test))] use pezsp_runtime::traits::Zero; impl StakingLedger { #[cfg(any(feature = "runtime-benchmarks", test))] pub fn default_from(stash: T::AccountId) -> Self { Self { stash: stash.clone(), total: Zero::zero(), active: Zero::zero(), unlocking: Default::default(), legacy_claimed_rewards: Default::default(), controller: Some(stash), } } /// Returns a new instance of a staking ledger. /// /// The [`Ledger`] storage is not mutated. In order to store, `StakingLedger::update` must be /// called on the returned staking ledger. /// /// Note: as the controller accounts are being deprecated, the stash account is the same as the /// controller account. pub fn new(stash: T::AccountId, stake: BalanceOf) -> Self { Self { stash: stash.clone(), active: stake, total: stake, unlocking: Default::default(), legacy_claimed_rewards: Default::default(), // controllers are deprecated and mapped 1-1 to stashes. controller: Some(stash), } } /// Returns the paired account, if any. /// /// A "pair" refers to the tuple (stash, controller). If the input is a /// [`StakingAccount::Stash`] variant, its pair account will be of type /// [`StakingAccount::Controller`] and vice-versa. /// /// This method is meant to abstract from the runtime development the difference between stash /// and controller. This will be deprecated once the controller is fully deprecated as well. pub(crate) fn paired_account(account: StakingAccount) -> Option { match account { StakingAccount::Stash(stash) => >::get(stash), StakingAccount::Controller(controller) => >::get(&controller).map(|ledger| ledger.stash), } } /// Returns whether a given account is bonded. pub(crate) fn is_bonded(account: StakingAccount) -> bool { match account { StakingAccount::Stash(stash) => >::contains_key(stash), StakingAccount::Controller(controller) => >::contains_key(controller), } } /// Returns a staking ledger, if it is bonded and it exists in storage. /// /// This getter can be called with either a controller or stash account, provided that the /// account is properly wrapped in the respective [`StakingAccount`] variant. This is meant to /// abstract the concept of controller/stash accounts from the caller. /// /// Returns [`Error::BadState`] when a bond is in "bad state". A bond is in a bad state when a /// stash has a controller which is bonding a ledger associated with another stash. pub(crate) fn get(account: StakingAccount) -> Result, Error> { let (stash, controller) = match account.clone() { StakingAccount::Stash(stash) => (stash.clone(), >::get(&stash).ok_or(Error::::NotStash)?), StakingAccount::Controller(controller) => ( Ledger::::get(&controller) .map(|l| l.stash) .ok_or(Error::::NotController)?, controller, ), }; let ledger = >::get(&controller) .map(|mut ledger| { ledger.controller = Some(controller.clone()); ledger }) .ok_or(Error::::NotController)?; // if ledger bond is in a bad state, return error to prevent applying operations that may // further spoil the ledger's state. A bond is in bad state when the bonded controller is // associated with a different ledger (i.e. a ledger with a different stash). // // See for more details. ensure!( Bonded::::get(&stash) == Some(controller) && ledger.stash == stash, Error::::BadState ); Ok(ledger) } /// Returns the reward destination of a staking ledger, stored in [`Payee`]. /// /// Note: if the stash is not bonded and/or does not have an entry in [`Payee`], it returns the /// default reward destination. pub(crate) fn reward_destination( account: StakingAccount, ) -> Option> { let stash = match account { StakingAccount::Stash(stash) => Some(stash), StakingAccount::Controller(controller) => Self::paired_account(StakingAccount::Controller(controller)), }; if let Some(stash) = stash { >::get(stash) } else { defensive!("fetched reward destination from unbonded stash {}", stash); None } } /// Returns the controller account of a staking ledger. /// /// Note: it will fallback into querying the [`Bonded`] storage with the ledger stash if the /// controller is not set in `self`, which most likely means that self was fetched directly from /// [`Ledger`] instead of through the methods exposed in [`StakingLedger`]. If the ledger does /// not exist in storage, it returns `None`. pub fn controller(&self) -> Option { self.controller.clone().or_else(|| { defensive!("fetched a controller on a ledger instance without it."); Self::paired_account(StakingAccount::Stash(self.stash.clone())) }) } /// Inserts/updates a staking ledger account. /// /// Bonds the ledger if it is not bonded yet, signalling that this is a new ledger. The staking /// locks of the stash account are updated accordingly. /// /// Note: To ensure lock consistency, all the [`Ledger`] storage updates should be made through /// this helper function. pub(crate) fn update(self) -> Result<(), Error> { if !>::contains_key(&self.stash) { return Err(Error::::NotStash); } // We skip locking virtual stakers. if !Pezpallet::::is_virtual_staker(&self.stash) { // for direct stakers, update lock on stash based on ledger. asset::update_stake::(&self.stash, self.total) .map_err(|_| Error::::NotEnoughFunds)?; } Ledger::::insert( &self.controller().ok_or_else(|| { defensive!("update called on a ledger that is not bonded."); Error::::NotController })?, &self, ); Ok(()) } /// Bonds a ledger. /// /// It sets the reward preferences for the bonded stash. pub(crate) fn bond(self, payee: RewardDestination) -> Result<(), Error> { if >::contains_key(&self.stash) { return Err(Error::::AlreadyBonded); } >::insert(&self.stash, payee); >::insert(&self.stash, &self.stash); self.update() } /// Sets the ledger Payee. pub(crate) fn set_payee(self, payee: RewardDestination) -> Result<(), Error> { if !>::contains_key(&self.stash) { return Err(Error::::NotStash); } >::insert(&self.stash, payee); Ok(()) } /// Sets the ledger controller to its stash. pub(crate) fn set_controller_to_stash(self) -> Result<(), Error> { let controller = self.controller.as_ref() .defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.") .ok_or(Error::::NotController)?; ensure!(self.stash != *controller, Error::::AlreadyPaired); // check if the ledger's stash is a controller of another ledger. if let Some(bonded_ledger) = Ledger::::get(&self.stash) { // there is a ledger bonded by the stash. In this case, the stash of the bonded ledger // should be the same as the ledger's stash. Otherwise fail to prevent data // inconsistencies. See for more // details. ensure!(bonded_ledger.stash == self.stash, Error::::BadState); } >::remove(&controller); >::insert(&self.stash, &self); >::insert(&self.stash, &self.stash); Ok(()) } /// Clears all data related to a staking ledger and its bond in both [`Ledger`] and [`Bonded`] /// storage items and updates the stash staking lock. pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult { let controller = >::get(stash).ok_or(Error::::NotStash)?; >::get(&controller).ok_or(Error::::NotController).map(|ledger| { Ledger::::remove(controller); >::remove(&stash); >::remove(&stash); // kill virtual staker if it exists. if >::take(&ledger.stash).is_none() { // if not virtual staker, clear locks. asset::kill_stake::(&ledger.stash)?; } Ok(()) })? } } #[cfg(test)] use { crate::UnlockChunk, codec::{Decode, Encode, MaxEncodedLen}, scale_info::TypeInfo, }; // This structs makes it easy to write tests to compare staking ledgers fetched from storage. This // is required because the controller field is not stored in storage and it is private. #[cfg(test)] #[derive(pezframe_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)] pub struct StakingLedgerInspect { pub stash: T::AccountId, #[codec(compact)] pub total: BalanceOf, #[codec(compact)] pub active: BalanceOf, pub unlocking: pezframe_support::BoundedVec>, T::MaxUnlockingChunks>, pub legacy_claimed_rewards: pezframe_support::BoundedVec, } #[cfg(test)] impl PartialEq> for StakingLedger { fn eq(&self, other: &StakingLedgerInspect) -> bool { self.stash == other.stash && self.total == other.total && self.active == other.active && self.unlocking == other.unlocking && self.legacy_claimed_rewards == other.legacy_claimed_rewards } } #[cfg(test)] impl codec::EncodeLike> for StakingLedgerInspect {}