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
4053
#[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};
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
48
	#[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 = 300;
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
		/// Maximum number of scheduled delegation requests per (collator, delegator).
176
		#[pallet::constant]
177
		type MaxScheduledRequestsPerDelegator: Get<u32>;
178
		/// Minimum stake required for any account to be a collator candidate
179
		#[pallet::constant]
180
		type MinCandidateStk: Get<BalanceOf<Self>>;
181
		/// Minimum stake for any registered on-chain account to delegate
182
		#[pallet::constant]
183
		type MinDelegation: Get<BalanceOf<Self>>;
184
		/// Get the current block author
185
		type BlockAuthor: Get<Self::AccountId>;
186
		/// Handler to notify the runtime when a collator is paid.
187
		/// If you don't need it, you can specify the type `()`.
188
		type OnCollatorPayout: OnCollatorPayout<Self::AccountId, BalanceOf<Self>>;
189
		/// Handler to distribute a collator's reward.
190
		/// To use the default implementation of minting rewards, specify the type `()`.
191
		type PayoutCollatorReward: PayoutCollatorReward<Self>;
192
		/// Handler to notify the runtime when a collator is inactive.
193
		/// The default behavior is to mark the collator as offline.
194
		/// If you need to use the default implementation, specify the type `()`.
195
		type OnInactiveCollator: OnInactiveCollator<Self>;
196
		/// Handler to notify the runtime when a new round begin.
197
		/// If you don't need it, you can specify the type `()`.
198
		type OnNewRound: OnNewRound;
199
		/// Get the current slot number
200
		type SlotProvider: Get<Slot>;
201
		/// Get the slot duration in milliseconds
202
		#[pallet::constant]
203
		type SlotDuration: Get<u64>;
204
		/// Get the average time between 2 blocks in milliseconds
205
		#[pallet::constant]
206
		type BlockTime: Get<u64>;
207
		/// Maximum candidates
208
		#[pallet::constant]
209
		type MaxCandidates: Get<u32>;
210
		/// Threshold after which inflation become linear
211
		/// If you don't want to use it, set it to `()`
212
		#[pallet::constant]
213
		type LinearInflationThreshold: Get<Option<BalanceOf<Self>>>;
214
		/// Weight information for extrinsics in this pallet.
215
		type WeightInfo: WeightInfo;
216
	}
217

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

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

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

            
483
71
	#[pallet::hooks]
484
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
485
30715
		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
486
30715
			let mut weight = <T as Config>::WeightInfo::base_on_initialize();
487
30715

            
488
30715
			let mut round = <Round<T>>::get();
489
30715
			if round.should_update(n) {
490
267
				// fetch current slot number
491
267
				let current_slot: u64 = T::SlotProvider::get().into();
492
267

            
493
267
				// account for SlotProvider read
494
267
				weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 0));
495
267

            
496
267
				// Compute round duration in slots
497
267
				let round_duration = (current_slot.saturating_sub(round.first_slot))
498
267
					.saturating_mul(T::SlotDuration::get());
499
267

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

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

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

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

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

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

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

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

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

            
593
	/// Stores outstanding delegation requests per collator & delegator.
594
	///
595
	/// Each `(collator, delegator)` pair can have up to
596
	/// `T::MaxScheduledRequestsPerDelegator` scheduled requests,
597
	/// which are always interpreted and executed in FIFO order.
598
1568
	#[pallet::storage]
599
	#[pallet::getter(fn delegation_scheduled_requests)]
600
	pub(crate) type DelegationScheduledRequests<T: Config> = StorageDoubleMap<
601
		_,
602
		Blake2_128Concat,
603
		T::AccountId,
604
		Blake2_128Concat,
605
		T::AccountId,
606
		BoundedVec<
607
			ScheduledRequest<T::AccountId, BalanceOf<T>>,
608
			T::MaxScheduledRequestsPerDelegator,
609
		>,
610
		ValueQuery,
611
	>;
612

            
613
	/// Tracks how many delegators have at least one pending delegation request for a given collator.
614
	///
615
	/// This is used to enforce that the number of delegators with pending requests per collator
616
	/// does not exceed `MaxTopDelegationsPerCandidate + MaxBottomDelegationsPerCandidate` without
617
	/// having to iterate over all scheduled requests.
618
252
	#[pallet::storage]
619
	pub(crate) type DelegationScheduledRequestsPerCollator<T: Config> =
620
		StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>;
621

            
622
	/// Stores auto-compounding configuration per collator.
623
1150
	#[pallet::storage]
624
	#[pallet::getter(fn auto_compounding_delegations)]
