1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! A staking pallet based on pools of shares.
18
//!
19
//! This pallet works with pools inspired by AMM liquidity pools to easily distribute
20
//! rewards with support for both non-compounding and compounding rewards.
21
//!
22
//! Each candidate internally have 3 pools:
23
//! - a pool for all delegators willing to auto compound.
24
//! - a pool for all delegators not willing to auto compound.
25
//! - a pool for all delegators that are in the process of removing stake.
26
//!
27
//! When delegating the funds of the delegator are reserved, and shares allow to easily
28
//! distribute auto compounding rewards (by simply increasing the total shared amount)
29
//! and easily slash (each share loose part of its value). Rewards are distributed to an account
30
//! id dedicated to the staking pallet, and delegators can call an extrinsic to transfer their rewards
31
//! to their own account (but as reserved). Keeping funds reserved in user accounts allow them to
32
//! participate in other processes such as gouvernance.
33

            
34
#![cfg_attr(not(feature = "std"), no_std)]
35
extern crate alloc;
36

            
37
mod calls;
38
mod candidate;
39
mod pools;
40
pub mod traits;
41

            
42
#[cfg(test)]
43
mod mock;
44

            
45
#[cfg(test)]
46
mod tests;
47

            
48
#[cfg(feature = "runtime-benchmarks")]
49
mod benchmarking;
50

            
51
#[cfg(any(feature = "migrations", feature = "try-runtime"))]
52
pub mod migrations;
53

            
54
pub mod weights;
55
use frame_support::pallet;
56
pub use weights::WeightInfo;
57

            
58
pub use {
59
    candidate::EligibleCandidate,
60
    pallet::*,
61
    pools::{ActivePoolKind, CandidateSummary, DelegatorCandidateSummary, PoolKind},
62
};
63

            
64
#[cfg(feature = "runtime-benchmarks")]
65
use frame_support::traits::fungible::Mutate;
66
60528
#[pallet]
67
pub mod pallet {
68
    use {
69
        super::*,
70
        crate::{
71
            traits::{IsCandidateEligible, Timer},
72
            weights::WeightInfo,
73
        },
74
        alloc::vec::Vec,
75
        calls::Calls,
76
        core::marker::PhantomData,
77
        frame_support::{
78
            pallet_prelude::*,
79
            storage::types::{StorageDoubleMap, StorageValue, ValueQuery},
80
            traits::{fungible, tokens::Balance, IsType},
81
            Blake2_128Concat,
82
        },
83
        frame_system::pallet_prelude::*,
84
        parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, FullCodec},
85
        scale_info::TypeInfo,
86
        serde::{Deserialize, Serialize},
87
        sp_core::Get,
88
        sp_runtime::{BoundedVec, Perbill},
89
        tp_maths::MulDiv,
90
    };
91

            
92
    /// A reason for this pallet placing a hold on funds.
93
    #[pallet::composite_enum]
94
    pub enum HoldReason {
95
751
        PooledStake,
96
    }
97

            
98
    // Type aliases for better readability.
99
    pub type Candidate<T> = <T as frame_system::Config>::AccountId;
100
    pub type CreditOf<T> =
101
        fungible::Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
102
    pub type Delegator<T> = <T as frame_system::Config>::AccountId;
103

            
104
    /// Key used by the `Pools` StorageDoubleMap, avoiding lots of maps.
105
    /// StorageDoubleMap first key is the account id of the candidate.
106
    #[derive(
107
        RuntimeDebug,
108
        PartialEq,
109
        Eq,
110
        Encode,
111
        Decode,
112
        Clone,
113
11856
        TypeInfo,
114
        Serialize,
115
        Deserialize,
116
        MaxEncodedLen,
117
    )]
