1
// Copyright 2019-2025 PureStake Inc.
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam 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
// Moonbeam 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 Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! # Parachain Staking
18
//! Minimal staking pallet that implements collator selection by total backed stake.
19
//! The main difference between this pallet and `frame/pallet-staking` is that this pallet
20
//! uses direct delegation. Delegators choose exactly who they delegate and with what stake.
21
//! This is different from `frame/pallet-staking` where delegators approval vote and run Phragmen.
22
//!
23
//! ### Rules
24
//! There is a new round every `<Round<T>>::get().length` blocks.
25
//!
26
//! At the start of every round,
27
//! * issuance is calculated for collators (and their delegators) for block authoring
28
//! `T::RewardPaymentDelay` rounds ago
29
//! * a new set of collators is chosen from the candidates
30
//!
31
//! Immediately following a round change, payments are made once-per-block until all payments have
32
//! been made. In each such block, one collator is chosen for a rewards payment and is paid along
33
//! with each of its top `T::MaxTopDelegationsPerCandidate` delegators.
34
//!
35
//! To join the set of candidates, call `join_candidates` with `bond >= MinCandidateStk`.
36
//! To leave the set of candidates, call `schedule_leave_candidates`. If the call succeeds,
37
//! the collator is removed from the pool of candidates so they cannot be selected for future
38
//! collator sets, but they are not unbonded until their exit request is executed. Any signed
39
//! account may trigger the exit `T::LeaveCandidatesDelay` rounds after the round in which the
40
//! original request was made.
41
//!
42
//! To join the set of delegators, call `delegate` and pass in an account that is
43
//! already a collator candidate and `bond >= MinDelegation`. Each delegator can delegate up to
44
//! `T::MaxDelegationsPerDelegator` collator candidates by calling `delegate`.
45
//!
46
//! To revoke a delegation, call `revoke_delegation` with the collator candidate's account.
47
//! To leave the set of delegators and revoke all delegations, call `leave_delegators`.
48

            
49
#![cfg_attr(not(feature = "std"), no_std)]
50

            
51
mod auto_compound;
52
mod delegation_requests;
53
pub mod inflation;
54
pub mod migrations;
55
pub mod traits;
56
pub mod types;
57
pub mod weights;
58

            
59
#[cfg(any(test, feature = "runtime-benchmarks"))]
60
mod benchmarks;
61
#[cfg(test)]
62
mod mock;
63
mod set;
64
#[cfg(test)]
65
mod tests;
66

            
67
use frame_support::pallet;
68
pub use inflation::{InflationInfo, Range};
69
pub use weights::WeightInfo;
70

            
71
pub use auto_compound::{AutoCompoundConfig, AutoCompoundDelegations};
72
pub use delegation_requests::{CancelledScheduledRequest, DelegationAction, ScheduledRequest};
73
pub use pallet::*;
74
pub use traits::*;
75
pub use types::*;
76
pub use RoundIndex;
77

            
78
4260
#[pallet]
79
pub mod pallet {
80
	use crate::delegation_requests::{
81
		CancelledScheduledRequest, DelegationAction, ScheduledRequest,
82
	};
83
	use crate::{set::BoundedOrderedSet, traits::*, types::*, InflationInfo, Range, WeightInfo};
84
	use crate::{AutoCompoundConfig, AutoCompoundDelegations};
85
	use frame_support::dispatch::{DispatchResultWithPostInfo, Pays, PostDispatchInfo};
86
	use frame_support::pallet_prelude::*;
87
	use frame_support::traits::{
88
		fungible::{Balanced, Inspect, Mutate, MutateFreeze},
89
		Currency, Get, LockIdentifier, LockableCurrency, ReservableCurrency,
90
	};
91
	use frame_system::pallet_prelude::*;
92
	use sp_consensus_slots::Slot;
93
	use sp_runtime::{
94
		traits::{Saturating, Zero},
95
		DispatchErrorWithPostInfo, Perbill, Percent,
96
	};
97
	use sp_std::{collections::btree_map::BTreeMap, prelude::*};
98

            
99
	/// Pallet for parachain staking
100
50
	#[pallet::pallet]
101
	#[pallet::without_storage_info]
102
	pub struct Pallet<T>(PhantomData<T>);
103

            
104
	pub type RoundIndex = u32;
105
	type RewardPoint = u32;
106
	pub type BalanceOf<T> =
107
		<<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
108

            
109
	// DEPRECATED: Remove after applying migration `MigrateLocksToFreezes`
110
	pub const COLLATOR_LOCK_ID: LockIdentifier = *b"stkngcol";
111
	pub const DELEGATOR_LOCK_ID: LockIdentifier = *b"stkngdel";
112

            
113
	/// A hard limit for weight computation purposes for the max candidates that _could_
114
	/// theoretically exist.
115
	pub const MAX_CANDIDATES: u32 = 200;
116

            
117
	/// Maximum number of accounts (delegators and candidates) that can be migrated at once in the `migrate_locks_to_freezes_batch` extrinsic.
118
	pub(crate) const MAX_ACCOUNTS_PER_MIGRATION_BATCH: u32 = 100;
119

            
120
	/// Configuration trait of this pallet.
121
	#[pallet::config]
122
	pub trait Config: frame_system::Config {
123
		/// Overarching event type
124
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
125
		/// The fungible type for handling balances
126
		type Currency: Inspect<Self::AccountId>
127
			+ Mutate<Self::AccountId>
128
			+ MutateFreeze<Self::AccountId, Id = Self::RuntimeFreezeReason>
129
			+ Balanced<Self::AccountId>
130
			// DEPRECATED: Remove traits below after applying migration `MigrateLocksToFreezes`
131
			+ Currency<Self::AccountId, Balance = BalanceOf<Self>>
132
			+ ReservableCurrency<Self::AccountId>
133
			+ LockableCurrency<Self::AccountId>;
134
		/// The overarching freeze identifier type.
135
		type RuntimeFreezeReason: From<FreezeReason>;
136
		/// The origin for monetary governance
137
		type MonetaryGovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
138
		/// Minimum number of blocks per round
139
		#[pallet::constant]
140
		type MinBlocksPerRound: Get<u32>;
141
		/// If a collator doesn't produce any block on this number of rounds, it is notified as inactive.
142
		/// This value must be less than or equal to RewardPaymentDelay.
143
		#[pallet::constant]
144
		type MaxOfflineRounds: Get<u32>;
145
		/// Number of rounds that candidates remain bonded before exit request is executable
146
		#[pallet::constant]
147
		type LeaveCandidatesDelay: Get<RoundIndex>;
148
		/// Number of rounds candidate requests to decrease self-bond must wait to be executable
149
		#[pallet::constant]
150
		type CandidateBondLessDelay: Get<RoundIndex>;
151
		/// Number of rounds that delegators remain bonded before exit request is executable
152
		#[pallet::constant]
153
		type LeaveDelegatorsDelay: Get<RoundIndex>;
154
		/// Number of rounds that delegations remain bonded before revocation request is executable
155
		#[pallet::constant]
156
		type RevokeDelegationDelay: Get<RoundIndex>;
157
		/// Number of rounds that delegation less requests must wait before executable
158
		#[pallet::constant]
159
		type DelegationBondLessDelay: Get<RoundIndex>;
160
		/// Number of rounds after which block authors are rewarded
161
		#[pallet::constant]
162
		type RewardPaymentDelay: Get<RoundIndex>;
163
		/// Minimum number of selected candidates every round
164
		#[pallet::constant]
165
		type MinSelectedCandidates: Get<u32>;
166
		/// Maximum top delegations counted per candidate
167
		#[pallet::constant]
168
		type MaxTopDelegationsPerCandidate: Get<u32>;
169
		/// Maximum bottom delegations (not counted) per candidate
170
		#[pallet::constant]
171
		type MaxBottomDelegationsPerCandidate: Get<u32>;
172
		/// Maximum delegations per delegator
173
		#[pallet::constant]
174
		type MaxDelegationsPerDelegator: Get<u32>;
175
		/// Minimum stake required for any account to be a collator candidate
176
		#[pallet::constant]
177
		type MinCandidateStk: Get<BalanceOf<Self>>;
178
		/// Minimum stake for any registered on-chain account to delegate
179
		#[pallet::constant]
180
		type MinDelegation: Get<BalanceOf<Self>>;
181
		/// Get the current block author
182
		type BlockAuthor: Get<Self::AccountId>;
183
		/// Handler to notify the runtime when a collator is paid.
184
		/// If you don't need it, you can specify the type `()`.
185
		type OnCollatorPayout: OnCollatorPayout<Self::AccountId, BalanceOf<Self>>;
186
		/// Handler to distribute a collator's reward.
187
		/// To use the default implementation of minting rewards, specify the type `()`.
188
		type PayoutCollatorReward: PayoutCollatorReward<Self>;
189
		/// Handler to notify the runtime when a collator is inactive.
190
		/// The default behavior is to mark the collator as offline.
191
		/// If you need to use the default implementation, specify the type `()`.
192
		type OnInactiveCollator: OnInactiveCollator<Self>;
193
		/// Handler to notify the runtime when a new round begin.
194
		/// If you don't need it, you can specify the type `()`.
195
		type OnNewRound: OnNewRound;
196
		/// Get the current slot number
197
		type SlotProvider: Get<Slot>;
198
		/// Get the slot duration in milliseconds
199
		#[pallet::constant]
200
		type SlotDuration: Get<u64>;
201
		/// Get the average time between 2 blocks in milliseconds
202
		#[pallet::constant]
203
		type BlockTime: Get<u64>;
204
		/// Maximum candidates
205
		#[pallet::constant]
206
		type MaxCandidates: Get<u32>;
207
		/// Threshold after which inflation become linear
208
		/// If you don't want to use it, set it to `()`
209
		#[pallet::constant]
210
		type LinearInflationThreshold: Get<Option<BalanceOf<Self>>>;
211
		/// Weight information for extrinsics in this pallet.
212
		type WeightInfo: WeightInfo;
213
	}
214

            
215
	/// The reason for freezing funds.
216
	#[pallet::composite_enum]
217
	pub enum FreezeReason {
218
125
		/// Funds frozen for staking as a collator
219
		StakingCollator,
220
399
		/// Funds frozen for staking as a delegator
221
		StakingDelegator,
222
	}
223

            
224
20
	#[pallet::error]
225
	pub enum Error<T> {
226
		DelegatorDNE,
227
		DelegatorDNEinTopNorBottom,
228
		DelegatorDNEInDelegatorSet,
229
		CandidateDNE,
230
		DelegationDNE,
231
		DelegatorExists,
232
		CandidateExists,
233
		CandidateBondBelowMin,
234
		InsufficientBalance,
235
		DelegatorBondBelowMin,
236
		DelegationBelowMin,
237
		AlreadyOffline,
238
		AlreadyActive,
239
		DelegatorAlreadyLeaving,
240
		DelegatorNotLeaving,
241
		DelegatorCannotLeaveYet,
242
		CannotDelegateIfLeaving,
243
		CandidateAlreadyLeaving,
244
		CandidateNotLeaving,
245
		CandidateCannotLeaveYet,
246
		CannotGoOnlineIfLeaving,
247
		ExceedMaxDelegationsPerDelegator,
248
		AlreadyDelegatedCandidate,
249
		InvalidSchedule,
250
		CannotSetBelowMin,
251
		RoundLengthMustBeGreaterThanTotalSelectedCollators,
252
		NoWritingSameValue,
253
		TotalInflationDistributionPercentExceeds100,
254
		TooLowCandidateCountWeightHintJoinCandidates,
255
		TooLowCandidateCountWeightHintCancelLeaveCandidates,
256
		TooLowCandidateCountToLeaveCandidates,
257
		TooLowDelegationCountToDelegate,
258
		TooLowCandidateDelegationCountToDelegate,
259
		TooLowCandidateDelegationCountToLeaveCandidates,
260
		TooLowDelegationCountToLeaveDelegators,
261
		PendingCandidateRequestsDNE,
262
		PendingCandidateRequestAlreadyExists,
263
		PendingCandidateRequestNotDueYet,
264
		PendingDelegationRequestDNE,
265
		PendingDelegationRequestAlreadyExists,
266
		PendingDelegationRequestNotDueYet,
267
		CannotDelegateLessThanOrEqualToLowestBottomWhenFull,
268
		PendingDelegationRevoke,
269
		TooLowDelegationCountToAutoCompound,
270
		TooLowCandidateAutoCompoundingDelegationCountToAutoCompound,
271
		TooLowCandidateAutoCompoundingDelegationCountToDelegate,
272
		TooLowCollatorCountToNotifyAsInactive,
273
		CannotBeNotifiedAsInactive,
274
		TooLowCandidateAutoCompoundingDelegationCountToLeaveCandidates,
275
		TooLowCandidateCountWeightHint,
276
		TooLowCandidateCountWeightHintGoOffline,
277
		CandidateLimitReached,
278
		CannotSetAboveMaxCandidates,
279
		MarkingOfflineNotEnabled,
280
		CurrentRoundTooLow,
281
		EmptyMigrationBatch,
282
	}
283

            
284
20
	#[pallet::event]
285
3516
	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
286
	pub enum Event<T: Config> {
287
		/// Started new round.
288
		NewRound {
289
			starting_block: BlockNumberFor<T>,
290
			round: RoundIndex,
291
			selected_collators_number: u32,
292
			total_balance: BalanceOf<T>,
293
		},
294
		/// Account joined the set of collator candidates.
295
		JoinedCollatorCandidates {
296
			account: T::AccountId,
297
			amount_locked: BalanceOf<T>,
298
			new_total_amt_locked: BalanceOf<T>,
299
		},
300
		/// Candidate selected for collators. Total Exposed Amount includes all delegations.
301
		CollatorChosen {
302
			round: RoundIndex,
303
			collator_account: T::AccountId,
304
			total_exposed_amount: BalanceOf<T>,
305
		},
306
		/// Candidate requested to decrease a self bond.
307
		CandidateBondLessRequested {
308
			candidate: T::AccountId,
309
			amount_to_decrease: BalanceOf<T>,
310
			execute_round: RoundIndex,
311
		},
312
		/// Candidate has increased a self bond.
313
		CandidateBondedMore {
314
			candidate: T::AccountId,
315
			amount: BalanceOf<T>,
316
			new_total_bond: BalanceOf<T>,
317
		},
318
		/// Candidate has decreased a self bond.
319
		CandidateBondedLess {
320
			candidate: T::AccountId,
321
			amount: BalanceOf<T>,
322
			new_bond: BalanceOf<T>,
323
		},
324
		/// Candidate temporarily leave the set of collator candidates without unbonding.
325
		CandidateWentOffline { candidate: T::AccountId },
326
		/// Candidate rejoins the set of collator candidates.
327
		CandidateBackOnline { candidate: T::AccountId },
328
		/// Candidate has requested to leave the set of candidates.
329
		CandidateScheduledExit {
330
			exit_allowed_round: RoundIndex,
331
			candidate: T::AccountId,
332
			scheduled_exit: RoundIndex,
333
		},
334
		/// Cancelled request to leave the set of candidates.
335
		CancelledCandidateExit { candidate: T::AccountId },
336
		/// Cancelled request to decrease candidate's bond.
337
		CancelledCandidateBondLess {
338
			candidate: T::AccountId,
339
			amount: BalanceOf<T>,
340
			execute_round: RoundIndex,
341
		},
342
		/// Candidate has left the set of candidates.
343
		CandidateLeft {
344
			ex_candidate: T::AccountId,
345
			unlocked_amount: BalanceOf<T>,
346
			new_total_amt_locked: BalanceOf<T>,
347
		},
348
		/// Delegator requested to decrease a bond for the collator candidate.
349
		DelegationDecreaseScheduled {
350
			delegator: T::AccountId,
351
			candidate: T::AccountId,
352
			amount_to_decrease: BalanceOf<T>,
353
			execute_round: RoundIndex,
354
		},
355
		// Delegation increased.
356
		DelegationIncreased {
357
			delegator: T::AccountId,
358
			candidate: T::AccountId,
359
			amount: BalanceOf<T>,
360
			in_top: bool,
361
		},
362
		// Delegation decreased.
363
		DelegationDecreased {
364
			delegator: T::AccountId,
365
			candidate: T::AccountId,
366
			amount: BalanceOf<T>,
367
			in_top: bool,
368
		},
369
		/// Delegator requested to leave the set of delegators.
370
		DelegatorExitScheduled {
371
			round: RoundIndex,
372
			delegator: T::AccountId,
373
			scheduled_exit: RoundIndex,
374
		},
375
		/// Delegator requested to revoke delegation.
376
		DelegationRevocationScheduled {
377
			round: RoundIndex,
378
			delegator: T::AccountId,
379
			candidate: T::AccountId,
380
			scheduled_exit: RoundIndex,
381
		},
382
		/// Delegator has left the set of delegators.
383
		DelegatorLeft {
384
			delegator: T::AccountId,
385
			unstaked_amount: BalanceOf<T>,
386
		},
387
		/// Delegation revoked.
388
		DelegationRevoked {
389
			delegator: T::AccountId,
390
			candidate: T::AccountId,
391
			unstaked_amount: BalanceOf<T>,
392
		},
393
		/// Delegation kicked.
394
		DelegationKicked {
395
			delegator: T::AccountId,
396
			candidate: T::AccountId,
397
			unstaked_amount: BalanceOf<T>,
398
		},
399
		/// Cancelled a pending request to exit the set of delegators.
400
		DelegatorExitCancelled { delegator: T::AccountId },
401
		/// Cancelled request to change an existing delegation.
402
		CancelledDelegationRequest {
403
			delegator: T::AccountId,
404
			cancelled_request: CancelledScheduledRequest<BalanceOf<T>>,
405
			collator: T::AccountId,
406
		},
407
		/// New delegation (increase of the existing one).
408
		Delegation {
409
			delegator: T::AccountId,
410
			locked_amount: BalanceOf<T>,
411
			candidate: T::AccountId,
412
			delegator_position: DelegatorAdded<BalanceOf<T>>,
413
			auto_compound: Percent,
414
		},
415
		/// Delegation from candidate state has been remove.
416
		DelegatorLeftCandidate {
417
			delegator: T::AccountId,
418
			candidate: T::AccountId,
419
			unstaked_amount: BalanceOf<T>,
420
			total_candidate_staked: BalanceOf<T>,
421
		},
422
		/// Paid the account (delegator or collator) the balance as liquid rewards.
423
		Rewarded {
424
			account: T::AccountId,
425
			rewards: BalanceOf<T>,
426
		},
427
		/// Transferred to account which holds funds reserved for parachain bond.
428
		InflationDistributed {
429
			index: u32,
430
			account: T::AccountId,
431
			value: BalanceOf<T>,
432
		},
433
		InflationDistributionConfigUpdated {
434
			old: InflationDistributionConfig<T::AccountId>,
435
			new: InflationDistributionConfig<T::AccountId>,
436
		},
437
		/// Annual inflation input (first 3) was used to derive new per-round inflation (last 3)
438
		InflationSet {
439
			annual_min: Perbill,
440
			annual_ideal: Perbill,
441
			annual_max: Perbill,
442
			round_min: Perbill,
443
			round_ideal: Perbill,
444
			round_max: Perbill,
445
		},
446
		/// Staking expectations set.
447
		StakeExpectationsSet {
448
			expect_min: BalanceOf<T>,
449
			expect_ideal: BalanceOf<T>,
450
			expect_max: BalanceOf<T>,
451
		},
452
		/// Set total selected candidates to this value.
453
		TotalSelectedSet { old: u32, new: u32 },
454
		/// Set collator commission to this value.
455
		CollatorCommissionSet { old: Perbill, new: Perbill },
456
		/// Set blocks per round
457
		BlocksPerRoundSet {
458
			current_round: RoundIndex,
459
			first_block: BlockNumberFor<T>,
460
			old: u32,
461
			new: u32,
462
			new_per_round_inflation_min: Perbill,
463
			new_per_round_inflation_ideal: Perbill,
464
			new_per_round_inflation_max: Perbill,
465
		},
466
		/// Auto-compounding reward percent was set for a delegation.
467
		AutoCompoundSet {
468
			candidate: T::AccountId,
469
			delegator: T::AccountId,
470
			value: Percent,
471
		},
472
		/// Compounded a portion of rewards towards the delegation.
473
		Compounded {
474
			candidate: T::AccountId,
475
			delegator: T::AccountId,
476
			amount: BalanceOf<T>,
477
		},
478
	}
479

            
480
74
	#[pallet::hooks]
481
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
482
30709
		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
483
30709
			let mut weight = <T as Config>::WeightInfo::base_on_initialize();
484
30709

            
485
30709
			let mut round = <Round<T>>::get();
486
30709
			if round.should_update(n) {
487
265
				// fetch current slot number
488
265
				let current_slot: u64 = T::SlotProvider::get().into();
489
265

            
490
265
				// account for SlotProvider read
491
265
				weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 0));
