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, 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
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	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
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		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/// Record needed for every member.
178#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
179pub struct MemberRecord {
180	/// The rank of the member.
181	rank: Rank,
182}
183
184impl MemberRecord {
185	// Constructs a new instance of [`MemberRecord`].
186	pub fn new(rank: Rank) -> Self {
187		Self { rank }
188	}
189}
190
191/// Record needed for every vote.
192#[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	/// Vote was an aye with given vote weight.
206	Aye(Votes),
207	/// Vote was a nay with given vote weight.
208	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
220/// Vote-weight scheme where all voters get one vote regardless of rank.
221pub struct Unit;
222impl Convert<Rank, Votes> for Unit {
223	fn convert(_: Rank) -> Votes {
224		1
225	}
226}
227
228/// Vote-weight scheme where all voters get one vote plus an additional vote for every excess rank
229/// they have. I.e.:
230///
231/// - Each member with an excess rank of 0 gets 1 vote;
232/// - ...with an excess rank of 1 gets 2 votes;
233/// - ...with an excess rank of 2 gets 3 votes;
234/// - ...with an excess rank of 3 gets 4 votes;
235/// - ...with an excess rank of 4 gets 5 votes.
236pub struct Linear;
237impl Convert<Rank, Votes> for Linear {
238	fn convert(r: Rank) -> Votes {
239		(r + 1) as Votes
240	}
241}
242
243/// Vote-weight scheme where all voters get one vote plus additional votes for every excess rank
244/// they have incrementing by one vote for each excess rank. I.e.:
245///
246/// - Each member with an excess rank of 0 gets 1 vote;
247/// - ...with an excess rank of 1 gets 3 votes;
248/// - ...with an excess rank of 2 gets 6 votes;
249/// - ...with an excess rank of 3 gets 10 votes;
250/// - ...with an excess rank of 4 gets 15 votes.
251pub 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
259/// Trait for getting the maximum number of voters for a given poll class.
260pub trait GetMaxVoters {
261	/// Poll class type.
262	type Class;
263	/// Return the maximum number of voters for the poll class `c`.
264	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
273/// Guard to ensure that the given origin is a member of the collective. The rank of the member is
274/// the `Success` value.
275pub 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
300/// Guard to ensure that the given origin is a member of the collective. The rank of the member is
301/// the `Success` value.
302pub 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
325/// Guard to ensure that the given origin is a member of the collective. The account ID of the
326/// member is the `Success` value.
327pub 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
355/// Guard to ensure that the given origin is a member of the collective. The pair of both the
356/// account ID and the rank of the member is the `Success` value.
357pub 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/// Helper functions to setup benchmarking.
389#[impl_trait_for_tuples::impl_for_tuples(8)]
390pub trait BenchmarkSetup<AccountId> {
391	/// Ensure that this member is registered correctly.
392	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		/// Weight information for extrinsics in this pallet.
408		type WeightInfo: WeightInfo;
409
410		/// The runtime event type.
411		#[allow(deprecated)]
412		type RuntimeEvent: From<Event<Self, I>>
413			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
414
415		/// The origin required to add a member.
416		type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>;
417
418		/// The origin required to remove a member.
419		///
420		/// The success value indicates the maximum rank *from which* the removal may be.
421		type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
422
423		/// The origin required to promote a member. The success value indicates the
424		/// maximum rank *to which* the promotion may be.
425		type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
426
427		/// The origin required to demote a member. The success value indicates the
428		/// maximum rank *from which* the demotion may be.
429		type DemoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
430
431		/// The origin that can swap the account of a member.
432		type ExchangeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
433
434		/// The polling system used for our voting.
435		type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = BlockNumberFor<Self>>;
436
437		/// Convert the tally class into the minimum rank required to vote on the poll. If
438		/// `Polls::Class` is the same type as `Rank`, then `Identity` can be used here to mean
439		/// "a rank of at least the poll class".
440		type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>;
441
442		/// An external handler that will be notified when two members are swapped.
443		type MemberSwappedHandler: RankedMembersSwapHandler<
444			<Pallet<Self, I> as RankedMembers>::AccountId,
445			<Pallet<Self, I> as RankedMembers>::Rank,
446		>;
447
448		/// Convert a rank_delta into a number of votes the rank gets.
449		///
450		/// Rank_delta is defined as the number of ranks above the minimum required to take part
451		/// in the poll.
452		type VoteWeight: Convert<Rank, Votes>;
453
454		/// The maximum number of members for a given rank in the collective.
455		///
456		/// The member at rank `x` contributes to the count at rank `x` and all ranks below it.
457		/// Therefore, the limit `m` at rank `x` sets the maximum total member count for rank `x`
458		/// and all ranks above.
459		/// The `None` indicates no member count limit for the given rank.
460		type MaxMemberCount: MaybeConvert<Rank, MemberIndex>;
461
462		/// Setup a member for benchmarking.
463		#[cfg(feature = "runtime-benchmarks")]
464		type BenchmarkSetup: BenchmarkSetup<Self::AccountId>;
465	}
466
467	/// The number of members in the collective who have at least the rank according to the index
468	/// of the vec.
469	#[pallet::storage]
470	pub type MemberCount<T: Config<I>, I: 'static = ()> =
471		StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>;
472
473	/// The current members of the collective.
474	#[pallet::storage]
475	pub type Members<T: Config<I>, I: 'static = ()> =
476		StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>;
477
478	/// The index of each ranks's member into the group of members who have at least that rank.
479	#[pallet::storage]
480	pub type IdToIndex<T: Config<I>, I: 'static = ()> =
481		StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>;
482
483	/// The members in the collective by index. All indices in the range `0..MemberCount` will
484	/// return `Some`, however a member's index is not guaranteed to remain unchanged over time.
485	#[pallet::storage]
486	pub type IndexToId<T: Config<I>, I: 'static = ()> =
487		StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>;
488
489	/// Votes on a given proposal, if it is ongoing.
490	#[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		/// A member `who` has been added.
508		MemberAdded { who: T::AccountId },
509		/// The member `who`se rank has been changed to the given `rank`.
510		RankChanged { who: T::AccountId, rank: Rank },
511		/// The member `who` of given `rank` has been removed from the collective.
512		MemberRemoved { who: T::AccountId, rank: Rank },
513		/// The member `who` has voted for the `poll` with the given `vote` leading to an updated
514		/// `tally`.
515		Voted { who: T::AccountId, poll: PollIndexOf<T, I>, vote: VoteRecord, tally: TallyOf<T, I> },
516		/// The member `who` had their `AccountId` changed to `new_who`.
517		MemberExchanged { who: T::AccountId, new_who: T::AccountId },
518	}
519
520	#[pallet::error]
521	pub enum Error<T, I = ()> {
522		/// Account is already a member.
523		AlreadyMember,
524		/// Account is not a member.
525		NotMember,
526		/// The given poll index is unknown or has closed.
527		NotPolling,
528		/// The given poll is still ongoing.
529		Ongoing,
530		/// There are no further records to be removed.
531		NoneRemaining,
532		/// Unexpected error in state.
533		Corruption,
534		/// The member's rank is too low to vote.
535		RankTooLow,
536		/// The information provided is incorrect.
537		InvalidWitness,
538		/// The origin is not sufficiently privileged to do the operation.
539		NoPermission,
540		/// The new member to exchange is the same as the old member
541		SameMember,
542		/// The max member count for the rank has been reached.
543		TooManyMembers,
544	}
545
546	#[pallet::call]
547	impl<T: Config<I>, I: 'static> Pallet<T, I> {
548		/// Introduce a new member.
549		///
550		/// - `origin`: Must be the `AddOrigin`.
551		/// - `who`: Account of non-member which will become a member.
552		///
553		/// Weight: `O(1)`
554		#[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		/// Increment the rank of an existing member by one.
563		///
564		/// - `origin`: Must be the `PromoteOrigin`.
565		/// - `who`: Account of existing member.
566		///
567		/// Weight: `O(1)`
568		#[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		/// Decrement the rank of an existing member by one. If the member is already at rank zero,
577		/// then they are removed entirely.
578		///
579		/// - `origin`: Must be the `DemoteOrigin`.
580		/// - `who`: Account of existing member of rank greater than zero.
581		///
582		/// Weight: `O(1)`, less if the member's index is highest in its rank.
583		#[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		/// Remove the member entirely.
592		///
593		/// - `origin`: Must be the `RemoveOrigin`.
594		/// - `who`: Account of existing member of rank greater than zero.
595		/// - `min_rank`: The rank of the member or greater.
596		///
597		/// Weight: `O(min_rank)`.
598		#[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		/// Add an aye or nay vote for the sender to the given proposal.
620		///
621		/// - `origin`: Must be `Signed` by a member account.
622		/// - `poll`: Index of a poll which is ongoing.
623		/// - `aye`: `true` if the vote is to approve the proposal, `false` otherwise.
624		///
625		/// Transaction fees are be waived if the member is voting on any particular proposal
626		/// for the first time and the call is successful. Subsequent vote changes will charge a
627		/// fee.
628		///
629		/// Weight: `O(1)`, less if there was no previous vote on the poll by the member.
630		#[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		/// Remove votes from the given poll. It must have ended.
678		///
679		/// - `origin`: Must be `Signed` by any account.
680		/// - `poll_index`: Index of a poll which is completed and for which votes continue to
681		///   exist.
682		/// - `max`: Maximum number of vote items from remove in this call.
683		///
684		/// Transaction fees are waived if the operation is successful.
685		///
686		/// Weight `O(max)` (less if there are fewer items to remove than `max`).
687		#[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 Err(Error::<T, I>::NoneRemaining)
704				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		/// Exchanges a member with a new account and the same existing rank.
716		///
717		/// - `origin`: Must be the `ExchangeOrigin`.
718		/// - `who`: Account of existing member of rank greater than zero to be exchanged.
719		/// - `new_who`: New Account of existing member of rank greater than zero to exchanged to.
720		#[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		/// Adds a member into the ranked collective at level 0.
785		///
786		/// No origin checks are executed.
787		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		/// Promotes a member in the ranked collective into the next higher rank.
806		///
807		/// A `maybe_max_rank` may be provided to check that the member does not get promoted beyond
808		/// a certain rank. Is `None` is provided, then the rank will be incremented without checks.
809		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		/// Demotes a member in the ranked collective into the next lower rank.
836		///
837		/// A `maybe_max_rank` may be provided to check that the member does not get demoted from
838		/// a certain rank. Is `None` is provided, then the rank will be decremented without checks.
839		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		/// Add a member to the rank collective, and continue to promote them until a certain rank
863		/// is reached.
864		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		/// Determine the rank of the account behind the `Signed` origin `o`, `None` if the account
877		/// is unknown to this collective or `o` is not `Signed`.
878		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		/// Removes a member from the rank collective
886		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		/// Ensure the correctness of the state of this pallet.
898		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		/// ### Invariants of Member storage items
906		///
907		/// Total number of [`Members`] in storage should be >= [`MemberIndex`] of a [`Rank`] in
908		///    [`MemberCount`].
909		/// [`Rank`] in Members should be in [`MemberCount`]
910		/// [`Sum`] of [`MemberCount`] index should be the same as the sum of all the index attained
911		/// for rank possessed by [`Members`]
912		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		/// ### Invariants of Index storage items
945		/// [`Member`] in storage of [`IdToIndex`] should be the same as [`Member`] in [`IndexToId`]
946		/// [`Rank`] in [`IdToIndex`] should be the same as the the [`Rank`] in  [`IndexToId`]
947		/// [`Rank`] of the member [`who`] in [`IdToIndex`] should be the same as the [`Rank`] of
948		/// the member [`who`] in [`Members`]
949		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		/// Checks if a rank is part of the `MemberCount`
979		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		/// Checks if a rank is the same as the rank `IndexToId`
990		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		/// Checks if a member(who) rank is the same as the rank of a member(who) in `IdToIndex`
1001		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		/// Determines the total index for a rank
1012		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}