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, DebugNoBound, EqNoBound, PartialEqNoBound,
55};
56use scale_info::TypeInfo;
57use sp_arithmetic::traits::Saturating;
58use sp_runtime::{
59 traits::{Convert, StaticLookup},
60 ArithmeticError::Overflow,
61 Debug, DispatchError, Perbill,
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 DebugNoBound,
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 let (ayes, nays) = (u64::from(self.ayes), u64::from(self.nays));
137 Perbill::from_rational(ayes, 1.max(ayes + nays))
138 }
139 #[cfg(feature = "runtime-benchmarks")]
140 fn unanimity(class: ClassOf<T, I>) -> Self {
141 Self {
142 bare_ayes: M::get_max_voters(class.clone()),
143 ayes: M::get_max_voters(class),
144 nays: 0,
145 dummy: PhantomData,
146 }
147 }
148 #[cfg(feature = "runtime-benchmarks")]
149 fn rejection(class: ClassOf<T, I>) -> Self {
150 Self { bare_ayes: 0, ayes: 0, nays: M::get_max_voters(class), dummy: PhantomData }
151 }
152 #[cfg(feature = "runtime-benchmarks")]
153 fn from_requirements(support: Perbill, approval: Perbill, class: ClassOf<T, I>) -> Self {
154 let c = M::get_max_voters(class);
155 let ayes = support * c;
156 let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes;
157 Self { bare_ayes: ayes, ayes, nays, dummy: PhantomData }
158 }
159
160 #[cfg(feature = "runtime-benchmarks")]
161 fn setup(class: ClassOf<T, I>, granularity: Perbill) {
162 if M::get_max_voters(class.clone()) == 0 {
163 let max_voters = granularity.saturating_reciprocal_mul(1u32);
164 for i in 0..max_voters {
165 let who: T::AccountId =
166 frame_benchmarking::account("ranked_collective_benchmarking", i, 0);
167 crate::Pallet::<T, I>::do_add_member_to_rank(
168 who,
169 T::MinRankOfClass::convert(class.clone()),
170 true,
171 )
172 .expect("could not add members for benchmarks");
173 }
174 assert_eq!(M::get_max_voters(class), max_voters);
175 }
176 }
177}
178
179#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
181pub struct MemberRecord {
182 rank: Rank,
184}
185
186impl MemberRecord {
187 pub fn new(rank: Rank) -> Self {
189 Self { rank }
190 }
191}
192
193#[derive(
195 PartialEq,
196 Eq,
197 Clone,
198 Copy,
199 Encode,
200 Decode,
201 DecodeWithMemTracking,
202 Debug,
203 TypeInfo,
204 MaxEncodedLen,
205)]
206pub enum VoteRecord {
207 Aye(Votes),
209 Nay(Votes),
211}
212
213impl From<(bool, Votes)> for VoteRecord {
214 fn from((aye, votes): (bool, Votes)) -> Self {
215 match aye {
216 true => VoteRecord::Aye(votes),
217 false => VoteRecord::Nay(votes),
218 }
219 }
220}
221
222pub struct Unit;
224impl Convert<Rank, Votes> for Unit {
225 fn convert(_: Rank) -> Votes {
226 1
227 }
228}
229
230pub struct Linear;
239impl Convert<Rank, Votes> for Linear {
240 fn convert(r: Rank) -> Votes {
241 Votes::from(r) + 1
243 }
244}
245
246pub struct Geometric;
255impl Convert<Rank, Votes> for Geometric {
256 fn convert(r: Rank) -> Votes {
257 let v = u64::from(r) + 1;
261 (v * (v + 1) / 2) as Votes
262 }
263}
264
265pub trait GetMaxVoters {
267 type Class;
269 fn get_max_voters(c: Self::Class) -> MemberIndex;
271}
272impl<T: Config<I>, I: 'static> GetMaxVoters for Pallet<T, I> {
273 type Class = ClassOf<T, I>;
274 fn get_max_voters(c: Self::Class) -> MemberIndex {
275 MemberCount::<T, I>::get(T::MinRankOfClass::convert(c))
276 }
277}
278
279pub struct EnsureRanked<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
282impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
283 for EnsureRanked<T, I, MIN_RANK>
284{
285 type Success = Rank;
286
287 fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
288 match o.as_signer().and_then(|who| Members::<T, I>::get(who)) {
289 Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(rank),
290 _ => Err(o),
291 }
292 }
293
294 #[cfg(feature = "runtime-benchmarks")]
295 fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
296 <EnsureRankedMember<T, I, MIN_RANK> as EnsureOrigin<_>>::try_successful_origin()
297 }
298}
299
300impl_ensure_origin_with_arg_ignoring_arg! {
301 impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
302 EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureRanked<T, I, MIN_RANK>
303 {}
304}
305
306pub struct EnsureOfRank<T, I>(PhantomData<(T, I)>);
309impl<T: Config<I>, I: 'static> EnsureOriginWithArg<T::RuntimeOrigin, Rank> for EnsureOfRank<T, I> {
310 type Success = (T::AccountId, Rank);
311
312 fn try_origin(o: T::RuntimeOrigin, min_rank: &Rank) -> Result<Self::Success, T::RuntimeOrigin> {
313 let Some(who) = o.as_signer() else {
314 return Err(o);
315 };
316 match Members::<T, I>::get(who) {
317 Some(MemberRecord { rank, .. }) if rank >= *min_rank => Ok((who.clone(), rank)),
318 _ => Err(o),
319 }
320 }
321
322 #[cfg(feature = "runtime-benchmarks")]
323 fn try_successful_origin(min_rank: &Rank) -> Result<T::RuntimeOrigin, ()> {
324 let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
325 crate::Pallet::<T, I>::do_add_member_to_rank(who.clone(), *min_rank, true)
326 .expect("Could not add members for benchmarks");
327 Ok(frame_system::RawOrigin::Signed(who).into())
328 }
329}
330
331pub struct EnsureMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
334impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
335 for EnsureMember<T, I, MIN_RANK>
336{
337 type Success = T::AccountId;
338
339 fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
340 let Some(who) = o.as_signer() else {
341 return Err(o);
342 };
343 match Members::<T, I>::get(who) {
344 Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(who.clone()),
345 _ => Err(o),
346 }
347 }
348
349 #[cfg(feature = "runtime-benchmarks")]
350 fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
351 <EnsureRankedMember<T, I, MIN_RANK> as EnsureOrigin<_>>::try_successful_origin()
352 }
353}
354
355impl_ensure_origin_with_arg_ignoring_arg! {
356 impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
357 EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureMember<T, I, MIN_RANK>
358 {}
359}
360
361pub struct EnsureRankedMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
364impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
365 for EnsureRankedMember<T, I, MIN_RANK>
366{
367 type Success = (T::AccountId, Rank);
368
369 fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
370 let Some(who) = o.as_signer() else {
371 return Err(o);
372 };
373 match Members::<T, I>::get(who) {
374 Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok((who.clone(), rank)),
375 _ => Err(o),
376 }
377 }
378
379 #[cfg(feature = "runtime-benchmarks")]
380 fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
381 let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
382 crate::Pallet::<T, I>::do_add_member_to_rank(who.clone(), MIN_RANK, true)
383 .expect("Could not add members for benchmarks");
384 Ok(frame_system::RawOrigin::Signed(who).into())
385 }
386}
387
388impl_ensure_origin_with_arg_ignoring_arg! {
389 impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
390 EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureRankedMember<T, I, MIN_RANK>
391 {}
392}
393
394#[impl_trait_for_tuples::impl_for_tuples(8)]
396pub trait BenchmarkSetup<AccountId> {
397 fn ensure_member(acc: &AccountId);
399}
400
401#[frame_support::pallet]
402pub mod pallet {
403 use super::*;
404 use frame_support::{pallet_prelude::*, storage::KeyLenOf};
405 use frame_system::pallet_prelude::*;
406 use sp_runtime::traits::MaybeConvert;
407
408 #[pallet::pallet]
409 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
410
411 #[pallet::config]
412 pub trait Config<I: 'static = ()>: frame_system::Config {
413 type WeightInfo: WeightInfo;
415
416 #[allow(deprecated)]
418 type RuntimeEvent: From<Event<Self, I>>
419 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
420
421 type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>;
423
424 type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
428
429 type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
432
433 type DemoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
436
437 type ExchangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
439
440 type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = BlockNumberFor<Self>>;
442
443 type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>;
447
448 type MemberSwappedHandler: RankedMembersSwapHandler<
450 <Pallet<Self, I> as RankedMembers>::AccountId,
451 <Pallet<Self, I> as RankedMembers>::Rank,
452 >;
453
454 type VoteWeight: Convert<Rank, Votes>;
459
460 type MaxMemberCount: MaybeConvert<Rank, MemberIndex>;
467
468 #[cfg(feature = "runtime-benchmarks")]
470 type BenchmarkSetup: BenchmarkSetup<Self::AccountId>;
471 }
472
473 #[pallet::storage]
476 pub type MemberCount<T: Config<I>, I: 'static = ()> =
477 StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>;
478
479 #[pallet::storage]
481 pub type Members<T: Config<I>, I: 'static = ()> =
482 StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>;
483
484 #[pallet::storage]
486 pub type IdToIndex<T: Config<I>, I: 'static = ()> =
487 StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>;
488
489 #[pallet::storage]
492 pub type IndexToId<T: Config<I>, I: 'static = ()> =
493 StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>;
494
495 #[pallet::storage]
497 pub type Voting<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
498 _,
499 Blake2_128Concat,
500 PollIndexOf<T, I>,
501 Twox64Concat,
502 T::AccountId,
503 VoteRecord,
504 >;
505
506 #[pallet::storage]
507 pub type VotingCleanup<T: Config<I>, I: 'static = ()> =
508 StorageMap<_, Blake2_128Concat, PollIndexOf<T, I>, BoundedVec<u8, KeyLenOf<Voting<T, I>>>>;
509
510 #[pallet::event]
511 #[pallet::generate_deposit(pub(super) fn deposit_event)]
512 pub enum Event<T: Config<I>, I: 'static = ()> {
513 MemberAdded { who: T::AccountId },
515 RankChanged { who: T::AccountId, rank: Rank },
517 MemberRemoved { who: T::AccountId, rank: Rank },
519 Voted { who: T::AccountId, poll: PollIndexOf<T, I>, vote: VoteRecord, tally: TallyOf<T, I> },
522 MemberExchanged { who: T::AccountId, new_who: T::AccountId },
524 }
525
526 #[pallet::error]
527 pub enum Error<T, I = ()> {
528 AlreadyMember,
530 NotMember,
532 NotPolling,
534 Ongoing,
536 NoneRemaining,
538 Corruption,
540 RankTooLow,
542 InvalidWitness,
544 NoPermission,
546 SameMember,
548 TooManyMembers,
550 }
551
552 #[pallet::call]
553 impl<T: Config<I>, I: 'static> Pallet<T, I> {
554 #[pallet::call_index(0)]
561 #[pallet::weight(T::WeightInfo::add_member())]
562 pub fn add_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
563 T::AddOrigin::ensure_origin(origin)?;
564 let who = T::Lookup::lookup(who)?;
565 Self::do_add_member(who, true)
566 }
567
568 #[pallet::call_index(1)]
575 #[pallet::weight(T::WeightInfo::promote_member(0))]
576 pub fn promote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
577 let max_rank = T::PromoteOrigin::ensure_origin(origin)?;
578 let who = T::Lookup::lookup(who)?;
579 Self::do_promote_member(who, Some(max_rank), true)
580 }
581
582 #[pallet::call_index(2)]
590 #[pallet::weight(T::WeightInfo::demote_member(0))]
591 pub fn demote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
592 let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
593 let who = T::Lookup::lookup(who)?;
594 Self::do_demote_member(who, Some(max_rank))
595 }
596
597 #[pallet::call_index(3)]
605 #[pallet::weight(T::WeightInfo::remove_member(*min_rank as u32))]
606 pub fn remove_member(
607 origin: OriginFor<T>,
608 who: AccountIdLookupOf<T>,
609 min_rank: Rank,
610 ) -> DispatchResultWithPostInfo {
611 let max_rank = T::RemoveOrigin::ensure_origin(origin)?;
612 let who = T::Lookup::lookup(who)?;
613 let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
614 ensure!(min_rank >= rank, Error::<T, I>::InvalidWitness);
615 ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
616
617 Self::do_remove_member_from_rank(&who, rank)?;
618 Self::deposit_event(Event::MemberRemoved { who, rank });
619 Ok(PostDispatchInfo {
620 actual_weight: Some(T::WeightInfo::remove_member(rank as u32)),
621 pays_fee: Pays::Yes,
622 })
623 }
624
625 #[pallet::call_index(4)]
637 #[pallet::weight(T::WeightInfo::vote())]
638 pub fn vote(
639 origin: OriginFor<T>,
640 poll: PollIndexOf<T, I>,
641 aye: bool,
642 ) -> DispatchResultWithPostInfo {
643 let who = ensure_signed(origin)?;
644 let record = Self::ensure_member(&who)?;
645 use VoteRecord::*;
646 let mut pays = Pays::Yes;
647
648 let (tally, vote) = T::Polls::try_access_poll(
649 poll,
650 |mut status| -> Result<(TallyOf<T, I>, VoteRecord), DispatchError> {
651 match status {
652 PollStatus::None | PollStatus::Completed(..) => {
653 Err(Error::<T, I>::NotPolling)?
654 },
655 PollStatus::Ongoing(ref mut tally, class) => {
656 match Voting::<T, I>::get(&poll, &who) {
657 Some(Aye(votes)) => {
658 tally.bare_ayes.saturating_dec();
659 tally.ayes.saturating_reduce(votes);
660 },
661 Some(Nay(votes)) => tally.nays.saturating_reduce(votes),
662 None => pays = Pays::No,
663 }
664 let min_rank = T::MinRankOfClass::convert(class);
665 let votes = Self::rank_to_votes(record.rank, min_rank)?;
666 let vote = VoteRecord::from((aye, votes));
667 match aye {
668 true => {
669 tally.bare_ayes.saturating_inc();
670 tally.ayes.saturating_accrue(votes);
671 },
672 false => tally.nays.saturating_accrue(votes),
673 }
674 Voting::<T, I>::insert(&poll, &who, &vote);
675 Ok((tally.clone(), vote))
676 },
677 }
678 },
679 )?;
680 Self::deposit_event(Event::Voted { who, poll, vote, tally });
681 Ok(pays.into())
682 }
683
684 #[pallet::call_index(5)]
695 #[pallet::weight(T::WeightInfo::cleanup_poll(*max))]
696 pub fn cleanup_poll(
697 origin: OriginFor<T>,
698 poll_index: PollIndexOf<T, I>,
699 max: u32,
700 ) -> DispatchResultWithPostInfo {
701 ensure_signed(origin)?;
702 ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::<T, I>::Ongoing);
703
704 let r = Voting::<T, I>::clear_prefix(
705 poll_index,
706 max,
707 VotingCleanup::<T, I>::take(poll_index).as_ref().map(|c| &c[..]),
708 );
709 if r.unique == 0 {
710 return Ok(Pays::Yes.into());
712 }
713 if let Some(cursor) = r.maybe_cursor {
714 VotingCleanup::<T, I>::insert(poll_index, BoundedVec::truncate_from(cursor));
715 }
716 Ok(PostDispatchInfo {
717 actual_weight: Some(T::WeightInfo::cleanup_poll(r.unique)),
718 pays_fee: Pays::No,
719 })
720 }
721
722 #[pallet::call_index(6)]
728 #[pallet::weight(T::WeightInfo::exchange_member())]
729 pub fn exchange_member(
730 origin: OriginFor<T>,
731 who: AccountIdLookupOf<T>,
732 new_who: AccountIdLookupOf<T>,
733 ) -> DispatchResult {
734 T::ExchangeOrigin::ensure_origin(origin)?;
735 let who = T::Lookup::lookup(who)?;
736 let new_who = T::Lookup::lookup(new_who)?;
737
738 ensure!(who != new_who, Error::<T, I>::SameMember);
739
740 let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
741
742 Self::do_remove_member_from_rank(&who, rank)?;
743 Self::do_add_member_to_rank(new_who.clone(), rank, false)?;
744
745 Self::deposit_event(Event::MemberExchanged {
746 who: who.clone(),
747 new_who: new_who.clone(),
748 });
749 T::MemberSwappedHandler::swapped(&who, &new_who, rank);
750
751 Ok(())
752 }
753 }
754
755 #[pallet::hooks]
756 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
757 #[cfg(feature = "try-runtime")]
758 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
759 Self::do_try_state()
760 }
761 }
762
763 impl<T: Config<I>, I: 'static> Pallet<T, I> {
764 fn ensure_member(who: &T::AccountId) -> Result<MemberRecord, DispatchError> {
765 Members::<T, I>::get(who).ok_or(Error::<T, I>::NotMember.into())
766 }
767
768 fn rank_to_votes(rank: Rank, min: Rank) -> Result<Votes, DispatchError> {
769 let excess = rank.checked_sub(min).ok_or(Error::<T, I>::RankTooLow)?;
770 Ok(T::VoteWeight::convert(excess))
771 }
772
773 fn remove_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult {
774 MemberCount::<T, I>::try_mutate(rank, |last_index| {
775 last_index.saturating_dec();
776 let index = IdToIndex::<T, I>::get(rank, &who).ok_or(Error::<T, I>::Corruption)?;
777 if index != *last_index {
778 let last = IndexToId::<T, I>::get(rank, *last_index)
779 .ok_or(Error::<T, I>::Corruption)?;
780 IdToIndex::<T, I>::insert(rank, &last, index);
781 IndexToId::<T, I>::insert(rank, index, &last);
782 }
783
784 IdToIndex::<T, I>::remove(rank, who);
785 IndexToId::<T, I>::remove(rank, last_index);
786
787 Ok(())
788 })
789 }
790
791 pub fn do_add_member(who: T::AccountId, emit_event: bool) -> DispatchResult {
795 ensure!(!Members::<T, I>::contains_key(&who), Error::<T, I>::AlreadyMember);
796 let index = MemberCount::<T, I>::get(0);
797 let count = index.checked_add(1).ok_or(Overflow)?;
798 if let Some(max) = T::MaxMemberCount::maybe_convert(0) {
799 ensure!(count <= max, Error::<T, I>::TooManyMembers);
800 }
801
802 Members::<T, I>::insert(&who, MemberRecord { rank: 0 });
803 IdToIndex::<T, I>::insert(0, &who, index);
804 IndexToId::<T, I>::insert(0, index, &who);
805 MemberCount::<T, I>::insert(0, count);
806 if emit_event {
807 Self::deposit_event(Event::MemberAdded { who });
808 }
809 Ok(())
810 }
811
812 pub fn do_promote_member(
817 who: T::AccountId,
818 maybe_max_rank: Option<Rank>,
819 emit_event: bool,
820 ) -> DispatchResult {
821 let record = Self::ensure_member(&who)?;
822 let rank = record.rank.checked_add(1).ok_or(Overflow)?;
823 if let Some(max_rank) = maybe_max_rank {
824 ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
825 }
826 let index = MemberCount::<T, I>::get(rank);
827 let count = index.checked_add(1).ok_or(Overflow)?;
828 if let Some(max) = T::MaxMemberCount::maybe_convert(rank) {
829 ensure!(count <= max, Error::<T, I>::TooManyMembers);
830 }
831
832 MemberCount::<T, I>::insert(rank, index.checked_add(1).ok_or(Overflow)?);
833 IdToIndex::<T, I>::insert(rank, &who, index);
834 IndexToId::<T, I>::insert(rank, index, &who);
835 Members::<T, I>::insert(&who, MemberRecord { rank });
836 if emit_event {
837 Self::deposit_event(Event::RankChanged { who, rank });
838 }
839 Ok(())
840 }
841
842 fn do_demote_member(who: T::AccountId, maybe_max_rank: Option<Rank>) -> DispatchResult {
847 let mut record = Self::ensure_member(&who)?;
848 let rank = record.rank;
849 if let Some(max_rank) = maybe_max_rank {
850 ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
851 }
852
853 Self::remove_from_rank(&who, rank)?;
854 let maybe_rank = rank.checked_sub(1);
855 match maybe_rank {
856 None => {
857 Members::<T, I>::remove(&who);
858 Self::deposit_event(Event::MemberRemoved { who, rank: 0 });
859 },
860 Some(rank) => {
861 record.rank = rank;
862 Members::<T, I>::insert(&who, &record);
863 Self::deposit_event(Event::RankChanged { who, rank });
864 },
865 }
866 Ok(())
867 }
868
869 pub fn do_add_member_to_rank(
872 who: T::AccountId,
873 rank: Rank,
874 emit_event: bool,
875 ) -> DispatchResult {
876 Self::do_add_member(who.clone(), emit_event)?;
877 for _ in 0..rank {
878 Self::do_promote_member(who.clone(), None, emit_event)?;
879 }
880 Ok(())
881 }
882
883 pub fn as_rank(
886 o: &<T::RuntimeOrigin as frame_support::traits::OriginTrait>::PalletsOrigin,
887 ) -> Option<u16> {
888 use frame_support::traits::CallerTrait;
889 o.as_signed().and_then(Self::rank_of)
890 }
891
892 pub fn do_remove_member_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult {
894 for r in 0..=rank {
895 Self::remove_from_rank(&who, r)?;
896 }
897 Members::<T, I>::remove(&who);
898 Ok(())
899 }
900 }
901
902 #[cfg(any(feature = "try-runtime", test))]
903 impl<T: Config<I>, I: 'static> Pallet<T, I> {
904 pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
906 Self::try_state_members()?;
907 Self::try_state_index()?;
908
909 Ok(())
910 }
911
912 fn try_state_members() -> Result<(), sp_runtime::TryRuntimeError> {
920 MemberCount::<T, I>::iter().try_for_each(|(_, member_index)| -> DispatchResult {
921 let total_members = Members::<T, I>::iter().count();
922 ensure!(
923 total_members as u32 >= member_index,
924 "Total count of `Members` should be greater than or equal to the number of `MemberIndex` of a particular `Rank` in `MemberCount`."
925 );
926
927 Ok(())
928 })?;
929
930 let mut sum_of_member_rank_indexes = 0;
931 Members::<T, I>::iter().try_for_each(|(_, member_record)| -> DispatchResult {
932 ensure!(
933 Self::is_rank_in_member_count(member_record.rank.into()),
934 "`Rank` in Members should be in `MemberCount`"
935 );
936
937 sum_of_member_rank_indexes += Self::determine_index_of_a_rank(member_record.rank);
938
939 Ok(())
940 })?;
941
942 let sum_of_all_member_count_indexes =
943 MemberCount::<T, I>::iter_values().fold(0, |sum, index| sum + index);
944 ensure!(
945 sum_of_all_member_count_indexes == sum_of_member_rank_indexes as u32,
946 "Sum of `MemberCount` index should be the same as the sum of all the index attained for rank possessed by `Members`"
947 );
948 Ok(())
949 }
950
951 fn try_state_index() -> Result<(), sp_runtime::TryRuntimeError> {
957 IdToIndex::<T, I>::iter().try_for_each(
958 |(rank, who, member_index)| -> DispatchResult {
959 let who_from_index = IndexToId::<T, I>::get(rank, member_index).unwrap();
960 ensure!(
961 who == who_from_index,
962 "`Member` in storage of `IdToIndex` should be the same as `Member` in `IndexToId`."
963 );
964
965 ensure!(
966 Self::is_rank_in_index_to_id_storage(rank.into()),
967 "`Rank` in `IdToIndex` should be the same as the `Rank` in `IndexToId`"
968 );
969 Ok(())
970 },
971 )?;
972
973 Members::<T, I>::iter().try_for_each(|(who, member_record)| -> DispatchResult {
974 ensure!(
975 Self::is_who_rank_in_id_to_index_storage(who, member_record.rank),
976 "`Rank` of the member `who` in `IdToIndex` should be the same as the `Rank` of the member `who` in `Members`"
977 );
978
979 Ok(())
980 })?;
981
982 Ok(())
983 }
984
985 fn is_rank_in_member_count(rank: u32) -> bool {
987 for (r, _) in MemberCount::<T, I>::iter() {
988 if r as u32 == rank {
989 return true;
990 }
991 }
992
993 return false;
994 }
995
996 fn is_rank_in_index_to_id_storage(rank: u32) -> bool {
998 for (r, _, _) in IndexToId::<T, I>::iter() {
999 if r as u32 == rank {
1000 return true;
1001 }
1002 }
1003
1004 return false;
1005 }
1006
1007 fn is_who_rank_in_id_to_index_storage(who: T::AccountId, rank: u16) -> bool {
1009 for (rank_, who_, _) in IdToIndex::<T, I>::iter() {
1010 if who == who_ && rank == rank_ {
1011 return true;
1012 }
1013 }
1014
1015 return false;
1016 }
1017
1018 fn determine_index_of_a_rank(rank: u16) -> u16 {
1020 let mut sum = 0;
1021 for _ in 0..rank + 1 {
1022 sum += 1;
1023 }
1024 sum
1025 }
1026 }
1027
1028 impl<T: Config<I>, I: 'static> RankedMembers for Pallet<T, I> {
1029 type AccountId = T::AccountId;
1030 type Rank = Rank;
1031
1032 fn min_rank() -> Self::Rank {
1033 0
1034 }
1035
1036 fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
1037 Some(Self::ensure_member(&who).ok()?.rank)
1038 }
1039
1040 fn induct(who: &Self::AccountId) -> DispatchResult {
1041 Self::do_add_member(who.clone(), true)
1042 }
1043
1044 fn promote(who: &Self::AccountId) -> DispatchResult {
1045 Self::do_promote_member(who.clone(), None, true)
1046 }
1047
1048 fn demote(who: &Self::AccountId) -> DispatchResult {
1049 Self::do_demote_member(who.clone(), None)
1050 }
1051 }
1052}