492
265

            
493
265
				// Compute round duration in slots
494
265
				let round_duration = (current_slot.saturating_sub(round.first_slot))
495
265
					.saturating_mul(T::SlotDuration::get());
496
265

            
497
265
				// mutate round
498
265
				round.update(n, current_slot);
499
265
				// notify that new round begin
500
265
				weight = weight.saturating_add(T::OnNewRound::on_new_round(round.current));
501
265
				// pay all stakers for T::RewardPaymentDelay rounds ago
502
265
				weight =
503
265
					weight.saturating_add(Self::prepare_staking_payouts(round, round_duration));
504
265
				// select top collator candidates for next round
505
265
				let (extra_weight, collator_count, _delegation_count, total_staked) =
506
265
					Self::select_top_candidates(round.current);
507
265
				weight = weight.saturating_add(extra_weight);
508
265
				// start next round
509
265
				<Round<T>>::put(round);
510
265
				Self::deposit_event(Event::NewRound {
511
265
					starting_block: round.first,
512
265
					round: round.current,
513
265
					selected_collators_number: collator_count,
514
265
					total_balance: total_staked,
515
265
				});
516
265
				// record inactive collators
517
265
				weight = weight.saturating_add(Self::mark_collators_as_inactive(round.current));
518
265
				// account for Round write
519
265
				weight = weight.saturating_add(T::DbWeight::get().reads_writes(0, 1));
520
30444
			} else {
521
30444
				weight = weight.saturating_add(Self::handle_delayed_payouts(round.current));
522
30444
			}
523

            
524
			// add on_finalize weight
525
			//   read:  Author, Points, AwardedPts, WasInactive
526
			//   write: Points, AwardedPts, WasInactive
527
30709
			weight = weight.saturating_add(T::DbWeight::get().reads_writes(4, 3));
528
30709
			weight
529
30709
		}
530
30647
		fn on_finalize(_n: BlockNumberFor<T>) {
531
30647
			Self::award_points_to_block_author();
532
30647
			Self::cleanup_inactive_collator_info();
533
30647
		}
534
	}
535

            
536
1628
	#[pallet::storage]
537
	#[pallet::getter(fn collator_commission)]
538
	/// Commission percent taken off of rewards for all collators
539
	type CollatorCommission<T: Config> = StorageValue<_, Perbill, ValueQuery>;
540

            
541
2768
	#[pallet::storage]
542
	#[pallet::getter(fn total_selected)]
543
	/// The total candidates selected every round
544
	pub(crate) type TotalSelected<T: Config> = StorageValue<_, u32, ValueQuery>;
545

            
546
1716
	#[pallet::storage]
547
	#[pallet::getter(fn inflation_distribution_info)]
548
	/// Inflation distribution configuration, including accounts that should receive inflation
549
	/// before it is distributed to collators and delegators.
550
	///
551
	/// The sum of the distribution percents must be less than or equal to 100.
552
	pub(crate) type InflationDistributionInfo<T: Config> =
553
		StorageValue<_, InflationDistributionConfig<T::AccountId>, ValueQuery>;
554

            
555
186616
	#[pallet::storage]
556
	#[pallet::getter(fn round)]
557
	/// Current round index and next round scheduled transition
558
	pub type Round<T: Config> = StorageValue<_, RoundInfo<BlockNumberFor<T>>, ValueQuery>;
559

            
560
48678
	#[pallet::storage]
561
	#[pallet::getter(fn delegator_state)]
562
	/// Get delegator state associated with an account if account is delegating else None
563
	pub(crate) type DelegatorState<T: Config> = StorageMap<
564
		_,
565
		Twox64Concat,
566
		T::AccountId,
567
		Delegator<T::AccountId, BalanceOf<T>>,
568
		OptionQuery,
569
	>;
570

            
571
24785
	#[pallet::storage]
572
	#[pallet::getter(fn candidate_info)]
573
	/// Get collator candidate info associated with an account if account is candidate else None
574
	pub(crate) type CandidateInfo<T: Config> =
575
		StorageMap<_, Twox64Concat, T::AccountId, CandidateMetadata<BalanceOf<T>>, OptionQuery>;
576

            
577
	pub struct AddGet<T, R> {
578
		_phantom: PhantomData<(T, R)>,
579
	}
580
	impl<T, R> Get<u32> for AddGet<T, R>
581
	where
582
		T: Get<u32>,
583
		R: Get<u32>,
584
	{
585
371
		fn get() -> u32 {
586
371
			T::get() + R::get()
587
371
		}
588
	}
589

            
590
	/// Stores outstanding delegation requests per collator.
591
1379
	#[pallet::storage]
592
	#[pallet::getter(fn delegation_scheduled_requests)]
593
	pub(crate) type DelegationScheduledRequests<T: Config> = StorageMap<
594
		_,
595
		Blake2_128Concat,
596
		T::AccountId,
597
		BoundedVec<
598
			ScheduledRequest<T::AccountId, BalanceOf<T>>,
599
			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
600
		>,
601
		ValueQuery,
602
	>;
603

            
604
	/// Stores auto-compounding configuration per collator.
605
1155
	#[pallet::storage]
606
	#[pallet::getter(fn auto_compounding_delegations)]
607
	pub(crate) type AutoCompoundingDelegations<T: Config> = StorageMap<
