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
#[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},
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
        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
        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
        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
        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
        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
    #[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
        /// The currency type.
289
        /// Shares will use the same Balance type.
290
        type Currency: fungible::Inspect<Self::AccountId, Balance = Self::Balance>
291
            + fungible::Mutate<Self::AccountId>
292
            + fungible::Balanced<Self::AccountId>
293
            + fungible::MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>;
294

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
650
            Ok(())
651
        }
652
    }
653

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

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

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

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

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

            
696
121
            Calls::<T>::execute_pending_operations(operations)
697
        }
698

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

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

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

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

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

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

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

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

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

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

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