625
	pub(crate) type AutoCompoundingDelegations<T: Config> = StorageMap<
626
		_,
627
		Blake2_128Concat,
628
		T::AccountId,
629
		BoundedVec<
630
			AutoCompoundConfig<T::AccountId>,
631
			AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
632
		>,
633
		ValueQuery,
634
	>;
635

            
636
2671
	#[pallet::storage]
637
	#[pallet::getter(fn top_delegations)]
638
	/// Top delegations for collator candidate
639
	pub(crate) type TopDelegations<T: Config> = StorageMap<
640
		_,
641
		Twox64Concat,
642
		T::AccountId,
643
		Delegations<T::AccountId, BalanceOf<T>>,
644
		OptionQuery,
645
	>;
646

            
647
1110
	#[pallet::storage]
648
	#[pallet::getter(fn bottom_delegations)]
649
	/// Bottom delegations for collator candidate
650
	pub(crate) type BottomDelegations<T: Config> = StorageMap<
651
		_,
652
		Twox64Concat,
653
		T::AccountId,
654
		Delegations<T::AccountId, BalanceOf<T>>,
655
		OptionQuery,
656
	>;
657

            
658
1803
	#[pallet::storage]
659
	#[pallet::getter(fn selected_candidates)]
660
	/// The collator candidates selected for the current round
661
	type SelectedCandidates<T: Config> =
662
		StorageValue<_, BoundedVec<T::AccountId, T::MaxCandidates>, ValueQuery>;
663

            
664
5413
	#[pallet::storage]
665
	#[pallet::getter(fn total)]
666
	/// Total capital locked by this staking pallet
667
	pub(crate) type Total<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
668

            
669
6785
	#[pallet::storage]
670
	#[pallet::getter(fn candidate_pool)]
671
	/// The pool of collator candidates, each with their total backing stake
672
	pub(crate) type CandidatePool<T: Config> = StorageValue<
673
		_,
674
		BoundedOrderedSet<Bond<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,
675
		ValueQuery,
676
	>;
677

            
678
1969
	#[pallet::storage]
679
	#[pallet::getter(fn at_stake)]
680
	/// Snapshot of collator delegation stake at the start of the round
681
	pub type AtStake<T: Config> = StorageDoubleMap<
682
		_,
683
		Twox64Concat,
684
		RoundIndex,
685
		Twox64Concat,
686
		T::AccountId,
687
		CollatorSnapshot<T::AccountId, BalanceOf<T>>,
688
		OptionQuery,
689
	>;
690

            
691
8231
	#[pallet::storage]
692
	#[pallet::getter(fn was_inactive)]
693
	/// Records collators' inactivity.
694
	/// Data persists for MaxOfflineRounds + 1 rounds before being pruned.
695
	pub type WasInactive<T: Config> =
696
		StorageDoubleMap<_, Twox64Concat, RoundIndex, Twox64Concat, T::AccountId, (), OptionQuery>;
697

            
698
14304
	#[pallet::storage]
699
	#[pallet::getter(fn delayed_payouts)]
700
	/// Delayed payouts
701
	pub type DelayedPayouts<T: Config> =
702
		StorageMap<_, Twox64Concat, RoundIndex, DelayedPayout<BalanceOf<T>>, OptionQuery>;
703

            
704
1695
	#[pallet::storage]
705
	#[pallet::getter(fn inflation_config)]
706
	/// Inflation configuration
707
	pub type InflationConfig<T: Config> = StorageValue<_, InflationInfo<BalanceOf<T>>, ValueQuery>;
708

            
709
31438
	#[pallet::storage]
710
	#[pallet::getter(fn points)]
711
	/// Total points awarded to collators for block production in the round
712
	pub type Points<T: Config> = StorageMap<_, Twox64Concat, RoundIndex, RewardPoint, ValueQuery>;
713

            
714
62282
	#[pallet::storage]
715
	#[pallet::getter(fn awarded_pts)]
716
	/// Points for each collator per round
717
	pub type AwardedPts<T: Config> = StorageDoubleMap<
718
		_,
719
		Twox64Concat,
720
		RoundIndex,
721
		Twox64Concat,
722
		T::AccountId,
723
		RewardPoint,
724
		ValueQuery,
725
	>;
726

            
727
53
	#[pallet::storage]
728
	#[pallet::getter(fn marking_offline)]
729
	/// Killswitch to enable/disable marking offline feature.
730
	pub type EnableMarkingOffline<T: Config> = StorageValue<_, bool, ValueQuery>;
731

            
732
2095
	#[pallet::storage]
