1use 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#[derive(
51 PartialEq, Eq, Clone, Encode, Decode, DecodeWithMemTracking, Debug, TypeInfo, MaxEncodedLen,
52)]
53pub struct UnlockChunk<Balance: HasCompact + MaxEncodedLen> {
54 #[codec(compact)]
56 pub value: Balance,
57 #[codec(compact)]
59 pub era: EraIndex,
60}
61
62#[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 pub stash: T::AccountId,
86
87 #[codec(compact)]
90 pub total: BalanceOf<T>,
91
92 #[codec(compact)]
95 pub active: BalanceOf<T>,
96
97 pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
101
102 #[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 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 controller: Some(stash),
137 }
138 }
139
140 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 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 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 ensure!(
199 Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
200 Error::<T>::BadState
201 );
202
203 Ok(ledger)
204 }
205
206 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 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 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 if !Pallet::<T>::is_virtual_staker(&self.stash) {
255 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 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 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 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 if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
304 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 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 if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
330 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 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 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 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 let slashable_chunks_start = offence_era.saturating_add(T::BondingDuration::get());
445
446 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 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 (None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
481 }
482 };
483
484 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 .min(*target)
504 .min(*slash_remaining);
507
508 *target = *target - slash_from_target;
510 if *target < minimum_balance {
511 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 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 slashed_unlocking.insert(chunk.era, chunk.value);
535 } else {
536 break;
537 }
538 }
539
540 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#[derive(PartialEq, Debug)]
556pub(crate) enum LedgerIntegrityState {
557 Ok,
559 Corrupted,
562 CorruptedKilled,
564 LockCorrupted,
566}
567
568#[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> {}