118
    pub enum PoolsKey<A: FullCodec> {
119
        /// Total amount of currency backing this candidate across all pools.
120
        CandidateTotalStake,
121

            
122
        /// Amount of joining shares a delegator have for that candidate.
123
        JoiningShares { delegator: A },
124
        /// Total amount of joining shares existing for that candidate.
125
        JoiningSharesSupply,
126
        /// Amount of currency backing all the joining shares of that candidate.
127
        JoiningSharesTotalStaked,
128
        /// Amount of currency held in the delegator account.
129
        JoiningSharesHeldStake { delegator: A },
130

            
131
        /// Amount of auto compounding shares a delegator have for that candidate.
132
        AutoCompoundingShares { delegator: A },
133
        /// Total amount of auto compounding shares existing for that candidate.
134
        AutoCompoundingSharesSupply,
135
        /// Amount of currency backing all the auto compounding shares of that candidate.
136
        AutoCompoundingSharesTotalStaked,
137
        /// Amount of currency held in the delegator account.
138
        AutoCompoundingSharesHeldStake { delegator: A },
139

            
140
        /// Amount of manual rewards shares a delegator have for that candidate.
141
        ManualRewardsShares { delegator: A },
142
        /// Total amount of manual rewards shares existing for that candidate.
143
        ManualRewardsSharesSupply,
144
        /// Amount of currency backing all the manual rewards shares of that candidate.
145
        ManualRewardsSharesTotalStaked,
146
        /// Amount of currency held in the delegator account.
147
        ManualRewardsSharesHeldStake { delegator: A },
148
        /// Counter of the cumulated rewards per share generated by that candidate since genesis.
149
        /// Is safe to wrap around the maximum value of the balance type.
150
        ManualRewardsCounter,
151
        /// Value of the counter at the last time the delegator claimed its rewards or changed its amount of shares
152
        /// (changing the amount of shares automatically claims pending rewards).
153
        /// The difference between the checkpoint and the counter is the amount of claimable reward per share for
154
        /// that delegator.
155
        ManualRewardsCheckpoint { delegator: A },
156

            
157
        /// Amount of shares of that delegator in the leaving pool of that candidate.
158
        /// When leaving delegating funds are placed in the leaving pool until the leaving period is elapsed.
159
        /// While in the leaving pool the funds are still slashable.
160
        LeavingShares { delegator: A },
161
        /// Total amount of leaving shares existing for that candidate.
162
        LeavingSharesSupply,
163
        /// Amount of currency backing all the leaving shares of that candidate.
164
        LeavingSharesTotalStaked,
165
        /// Amount of currency held in the delegator account.
166
        LeavingSharesHeldStake { delegator: A },
167
    }
168

            
169
    /// Key used by the "PendingOperations" StorageDoubleMap.
170
    /// StorageDoubleMap first key is the account id of the delegator who made the request.
171
    /// Value is the amount of shares in the joining/leaving pool.
172

            
173
    #[derive(
174
        RuntimeDebug,
175
        PartialEq,
176
        Eq,
177
        Encode,
178
        Decode,
179
        Clone,
180
3744
        TypeInfo,
181
        Serialize,
182
        Deserialize,
183
        MaxEncodedLen,
184
        DecodeWithMemTracking,
185
    )]
186
    #[allow(clippy::multiple_bound_locations)]
187
    pub enum PendingOperationKey<A: FullCodec, J: FullCodec, L: FullCodec> {
188
        /// Candidate requested to join the auto compounding pool of a candidate.
189
        JoiningAutoCompounding { candidate: A, at: J },
190
        /// Candidate requested to join the manual rewards pool of a candidate.
191
        JoiningManualRewards { candidate: A, at: J },
192
        /// Candidate requested to to leave a pool of a candidate.
193
        Leaving { candidate: A, at: L },
194
    }
195

            
196
    pub type PendingOperationKeyOf<T> = PendingOperationKey<
197
        <T as frame_system::Config>::AccountId,
198
        <<T as Config>::JoiningRequestTimer as Timer>::Instant,
199
        <<T as Config>::LeavingRequestTimer as Timer>::Instant,
200
    >;
201

            
202
    #[derive(
203
        RuntimeDebug,
204
        PartialEq,
205
        Eq,
206
        Encode,
207
        Decode,
208
        Clone,
209
1248
        TypeInfo,
210
        Serialize,
211
        Deserialize,
212
        DecodeWithMemTracking,
213
    )]
214
    #[allow(clippy::multiple_bound_locations)]
215
    pub struct PendingOperationQuery<A: FullCodec, J: FullCodec, L: FullCodec> {
216
        pub delegator: A,
217
        pub operation: PendingOperationKey<A, J, L>,
218
    }