733
	/// Temporary storage to track candidates that have been migrated from locks to freezes.
734
	/// This storage should be removed after all accounts have been migrated.
735
	pub type MigratedCandidates<T: Config> =
736
		StorageMap<_, Twox64Concat, T::AccountId, (), OptionQuery>;
737

            
738
19551
	#[pallet::storage]
739
	/// Temporary storage to track delegators that have been migrated from locks to freezes.
740
	/// This storage should be removed after all accounts have been migrated.
741
	pub type MigratedDelegators<T: Config> =
742
		StorageMap<_, Twox64Concat, T::AccountId, (), OptionQuery>;
743

            
744
	#[pallet::genesis_config]
745
	pub struct GenesisConfig<T: Config> {
746
		/// Initialize balance and register all as collators: `(collator AccountId, balance Amount)`
747
		pub candidates: Vec<(T::AccountId, BalanceOf<T>)>,
748
		/// Initialize balance and make delegations:
749
		/// `(delegator AccountId, collator AccountId, delegation Amount, auto-compounding Percent)`
750
		pub delegations: Vec<(T::AccountId, T::AccountId, BalanceOf<T>, Percent)>,
751
		/// Inflation configuration
752
		pub inflation_config: InflationInfo<BalanceOf<T>>,
753
		/// Default fixed percent a collator takes off the top of due rewards
754
		pub collator_commission: Perbill,
755
		/// Default percent of inflation set aside for parachain bond every round
756
		pub parachain_bond_reserve_percent: Percent,
757
		/// Default number of blocks in a round
758
		pub blocks_per_round: u32,
759
		/// Number of selected candidates every round. Cannot be lower than MinSelectedCandidates
760
		pub num_selected_candidates: u32,
761
	}
762

            
763
	impl<T: Config> Default for GenesisConfig<T> {
764
3
		fn default() -> Self {
765
3
			Self {
766
3
				candidates: vec![],
767
3
				delegations: vec![],
768
3
				inflation_config: Default::default(),
769
3
				collator_commission: Default::default(),
770
3
				parachain_bond_reserve_percent: Default::default(),
771
3
				blocks_per_round: 1u32,
772
3
				num_selected_candidates: T::MinSelectedCandidates::get(),
773
3
			}
774
3
		}
775
	}
776

            
777
521
	#[pallet::genesis_build]