608
		_,
609
		Blake2_128Concat,
610
		T::AccountId,
611
		BoundedVec<
612
			AutoCompoundConfig<T::AccountId>,
613
			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
614
		>,
615
		ValueQuery,
616
	>;
617

            
618
2666
	#[pallet::storage]
619
	#[pallet::getter(fn top_delegations)]
620
	/// Top delegations for collator candidate
621
	pub(crate) type TopDelegations<T: Config> = StorageMap<
622
		_,
623
		Twox64Concat,
624
		T::AccountId,
625
		Delegations<T::AccountId, BalanceOf<T>>,
626
		OptionQuery,
627
	>;
628

            
629
1117
	#[pallet::storage]
630
	#[pallet::getter(fn bottom_delegations)]
631
	/// Bottom delegations for collator candidate
632
	pub(crate) type BottomDelegations<T: Config> = StorageMap<
633
		_,
634
		Twox64Concat,
635
		T::AccountId,
636
		Delegations<T::AccountId, BalanceOf<T>>,
637
		OptionQuery,
638
	>;
639

            
640
1823
	#[pallet::storage]
641
	#[pallet::getter(fn selected_candidates)]
642
	/// The collator candidates selected for the current round
643
	type SelectedCandidates<T: Config> =
644
		StorageValue<_, BoundedVec<T::AccountId, T::MaxCandidates>, ValueQuery>;
645

            
646
5406
	#[pallet::storage]
647
	#[pallet::getter(fn total)]
648
	/// Total capital locked by this staking pallet
649
	pub(crate) type Total<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
650

            
651
6788
	#[pallet::storage]
652
	#[pallet::getter(fn candidate_pool)]
653
	/// The pool of collator candidates, each with their total backing stake
654
	pub(crate) type CandidatePool<T: Config> = StorageValue<
655
		_,
656
		BoundedOrderedSet<Bond<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,
657
		ValueQuery,
658
	>;
659

            
660
1972
	#[pallet::storage]
661
	#[pallet::getter(fn at_stake)]
662
	/// Snapshot of collator delegation stake at the start of the round
663
	pub type AtStake<T: Config> = StorageDoubleMap<
664
		_,
665
		Twox64Concat,
666
		RoundIndex,
667
		Twox64Concat,
668
		T::AccountId,
669
		CollatorSnapshot<T::AccountId, BalanceOf<T>>,
670
		OptionQuery,
671
	>;
672

            
673
8230
	#[pallet::storage]
674
	#[pallet::getter(fn was_inactive)]
675
	/// Records collators' inactivity.
676
	/// Data persists for MaxOfflineRounds + 1 rounds before being pruned.
677
	pub type WasInactive<T: Config> =
678
		StorageDoubleMap<_, Twox64Concat, RoundIndex, Twox64Concat, T::AccountId, (), OptionQuery>;
679

            
680
14299
	#[pallet::storage]
681
	#[pallet::getter(fn delayed_payouts)]
682
	/// Delayed payouts
683
	pub type DelayedPayouts<T: Config> =
684
		StorageMap<_, Twox64Concat, RoundIndex, DelayedPayout<BalanceOf<T>>, OptionQuery>;
685

            
686
1706
	#[pallet::storage]
687
	#[pallet::getter(fn inflation_config)]
688
	/// Inflation configuration
689
	pub type InflationConfig<T: Config> = StorageValue<_, InflationInfo<BalanceOf<T>>, ValueQuery>;
690

            
691
31428
	#[pallet::storage]
692
	#[pallet::getter(fn points)]
693
	/// Total points awarded to collators for block production in the round
694
	pub type Points<T: Config> = StorageMap<_, Twox64Concat, RoundIndex, RewardPoint, ValueQuery>;
695

            
696
62263
	#[pallet::storage]
697
	#[pallet::getter(fn awarded_pts)]
698
	/// Points for each collator per round
699
	pub type AwardedPts<T: Config> = StorageDoubleMap<
700
		_,
701
		Twox64Concat,
702
		RoundIndex,
703
		Twox64Concat,
704
		T::AccountId,
705
		RewardPoint,
706
		ValueQuery,
707
	>;
708

            
709
54
	#[pallet::storage]
710
	#[pallet::getter(fn marking_offline)]
711
	/// Killswitch to enable/disable marking offline feature.
712
	pub type EnableMarkingOffline<T: Config> = StorageValue<_, bool, ValueQuery>;
713

            
714
2120
	#[pallet::storage]
715
	/// Temporary storage to track candidates that have been migrated from locks to freezes.
716
	/// This storage should be removed after all accounts have been migrated.
717
	pub type MigratedCandidates<T: Config> =
718
		StorageMap<_, Twox64Concat, T::AccountId, (), OptionQuery>;
719

            
720
19524
	#[pallet::storage]
721
	/// Temporary storage to track delegators that have been migrated from locks to freezes.
722
	/// This storage should be removed after all accounts have been migrated.
723
	pub type MigratedDelegators<T: Config> =
724
		StorageMap<_, Twox64Concat, T::AccountId, (), OptionQuery>;
725

            
726
	#[pallet::genesis_config]
727
	pub struct GenesisConfig<T: Config> {
728
		/// Initialize balance and register all as collators: `(collator AccountId, balance Amount)`
729
		pub candidates: Vec<(T::AccountId, BalanceOf<T>)>,
730
		/// Initialize balance and make delegations:
731
		/// `(delegator AccountId, collator AccountId, delegation Amount, auto-compounding Percent)`
732
		pub delegations: Vec<(T::AccountId, T::AccountId, BalanceOf<T>, Percent)>,
733
		/// Inflation configuration
734
		pub inflation_config: InflationInfo<BalanceOf<T>>,
735
		/// Default fixed percent a collator takes off the top of due rewards
736
		pub collator_commission: Perbill,
737
		/// Default percent of inflation set aside for parachain bond every round
738
		pub parachain_bond_reserve_percent: Percent,
739
		/// Default number of blocks in a round
740
		pub blocks_per_round: u32,
741
		/// Number of selected candidates every round. Cannot be lower than MinSelectedCandidates
742
		pub num_selected_candidates: u32,
743
	}
744

            
745
	impl<T: Config> Default for GenesisConfig<T> {
746
3
		fn default() -> Self {
747
3
			Self {
748
3
				candidates: vec![],
749
3
				delegations: vec![],
750
3
				inflation_config: Default::default(),
751
3
				collator_commission: Default::default(),
752
3
				parachain_bond_reserve_percent: Default::default(),
753
3
				blocks_per_round: 1u32,
754
3
				num_selected_candidates: T::MinSelectedCandidates::get(),
755
3
			}
756
3
		}
757
	}
758

            
759
528
	#[pallet::genesis_build]
760
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
761
531
		fn build(&self) {
762
531
			assert!(self.blocks_per_round > 0, "Blocks per round must be > 0");
763
531
			<InflationConfig<T>>::put(self.inflation_config.clone());
764
531
			let mut candidate_count = 0u32;
765
			// Initialize the candidates
766
1171
			for &(ref candidate, balance) in &self.candidates {
767
640
				assert!(
768
640
					<Pallet<T>>::get_collator_stakable_free_balance(candidate) >= balance,
769
					"Account does not have enough balance to bond as a candidate."
770
				);
771
640
				if let Err(error) = <Pallet<T>>::join_candidates(
772
640
					T::RuntimeOrigin::from(Some(candidate.clone()).into()),
773
640
					balance,
774
640
					candidate_count,
775
640
				) {
776
13
					log::warn!("Join candidates failed in genesis with error {:?}", error);
777
627
				} else {
778
627
					candidate_count = candidate_count.saturating_add(1u32);
779
627
				}
780
			}
781

            
782
531
			let mut col_delegator_count: BTreeMap<T::AccountId, u32> = BTreeMap::new();
783
531
			let mut col_auto_compound_delegator_count: BTreeMap<T::AccountId, u32> =
784
531
				BTreeMap::new();
785
531
			let mut del_delegation_count: BTreeMap<T::AccountId, u32> = BTreeMap::new();
786
			// Initialize the delegations
787
9537
			for &(ref delegator, ref target, balance, auto_compound) in &self.delegations {
788
9006
				assert!(
789
9006
					<Pallet<T>>::get_delegator_stakable_balance(delegator) >= balance,
790
					"Account does not have enough balance to place delegation."
791
				);
792
9006
				let cd_count = if let Some(x) = col_delegator_count.get(target) {
793
8780
					*x
794
				} else {
795
226
					0u32
796
				};
797
9006
				let dd_count = if let Some(x) = del_delegation_count.get(delegator) {
798
44
					*x
799
				} else {
800
8962
					0u32
801
				};
802
9006
				let cd_auto_compound_count = col_auto_compound_delegator_count
803
9006
					.get(target)
804
9006
					.cloned()
805
9006
					.unwrap_or_default();
806
9006
				if let Err(error) = <Pallet<T>>::delegate_with_auto_compound(
807
9006
					T::RuntimeOrigin::from(Some(delegator.clone()).into()),
808
9006
					target.clone(),
809
9006
					balance,
810
9006
					auto_compound,
811
9006
					cd_count,
812
9006
					cd_auto_compound_count,
813
9006
					dd_count,
814
9006
				) {
815
8472
					log::warn!("Delegate failed in genesis with error {:?}", error);
816
				} else {
817
534
					if let Some(x) = col_delegator_count.get_mut(target) {
818
312
						*x = x.saturating_add(1u32);
819
354
					} else {
820
222
						col_delegator_count.insert(target.clone(), 1u32);
821
222
					};
822
534
					if let Some(x) = del_delegation_count.get_mut(delegator) {
823
44
						*x = x.saturating_add(1u32);
824
490
					} else {
825
490
						del_delegation_count.insert(delegator.clone(), 1u32);
826
490
					};
827
534
					if !auto_compound.is_zero() {
828
4
						col_auto_compound_delegator_count
829
4
							.entry(target.clone())
830
4
							.and_modify(|x| *x = x.saturating_add(1))
831
4
							.or_insert(1);
832
530
					}
833
				}
834
			}
835
			// Set collator commission to default config
836
531
			<CollatorCommission<T>>::put(self.collator_commission);
837
531
			// Set parachain bond config to default config
838
531
			let pbr = InflationDistributionAccount {
839
531
				// must be set soon; if not => due inflation will be sent to collators/delegators
840
531
				account: T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
841
531
					.expect("infinite length input; no invalid inputs for type; qed"),
842
531
				percent: self.parachain_bond_reserve_percent,
843
531
			};
844
531
			let zeroed_account = InflationDistributionAccount {
845
531
				account: T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
846
531
					.expect("infinite length input; no invalid inputs for type; qed"),
847
531
				percent: Percent::zero(),
848
531
			};
849
531
			<InflationDistributionInfo<T>>::put::<InflationDistributionConfig<T::AccountId>>(
850
531
				[pbr, zeroed_account].into(),
851
531
			);
852
531
			// Set total selected candidates to value from config
853
531
			assert!(
854
531
				self.num_selected_candidates >= T::MinSelectedCandidates::get(),
855
				"{:?}",
856
				Error::<T>::CannotSetBelowMin
857
			);
858
531
			assert!(
859
531
				self.num_selected_candidates <= T::MaxCandidates::get(),
860
				"{:?}",
861
				Error::<T>::CannotSetAboveMaxCandidates
862
			);
863
531
			<TotalSelected<T>>::put(self.num_selected_candidates);
864
531
			// Choose top TotalSelected collator candidates
865
531
			let (_, v_count, _, total_staked) = <Pallet<T>>::select_top_candidates(1u32);
866
531
			// Start Round 1 at Block 0
867
531
			let round: RoundInfo<BlockNumberFor<T>> =
868
531
				RoundInfo::new(1u32, Zero::zero(), self.blocks_per_round, 0);
869
531
			<Round<T>>::put(round);
870
531
			<Pallet<T>>::deposit_event(Event::NewRound {
871
531
				starting_block: Zero::zero(),
872
531
				round: 1u32,
873
531
				selected_collators_number: v_count,
874
531
				total_balance: total_staked,
875
531
			});
876
531
		}
877
	}
878

            
879
20
	#[pallet::call]