219

            
220
    pub type PendingOperationQueryOf<T> = PendingOperationQuery<
221
        <T as frame_system::Config>::AccountId,
222
        <<T as Config>::JoiningRequestTimer as Timer>::Instant,
223
        <<T as Config>::LeavingRequestTimer as Timer>::Instant,
224
    >;
225

            
226
    /// Allow calls to be performed using either share amounts or stake.
227
    /// When providing stake, calls will convert them into share amounts that are
228
    /// worth up to the provided stake. The amount of stake thus will be at most the provided
229
    /// amount.
230
    #[derive(
231
        RuntimeDebug,
232
        PartialEq,
233
        Eq,
234
        Encode,
235
        Decode,
236
        Clone,
237
1248
        TypeInfo,
238
        Serialize,
239
        Deserialize,
240
        DecodeWithMemTracking,
241
    )]
242
    pub enum SharesOrStake<T> {
243
        Shares(T),
244
        Stake(T),
245
    }
246

            
247
    /// Wrapper type for an amount of shares.
248
    #[derive(
249
        RuntimeDebug,
250
        Default,
251
        PartialEq,
252
        Eq,
253
        Encode,
254
        Decode,
255
        Copy,
256
        Clone,
257
        TypeInfo,
258
        Serialize,
259
        Deserialize,
260
    )]
261
    pub struct Shares<T>(pub T);
262

            
263
    /// Wrapper type for an amount of staked currency.
264
    #[derive(
265
        RuntimeDebug,
266
        Default,
267
        PartialEq,
268
        Eq,
269
        Encode,
270
        Decode,
271
        Copy,
272
        Clone,
273
        TypeInfo,
274
        Serialize,
275
        Deserialize,
276
    )]
277
    pub struct Stake<T>(pub T);
278

            
279
    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
280

            
281
    /// Pooled Staking pallet.
282
2818
    #[pallet::pallet]
283
    #[pallet::storage_version(STORAGE_VERSION)]
284
    pub struct Pallet<T>(PhantomData<T>);
285

            
286
    #[pallet::config]
287
    pub trait Config: frame_system::Config {
288
        /// Overarching event type
289
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
290
        /// The currency type.
291
        /// Shares will use the same Balance type.
292
        type Currency: fungible::Inspect<Self::AccountId, Balance = Self::Balance>
293
            + fungible::Mutate<Self::AccountId>
294
            + fungible::Balanced<Self::AccountId>
295
            + fungible::MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>;
296

            
297
        /// Same as Currency::Balance. Must impl `MulDiv` which perform
298
        /// multiplication followed by division using a bigger type to avoid
299
        /// overflows.
300
        type Balance: Balance + MulDiv;
301

            
302
        /// Account holding Currency of all delegators.
303
        #[pallet::constant]
304
        type StakingAccount: Get<Self::AccountId>;
305

            
306
        /// When creating the first Shares for a candidate the supply can be arbitrary.
307
        /// Picking a value too low will make an higher supply, which means each share will get
308
        /// less rewards, and rewards calculations will have more impactful rounding errors.
309
        /// Picking a value too high is a barrier of entry for staking.
310
        #[pallet::constant]
311
        type InitialManualClaimShareValue: Get<Self::Balance>;
312
        /// When creating the first Shares for a candidate the supply can arbitrary.
313
        /// Picking a value too high is a barrier of entry for staking, which will increase overtime
314
        /// as the value of each share will increase due to auto compounding.
315
        #[pallet::constant]
316
        type InitialAutoCompoundingShareValue: Get<Self::Balance>;
317

            
318
        /// Minimum amount of stake a Candidate must delegate (stake) towards itself. Not reaching
319
        /// this minimum prevents from being elected.
320
        #[pallet::constant]
321
        type MinimumSelfDelegation: Get<Self::Balance>;
322
        /// Part of the rewards that will be sent exclusively to the collator.
323
        #[pallet::constant]
324
        type RewardsCollatorCommission: Get<Perbill>;
325

            
326
        /// The overarching runtime hold reason.
327
        type RuntimeHoldReason: From<HoldReason>;
328

            
329
        /// Condition for when a joining request can be executed.
330
        type JoiningRequestTimer: Timer;
331
        /// Condition for when a leaving request can be executed.
332
        type LeavingRequestTimer: Timer;
333
        /// All eligible candidates are stored in a sorted list that is modified each time
334
        /// delegations changes. It is safer to bound this list, in which case eligible candidate
335
        /// could fall out of this list if they have less stake than the top `EligibleCandidatesBufferSize`
336
        /// eligible candidates. One of this top candidates leaving will then not bring the dropped candidate
337
        /// in the list. An extrinsic is available to manually bring back such dropped candidate.
338
        #[pallet::constant]
339
        type EligibleCandidatesBufferSize: Get<u32>;
340
        /// Additional filter for candidates to be eligible.
341
        type EligibleCandidatesFilter: IsCandidateEligible<Self::AccountId>;
342
        type WeightInfo: WeightInfo;
343
    }