778
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
779
524
		fn build(&self) {
780
524
			assert!(self.blocks_per_round > 0, "Blocks per round must be > 0");
781
524
			<InflationConfig<T>>::put(self.inflation_config.clone());
782
524
			let mut candidate_count = 0u32;
783
			// Initialize the candidates
784
1152
			for &(ref candidate, balance) in &self.candidates {
785
628
				assert!(
786
628
					<Pallet<T>>::get_collator_stakable_free_balance(candidate) >= balance,
787
					"Account does not have enough balance to bond as a candidate."
788
				);
789
628
				if let Err(error) = <Pallet<T>>::join_candidates(
790
628
					T::RuntimeOrigin::from(Some(candidate.clone()).into()),
791
628
					balance,
792
628
					candidate_count,
793
628
				) {
794
7
					log::warn!("Join candidates failed in genesis with error {:?}", error);
795
621
				} else {
796
621
					candidate_count = candidate_count.saturating_add(1u32);
797
621
				}
798
			}
799

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

            
897
19
	#[pallet::call]
898
	impl<T: Config> Pallet<T> {
899
		/// Set the expectations for total staked. These expectations determine the issuance for
900
		/// the round according to logic in `fn compute_issuance`
901
		#[pallet::call_index(0)]
902
		#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
903
		pub fn set_staking_expectations(
904
			origin: OriginFor<T>,
905
			expectations: Range<BalanceOf<T>>,
906
6
		) -> DispatchResultWithPostInfo {
907
6
			T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
908
5
			ensure!(expectations.is_valid(), Error::<T>::InvalidSchedule);
909
4
			let mut config = <InflationConfig<T>>::get();
910
4
			ensure!(
911
4
				config.expect != expectations,
912
1
				Error::<T>::NoWritingSameValue
913
			);
914
3
			config.set_expectations(expectations);
915
3
			Self::deposit_event(Event::StakeExpectationsSet {
916
3
				expect_min: config.expect.min,
917
3
				expect_ideal: config.expect.ideal,
918
3
				expect_max: config.expect.max,
919
3
			});
920
3
			<InflationConfig<T>>::put(config);
921
3
			Ok(().into())
922
		}
923

            
924
		/// Set the annual inflation rate to derive per-round inflation
925
		#[pallet::call_index(1)]
926
		#[pallet::weight(<T as Config>::WeightInfo::set_inflation())]
927
		pub fn set_inflation(
928
			origin: OriginFor<T>,
929
			schedule: Range<Perbill>,
930
7
		) -> DispatchResultWithPostInfo {
931
7
			T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
932
5
			ensure!(schedule.is_valid(), Error::<T>::InvalidSchedule);
933
4
			let mut config = <InflationConfig<T>>::get();
934
4
			ensure!(config.annual != schedule, Error::<T>::NoWritingSameValue);
935
3
			config.annual = schedule;
936
3
			config.set_round_from_annual::<T>(schedule);
937
3
			Self::deposit_event(Event::InflationSet {
938
3
				annual_min: config.annual.min,
939
3
				annual_ideal: config.annual.ideal,
940
3
				annual_max: config.annual.max,
941
3
				round_min: config.round.min,
942
3
				round_ideal: config.round.ideal,
943
3
				round_max: config.round.max,
944
3
			});
945
3
			<InflationConfig<T>>::put(config);
946
3
			Ok(().into())
947
		}
948

            
949
		/// Set the total number of collator candidates selected per round
950
		/// - changes are not applied until the start of the next round
951
		#[pallet::call_index(4)]
952
		#[pallet::weight(<T as Config>::WeightInfo::set_total_selected())]
953
11
		pub fn set_total_selected(origin: OriginFor<T>, new: u32) -> DispatchResultWithPostInfo {
954
11
			frame_system::ensure_root(origin)?;
955
10
			ensure!(
956
10
				new >= T::MinSelectedCandidates::get(),
957
1
				Error::<T>::CannotSetBelowMin
958
			);
959
9
			ensure!(
960
9
				new <= T::MaxCandidates::get(),
961
1
				Error::<T>::CannotSetAboveMaxCandidates
962
			);
963
8
			let old = <TotalSelected<T>>::get();
964
8
			ensure!(old != new, Error::<T>::NoWritingSameValue);
965
7
			ensure!(
966
7
				new < <Round<T>>::get().length,
967
2
				Error::<T>::RoundLengthMustBeGreaterThanTotalSelectedCollators,
968
			);
969
5
			<TotalSelected<T>>::put(new);
970
5
			Self::deposit_event(Event::TotalSelectedSet { old, new });
971
5
			Ok(().into())
972
		}
973

            
974
		/// Set the commission for all collators
975
		#[pallet::call_index(5)]
976
		#[pallet::weight(<T as Config>::WeightInfo::set_collator_commission())]
977
		pub fn set_collator_commission(
978
			origin: OriginFor<T>,
979
			new: Perbill,
980
4
		) -> DispatchResultWithPostInfo {
981
4
			frame_system::ensure_root(origin)?;
982
3
			let old = <CollatorCommission<T>>::get();
983
3
			ensure!(old != new, Error::<T>::NoWritingSameValue);
984
2
			<CollatorCommission<T>>::put(new);
985
2
			Self::deposit_event(Event::CollatorCommissionSet { old, new });
986
2
			Ok(().into())
987
		}
988

            
989
		/// Set blocks per round
990
		/// - if called with `new` less than length of current round, will transition immediately
991
		/// in the next block
992
		/// - also updates per-round inflation config
993
		#[pallet::call_index(6)]
994
		#[pallet::weight(<T as Config>::WeightInfo::set_blocks_per_round())]
995
17
		pub fn set_blocks_per_round(origin: OriginFor<T>, new: u32) -> DispatchResultWithPostInfo {
996
17
			frame_system::ensure_root(origin)?;
997
16
			ensure!(
998
16
				new >= T::MinBlocksPerRound::get(),
999
1
				Error::<T>::CannotSetBelowMin
			);
15
			let mut round = <Round<T>>::get();
15
			let (now, first, old) = (round.current, round.first, round.length);
15
			ensure!(old != new, Error::<T>::NoWritingSameValue);
14
			ensure!(
14
				new > <TotalSelected<T>>::get(),
2
				Error::<T>::RoundLengthMustBeGreaterThanTotalSelectedCollators,
			);
12
			round.length = new;
12
			// update per-round inflation given new rounds per year
12
			let mut inflation_config = <InflationConfig<T>>::get();
12
			inflation_config.reset_round::<T>(new);
12
			<Round<T>>::put(round);
12
			Self::deposit_event(Event::BlocksPerRoundSet {
12
				current_round: now,
12
				first_block: first,
12
				old: old,
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,
668
		) -> DispatchResultWithPostInfo {
668
			let acc = ensure_signed(origin)?;
668
			ensure!(
668
				bond >= T::MinCandidateStk::get(),
8
				Error::<T>::CandidateBondBelowMin
			);
660
			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,
9090
		) -> DispatchResultWithPostInfo {
9090
			let delegator = ensure_signed(origin)?;
9090
			<AutoCompoundDelegations<T>>::delegate_with_auto_compound(
9090
				candidate,
9090
				delegator,
9090
				amount,
9090
				auto_compound,
9090
				candidate_delegation_count,
9090
				candidate_auto_compounding_delegation_count,
9090
				delegation_count,
9090
			)
		}
		/// 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,
52
		) -> DispatchResultWithPostInfo {
52
			let delegator = ensure_signed(origin)?;
52
			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 a revoke request is pending for the same delegation.
		#[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>,
89
		) -> DispatchResultWithPostInfo {
89
			let delegator = ensure_signed(origin)?;
89
			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,
39
		) -> DispatchResultWithPostInfo {
39
			ensure_signed(origin)?; // we may want to reward caller if caller != delegator
39
			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 MAX_ACCOUNTS_PER_MIGRATION_BATCH.
		/// 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(200)]
		#[pallet::weight(T::WeightInfo::migrate_locks_to_freezes_batch(
			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;
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
				}
			}
			// Calculate success rate
14
			let success_rate = if total_accounts > 0 {
14
				Percent::from_rational(successful_migrations, total_accounts)
			} else {
				Percent::zero()
			};
			// 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
			};
			// Returning `actual_weight` as `None` stands for the worst case static weight.
14
			Ok(pays_fee.into())
		}
	}
	/// 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