880
	impl<T: Config> Pallet<T> {
881
		/// Set the expectations for total staked. These expectations determine the issuance for
882
		/// the round according to logic in `fn compute_issuance`
883
		#[pallet::call_index(0)]
884
		#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
885
		pub fn set_staking_expectations(
886
			origin: OriginFor<T>,
887
			expectations: Range<BalanceOf<T>>,
888
6
		) -> DispatchResultWithPostInfo {
889
6
			T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
890
5
			ensure!(expectations.is_valid(), Error::<T>::InvalidSchedule);
891
4
			let mut config = <InflationConfig<T>>::get();
892
4
			ensure!(
893
4
				config.expect != expectations,
894
1
				Error::<T>::NoWritingSameValue
895
			);
896
3
			config.set_expectations(expectations);
897
3
			Self::deposit_event(Event::StakeExpectationsSet {
898
3
				expect_min: config.expect.min,
899
3
				expect_ideal: config.expect.ideal,
900
3
				expect_max: config.expect.max,
901
3
			});
902
3
			<InflationConfig<T>>::put(config);
903
3
			Ok(().into())
904
		}
905

            
906
		/// Set the annual inflation rate to derive per-round inflation
907
		#[pallet::call_index(1)]
908
		#[pallet::weight(<T as Config>::WeightInfo::set_inflation())]
909
		pub fn set_inflation(
910
			origin: OriginFor<T>,
911
			schedule: Range<Perbill>,
912
7
		) -> DispatchResultWithPostInfo {
913
7
			T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
914
5
			ensure!(schedule.is_valid(), Error::<T>::InvalidSchedule);
915
4
			let mut config = <InflationConfig<T>>::get();
916
4
			ensure!(config.annual != schedule, Error::<T>::NoWritingSameValue);
917
3
			config.annual = schedule;
918
3
			config.set_round_from_annual::<T>(schedule);
919
3
			Self::deposit_event(Event::InflationSet {
920
3
				annual_min: config.annual.min,
921
3
				annual_ideal: config.annual.ideal,
922
3
				annual_max: config.annual.max,
923
3
				round_min: config.round.min,
924
3
				round_ideal: config.round.ideal,
925
3
				round_max: config.round.max,
926
3
			});
927
3
			<InflationConfig<T>>::put(config);
928
3
			Ok(().into())
929
		}
930

            
931
		/// Set the total number of collator candidates selected per round
932
		/// - changes are not applied until the start of the next round
933
		#[pallet::call_index(4)]
934
		#[pallet::weight(<T as Config>::WeightInfo::set_total_selected())]
935
11
		pub fn set_total_selected(origin: OriginFor<T>, new: u32) -> DispatchResultWithPostInfo {
936
11
			frame_system::ensure_root(origin)?;
937
10
			ensure!(
938
10
				new >= T::MinSelectedCandidates::get(),
939
1
				Error::<T>::CannotSetBelowMin
940
			);
941
9
			ensure!(
942
9
				new <= T::MaxCandidates::get(),
943
1
				Error::<T>::CannotSetAboveMaxCandidates
944
			);
945
8
			let old = <TotalSelected<T>>::get();
946
8
			ensure!(old != new, Error::<T>::NoWritingSameValue);
947
7
			ensure!(
948
7
				new < <Round<T>>::get().length,
949
2
				Error::<T>::RoundLengthMustBeGreaterThanTotalSelectedCollators,
950
			);
951
5
			<TotalSelected<T>>::put(new);
952
5
			Self::deposit_event(Event::TotalSelectedSet { old, new });
953
5
			Ok(().into())
954
		}
955

            
956
		/// Set the commission for all collators
957
		#[pallet::call_index(5)]
958
		#[pallet::weight(<T as Config>::WeightInfo::set_collator_commission())]
959
		pub fn set_collator_commission(
960
			origin: OriginFor<T>,
961
			new: Perbill,
962
4
		) -> DispatchResultWithPostInfo {
963
4
			frame_system::ensure_root(origin)?;
964
3
			let old = <CollatorCommission<T>>::get();
965
3
			ensure!(old != new, Error::<T>::NoWritingSameValue);
966
2
			<CollatorCommission<T>>::put(new);
967
2
			Self::deposit_event(Event::CollatorCommissionSet { old, new });
968
2
			Ok(().into())
969
		}
970

            
971
		/// Set blocks per round
972
		/// - if called with `new` less than length of current round, will transition immediately
973
		/// in the next block
974
		/// - also updates per-round inflation config
975
		#[pallet::call_index(6)]
976
		#[pallet::weight(<T as Config>::WeightInfo::set_blocks_per_round())]
977
17
		pub fn set_blocks_per_round(origin: OriginFor<T>, new: u32) -> DispatchResultWithPostInfo {
978
17
			frame_system::ensure_root(origin)?;
979
16
			ensure!(
980
16
				new >= T::MinBlocksPerRound::get(),
981
1
				Error::<T>::CannotSetBelowMin
982
			);
983
15
			let mut round = <Round<T>>::get();
984
15
			let (now, first, old) = (round.current, round.first, round.length);
985
15
			ensure!(old != new, Error::<T>::NoWritingSameValue);
986
14
			ensure!(
987
14
				new > <TotalSelected<T>>::get(),
988
2
				Error::<T>::RoundLengthMustBeGreaterThanTotalSelectedCollators,
989
			);
990
12
			round.length = new;
991
12
			// update per-round inflation given new rounds per year
992
12
			let mut inflation_config = <InflationConfig<T>>::get();
993
12
			inflation_config.reset_round::<T>(new);
994
12
			<Round<T>>::put(round);
995
12
			Self::deposit_event(Event::BlocksPerRoundSet {
996
12
				current_round: now,
997
12
				first_block: first,
998
12
				old: old,
999
12
				new: new,
12
				new_per_round_inflation_min: inflation_config.round.min,
12
				new_per_round_inflation_ideal: inflation_config.round.ideal,
12
				new_per_round_inflation_max: inflation_config.round.max,
12
			});
12
			<InflationConfig<T>>::put(inflation_config);
12
			Ok(().into())
		}
		/// Join the set of collator candidates
		#[pallet::call_index(7)]
		#[pallet::weight(<T as Config>::WeightInfo::join_candidates(*candidate_count))]
		pub fn join_candidates(
			origin: OriginFor<T>,
			bond: BalanceOf<T>,
			candidate_count: u32,
680
		) -> DispatchResultWithPostInfo {
680
			let acc = ensure_signed(origin)?;
680
			ensure!(
680
				bond >= T::MinCandidateStk::get(),
14
				Error::<T>::CandidateBondBelowMin
			);
666
			Self::join_candidates_inner(acc, bond, candidate_count)
		}
		/// Request to leave the set of candidates. If successful, the account is immediately
		/// removed from the candidate pool to prevent selection as a collator.
		#[pallet::call_index(8)]
		#[pallet::weight(<T as Config>::WeightInfo::schedule_leave_candidates(*candidate_count))]
		pub fn schedule_leave_candidates(
			origin: OriginFor<T>,
			candidate_count: u32,
48
		) -> DispatchResultWithPostInfo {
48
			let collator = ensure_signed(origin)?;
48
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
47
			let (now, when) = state.schedule_leave::<T>()?;
46
			let mut candidates = <CandidatePool<T>>::get();
46
			ensure!(
46
				candidate_count >= candidates.0.len() as u32,
5
				Error::<T>::TooLowCandidateCountToLeaveCandidates
			);
41
			if candidates.remove(&Bond::from_owner(collator.clone())) {
41
				<CandidatePool<T>>::put(candidates);
41
			}
41
			<CandidateInfo<T>>::insert(&collator, state);
41
			Self::deposit_event(Event::CandidateScheduledExit {
41
				exit_allowed_round: now,
41
				candidate: collator,
41
				scheduled_exit: when,
41
			});
41
			Ok(().into())
		}
		/// Execute leave candidates request
		#[pallet::call_index(9)]
		#[pallet::weight(
			<T as Config>::WeightInfo::execute_leave_candidates_worst_case(*candidate_delegation_count)
		)]
		pub fn execute_leave_candidates(
			origin: OriginFor<T>,
			candidate: T::AccountId,
			candidate_delegation_count: u32,
27
		) -> DispatchResultWithPostInfo {
27
			ensure_signed(origin)?;
27
			let state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
27
			ensure!(
27
				state.delegation_count <= candidate_delegation_count,
3
				Error::<T>::TooLowCandidateDelegationCountToLeaveCandidates
			);
24
			<Pallet<T>>::execute_leave_candidates_inner(candidate)
		}
		/// Cancel open request to leave candidates
		/// - only callable by collator account
		/// - result upon successful call is the candidate is active in the candidate pool
		#[pallet::call_index(10)]
		#[pallet::weight(<T as Config>::WeightInfo::cancel_leave_candidates(*candidate_count))]
		pub fn cancel_leave_candidates(
			origin: OriginFor<T>,
			candidate_count: u32,
4
		) -> DispatchResultWithPostInfo {
4
			let collator = ensure_signed(origin)?;
4
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
4
			ensure!(state.is_leaving(), Error::<T>::CandidateNotLeaving);
4
			state.go_online();
4
			let mut candidates = <CandidatePool<T>>::get();
4
			ensure!(
4
				candidates.0.len() as u32 <= candidate_count,
				Error::<T>::TooLowCandidateCountWeightHintCancelLeaveCandidates
			);
4
			let maybe_inserted_candidate = candidates
4
				.try_insert(Bond {
4
					owner: collator.clone(),
4
					amount: state.total_counted,
4
				})
4
				.map_err(|_| Error::<T>::CandidateLimitReached)?;
4
			ensure!(maybe_inserted_candidate, Error::<T>::AlreadyActive);
4
			<CandidatePool<T>>::put(candidates);
4
			<CandidateInfo<T>>::insert(&collator, state);
4
			Self::deposit_event(Event::CancelledCandidateExit {
4
				candidate: collator,
4
			});
4
			Ok(().into())
		}
		/// Temporarily leave the set of collator candidates without unbonding
		#[pallet::call_index(11)]
		#[pallet::weight(<T as Config>::WeightInfo::go_offline(MAX_CANDIDATES))]
14
		pub fn go_offline(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
14
			let collator = ensure_signed(origin)?;
14
			<Pallet<T>>::go_offline_inner(collator)
		}
		/// Rejoin the set of collator candidates if previously had called `go_offline`
		#[pallet::call_index(12)]
		#[pallet::weight(<T as Config>::WeightInfo::go_online(MAX_CANDIDATES))]
7
		pub fn go_online(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
7
			let collator = ensure_signed(origin)?;
7
			<Pallet<T>>::go_online_inner(collator)
		}
		/// Increase collator candidate self bond by `more`
		#[pallet::call_index(13)]
		#[pallet::weight(<T as Config>::WeightInfo::candidate_bond_more(MAX_CANDIDATES))]
		pub fn candidate_bond_more(
			origin: OriginFor<T>,
			more: BalanceOf<T>,
8
		) -> DispatchResultWithPostInfo {
8
			let candidate = ensure_signed(origin)?;
8
			<Pallet<T>>::candidate_bond_more_inner(candidate, more)
		}
		/// Request by collator candidate to decrease self bond by `less`
		#[pallet::call_index(14)]
		#[pallet::weight(<T as Config>::WeightInfo::schedule_candidate_bond_less())]
		pub fn schedule_candidate_bond_less(
			origin: OriginFor<T>,
			less: BalanceOf<T>,
20
		) -> DispatchResultWithPostInfo {
20
			let collator = ensure_signed(origin)?;
20
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
18
			let when = state.schedule_bond_less::<T>(less)?;
16
			<CandidateInfo<T>>::insert(&collator, state);
16
			Self::deposit_event(Event::CandidateBondLessRequested {
16
				candidate: collator,
16
				amount_to_decrease: less,
16
				execute_round: when,
16
			});
16
			Ok(().into())
		}
		/// Execute pending request to adjust the collator candidate self bond
		#[pallet::call_index(15)]
		#[pallet::weight(<T as Config>::WeightInfo::execute_candidate_bond_less(MAX_CANDIDATES))]
		pub fn execute_candidate_bond_less(
			origin: OriginFor<T>,
			candidate: T::AccountId,
7
		) -> DispatchResultWithPostInfo {
7
			ensure_signed(origin)?; // we may want to reward this if caller != candidate
7
			<Pallet<T>>::execute_candidate_bond_less_inner(candidate)
		}
		/// Cancel pending request to adjust the collator candidate self bond
		#[pallet::call_index(16)]
		#[pallet::weight(<T as Config>::WeightInfo::cancel_candidate_bond_less())]