344

            
345
    /// Keeps a list of all eligible candidates, sorted by the amount of stake backing them.
346
    /// This can be quickly updated using a binary search, and allow to easily take the top
347
    /// `MaxCollatorSetSize`.
348
12934
    #[pallet::storage]
349
    pub type SortedEligibleCandidates<T: Config> = StorageValue<
350
        _,
351
        BoundedVec<
352
            candidate::EligibleCandidate<Candidate<T>, T::Balance>,
353
            T::EligibleCandidatesBufferSize,
354
        >,
355
        ValueQuery,
356
    >;
357

            
358
    /// Pools balances.
359
18678
    #[pallet::storage]
360
    pub type Pools<T: Config> = StorageDoubleMap<
361
        _,
362
        Blake2_128Concat,
363
        Candidate<T>,
364
        Blake2_128Concat,
365
        PoolsKey<T::AccountId>,
366
        T::Balance,
367
        ValueQuery,
368
    >;
369

            
370
    /// Pending operations balances.
371
    /// Balances are expressed in joining/leaving shares amounts.
372
1804
    #[pallet::storage]
373
    pub type PendingOperations<T: Config> = StorageDoubleMap<
374
        _,
375
        Blake2_128Concat,
376
        Delegator<T>,
377
        Blake2_128Concat,
378
        PendingOperationKeyOf<T>,
379
        T::Balance,
380
        ValueQuery,
381
    >;
382

            
383
    /// Summary of a delegator's delegation.
384
    /// Used to quickly fetch all delegations of a delegator.
385
1410
    #[pallet::storage]
386
    pub type DelegatorCandidateSummaries<T: Config> = StorageDoubleMap<
387
        _,
388
        Blake2_128Concat,
389
        Delegator<T>,
390
        Blake2_128Concat,
391
        Candidate<T>,
392
        DelegatorCandidateSummary,
393
        ValueQuery,
394
    >;
395

            
396
    /// Summary of a candidate state.
397
1393
    #[pallet::storage]
398
    pub type CandidateSummaries<T: Config> =
399
        StorageMap<_, Blake2_128Concat, Candidate<T>, CandidateSummary, ValueQuery>;
400

            
401
    /// Pauses the ability to modify pools through extrinsics.
402
    ///
403
    /// Currently added only to run the multi-block migration to compute
404
    /// `DelegatorCandidateSummaries` and `CandidateSummaries`. It will NOT
405
    /// prevent to distribute rewards, which is fine as the reward distribution
406
    /// process doesn't alter the pools in a way that will mess with the migration.
407
1996
    #[pallet::storage]
408
    pub type PausePoolsExtrinsics<T: Config> = StorageValue<_, bool, ValueQuery>;
409

            
410
624
    #[pallet::event]
