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 pub(crate) fn is_bonded(account: StakingAccount<T::AccountId>) -> bool {
158 match account {
159 StakingAccount::Stash(stash) => <Bonded<T>>::contains_key(stash),
160 StakingAccount::Controller(controller) => <Ledger<T>>::contains_key(controller),
161 }
162 }
163
164 pub(crate) fn get(account: StakingAccount<T::AccountId>) -> Result<StakingLedger<T>, Error<T>> {
173 let (stash, controller) = match account {
174 StakingAccount::Stash(stash) =>
175 (stash.clone(), <Bonded<T>>::get(&stash).ok_or(Error::<T>::NotStash)?),
176 StakingAccount::Controller(controller) => (
177 Ledger::<T>::get(&controller)
178 .map(|l| l.stash)
179 .ok_or(Error::<T>::NotController)?,
180 controller,
181 ),
182 };
183
184 let ledger = <Ledger<T>>::get(&controller)
185 .map(|mut ledger| {
186 ledger.controller = Some(controller.clone());
187 ledger
188 })
189 .ok_or(Error::<T>::NotController)?;
190
191 ensure!(
197 Bonded::<T>::get(&stash) == Some(controller) && ledger.stash == stash,
198 Error::<T>::BadState
199 );
200
201 Ok(ledger)
202 }
203
204 pub(crate) fn reward_destination(
209 account: StakingAccount<T::AccountId>,
210 ) -> Option<RewardDestination<T::AccountId>> {
211 let stash = match account {
212 StakingAccount::Stash(stash) => Some(stash),
213 StakingAccount::Controller(controller) =>
214 Self::paired_account(StakingAccount::Controller(controller)),
215 };
216
217 if let Some(stash) = stash {
218 <Payee<T>>::get(stash)
219 } else {
220 defensive!("fetched reward destination from unbonded stash {}", stash);
221 None
222 }
223 }
224
225 pub fn controller(&self) -> Option<T::AccountId> {
232 self.controller.clone().or_else(|| {
233 defensive!("fetched a controller on a ledger instance without it.");
234 Self::paired_account(StakingAccount::Stash(self.stash.clone()))
235 })
236 }
237
238 pub(crate) fn update(self) -> Result<(), Error<T>> {
246 if !<Bonded<T>>::contains_key(&self.stash) {
247 return Err(Error::<T>::NotStash)
248 }
249
250 if !Pallet::<T>::is_virtual_staker(&self.stash) {
252 asset::update_stake::<T>(&self.stash, self.total)
254 .map_err(|_| Error::<T>::NotEnoughFunds)?;
255 }
256
257 Ledger::<T>::insert(
258 &self.controller().ok_or_else(|| {
259 defensive!("update called on a ledger that is not bonded.");
260 Error::<T>::NotController
261 })?,
262 &self,
263 );
264
265 Ok(())
266 }
267
268 pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
272 if <Bonded<T>>::contains_key(&self.stash) {
273 return Err(Error::<T>::AlreadyBonded)
274 }
275
276 <Payee<T>>::insert(&self.stash, payee);
277 <Bonded<T>>::insert(&self.stash, &self.stash);
278 self.update()
279 }
280
281 pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
283 if !<Bonded<T>>::contains_key(&self.stash) {
284 return Err(Error::<T>::NotStash)
285 }
286
287 <Payee<T>>::insert(&self.stash, payee);
288 Ok(())
289 }
290
291 pub(crate) fn set_controller_to_stash(self) -> Result<(), Error<T>> {
293 let controller = self.controller.as_ref()
294 .defensive_proof("Ledger's controller field didn't exist. The controller should have been fetched using StakingLedger.")
295 .ok_or(Error::<T>::NotController)?;
296
297 ensure!(self.stash != *controller, Error::<T>::AlreadyPaired);
298
299 if let Some(bonded_ledger) = Ledger::<T>::get(&self.stash) {
301 ensure!(bonded_ledger.stash == self.stash, Error::<T>::BadState);
306 }
307
308 <Ledger<T>>::remove(&controller);
309 <Ledger<T>>::insert(&self.stash, &self);
310 <Bonded<T>>::insert(&self.stash, &self.stash);
311
312 Ok(())
313 }
314
315 pub(crate) fn kill(stash: &T::AccountId) -> DispatchResult {
318 let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
319
320 <Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
321 Ledger::<T>::remove(controller);
322 <Bonded<T>>::remove(&stash);
323 <Payee<T>>::remove(&stash);
324
325 if <VirtualStakers<T>>::take(&ledger.stash).is_none() {
327 asset::kill_stake::<T>(&ledger.stash)?;
329 }
330 Pallet::<T>::deposit_event(crate::Event::<T>::StakerRemoved {
331 stash: ledger.stash.clone(),
332 });
333 Ok(())
334 })?
335 }
336
337 #[cfg(test)]
338 pub(crate) fn assert_stash_killed(stash: T::AccountId) {
339 assert!(!Ledger::<T>::contains_key(&stash));
340 assert!(!Bonded::<T>::contains_key(&stash));
341 assert!(!Payee::<T>::contains_key(&stash));
342 assert!(!VirtualStakers::<T>::contains_key(&stash));
343 }
344
345 pub(crate) fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
348 let mut total = self.total;
349 let unlocking: BoundedVec<_, _> = self
350 .unlocking
351 .into_iter()
352 .filter(|chunk| {
353 if chunk.era > current_era {
354 true
355 } else {
356 total = total.saturating_sub(chunk.value);
357 false
358 }
359 })
360 .collect::<Vec<_>>()
361 .try_into()
362 .expect(
363 "filtering items from a bounded vec always leaves length less than bounds. qed",
364 );
365
366 Self {
367 stash: self.stash,
368 total,
369 active: self.active,
370 unlocking,
371 controller: self.controller,
372 }
373 }
374
375 pub(crate) fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
379 let mut unlocking_balance = BalanceOf::<T>::zero();
380
381 while let Some(last) = self.unlocking.last_mut() {
382 if unlocking_balance.defensive_saturating_add(last.value) <= value {
383 unlocking_balance += last.value;
384 self.active += last.value;
385 self.unlocking.pop();
386 } else {
387 let diff = value.defensive_saturating_sub(unlocking_balance);
388
389 unlocking_balance += diff;
390 self.active += diff;
391 last.value -= diff;
392 }
393
394 if unlocking_balance >= value {
395 break
396 }
397 }
398
399 (self, unlocking_balance)
400 }
401
402 pub fn slash(
426 &mut self,
427 slash_amount: BalanceOf<T>,
428 minimum_balance: BalanceOf<T>,
429 slash_era: EraIndex,
430 ) -> BalanceOf<T> {
431 if slash_amount.is_zero() {
432 return Zero::zero()
433 }
434
435 use sp_runtime::PerThing as _;
436 let mut remaining_slash = slash_amount;
437 let pre_slash_total = self.total;
438
439 let slashable_chunks_start = slash_era.saturating_add(T::BondingDuration::get());
442
443 let (maybe_proportional, slash_chunks_priority) = {
446 if let Some(first_slashable_index) =
447 self.unlocking.iter().position(|c| c.era >= slashable_chunks_start)
448 {
449 let affected_indices = first_slashable_index..self.unlocking.len();
456 let unbonding_affected_balance =
457 affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
458 if let Some(chunk) = self.unlocking.get(i).defensive() {
459 sum.saturating_add(chunk.value)
460 } else {
461 sum
462 }
463 });
464 let affected_balance = self.active.saturating_add(unbonding_affected_balance);
465 let ratio = Perquintill::from_rational_with_rounding(
466 slash_amount,
467 affected_balance,
468 Rounding::Up,
469 )
470 .unwrap_or_else(|_| Perquintill::one());
471 (
472 Some(ratio),
473 affected_indices.chain((0..first_slashable_index).rev()).collect::<Vec<_>>(),
474 )
475 } else {
476 (None, (0..self.unlocking.len()).rev().collect::<Vec<_>>())
478 }
479 };
480
481 log!(
483 trace,
484 "slashing {:?} for era {:?} out of {:?}, priority: {:?}, proportional = {:?}",
485 slash_amount,
486 slash_era,
487 self,
488 slash_chunks_priority,
489 maybe_proportional,
490 );
491
492 let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
493 let mut slash_from_target = if let Some(ratio) = maybe_proportional {
494 ratio.mul_ceil(*target)
495 } else {
496 *slash_remaining
497 }
498 .min(*target)
501 .min(*slash_remaining);
504
505 *target = *target - slash_from_target;
507 if *target < minimum_balance {
508 slash_from_target =
512 core::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
513 }
514
515 self.total = self.total.saturating_sub(slash_from_target);
516 *slash_remaining = slash_remaining.saturating_sub(slash_from_target);
517 };
518
519 slash_out_of(&mut self.active, &mut remaining_slash);
521
522 let mut slashed_unlocking = BTreeMap::<_, _>::new();
523 for i in slash_chunks_priority {
524 if remaining_slash.is_zero() {
525 break
526 }
527
528 if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
529 slash_out_of(&mut chunk.value, &mut remaining_slash);
530 slashed_unlocking.insert(chunk.era, chunk.value);
532 } else {
533 break
534 }
535 }
536
537 self.unlocking.retain(|c| !c.value.is_zero());
539
540 let final_slashed_amount = pre_slash_total.saturating_sub(self.total);
541 T::EventListeners::on_slash(
542 &self.stash,
543 self.active,
544 &slashed_unlocking,
545 final_slashed_amount,
546 );
547 final_slashed_amount
548 }
549}
550
551#[derive(PartialEq, Debug)]
553pub(crate) enum LedgerIntegrityState {
554 Ok,
556 Corrupted,
559 CorruptedKilled,
561 LockCorrupted,
563}
564
565#[cfg(test)]
568#[derive(frame_support::DebugNoBound, Clone, Encode, Decode, TypeInfo, MaxEncodedLen)]
569pub struct StakingLedgerInspect<T: Config> {
570 pub stash: T::AccountId,
571 #[codec(compact)]
572 pub total: BalanceOf<T>,
573 #[codec(compact)]
574 pub active: BalanceOf<T>,
575 pub unlocking:
576 frame_support::BoundedVec<crate::UnlockChunk<BalanceOf<T>>, T::MaxUnlockingChunks>,
577}
578
579#[cfg(test)]
580impl<T: Config> PartialEq<StakingLedgerInspect<T>> for StakingLedger<T> {
581 fn eq(&self, other: &StakingLedgerInspect<T>) -> bool {
582 self.stash == other.stash &&
583 self.total == other.total &&
584 self.active == other.active &&
585 self.unlocking == other.unlocking
586 }
587}
588
589#[cfg(test)]
590impl<T: Config> codec::EncodeLike<StakingLedger<T>> for StakingLedgerInspect<T> {}