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(
68 PartialEqNoBound,
69 EqNoBound,
70 CloneNoBound,
71 Encode,
72 Decode,
73 DebugNoBound,
74 TypeInfo,
75 MaxEncodedLen,
76 DecodeWithMemTracking,
77)]
78#[scale_info(skip_type_params(T))]
79pub struct StakingLedger<T: Config> {
80 pub stash: T::AccountId,
82
83 #[codec(compact)]
86 pub total: BalanceOf<T>,
87
88 #[codec(compact)]
91 pub active: BalanceOf<T>,
92
93 pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
97
98 #[codec(skip)]
103 pub controller: Option<T::AccountId>,
104}
105
106impl<T: Config> StakingLedger<T> {
107 #[cfg(any(feature = "runtime-benchmarks", test))]
108 pub fn default_from(stash: T::AccountId) -> Self {
109 Self {
110 stash: stash.clone(),
111 total: Zero::zero(),
112 active: Zero::zero(),
113 unlocking: Default::default(),
114 controller: Some(stash),
115 }
116 }
117
118 pub fn new(stash: T::AccountId, stake: BalanceOf<T>) -> Self {
126 Self {
127 stash: stash.clone(),
128 active: stake,
129 total: stake,
130 unlocking: Default::default(),
131 controller: Some(stash),
133 }
134 }
135
136 pub(crate) fn paired_account(account: StakingAccount<T::AccountId>) -> Option<T::AccountId> {
145 match account {
146 StakingAccount::Stash(stash) => <Bonded<T>>::get(stash),
147 StakingAccount::Controller(controller) => {
148 <Ledger<T>>::get(&controller).map(|ledger| ledger.stash)
149 },
150 }
151 }
152
153 pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool {
155 match account {
156 StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash),
157 StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller),
158 }
159 }
160
161 pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
170 let (stash, controller) = match account {
171 StakingAccount::Stash(stash) => {
172 (stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?)
173 },
174 StakingAccount::Controller(controller) => (
175 Ledger::<T>::get(&controller)
176 .map(|l| l.stash)
177 .ok_or(Error::<T>::NotController)?,
178 controller,
179 ),
180 };
181
182 let ledger = <Ledger<T>>::get(&controller)
183 .map(|mut ledger| {
184 ledger.controller = Some(controller.clone());
185 ledger
186 })
187 .ok_or(Error::<T>::NotController)?;
188
189 ensure!(
195 Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
196 Error::<T>::BadState
197 );
198
199 Ok(ledger)
200 }
201
202 pub(crate) fn reward_destination(
207 account: StakingAccount<T::AccountId>,
208 ) -> Option<RewardDestination<T::AccountId>> {
209 let stash = match account {
210 StakingAccount::Stash(stash) => Some(stash),
211 StakingAccount::Controller(controller) => {
212 Self::paired_account(StakingAccount::Controller(controller))
213 },
214 };
215
216 if let Some(stash) = stash {
217 <Payee<T>>::get(stash)
218 } else {
219 defensive!("fetched reward destination from unbonded stash {}", stash);
220 None
221 }
222 }
223
224 pub fn controller(&self) -> Option<T::AccountId> {
231 self.controller.clone().or_else(|| {
232 defensive!("fetched a controller on a ledger instance without it.");
233 Self::paired_account(StakingAccount::Stash(self.stash.clone()))
234 })
235 }
236
237 pub(crate) fn update(self) -> Result<(), Error<T>> {
245 if !<Bonded<T>>::contains_key(&self.stash) {
246 return Err(Error::<T>::NotStash);
247 }
248
249 if !Pallet::<T>::is_virtual_staker(&self.stash) {
251 asset::update_stake::<T>(&self.stash, self.total)
253 .map_err(|_| Error::<T>::NotEnoughFunds)?;
254 }
255
256 Ledger::<T>::insert(
257 &self.controller().ok_or_else(|| {
258 defensive!("update called on a ledger that is not bonded.");
259 Error::<T>::NotController
260 })?,
261 &self,
262 );
263
264 Ok(())
265 }
266
267 pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
271 if <Bonded<T>>::contains_key(&self.stash) {
272 return Err(Error::<T>::AlreadyBonded);
273 }
274
275 <Payee<T>>::insert(&self.stash, payee);
276 <Bonded<T>>::insert(&self.stash, &self.stash);
277 self.update()
278 }
279
280 pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
282 if !<Bonded<T>>::contains_key(&self.stash) {
283 return Err(Error::<T>::NotStash);
284 }
285
286 <Payee<T>>::insert(&self.stash, payee);
287 Ok(())
288 }
289
290 pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> {
292 let controller = self.controller.as_ref()
293 .defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.")
294 .ok_or(Error::<T>::NotController)?;
295
296 ensure!(self.stash != *controller, Error::<T>::AlreadyPaired);
297
298 if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
300 ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState);
305 }
306
307 <Ledger<T>>::remove(&controller);
308 <Ledger<T>>::insert(&self.stash, &self);
309 <Bonded<T>>::insert(&self.stash, &self.stash);
310
311 Ok(())
312 }
313
314 pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult {
317 let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
318
319 <Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
320 Ledger::<T>::remove(controller);
321 <Bonded<T>>::remove(&stash);
322 <Payee<T>>::remove(&stash);
323
324 if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
326 asset::kill_stake::<T>(&ledger.stash)?;
328 }
329 Pallet::<T>::deposit_event(crate::Event::<T>::StakerRemoved {
330 stash: ledger.stash.clone(),
331 });
332 Ok(())
333 })?
334 }
335
336 #[cfg(test)]
337 pub(crate) fn assert_stash_killed(stash: T::AccountId) {
338 assert!(!Ledger::<T>::contains_key(&stash));
339 assert!(!Bonded::<T>::contains_key(&stash));
340 assert!(!Payee::<T>::contains_key(&stash));
341 assert!(!VirtualStakers::<T>::contains_key(&stash));
342 }
343
344 pub(crate) fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
347 let mut total = self.total;
348 let unlocking: BoundedVec<_, _> = self
349 .unlocking
350 .into_iter()
351 .filter(|chunk| {
352 if chunk.era > current_era {
353 true
354 } else {
355 total = total.saturating_sub(chunk.value);
356 false
357 }
358 })
359 .collect::<Vec<_>>()
360 .try_into()
361 .expect(
362 "filtering items from a bounded vec always leaves length less than bounds. qed",
363 );
364
365 Self {
366 stash: self.stash,
367 total,
368 active: self.active,
369 unlocking,
370 controller: self.controller,
371 }
372 }
373
374 pub(crate) fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
378 let mut unlocking_balance = BalanceOf::<T>::zero();
379
380 while let Some(last) = self.unlocking.last_mut() {
381 if unlocking_balance.defensive_saturating_add(last.value) <= value {
382 unlocking_balance += last.value;
383 self.active += last.value;
384 self.unlocking.pop();
385 } else {
386 let diff = value.defensive_saturating_sub(unlocking_balance);
387
388 unlocking_balance += diff;
389 self.active += diff;
390 last.value -= diff;
391 }
392
393 if unlocking_balance >= value {
394 break;
395 }
396 }
397
398 (self, unlocking_balance)
399 }
400
401 pub(crate) fn slash(
425 &mut self,
426 slash_amount: BalanceOf<T>,
427 minimum_balance: BalanceOf<T>,
428 offence_era: EraIndex,
429 ) -> BalanceOf<T> {
430 if slash_amount.is_zero() {
431 return Zero::zero();
432 }
433
434 use sp_runtime::PerThing as _;
435 let mut remaining_slash = slash_amount;
436 let pre_slash_total = self.total;
437
438 let slashable_chunks_start = offence_era.saturating_add(T::BondingDuration::get());
441
442 let (maybe_proportional, slash_chunks_priority) = {
445 if let Some(first_slashable_index) =
446 self.unlocking.iter().position(|c| c.era >= slashable_chunks_start)
447 {
448 let affected_indices = first_slashable_index..self.unlocking.len();
455 let unbonding_affected_balance =
456 affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
457 if let Some(chunk) = self.unlocking.get(i).defensive() {
458 sum.saturating_add(chunk.value)
459 } else {
460 sum
461 }
462 });
463 let affected_balance = self.active.saturating_add(unbonding_affected_balance);
464 let ratio = Perquintill::from_rational_with_rounding(
465 slash_amount,
466 affected_balance,
467 Rounding::Up,
468 )
469 .unwrap_or_else(|_| Perquintill::one());
470 (
471 Some(ratio),
472 affected_indices.chain((0..first_slashable_index).rev()).collect::<Vec<_>>(),
473 )
474 } else {
475 (None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
477 }
478 };
479
480 log!(
482 trace,
483 "slashing {:?} for offence era {:?} out of {:?}, priority: {:?}, proportional = {:?}",
484 slash_amount,
485 offence_era,
486 self,
487 slash_chunks_priority,
488 maybe_proportional,
489 );
490
491 let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
492 let mut slash_from_target = if let Some(ratio) = maybe_proportional {
493 ratio.mul_ceil(*target)
494 } else {
495 *slash_remaining
496 }
497 .min(*target)
500 .min(*slash_remaining);
503
504 *target = *target - slash_from_target;
506 if *target < minimum_balance {
507 slash_from_target =
511 core::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
512 }
513
514 self.total = self.total.saturating_sub(slash_from_target);
515 *slash_remaining = slash_remaining.saturating_sub(slash_from_target);
516 };
517
518 slash_out_of(&mut self.active, &mut remaining_slash);
520
521 let mut slashed_unlocking = BTreeMap::<_, _>::new();
522 for i in slash_chunks_priority {
523 if remaining_slash.is_zero() {
524 break;
525 }
526
527 if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
528 slash_out_of(&mut chunk.value, &mut remaining_slash);
529 slashed_unlocking.insert(chunk.era, chunk.value);
531 } else {
532 break;
533 }
534 }
535
536 self.unlocking.retain(|c| !c.value.is_zero());
538
539 let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
540 T::EventListeners::on_slash(
541 &self.stash,
542 self.active,
543 &slashed_unlocking,
544 final_slashed_amount,
545 );
546 final_slashed_amount
547 }
548}
549
550#[derive(PartialEq, Debug)]
552pub(crate) enum LedgerIntegrityState {
553 Ok,
555 Corrupted,
558 CorruptedKilled,
560 LockCorrupted,
562}
563
564#[cfg(test)]
567#[derive(frame_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
568pub struct StakingLedgerInspect<T: Config> {
569 pub stash: T::AccountId,
570 #[codec(compact)]
571 pub total: BalanceOf<T>,
572 #[codec(compact)]
573 pub active: BalanceOf<T>,
574 pub unlocking:
575 frame_support::BoundedVec<crate::UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
576}
577
578#[cfg(test)]
579impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> {
580 fn eq(&self, other: &StakingLedgerInspect<T>) -> bool {
581 self.stash == other.stash &&
582 self.total == other.total &&
583 self.active == other.active &&
584 self.unlocking == other.unlocking
585 }
586}
587
588#[cfg(test)]
589impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}