411
3596
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
412
    pub enum Event<T: Config> {
413
        /// Stake of the candidate has changed, which may have modified its
414
        /// position in the eligible candidates list.
415
        UpdatedCandidatePosition {
416
            candidate: Candidate<T>,
417
            stake: T::Balance,
418
            self_delegation: T::Balance,
419
            before: Option<u32>,
420
            after: Option<u32>,
421
        },
422

            
423
        /// User requested to delegate towards a candidate.
424
        RequestedDelegate {
425
            candidate: Candidate<T>,
426
            delegator: Delegator<T>,
427
            pool: ActivePoolKind,
428
            pending: T::Balance,
429
        },
430
        /// Delegation request was executed. `staked` has been properly staked
431
        /// in `pool`, while the rounding when converting to shares has been
432
        /// `released`.
433
        ExecutedDelegate {
434
            candidate: Candidate<T>,
435
            delegator: Delegator<T>,
436
            pool: ActivePoolKind,
437
            staked: T::Balance,
438
            released: T::Balance,
439
        },
440
        /// User requested to undelegate from a candidate.
441
        /// Stake was removed from a `pool` and is `pending` for the request
442
        /// to be executed. The rounding when converting to leaving shares has
443
        /// been `released` immediately.
444
        RequestedUndelegate {
445
            candidate: Candidate<T>,
446
            delegator: Delegator<T>,
447
            from: ActivePoolKind,
448
            pending: T::Balance,
449
            released: T::Balance,
450
        },
451
        /// Undelegation request was executed.
452
        ExecutedUndelegate {
453
            candidate: Candidate<T>,
454
            delegator: Delegator<T>,
455
            released: T::Balance,
456
        },
457

            
458
        /// Stake of that Candidate increased.
459
        IncreasedStake {
460
            candidate: Candidate<T>,
461
            stake_diff: T::Balance,
462
        },
463
        /// Stake of that Candidate decreased.
464
        DecreasedStake {
465
            candidate: Candidate<T>,
466
            stake_diff: T::Balance,
467
        },
468
        /// Delegator staked towards a Candidate for AutoCompounding Shares.
469
        StakedAutoCompounding {
470
            candidate: Candidate<T>,
471
            delegator: Delegator<T>,
472
            shares: T::Balance,
473
            stake: T::Balance,
474
        },
475
        /// Delegator unstaked towards a candidate with AutoCompounding Shares.
476
        UnstakedAutoCompounding {
477
            candidate: Candidate<T>,
478
            delegator: Delegator<T>,
479
            shares: T::Balance,
480
            stake: T::Balance,
481
        },
482
        /// Delegator staked towards a candidate for ManualRewards Shares.
483
        StakedManualRewards {
484
            candidate: Candidate<T>,
485
            delegator: Delegator<T>,
486
            shares: T::Balance,
487
            stake: T::Balance,
488
        },
489
        /// Delegator unstaked towards a candidate with ManualRewards Shares.
490
        UnstakedManualRewards {
491
            candidate: Candidate<T>,
492
            delegator: Delegator<T>,
493
            shares: T::Balance,
494
            stake: T::Balance,
495
        },
496
        /// Collator has been rewarded.
497
        RewardedCollator {
498
            collator: Candidate<T>,
499
            auto_compounding_rewards: T::Balance,
500
            manual_claim_rewards: T::Balance,
501
        },
502
        /// Delegators have been rewarded.
503
        RewardedDelegators {
504
            collator: Candidate<T>,
505
            auto_compounding_rewards: T::Balance,
506
            manual_claim_rewards: T::Balance,
507
        },
508
        /// Rewards manually claimed.
509
        ClaimedManualRewards {
510
            candidate: Candidate<T>,
511
            delegator: Delegator<T>,
512
            rewards: T::Balance,
513
        },
514
        /// Swapped between AutoCompounding and ManualReward shares
515
        SwappedPool {
516
            candidate: Candidate<T>,
517
            delegator: Delegator<T>,
518
            source_pool: ActivePoolKind,
519
            source_shares: T::Balance,
520
            source_stake: T::Balance,
521
            target_shares: T::Balance,
522
            target_stake: T::Balance,
523
            pending_leaving: T::Balance,
524
            released: T::Balance,
525
        },
526
    }
527

            
528
624
    #[pallet::error]
