-
Notifications
You must be signed in to change notification settings - Fork 785
/
Copy pathlib.rs
4094 lines (3689 loc) · 153 KB
/
lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Nomination Pools for Staking Delegation
//!
//! A pallet that allows members to delegate their stake to nominating pools. A nomination pool acts
//! as nominator and nominates validators on the members behalf.
//!
//! # Index
//!
//! * [Key terms](#key-terms)
//! * [Usage](#usage)
//! * [Implementor's Guide](#implementors-guide)
//! * [Design](#design)
//!
//! ## Key Terms
//!
//! * pool id: A unique identifier of each pool. Set to u32.
//! * bonded pool: Tracks the distribution of actively staked funds. See [`BondedPool`] and
//! [`BondedPoolInner`].
//! * reward pool: Tracks rewards earned by actively staked funds. See [`RewardPool`] and
//! [`RewardPools`].
//! * unbonding sub pools: Collection of pools at different phases of the unbonding lifecycle. See
//! [`SubPools`] and [`SubPoolsStorage`].
//! * members: Accounts that are members of pools. See [`PoolMember`] and [`PoolMembers`].
//! * roles: Administrative roles of each pool, capable of controlling nomination, and the state of
//! the pool.
//! * point: A unit of measure for a members portion of a pool's funds. Points initially have a
//! ratio of 1 (as set by `POINTS_TO_BALANCE_INIT_RATIO`) to balance, but as slashing happens,
//! this can change.
//! * kick: The act of a pool administrator forcibly ejecting a member.
//! * bonded account: A key-less account id derived from the pool id that acts as the bonded
//! account. This account registers itself as a nominator in the staking system, and follows
//! exactly the same rules and conditions as a normal staker. Its bond increases or decreases as
//! members join, it can `nominate` or `chill`, and might not even earn staking rewards if it is
//! not nominating proper validators.
//! * reward account: A similar key-less account, that is set as the `Payee` account for the bonded
//! account for all staking rewards.
//! * change rate: The rate at which pool commission can be changed. A change rate consists of a
//! `max_increase` and `min_delay`, dictating the maximum percentage increase that can be applied
//! to the commission per number of blocks.
//! * throttle: An attempted commission increase is throttled if the attempted change falls outside
//! the change rate bounds.
//!
//! ## Usage
//!
//! ### Join
//!
//! An account can stake funds with a nomination pool by calling [`Call::join`].
//!
//! ### Claim rewards
//!
//! After joining a pool, a member can claim rewards by calling [`Call::claim_payout`].
//!
//! A pool member can also set a `ClaimPermission` with [`Call::set_claim_permission`], to allow
//! other members to permissionlessly bond or withdraw their rewards by calling
//! [`Call::bond_extra_other`] or [`Call::claim_payout_other`] respectively.
//!
//! For design docs see the [reward pool](#reward-pool) section.
//!
//! ### Leave
//!
//! In order to leave, a member must take two steps.
//!
//! First, they must call [`Call::unbond`]. The unbond extrinsic will start the unbonding process by
//! unbonding all or a portion of the members funds.
//!
//! > A member can have up to [`Config::MaxUnbonding`] distinct active unbonding requests.
//!
//! Second, once [`sp_staking::StakingInterface::bonding_duration`] eras have passed, the member can
//! call [`Call::withdraw_unbonded`] to withdraw any funds that are free.
//!
//! For design docs see the [bonded pool](#bonded-pool) and [unbonding sub
//! pools](#unbonding-sub-pools) sections.
//!
//! ### Slashes
//!
//! Slashes are distributed evenly across the bonded pool and the unbonding pools from slash era+1
//! through the slash apply era. Thus, any member who either
//!
//! 1. unbonded, or
//! 2. was actively bonded
//
//! in the aforementioned range of eras will be affected by the slash. A member is slashed pro-rata
//! based on its stake relative to the total slash amount.
//!
//! Slashing does not change any single member's balance. Instead, the slash will only reduce the
//! balance associated with a particular pool. But, we never change the total *points* of a pool
//! because of slashing. Therefore, when a slash happens, the ratio of points to balance changes in
//! a pool. In other words, the value of one point, which is initially 1-to-1 against a unit of
//! balance, is now less than one balance because of the slash.
//!
//! ### Administration
//!
//! A pool can be created with the [`Call::create`] call. Once created, the pools nominator or root
//! user must call [`Call::nominate`] to start nominating. [`Call::nominate`] can be called at
//! anytime to update validator selection.
//!
//! Similar to [`Call::nominate`], [`Call::chill`] will chill to pool in the staking system, and
//! [`Call::pool_withdraw_unbonded`] will withdraw any unbonding chunks of the pool bonded account.
//! The latter call is permissionless and can be called by anyone at any time.
//!
//! To help facilitate pool administration the pool has one of three states (see [`PoolState`]):
//!
//! * Open: Anyone can join the pool and no members can be permissionlessly removed.
//! * Blocked: No members can join and some admin roles can kick members. Kicking is not instant,
//! and follows the same process of `unbond` and then `withdraw_unbonded`. In other words,
//! administrators can permissionlessly unbond other members.
//! * Destroying: No members can join and all members can be permissionlessly removed with
//! [`Call::unbond`] and [`Call::withdraw_unbonded`]. Once a pool is in destroying state, it
//! cannot be reverted to another state.
//!
//! A pool has 4 administrative roles (see [`PoolRoles`]):
//!
//! * Depositor: creates the pool and is the initial member. They can only leave the pool once all
//! other members have left. Once they fully withdraw their funds, the pool is destroyed.
//! * Nominator: can select which validators the pool nominates.
//! * Bouncer: can change the pools state and kick members if the pool is blocked.
//! * Root: can change the nominator, bouncer, or itself, manage and claim commission, and can
//! perform any of the actions the nominator or bouncer can.
//!
//! ### Commission
//!
//! A pool can optionally have a commission configuration, via the `root` role, set with
//! [`Call::set_commission`] and claimed with [`Call::claim_commission`]. A payee account must be
//! supplied with the desired commission percentage. Beyond the commission itself, a pool can have a
//! maximum commission and a change rate.
//!
//! Importantly, both max commission [`Call::set_commission_max`] and change rate
//! [`Call::set_commission_change_rate`] can not be removed once set, and can only be set to more
//! restrictive values (i.e. a lower max commission or a slower change rate) in subsequent updates.
//!
//! If set, a pool's commission is bound to [`GlobalMaxCommission`] at the time it is applied to
//! pending rewards. [`GlobalMaxCommission`] is intended to be updated only via governance.
//!
//! When a pool is dissolved, any outstanding pending commission that has not been claimed will be
//! transferred to the depositor.
//!
//! Implementation note: Commission is analogous to a separate member account of the pool, with its
//! own reward counter in the form of `current_pending_commission`.
//!
//! Crucially, commission is applied to rewards based on the current commission in effect at the
//! time rewards are transferred into the reward pool. This is to prevent the malicious behaviour of
//! changing the commission rate to a very high value after rewards are accumulated, and thus claim
//! an unexpectedly high chunk of the reward.
//!
//! ### Dismantling
//!
//! As noted, a pool is destroyed once
//!
//! 1. First, all members need to fully unbond and withdraw. If the pool state is set to
//! `Destroying`, this can happen permissionlessly.
//! 2. The depositor itself fully unbonds and withdraws.
//!
//! > Note that at this point, based on the requirements of the staking system, the pool's bonded
//! > account's stake might not be able to ge below a certain threshold as a nominator. At this
//! > point, the pool should `chill` itself to allow the depositor to leave. See [`Call::chill`].
//!
//! ## Implementor's Guide
//!
//! Some notes and common mistakes that wallets/apps wishing to implement this pallet should be
//! aware of:
//!
//!
//! ### Pool Members
//!
//! * In general, whenever a pool member changes their total point, the chain will automatically
//! claim all their pending rewards for them. This is not optional, and MUST happen for the reward
//! calculation to remain correct (see the documentation of `bond` as an example). So, make sure
//! you are warning your users about it. They might be surprised if they see that they bonded an
//! extra 100 DOTs, and now suddenly their 5.23 DOTs in pending reward is gone. It is not gone, it
//! has been paid out to you!
//! * Joining a pool implies transferring funds to the pool account. So it might be (based on which
//! wallet that you are using) that you no longer see the funds that are moved to the pool in your
//! “free balance” section. Make sure the user is aware of this, and not surprised by seeing this.
//! Also, the transfer that happens here is configured to to never accidentally destroy the sender
//! account. So to join a Pool, your sender account must remain alive with 1 DOT left in it. This
//! means, with 1 DOT as existential deposit, and 1 DOT as minimum to join a pool, you need at
//! least 2 DOT to join a pool. Consequently, if you are suggesting members to join a pool with
//! “Maximum possible value”, you must subtract 1 DOT to remain in the sender account to not
//! accidentally kill it.
//! * Points and balance are not the same! Any pool member, at any point in time, can have points in
//! either the bonded pool or any of the unbonding pools. The crucial fact is that in any of these
//! pools, the ratio of point to balance is different and might not be 1. Each pool starts with a
//! ratio of 1, but as time goes on, for reasons such as slashing, the ratio gets broken. Over
//! time, 100 points in a bonded pool can be worth 90 DOTs. Make sure you are either representing
//! points as points (not as DOTs), or even better, always display both: “You have x points in
//! pool y which is worth z DOTs”. See here and here for examples of how to calculate point to
//! balance ratio of each pool (it is almost trivial ;))
//!
//! ### Pool Management
//!
//! * The pool will be seen from the perspective of the rest of the system as a single nominator.
//! Ergo, This nominator must always respect the `staking.minNominatorBond` limit. Similar to a
//! normal nominator, who has to first `chill` before fully unbonding, the pool must also do the
//! same. The pool’s bonded account will be fully unbonded only when the depositor wants to leave
//! and dismantle the pool. All that said, the message is: the depositor can only leave the chain
//! when they chill the pool first.
//!
//! ## Design
//!
//! _Notes_: this section uses pseudo code to explain general design and does not necessarily
//! reflect the exact implementation. Additionally, a working knowledge of `pallet-staking`'s api is
//! assumed.
//!
//! ### Goals
//!
//! * Maintain network security by upholding integrity of slashing events, sufficiently penalizing
//! members that where in the pool while it was backing a validator that got slashed.
//! * Maximize scalability in terms of member count.
//!
//! In order to maintain scalability, all operations are independent of the number of members. To do
//! this, delegation specific information is stored local to the member while the pool data
//! structures have bounded datum.
//!
//! ### Bonded pool
//!
//! A bonded pool nominates with its total balance, excluding that which has been withdrawn for
//! unbonding. The total points of a bonded pool are always equal to the sum of points of the
//! delegation members. A bonded pool tracks its points and reads its bonded balance.
//!
//! When a member joins a pool, `amount_transferred` is transferred from the members account to the
//! bonded pools account. Then the pool calls `staking::bond_extra(amount_transferred)` and issues
//! new points which are tracked by the member and added to the bonded pool's points.
//!
//! When the pool already has some balance, we want the value of a point before the transfer to
//! equal the value of a point after the transfer. So, when a member joins a bonded pool with a
//! given `amount_transferred`, we maintain the ratio of bonded balance to points such that:
//!
//! ```text
//! balance_after_transfer / points_after_transfer == balance_before_transfer / points_before_transfer;
//! ```
//!
//! To achieve this, we issue points based on the following:
//!
//! ```text
//! points_issued = (points_before_transfer / balance_before_transfer) * amount_transferred;
//! ```
//!
//! For new bonded pools we can set the points issued per balance arbitrarily. In this
//! implementation we use a 1 points to 1 balance ratio for pool creation (see
//! [`POINTS_TO_BALANCE_INIT_RATIO`]).
//!
//! **Relevant extrinsics:**
//!
//! * [`Call::create`]
//! * [`Call::join`]
//!
//! ### Reward pool
//!
//! When a pool is first bonded it sets up a deterministic, inaccessible account as its reward
//! destination. This reward account combined with `RewardPool` compose a reward pool.
//!
//! Reward pools are completely separate entities to bonded pools. Along with its account, a reward
//! pool also tracks its outstanding and claimed rewards as counters, in addition to pending and
//! claimed commission. These counters are updated with `RewardPool::update_records`. The current
//! reward counter of the pool (the total outstanding rewards, in points) is also callable with the
//! `RewardPool::current_reward_counter` method.
//!
//! See [this link](https://hackmd.io/PFGn6wI5TbCmBYoEA_f2Uw) for an in-depth explanation of the
//! reward pool mechanism.
//!
//! **Relevant extrinsics:**
//!
//! * [`Call::claim_payout`]
//!
//! ### Unbonding sub pools
//!
//! When a member unbonds, it's balance is unbonded in the bonded pool's account and tracked in an
//! unbonding pool associated with the active era. If no such pool exists, one is created. To track
//! which unbonding sub pool a member belongs too, a member tracks it's `unbonding_era`.
//!
//! When a member initiates unbonding it's claim on the bonded pool (`balance_to_unbond`) is
//! computed as:
//!
//! ```text
//! balance_to_unbond = (bonded_pool.balance / bonded_pool.points) * member.points;
//! ```
//!
//! If this is the first transfer into an unbonding pool arbitrary amount of points can be issued
//! per balance. In this implementation unbonding pools are initialized with a 1 point to 1 balance
//! ratio (see [`POINTS_TO_BALANCE_INIT_RATIO`]). Otherwise, the unbonding pools hold the same
//! points to balance ratio properties as the bonded pool, so member points in the unbonding pool
//! are issued based on
//!
//! ```text
//! new_points_issued = (points_before_transfer / balance_before_transfer) * balance_to_unbond;
//! ```
//!
//! For scalability, a bound is maintained on the number of unbonding sub pools (see
//! [`TotalUnbondingPools`]). An unbonding pool is removed once its older than `current_era -
//! TotalUnbondingPools`. An unbonding pool is merged into the unbonded pool with
//!
//! ```text
//! unbounded_pool.balance = unbounded_pool.balance + unbonding_pool.balance;
//! unbounded_pool.points = unbounded_pool.points + unbonding_pool.points;
//! ```
//!
//! This scheme "averages" out the points value in the unbonded pool.
//!
//! Once a members `unbonding_era` is older than `current_era -
//! [sp_staking::StakingInterface::bonding_duration]`, it can can cash it's points out of the
//! corresponding unbonding pool. If it's `unbonding_era` is older than `current_era -
//! TotalUnbondingPools`, it can cash it's points from the unbonded pool.
//!
//! **Relevant extrinsics:**
//!
//! * [`Call::unbond`]
//! * [`Call::withdraw_unbonded`]
//!
//! ### Slashing
//!
//! This section assumes that the slash computation is executed by
//! `pallet_staking::StakingLedger::slash`, which passes the information to this pallet via
//! [`sp_staking::OnStakingUpdate::on_slash`].
//!
//! Unbonding pools need to be slashed to ensure all nominators whom where in the bonded pool while
//! it was backing a validator that equivocated are punished. Without these measures a member could
//! unbond right after a validator equivocated with no consequences.
//!
//! This strategy is unfair to members who joined after the slash, because they get slashed as well,
//! but spares members who unbond. The latter is much more important for security: if a pool's
//! validators are attacking the network, their members need to unbond fast! Avoiding slashes gives
//! them an incentive to do that if validators get repeatedly slashed.
//!
//! To be fair to joiners, this implementation also need joining pools, which are actively staking,
//! in addition to the unbonding pools. For maintenance simplicity these are not implemented.
//! Related: <https://github.com/paritytech/substrate/issues/10860>
//!
//! ### Limitations
//!
//! * PoolMembers cannot vote with their staked funds because they are transferred into the pools
//! account. In the future this can be overcome by allowing the members to vote with their bonded
//! funds via vote splitting.
//! * PoolMembers cannot quickly transfer to another pool if they do no like nominations, instead
//! they must wait for the unbonding duration.
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use adapter::{Member, Pool, StakeStrategy};
use alloc::{collections::btree_map::BTreeMap, vec::Vec};
use codec::Codec;
use core::{fmt::Debug, ops::Div};
use frame_support::{
defensive, defensive_assert, ensure,
pallet_prelude::{MaxEncodedLen, *},
storage::bounded_btree_map::BoundedBTreeMap,
traits::{
fungible::{Inspect, InspectFreeze, Mutate, MutateFreeze},
tokens::{Fortitude, Preservation},
Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get,
},
DefaultNoBound, PalletError,
};
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::TypeInfo;
use sp_core::U256;
use sp_runtime::{
traits::{
AccountIdConversion, Bounded, CheckedAdd, CheckedSub, Convert, Saturating, StaticLookup,
Zero,
},
FixedPointNumber, Perbill,
};
use sp_staking::{EraIndex, StakingInterface};
#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))]
use sp_runtime::TryRuntimeError;
/// The log target of this pallet.
pub const LOG_TARGET: &str = "runtime::nomination-pools";
// syntactic sugar for logging.
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: $crate::LOG_TARGET,
concat!("[{:?}] 🏊♂️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
)
};
}
#[cfg(any(test, feature = "fuzzing"))]
pub mod mock;
#[cfg(test)]
mod tests;
pub mod adapter;
pub mod migration;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
/// The balance type used by the currency system.
pub type BalanceOf<T> =
<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
/// Type used for unique identifier of each pool.
pub type PoolId = u32;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
pub const POINTS_TO_BALANCE_INIT_RATIO: u32 = 1;
/// Possible operations on the configuration values of this pallet.
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, PartialEq, Clone)]
pub enum ConfigOp<T: Codec + Debug> {
/// Don't change.
Noop,
/// Set the given value.
Set(T),
/// Remove from storage.
Remove,
}
/// The type of bonding that can happen to a pool.
pub enum BondType {
/// Someone is bonding into the pool upon creation.
Create,
/// Someone is adding more funds later to this pool.
Extra,
}
/// How to increase the bond of a member.
#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
pub enum BondExtra<Balance> {
/// Take from the free balance.
FreeBalance(Balance),
/// Take the entire amount from the accumulated rewards.
Rewards,
}
/// The type of account being created.
#[derive(Encode, Decode)]
enum AccountType {
Bonded,
Reward,
}
/// The permission a pool member can set for other accounts to claim rewards on their behalf.
#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
pub enum ClaimPermission {
/// Only the pool member themselves can claim their rewards.
Permissioned,
/// Anyone can compound rewards on a pool member's behalf.
PermissionlessCompound,
/// Anyone can withdraw rewards on a pool member's behalf.
PermissionlessWithdraw,
/// Anyone can withdraw and compound rewards on a pool member's behalf.
PermissionlessAll,
}
impl Default for ClaimPermission {
fn default() -> Self {
Self::PermissionlessWithdraw
}
}
impl ClaimPermission {
/// Permissionless compounding of pool rewards is allowed if the current permission is
/// `PermissionlessCompound`, or permissionless.
fn can_bond_extra(&self) -> bool {
matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessCompound)
}
/// Permissionless payout claiming is allowed if the current permission is
/// `PermissionlessWithdraw`, or permissionless.
fn can_claim_payout(&self) -> bool {
matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessWithdraw)
}
}
/// A member in a pool.
#[derive(
Encode,
Decode,
MaxEncodedLen,
TypeInfo,
RuntimeDebugNoBound,
CloneNoBound,
frame_support::PartialEqNoBound,
)]
#[cfg_attr(feature = "std", derive(DefaultNoBound))]
#[scale_info(skip_type_params(T))]
pub struct PoolMember<T: Config> {
/// The identifier of the pool to which `who` belongs.
pub pool_id: PoolId,
/// The quantity of points this member has in the bonded pool or in a sub pool if
/// `Self::unbonding_era` is some.
pub points: BalanceOf<T>,
/// The reward counter at the time of this member's last payout claim.
pub last_recorded_reward_counter: T::RewardCounter,
/// The eras in which this member is unbonding, mapped from era index to the number of
/// points scheduled to unbond in the given era.
pub unbonding_eras: BoundedBTreeMap<EraIndex, BalanceOf<T>, T::MaxUnbonding>,
}
impl<T: Config> PoolMember<T> {
/// The pending rewards of this member.
fn pending_rewards(
&self,
current_reward_counter: T::RewardCounter,
) -> Result<BalanceOf<T>, Error<T>> {
// accuracy note: Reward counters are `FixedU128` with base of 10^18. This value is being
// multiplied by a point. The worse case of a point is 10x the granularity of the balance
// (10x is the common configuration of `MaxPointsToBalance`).
//
// Assuming roughly the current issuance of polkadot (12,047,781,394,999,601,455, which is
// 1.2 * 10^9 * 10^10 = 1.2 * 10^19), the worse case point value is around 10^20.
//
// The final multiplication is:
//
// rc * 10^20 / 10^18 = rc * 100
//
// the implementation of `multiply_by_rational_with_rounding` shows that it will only fail
// if the final division is not enough to fit in u128. In other words, if `rc * 100` is more
// than u128::max. Given that RC is interpreted as reward per unit of point, and unit of
// point is equal to balance (normally), and rewards are usually a proportion of the points
// in the pool, the likelihood of rc reaching near u128::MAX is near impossible.
(current_reward_counter.defensive_saturating_sub(self.last_recorded_reward_counter))
.checked_mul_int(self.active_points())
.ok_or(Error::<T>::OverflowRisk)
}
/// Active balance of the member.
///
/// This is derived from the ratio of points in the pool to which the member belongs to.
/// Might return different values based on the pool state for the same member and points.
fn active_balance(&self) -> BalanceOf<T> {
if let Some(pool) = BondedPool::<T>::get(self.pool_id).defensive() {
pool.points_to_balance(self.points)
} else {
Zero::zero()
}
}
/// Total balance of the member, both active and unbonding.
/// Doesn't mutate state.
///
/// Worst case, iterates over [`TotalUnbondingPools`] member unbonding pools to calculate member
/// balance.
pub fn total_balance(&self) -> BalanceOf<T> {
let pool = match BondedPool::<T>::get(self.pool_id) {
Some(pool) => pool,
None => {
// this internal function is always called with a valid pool id.
defensive!("pool should exist; qed");
return Zero::zero();
},
};
let active_balance = pool.points_to_balance(self.active_points());
let sub_pools = match SubPoolsStorage::<T>::get(self.pool_id) {
Some(sub_pools) => sub_pools,
None => return active_balance,
};
let unbonding_balance = self.unbonding_eras.iter().fold(
BalanceOf::<T>::zero(),
|accumulator, (era, unlocked_points)| {
// if the `SubPools::with_era` has already been merged into the
// `SubPools::no_era` use this pool instead.
let era_pool = sub_pools.with_era.get(era).unwrap_or(&sub_pools.no_era);
accumulator + (era_pool.point_to_balance(*unlocked_points))
},
);
active_balance + unbonding_balance
}
/// Total points of this member, both active and unbonding.
fn total_points(&self) -> BalanceOf<T> {
self.active_points().saturating_add(self.unbonding_points())
}
/// Active points of the member.
fn active_points(&self) -> BalanceOf<T> {
self.points
}
/// Inactive points of the member, waiting to be withdrawn.
fn unbonding_points(&self) -> BalanceOf<T> {
self.unbonding_eras
.as_ref()
.iter()
.fold(BalanceOf::<T>::zero(), |acc, (_, v)| acc.saturating_add(*v))
}
/// Try and unbond `points_dissolved` from self, and in return mint `points_issued` into the
/// corresponding `era`'s unlock schedule.
///
/// In the absence of slashing, these two points are always the same. In the presence of
/// slashing, the value of points in different pools varies.
///
/// Returns `Ok(())` and updates `unbonding_eras` and `points` if success, `Err(_)` otherwise.
fn try_unbond(
&mut self,
points_dissolved: BalanceOf<T>,
points_issued: BalanceOf<T>,
unbonding_era: EraIndex,
) -> Result<(), Error<T>> {
if let Some(new_points) = self.points.checked_sub(&points_dissolved) {
match self.unbonding_eras.get_mut(&unbonding_era) {
Some(already_unbonding_points) =>
*already_unbonding_points =
already_unbonding_points.saturating_add(points_issued),
None => self
.unbonding_eras
.try_insert(unbonding_era, points_issued)
.map(|old| {
if old.is_some() {
defensive!("value checked to not exist in the map; qed");
}
})
.map_err(|_| Error::<T>::MaxUnbondingLimit)?,
}
self.points = new_points;
Ok(())
} else {
Err(Error::<T>::MinimumBondNotMet)
}
}
/// Withdraw any funds in [`Self::unbonding_eras`] who's deadline in reached and is fully
/// unlocked.
///
/// Returns a a subset of [`Self::unbonding_eras`] that got withdrawn.
///
/// Infallible, noop if no unbonding eras exist.
fn withdraw_unlocked(
&mut self,
current_era: EraIndex,
) -> BoundedBTreeMap<EraIndex, BalanceOf<T>, T::MaxUnbonding> {
// NOTE: if only drain-filter was stable..
let mut removed_points =
BoundedBTreeMap::<EraIndex, BalanceOf<T>, T::MaxUnbonding>::default();
self.unbonding_eras.retain(|e, p| {
if *e > current_era {
true
} else {
removed_points
.try_insert(*e, *p)
.expect("source map is bounded, this is a subset, will be bounded; qed");
false
}
});
removed_points
}
}
/// A pool's possible states.
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, RuntimeDebugNoBound, Clone, Copy)]
pub enum PoolState {
/// The pool is open to be joined, and is working normally.
Open,
/// The pool is blocked. No one else can join.
Blocked,
/// The pool is in the process of being destroyed.
///
/// All members can now be permissionlessly unbonded, and the pool can never go back to any
/// other state other than being dissolved.
Destroying,
}
/// Pool administration roles.
///
/// Any pool has a depositor, which can never change. But, all the other roles are optional, and
/// cannot exist. Note that if `root` is set to `None`, it basically means that the roles of this
/// pool can never change again (except via governance).
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Clone)]
pub struct PoolRoles<AccountId> {
/// Creates the pool and is the initial member. They can only leave the pool once all other
/// members have left. Once they fully leave, the pool is destroyed.
pub depositor: AccountId,
/// Can change the nominator, bouncer, or itself and can perform any of the actions the
/// nominator or bouncer can.
pub root: Option<AccountId>,
/// Can select which validators the pool nominates.
pub nominator: Option<AccountId>,
/// Can change the pools state and kick members if the pool is blocked.
pub bouncer: Option<AccountId>,
}
// A pool's possible commission claiming permissions.
#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum CommissionClaimPermission<AccountId> {
Permissionless,
Account(AccountId),
}
/// Pool commission.
///
/// The pool `root` can set commission configuration after pool creation. By default, all commission
/// values are `None`. Pool `root` can also set `max` and `change_rate` configurations before
/// setting an initial `current` commission.
///
/// `current` is a tuple of the commission percentage and payee of commission. `throttle_from`
/// keeps track of which block `current` was last updated. A `max` commission value can only be
/// decreased after the initial value is set, to prevent commission from repeatedly increasing.
///
/// An optional commission `change_rate` allows the pool to set strict limits to how much commission
/// can change in each update, and how often updates can take place.
#[derive(
Encode, Decode, DefaultNoBound, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Copy, Clone,
)]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct Commission<T: Config> {
/// Optional commission rate of the pool along with the account commission is paid to.
pub current: Option<(Perbill, T::AccountId)>,
/// Optional maximum commission that can be set by the pool `root`. Once set, this value can
/// only be updated to a decreased value.
pub max: Option<Perbill>,
/// Optional configuration around how often commission can be updated, and when the last
/// commission update took place.
pub change_rate: Option<CommissionChangeRate<BlockNumberFor<T>>>,
/// The block from where throttling should be checked from. This value will be updated on all
/// commission updates and when setting an initial `change_rate`.
pub throttle_from: Option<BlockNumberFor<T>>,
// Whether commission can be claimed permissionlessly, or whether an account can claim
// commission. `Root` role can always claim.
pub claim_permission: Option<CommissionClaimPermission<T::AccountId>>,
}
impl<T: Config> Commission<T> {
/// Returns true if the current commission updating to `to` would exhaust the change rate
/// limits.
///
/// A commission update will be throttled (disallowed) if:
/// 1. not enough blocks have passed since the `throttle_from` block, if exists, or
/// 2. the new commission is greater than the maximum allowed increase.
fn throttling(&self, to: &Perbill) -> bool {
if let Some(t) = self.change_rate.as_ref() {
let commission_as_percent =
self.current.as_ref().map(|(x, _)| *x).unwrap_or(Perbill::zero());
// do not throttle if `to` is the same or a decrease in commission.
if *to <= commission_as_percent {
return false
}
// Test for `max_increase` throttling.
//
// Throttled if the attempted increase in commission is greater than `max_increase`.
if (*to).saturating_sub(commission_as_percent) > t.max_increase {
return true
}
// Test for `min_delay` throttling.
//
// Note: matching `None` is defensive only. `throttle_from` should always exist where
// `change_rate` has already been set, so this scenario should never happen.
return self.throttle_from.map_or_else(
|| {
defensive!("throttle_from should exist if change_rate is set");
true
},
|f| {
// if `min_delay` is zero (no delay), not throttling.
if t.min_delay == Zero::zero() {
false
} else {
// throttling if blocks passed is less than `min_delay`.
let blocks_surpassed =
<frame_system::Pallet<T>>::block_number().saturating_sub(f);
blocks_surpassed < t.min_delay
}
},
)
}
false
}
/// Gets the pool's current commission, or returns Perbill::zero if none is set.
/// Bounded to global max if current is greater than `GlobalMaxCommission`.
fn current(&self) -> Perbill {
self.current
.as_ref()
.map_or(Perbill::zero(), |(c, _)| *c)
.min(GlobalMaxCommission::<T>::get().unwrap_or(Bounded::max_value()))
}
/// Set the pool's commission.
///
/// Update commission based on `current`. If a `None` is supplied, allow the commission to be
/// removed without any change rate restrictions. Updates `throttle_from` to the current block.
/// If the supplied commission is zero, `None` will be inserted and `payee` will be ignored.
fn try_update_current(&mut self, current: &Option<(Perbill, T::AccountId)>) -> DispatchResult {
self.current = match current {
None => None,
Some((commission, payee)) => {
ensure!(!self.throttling(commission), Error::<T>::CommissionChangeThrottled);
ensure!(
commission <= &GlobalMaxCommission::<T>::get().unwrap_or(Bounded::max_value()),
Error::<T>::CommissionExceedsGlobalMaximum
);
ensure!(
self.max.map_or(true, |m| commission <= &m),
Error::<T>::CommissionExceedsMaximum
);
if commission.is_zero() {
None
} else {
Some((*commission, payee.clone()))
}
},
};
self.register_update();
Ok(())
}
/// Set the pool's maximum commission.
///
/// The pool's maximum commission can initially be set to any value, and only smaller values
/// thereafter. If larger values are attempted, this function will return a dispatch error.
///
/// If `current.0` is larger than the updated max commission value, `current.0` will also be
/// updated to the new maximum. This will also register a `throttle_from` update.
/// A `PoolCommissionUpdated` event is triggered if `current.0` is updated.
fn try_update_max(&mut self, pool_id: PoolId, new_max: Perbill) -> DispatchResult {
ensure!(
new_max <= GlobalMaxCommission::<T>::get().unwrap_or(Bounded::max_value()),
Error::<T>::CommissionExceedsGlobalMaximum
);
if let Some(old) = self.max.as_mut() {
if new_max > *old {
return Err(Error::<T>::MaxCommissionRestricted.into())
}
*old = new_max;
} else {
self.max = Some(new_max)
};
let updated_current = self
.current
.as_mut()
.map(|(c, _)| {
let u = *c > new_max;
*c = (*c).min(new_max);
u
})
.unwrap_or(false);
if updated_current {
if let Some((_, payee)) = self.current.as_ref() {
Pallet::<T>::deposit_event(Event::<T>::PoolCommissionUpdated {
pool_id,
current: Some((new_max, payee.clone())),
});
}
self.register_update();
}
Ok(())
}
/// Set the pool's commission `change_rate`.
///
/// Once a change rate configuration has been set, only more restrictive values can be set
/// thereafter. These restrictions translate to increased `min_delay` values and decreased
/// `max_increase` values.
///
/// Update `throttle_from` to the current block upon setting change rate for the first time, so
/// throttling can be checked from this block.
fn try_update_change_rate(
&mut self,
change_rate: CommissionChangeRate<BlockNumberFor<T>>,
) -> DispatchResult {
ensure!(!&self.less_restrictive(&change_rate), Error::<T>::CommissionChangeRateNotAllowed);
if self.change_rate.is_none() {
self.register_update();
}
self.change_rate = Some(change_rate);
Ok(())
}
/// Updates a commission's `throttle_from` field to the current block.
fn register_update(&mut self) {
self.throttle_from = Some(<frame_system::Pallet<T>>::block_number());
}
/// Checks whether a change rate is less restrictive than the current change rate, if any.
///
/// No change rate will always be less restrictive than some change rate, so where no
/// `change_rate` is currently set, `false` is returned.
fn less_restrictive(&self, new: &CommissionChangeRate<BlockNumberFor<T>>) -> bool {
self.change_rate
.as_ref()
.map(|c| new.max_increase > c.max_increase || new.min_delay < c.min_delay)
.unwrap_or(false)
}
}
/// Pool commission change rate preferences.
///
/// The pool root is able to set a commission change rate for their pool. A commission change rate
/// consists of 2 values; (1) the maximum allowed commission change, and (2) the minimum amount of
/// blocks that must elapse before commission updates are allowed again.
///
/// Commission change rates are not applied to decreases in commission.
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Copy, Clone)]
pub struct CommissionChangeRate<BlockNumber> {
/// The maximum amount the commission can be updated by per `min_delay` period.
pub max_increase: Perbill,
/// How often an update can take place.
pub min_delay: BlockNumber,
}
/// Pool permissions and state
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Clone)]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct BondedPoolInner<T: Config> {
/// The commission rate of the pool.
pub commission: Commission<T>,
/// Count of members that belong to the pool.
pub member_counter: u32,
/// Total points of all the members in the pool who are actively bonded.
pub points: BalanceOf<T>,
/// See [`PoolRoles`].
pub roles: PoolRoles<T::AccountId>,
/// The current state of the pool.
pub state: PoolState,
}
/// A wrapper for bonded pools, with utility functions.
///
/// The main purpose of this is to wrap a [`BondedPoolInner`], with the account
/// + id of the pool, for easier access.
#[derive(RuntimeDebugNoBound)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq))]
pub struct BondedPool<T: Config> {
/// The identifier of the pool.
id: PoolId,
/// The inner fields.
inner: BondedPoolInner<T>,
}
impl<T: Config> core::ops::Deref for BondedPool<T> {
type Target = BondedPoolInner<T>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T: Config> core::ops::DerefMut for BondedPool<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<T: Config> BondedPool<T> {
/// Create a new bonded pool with the given roles and identifier.
fn new(id: PoolId, roles: PoolRoles<T::AccountId>) -> Self {
Self {
id,
inner: BondedPoolInner {
commission: Commission::default(),
member_counter: Zero::zero(),
points: Zero::zero(),
roles,
state: PoolState::Open,
},
}
}
/// Get [`Self`] from storage. Returns `None` if no entry for `pool_account` exists.
pub fn get(id: PoolId) -> Option<Self> {
BondedPools::<T>::try_get(id).ok().map(|inner| Self { id, inner })
}
/// Get the bonded account id of this pool.
fn bonded_account(&self) -> T::AccountId {
Pallet::<T>::generate_bonded_account(self.id)
}
/// Get the reward account id of this pool.
fn reward_account(&self) -> T::AccountId {
Pallet::<T>::generate_reward_account(self.id)
}
/// Consume self and put into storage.
fn put(self) {
BondedPools::<T>::insert(self.id, self.inner);