4
		pub fn cancel_candidate_bond_less(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
4
			let collator = ensure_signed(origin)?;
4
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
3
			state.cancel_bond_less::<T>(collator.clone())?;
3
			<CandidateInfo<T>>::insert(&collator, state);
3
			Ok(().into())
		}
		/// If caller is not a delegator and not a collator, then join the set of delegators
		/// If caller is a delegator, then makes delegation to change their delegation state
		/// Sets the auto-compound config for the delegation
		#[pallet::call_index(18)]
		#[pallet::weight(
			<T as Config>::WeightInfo::delegate_with_auto_compound(
				*candidate_delegation_count,
				*candidate_auto_compounding_delegation_count,
				*delegation_count,
			)
		)]
		pub fn delegate_with_auto_compound(
			origin: OriginFor<T>,
			candidate: T::AccountId,
			amount: BalanceOf<T>,
			auto_compound: Percent,
			candidate_delegation_count: u32,
			candidate_auto_compounding_delegation_count: u32,
			delegation_count: u32,
9084
		) -> DispatchResultWithPostInfo {
9084
			let delegator = ensure_signed(origin)?;
9084
			<AutoCompoundDelegations<T>>::delegate_with_auto_compound(
9084
				candidate,
9084
				delegator,
9084
				amount,
9084
				auto_compound,
9084
				candidate_delegation_count,
9084
				candidate_auto_compounding_delegation_count,
9084
				delegation_count,
9084
			)
		}
		/// Request to revoke an existing delegation. If successful, the delegation is scheduled
		/// to be allowed to be revoked via the `execute_delegation_request` extrinsic.
		/// The delegation receives no rewards for the rounds while a revoke is pending.
		/// A revoke may not be performed if any other scheduled request is pending.
		#[pallet::call_index(22)]
		#[pallet::weight(<T as Config>::WeightInfo::schedule_revoke_delegation(
			T::MaxTopDelegationsPerCandidate::get() + T::MaxBottomDelegationsPerCandidate::get()
		))]
		pub fn schedule_revoke_delegation(
			origin: OriginFor<T>,
			collator: T::AccountId,
51
		) -> DispatchResultWithPostInfo {
51
			let delegator = ensure_signed(origin)?;
51
			Self::delegation_schedule_revoke(collator, delegator)
		}
		/// Bond more for delegators wrt a specific collator candidate.
		#[pallet::call_index(23)]
		#[pallet::weight(<T as Config>::WeightInfo::delegator_bond_more(
			T::MaxTopDelegationsPerCandidate::get() + T::MaxBottomDelegationsPerCandidate::get()
		))]
		pub fn delegator_bond_more(
			origin: OriginFor<T>,
			candidate: T::AccountId,
			more: BalanceOf<T>,
19
		) -> DispatchResultWithPostInfo {
19
			let delegator = ensure_signed(origin)?;
19
			let (in_top, weight) = Self::delegation_bond_more_without_event(
19
				delegator.clone(),
19
				candidate.clone(),
19
				more.clone(),
19
			)?;
18
			Pallet::<T>::deposit_event(Event::DelegationIncreased {
18
				delegator,
18
				candidate,
18
				amount: more,
18
				in_top,
18
			});
18

            
18
			Ok(Some(weight).into())
		}
		/// Request bond less for delegators wrt a specific collator candidate. The delegation's
		/// rewards for rounds while the request is pending use the reduced bonded amount.
		/// A bond less may not be performed if any other scheduled request is pending.
		#[pallet::call_index(24)]
		#[pallet::weight(<T as Config>::WeightInfo::schedule_delegator_bond_less(
			T::MaxTopDelegationsPerCandidate::get() + T::MaxBottomDelegationsPerCandidate::get()
		))]
		pub fn schedule_delegator_bond_less(
			origin: OriginFor<T>,
			candidate: T::AccountId,
			less: BalanceOf<T>,
31
		) -> DispatchResultWithPostInfo {
31
			let delegator = ensure_signed(origin)?;
31
			Self::delegation_schedule_bond_decrease(candidate, delegator, less)
		}
		/// Execute pending request to change an existing delegation
		#[pallet::call_index(25)]
		#[pallet::weight(<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst())]
		pub fn execute_delegation_request(
			origin: OriginFor<T>,
			delegator: T::AccountId,
			candidate: T::AccountId,
37
		) -> DispatchResultWithPostInfo {
37
			ensure_signed(origin)?; // we may want to reward caller if caller != delegator
37
			Self::delegation_execute_scheduled_request(candidate, delegator)
		}
		/// Cancel request to change an existing delegation.
		#[pallet::call_index(26)]
		#[pallet::weight(<T as Config>::WeightInfo::cancel_delegation_request(350))]
		pub fn cancel_delegation_request(
			origin: OriginFor<T>,
			candidate: T::AccountId,
10
		) -> DispatchResultWithPostInfo {
10
			let delegator = ensure_signed(origin)?;
10
			Self::delegation_cancel_request(candidate, delegator)
		}
		/// Sets the auto-compounding reward percentage for a delegation.
		#[pallet::call_index(27)]
		#[pallet::weight(<T as Config>::WeightInfo::set_auto_compound(
			*candidate_auto_compounding_delegation_count_hint,
			*delegation_count_hint,
		))]
		pub fn set_auto_compound(
			origin: OriginFor<T>,
			candidate: T::AccountId,
			value: Percent,
			candidate_auto_compounding_delegation_count_hint: u32,
			delegation_count_hint: u32,
21
		) -> DispatchResultWithPostInfo {
21
			let delegator = ensure_signed(origin)?;
21
			<AutoCompoundDelegations<T>>::set_auto_compound(
21
				candidate,
21
				delegator,
21
				value,
21
				candidate_auto_compounding_delegation_count_hint,
21
				delegation_count_hint,
21
			)
		}
		/// Notify a collator is inactive during MaxOfflineRounds
		#[pallet::call_index(29)]
		#[pallet::weight(<T as Config>::WeightInfo::notify_inactive_collator())]
		pub fn notify_inactive_collator(
			origin: OriginFor<T>,
			collator: T::AccountId,
6
		) -> DispatchResult {
6
			ensure!(
6
				<EnableMarkingOffline<T>>::get(),
				<Error<T>>::MarkingOfflineNotEnabled
			);
6
			ensure_signed(origin)?;
6
			let mut collators_len = 0usize;
6
			let max_collators = <TotalSelected<T>>::get();
6
			if let Some(len) = <SelectedCandidates<T>>::decode_len() {
6
				collators_len = len;
6
			};
			// Check collators length is not below or eq to 66% of max_collators.
			// We use saturating logic here with (2/3)
			// as it is dangerous to use floating point numbers directly.
6
			ensure!(
6
				collators_len * 3 > (max_collators * 2) as usize,
1
				<Error<T>>::TooLowCollatorCountToNotifyAsInactive
			);
5
			let round_info = <Round<T>>::get();
5
			let max_offline_rounds = T::MaxOfflineRounds::get();
5

            
5
			ensure!(
5
				round_info.current > max_offline_rounds,
1
				<Error<T>>::CurrentRoundTooLow
			);
			// Have rounds_to_check = [8,9]
			// in case we are in round 10 for instance
			// with MaxOfflineRounds = 2
4
			let first_round_to_check = round_info.current.saturating_sub(max_offline_rounds);
4
			let rounds_to_check = first_round_to_check..round_info.current;
4

            
4
			// If this counter is eq to max_offline_rounds,
4
			// the collator should be notified as inactive
4
			let mut inactive_counter: RoundIndex = 0u32;
			// Iter rounds and check whether the collator has been inactive
12
			for r in rounds_to_check {
8
				if <WasInactive<T>>::get(r, &collator).is_some() {
4
					inactive_counter = inactive_counter.saturating_add(1);
4
				}
			}
4
			if inactive_counter == max_offline_rounds {
2
				let _ = T::OnInactiveCollator::on_inactive_collator(
2
					collator.clone(),
2
					round_info.current.saturating_sub(1),
2
				);
2
			} else {
2
				return Err(<Error<T>>::CannotBeNotifiedAsInactive.into());
			}
2
			Ok(().into())
		}
		/// Enable/Disable marking offline feature
		#[pallet::call_index(30)]
		#[pallet::weight(
			Weight::from_parts(3_000_000u64, 4_000u64)
				.saturating_add(T::DbWeight::get().writes(1u64))
		)]