529
    pub enum Error<T> {
530
        InvalidPalletSetting,
531
        DisabledFeature,
532
        NoOneIsStaking,
533
        StakeMustBeNonZero,
534
        RewardsMustBeNonZero,
535
        MathUnderflow,
536
        MathOverflow,
537
        NotEnoughShares,
538
        TryingToLeaveTooSoon,
539
        InconsistentState,
540
        UnsufficientSharesForTransfer,
541
        CandidateTransferingOwnSharesForbidden,
542
        RequestCannotBeExecuted(u16),
543
        SwapResultsInZeroShares,
544
        PoolsExtrinsicsArePaused,
545
    }
546

            
547
    impl<T: Config> From<tp_maths::OverflowError> for Error<T> {
548
        fn from(_: tp_maths::OverflowError) -> Self {
549
            Error::MathOverflow
550
        }
551
    }
552

            
553
    impl<T: Config> From<tp_maths::UnderflowError> for Error<T> {
554
5
        fn from(_: tp_maths::UnderflowError) -> Self {
555
5
            Error::MathUnderflow
556
5
        }
557
    }
558

            
559
66622
    #[pallet::hooks]
560
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
561
        #[cfg(feature = "try-runtime")]
562
        fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
563
            use alloc::collections::btree_set::BTreeSet;
564
            use frame_support::storage_alias;
565

            
566
            let mut all_candidates = BTreeSet::new();
567
            for (candidate, _k2) in Pools::<T>::iter_keys() {
568
                all_candidates.insert(candidate);
569
            }
570

            
571
            for candidate in all_candidates {
572
                pools::check_candidate_consistency::<T>(&candidate)?;
573
            }
574

            
575
            // Sorted storage items are sorted
576
            fn assert_is_sorted_and_unique<T: Ord>(x: &[T], name: &str) {
577
                assert!(
578
                    x.windows(2).all(|w| w[0] < w[1]),
579
                    "sorted list not sorted or not unique: {}",
580
                    name,
581
                );
582
            }
583
            assert_is_sorted_and_unique(
584
                &SortedEligibleCandidates::<T>::get(),
585
                "SortedEligibleCandidates",
586
            );
587

            
588
            if Pallet::<T>::on_chain_storage_version() < 1 {
589
                return Ok(());
590
            }
591

            
592
            // Summaries updated dynamically matches summaries generated entirely from shares.
593
            #[storage_alias(pallet_name)]
594
            pub type TryStateDelegatorSummaries<T: Config> = StorageDoubleMap<
595
                Pallet<T>,
596
                Blake2_128Concat,
597
                Delegator<T>,
598
                Blake2_128Concat,
599
                Candidate<T>,
600
                DelegatorCandidateSummary,
601
                ValueQuery,
602
            >;
603

            
604
            #[storage_alias(pallet_name)]
605
            pub type TryStateCandidateSummaries<T: Config> =
606
                StorageMap<Pallet<T>, Blake2_128Concat, Candidate<T>, CandidateSummary, ValueQuery>;
607

            
608
            assert!(
609
                migrations::stepped_generate_summaries::<
610
                    T,
611
                    TryStateDelegatorSummaries<T>,
612
                    TryStateCandidateSummaries<T>,
613
                >(
614
                    None,
615
                    &mut frame_support::weights::WeightMeter::new(), // call with no limit on weight
616
                )
617
                .expect("to generate summaries without errors")
618
                .is_none(),
619
                "failed to generate all summaries"
620
            );
621

            
622
            let mut candidate_summaries_count = 0;
623
            for (candidate, summary) in TryStateCandidateSummaries::<T>::iter() {
624
                candidate_summaries_count += 1;
625
                assert_eq!(
626
                    CandidateSummaries::<T>::get(&candidate),
627
                    summary,
628
                    "candidate summary for {candidate:?} didn't match"
629
                );
630
            }
631
            assert_eq!(
632
                candidate_summaries_count,
633
                CandidateSummaries::<T>::iter().count(),
634
                "count of candidate summaries didn't match"
635
            );
636

            
637
            let mut delegator_summaries_count = 0;
638
            for (delegator, candidate, summary) in TryStateDelegatorSummaries::<T>::iter() {
639
                delegator_summaries_count += 1;
640
                assert_eq!(
641
                    DelegatorCandidateSummaries::<T>::get(&delegator, &candidate),
642
                    summary,
643
                    "delegator summary for {delegator:?} to {candidate:?} didn't match"
644
                );
645
            }