21391
		fn check_and_migrate_lock(account: &T::AccountId, is_collator: bool) -> bool {
			use frame_support::traits::{fungible::MutateFreeze, LockableCurrency};
			// Check if already migrated
21391
			if is_collator {
1986
				if <MigratedCandidates<T>>::contains_key(account) {
16
					return false;
1970
				}
			} else {
19405
				if <MigratedDelegators<T>>::contains_key(account) {
240
					return false;
19165
				}
			}
			// Not migrated yet, proceed with migration
21135
			let (lock_id, freeze_reason) = if is_collator {
1970
				(COLLATOR_LOCK_ID, FreezeReason::StakingCollator)
			} else {
19165
				(DELEGATOR_LOCK_ID, FreezeReason::StakingDelegator)
			};
			// Get the amount that should be locked/frozen from storage
21135
			let amount = if is_collator {
				// For collators, get the bond amount from storage
1970
				match <CandidateInfo<T>>::get(account) {
45
					Some(info) => info.bond,
1925
					None => return false,
				}
			} else {
				// For delegators, get the total delegated amount from storage
19165
				match <DelegatorState<T>>::get(account) {
115
					Some(state) => state.total,
19050
					None => return false,
				}
			};
160
			if amount > BalanceOf::<T>::zero() {
				// Remove any existing lock
159
				T::Currency::remove_lock(lock_id, account);
159

            
159
				// Set the freeze
159
				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;
159
				}
1
			}
160
			if is_collator {
45
				<MigratedCandidates<T>>::insert(account, ());
115
			} else {
115
				<MigratedDelegators<T>>::insert(account, ());
115
			}
160
			true
21391
		}
		/// 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
1309
		pub(crate) fn freeze_extended(
1309
			account: &T::AccountId,
1309
			amount: BalanceOf<T>,
1309
			is_collator: bool,
1309
		) -> DispatchResult {
			use frame_support::traits::fungible::MutateFreeze;
			// First check and migrate any existing lock
1309
			let _ = Self::check_and_migrate_lock(account, is_collator);
			// Now set the freeze
1309
			let freeze_reason = if is_collator {
658
				FreezeReason::StakingCollator
			} else {
651
				FreezeReason::StakingDelegator
			};
1309
			T::Currency::set_freeze(&freeze_reason.into(), account, amount)
1309
		}
		/// 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
			T::Currency::thaw(&freeze_reason.into(), account)
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 {
1302
				<CandidateInfo<T>>::get(account).map(|info| info.bond)
			} else {
18746
				<DelegatorState<T>>::get(account).map(|state| state.total)
			}
20048
		}
686
		pub fn is_delegator(acc: &T::AccountId) -> bool {
686
			<DelegatorState<T>>::get(acc).is_some()
686
		}
9683
		pub fn is_candidate(acc: &T::AccountId) -> bool {
9683
			<CandidateInfo<T>>::get(acc).is_some()
9683
		}
