referrerpolicy=no-referrer-when-downgrade

pallet_ranked_collective/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Ranked collective system.
19//!
20//! This is a membership pallet providing a `Tally` implementation ready for use with polling
21//! systems such as the Referenda pallet. Members each have a rank, with zero being the lowest.
22//! There is no complexity limitation on either the number of members at a rank or the number of
23//! ranks in the system thus allowing potentially public membership. A member of at least a given
24//! rank can be selected at random in O(1) time, allowing for various games to be constructed using
25//! this as a primitive. Members may only be promoted and demoted by one rank at a time, however
26//! all operations (save one) are O(1) in complexity. The only operation which is not O(1) is the
27//! `remove_member` since they must be removed from all ranks from the present down to zero.
28//!
29//! Different ranks have different voting power, and are able to vote in different polls. In general
30//! rank privileges are cumulative. Higher ranks are able to vote in any polls open to lower ranks.
31//! Similarly, higher ranks always have at least as much voting power in any given poll as lower
32//! ranks.
33//!
34//! Two `Config` trait items control these "rank privileges": `MinRankOfClass` and `VoteWeight`.
35//! The first controls which ranks are allowed to vote on a particular class of poll. The second
36//! controls the weight of a vote given the voter's rank compared to the minimum rank of the poll.
37//!
38//! An origin control, `EnsureRank`, ensures that the origin is a member of the collective of at
39//! least a particular rank.
40
41#![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
74/// A number of members.
75pub type MemberIndex = u32;
76
77/// Member rank.
78pub type Rank = u16;
79
80/// Votes.
81pub type Votes = u32;
82
83/// Aggregated votes for an ongoing poll by members of the ranked collective.
84#[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
110// Use (non-rank-weighted) ayes for calculating support.
111// Allow only promotion/demotion by one rank only.
112// Allow removal of member with rank zero only.
113// This keeps everything O(1) while still allowing arbitrary number of ranks.
114
115// All functions of VoteTally now include the class as a param.
116
117pub 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		// Both fields accrue saturatingly, so their sum can exceed `Votes`.
136		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/// Record needed for every member.
180#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
181pub struct MemberRecord {
182	/// The rank of the member.
183	rank: Rank,
184}
185
186impl MemberRecord {
187	// Constructs a new instance of [`MemberRecord`].
188	pub fn new(rank: Rank) -> Self {
189		Self { rank }
190	}
191}
192
193/// Record needed for every vote.
194#[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	/// Vote was an aye with given vote weight.
208	Aye(Votes),
209	/// Vote was a nay with given vote weight.
210	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
222/// Vote-weight scheme where all voters get one vote regardless of rank.
223pub struct Unit;
224impl Convert<Rank, Votes> for Unit {
225	fn convert(_: Rank) -> Votes {
226		1
227	}
228}
229
230/// Vote-weight scheme where all voters get one vote plus an additional vote for every excess rank
231/// they have. I.e.:
232///
233/// - Each member with an excess rank of 0 gets 1 vote;
234/// - ...with an excess rank of 1 gets 2 votes;
235/// - ...with an excess rank of 2 gets 3 votes;
236/// - ...with an excess rank of 3 gets 4 votes;
237/// - ...with an excess rank of 4 gets 5 votes.
238pub struct Linear;
239impl Convert<Rank, Votes> for Linear {
240	fn convert(r: Rank) -> Votes {
241		// `r + 1` would overflow `Rank` at the maximum rank; the result always fits in `Votes`.
242		Votes::from(r) + 1
243	}
244}
245
246/// Vote-weight scheme where all voters get one vote plus additional votes for every excess rank
247/// they have incrementing by one vote for each excess rank. I.e.:
248///
249/// - Each member with an excess rank of 0 gets 1 vote;
250/// - ...with an excess rank of 1 gets 3 votes;
251/// - ...with an excess rank of 2 gets 6 votes;
252/// - ...with an excess rank of 3 gets 10 votes;
253/// - ...with an excess rank of 4 gets 15 votes.
254pub struct Geometric;
255impl Convert<Rank, Votes> for Geometric {
256	fn convert(r: Rank) -> Votes {
257		// Computed in `u64`: at the maximum rank `r + 1` would overflow `Rank`, and once widened,
258		// `v * (v + 1)` would overflow `Votes`. The result is at most 2_147_516_416, so it always
259		// fits in `Votes`.
260		let v = u64::from(r) + 1;
261		(v * (v + 1) / 2) as Votes
262	}
263}
264
265/// Trait for getting the maximum number of voters for a given poll class.
266pub trait GetMaxVoters {
267	/// Poll class type.
268	type Class;
269	/// Return the maximum number of voters for the poll class `c`.
270	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
279/// Guard to ensure that the given origin is a member of the collective. The rank of the member is
280/// the `Success` value.
281pub 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
306/// Guard to ensure that the given origin is a member of the collective. The rank of the member is
307/// the `Success` value.
308pub 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
331/// Guard to ensure that the given origin is a member of the collective. The account ID of the
332/// member is the `Success` value.
333pub 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
361/// Guard to ensure that the given origin is a member of the collective. The pair of both the
362/// account ID and the rank of the member is the `Success` value.
363pub 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/// Helper functions to setup benchmarking.
395#[impl_trait_for_tuples::impl_for_tuples(8)]
396pub trait BenchmarkSetup<AccountId> {
397	/// Ensure that this member is registered correctly.
398	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		/// Weight information for extrinsics in this pallet.
414		type WeightInfo: WeightInfo;
415
416		/// The runtime event type.
417		#[allow(deprecated)]
418		type RuntimeEvent: From<Event<Self, I>>
419			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
420
421		/// The origin required to add a member.
422		type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>;
423
424		/// The origin required to remove a member.
425		///
426		/// The success value indicates the maximum rank *from which* the removal may be.
427		type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
428
429		/// The origin required to promote a member. The success value indicates the
430		/// maximum rank *to which* the promotion may be.
431		type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
432
433		/// The origin required to demote a member. The success value indicates the
434		/// maximum rank *from which* the demotion may be.
435		type DemoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
436
437		/// The origin that can swap the account of a member.
438		type ExchangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
439
440		/// The polling system used for our voting.
441		type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = BlockNumberFor<Self>>;
442
443		/// Convert the tally class into the minimum rank required to vote on the poll. If
444		/// `Polls::Class` is the same type as `Rank`, then `Identity` can be used here to mean
445		/// "a rank of at least the poll class".
446		type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>;
447
448		/// An external handler that will be notified when two members are swapped.
449		type MemberSwappedHandler: RankedMembersSwapHandler<
450			<Pallet<Self, I> as RankedMembers>::AccountId,
451			<Pallet<Self, I> as RankedMembers>::Rank,
452		>;
453
454		/// Convert a rank_delta into a number of votes the rank gets.
455		///
456		/// Rank_delta is defined as the number of ranks above the minimum required to take part
457		/// in the poll.
458		type VoteWeight: Convert<Rank, Votes>;
459
460		/// The maximum number of members for a given rank in the collective.
461		///
462		/// The member at rank `x` contributes to the count at rank `x` and all ranks below it.
463		/// Therefore, the limit `m` at rank `x` sets the maximum total member count for rank `x`
464		/// and all ranks above.
465		/// The `None` indicates no member count limit for the given rank.
466		type MaxMemberCount: MaybeConvert<Rank, MemberIndex>;
467
468		/// Setup a member for benchmarking.
469		#[cfg(feature = "runtime-benchmarks")]
470		type BenchmarkSetup: BenchmarkSetup<Self::AccountId>;
471	}
472
473	/// The number of members in the collective who have at least the rank according to the index
474	/// of the vec.
475	#[pallet::storage]
476	pub type MemberCount<T: Config<I>, I: 'static = ()> =
477		StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>;
478
479	/// The current members of the collective.
480	#[pallet::storage]
481	pub type Members<T: Config<I>, I: 'static = ()> =
482		StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>;
483
484	/// The index of each ranks's member into the group of members who have at least that rank.
485	#[pallet::storage]
486	pub type IdToIndex<T: Config<I>, I: 'static = ()> =
487		StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>;
488
489	/// The members in the collective by index. All indices in the range `0..MemberCount` will
490	/// return `Some`, however a member's index is not guaranteed to remain unchanged over time.
491	#[pallet::storage]
492	pub type IndexToId<T: Config<I>, I: 'static = ()> =
493		StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>;
494
495	/// Votes on a given proposal, if it is ongoing.
496	#[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		/// A member `who` has been added.
514		MemberAdded { who: T::AccountId },
515		/// The member `who`se rank has been changed to the given `rank`.
516		RankChanged { who: T::AccountId, rank: Rank },
517		/// The member `who` of given `rank` has been removed from the collective.
518		MemberRemoved { who: T::AccountId, rank: Rank },
519		/// The member `who` has voted for the `poll` with the given `vote` leading to an updated
520		/// `tally`.
521		Voted { who: T::AccountId, poll: PollIndexOf<T, I>, vote: VoteRecord, tally: TallyOf<T, I> },
522		/// The member `who` had their `AccountId` changed to `new_who`.
523		MemberExchanged { who: T::AccountId, new_who: T::AccountId },
524	}
525
526	#[pallet::error]
527	pub enum Error<T, I = ()> {
528		/// Account is already a member.
529		AlreadyMember,
530		/// Account is not a member.
531		NotMember,
532		/// The given poll index is unknown or has closed.
533		NotPolling,
534		/// The given poll is still ongoing.
535		Ongoing,
536		/// There are no further records to be removed.
537		NoneRemaining,
538		/// Unexpected error in state.
539		Corruption,
540		/// The member's rank is too low to vote.
541		RankTooLow,
542		/// The information provided is incorrect.
543		InvalidWitness,
544		/// The origin is not sufficiently privileged to do the operation.
545		NoPermission,
546		/// The new member to exchange is the same as the old member
547		SameMember,
548		/// The max member count for the rank has been reached.
549		TooManyMembers,
550	}
551
552	#[pallet::call]
553	impl<T: Config<I>, I: 'static> Pallet<T, I> {
554		/// Introduce a new member.
555		///
556		/// - `origin`: Must be the `AddOrigin`.
557		/// - `who`: Account of non-member which will become a member.
558		///
559		/// Weight: `O(1)`
560		#[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		/// Increment the rank of an existing member by one.
569		///
570		/// - `origin`: Must be the `PromoteOrigin`.
571		/// - `who`: Account of existing member.
572		///
573		/// Weight: `O(1)`
574		#[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		/// Decrement the rank of an existing member by one. If the member is already at rank zero,
583		/// then they are removed entirely.
584		///
585		/// - `origin`: Must be the `DemoteOrigin`.
586		/// - `who`: Account of existing member of rank greater than zero.
587		///
588		/// Weight: `O(1)`, less if the member's index is highest in its rank.
589		#[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		/// Remove the member entirely.
598		///
599		/// - `origin`: Must be the `RemoveOrigin`.
600		/// - `who`: Account of existing member of rank greater than zero.
601		/// - `min_rank`: The rank of the member or greater.
602		///
603		/// Weight: `O(min_rank)`.
604		#[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		/// Add an aye or nay vote for the sender to the given proposal.
626		///
627		/// - `origin`: Must be `Signed` by a member account.
628		/// - `poll`: Index of a poll which is ongoing.
629		/// - `aye`: `true` if the vote is to approve the proposal, `false` otherwise.
630		///
631		/// Transaction fees are be waived if the member is voting on any particular proposal
632		/// for the first time and the call is successful. Subsequent vote changes will charge a
633		/// fee.
634		///
635		/// Weight: `O(1)`, less if there was no previous vote on the poll by the member.
636		#[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		/// Remove votes from the given poll. It must have ended.
685		///
686		/// - `origin`: Must be `Signed` by any account.
687		/// - `poll_index`: Index of a poll which is completed and for which votes continue to
688		///   exist.
689		/// - `max`: Maximum number of vote items from remove in this call.
690		///
691		/// Transaction fees are waived if the operation is successful.
692		///
693		/// Weight `O(max)` (less if there are fewer items to remove than `max`).
694		#[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 Err(Error::<T, I>::NoneRemaining)
711				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		/// Exchanges a member with a new account and the same existing rank.
723		///
724		/// - `origin`: Must be the `ExchangeOrigin`.
725		/// - `who`: Account of existing member of rank greater than zero to be exchanged.
726		/// - `new_who`: New Account of existing member of rank greater than zero to exchanged to.
727		#[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		/// Adds a member into the ranked collective at level 0.
792		///
793		/// No origin checks are executed.
794		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		/// Promotes a member in the ranked collective into the next higher rank.
813		///
814		/// A `maybe_max_rank` may be provided to check that the member does not get promoted beyond
815		/// a certain rank. Is `None` is provided, then the rank will be incremented without checks.
816		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		/// Demotes a member in the ranked collective into the next lower rank.
843		///
844		/// A `maybe_max_rank` may be provided to check that the member does not get demoted from
845		/// a certain rank. Is `None` is provided, then the rank will be decremented without checks.
846		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		/// Add a member to the rank collective, and continue to promote them until a certain rank
870		/// is reached.
871		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		/// Determine the rank of the account behind the `Signed` origin `o`, `None` if the account
884		/// is unknown to this collective or `o` is not `Signed`.
885		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		/// Removes a member from the rank collective
893		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		/// Ensure the correctness of the state of this pallet.
905		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		/// ### Invariants of Member storage items
913		///
914		/// Total number of [`Members`] in storage should be >= [`MemberIndex`] of a [`Rank`] in
915		///    [`MemberCount`].
916		/// [`Rank`] in Members should be in [`MemberCount`]
917		/// [`Sum`] of [`MemberCount`] index should be the same as the sum of all the index attained
918		/// for rank possessed by [`Members`]
919		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		/// ### Invariants of Index storage items
952		/// [`Member`] in storage of [`IdToIndex`] should be the same as [`Member`] in [`IndexToId`]
953		/// [`Rank`] in [`IdToIndex`] should be the same as the the [`Rank`] in  [`IndexToId`]
954		/// [`Rank`] of the member [`who`] in [`IdToIndex`] should be the same as the [`Rank`] of
955		/// the member [`who`] in [`Members`]
956		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		/// Checks if a rank is part of the `MemberCount`
986		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		/// Checks if a rank is the same as the rank `IndexToId`
997		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		/// Checks if a member(who) rank is the same as the rank of a member(who) in `IdToIndex`
1008		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		/// Determines the total index for a rank
1019		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}