3
		pub fn enable_marking_offline(origin: OriginFor<T>, value: bool) -> DispatchResult {
3
			ensure_root(origin)?;
2
			<EnableMarkingOffline<T>>::set(value);
2
			Ok(())
		}
		/// Force join the set of collator candidates.
		/// It will skip the minimum required bond check.
		#[pallet::call_index(31)]
		#[pallet::weight(<T as Config>::WeightInfo::join_candidates(*candidate_count))]
		pub fn force_join_candidates(
			origin: OriginFor<T>,
			account: T::AccountId,
			bond: BalanceOf<T>,
			candidate_count: u32,
1
		) -> DispatchResultWithPostInfo {
1
			T::MonetaryGovernanceOrigin::ensure_origin(origin.clone())?;
1
			Self::join_candidates_inner(account, bond, candidate_count)
		}
		/// Set the inflation distribution configuration.
		#[pallet::call_index(32)]
		#[pallet::weight(<T as Config>::WeightInfo::set_inflation_distribution_config())]
		pub fn set_inflation_distribution_config(
			origin: OriginFor<T>,
			new: InflationDistributionConfig<T::AccountId>,
30
		) -> DispatchResultWithPostInfo {
30
			T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
29
			let old = <InflationDistributionInfo<T>>::get().0;
29
			let new = new.0;
29
			ensure!(old != new, Error::<T>::NoWritingSameValue);
56
			let total_percent = new.iter().fold(0, |acc, x| acc + x.percent.deconstruct());
28
			ensure!(
28
				total_percent <= 100,
8
				Error::<T>::TotalInflationDistributionPercentExceeds100,
			);
20
			<InflationDistributionInfo<T>>::put::<InflationDistributionConfig<T::AccountId>>(
20
				new.clone().into(),
20
			);
20
			Self::deposit_event(Event::InflationDistributionConfigUpdated {
20
				old: old.into(),
20
				new: new.into(),
20
			});
20
			Ok(().into())
		}
		/// Batch migrate locks to freezes for a list of accounts.
		///
		/// This function allows migrating multiple accounts from the old lock-based
		/// staking to the new freeze-based staking in a single transaction.
		///
		/// Parameters:
		/// - `accounts`: List of tuples containing (account_id, is_collator)
		///   where is_collator indicates if the account is a collator (true) or delegator (false)
		///
		/// The maximum number of accounts that can be migrated in one batch is 100.
		/// The batch cannot be empty.
		///
		/// If 50% or more of the migration attempts are successful, the entire
		/// extrinsic fee is refunded to incentivize successful batch migrations.
		/// Weight is calculated based on actual successful operations performed.
		#[pallet::call_index(33)]
		#[pallet::weight({
			T::WeightInfo::migrate_locks_to_freezes_batch_delegators(MAX_ACCOUNTS_PER_MIGRATION_BATCH).max(T::WeightInfo::migrate_locks_to_freezes_batch_candidates(MAX_ACCOUNTS_PER_MIGRATION_BATCH))
		})]
		pub fn migrate_locks_to_freezes_batch(
			origin: OriginFor<T>,
			accounts: BoundedVec<(T::AccountId, bool), ConstU32<MAX_ACCOUNTS_PER_MIGRATION_BATCH>>,
15
		) -> DispatchResultWithPostInfo {
15
			ensure_signed(origin)?;
15
			ensure!(!accounts.is_empty(), Error::<T>::EmptyMigrationBatch);
14
			let total_accounts = accounts.len() as u32;
14
			let mut successful_migrations = 0u32;
14
			let mut delegators = 0u32;
14
			let mut candidates = 0u32;
34
			for (account, is_collator) in accounts.iter() {
34
				if Self::check_and_migrate_lock(account, *is_collator) {
25
					successful_migrations = successful_migrations.saturating_add(1);
25
				}
34
				if *is_collator {
26
					candidates = candidates.saturating_add(1);
26
				} else {
8
					delegators = delegators.saturating_add(1);
8
				}
			}
			// Calculate success rate
14
			let success_rate = if total_accounts > 0 {
14
				Percent::from_rational(successful_migrations, total_accounts)
			} else {
				Percent::zero()
			};
			// Calculate actual weight consumed
14
			let delegator_weight = if delegators > 0 {
5
				T::WeightInfo::migrate_locks_to_freezes_batch_delegators(delegators)
			} else {
9
				Weight::zero()
			};
14
			let candidate_weight = if candidates > 0 {
13
				T::WeightInfo::migrate_locks_to_freezes_batch_candidates(candidates)
			} else {
1
				Weight::zero()
			};
14
			let actual_weight = delegator_weight.saturating_add(candidate_weight);
			// Apply refund if 50% or more successful using Percent comparison
14
			let pays_fee = if success_rate >= Percent::from_percent(50) {
12
				Pays::No
			} else {
2
				Pays::Yes
			};
14
			Ok(PostDispatchInfo {
14
				actual_weight: Some(actual_weight),
14
				pays_fee,
14
			})
		}
	}
	/// Represents a payout made via `pay_one_collator_reward`.
	pub(crate) enum RewardPayment {
		/// A collator was paid
		Paid,
		/// A collator was skipped for payment. This can happen if they haven't been awarded any
		/// points, that is, they did not produce any blocks.
		Skipped,
		/// All collator payments have been processed.
		Finished,
	}
	impl<T: Config> Pallet<T> {
		/// Check if an account has been migrated from lock to freeze.
		///
		/// Returns `true` if migration was performed, `false` if already migrated or is not a collator/delegator
		///
		/// `is_collator` determines whether the account is a collator or delegator
21389
		fn check_and_migrate_lock(account: &T::AccountId, is_collator: bool) -> bool {
			use frame_support::traits::{fungible::MutateFreeze, LockableCurrency};
			// Check if already migrated
21389
			if is_collator {
2010
				if <MigratedCandidates<T>>::contains_key(account) {
16
					return false;
1994
				}
			} else {
19379
				if <MigratedDelegators<T>>::contains_key(account) {
236
					return false;
19143
				}
			}
			// Not migrated yet, proceed with migration
21137
			let (lock_id, freeze_reason) = if is_collator {
1994
				(COLLATOR_LOCK_ID, FreezeReason::StakingCollator)
			} else {
19143
				(DELEGATOR_LOCK_ID, FreezeReason::StakingDelegator)
			};
			// Get the amount that should be locked/frozen from storage
21137
			let amount = if is_collator {
				// For collators, get the bond amount from storage
1994
				match <CandidateInfo<T>>::get(account) {
45
					Some(info) => info.bond,
1949
					None => return false,
				}
			} else {
				// For delegators, get the total delegated amount from storage
19143
				match <DelegatorState<T>>::get(account) {
113
					Some(state) => state.total,
19030
					None => return false,
				}
			};
158
			if amount > BalanceOf::<T>::zero() {
				// Remove any existing lock
157
				T::Currency::remove_lock(lock_id, account);
157

            
157
				// Set the freeze
157
				if T::Currency::set_freeze(&freeze_reason.into(), account, amount).is_err() {
					// set_freeze should be infallible as long as the runtime use pallet balance implementation and
					// set `MaxFreezes = VariantCountOf<RuntimeFreezeReason>`.
					log::error!(
						"Failed to set freeze for account {:?}, amount {:?}",
						account,
						amount
					);
					return false;
157
				}
1
			}
158
			if is_collator {
45
				<MigratedCandidates<T>>::insert(account, ());
113
			} else {
113
				<MigratedDelegators<T>>::insert(account, ());
113
			}
158
			true
21389
		}
		/// Set freeze with lazy migration support
		/// This will check for existing locks and migrate them before setting the freeze
		///
		/// `is_collator` determines whether the account is a collator or delegator
1307
		pub(crate) fn freeze_extended(
1307
			account: &T::AccountId,
1307
			amount: BalanceOf<T>,
1307
			is_collator: bool,
1307
		) -> DispatchResult {
			use frame_support::traits::fungible::MutateFreeze;
			// First check and migrate any existing lock
1307
			let _ = Self::check_and_migrate_lock(account, is_collator);
			// Now set the freeze
1307
			let freeze_reason = if is_collator {
664
				FreezeReason::StakingCollator
			} else {
643
				FreezeReason::StakingDelegator
			};
1307
			T::Currency::set_freeze(&freeze_reason.into(), account, amount)
1307
		}
		/// Thaw with lazy migration support
		/// This will check for existing locks and remove them before thawing
		///
		/// `is_collator` determines whether the account is a collator or delegator
54
		pub(crate) fn thaw_extended(account: &T::AccountId, is_collator: bool) -> DispatchResult {
			use frame_support::traits::{fungible::MutateFreeze, LockableCurrency};
			// First check and remove any existing lock
54
			let lock_id = if is_collator {
22
				COLLATOR_LOCK_ID
			} else {
32
				DELEGATOR_LOCK_ID
			};
			// Remove the lock if it exists
54
			T::Currency::remove_lock(lock_id, account);
			// Now thaw the freeze
54
			let freeze_reason = if is_collator {
22
				FreezeReason::StakingCollator
			} else {
32
				FreezeReason::StakingDelegator
			};
54
			let _ = T::Currency::thaw(&freeze_reason.into(), account);
54
			Ok(())
54
		}
		/// Get frozen balance with lazy migration support
		/// This will check for existing locks and migrate them before returning the frozen balance
		///
		/// `is_collator` determines whether the account is a collator or delegator
20048
		pub(crate) fn balance_frozen_extended(
20048
			account: &T::AccountId,
20048
			is_collator: bool,
20048
		) -> Option<BalanceOf<T>> {
20048
			// First check and migrate any existing lock
20048
			// We ignore the result as we want to return the frozen balance regardless
20048
			let _ = Self::check_and_migrate_lock(account, is_collator);
20048

            
20048
			// Now return the frozen balance
20048
			if is_collator {
1320
				<CandidateInfo<T>>::get(account).map(|info| info.bond)
			} else {
18728
				<DelegatorState<T>>::get(account).map(|state| state.total)
			}
20048
		}
		pub fn set_candidate_bond_to_zero(acc: &T::AccountId) -> Weight {
			let actual_weight =
				<T as Config>::WeightInfo::set_candidate_bond_to_zero(T::MaxCandidates::get());
			if let Some(mut state) = <CandidateInfo<T>>::get(&acc) {
				state.bond_less::<T>(acc.clone(), state.bond);
				<CandidateInfo<T>>::insert(&acc, state);
			}
			actual_weight
		}
692
		pub fn is_delegator(acc: &T::AccountId) -> bool {
692
			<DelegatorState<T>>::get(acc).is_some()
692
		}
9684
		pub fn is_candidate(acc: &T::AccountId) -> bool {
9684
			<CandidateInfo<T>>::get(acc).is_some()
9684
		}
2
		pub fn is_selected_candidate(acc: &T::AccountId) -> bool {
2
			<SelectedCandidates<T>>::get().binary_search(acc).is_ok()
2
		}
667
		pub fn join_candidates_inner(
667
			acc: T::AccountId,
667
			bond: BalanceOf<T>,
667
			candidate_count: u32,
667
		) -> DispatchResultWithPostInfo {
667
			ensure!(!Self::is_candidate(&acc), Error::<T>::CandidateExists);
663
			ensure!(!Self::is_delegator(&acc), Error::<T>::DelegatorExists);
659
			let mut candidates = <CandidatePool<T>>::get();
659
			let old_count = candidates.0.len() as u32;
659
			ensure!(
659
				candidate_count >= old_count,
5
				Error::<T>::TooLowCandidateCountWeightHintJoinCandidates
			);
654
			let maybe_inserted_candidate = candidates
654
				.try_insert(Bond {
654
					owner: acc.clone(),
654
					amount: bond,
654
				})
654
				.map_err(|_| Error::<T>::CandidateLimitReached)?;
653
			ensure!(maybe_inserted_candidate, Error::<T>::CandidateExists);
653
			ensure!(
653
				Self::get_collator_stakable_free_balance(&acc) >= bond,
4
				Error::<T>::InsufficientBalance,
			);
649
			Self::freeze_extended(&acc, bond, true).map_err(|_| Error::<T>::InsufficientBalance)?;
649
			let candidate = CandidateMetadata::new(bond);
649
			<CandidateInfo<T>>::insert(&acc, candidate);
649
			let empty_delegations: Delegations<T::AccountId, BalanceOf<T>> = Default::default();
649
			// insert empty top delegations
649
			<TopDelegations<T>>::insert(&acc, empty_delegations.clone());
649
			// insert empty bottom delegations
649
			<BottomDelegations<T>>::insert(&acc, empty_delegations);
649
			<CandidatePool<T>>::put(candidates);
649
			let new_total = <Total<T>>::get().saturating_add(bond);
649
			<Total<T>>::put(new_total);
649
			Self::deposit_event(Event::JoinedCollatorCandidates {
649
				account: acc,
649
				amount_locked: bond,
649
				new_total_amt_locked: new_total,
649
			});
649
			Ok(().into())
667
		}
16
		pub fn go_offline_inner(collator: T::AccountId) -> DispatchResultWithPostInfo {
16
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
15
			let mut candidates = <CandidatePool<T>>::get();
15
			let actual_weight = <T as Config>::WeightInfo::go_offline(candidates.0.len() as u32);
15

            
15
			ensure!(
15
				state.is_active(),
1
				DispatchErrorWithPostInfo {
1
					post_info: Some(actual_weight).into(),
1
					error: <Error<T>>::AlreadyOffline.into(),
1
				}
			);
14
			state.go_offline();
14

            
14
			if candidates.remove(&Bond::from_owner(collator.clone())) {
14
				<CandidatePool<T>>::put(candidates);
14
			}
14
			<CandidateInfo<T>>::insert(&collator, state);
14
			Self::deposit_event(Event::CandidateWentOffline {
14
				candidate: collator,
14
			});
14
			Ok(Some(actual_weight).into())
16
		}
7
		pub fn go_online_inner(collator: T::AccountId) -> DispatchResultWithPostInfo {
7
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
6
			let mut candidates = <CandidatePool<T>>::get();
6
			let actual_weight = <T as Config>::WeightInfo::go_online(candidates.0.len() as u32);
6

            
6
			ensure!(
6
				!state.is_active(),
1
				DispatchErrorWithPostInfo {
1
					post_info: Some(actual_weight).into(),
1
					error: <Error<T>>::AlreadyActive.into(),
1
				}
			);
5
			ensure!(
5
				!state.is_leaving(),
1
				DispatchErrorWithPostInfo {
1
					post_info: Some(actual_weight).into(),
1
					error: <Error<T>>::CannotGoOnlineIfLeaving.into(),
1
				}
			);
4
			state.go_online();
4
			let maybe_inserted_candidate = candidates
4
				.try_insert(Bond {
4
					owner: collator.clone(),
4
					amount: state.total_counted,
4
				})
4
				.map_err(|_| Error::<T>::CandidateLimitReached)?;
4
			ensure!(
4
				maybe_inserted_candidate,
				DispatchErrorWithPostInfo {
					post_info: Some(actual_weight).into(),
					error: <Error<T>>::AlreadyActive.into(),
				},
			);
4
			<CandidatePool<T>>::put(candidates);
4
			<CandidateInfo<T>>::insert(&collator, state);
4
			Self::deposit_event(Event::CandidateBackOnline {
4
				candidate: collator,
4
			});
4
			Ok(Some(actual_weight).into())
7
		}
8
		pub fn candidate_bond_more_inner(
8
			collator: T::AccountId,
8
			more: BalanceOf<T>,
8
		) -> DispatchResultWithPostInfo {
8
			let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
8
			let actual_weight =
8
				<T as Config>::WeightInfo::candidate_bond_more(T::MaxCandidates::get());
8

            
8
			state
8
				.bond_more::<T>(collator.clone(), more)
8
				.map_err(|err| DispatchErrorWithPostInfo {
					post_info: Some(actual_weight).into(),
					error: err,
8
				})?;
8
			let (is_active, total_counted) = (state.is_active(), state.total_counted);
8
			<CandidateInfo<T>>::insert(&collator, state);
8
			if is_active {
8
				Self::update_active(collator, total_counted);
8
			}
8
			Ok(Some(actual_weight).into())
8
		}
7
		pub fn execute_candidate_bond_less_inner(
7
			candidate: T::AccountId,
7
		) -> DispatchResultWithPostInfo {
7
			let mut state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
7
			let actual_weight =
7
				<T as Config>::WeightInfo::execute_candidate_bond_less(T::MaxCandidates::get());
7

            
7
			state
7
				.execute_bond_less::<T>(candidate.clone())
7
				.map_err(|err| DispatchErrorWithPostInfo {
					post_info: Some(actual_weight).into(),
					error: err,
7
				})?;
7
			<CandidateInfo<T>>::insert(&candidate, state);
7
			Ok(Some(actual_weight).into())
7
		}
24
		pub fn execute_leave_candidates_inner(
24
			candidate: T::AccountId,
24
		) -> DispatchResultWithPostInfo {
24
			let state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
24
			let actual_auto_compound_delegation_count =
24
				<AutoCompoundingDelegations<T>>::decode_len(&candidate).unwrap_or_default() as u32;
24

            
24
			// TODO use these to return actual weight used via `execute_leave_candidates_ideal`
24
			let actual_delegation_count = state.delegation_count;
24
			let actual_weight = <T as Config>::WeightInfo::execute_leave_candidates_ideal(
24
				actual_delegation_count,
24
				actual_auto_compound_delegation_count,
24
			);
24

            
24
			state
24
				.can_leave::<T>()
24
				.map_err(|err| DispatchErrorWithPostInfo {
2
					post_info: Some(actual_weight).into(),
2
					error: err,
24
				})?;
22
			let return_stake = |bond: Bond<T::AccountId, BalanceOf<T>>| {
15
				// remove delegation from delegator state
15
				let mut delegator = DelegatorState::<T>::get(&bond.owner).expect(
15
					"Collator state and delegator state are consistent.
15
						Collator state has a record of this delegation. Therefore,
15
						Delegator state also has a record. qed.",
15
				);
15
				if let Some(remaining) = delegator.rm_delegation::<T>(&candidate) {
15
					Self::delegation_remove_request_with_state(
15
						&candidate,
15
						&bond.owner,
15
						&mut delegator,
15
					);
15
					<AutoCompoundDelegations<T>>::remove_auto_compound(&candidate, &bond.owner);
15

            
15
					if remaining.is_zero() {
9
						// we do not remove the scheduled delegation requests from other collators
9
						// since it is assumed that they were removed incrementally before only the
9
						// last delegation was left.
9
						<DelegatorState<T>>::remove(&bond.owner);
9
						// Thaw all frozen funds for delegator
9
						let _ = Self::thaw_extended(&bond.owner, false);
9
					} else {
6
						<DelegatorState<T>>::insert(&bond.owner, delegator);
6
					}
				} else {
					// TODO: review. we assume here that this delegator has no remaining staked
					// balance, so we ensure the funds are freed
					let _ = Self::thaw_extended(&bond.owner, false);
				}
15
			};
			// total backing stake is at least the candidate self bond
22
			let mut total_backing = state.bond;
22
			// return all top delegations
22
			let top_delegations =
22
				<TopDelegations<T>>::take(&candidate).expect("CandidateInfo existence checked");
36
			for bond in top_delegations.delegations {
14
				return_stake(bond);
14
			}
22
			total_backing = total_backing.saturating_add(top_delegations.total);
22
			// return all bottom delegations
22
			let bottom_delegations =
22
				<BottomDelegations<T>>::take(&candidate).expect("CandidateInfo existence checked");
23
			for bond in bottom_delegations.delegations {
1
				return_stake(bond);
1
			}
22
			total_backing = total_backing.saturating_add(bottom_delegations.total);
22
			// Thaw all frozen funds for collator
22
			let _ = Self::thaw_extended(&candidate, true);
22
			<CandidateInfo<T>>::remove(&candidate);
22
			<DelegationScheduledRequests<T>>::remove(&candidate);
22
			<AutoCompoundingDelegations<T>>::remove(&candidate);
22
			<TopDelegations<T>>::remove(&candidate);
22
			<BottomDelegations<T>>::remove(&candidate);
22
			let new_total_staked = <Total<T>>::get().saturating_sub(total_backing);
22
			<Total<T>>::put(new_total_staked);
22
			Self::deposit_event(Event::CandidateLeft {
22
				ex_candidate: candidate,
22
				unlocked_amount: total_backing,
22
				new_total_amt_locked: new_total_staked,
22
			});
22
			Ok(Some(actual_weight).into())
24
		}
		/// Returns an account's stakable balance (including the reserved) which is not frozen in delegation staking
18728
		pub fn get_delegator_stakable_balance(acc: &T::AccountId) -> BalanceOf<T> {
18728
			let total_balance =
18728
				T::Currency::balance(acc).saturating_add(T::Currency::reserved_balance(acc));
18728
			if let Some(frozen_balance) = Self::balance_frozen_extended(acc, false) {
226
				return total_balance.saturating_sub(frozen_balance);
18502
			}
18502
			total_balance
18728
		}
		/// Returns an account's free balance which is not frozen in collator staking
1320
		pub fn get_collator_stakable_free_balance(acc: &T::AccountId) -> BalanceOf<T> {
1320
			let total_balance = T::Currency::balance(acc);
1320
			if let Some(frozen_balance) = Self::balance_frozen_extended(acc, true) {
22
				return total_balance.saturating_sub(frozen_balance);
1298
			}
1298
			total_balance
1320
		}
		/// Returns a delegations auto-compound value.
5
		pub fn delegation_auto_compound(
5
			candidate: &T::AccountId,
5
			delegator: &T::AccountId,
5
		) -> Percent {
5
			<AutoCompoundDelegations<T>>::auto_compound(candidate, delegator)
5
		}
		/// Caller must ensure candidate is active before calling
517
		pub(crate) fn update_active(candidate: T::AccountId, total: BalanceOf<T>) {
517
			let mut candidates = <CandidatePool<T>>::get();
517
			candidates.remove(&Bond::from_owner(candidate.clone()));
517
			candidates
517
				.try_insert(Bond {
517
					owner: candidate,
517
					amount: total,
517
				})
517
				.expect(
517
					"the candidate is removed in previous step so the length cannot increase; qed",
517
				);
517
			<CandidatePool<T>>::put(candidates);
517
		}
		/// Compute round issuance based on duration of the given round
265
		fn compute_issuance(round_duration: u64, round_length: u32) -> BalanceOf<T> {
265
			let ideal_duration: BalanceOf<T> = round_length
265
				.saturating_mul(T::BlockTime::get() as u32)
265
				.into();
265
			let config = <InflationConfig<T>>::get();
265
			let round_issuance = crate::inflation::round_issuance_range::<T>(config.round);
265

            
265
			// Initial formula: (round_duration / ideal_duration) * ideal_issuance
265
			// We multiply before the division to reduce rounding effects
265
			BalanceOf::<T>::from(round_duration as u32).saturating_mul(round_issuance.ideal)
265
				/ (ideal_duration)
265
		}
		/// Remove delegation from candidate state
		/// Amount input should be retrieved from delegator and it informs the storage lookups
23
		pub(crate) fn delegator_leaves_candidate(
23
			candidate: T::AccountId,
23
			delegator: T::AccountId,
23
			amount: BalanceOf<T>,
23
		) -> DispatchResult {
23
			let mut state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
23
			state.rm_delegation_if_exists::<T>(&candidate, delegator.clone(), amount)?;
23
			let new_total_locked = <Total<T>>::get().saturating_sub(amount);
23
			<Total<T>>::put(new_total_locked);
23
			let new_total = state.total_counted;
23
			<CandidateInfo<T>>::insert(&candidate, state);
23
			Self::deposit_event(Event::DelegatorLeftCandidate {
23
				delegator: delegator,
23
				candidate: candidate,
23
				unstaked_amount: amount,
23
				total_candidate_staked: new_total,
23
			});
23
			Ok(())
23
		}
265
		pub(crate) fn prepare_staking_payouts(
265
			round_info: RoundInfo<BlockNumberFor<T>>,
265
			round_duration: u64,
265
		) -> Weight {
265
			let RoundInfo {
265
				current: now,
265
				length: round_length,
265
				..
265
			} = round_info;
265

            
265
			// This function is called right after the round index increment,
265
			// and the goal is to compute the payout informations for the round that just ended.
265
			// We don't need to saturate here because the genesis round is 1.
265
			let prepare_payout_for_round = now - 1;
265

            
265
			// Return early if there is no blocks for this round
265
			if <Points<T>>::get(prepare_payout_for_round).is_zero() {
				return Weight::zero();
265
			}
265

            
265
			// Compute total issuance based on round duration
265
			let total_issuance = Self::compute_issuance(round_duration, round_length);
265
			// reserve portion of issuance for parachain bond account
265
			let mut left_issuance = total_issuance;
265

            
265
			let configs = <InflationDistributionInfo<T>>::get().0;
530
			for (index, config) in configs.iter().enumerate() {
530
				if config.percent.is_zero() {
265
					continue;
265
				}
265
				let reserve = config.percent * total_issuance;
265
				if frame_system::Pallet::<T>::account_exists(&config.account) {
6
					if let Ok(minted) = T::Currency::mint_into(&config.account, reserve) {
6
						// update round issuance if minting succeeds
6
						left_issuance = left_issuance.saturating_sub(minted);
6
						Self::deposit_event(Event::InflationDistributed {
6
							index: index as u32,
6
							account: config.account.clone(),
6
							value: minted,
6
						});
6
					}
259
				}
			}
265
			let payout = DelayedPayout {
265
				round_issuance: total_issuance,
265
				total_staking_reward: left_issuance,
265
				collator_commission: <CollatorCommission<T>>::get(),
265
			};
265

            
265
			<DelayedPayouts<T>>::insert(prepare_payout_for_round, payout);
265

            
265
			<T as Config>::WeightInfo::prepare_staking_payouts()
265
		}
		/// Wrapper around pay_one_collator_reward which handles the following logic:
		/// * whether or not a payout needs to be made
		/// * cleaning up when payouts are done
		/// * returns the weight consumed by pay_one_collator_reward if applicable
30444
		fn handle_delayed_payouts(now: RoundIndex) -> Weight {
30444
			let delay = T::RewardPaymentDelay::get();
30444

            
30444
			// don't underflow uint
30444
			if now < delay {
16499
				return Weight::from_parts(0u64, 0);
13945
			}
13945

            
13945
			let paid_for_round = now.saturating_sub(delay);
13945
			if let Some(payout_info) = <DelayedPayouts<T>>::get(paid_for_round) {
352
				let result = Self::pay_one_collator_reward(paid_for_round, payout_info);
				// clean up storage items that we no longer need
352
				if matches!(result.0, RewardPayment::Finished) {
63
					<DelayedPayouts<T>>::remove(paid_for_round);
63
					<Points<T>>::remove(paid_for_round);
296
				}
352
				result.1 // weight consumed by pay_one_collator_reward
			} else {
13593
				Weight::from_parts(0u64, 0)
			}
30444
		}
		/// Payout a single collator from the given round.
		///
		/// Returns an optional tuple of (Collator's AccountId, total paid)
		/// or None if there were no more payouts to be made for the round.
352
		pub(crate) fn pay_one_collator_reward(
352
			paid_for_round: RoundIndex,
352
			payout_info: DelayedPayout<BalanceOf<T>>,
352
		) -> (RewardPayment, Weight) {
352
			// 'early_weight' tracks weight used for reads/writes done early in this fn before its
352
			// early-exit codepaths.
352
			let mut early_weight = Weight::zero();
352

            
352
			// TODO: it would probably be optimal to roll Points into the DelayedPayouts storage
352
			// item so that we do fewer reads each block
352
			let total_points = <Points<T>>::get(paid_for_round);
352
			early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 0));