2
		pub fn is_selected_candidate(acc: &T::AccountId) -> bool {
2
			<SelectedCandidates<T>>::get().binary_search(acc).is_ok()
2
		}
661
		pub fn join_candidates_inner(
661
			acc: T::AccountId,
661
			bond: BalanceOf<T>,
661
			candidate_count: u32,
661
		) -> DispatchResultWithPostInfo {
661
			ensure!(!Self::is_candidate(&acc), Error::<T>::CandidateExists);
657
			ensure!(!Self::is_delegator(&acc), Error::<T>::DelegatorExists);
653
			let mut candidates = <CandidatePool<T>>::get();
653
			let old_count = candidates.0.len() as u32;
653
			ensure!(
653
				candidate_count >= old_count,
5
				Error::<T>::TooLowCandidateCountWeightHintJoinCandidates
			);
648
			let maybe_inserted_candidate = candidates
648
				.try_insert(Bond {
648
					owner: acc.clone(),
648
					amount: bond,
648
				})
648
				.map_err(|_| Error::<T>::CandidateLimitReached)?;
647
			ensure!(maybe_inserted_candidate, Error::<T>::CandidateExists);
647
			ensure!(
647
				Self::get_collator_stakable_free_balance(&acc) >= bond,
4
				Error::<T>::InsufficientBalance,
			);
643
			Self::freeze_extended(&acc, bond, true).map_err(|_| Error::<T>::InsufficientBalance)?;
643
			let candidate = CandidateMetadata::new(bond);
643
			<CandidateInfo<T>>::insert(&acc, candidate);
643
			let empty_delegations: Delegations<T::AccountId, BalanceOf<T>> = Default::default();
643
			// insert empty top delegations
643
			<TopDelegations<T>>::insert(&acc, empty_delegations.clone());
643
			// insert empty bottom delegations
643
			<BottomDelegations<T>>::insert(&acc, empty_delegations);
643
			<CandidatePool<T>>::put(candidates);
643
			let new_total = <Total<T>>::get().saturating_add(bond);
643
			<Total<T>>::put(new_total);
643
			Self::deposit_event(Event::JoinedCollatorCandidates {
643
				account: acc,
643
				amount_locked: bond,
643
				new_total_amt_locked: new_total,
643
			});
643
			Ok(().into())
661
		}
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>>| -> DispatchResult {
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() {
						// we do not remove the scheduled delegation requests from other collators
						// since it is assumed that they were removed incrementally before only the
						// last delegation was left.
9
						<DelegatorState<T>>::remove(&bond.owner);
9
						// Thaw all frozen funds for delegator
9
						Self::thaw_extended(&bond.owner, false)?;
6
					} else {
6
						<DelegatorState<T>>::insert(&bond.owner, delegator);
6
					}
				} else {
					Self::thaw_extended(&bond.owner, false)?;
				}
15
				Ok(())
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)?;
			}
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)?;
			}
22
			total_backing = total_backing.saturating_add(bottom_delegations.total);
22
			// Thaw all frozen funds for collator
22
			Self::thaw_extended(&candidate, true)?;
22
			<CandidateInfo<T>>::remove(&candidate);
22
			// Remove all scheduled delegation requests for this collator
22
			let _ = <DelegationScheduledRequests<T>>::clear_prefix(
22
				&candidate,
22
				Self::max_delegators_per_candidate(),
22
				None,
22
			);
22
			<DelegationScheduledRequestsPerCollator<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
		}
104
		pub fn max_delegators_per_candidate() -> u32 {
104
			AddGet::<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>::get()
104
		}
		/// Returns an account's stakable balance (including the reserved) which is not frozen in delegation staking
18746
		pub fn get_delegator_stakable_balance(acc: &T::AccountId) -> BalanceOf<T> {
18746
			let total_balance =
18746
				T::Currency::balance(acc).saturating_add(T::Currency::reserved_balance(acc));
18746
			if let Some(frozen_balance) = Self::balance_frozen_extended(acc, false) {
229
				return total_balance.saturating_sub(frozen_balance);
18517
			}
18517
			total_balance
18746
		}
		/// Returns an account's free balance which is not frozen in collator staking
1302
		pub fn get_collator_stakable_free_balance(acc: &T::AccountId) -> BalanceOf<T> {
1302
			let total_balance = T::Currency::balance(acc);
1302
			if let Some(frozen_balance) = Self::balance_frozen_extended(acc, true) {
22
				return total_balance.saturating_sub(frozen_balance);
1280
			}
1280
			total_balance
1302
		}
		/// 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