646
            assert_eq!(
647
                delegator_summaries_count,
648
                DelegatorCandidateSummaries::<T>::iter().count(),
649
                "count of delegator summaries didn't match"
650
            );
651

            
652
            Ok(())
653
        }
654
    }
655

            
656
624
    #[pallet::call]
657
    impl<T: Config> Pallet<T> {
658
        #[pallet::call_index(0)]
659
        #[pallet::weight(T::WeightInfo::rebalance_hold())]
660
        #[allow(clippy::useless_conversion)]
661
        pub fn rebalance_hold(
662
            origin: OriginFor<T>,
663
            candidate: Candidate<T>,
664
            delegator: Delegator<T>,
665
            pool: PoolKind,
666
7
        ) -> DispatchResultWithPostInfo {
667
7
            // We don't care about the sender.
668
7
            let _ = ensure_signed(origin)?;
669

            
670
7
            Calls::<T>::rebalance_hold(candidate, delegator, pool)
671
        }
672

            
673
        #[pallet::call_index(1)]
674
        #[pallet::weight(T::WeightInfo::request_delegate())]
675
        #[allow(clippy::useless_conversion)]
676
        pub fn request_delegate(
677
            origin: OriginFor<T>,
678
            candidate: Candidate<T>,
679
            pool: ActivePoolKind,
680
            stake: T::Balance,
681
277
        ) -> DispatchResultWithPostInfo {
682
277
            let delegator = ensure_signed(origin)?;
683

            
684
274
            Calls::<T>::request_delegate(candidate, delegator, pool, stake)
685
        }
686

            
687
        /// Execute pending operations can incur in claim manual rewards per operation, we simply add the worst case
688
        #[pallet::call_index(2)]
689
        #[pallet::weight(T::WeightInfo::execute_pending_operations(operations.len() as u32).saturating_add(T::WeightInfo::claim_manual_rewards(operations.len() as u32)))]
690
        #[allow(clippy::useless_conversion)]
691
        pub fn execute_pending_operations(
692
            origin: OriginFor<T>,
693
            operations: Vec<PendingOperationQueryOf<T>>,
694
175
        ) -> DispatchResultWithPostInfo {
695
175
            // We don't care about the sender.
696
175
            let _ = ensure_signed(origin)?;
697

            
698
169
            Calls::<T>::execute_pending_operations(operations)
699
        }
700

            
701
        /// Request undelegate can incur in either claim manual rewards or hold rebalances, we simply add the worst case
702
        #[pallet::call_index(3)]
703
        #[pallet::weight(T::WeightInfo::request_undelegate().saturating_add(T::WeightInfo::claim_manual_rewards(1).max(T::WeightInfo::rebalance_hold())))]
704
        #[allow(clippy::useless_conversion)]
705
        pub fn request_undelegate(
706
            origin: OriginFor<T>,
707
            candidate: Candidate<T>,
708
            pool: ActivePoolKind,
709
            amount: SharesOrStake<T::Balance>,
710
49
        ) -> DispatchResultWithPostInfo {
711
49
            let delegator = ensure_signed(origin)?;
712

            
713
46
            Calls::<T>::request_undelegate(candidate, delegator, pool, amount)
714
        }
715

            
716
        #[pallet::call_index(4)]
717
        #[pallet::weight(T::WeightInfo::claim_manual_rewards(pairs.len() as u32))]
718
        #[allow(clippy::useless_conversion)]
719
        pub fn claim_manual_rewards(
720
            origin: OriginFor<T>,
721
            pairs: Vec<(Candidate<T>, Delegator<T>)>,
722
1
        ) -> DispatchResultWithPostInfo {
723
1
            // We don't care about the sender.
724
1
            let _ = ensure_signed(origin)?;
725

            
726
1
            Calls::<T>::claim_manual_rewards(&pairs)
727
        }
728

            
729
        #[pallet::call_index(5)]
730
        #[pallet::weight(T::WeightInfo::update_candidate_position(candidates.len() as u32))]
731
        #[allow(clippy::useless_conversion)]
732
        pub fn update_candidate_position(
733
            origin: OriginFor<T>,
734
            candidates: Vec<Candidate<T>>,
735
5
        ) -> DispatchResultWithPostInfo {
736
5
            // We don't care about the sender.
737
5
            let _ = ensure_signed(origin)?;
738

            
739
5
            Calls::<T>::update_candidate_position(&candidates)
740
        }
741

            
742
        #[pallet::call_index(6)]
743
        #[pallet::weight(T::WeightInfo::swap_pool())]
744
        #[allow(clippy::useless_conversion)]
745
        pub fn swap_pool(
746
            origin: OriginFor<T>,
747
            candidate: Candidate<T>,
748
            source_pool: ActivePoolKind,
749
            amount: SharesOrStake<T::Balance>,
750
23
        ) -> DispatchResultWithPostInfo {
751
23
            let delegator = ensure_signed(origin)?;
752

            
753
23
            Calls::<T>::swap_pool(candidate, delegator, source_pool, amount)
754
        }
755
    }