352

            
352
			if total_points.is_zero() {
				// TODO: this case is obnoxious... it's a value query, so it could mean one of two
				// different logic errors:
				// 1. we removed it before we should have
				// 2. we called pay_one_collator_reward when we were actually done with deferred
				//    payouts
				log::warn!("pay_one_collator_reward called with no <Points<T>> for the round!");
				return (RewardPayment::Finished, early_weight);
352
			}
352

            
352
			let collator_fee = payout_info.collator_commission;
352
			let collator_issuance = collator_fee * payout_info.round_issuance;
289
			if let Some((collator, state)) =
352
				<AtStake<T>>::iter_prefix(paid_for_round).drain().next()
			{
				// read and kill AtStake
289
				early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
289

            
289
				// Take the awarded points for the collator
289
				let pts = <AwardedPts<T>>::take(paid_for_round, &collator);
289
				// read and kill AwardedPts
289
				early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
289
				if pts == 0 {
233
					return (RewardPayment::Skipped, early_weight);
56
				}
56

            
56
				// 'extra_weight' tracks weight returned from fns that we delegate to which can't be
56
				// known ahead of time.
56
				let mut extra_weight = Weight::zero();
56
				let pct_due = Perbill::from_rational(pts, total_points);
56
				let total_paid = pct_due * payout_info.total_staking_reward;
56
				let mut amt_due = total_paid;
56

            
56
				let num_delegators = state.delegations.len();
56
				let mut num_paid_delegations = 0u32;
56
				let mut num_auto_compounding = 0u32;
56
				let num_scheduled_requests =
56
					<DelegationScheduledRequests<T>>::decode_len(&collator).unwrap_or_default();
56
				if state.delegations.is_empty() {
21
					// solo collator with no delegators
21
					extra_weight = extra_weight
21
						.saturating_add(T::PayoutCollatorReward::payout_collator_reward(
21
							paid_for_round,
21
							collator.clone(),
21
							amt_due,
21
						))
21
						.saturating_add(T::OnCollatorPayout::on_collator_payout(
21
							paid_for_round,
21
							collator.clone(),
21
							amt_due,
21
						));
21
				} else {
					// pay collator first; commission + due_portion
35
					let collator_pct = Perbill::from_rational(state.bond, state.total);
35
					let commission = pct_due * collator_issuance;
35
					amt_due = amt_due.saturating_sub(commission);
35
					let collator_reward = (collator_pct * amt_due).saturating_add(commission);
35
					extra_weight = extra_weight
35
						.saturating_add(T::PayoutCollatorReward::payout_collator_reward(
35
							paid_for_round,
35
							collator.clone(),
35
							collator_reward,
35
						))
35
						.saturating_add(T::OnCollatorPayout::on_collator_payout(
35
							paid_for_round,
35
							collator.clone(),
35
							collator_reward,
35
						));
					// pay delegators due portion
					for BondWithAutoCompound {
61
						owner,
61
						amount,
61
						auto_compound,
96
					} in state.delegations
					{
61
						let percent = Perbill::from_rational(amount, state.total);
61
						let due = percent * amt_due;
61
						if !due.is_zero() {
55
							num_auto_compounding += if auto_compound.is_zero() { 0 } else { 1 };
55
							num_paid_delegations += 1u32;
55
							Self::mint_and_compound(
55
								due,
55
								auto_compound.clone(),
55
								collator.clone(),
55
								owner.clone(),
55
							);
6
						}
					}
				}
56
				extra_weight = extra_weight.saturating_add(
56
					<T as Config>::WeightInfo::pay_one_collator_reward_best(
56
						num_paid_delegations,
56
						num_auto_compounding,
56
						num_scheduled_requests as u32,
56
					),
56
				);
56

            
56
				(
56
					RewardPayment::Paid,
56
					<T as Config>::WeightInfo::pay_one_collator_reward(num_delegators as u32)
56
						.saturating_add(extra_weight),
56
				)
			} else {
				// Note that we don't clean up storage here; it is cleaned up in
				// handle_delayed_payouts()
63
				(RewardPayment::Finished, Weight::from_parts(0u64, 0))
			}
352
		}
		/// Compute the top `TotalSelected` candidates in the CandidatePool and return
		/// a vec of their AccountIds (sorted by AccountId).
		///
		/// If the returned vec is empty, the previous candidates should be used.