525
		pub(crate) fn update_active(candidate: T::AccountId, total: BalanceOf<T>) {
525
			let mut candidates = <CandidatePool<T>>::get();
525
			candidates.remove(&Bond::from_owner(candidate.clone()));
525
			candidates
525
				.try_insert(Bond {
525
					owner: candidate,
525
					amount: total,
525
				})
525
				.expect(
525
					"the candidate is removed in previous step so the length cannot increase; qed",
525
				);
525
			<CandidatePool<T>>::put(candidates);
525
		}
		/// Compute round issuance based on duration of the given round
267
		fn compute_issuance(round_duration: u64, round_length: u32) -> BalanceOf<T> {
267
			let ideal_duration: BalanceOf<T> = round_length
267
				.saturating_mul(T::BlockTime::get() as u32)
267
				.into();
267
			let config = <InflationConfig<T>>::get();
267
			let round_issuance = crate::inflation::round_issuance_range::<T>(config.round);
267

            
267
			// Initial formula: (round_duration / ideal_duration) * ideal_issuance
267
			// We multiply before the division to reduce rounding effects
267
			BalanceOf::<T>::from(round_duration as u32).saturating_mul(round_issuance.ideal)
267
				/ (ideal_duration)
267
		}
		/// 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
		}
267
		pub(crate) fn prepare_staking_payouts(
267
			round_info: RoundInfo<BlockNumberFor<T>>,
267
			round_duration: u64,
267
		) -> Weight {
267
			let RoundInfo {
267
				current: now,
267
				length: round_length,
267
				..
267
			} = round_info;
267

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

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

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

            
267
			let configs = <InflationDistributionInfo<T>>::get().0;
534
			for (index, config) in configs.iter().enumerate() {
534
				if config.percent.is_zero() {
267
					continue;
267
				}
267
				let reserve = config.percent * total_issuance;
267
				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
					}
261
				}
			}
267
			let payout = DelayedPayout {
267
				round_issuance: total_issuance,
267
				total_staking_reward: left_issuance,
267
				collator_commission: <CollatorCommission<T>>::get(),
267
			};
267

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

            
267
			<T as Config>::WeightInfo::prepare_staking_payouts()
267
		}
		/// 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
30448
		fn handle_delayed_payouts(now: RoundIndex) -> Weight {
30448
			let delay = T::RewardPaymentDelay::get();
30448

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

            
13949
			let paid_for_round = now.saturating_sub(delay);
13949
			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 {
13597
				Weight::from_parts(0u64, 0)
			}
30448
		}
		/// 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
				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
					),
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.
792
		pub fn compute_top_candidates() -> Vec<T::AccountId> {
792
			let top_n = <TotalSelected<T>>::get() as usize;
792
			if top_n == 0 {
				return vec![];
792
			}
792

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

            
792
			// If the number of candidates is greater than top_n, select the candidates with higher
792
			// amount. Otherwise, return all the candidates.
792
			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
1036
				candidates.into_iter().map(|x| x.owner).collect::<Vec<_>>()
			}
792
		}
		/// Best as in most cumulatively supported in terms of stake
		/// Returns [collator_count, delegation_count, total staked]
791
		pub(crate) fn select_top_candidates(now: RoundIndex) -> (Weight, u32, u32, BalanceOf<T>) {
791
			let (mut collator_count, mut delegation_count, mut total) =
791
				(0u32, 0u32, BalanceOf::<T>::zero());
791
			// choose the top TotalSelected qualified candidates, ordered by stake
791
			let collators = Self::compute_top_candidates();
791
			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);
476
			}
			// snapshot exposure for round for weighting reward distribution
921
			for account in collators.iter() {
921
				let state = <CandidateInfo<T>>::get(account)
921
					.expect("all members of CandidateQ must be candidates");
921

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

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

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

            
476
			let avg_delegator_count = delegation_count.checked_div(collator_count).unwrap_or(0);
476
			let weight = <T as Config>::WeightInfo::select_top_candidates(
476
				collator_count,
476
				avg_delegator_count,
476
			);
476
			(weight, collator_count, delegation_count, total)
791
		}
		/// 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.
