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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
651
            Ok(())
652
        }
653
    }
654

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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