797
		pub fn compute_top_candidates() -> Vec<T::AccountId> {
797
			let top_n = <TotalSelected<T>>::get() as usize;
797
			if top_n == 0 {
				return vec![];
797
			}
797

            
797
			let candidates = <CandidatePool<T>>::get().0;
797

            
797
			// If the number of candidates is greater than top_n, select the candidates with higher
797
			// amount. Otherwise, return all the candidates.
797
			if candidates.len() > top_n {
				// Partially sort candidates such that element at index `top_n - 1` is sorted, and
				// all the elements in the range 0..top_n are the top n elements.
15
				let sorted_candidates = candidates
15
					.try_mutate(|inner| {
1083
						inner.select_nth_unstable_by(top_n - 1, |a, b| {
1083
							// Order by amount, then owner. The owner is needed to ensure a stable order
1083
							// when two accounts have the same amount.
1083
							a.amount
1083
								.cmp(&b.amount)
1083
								.then_with(|| a.owner.cmp(&b.owner))
1083
								.reverse()
1083
						});
15
					})
15
					.expect("sort cannot increase item count; qed");
15

            
15
				let mut collators = sorted_candidates
15
					.into_iter()
15
					.take(top_n)
75
					.map(|x| x.owner)
15
					.collect::<Vec<_>>();
15

            
15
				// Sort collators by AccountId
15
				collators.sort();
15

            
15
				collators
			} else {
				// Return all candidates
				// The candidates are already sorted by AccountId, so no need to sort again
1040
				candidates.into_iter().map(|x| x.owner).collect::<Vec<_>>()
			}
797
		}
		/// Best as in most cumulatively supported in terms of stake
		/// Returns [collator_count, delegation_count, total staked]
796
		pub(crate) fn select_top_candidates(now: RoundIndex) -> (Weight, u32, u32, BalanceOf<T>) {
796
			let (mut collator_count, mut delegation_count, mut total) =
796
				(0u32, 0u32, BalanceOf::<T>::zero());
796
			// choose the top TotalSelected qualified candidates, ordered by stake
796
			let collators = Self::compute_top_candidates();
796
			if collators.is_empty() {
				// SELECTION FAILED TO SELECT >=1 COLLATOR => select collators from previous round
315
				let last_round = now.saturating_sub(1u32);
315
				let mut total_per_candidate: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
				// set this round AtStake to last round AtStake
315
				for (account, snapshot) in <AtStake<T>>::iter_prefix(last_round) {
43
					collator_count = collator_count.saturating_add(1u32);
43
					delegation_count =
43
						delegation_count.saturating_add(snapshot.delegations.len() as u32);
43
					total = total.saturating_add(snapshot.total);
43
					total_per_candidate.insert(account.clone(), snapshot.total);
43
					<AtStake<T>>::insert(now, account, snapshot);
43
				}
				// `SelectedCandidates` remains unchanged from last round
				// emit CollatorChosen event for tools that use this event
358
				for candidate in <SelectedCandidates<T>>::get() {
43
					let snapshot_total = total_per_candidate
43
						.get(&candidate)
43
						.expect("all selected candidates have snapshots");
43
					Self::deposit_event(Event::CollatorChosen {
43
						round: now,
43
						collator_account: candidate,
43
						total_exposed_amount: *snapshot_total,
43
					})
				}
315
				let weight = <T as Config>::WeightInfo::select_top_candidates(0, 0);
315
				return (weight, collator_count, delegation_count, total);
481
			}
			// snapshot exposure for round for weighting reward distribution
925
			for account in collators.iter() {
925
				let state = <CandidateInfo<T>>::get(account)
925
					.expect("all members of CandidateQ must be candidates");
925

            
925
				collator_count = collator_count.saturating_add(1u32);
925
				delegation_count = delegation_count.saturating_add(state.delegation_count);
925
				total = total.saturating_add(state.total_counted);
925
				let CountedDelegations {
925
					uncounted_stake,
925
					rewardable_delegations,
925
				} = Self::get_rewardable_delegators(&account);
925
				let total_counted = state.total_counted.saturating_sub(uncounted_stake);
925

            
925
				let auto_compounding_delegations = <AutoCompoundingDelegations<T>>::get(&account)
925
					.into_iter()
925
					.map(|x| (x.delegator, x.value))
925
					.collect::<BTreeMap<_, _>>();
925
				let rewardable_delegations = rewardable_delegations
925
					.into_iter()
925
					.map(|d| BondWithAutoCompound {
763
						owner: d.owner.clone(),
763
						amount: d.amount,
763
						auto_compound: auto_compounding_delegations
763
							.get(&d.owner)
763
							.cloned()
763
							.unwrap_or_else(|| Percent::zero()),
925
					})
925
					.collect();
925

            
925
				let snapshot = CollatorSnapshot {
925
					bond: state.bond,
925
					delegations: rewardable_delegations,
925
					total: total_counted,
925
				};
925
				<AtStake<T>>::insert(now, account, snapshot);
925
				Self::deposit_event(Event::CollatorChosen {
925
					round: now,
925
					collator_account: account.clone(),
925
					total_exposed_amount: state.total_counted,
925
				});
925
			}
			// insert canonical collator set
481
			<SelectedCandidates<T>>::put(
481
				BoundedVec::try_from(collators)
481
					.expect("subset of collators is always less than or equal to max candidates"),
481
			);
481

            
481
			let avg_delegator_count = delegation_count.checked_div(collator_count).unwrap_or(0);
481
			let weight = <T as Config>::WeightInfo::select_top_candidates(
481
				collator_count,
481
				avg_delegator_count,
481
			);
481
			(weight, collator_count, delegation_count, total)
796
		}
		/// Apply the delegator intent for revoke and decrease in order to build the
		/// effective list of delegators with their intended bond amount.
		///
		/// This will:
		/// - if [DelegationChange::Revoke] is outstanding, set the bond amount to 0.
		/// - if [DelegationChange::Decrease] is outstanding, subtract the bond by specified amount.
		/// - else, do nothing
		///
		/// The intended bond amounts will be used while calculating rewards.
925
		pub(crate) fn get_rewardable_delegators(collator: &T::AccountId) -> CountedDelegations<T> {
925
			let requests = <DelegationScheduledRequests<T>>::get(collator)
925
				.into_iter()
925
				.map(|x| (x.delegator, x.action))
925
				.collect::<BTreeMap<_, _>>();
925
			let mut uncounted_stake = BalanceOf::<T>::zero();
925
			let rewardable_delegations = <TopDelegations<T>>::get(collator)
925
				.expect("all members of CandidateQ must be candidates")
925
				.delegations
925
				.into_iter()
925
				.map(|mut bond| {
763
					bond.amount = match requests.get(&bond.owner) {
652
						None => bond.amount,
						Some(DelegationAction::Revoke(_)) => {
75
							uncounted_stake = uncounted_stake.saturating_add(bond.amount);
75
							BalanceOf::<T>::zero()
						}
36
						Some(DelegationAction::Decrease(amount)) => {
36
							uncounted_stake = uncounted_stake.saturating_add(*amount);
36
							bond.amount.saturating_sub(*amount)
						}
					};
763
					bond
925
				})
925
				.collect();
925
			CountedDelegations {
925
				uncounted_stake,
925
				rewardable_delegations,
925
			}
925
		}
		/// This function exists as a helper to delegator_bond_more & auto_compound functionality.
		/// Any changes to this function must align with both user-initiated bond increases and
		/// auto-compounding bond increases.
		/// Any feature-specific preconditions should be validated before this function is invoked.
		/// Any feature-specific events must be emitted after this function is invoked.
23
		pub fn delegation_bond_more_without_event(
23
			delegator: T::AccountId,
23
			candidate: T::AccountId,
23
			more: BalanceOf<T>,
23
		) -> Result<
23
			(bool, Weight),
23
			DispatchErrorWithPostInfo<frame_support::dispatch::PostDispatchInfo>,
23
		> {
23
			let mut state = <DelegatorState<T>>::get(&delegator).ok_or(Error::<T>::DelegatorDNE)?;
23
			ensure!(
23
				!Self::delegation_request_revoke_exists(&candidate, &delegator),
2
				Error::<T>::PendingDelegationRevoke
			);
21
			let actual_weight = <T as Config>::WeightInfo::delegator_bond_more(
21
				<DelegationScheduledRequests<T>>::decode_len(&candidate).unwrap_or_default() as u32,
21
			);
21
			let in_top = state
21
				.increase_delegation::<T>(candidate.clone(), more)
21
				.map_err(|err| DispatchErrorWithPostInfo {
					post_info: Some(actual_weight).into(),
					error: err,
21
				})?;
21
			Ok((in_top, actual_weight))
23
		}
		/// Mint a specified reward amount to the beneficiary account. Emits the [Rewarded] event.
145
		pub fn mint(amt: BalanceOf<T>, to: &T::AccountId) -> Result<BalanceOf<T>, DispatchError> {
			// Mint rewards to the account
145
			let minted = T::Currency::mint_into(&to, amt)?;
145
			Self::deposit_event(Event::Rewarded {
145
				account: to.clone(),
145
				rewards: minted,
145
			});
145
			Ok(minted)
145
		}
		/// Mint a specified reward amount to the collator's account. Emits the [Rewarded] event.
90
		pub fn mint_collator_reward(
90
			_paid_for_round: RoundIndex,
90
			collator_id: T::AccountId,
90
			amt: BalanceOf<T>,
90
		) -> Weight {
			// Mint rewards to the collator
90
			if let Err(e) = Self::mint(amt, &collator_id) {
				log::warn!(
					"Failed to mint collator reward for {:?}: {:?}",
					collator_id,
					e
				);
90
			}
90
			<T as Config>::WeightInfo::mint_collator_reward()
90
		}
		/// Mint and compound delegation rewards. The function mints the amount towards the
		/// delegator and tries to compound a specified percent of it back towards the delegation.
		/// If a scheduled delegation revoke exists, then the amount is only minted, and nothing is
		/// compounded. Emits the [Compounded] event.
55
		pub fn mint_and_compound(
55
			amt: BalanceOf<T>,
55
			compound_percent: Percent,
55
			candidate: T::AccountId,
55
			delegator: T::AccountId,
55
		) {
55
			// Mint rewards to the delegator
55
			if frame_system::Pallet::<T>::account_exists(&delegator) {
55
				if let Ok(minted) = Self::mint(amt.clone(), &delegator) {
55
					let compound_amount = compound_percent.mul_ceil(minted);
55
					if compound_amount.is_zero() {
51
						return;
4
					}
4
					if let Err(err) = Self::delegation_bond_more_without_event(
4
						delegator.clone(),
4
						candidate.clone(),
4
						compound_amount.clone(),
4
					) {
1
						log::debug!(
							"skipped compounding staking reward towards candidate '{:?}' for delegator '{:?}': {:?}",
							candidate,
							delegator,
							err
						);
1
						return;
3
					};
3

            
3
					Pallet::<T>::deposit_event(Event::Compounded {
3
						delegator,
3
						candidate,
3
						amount: compound_amount.clone(),
3
					});
				};
			}
55
		}
		/// Add reward points to block authors:
		/// * 20 points to the block producer for producing a block in the chain
30647
		fn award_points_to_block_author() {
30647
			let author = T::BlockAuthor::get();
30647
			let now = <Round<T>>::get().current;
30647
			let score_plus_20 = <AwardedPts<T>>::get(now, &author).saturating_add(20);
30647
			<AwardedPts<T>>::insert(now, author, score_plus_20);
30647
			<Points<T>>::mutate(now, |x| *x = x.saturating_add(20));
30647
		}
		/// Marks collators as inactive for the previous round if they received zero awarded points.
265
		pub fn mark_collators_as_inactive(cur: RoundIndex) -> Weight {
265
			// This function is called after round index increment,
265
			// We don't need to saturate here because the genesis round is 1.
265
			let prev = cur - 1;
265

            
265
			let mut collators_at_stake_count = 0u32;
573
			for (account, _) in <AtStake<T>>::iter_prefix(prev) {
564
				collators_at_stake_count = collators_at_stake_count.saturating_add(1u32);
564
				if <AwardedPts<T>>::get(prev, &account).is_zero() {
468
					<WasInactive<T>>::insert(prev, account, ());
490
				}
			}
265
			<T as Config>::WeightInfo::mark_collators_as_inactive(collators_at_stake_count)
265
		}
		/// Cleans up historical staking information that is older than MaxOfflineRounds
		/// by removing entries from the WasIactive storage map.
30647
		fn cleanup_inactive_collator_info() {
30647
			let now = <Round<T>>::get().current;
30647
			let minimum_rounds_required = T::MaxOfflineRounds::get() + 1;
30647

            
30647
			if now < minimum_rounds_required {
22922
				return;
7725
			}
7725

            
7725
			let _ = <WasInactive<T>>::iter_prefix(now - minimum_rounds_required)
7725
				.drain()
7725
				.next();
30647
		}
	}
	impl<T: Config> nimbus_primitives::CanAuthor<T::AccountId> for Pallet<T> {
		fn can_author(account: &T::AccountId, _slot: &u32) -> bool {
			Self::is_selected_candidate(account)
		}
	}
	impl<T: Config> Get<Vec<T::AccountId>> for Pallet<T> {
60
		fn get() -> Vec<T::AccountId> {
60
			Self::selected_candidates().into_inner()
60
		}
	}
}