756

            
757
    impl<T: Config> Pallet<T> {
758
12
        pub fn computed_stake(
759
12
            candidate: Candidate<T>,
760
12
            delegator: Delegator<T>,
761
12
            pool: PoolKind,
762
12
        ) -> Option<T::Balance> {
763
            use pools::Pool;
764
12
            match pool {
765
                PoolKind::Joining => pools::Joining::<T>::computed_stake(&candidate, &delegator),
766
                PoolKind::AutoCompounding => {
767
6
                    pools::AutoCompounding::<T>::computed_stake(&candidate, &delegator)
768
                }
769
                PoolKind::ManualRewards => {
770
6
                    pools::ManualRewards::<T>::computed_stake(&candidate, &delegator)
771
                }
772
                PoolKind::Leaving => pools::Leaving::<T>::computed_stake(&candidate, &delegator),
773
            }
774
12
            .ok()
775
12
            .map(|x| x.0)
776
12
        }
777
    }
778

            
779
    impl<T: Config> tp_traits::DistributeRewards<Candidate<T>, CreditOf<T>> for Pallet<T> {
780
1099
        fn distribute_rewards(
781
1099
            candidate: Candidate<T>,
782
1099
            rewards: CreditOf<T>,
783
1099
        ) -> DispatchResultWithPostInfo {
784
1099
            pools::distribute_rewards::<T>(&candidate, rewards)
785
1099
        }
786
    }
787
    impl<T: Config> tp_traits::StakingCandidateHelper<Candidate<T>> for Pallet<T> {
788
12
        fn is_candidate_selected(candidate: &Candidate<T>) -> bool {
789
12
            <SortedEligibleCandidates<T>>::get()
790
12
                .into_iter()
791
12
                .any(|c| &c.candidate == candidate)
792
12
        }
793
15
        fn on_online_status_change(
794
15
            candidate: &Candidate<T>,
795
15
            _is_online: bool,
796
15
        ) -> DispatchResultWithPostInfo {
797
15
            Calls::<T>::update_candidate_position(&[candidate.clone()])
798
15
        }
799

            
800
        #[cfg(feature = "runtime-benchmarks")]
801
        fn make_collator_eligible_candidate(collator: &Candidate<T>) {
802
            use alloc::vec;
803
            let minimum_stake = T::MinimumSelfDelegation::get();
804
            T::Currency::set_balance(collator, minimum_stake + minimum_stake);
805
            T::EligibleCandidatesFilter::make_candidate_eligible(collator, true);
806
            Calls::<T>::request_delegate(
807
                collator.clone(),
808
                collator.clone(),
809
                ActivePoolKind::AutoCompounding,
810
                minimum_stake,
811
            )
812
            .expect("request_delegate should not fail in benchmarks");
813
            let timer = T::JoiningRequestTimer::now();
814
            T::JoiningRequestTimer::skip_to_elapsed();
815
            Calls::<T>::execute_pending_operations(vec![PendingOperationQuery {
816
                delegator: collator.clone(),
817
                operation: PendingOperationKey::JoiningAutoCompounding {
818
                    candidate: collator.clone(),
819
                    at: timer.clone(),
820
                },
821
            }])
822
            .expect("execute_pending_operations should not fail in benchmarks");
823
        }
824
    }
825
}