1#![cfg_attr(not(feature = "std"), no_std)]
42
43extern crate alloc;
44
45use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
46use core::marker::PhantomData;
47use frame_support::{
48 dispatch::{DispatchResultWithPostInfo, PostDispatchInfo},
49 ensure, impl_ensure_origin_with_arg_ignoring_arg,
50 traits::{
51 EnsureOrigin, EnsureOriginWithArg, OriginTrait, PollStatus, Polling, RankedMembers,
52 RankedMembersSwapHandler, VoteTally,
53 },
54 CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
55};
56use scale_info::TypeInfo;
57use sp_arithmetic::traits::Saturating;
58use sp_runtime::{
59 traits::{Convert, StaticLookup},
60 ArithmeticError::Overflow,
61 DispatchError, Perbill, RuntimeDebug,
62};
63
64#[cfg(test)]
65mod tests;
66
67#[cfg(feature = "runtime-benchmarks")]
68mod benchmarking;
69pub mod weights;
70
71pub use pallet::*;
72pub use weights::WeightInfo;
73
74pub type MemberIndex = u32;
76
77pub type Rank = u16;
79
80pub type Votes = u32;
82
83#[derive(
85 CloneNoBound,
86 PartialEqNoBound,
87 EqNoBound,
88 RuntimeDebugNoBound,
89 TypeInfo,
90 Encode,
91 Decode,
92 DecodeWithMemTracking,
93 MaxEncodedLen,
94)]
95#[scale_info(skip_type_params(T, I, M))]
96#[codec(mel_bound())]
97pub struct Tally<T, I, M: GetMaxVoters> {
98 bare_ayes: MemberIndex,
99 ayes: Votes,
100 nays: Votes,
101 dummy: PhantomData<(T, I, M)>,
102}
103
104impl<T: Config<I>, I: 'static, M: GetMaxVoters> Tally<T, I, M> {
105 pub fn from_parts(bare_ayes: MemberIndex, ayes: Votes, nays: Votes) -> Self {
106 Tally { bare_ayes, ayes, nays, dummy: PhantomData }
107 }
108}
109
110pub type TallyOf<T, I = ()> = Tally<T, I, Pallet<T, I>>;
118pub type PollIndexOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Index;
119pub type ClassOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Class;
120type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
121
122impl<T: Config<I>, I: 'static, M: GetMaxVoters<Class = ClassOf<T, I>>>
123 VoteTally<Votes, ClassOf<T, I>> for Tally<T, I, M>
124{
125 fn new(_: ClassOf<T, I>) -> Self {
126 Self { bare_ayes: 0, ayes: 0, nays: 0, dummy: PhantomData }
127 }
128 fn ayes(&self, _: ClassOf<T, I>) -> Votes {
129 self.bare_ayes
130 }
131 fn support(&self, class: ClassOf<T, I>) -> Perbill {
132 Perbill::from_rational(self.bare_ayes, M::get_max_voters(class))
133 }
134 fn approval(&self, _: ClassOf<T, I>) -> Perbill {
135 Perbill::from_rational(self.ayes, 1.max(self.ayes + self.nays))
136 }
137 #[cfg(feature = "runtime-benchmarks")]
138 fn unanimity(class: ClassOf<T, I>) -> Self {
139 Self {
140 bare_ayes: M::get_max_voters(class.clone()),
141 ayes: M::get_max_voters(class),
142 nays: 0,
143 dummy: PhantomData,
144 }
145 }
146 #[cfg(feature = "runtime-benchmarks")]
147 fn rejection(class: ClassOf<T, I>) -> Self {
148 Self { bare_ayes: 0, ayes: 0, nays: M::get_max_voters(class), dummy: PhantomData }
149 }
150 #[cfg(feature = "runtime-benchmarks")]
151 fn from_requirements(support: Perbill, approval: Perbill, class: ClassOf<T, I>) -> Self {
152 let c = M::get_max_voters(class);
153 let ayes = support * c;
154 let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes;
155 Self { bare_ayes: ayes, ayes, nays, dummy: PhantomData }
156 }
157
158 #[cfg(feature = "runtime-benchmarks")]
159 fn setup(class: ClassOf<T, I>, granularity: Perbill) {
160 if M::get_max_voters(class.clone()) == 0 {
161 let max_voters = granularity.saturating_reciprocal_mul(1u32);
162 for i in 0..max_voters {
163 let who: T::AccountId =
164 frame_benchmarking::account("ranked_collective_benchmarking", i, 0);
165 crate::Pallet::<T, I>::do_add_member_to_rank(
166 who,
167 T::MinRankOfClass::convert(class.clone()),
168 true,
169 )
170 .expect("could not add members for benchmarks");
171 }
172 assert_eq!(M::get_max_voters(class), max_voters);
173 }
174 }
175}
176
177#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
179pub struct MemberRecord {
180 rank: Rank,
182}
183
184impl MemberRecord {
185 pub fn new(rank: Rank) -> Self {
187 Self { rank }
188 }
189}
190
191#[derive(
193 PartialEq,
194 Eq,
195 Clone,
196 Copy,
197 Encode,
198 Decode,
199 DecodeWithMemTracking,
200 RuntimeDebug,
201 TypeInfo,
202 MaxEncodedLen,
203)]
204pub enum VoteRecord {
205 Aye(Votes),
207 Nay(Votes),
209}
210
211impl From<(bool, Votes)> for VoteRecord {
212 fn from((aye, votes): (bool, Votes)) -> Self {
213 match aye {
214 true => VoteRecord::Aye(votes),
215 false => VoteRecord::Nay(votes),
216 }
217 }
218}
219
220pub struct Unit;
222impl Convert<Rank, Votes> for Unit {
223 fn convert(_: Rank) -> Votes {
224 1
225 }
226}
227
228pub struct Linear;
237impl Convert<Rank, Votes> for Linear {
238 fn convert(r: Rank) -> Votes {
239 (r + 1) as Votes
240 }
241}
242
243pub struct Geometric;
252impl Convert<Rank, Votes> for Geometric {
253 fn convert(r: Rank) -> Votes {
254 let v = (r + 1) as Votes;
255 v * (v + 1) / 2
256 }
257}
258
259pub trait GetMaxVoters {
261 type Class;
263 fn get_max_voters(c: Self::Class) -> MemberIndex;
265}
266impl<T: Config<I>, I: 'static> GetMaxVoters for Pallet<T, I> {
267 type Class = ClassOf<T, I>;
268 fn get_max_voters(c: Self::Class) -> MemberIndex {
269 MemberCount::<T, I>::get(T::MinRankOfClass::convert(c))
270 }
271}
272
273pub struct EnsureRanked<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
276impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
277 for EnsureRanked<T, I, MIN_RANK>
278{
279 type Success = Rank;
280
281 fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
282 match o.as_signer().and_then(|who| Members::<T, I>::get(who)) {
283 Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(rank),
284 _ => Err(o),
285 }
286 }
287
288 #[cfg(feature = "runtime-benchmarks")]
289 fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
290 <EnsureRankedMember<T, I, MIN_RANK> as EnsureOrigin<_>>::try_successful_origin()
291 }
292}
293
294impl_ensure_origin_with_arg_ignoring_arg! {
295 impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
296 EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureRanked<T, I, MIN_RANK>
297 {}
298}
299
300pub struct EnsureOfRank<T, I>(PhantomData<(T, I)>);
303impl<T: Config<I>, I: 'static> EnsureOriginWithArg<T::RuntimeOrigin, Rank> for EnsureOfRank<T, I> {
304 type Success = (T::AccountId, Rank);
305
306 fn try_origin(o: T::RuntimeOrigin, min_rank: &Rank) -> Result<Self::Success, T::RuntimeOrigin> {
307 let Some(who) = o.as_signer() else {
308 return Err(o);
309 };
310 match Members::<T, I>::get(who) {
311 Some(MemberRecord { rank, .. }) if rank >= *min_rank => Ok((who.clone(), rank)),
312 _ => Err(o),
313 }
314 }
315
316 #[cfg(feature = "runtime-benchmarks")]
317 fn try_successful_origin(min_rank: &Rank) -> Result<T::RuntimeOrigin, ()> {
318 let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
319 crate::Pallet::<T, I>::do_add_member_to_rank(who.clone(), *min_rank, true)
320 .expect("Could not add members for benchmarks");
321 Ok(frame_system::RawOrigin::Signed(who).into())
322 }
323}
324
325pub struct EnsureMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
328impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
329 for EnsureMember<T, I, MIN_RANK>
330{
331 type Success = T::AccountId;
332
333 fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
334 let Some(who) = o.as_signer() else {
335 return Err(o);
336 };
337 match Members::<T, I>::get(who) {
338 Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(who.clone()),
339 _ => Err(o),
340 }
341 }
342
343 #[cfg(feature = "runtime-benchmarks")]
344 fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
345 <EnsureRankedMember<T, I, MIN_RANK> as EnsureOrigin<_>>::try_successful_origin()
346 }
347}
348
349impl_ensure_origin_with_arg_ignoring_arg! {
350 impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
351 EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureMember<T, I, MIN_RANK>
352 {}
353}
354
355pub struct EnsureRankedMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
358impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
359 for EnsureRankedMember<T, I, MIN_RANK>
360{
361 type Success = (T::AccountId, Rank);
362
363 fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
364 let Some(who) = o.as_signer() else {
365 return Err(o);
366 };
367 match Members::<T, I>::get(who) {
368 Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok((who.clone(), rank)),
369 _ => Err(o),
370 }
371 }
372
373 #[cfg(feature = "runtime-benchmarks")]
374 fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
375 let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
376 crate::Pallet::<T, I>::do_add_member_to_rank(who.clone(), MIN_RANK, true)
377 .expect("Could not add members for benchmarks");
378 Ok(frame_system::RawOrigin::Signed(who).into())
379 }
380}
381
382impl_ensure_origin_with_arg_ignoring_arg! {
383 impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
384 EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureRankedMember<T, I, MIN_RANK>
385 {}
386}
387
388#[impl_trait_for_tuples::impl_for_tuples(8)]
390pub trait BenchmarkSetup<AccountId> {
391 fn ensure_member(acc: &AccountId);
393}
394
395#[frame_support::pallet]
396pub mod pallet {
397 use super::*;
398 use frame_support::{pallet_prelude::*, storage::KeyLenOf};
399 use frame_system::pallet_prelude::*;
400 use sp_runtime::traits::MaybeConvert;
401
402 #[pallet::pallet]
403 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
404
405 #[pallet::config]
406 pub trait Config<I: 'static = ()>: frame_system::Config {
407 type WeightInfo: WeightInfo;
409
410 #[allow(deprecated)]
412 type RuntimeEvent: From<Event<Self, I>>
413 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
414
415 type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>;
417
418 type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
422
423 type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
426
427 type DemoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
430
431 type ExchangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
433
434 type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = BlockNumberFor<Self>>;
436
437 type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>;
441
442 type MemberSwappedHandler: RankedMembersSwapHandler<
444 <Pallet<Self, I> as RankedMembers>::AccountId,
445 <Pallet<Self, I> as RankedMembers>::Rank,
446 >;
447
448 type VoteWeight: Convert<Rank, Votes>;
453
454 type MaxMemberCount: MaybeConvert<Rank, MemberIndex>;
461
462 #[cfg(feature = "runtime-benchmarks")]
464 type BenchmarkSetup: BenchmarkSetup<Self::AccountId>;
465 }
466
467 #[pallet::storage]
470 pub type MemberCount<T: Config<I>, I: 'static = ()> =
471 StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>;
472
473 #[pallet::storage]
475 pub type Members<T: Config<I>, I: 'static = ()> =
476 StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>;
477
478 #[pallet::storage]
480 pub type IdToIndex<T: Config<I>, I: 'static = ()> =
481 StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>;
482
483 #[pallet::storage]
486 pub type IndexToId<T: Config<I>, I: 'static = ()> =
487 StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>;
488
489 #[pallet::storage]
491 pub type Voting<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
492 _,
493 Blake2_128Concat,
494 PollIndexOf<T, I>,
495 Twox64Concat,
496 T::AccountId,
497 VoteRecord,
498 >;
499
500 #[pallet::storage]
501 pub type VotingCleanup<T: Config<I>, I: 'static = ()> =
502 StorageMap<_, Blake2_128Concat, PollIndexOf<T, I>, BoundedVec<u8, KeyLenOf<Voting<T, I>>>>;
503
504 #[pallet::event]
505 #[pallet::generate_deposit(pub(super) fn deposit_event)]
506 pub enum Event<T: Config<I>, I: 'static = ()> {
507 MemberAdded { who: T::AccountId },
509 RankChanged { who: T::AccountId, rank: Rank },
511 MemberRemoved { who: T::AccountId, rank: Rank },
513 Voted { who: T::AccountId, poll: PollIndexOf<T, I>, vote: VoteRecord, tally: TallyOf<T, I> },
516 MemberExchanged { who: T::AccountId, new_who: T::AccountId },
518 }
519
520 #[pallet::error]
521 pub enum Error<T, I = ()> {
522 AlreadyMember,
524 NotMember,
526 NotPolling,
528 Ongoing,
530 NoneRemaining,
532 Corruption,
534 RankTooLow,
536 InvalidWitness,
538 NoPermission,
540 SameMember,
542 TooManyMembers,
544 }
545
546 #[pallet::call]
547 impl<T: Config<I>, I: 'static> Pallet<T, I> {
548 #[pallet::call_index(0)]
555 #[pallet::weight(T::WeightInfo::add_member())]
556 pub fn add_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
557 T::AddOrigin::ensure_origin(origin)?;
558 let who = T::Lookup::lookup(who)?;
559 Self::do_add_member(who, true)
560 }
561
562 #[pallet::call_index(1)]
569 #[pallet::weight(T::WeightInfo::promote_member(0))]
570 pub fn promote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
571 let max_rank = T::PromoteOrigin::ensure_origin(origin)?;
572 let who = T::Lookup::lookup(who)?;
573 Self::do_promote_member(who, Some(max_rank), true)
574 }
575
576 #[pallet::call_index(2)]
584 #[pallet::weight(T::WeightInfo::demote_member(0))]
585 pub fn demote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
586 let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
587 let who = T::Lookup::lookup(who)?;
588 Self::do_demote_member(who, Some(max_rank))
589 }
590
591 #[pallet::call_index(3)]
599 #[pallet::weight(T::WeightInfo::remove_member(*min_rank as u32))]
600 pub fn remove_member(
601 origin: OriginFor<T>,
602 who: AccountIdLookupOf<T>,
603 min_rank: Rank,
604 ) -> DispatchResultWithPostInfo {
605 let max_rank = T::RemoveOrigin::ensure_origin(origin)?;
606 let who = T::Lookup::lookup(who)?;
607 let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
608 ensure!(min_rank >= rank, Error::<T, I>::InvalidWitness);
609 ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
610
611 Self::do_remove_member_from_rank(&who, rank)?;
612 Self::deposit_event(Event::MemberRemoved { who, rank });
613 Ok(PostDispatchInfo {
614 actual_weight: Some(T::WeightInfo::remove_member(rank as u32)),
615 pays_fee: Pays::Yes,
616 })
617 }
618
619 #[pallet::call_index(4)]
631 #[pallet::weight(T::WeightInfo::vote())]
632 pub fn vote(
633 origin: OriginFor<T>,
634 poll: PollIndexOf<T, I>,
635 aye: bool,
636 ) -> DispatchResultWithPostInfo {
637 let who = ensure_signed(origin)?;
638 let record = Self::ensure_member(&who)?;
639 use VoteRecord::*;
640 let mut pays = Pays::Yes;
641
642 let (tally, vote) = T::Polls::try_access_poll(
643 poll,
644 |mut status| -> Result<(TallyOf<T, I>, VoteRecord), DispatchError> {
645 match status {
646 PollStatus::None | PollStatus::Completed(..) =>
647 Err(Error::<T, I>::NotPolling)?,
648 PollStatus::Ongoing(ref mut tally, class) => {
649 match Voting::<T, I>::get(&poll, &who) {
650 Some(Aye(votes)) => {
651 tally.bare_ayes.saturating_dec();
652 tally.ayes.saturating_reduce(votes);
653 },
654 Some(Nay(votes)) => tally.nays.saturating_reduce(votes),
655 None => pays = Pays::No,
656 }
657 let min_rank = T::MinRankOfClass::convert(class);
658 let votes = Self::rank_to_votes(record.rank, min_rank)?;
659 let vote = VoteRecord::from((aye, votes));
660 match aye {
661 true => {
662 tally.bare_ayes.saturating_inc();
663 tally.ayes.saturating_accrue(votes);
664 },
665 false => tally.nays.saturating_accrue(votes),
666 }
667 Voting::<T, I>::insert(&poll, &who, &vote);
668 Ok((tally.clone(), vote))
669 },
670 }
671 },
672 )?;
673 Self::deposit_event(Event::Voted { who, poll, vote, tally });
674 Ok(pays.into())
675 }
676
677 #[pallet::call_index(5)]
688 #[pallet::weight(T::WeightInfo::cleanup_poll(*max))]
689 pub fn cleanup_poll(
690 origin: OriginFor<T>,
691 poll_index: PollIndexOf<T, I>,
692 max: u32,
693 ) -> DispatchResultWithPostInfo {
694 ensure_signed(origin)?;
695 ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::<T, I>::Ongoing);
696
697 let r = Voting::<T, I>::clear_prefix(
698 poll_index,
699 max,
700 VotingCleanup::<T, I>::take(poll_index).as_ref().map(|c| &c[..]),
701 );
702 if r.unique == 0 {
703 return Ok(Pays::Yes.into())
705 }
706 if let Some(cursor) = r.maybe_cursor {
707 VotingCleanup::<T, I>::insert(poll_index, BoundedVec::truncate_from(cursor));
708 }
709 Ok(PostDispatchInfo {
710 actual_weight: Some(T::WeightInfo::cleanup_poll(r.unique)),
711 pays_fee: Pays::No,
712 })
713 }
714
715 #[pallet::call_index(6)]
721 #[pallet::weight(T::WeightInfo::exchange_member())]
722 pub fn exchange_member(
723 origin: OriginFor<T>,
724 who: AccountIdLookupOf<T>,
725 new_who: AccountIdLookupOf<T>,
726 ) -> DispatchResult {
727 T::ExchangeOrigin::ensure_origin(origin)?;
728 let who = T::Lookup::lookup(who)?;
729 let new_who = T::Lookup::lookup(new_who)?;
730
731 ensure!(who != new_who, Error::<T, I>::SameMember);
732
733 let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
734
735 Self::do_remove_member_from_rank(&who, rank)?;
736 Self::do_add_member_to_rank(new_who.clone(), rank, false)?;
737
738 Self::deposit_event(Event::MemberExchanged {
739 who: who.clone(),
740 new_who: new_who.clone(),
741 });
742 T::MemberSwappedHandler::swapped(&who, &new_who, rank);
743
744 Ok(())
745 }
746 }
747
748 #[pallet::hooks]
749 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
750 #[cfg(feature = "try-runtime")]
751 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
752 Self::do_try_state()
753 }
754 }
755
756 impl<T: Config<I>, I: 'static> Pallet<T, I> {
757 fn ensure_member(who: &T::AccountId) -> Result<MemberRecord, DispatchError> {
758 Members::<T, I>::get(who).ok_or(Error::<T, I>::NotMember.into())
759 }
760
761 fn rank_to_votes(rank: Rank, min: Rank) -> Result<Votes, DispatchError> {
762 let excess = rank.checked_sub(min).ok_or(Error::<T, I>::RankTooLow)?;
763 Ok(T::VoteWeight::convert(excess))
764 }
765
766 fn remove_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult {
767 MemberCount::<T, I>::try_mutate(rank, |last_index| {
768 last_index.saturating_dec();
769 let index = IdToIndex::<T, I>::get(rank, &who).ok_or(Error::<T, I>::Corruption)?;
770 if index != *last_index {
771 let last = IndexToId::<T, I>::get(rank, *last_index)
772 .ok_or(Error::<T, I>::Corruption)?;
773 IdToIndex::<T, I>::insert(rank, &last, index);
774 IndexToId::<T, I>::insert(rank, index, &last);
775 }
776
777 IdToIndex::<T, I>::remove(rank, who);
778 IndexToId::<T, I>::remove(rank, last_index);
779
780 Ok(())
781 })
782 }
783
784 pub fn do_add_member(who: T::AccountId, emit_event: bool) -> DispatchResult {
788 ensure!(!Members::<T, I>::contains_key(&who), Error::<T, I>::AlreadyMember);
789 let index = MemberCount::<T, I>::get(0);
790 let count = index.checked_add(1).ok_or(Overflow)?;
791 if let Some(max) = T::MaxMemberCount::maybe_convert(0) {
792 ensure!(count <= max, Error::<T, I>::TooManyMembers);
793 }
794
795 Members::<T, I>::insert(&who, MemberRecord { rank: 0 });
796 IdToIndex::<T, I>::insert(0, &who, index);
797 IndexToId::<T, I>::insert(0, index, &who);
798 MemberCount::<T, I>::insert(0, count);
799 if emit_event {
800 Self::deposit_event(Event::MemberAdded { who });
801 }
802 Ok(())
803 }
804
805 pub fn do_promote_member(
810 who: T::AccountId,
811 maybe_max_rank: Option<Rank>,
812 emit_event: bool,
813 ) -> DispatchResult {
814 let record = Self::ensure_member(&who)?;
815 let rank = record.rank.checked_add(1).ok_or(Overflow)?;
816 if let Some(max_rank) = maybe_max_rank {
817 ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
818 }
819 let index = MemberCount::<T, I>::get(rank);
820 let count = index.checked_add(1).ok_or(Overflow)?;
821 if let Some(max) = T::MaxMemberCount::maybe_convert(rank) {
822 ensure!(count <= max, Error::<T, I>::TooManyMembers);
823 }
824
825 MemberCount::<T, I>::insert(rank, index.checked_add(1).ok_or(Overflow)?);
826 IdToIndex::<T, I>::insert(rank, &who, index);
827 IndexToId::<T, I>::insert(rank, index, &who);
828 Members::<T, I>::insert(&who, MemberRecord { rank });
829 if emit_event {
830 Self::deposit_event(Event::RankChanged { who, rank });
831 }
832 Ok(())
833 }
834
835 fn do_demote_member(who: T::AccountId, maybe_max_rank: Option<Rank>) -> DispatchResult {
840 let mut record = Self::ensure_member(&who)?;
841 let rank = record.rank;
842 if let Some(max_rank) = maybe_max_rank {
843 ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
844 }
845
846 Self::remove_from_rank(&who, rank)?;
847 let maybe_rank = rank.checked_sub(1);
848 match maybe_rank {
849 None => {
850 Members::<T, I>::remove(&who);
851 Self::deposit_event(Event::MemberRemoved { who, rank: 0 });
852 },
853 Some(rank) => {
854 record.rank = rank;
855 Members::<T, I>::insert(&who, &record);
856 Self::deposit_event(Event::RankChanged { who, rank });
857 },
858 }
859 Ok(())
860 }
861
862 pub fn do_add_member_to_rank(
865 who: T::AccountId,
866 rank: Rank,
867 emit_event: bool,
868 ) -> DispatchResult {
869 Self::do_add_member(who.clone(), emit_event)?;
870 for _ in 0..rank {
871 Self::do_promote_member(who.clone(), None, emit_event)?;
872 }
873 Ok(())
874 }
875
876 pub fn as_rank(
879 o: &<T::RuntimeOrigin as frame_support::traits::OriginTrait>::PalletsOrigin,
880 ) -> Option<u16> {
881 use frame_support::traits::CallerTrait;
882 o.as_signed().and_then(Self::rank_of)
883 }
884
885 pub fn do_remove_member_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult {
887 for r in 0..=rank {
888 Self::remove_from_rank(&who, r)?;
889 }
890 Members::<T, I>::remove(&who);
891 Ok(())
892 }
893 }
894
895 #[cfg(any(feature = "try-runtime", test))]
896 impl<T: Config<I>, I: 'static> Pallet<T, I> {
897 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
899 Self::try_state_members()?;
900 Self::try_state_index()?;
901
902 Ok(())
903 }
904
905 fn try_state_members() -> Result<(), sp_runtime::TryRuntimeError> {
913 MemberCount::<T, I>::iter().try_for_each(|(_, member_index)| -> DispatchResult {
914 let total_members = Members::<T, I>::iter().count();
915 ensure!(
916 total_members as u32 >= member_index,
917 "Total count of `Members` should be greater than or equal to the number of `MemberIndex` of a particular `Rank` in `MemberCount`."
918 );
919
920 Ok(())
921 })?;
922
923 let mut sum_of_member_rank_indexes = 0;
924 Members::<T, I>::iter().try_for_each(|(_, member_record)| -> DispatchResult {
925 ensure!(
926 Self::is_rank_in_member_count(member_record.rank.into()),
927 "`Rank` in Members should be in `MemberCount`"
928 );
929
930 sum_of_member_rank_indexes += Self::determine_index_of_a_rank(member_record.rank);
931
932 Ok(())
933 })?;
934
935 let sum_of_all_member_count_indexes =
936 MemberCount::<T, I>::iter_values().fold(0, |sum, index| sum + index);
937 ensure!(
938 sum_of_all_member_count_indexes == sum_of_member_rank_indexes as u32,
939 "Sum of `MemberCount` index should be the same as the sum of all the index attained for rank possessed by `Members`"
940 );
941 Ok(())
942 }
943
944 fn try_state_index() -> Result<(), sp_runtime::TryRuntimeError> {
950 IdToIndex::<T, I>::iter().try_for_each(
951 |(rank, who, member_index)| -> DispatchResult {
952 let who_from_index = IndexToId::<T, I>::get(rank, member_index).unwrap();
953 ensure!(
954 who == who_from_index,
955 "`Member` in storage of `IdToIndex` should be the same as `Member` in `IndexToId`."
956 );
957
958 ensure!(
959 Self::is_rank_in_index_to_id_storage(rank.into()),
960 "`Rank` in `IdToIndex` should be the same as the `Rank` in `IndexToId`"
961 );
962 Ok(())
963 },
964 )?;
965
966 Members::<T, I>::iter().try_for_each(|(who, member_record)| -> DispatchResult {
967 ensure!(
968 Self::is_who_rank_in_id_to_index_storage(who, member_record.rank),
969 "`Rank` of the member `who` in `IdToIndex` should be the same as the `Rank` of the member `who` in `Members`"
970 );
971
972 Ok(())
973 })?;
974
975 Ok(())
976 }
977
978 fn is_rank_in_member_count(rank: u32) -> bool {
980 for (r, _) in MemberCount::<T, I>::iter() {
981 if r as u32 == rank {
982 return true;
983 }
984 }
985
986 return false;
987 }
988
989 fn is_rank_in_index_to_id_storage(rank: u32) -> bool {
991 for (r, _, _) in IndexToId::<T, I>::iter() {
992 if r as u32 == rank {
993 return true;
994 }
995 }
996
997 return false;
998 }
999
1000 fn is_who_rank_in_id_to_index_storage(who: T::AccountId, rank: u16) -> bool {
1002 for (rank_, who_, _) in IdToIndex::<T, I>::iter() {
1003 if who == who_ && rank == rank_ {
1004 return true;
1005 }
1006 }
1007
1008 return false;
1009 }
1010
1011 fn determine_index_of_a_rank(rank: u16) -> u16 {
1013 let mut sum = 0;
1014 for _ in 0..rank + 1 {
1015 sum += 1;
1016 }
1017 sum
1018 }
1019 }
1020
1021 impl<T: Config<I>, I: 'static> RankedMembers for Pallet<T, I> {
1022 type AccountId = T::AccountId;
1023 type Rank = Rank;
1024
1025 fn min_rank() -> Self::Rank {
1026 0
1027 }
1028
1029 fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
1030 Some(Self::ensure_member(&who).ok()?.rank)
1031 }
1032
1033 fn induct(who: &Self::AccountId) -> DispatchResult {
1034 Self::do_add_member(who.clone(), true)
1035 }
1036
1037 fn promote(who: &Self::AccountId) -> DispatchResult {
1038 Self::do_promote_member(who.clone(), None, true)
1039 }
1040
1041 fn demote(who: &Self::AccountId) -> DispatchResult {
1042 Self::do_demote_member(who.clone(), None)
1043 }
1044 }
1045}