1#![cfg_attr(not(feature = "std"), no_std)]
82#![allow(clippy::useless_conversion)]
83
84pub use pallet::*;
85pub mod migrations;
86
87#[cfg(test)]
88mod mock;
89
90#[cfg(test)]
91mod tests;
92
93#[cfg(feature = "runtime-benchmarks")]
94mod benchmarking;
95pub mod weights;
96
97#[frame_support::pallet]
98pub mod pallet {
99 pub use crate::weights::WeightInfo;
100 use core::ops::Div;
101 use frame_support::{
102 dispatch::{DispatchClass, DispatchResultWithPostInfo},
103 ensure,
104 pallet_prelude::*,
105 sp_runtime::{
106 traits::{AccountIdConversion, CheckedSub, Saturating, Zero},
107 RuntimeDebug,
108 },
109 traits::{
110 Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, ReservableCurrency,
111 ValidatorRegistration, ValidatorSet,
112 },
113 DefaultNoBound, PalletId,
114 };
115 use frame_system::{pallet_prelude::*, Config as SystemConfig};
116 use pallet_session::SessionManager;
117 use sp_runtime::{
118 traits::{BadOrigin, Convert},
119 Perbill,
120 };
121 use sp_staking::SessionIndex;
122 use sp_std::prelude::*;
123
124 type BalanceOf<T> =
125 <<T as Config>::Currency as Currency<<T as SystemConfig>::AccountId>>::Balance;
126
127 pub struct IdentityCollator;
130 impl<T> sp_runtime::traits::Convert<T, Option<T>> for IdentityCollator {
131 fn convert(t: T) -> Option<T> {
132 Some(t)
133 }
134 }
135
136 pub trait AccountCheck<AccountId> {
138 fn allowed_candidacy(account: &AccountId) -> bool;
140 }
141
142 #[pallet::config]
144 pub trait Config: frame_system::Config {
145 type Currency: ReservableCurrency<Self::AccountId>;
147
148 type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
150
151 type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
154
155 type ForceRemovalOrigin: EnsureOrigin<Self::RuntimeOrigin>;
158
159 type PotId: Get<PalletId>;
161
162 type MaxCandidates: Get<u32>;
167
168 type MinCandidates: Get<u32>;
172
173 type MaxInvulnerables: Get<u32>;
177
178 type KickThreshold: Get<BlockNumberFor<Self>>;
180
181 type ValidatorId: Member + Parameter;
183
184 type ValidatorIdOf: Convert<Self::AccountId, Option<Self::ValidatorId>>;
188
189 type ValidatorRegistration: ValidatorRegistration<Self::ValidatorId>;
191
192 type ValidatorSet: ValidatorSet<Self::AccountId, ValidatorId = Self::AccountId>;
194
195 type SlashRatio: Get<Perbill>;
197
198 type AccountCheck: AccountCheck<Self::AccountId>;
200
201 type WeightInfo: WeightInfo;
203 }
204
205 #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)]
207 pub struct CandidateInfo<AccountId, Balance> {
208 pub who: AccountId,
210 pub deposit: Balance,
212 }
213
214 #[pallet::pallet]
215 #[pallet::without_storage_info]
216 pub struct Pallet<T>(_);
217
218 #[pallet::storage]
220 pub type Invulnerables<T: Config> = StorageValue<_, Vec<T::AccountId>, ValueQuery>;
221
222 #[pallet::storage]
224 pub type Candidates<T: Config> =
225 StorageValue<_, Vec<CandidateInfo<T::AccountId, BalanceOf<T>>>, ValueQuery>;
226
227 #[pallet::storage]
229 pub type NonCandidates<T: Config> =
230 StorageMap<_, Twox64Concat, T::AccountId, (SessionIndex, BalanceOf<T>), OptionQuery>;
231
232 #[pallet::storage]
234 pub type LastAuthoredBlock<T: Config> =
235 StorageMap<_, Twox64Concat, T::AccountId, BlockNumberFor<T>, ValueQuery>;
236
237 #[pallet::storage]
241 pub type DesiredCandidates<T> = StorageValue<_, u32, ValueQuery>;
242
243 #[pallet::storage]
247 pub type CandidacyBond<T> = StorageValue<_, BalanceOf<T>, ValueQuery>;
248
249 #[pallet::storage]
251 pub type SlashDestination<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
252
253 #[pallet::storage]
255 pub type PendingApplications<T: Config> =
256 StorageMap<_, Twox64Concat, T::AccountId, BalanceOf<T>, OptionQuery>;
257
258 #[pallet::genesis_config]
259 #[derive(DefaultNoBound)]
260 pub struct GenesisConfig<T: Config> {
261 pub invulnerables: Vec<T::AccountId>,
262 pub candidacy_bond: BalanceOf<T>,
263 pub desired_candidates: u32,
264 }
265
266 #[pallet::genesis_build]
267 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
268 fn build(&self) {
269 let duplicate_invulnerables = self
270 .invulnerables
271 .iter()
272 .collect::<sp_std::collections::btree_set::BTreeSet<_>>();
273 assert_eq!(
274 duplicate_invulnerables.len(),
275 self.invulnerables.len(),
276 "duplicate invulnerables in genesis."
277 );
278
279 assert!(
280 T::MaxInvulnerables::get() >= (self.invulnerables.len() as u32),
281 "genesis invulnerables are more than T::MaxInvulnerables",
282 );
283 assert!(
284 T::MaxCandidates::get() >= self.desired_candidates,
285 "genesis desired_candidates are more than T::MaxCandidates",
286 );
287
288 <DesiredCandidates<T>>::put(self.desired_candidates);
289 <CandidacyBond<T>>::put(self.candidacy_bond);
290 <Invulnerables<T>>::put(&self.invulnerables);
291 }
292 }
293
294 #[pallet::event]
295 #[pallet::generate_deposit(pub(super) fn deposit_event)]
296 pub enum Event<T: Config> {
297 NewInvulnerables(Vec<T::AccountId>),
299 NewDesiredCandidates(u32),
301 NewCandidacyBond(BalanceOf<T>),
303 CandidateAdded(T::AccountId, BalanceOf<T>),
305 CandidateRemoved(T::AccountId),
307 CandidateSlashed(T::AccountId),
309 CandidacyApplicationSubmitted(T::AccountId, BalanceOf<T>),
311 CandidacyApplicationApproved(T::AccountId, BalanceOf<T>),
313 CandidacyApplicationClosed(T::AccountId),
315 CandidateKicked(T::AccountId),
317 }
318
319 #[pallet::error]
321 pub enum Error<T> {
322 TooManyCandidates,
324 TooFewCandidates,
326 Unknown,
328 Permission,
330 AlreadyCandidate,
332 NotCandidate,
334 AlreadyInvulnerable,
336 NotInvulnerable,
338 NoAssociatedValidatorId,
340 ValidatorNotRegistered,
342 NotAllowedCandidate,
344 BondStillLocked,
346 NoCandidacyBond,
348 PendingApplicationExists,
350 NoApplicationFound,
352 }
353
354 #[pallet::hooks]
355 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
356
357 #[pallet::call]
358 impl<T: Config> Pallet<T> {
359 #[pallet::call_index(0)]
361 #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]
362 pub fn set_invulnerables(
363 origin: OriginFor<T>,
364 new: Vec<T::AccountId>,
365 ) -> DispatchResultWithPostInfo {
366 T::UpdateOrigin::ensure_origin(origin)?;
367 if (new.len() as u32) > T::MaxInvulnerables::get() {
369 log::warn!(
370 "invulnerables > T::MaxInvulnerables; you might need to run benchmarks again"
371 );
372 }
373
374 for account_id in &new {
376 Self::is_validator_registered(account_id)?;
377 }
378
379 <Invulnerables<T>>::put(&new);
380 Self::deposit_event(Event::NewInvulnerables(new));
381 Ok(().into())
382 }
383
384 #[pallet::call_index(1)]
388 #[pallet::weight(T::WeightInfo::set_desired_candidates())]
389 pub fn set_desired_candidates(
390 origin: OriginFor<T>,
391 max: u32,
392 ) -> DispatchResultWithPostInfo {
393 T::UpdateOrigin::ensure_origin(origin)?;
394 if max > T::MaxCandidates::get() {
396 log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");
397 }
398 <DesiredCandidates<T>>::put(max);
399 Self::deposit_event(Event::NewDesiredCandidates(max));
400 Ok(().into())
401 }
402
403 #[pallet::call_index(2)]
405 #[pallet::weight(T::WeightInfo::set_candidacy_bond())]
406 pub fn set_candidacy_bond(
407 origin: OriginFor<T>,
408 bond: BalanceOf<T>,
409 ) -> DispatchResultWithPostInfo {
410 T::UpdateOrigin::ensure_origin(origin)?;
411 <CandidacyBond<T>>::put(bond);
412 Self::deposit_event(Event::NewCandidacyBond(bond));
413 Ok(().into())
414 }
415
416 #[pallet::call_index(3)]
425 #[pallet::weight(T::WeightInfo::register_as_candidate())]
426 pub fn register_as_candidate(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
427 let _who = ensure_signed(origin)?;
428 Err(Error::<T>::Permission.into())
430 }
431
432 #[pallet::call_index(4)]
439 #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]
440 pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
441 let who = ensure_signed(origin)?;
442 ensure!(
443 Candidates::<T>::get().len() as u32 > T::MinCandidates::get(),
444 Error::<T>::TooFewCandidates
445 );
446 let current_count = Self::try_remove_candidate(&who)?;
447 Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
448 }
449
450 #[pallet::call_index(5)]
453 #[pallet::weight(T::WeightInfo::withdraw_bond())]
454 pub fn withdraw_bond(origin: OriginFor<T>) -> DispatchResult {
455 let who = ensure_signed(origin)?;
456
457 <NonCandidates<T>>::try_mutate_exists(&who, |maybe| -> DispatchResult {
458 if let Some((index, deposit)) = maybe.take() {
459 ensure!(
460 T::ValidatorSet::session_index() >= index,
461 Error::<T>::BondStillLocked
462 );
463 T::Currency::unreserve(&who, deposit);
464 <LastAuthoredBlock<T>>::remove(&who);
465 Ok(())
466 } else {
467 Err(Error::<T>::NoCandidacyBond.into())
468 }
469 })?;
470
471 Ok(())
472 }
473
474 #[pallet::call_index(6)]
477 #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
478 pub fn set_slash_destination(
479 origin: OriginFor<T>,
480 destination: Option<T::AccountId>,
481 ) -> DispatchResult {
482 T::UpdateOrigin::ensure_origin(origin)?;
483 match destination {
484 Some(account) => <SlashDestination<T>>::put(account),
485 None => <SlashDestination<T>>::kill(),
486 }
487 Ok(())
488 }
489
490 #[pallet::call_index(7)]
492 #[pallet::weight(T::WeightInfo::add_invulnerable(T::MaxInvulnerables::get()))]
493 pub fn add_invulnerable(
494 origin: OriginFor<T>,
495 who: T::AccountId,
496 ) -> DispatchResultWithPostInfo {
497 T::UpdateOrigin::ensure_origin(origin)?;
498 Self::is_validator_registered(&who)?;
499
500 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
501 ensure!(
502 !invulnerables.contains(&who),
503 Error::<T>::AlreadyInvulnerable
504 );
505 invulnerables.push(who);
506 Ok(())
507 })?;
508
509 Self::deposit_event(Event::NewInvulnerables(<Invulnerables<T>>::get()));
510 Ok(().into())
511 }
512
513 #[pallet::call_index(8)]
515 #[pallet::weight(T::WeightInfo::remove_invulnerable(T::MaxInvulnerables::get()))]
516 pub fn remove_invulnerable(
517 origin: OriginFor<T>,
518 who: T::AccountId,
519 ) -> DispatchResultWithPostInfo {
520 T::UpdateOrigin::ensure_origin(origin)?;
521 <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
522 if let Some(pos) = invulnerables.iter().position(|acc| *acc == who) {
523 invulnerables.remove(pos);
524 Ok(())
525 } else {
526 Err(Error::<T>::NotInvulnerable.into())
527 }
528 })?;
529
530 Self::deposit_event(Event::NewInvulnerables(<Invulnerables<T>>::get()));
531 Ok(().into())
532 }
533
534 #[pallet::call_index(9)]
539 #[pallet::weight(T::WeightInfo::apply_for_candidacy())]
540 pub fn apply_for_candidacy(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
541 let who = ensure_signed(origin)?;
542
543 ensure!(
544 !PendingApplications::<T>::contains_key(&who),
545 Error::<T>::PendingApplicationExists
546 );
547 ensure!(
548 !Invulnerables::<T>::get().contains(&who),
549 Error::<T>::AlreadyInvulnerable
550 );
551 ensure!(
552 T::AccountCheck::allowed_candidacy(&who),
553 Error::<T>::NotAllowedCandidate
554 );
555 ensure!(
556 !Self::is_account_candidate(&who),
557 Error::<T>::AlreadyCandidate
558 );
559
560 Self::is_validator_registered(&who)?;
561
562 <NonCandidates<T>>::try_mutate_exists(&who, |maybe| -> DispatchResult {
564 if let Some((index, deposit)) = maybe.take() {
565 ensure!(
566 T::ValidatorSet::session_index() >= index,
567 Error::<T>::BondStillLocked
568 );
569 T::Currency::unreserve(&who, deposit);
571 }
572 Ok(())
573 })?;
574
575 let bond = CandidacyBond::<T>::get();
576 T::Currency::reserve(&who, bond)?;
577 PendingApplications::<T>::insert(&who, bond);
578
579 Self::deposit_event(Event::CandidacyApplicationSubmitted(who, bond));
580 Ok(().into())
581 }
582
583 #[pallet::call_index(10)]
588 #[pallet::weight(T::WeightInfo::close_application())]
589 pub fn close_application(
590 origin: OriginFor<T>,
591 who: T::AccountId,
592 ) -> DispatchResultWithPostInfo {
593 Self::ensure_governance_or_caller(origin, &who)?;
594
595 let deposit =
596 PendingApplications::<T>::take(&who).ok_or(Error::<T>::NoApplicationFound)?;
597
598 T::Currency::unreserve(&who, deposit);
599
600 Self::deposit_event(Event::CandidacyApplicationClosed(who));
601 Ok(().into())
602 }
603
604 #[pallet::call_index(11)]
609 #[pallet::weight(T::WeightInfo::approve_application(T::MaxCandidates::get()))]
610 pub fn approve_application(
611 origin: OriginFor<T>,
612 who: T::AccountId,
613 ) -> DispatchResultWithPostInfo {
614 T::GovernanceOrigin::ensure_origin(origin)?;
615
616 let deposit =
617 PendingApplications::<T>::take(&who).ok_or(Error::<T>::NoApplicationFound)?;
618
619 ensure!(
620 T::AccountCheck::allowed_candidacy(&who),
621 Error::<T>::NotAllowedCandidate
622 );
623
624 let current_candidates = Candidates::<T>::decode_len().unwrap_or_default() as u32;
626 ensure!(
627 current_candidates < DesiredCandidates::<T>::get(),
628 Error::<T>::TooManyCandidates
629 );
630
631 Self::is_validator_registered(&who)?;
632
633 let incoming = CandidateInfo {
634 who: who.clone(),
635 deposit,
636 };
637
638 let current_count = <Candidates<T>>::mutate(|candidates| {
639 candidates.push(incoming);
640 <LastAuthoredBlock<T>>::insert(
641 &who,
642 frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),
644 );
645 candidates.len()
646 });
647
648 Self::deposit_event(Event::CandidateAdded(who, deposit));
649 Ok(Some(T::WeightInfo::approve_application(current_count as u32)).into())
650 }
651
652 #[pallet::call_index(12)]
660 #[pallet::weight(T::WeightInfo::kick_candidate(T::MaxInvulnerables::get()))]
661 pub fn kick_candidate(
662 origin: OriginFor<T>,
663 who: T::AccountId,
664 ) -> DispatchResultWithPostInfo {
665 T::ForceRemovalOrigin::ensure_origin(origin)?;
666 ensure!(
667 Candidates::<T>::get().len() > T::MinCandidates::get() as usize,
668 Error::<T>::TooFewCandidates
669 );
670 let current_count = Self::try_remove_candidate(&who)?;
671 Self::slash_non_candidate(&who);
674
675 Self::deposit_event(Event::CandidateKicked(who));
676 Ok(Some(T::WeightInfo::kick_candidate(current_count as u32)).into())
677 }
678 }
679
680 impl<T: Config> Pallet<T> {
681 pub fn account_id() -> T::AccountId {
683 T::PotId::get().into_account_truncating()
684 }
685
686 fn ensure_governance_or_caller(
687 origin: T::RuntimeOrigin,
688 who: &T::AccountId,
689 ) -> DispatchResult {
690 if T::GovernanceOrigin::ensure_origin(origin.clone()).is_err() {
691 let caller = ensure_signed(origin.clone())?;
692 ensure!(&caller == who, BadOrigin);
693 }
694 Ok(())
695 }
696
697 fn is_validator_registered(who: &T::AccountId) -> DispatchResult {
699 let validator_key = T::ValidatorIdOf::convert(who.clone())
700 .ok_or(Error::<T>::NoAssociatedValidatorId)?;
701 ensure!(
702 T::ValidatorRegistration::is_registered(&validator_key),
703 Error::<T>::ValidatorNotRegistered
704 );
705
706 Ok(())
707 }
708
709 fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {
711 let current_count =
712 <Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {
713 let index = candidates
714 .iter()
715 .position(|candidate| candidate.who == *who)
716 .ok_or(Error::<T>::NotCandidate)?;
717
718 let candidate = candidates.remove(index);
719 let session_index = T::ValidatorSet::session_index().saturating_add(1);
721 <NonCandidates<T>>::insert(who, (session_index, candidate.deposit));
722 Ok(candidates.len())
723 })?;
724 Self::deposit_event(Event::CandidateRemoved(who.clone()));
725 Ok(current_count)
726 }
727
728 fn slash_non_candidate(who: &T::AccountId) {
733 NonCandidates::<T>::mutate_exists(who, |maybe| {
734 if let Some((_index, deposit)) = maybe.take() {
735 let slash = T::SlashRatio::get() * deposit;
736 let remain = deposit.saturating_sub(slash);
737
738 let (imbalance, _) = T::Currency::slash_reserved(who, slash);
739 T::Currency::unreserve(who, remain);
740
741 if let Some(dest) = SlashDestination::<T>::get() {
742 T::Currency::resolve_creating(&dest, imbalance);
743 }
744
745 <LastAuthoredBlock<T>>::remove(who);
746
747 Self::deposit_event(Event::CandidateSlashed(who.clone()));
748 }
749 });
750 }
751
752 pub fn assemble_collators(candidates: Vec<T::AccountId>) -> Vec<T::AccountId> {
756 let mut collators = Invulnerables::<T>::get();
757 collators.extend(candidates.into_iter());
758 collators
759 }
760 pub fn kick_stale_candidates() -> (u32, u32) {
763 let now = frame_system::Pallet::<T>::block_number();
764 let kick_threshold = T::KickThreshold::get();
765 let count = Candidates::<T>::get().len() as u32;
766 for (who, last_authored) in LastAuthoredBlock::<T>::iter() {
767 if now.saturating_sub(last_authored) < kick_threshold {
768 continue;
769 }
770 if Self::is_account_candidate(&who) {
772 if Candidates::<T>::get().len() > T::MinCandidates::get() as usize {
773 let _ = Self::try_remove_candidate(&who);
775 Self::slash_non_candidate(&who);
776 }
777 } else if let Some((locked_until, _)) = NonCandidates::<T>::get(&who) {
778 if T::ValidatorSet::session_index() > locked_until {
779 <LastAuthoredBlock<T>>::remove(who);
781 } else {
782 Self::slash_non_candidate(&who);
784 }
785 }
786 }
787 (
788 count,
789 count.saturating_sub(Candidates::<T>::get().len() as u32),
790 )
791 }
792
793 pub fn is_account_candidate(account: &T::AccountId) -> bool {
795 Candidates::<T>::get().iter().any(|c| &c.who == account)
796 }
797 }
798
799 impl<T: Config + pallet_authorship::Config>
802 pallet_authorship::EventHandler<T::AccountId, BlockNumberFor<T>> for Pallet<T>
803 {
804 fn note_author(author: T::AccountId) {
805 let pot = Self::account_id();
806 let reward = T::Currency::free_balance(&pot)
808 .checked_sub(&T::Currency::minimum_balance())
809 .unwrap_or_else(Zero::zero)
810 .div(2u32.into());
811 let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);
813 debug_assert!(_success.is_ok());
814 <LastAuthoredBlock<T>>::insert(author, frame_system::Pallet::<T>::block_number());
815
816 frame_system::Pallet::<T>::register_extra_weight_unchecked(
817 T::WeightInfo::note_author(),
818 DispatchClass::Mandatory,
819 );
820 }
821 }
822
823 impl<T: Config> SessionManager<T::AccountId> for Pallet<T> {
825 fn new_session(index: SessionIndex) -> Option<Vec<T::AccountId>> {
826 log::info!(
827 "assembling new collators for new session {} at #{:?}",
828 index,
829 <frame_system::Pallet<T>>::block_number(),
830 );
831
832 let (candidates_len_before, removed) = Self::kick_stale_candidates();
833 frame_system::Pallet::<T>::register_extra_weight_unchecked(
834 T::WeightInfo::new_session(candidates_len_before, removed),
835 DispatchClass::Mandatory,
836 );
837
838 let active_candidates = Candidates::<T>::get()
839 .into_iter()
840 .map(|x| x.who)
841 .collect::<Vec<_>>();
842
843 Some(Self::assemble_collators(active_candidates))
844 }
845 fn start_session(_: SessionIndex) {
846 }
848 fn end_session(_: SessionIndex) {
849 }
851 }
852}