921
		pub(crate) fn get_rewardable_delegators(collator: &T::AccountId) -> CountedDelegations<T> {
921
			// Aggregate the net effect of all scheduled requests per delegator for this collator.
921
			// If a revoke exists, it dominates and is treated as a full revoke.
921
			// Otherwise, decreases are summed.
921
			let mut requests: BTreeMap<T::AccountId, DelegationAction<BalanceOf<T>>> =
921
				BTreeMap::new();
115
			for (delegator, scheduled_requests) in
921
				<DelegationScheduledRequests<T>>::iter_prefix(collator)
			{
115
				if scheduled_requests.is_empty() {
					continue;
115
				}
115

            
115
				// Compute in a single pass whether any revoke exists and, if not,
115
				// the total amount of all decreases.
115
				let (has_revoke, total) = scheduled_requests.iter().fold(
115
					(false, BalanceOf::<T>::zero()),
117
					|(has_revoke, total), req| {
117
						let has_revoke =
117
							has_revoke || matches!(req.action, DelegationAction::Revoke(_));
117
						let total = if has_revoke {
							// Once a revoke is present, we ignore the accumulated decrease total.
75
							BalanceOf::<T>::zero()
						} else {
42
							total.saturating_add(req.action.amount())
						};
117
						(has_revoke, total)
117
					},
115
				);
115

            
115
				if has_revoke {
75
					// Amount is irrelevant for revokes in this context, since we always
75
					// zero out the bond and account the full previous stake as uncounted.
75
					requests.insert(delegator, DelegationAction::Revoke(BalanceOf::<T>::zero()));
75
				} else if !total.is_zero() {
40
					requests.insert(delegator, DelegationAction::Decrease(total));
40
				}
			}
921
			let mut uncounted_stake = BalanceOf::<T>::zero();
921
			let rewardable_delegations = <TopDelegations<T>>::get(collator)
921
				.expect("all members of CandidateQ must be candidates")
921
				.delegations
921
				.into_iter()
921
				.map(|mut bond| {
771
					bond.amount = match requests.get(&bond.owner) {
658
						None => bond.amount,
						Some(DelegationAction::Revoke(_)) => {
75
							uncounted_stake = uncounted_stake.saturating_add(bond.amount);
75
							BalanceOf::<T>::zero()
						}
38
						Some(DelegationAction::Decrease(amount)) => {
38
							uncounted_stake = uncounted_stake.saturating_add(*amount);
38
							bond.amount.saturating_sub(*amount)
						}
					};
771
					bond
921
				})
921
				.collect();
921
			CountedDelegations {
921
				uncounted_stake,
921
				rewardable_delegations,
921
			}
921
		}
		/// 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
			);
			// This helper does not depend on the number of scheduled requests; we pass 0
			// here and rely on the extrinsic declaration for an upper bound.
21
			let actual_weight = <T as Config>::WeightInfo::delegator_bond_more(0);
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.
143
		pub fn mint(amt: BalanceOf<T>, to: &T::AccountId) -> Result<BalanceOf<T>, DispatchError> {
			// Mint rewards to the account
143
			let minted = T::Currency::mint_into(&to, amt)?;
143
			Self::deposit_event(Event::Rewarded {
143
				account: to.clone(),
143
				rewards: minted,
143
			});
143
			Ok(minted)
143
		}
		/// Mint a specified reward amount to the collator's account. Emits the [Rewarded] event.
88
		pub fn mint_collator_reward(
88
			_paid_for_round: RoundIndex,
88
			collator_id: T::AccountId,
88
			amt: BalanceOf<T>,
88
		) -> Weight {
			// Mint rewards to the collator
88
			if let Err(e) = Self::mint(amt, &collator_id) {
				log::warn!(
					"Failed to mint collator reward for {:?}: {:?}",
					collator_id,
					e
				);
88
			}
88
			<T as Config>::WeightInfo::mint_collator_reward()
88
		}
		/// 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
30656
		fn award_points_to_block_author() {
30656
			let author = T::BlockAuthor::get();
30656
			let now = <Round<T>>::get().current;
30656
			let score_plus_20 = <AwardedPts<T>>::get(now, &author).saturating_add(20);
30656
			<AwardedPts<T>>::insert(now, author, score_plus_20);
30656
			<Points<T>>::mutate(now, |x| *x = x.saturating_add(20));
30656
		}
		/// Marks collators as inactive for the previous round if they received zero awarded points.
267
		pub fn mark_collators_as_inactive(cur: RoundIndex) -> Weight {
267
			// This function is called after round index increment,
267
			// We don't need to saturate here because the genesis round is 1.
267
			let prev = cur - 1;
267

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

            
30656
			if now < minimum_rounds_required {
22931
				return;
7725
			}
7725

            
7725
			let _ = <WasInactive<T>>::iter_prefix(now - minimum_rounds_required)
7725
				.drain()
7725
				.next();
30656
		}
	}
	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> {
57
		fn get() -> Vec<T::AccountId> {
57
			Self::selected_candidates().into_inner()
57
		}
	}
}