use crate::{
asset, log, BalanceOf, Bonded, Config, DecodeWithMemTracking, Error, Ledger, Pallet, Payee,
RewardDestination, Vec, VirtualStakers,
};
use alloc::{collections::BTreeMap, fmt::Debug};
use codec::{Decode, Encode, HasCompact, MaxEncodedLen};
use frame_support::{
defensive, ensure,
traits::{Defensive, DefensiveSaturating, Get},
BoundedVec, CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound,
};
use scale_info::TypeInfo;
use sp_runtime::{traits::Zero, DispatchResult, Perquintill, Rounding, Saturating};
use sp_staking::{EraIndex, OnStakingUpdate, StakingAccount, StakingInterface};
#[derive(
PartialEq, Eq, Clone, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo, MaxEncodedLen,
)]
pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> {
#[codec(compact)]
pub value: Balance,
#[codec(compact)]
pub era: EraIndex,
}
#[derive(
PartialEqNoBound,
EqNoBound,
CloneNoBound,
Encode,
Decode,
DebugNoBound,
TypeInfo,
MaxEncodedLen,
DecodeWithMemTracking,
)]
#[scale_info(skip_type_params(T))]
pub struct StakingLedger<T: Config> {
pub stash: T::AccountId,
#[codec(compact)]
pub total: BalanceOf<T>,
#[codec(compact)]
pub active: BalanceOf<T>,
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
#[codec(skip)]
pub controller: Option<T::AccountId>,
}
impl<T: Config> StakingLedger<T> {
#[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(),
controller: Some(stash),
}
}
pub fn new(stash: T::AccountId, stake: BalanceOf<T>) -> Self {
Self {
stash: stash.clone(),
active: stake,
total: stake,
unlocking: Default::default(),
controller: Some(stash),
}
}
pub(crate) fn paired_account(account: StakingAccount<T::AccountId>) -> Option<T::AccountId> {
match account {
StakingAccount::Stash(stash) => <Bonded<T>>::get(stash),
StakingAccount::Controller(controller) => {
<Ledger<T>>::get(&controller).map(|ledger| ledger.stash)
},
}
}
pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool {
match account {
StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash),
StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller),
}
}
pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
let (stash, controller) = match account {
StakingAccount::Stash(stash) => {
(stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?)
},
StakingAccount::Controller(controller) => (
Ledger::<T>::get(&controller)
.map(|l| l.stash)
.ok_or(Error::<T>::NotController)?,
controller,
),
};
let ledger = <Ledger<T>>::get(&controller)
.map(|mut ledger| {
ledger.controller = Some(controller.clone());
ledger
})
.ok_or(Error::<T>::NotController)?;
ensure!(
Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
Error::<T>::BadState
);
Ok(ledger)
}
pub(crate) fn reward_destination(
account: StakingAccount<T::AccountId>,
) -> Option<RewardDestination<T::AccountId>> {
let stash = match account {
StakingAccount::Stash(stash) => Some(stash),
StakingAccount::Controller(controller) => {
Self::paired_account(StakingAccount::Controller(controller))
},
};
if let Some(stash) = stash {
<Payee<T>>::get(stash)
} else {
defensive!("fetched reward destination from unbonded stash {}", stash);
None
}
}
pub fn controller(&self) -> Option<T::AccountId> {
self.controller.clone().or_else(|| {
defensive!("fetched a controller on a ledger instance without it.");
Self::paired_account(StakingAccount::Stash(self.stash.clone()))
})
}
pub(crate) fn update(self) -> Result<(), Error<T>> {
if !<Bonded<T>>::contains_key(&self.stash) {
return Err(Error::<T>::NotStash);
}
if !Pallet::<T>::is_virtual_staker(&self.stash) {
asset::update_stake::<T>(&self.stash, self.total)
.map_err(|_| Error::<T>::NotEnoughFunds)?;
}
Ledger::<T>::insert(
&self.controller().ok_or_else(|| {
defensive!("update called on a ledger that is not bonded.");
Error::<T>::NotController
})?,
&self,
);
Ok(())
}
pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
if <Bonded<T>>::contains_key(&self.stash) {
return Err(Error::<T>::AlreadyBonded);
}
<Payee<T>>::insert(&self.stash, payee);
<Bonded<T>>::insert(&self.stash, &self.stash);
self.update()
}
pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
if !<Bonded<T>>::contains_key(&self.stash) {
return Err(Error::<T>::NotStash);
}
<Payee<T>>::insert(&self.stash, payee);
Ok(())
}
pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> {
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::<T>::NotController)?;
ensure!(self.stash != *controller, Error::<T>::AlreadyPaired);
if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState);
}
<Ledger<T>>::remove(&controller);
<Ledger<T>>::insert(&self.stash, &self);
<Bonded<T>>::insert(&self.stash, &self.stash);
Ok(())
}
pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult {
let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
<Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
Ledger::<T>::remove(controller);
<Bonded<T>>::remove(&stash);
<Payee<T>>::remove(&stash);
if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
asset::kill_stake::<T>(&ledger.stash)?;
}
Pallet::<T>::deposit_event(crate::Event::<T>::StakerRemoved {
stash: ledger.stash.clone(),
});
Ok(())
})?
}
#[cfg(test)]
pub(crate) fn assert_stash_killed(stash: T::AccountId) {
assert!(!Ledger::<T>::contains_key(&stash));
assert!(!Bonded::<T>::contains_key(&stash));
assert!(!Payee::<T>::contains_key(&stash));
assert!(!VirtualStakers::<T>::contains_key(&stash));
}
pub(crate) fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
let mut total = self.total;
let unlocking: BoundedVec<_, _> = self
.unlocking
.into_iter()
.filter(|chunk| {
if chunk.era > current_era {
true
} else {
total = total.saturating_sub(chunk.value);
false
}
})
.collect::<Vec<_>>()
.try_into()
.expect(
"filtering items from a bounded vec always leaves length less than bounds. qed",
);
Self {
stash: self.stash,
total,
active: self.active,
unlocking,
controller: self.controller,
}
}
pub(crate) fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
let mut unlocking_balance = BalanceOf::<T>::zero();
while let Some(last) = self.unlocking.last_mut() {
if unlocking_balance.defensive_saturating_add(last.value) <= value {
unlocking_balance += last.value;
self.active += last.value;
self.unlocking.pop();
} else {
let diff = value.defensive_saturating_sub(unlocking_balance);
unlocking_balance += diff;
self.active += diff;
last.value -= diff;
}
if unlocking_balance >= value {
break;
}
}
(self, unlocking_balance)
}
pub fn slash(
&mut self,
slash_amount: BalanceOf<T>,
minimum_balance: BalanceOf<T>,
slash_era: EraIndex,
) -> BalanceOf<T> {
if slash_amount.is_zero() {
return Zero::zero();
}
use sp_runtime::PerThing as _;
let mut remaining_slash = slash_amount;
let pre_slash_total = self.total;
let slashable_chunks_start = slash_era.saturating_add(T::BondingDuration::get());
let (maybe_proportional, slash_chunks_priority) = {
if let Some(first_slashable_index) =
self.unlocking.iter().position(|c| c.era >= slashable_chunks_start)
{
let affected_indices = first_slashable_index..self.unlocking.len();
let unbonding_affected_balance =
affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
if let Some(chunk) = self.unlocking.get(i).defensive() {
sum.saturating_add(chunk.value)
} else {
sum
}
});
let affected_balance = self.active.saturating_add(unbonding_affected_balance);
let ratio = Perquintill::from_rational_with_rounding(
slash_amount,
affected_balance,
Rounding::Up,
)
.unwrap_or_else(|_| Perquintill::one());
(
Some(ratio),
affected_indices.chain((0..first_slashable_index).rev()).collect::<Vec<_>>(),
)
} else {
(None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
}
};
log!(
trace,
"slashing {:?} for era {:?} out of {:?}, priority: {:?}, proportional = {:?}",
slash_amount,
slash_era,
self,
slash_chunks_priority,
maybe_proportional,
);
let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
let mut slash_from_target = if let Some(ratio) = maybe_proportional {
ratio.mul_ceil(*target)
} else {
*slash_remaining
}
.min(*target)
.min(*slash_remaining);
*target = *target - slash_from_target;
if *target < minimum_balance {
slash_from_target =
core::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
}
self.total = self.total.saturating_sub(slash_from_target);
*slash_remaining = slash_remaining.saturating_sub(slash_from_target);
};
slash_out_of(&mut self.active, &mut remaining_slash);
let mut slashed_unlocking = BTreeMap::<_, _>::new();
for i in slash_chunks_priority {
if remaining_slash.is_zero() {
break;
}
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
slash_out_of(&mut chunk.value, &mut remaining_slash);
slashed_unlocking.insert(chunk.era, chunk.value);
} else {
break;
}
}
self.unlocking.retain(|c| !c.value.is_zero());
let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
T::EventListeners::on_slash(
&self.stash,
self.active,
&slashed_unlocking,
final_slashed_amount,
);
final_slashed_amount
}
}
#[derive(PartialEq, Debug)]
pub(crate) enum LedgerIntegrityState {
Ok,
Corrupted,
CorruptedKilled,
LockCorrupted,
}
#[cfg(test)]
#[derive(frame_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct StakingLedgerInspect<T: Config> {
pub stash: T::AccountId,
#[codec(compact)]
pub total: BalanceOf<T>,
#[codec(compact)]
pub active: BalanceOf<T>,
pub unlocking:
frame_support::BoundedVec<crate::UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
}
#[cfg(test)]
impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> {
fn eq(&self, other: &StakingLedgerInspect<T>) -> bool {
self.stash == other.stash &&
self.total == other.total &&
self.active == other.active &&
self.unlocking == other.unlocking
}
}
#[cfg(test)]
impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}