referrerpolicy=no-referrer-when-downgrade

pallet_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//! Collective system: Members of a set of account IDs can make their collective feelings known
19//! through dispatched calls from one of two specialized origins.
20//!
21//! The membership can be provided in one of two ways: either directly, using the Root-dispatchable
22//! function `set_members`, or indirectly, through implementing the `ChangeMembers`.
23//! The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight
24//! calculations, but enforces this neither in `set_members` nor in `change_members_sorted`.
25//!
26//! A "prime" member may be set to help determine the default vote behavior based on chain
27//! config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any
28//! abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then
29//! abstentions will first follow the majority of the collective voting, and then the prime
30//! member.
31//!
32//! Voting happens through motions comprising a proposal (i.e. a curried dispatchable) plus a
33//! number of approvals required for it to pass and be called. Motions are open for members to
34//! vote on for a minimum period given by `MotionDuration`. As soon as the needed number of
35//! approvals is given, the motion is closed and executed. If the number of approvals is not reached
36//! during the voting period, then `close` may be called by any account in order to force the end
37//! the motion explicitly. If a prime member is defined then their vote is used in place of any
38//! abstentions and the proposal is executed if there are enough approvals counting the new votes.
39//!
40//! If there are not, or if no prime is set, then the motion is dropped without being executed.
41
42#![cfg_attr(not(feature = "std"), no_std)]
43
44extern crate alloc;
45
46use alloc::{boxed::Box, vec, vec::Vec};
47use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
48use core::{marker::PhantomData, result};
49use scale_info::TypeInfo;
50use sp_io::storage;
51use sp_runtime::{
52	traits::{Dispatchable, Hash},
53	DispatchError, RuntimeDebug,
54};
55
56use frame_support::{
57	dispatch::{
58		DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, Pays, PostDispatchInfo,
59	},
60	ensure,
61	traits::{
62		Backing, ChangeMembers, Consideration, EnsureOrigin, EnsureOriginWithArg, Get, GetBacking,
63		InitializeMembers, MaybeConsideration, OriginTrait, StorageVersion,
64	},
65	weights::Weight,
66};
67
68#[cfg(any(feature = "try-runtime", test))]
69use sp_runtime::TryRuntimeError;
70
71#[cfg(test)]
72mod tests;
73
74#[cfg(feature = "runtime-benchmarks")]
75mod benchmarking;
76pub mod migrations;
77pub mod weights;
78
79pub use pallet::*;
80pub use weights::WeightInfo;
81
82const LOG_TARGET: &str = "runtime::collective";
83
84/// Simple index type for proposal counting.
85pub type ProposalIndex = u32;
86
87/// A number of members.
88///
89/// This also serves as a number of voting members, and since for motions, each member may
90/// vote exactly once, therefore also the number of votes for any given motion.
91pub type MemberCount = u32;
92
93/// Default voting strategy when a member is inactive.
94pub trait DefaultVote {
95	/// Get the default voting strategy, given:
96	///
97	/// - Whether the prime member voted Aye.
98	/// - Raw number of yes votes.
99	/// - Raw number of no votes.
100	/// - Total number of member count.
101	fn default_vote(
102		prime_vote: Option<bool>,
103		yes_votes: MemberCount,
104		no_votes: MemberCount,
105		len: MemberCount,
106	) -> bool;
107}
108
109/// Set the prime member's vote as the default vote.
110pub struct PrimeDefaultVote;
111
112impl DefaultVote for PrimeDefaultVote {
113	fn default_vote(
114		prime_vote: Option<bool>,
115		_yes_votes: MemberCount,
116		_no_votes: MemberCount,
117		_len: MemberCount,
118	) -> bool {
119		prime_vote.unwrap_or(false)
120	}
121}
122
123/// First see if yes vote are over majority of the whole collective. If so, set the default vote
124/// as yes. Otherwise, use the prime member's vote as the default vote.
125pub struct MoreThanMajorityThenPrimeDefaultVote;
126
127impl DefaultVote for MoreThanMajorityThenPrimeDefaultVote {
128	fn default_vote(
129		prime_vote: Option<bool>,
130		yes_votes: MemberCount,
131		_no_votes: MemberCount,
132		len: MemberCount,
133	) -> bool {
134		let more_than_majority = yes_votes * 2 > len;
135		more_than_majority || prime_vote.unwrap_or(false)
136	}
137}
138
139/// Origin for the collective module.
140#[derive(
141	PartialEq,
142	Eq,
143	Clone,
144	RuntimeDebug,
145	Encode,
146	Decode,
147	DecodeWithMemTracking,
148	TypeInfo,
149	MaxEncodedLen,
150)]
151#[scale_info(skip_type_params(I))]
152#[codec(mel_bound(AccountId: MaxEncodedLen))]
153pub enum RawOrigin<AccountId, I> {
154	/// It has been condoned by a given number of members of the collective from a given total.
155	Members(MemberCount, MemberCount),
156	/// It has been condoned by a single member of the collective.
157	Member(AccountId),
158	/// Dummy to manage the fact we have instancing.
159	_Phantom(PhantomData<I>),
160}
161
162impl<AccountId, I> GetBacking for RawOrigin<AccountId, I> {
163	fn get_backing(&self) -> Option<Backing> {
164		match self {
165			RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }),
166			_ => None,
167		}
168	}
169}
170
171/// Info for keeping track of a motion being voted on.
172#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
173pub struct Votes<AccountId, BlockNumber> {
174	/// The proposal's unique index.
175	index: ProposalIndex,
176	/// The number of approval votes that are needed to pass the motion.
177	threshold: MemberCount,
178	/// The current set of voters that approved it.
179	ayes: Vec<AccountId>,
180	/// The current set of voters that rejected it.
181	nays: Vec<AccountId>,
182	/// The hard end time of this vote.
183	end: BlockNumber,
184}
185
186/// Types implementing various cost strategies for a given proposal count.
187///
188/// These types implement [Convert](sp_runtime::traits::Convert) trait and can be used with types
189/// like [HoldConsideration](`frame_support::traits::fungible::HoldConsideration`) implementing
190/// [Consideration](`frame_support::traits::Consideration`) trait.
191///
192/// ### Example:
193///
194/// 1. Linear increasing with helper types.
195#[doc = docify::embed!("src/tests.rs", deposit_types_with_linear_work)]
196///
197/// 2. Geometrically increasing with helper types.
198#[doc = docify::embed!("src/tests.rs", deposit_types_with_geometric_work)]
199///
200/// 3. Geometrically increasing with rounding.
201#[doc = docify::embed!("src/tests.rs", deposit_round_with_geometric_work)]
202pub mod deposit {
203	use core::marker::PhantomData;
204	use sp_core::Get;
205	use sp_runtime::{traits::Convert, FixedPointNumber, FixedU128, Saturating};
206
207	/// Constant deposit amount regardless of current proposal count.
208	/// Returns `None` if configured with zero deposit.
209	pub struct Constant<Deposit>(PhantomData<Deposit>);
210	impl<Deposit, Balance> Convert<u32, Balance> for Constant<Deposit>
211	where
212		Deposit: Get<Balance>,
213		Balance: frame_support::traits::tokens::Balance,
214	{
215		fn convert(_: u32) -> Balance {
216			Deposit::get()
217		}
218	}
219
220	/// Linear increasing with some offset.
221	/// f(x) = ax + b, a = `Slope`, x = `proposal_count`, b = `Offset`.
222	pub struct Linear<Slope, Offset>(PhantomData<(Slope, Offset)>);
223	impl<Slope, Offset, Balance> Convert<u32, Balance> for Linear<Slope, Offset>
224	where
225		Slope: Get<u32>,
226		Offset: Get<Balance>,
227		Balance: frame_support::traits::tokens::Balance,
228	{
229		fn convert(proposal_count: u32) -> Balance {
230			let base: Balance = Slope::get().saturating_mul(proposal_count).into();
231			Offset::get().saturating_add(base)
232		}
233	}
234
235	/// Geometrically increasing.
236	/// f(x) = a * r^x, a = `Base`, x = `proposal_count`, r = `Ratio`.
237	pub struct Geometric<Ratio, Base>(PhantomData<(Ratio, Base)>);
238	impl<Ratio, Base, Balance> Convert<u32, Balance> for Geometric<Ratio, Base>
239	where
240		Ratio: Get<FixedU128>,
241		Base: Get<Balance>,
242		Balance: frame_support::traits::tokens::Balance,
243	{
244		fn convert(proposal_count: u32) -> Balance {
245			Ratio::get()
246				.saturating_pow(proposal_count as usize)
247				.saturating_mul_int(Base::get())
248		}
249	}
250
251	/// Rounds a `Deposit` result with `Precision`.
252	/// Particularly useful for types like [`Geometric`] that might produce deposits with high
253	/// precision.
254	pub struct Round<Precision, Deposit>(PhantomData<(Precision, Deposit)>);
255	impl<Precision, Deposit, Balance> Convert<u32, Balance> for Round<Precision, Deposit>
256	where
257		Precision: Get<u32>,
258		Deposit: Convert<u32, Balance>,
259		Balance: frame_support::traits::tokens::Balance,
260	{
261		fn convert(proposal_count: u32) -> Balance {
262			let deposit = Deposit::convert(proposal_count);
263			if !deposit.is_zero() {
264				let factor: Balance =
265					Balance::from(10u32).saturating_pow(Precision::get() as usize);
266				if factor > deposit {
267					deposit
268				} else {
269					(deposit / factor) * factor
270				}
271			} else {
272				deposit
273			}
274		}
275	}
276
277	/// Defines `Period` for supplied `Step` implementing [`Convert`] trait.
278	pub struct Stepped<Period, Step>(PhantomData<(Period, Step)>);
279	impl<Period, Step, Balance> Convert<u32, Balance> for Stepped<Period, Step>
280	where
281		Period: Get<u32>,
282		Step: Convert<u32, Balance>,
283		Balance: frame_support::traits::tokens::Balance,
284	{
285		fn convert(proposal_count: u32) -> Balance {
286			let step_num = proposal_count / Period::get();
287			Step::convert(step_num)
288		}
289	}
290
291	/// Defines `Delay` for supplied `Step` implementing [`Convert`] trait.
292	pub struct Delayed<Delay, Deposit>(PhantomData<(Delay, Deposit)>);
293	impl<Delay, Deposit, Balance> Convert<u32, Balance> for Delayed<Delay, Deposit>
294	where
295		Delay: Get<u32>,
296		Deposit: Convert<u32, Balance>,
297		Balance: frame_support::traits::tokens::Balance,
298	{
299		fn convert(proposal_count: u32) -> Balance {
300			let delay = Delay::get();
301			if delay > proposal_count {
302				return Balance::zero();
303			}
304			let pos = proposal_count.saturating_sub(delay);
305			Deposit::convert(pos)
306		}
307	}
308
309	/// Defines `Ceil` for supplied `Step` implementing [`Convert`] trait.
310	pub struct WithCeil<Ceil, Deposit>(PhantomData<(Ceil, Deposit)>);
311	impl<Ceil, Deposit, Balance> Convert<u32, Balance> for WithCeil<Ceil, Deposit>
312	where
313		Ceil: Get<Balance>,
314		Deposit: Convert<u32, Balance>,
315		Balance: frame_support::traits::tokens::Balance,
316	{
317		fn convert(proposal_count: u32) -> Balance {
318			Deposit::convert(proposal_count).min(Ceil::get())
319		}
320	}
321}
322
323#[frame_support::pallet]
324pub mod pallet {
325	use super::*;
326	use frame_support::pallet_prelude::*;
327	use frame_system::pallet_prelude::*;
328
329	/// The in-code storage version.
330	const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
331
332	#[pallet::pallet]
333	#[pallet::storage_version(STORAGE_VERSION)]
334	#[pallet::without_storage_info]
335	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
336
337	#[pallet::config]
338	pub trait Config<I: 'static = ()>: frame_system::Config {
339		/// The runtime origin type.
340		type RuntimeOrigin: From<RawOrigin<Self::AccountId, I>>;
341
342		/// The runtime call dispatch type.
343		type Proposal: Parameter
344			+ Dispatchable<
345				RuntimeOrigin = <Self as Config<I>>::RuntimeOrigin,
346				PostInfo = PostDispatchInfo,
347			> + From<frame_system::Call<Self>>
348			+ GetDispatchInfo;
349
350		/// The runtime event type.
351		#[allow(deprecated)]
352		type RuntimeEvent: From<Event<Self, I>>
353			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
354
355		/// The time-out for council motions.
356		type MotionDuration: Get<BlockNumberFor<Self>>;
357
358		/// Maximum number of proposals allowed to be active in parallel.
359		type MaxProposals: Get<ProposalIndex>;
360
361		/// The maximum number of members supported by the pallet. Used for weight estimation.
362		///
363		/// NOTE:
364		/// + Benchmarks will need to be re-run and weights adjusted if this changes.
365		/// + This pallet assumes that dependents keep to the limit without enforcing it.
366		type MaxMembers: Get<MemberCount>;
367
368		/// Default vote strategy of this collective.
369		type DefaultVote: DefaultVote;
370
371		/// Weight information for extrinsics in this pallet.
372		type WeightInfo: WeightInfo;
373
374		/// Origin allowed to set collective members
375		type SetMembersOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
376
377		/// The maximum weight of a dispatch call that can be proposed and executed.
378		#[pallet::constant]
379		type MaxProposalWeight: Get<Weight>;
380
381		/// Origin from which a proposal in any status may be disapproved without associated cost
382		/// for a proposer.
383		type DisapproveOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
384
385		/// Origin from which any malicious proposal may be killed with associated cost for a
386		/// proposer.
387		///
388		/// The associated cost is set by [`Config::Consideration`] and can be none.
389		type KillOrigin: EnsureOrigin<<Self as frame_system::Config>::RuntimeOrigin>;
390
391		/// Mechanism to assess the necessity of some cost for publishing and storing a proposal.
392		///
393		/// The footprint is defined as `proposal_count`, which reflects the total number of active
394		/// proposals in the system, excluding the one currently being proposed. The cost may vary
395		/// based on this count.
396		///
397		/// Note: If the resulting deposits are excessively high and cause benchmark failures,
398		/// consider using a constant cost (e.g., [`crate::deposit::Constant`]) equal to the minimum
399		/// balance under the `runtime-benchmarks` feature.
400		type Consideration: MaybeConsideration<Self::AccountId, u32>;
401	}
402
403	#[pallet::genesis_config]
404	#[derive(frame_support::DefaultNoBound)]
405	pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
406		#[serde(skip)]
407		pub phantom: PhantomData<I>,
408		pub members: Vec<T::AccountId>,
409	}
410
411	#[pallet::genesis_build]
412	impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
413		fn build(&self) {
414			use alloc::collections::btree_set::BTreeSet;
415			let members_set: BTreeSet<_> = self.members.iter().collect();
416			assert_eq!(
417				members_set.len(),
418				self.members.len(),
419				"Members cannot contain duplicate accounts."
420			);
421			assert!(
422				self.members.len() <= T::MaxMembers::get() as usize,
423				"Members length cannot exceed MaxMembers.",
424			);
425
426			Pallet::<T, I>::initialize_members(&self.members)
427		}
428	}
429
430	/// Origin for the collective pallet.
431	#[pallet::origin]
432	pub type Origin<T, I = ()> = RawOrigin<<T as frame_system::Config>::AccountId, I>;
433
434	/// The hashes of the active proposals.
435	#[pallet::storage]
436	pub type Proposals<T: Config<I>, I: 'static = ()> =
437		StorageValue<_, BoundedVec<T::Hash, T::MaxProposals>, ValueQuery>;
438
439	/// Actual proposal for a given hash, if it's current.
440	#[pallet::storage]
441	pub type ProposalOf<T: Config<I>, I: 'static = ()> =
442		StorageMap<_, Identity, T::Hash, <T as Config<I>>::Proposal, OptionQuery>;
443
444	/// Consideration cost created for publishing and storing a proposal.
445	///
446	/// Determined by [Config::Consideration] and may be not present for certain proposals (e.g. if
447	/// the proposal count at the time of creation was below threshold N).
448	#[pallet::storage]
449	pub type CostOf<T: Config<I>, I: 'static = ()> =
450		StorageMap<_, Identity, T::Hash, (T::AccountId, T::Consideration), OptionQuery>;
451
452	/// Votes on a given proposal, if it is ongoing.
453	#[pallet::storage]
454	pub type Voting<T: Config<I>, I: 'static = ()> =
455		StorageMap<_, Identity, T::Hash, Votes<T::AccountId, BlockNumberFor<T>>, OptionQuery>;
456
457	/// Proposals so far.
458	#[pallet::storage]
459	pub type ProposalCount<T: Config<I>, I: 'static = ()> = StorageValue<_, u32, ValueQuery>;
460
461	/// The current members of the collective. This is stored sorted (just by value).
462	#[pallet::storage]
463	pub type Members<T: Config<I>, I: 'static = ()> =
464		StorageValue<_, Vec<T::AccountId>, ValueQuery>;
465
466	/// The prime member that helps determine the default vote behavior in case of abstentions.
467	#[pallet::storage]
468	pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
469
470	#[pallet::event]
471	#[pallet::generate_deposit(pub(super) fn deposit_event)]
472	pub enum Event<T: Config<I>, I: 'static = ()> {
473		/// A motion (given hash) has been proposed (by given account) with a threshold (given
474		/// `MemberCount`).
475		Proposed {
476			account: T::AccountId,
477			proposal_index: ProposalIndex,
478			proposal_hash: T::Hash,
479			threshold: MemberCount,
480		},
481		/// A motion (given hash) has been voted on by given account, leaving
482		/// a tally (yes votes and no votes given respectively as `MemberCount`).
483		Voted {
484			account: T::AccountId,
485			proposal_hash: T::Hash,
486			voted: bool,
487			yes: MemberCount,
488			no: MemberCount,
489		},
490		/// A motion was approved by the required threshold.
491		Approved { proposal_hash: T::Hash },
492		/// A motion was not approved by the required threshold.
493		Disapproved { proposal_hash: T::Hash },
494		/// A motion was executed; result will be `Ok` if it returned without error.
495		Executed { proposal_hash: T::Hash, result: DispatchResult },
496		/// A single member did some action; result will be `Ok` if it returned without error.
497		MemberExecuted { proposal_hash: T::Hash, result: DispatchResult },
498		/// A proposal was closed because its threshold was reached or after its duration was up.
499		Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount },
500		/// A proposal was killed.
501		Killed { proposal_hash: T::Hash },
502		/// Some cost for storing a proposal was burned.
503		ProposalCostBurned { proposal_hash: T::Hash, who: T::AccountId },
504		/// Some cost for storing a proposal was released.
505		ProposalCostReleased { proposal_hash: T::Hash, who: T::AccountId },
506	}
507
508	#[pallet::error]
509	pub enum Error<T, I = ()> {
510		/// Account is not a member
511		NotMember,
512		/// Duplicate proposals not allowed
513		DuplicateProposal,
514		/// Proposal must exist
515		ProposalMissing,
516		/// Mismatched index
517		WrongIndex,
518		/// Duplicate vote ignored
519		DuplicateVote,
520		/// Members are already initialized!
521		AlreadyInitialized,
522		/// The close call was made too early, before the end of the voting.
523		TooEarly,
524		/// There can only be a maximum of `MaxProposals` active proposals.
525		TooManyProposals,
526		/// The given weight bound for the proposal was too low.
527		WrongProposalWeight,
528		/// The given length bound for the proposal was too low.
529		WrongProposalLength,
530		/// Prime account is not a member
531		PrimeAccountNotMember,
532		/// Proposal is still active.
533		ProposalActive,
534	}
535
536	#[pallet::hooks]
537	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
538		#[cfg(feature = "try-runtime")]
539		fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
540			Self::do_try_state()
541		}
542	}
543
544	/// A reason for the pallet placing a hold on funds.
545	#[pallet::composite_enum]
546	pub enum HoldReason<I: 'static = ()> {
547		/// Funds are held for submitting and storing a proposal.
548		#[codec(index = 0)]
549		ProposalSubmission,
550	}
551
552	// Note that councillor operations are assigned to the operational class.
553	#[pallet::call]
554	impl<T: Config<I>, I: 'static> Pallet<T, I> {
555		/// Set the collective's membership.
556		///
557		/// - `new_members`: The new member list. Be nice to the chain and provide it sorted.
558		/// - `prime`: The prime member whose vote sets the default.
559		/// - `old_count`: The upper bound for the previous number of members in storage. Used for
560		///   weight estimation.
561		///
562		/// The dispatch of this call must be `SetMembersOrigin`.
563		///
564		/// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but
565		///       the weight estimations rely on it to estimate dispatchable weight.
566		///
567		/// # WARNING:
568		///
569		/// The `pallet-collective` can also be managed by logic outside of the pallet through the
570		/// implementation of the trait [`ChangeMembers`].
571		/// Any call to `set_members` must be careful that the member set doesn't get out of sync
572		/// with other logic managing the member set.
573		///
574		/// ## Complexity:
575		/// - `O(MP + N)` where:
576		///   - `M` old-members-count (code- and governance-bounded)
577		///   - `N` new-members-count (code- and governance-bounded)
578		///   - `P` proposals-count (code-bounded)
579		#[pallet::call_index(0)]
580		#[pallet::weight((
581			T::WeightInfo::set_members(
582				*old_count, // M
583				new_members.len() as u32, // N
584				T::MaxProposals::get() // P
585			),
586			DispatchClass::Operational
587		))]
588		pub fn set_members(
589			origin: OriginFor<T>,
590			new_members: Vec<T::AccountId>,
591			prime: Option<T::AccountId>,
592			old_count: MemberCount,
593		) -> DispatchResultWithPostInfo {
594			T::SetMembersOrigin::ensure_origin(origin)?;
595			if new_members.len() > T::MaxMembers::get() as usize {
596				log::error!(
597					target: LOG_TARGET,
598					"New members count ({}) exceeds maximum amount of members expected ({}).",
599					new_members.len(),
600					T::MaxMembers::get(),
601				);
602			}
603
604			let old = Members::<T, I>::get();
605			if old.len() > old_count as usize {
606				log::warn!(
607					target: LOG_TARGET,
608					"Wrong count used to estimate set_members weight. expected ({}) vs actual ({})",
609					old_count,
610					old.len(),
611				);
612			}
613			if let Some(p) = &prime {
614				ensure!(new_members.contains(p), Error::<T, I>::PrimeAccountNotMember);
615			}
616			let mut new_members = new_members;
617			new_members.sort();
618			<Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members, &old);
619			Prime::<T, I>::set(prime);
620
621			Ok(Some(T::WeightInfo::set_members(
622				old.len() as u32,         // M
623				new_members.len() as u32, // N
624				T::MaxProposals::get(),   // P
625			))
626			.into())
627		}
628
629		/// Dispatch a proposal from a member using the `Member` origin.
630		///
631		/// Origin must be a member of the collective.
632		///
633		/// ## Complexity:
634		/// - `O(B + M + P)` where:
635		/// - `B` is `proposal` size in bytes (length-fee-bounded)
636		/// - `M` members-count (code-bounded)
637		/// - `P` complexity of dispatching `proposal`
638		#[pallet::call_index(1)]
639		#[pallet::weight((
640			T::WeightInfo::execute(
641				*length_bound, // B
642				T::MaxMembers::get(), // M
643			).saturating_add(proposal.get_dispatch_info().call_weight), // P
644			DispatchClass::Operational
645		))]
646		pub fn execute(
647			origin: OriginFor<T>,
648			proposal: Box<<T as Config<I>>::Proposal>,
649			#[pallet::compact] length_bound: u32,
650		) -> DispatchResultWithPostInfo {
651			let who = ensure_signed(origin)?;
652			let members = Members::<T, I>::get();
653			ensure!(members.contains(&who), Error::<T, I>::NotMember);
654			let proposal_len = proposal.encoded_size();
655			ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
656
657			let proposal_hash = T::Hashing::hash_of(&proposal);
658			let result = proposal.dispatch(RawOrigin::Member(who).into());
659			Self::deposit_event(Event::MemberExecuted {
660				proposal_hash,
661				result: result.map(|_| ()).map_err(|e| e.error),
662			});
663
664			Ok(get_result_weight(result)
665				.map(|w| {
666					T::WeightInfo::execute(
667						proposal_len as u32,  // B
668						members.len() as u32, // M
669					)
670					.saturating_add(w) // P
671				})
672				.into())
673		}
674
675		/// Add a new proposal to either be voted on or executed directly.
676		///
677		/// Requires the sender to be member.
678		///
679		/// `threshold` determines whether `proposal` is executed directly (`threshold < 2`)
680		/// or put up for voting.
681		///
682		/// ## Complexity
683		/// - `O(B + M + P1)` or `O(B + M + P2)` where:
684		///   - `B` is `proposal` size in bytes (length-fee-bounded)
685		///   - `M` is members-count (code- and governance-bounded)
686		///   - branching is influenced by `threshold` where:
687		///     - `P1` is proposal execution complexity (`threshold < 2`)
688		///     - `P2` is proposals-count (code-bounded) (`threshold >= 2`)
689		#[pallet::call_index(2)]
690		#[pallet::weight((
691			if *threshold < 2 {
692				T::WeightInfo::propose_execute(
693					*length_bound, // B
694					T::MaxMembers::get(), // M
695				).saturating_add(proposal.get_dispatch_info().call_weight) // P1
696			} else {
697				T::WeightInfo::propose_proposed(
698					*length_bound, // B
699					T::MaxMembers::get(), // M
700					T::MaxProposals::get(), // P2
701				)
702			},
703			DispatchClass::Operational
704		))]
705		pub fn propose(
706			origin: OriginFor<T>,
707			#[pallet::compact] threshold: MemberCount,
708			proposal: Box<<T as Config<I>>::Proposal>,
709			#[pallet::compact] length_bound: u32,
710		) -> DispatchResultWithPostInfo {
711			let who = ensure_signed(origin)?;
712			let members = Members::<T, I>::get();
713			ensure!(members.contains(&who), Error::<T, I>::NotMember);
714
715			if threshold < 2 {
716				let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?;
717
718				Ok(get_result_weight(result)
719					.map(|w| {
720						T::WeightInfo::propose_execute(
721							proposal_len as u32,  // B
722							members.len() as u32, // M
723						)
724						.saturating_add(w) // P1
725					})
726					.into())
727			} else {
728				let (proposal_len, active_proposals) =
729					Self::do_propose_proposed(who, threshold, proposal, length_bound)?;
730
731				Ok(Some(T::WeightInfo::propose_proposed(
732					proposal_len as u32,  // B
733					members.len() as u32, // M
734					active_proposals,     // P2
735				))
736				.into())
737			}
738		}
739
740		/// Add an aye or nay vote for the sender to the given proposal.
741		///
742		/// Requires the sender to be a member.
743		///
744		/// Transaction fees will be waived if the member is voting on any particular proposal
745		/// for the first time and the call is successful. Subsequent vote changes will charge a
746		/// fee.
747		/// ## Complexity
748		/// - `O(M)` where `M` is members-count (code- and governance-bounded)
749		#[pallet::call_index(3)]
750		#[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))]
751		pub fn vote(
752			origin: OriginFor<T>,
753			proposal: T::Hash,
754			#[pallet::compact] index: ProposalIndex,
755			approve: bool,
756		) -> DispatchResultWithPostInfo {
757			let who = ensure_signed(origin)?;
758			let members = Members::<T, I>::get();
759			ensure!(members.contains(&who), Error::<T, I>::NotMember);
760
761			// Detects first vote of the member in the motion
762			let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?;
763
764			if is_account_voting_first_time {
765				Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into())
766			} else {
767				Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into())
768			}
769		}
770
771		// Index 4 was `close_old_weight`; it was removed due to weights v1 deprecation.
772
773		/// Disapprove a proposal, close, and remove it from the system, regardless of its current
774		/// state.
775		///
776		/// Must be called by the Root origin.
777		///
778		/// Parameters:
779		/// * `proposal_hash`: The hash of the proposal that should be disapproved.
780		///
781		/// ## Complexity
782		/// O(P) where P is the number of max proposals
783		#[pallet::call_index(5)]
784		#[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))]
785		pub fn disapprove_proposal(
786			origin: OriginFor<T>,
787			proposal_hash: T::Hash,
788		) -> DispatchResultWithPostInfo {
789			T::DisapproveOrigin::ensure_origin(origin)?;
790			let proposal_count = Self::do_disapprove_proposal(proposal_hash);
791			Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into())
792		}
793
794		/// Close a vote that is either approved, disapproved or whose voting period has ended.
795		///
796		/// May be called by any signed account in order to finish voting and close the proposal.
797		///
798		/// If called before the end of the voting period it will only close the vote if it is
799		/// has enough votes to be approved or disapproved.
800		///
801		/// If called after the end of the voting period abstentions are counted as rejections
802		/// unless there is a prime member set and the prime member cast an approval.
803		///
804		/// If the close operation completes successfully with disapproval, the transaction fee will
805		/// be waived. Otherwise execution of the approved operation will be charged to the caller.
806		///
807		/// + `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed
808		/// proposal.
809		/// + `length_bound`: The upper bound for the length of the proposal in storage. Checked via
810		/// `storage::read` so it is `size_of::<u32>() == 4` larger than the pure length.
811		///
812		/// ## Complexity
813		/// - `O(B + M + P1 + P2)` where:
814		///   - `B` is `proposal` size in bytes (length-fee-bounded)
815		///   - `M` is members-count (code- and governance-bounded)
816		///   - `P1` is the complexity of `proposal` preimage.
817		///   - `P2` is proposal-count (code-bounded)
818		#[pallet::call_index(6)]
819		#[pallet::weight((
820			{
821				let b = *length_bound;
822				let m = T::MaxMembers::get();
823				let p1 = *proposal_weight_bound;
824				let p2 = T::MaxProposals::get();
825				T::WeightInfo::close_early_approved(b, m, p2)
826					.max(T::WeightInfo::close_early_disapproved(m, p2))
827					.max(T::WeightInfo::close_approved(b, m, p2))
828					.max(T::WeightInfo::close_disapproved(m, p2))
829					.saturating_add(p1)
830			},
831			DispatchClass::Operational
832		))]
833		pub fn close(
834			origin: OriginFor<T>,
835			proposal_hash: T::Hash,
836			#[pallet::compact] index: ProposalIndex,
837			proposal_weight_bound: Weight,
838			#[pallet::compact] length_bound: u32,
839		) -> DispatchResultWithPostInfo {
840			ensure_signed(origin)?;
841
842			Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
843		}
844
845		/// Disapprove the proposal and burn the cost held for storing this proposal.
846		///
847		/// Parameters:
848		/// - `origin`: must be the `KillOrigin`.
849		/// - `proposal_hash`: The hash of the proposal that should be killed.
850		///
851		/// Emits `Killed` and `ProposalCostBurned` if any cost was held for a given proposal.
852		#[pallet::call_index(7)]
853		#[pallet::weight(T::WeightInfo::kill(1, T::MaxProposals::get()))]
854		pub fn kill(origin: OriginFor<T>, proposal_hash: T::Hash) -> DispatchResultWithPostInfo {
855			T::KillOrigin::ensure_origin(origin)?;
856			ensure!(
857				ProposalOf::<T, I>::get(&proposal_hash).is_some(),
858				Error::<T, I>::ProposalMissing
859			);
860			let burned = if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
861				cost.burn(&who);
862				Self::deposit_event(Event::ProposalCostBurned { proposal_hash, who });
863				true
864			} else {
865				false
866			};
867			let proposal_count = Self::remove_proposal(proposal_hash);
868
869			Self::deposit_event(Event::Killed { proposal_hash });
870
871			Ok(Some(T::WeightInfo::kill(burned as u32, proposal_count)).into())
872		}
873
874		/// Release the cost held for storing a proposal once the given proposal is completed.
875		///
876		/// If there is no associated cost for the given proposal, this call will have no effect.
877		///
878		/// Parameters:
879		/// - `origin`: must be `Signed` or `Root`.
880		/// - `proposal_hash`: The hash of the proposal.
881		///
882		/// Emits `ProposalCostReleased` if any cost held for a given proposal.
883		#[pallet::call_index(8)]
884		#[pallet::weight(T::WeightInfo::release_proposal_cost())]
885		pub fn release_proposal_cost(
886			origin: OriginFor<T>,
887			proposal_hash: T::Hash,
888		) -> DispatchResult {
889			ensure_signed_or_root(origin)?;
890			ensure!(
891				ProposalOf::<T, I>::get(&proposal_hash).is_none(),
892				Error::<T, I>::ProposalActive
893			);
894			if let Some((who, cost)) = <CostOf<T, I>>::take(proposal_hash) {
895				cost.drop(&who)?;
896				Self::deposit_event(Event::ProposalCostReleased { proposal_hash, who });
897			}
898
899			Ok(())
900		}
901	}
902}
903
904/// Return the weight of a dispatch call result as an `Option`.
905///
906/// Will return the weight regardless of what the state of the result is.
907fn get_result_weight(result: DispatchResultWithPostInfo) -> Option<Weight> {
908	match result {
909		Ok(post_info) => post_info.actual_weight,
910		Err(err) => err.post_info.actual_weight,
911	}
912}
913
914impl<T: Config<I>, I: 'static> Pallet<T, I> {
915	/// Check whether `who` is a member of the collective.
916	pub fn is_member(who: &T::AccountId) -> bool {
917		// Note: The dispatchables *do not* use this to check membership so make sure
918		// to update those if this is changed.
919		Members::<T, I>::get().contains(who)
920	}
921
922	/// Execute immediately when adding a new proposal.
923	pub fn do_propose_execute(
924		proposal: Box<<T as Config<I>>::Proposal>,
925		length_bound: MemberCount,
926	) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> {
927		let proposal_len = proposal.encoded_size();
928		ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
929		let proposal_weight = proposal.get_dispatch_info().call_weight;
930		ensure!(
931			proposal_weight.all_lte(T::MaxProposalWeight::get()),
932			Error::<T, I>::WrongProposalWeight
933		);
934
935		let proposal_hash = T::Hashing::hash_of(&proposal);
936		ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
937
938		let seats = Members::<T, I>::get().len() as MemberCount;
939		let result = proposal.dispatch(RawOrigin::Members(1, seats).into());
940		Self::deposit_event(Event::Executed {
941			proposal_hash,
942			result: result.map(|_| ()).map_err(|e| e.error),
943		});
944		Ok((proposal_len as u32, result))
945	}
946
947	/// Add a new proposal to be voted.
948	pub fn do_propose_proposed(
949		who: T::AccountId,
950		threshold: MemberCount,
951		proposal: Box<<T as Config<I>>::Proposal>,
952		length_bound: MemberCount,
953	) -> Result<(u32, u32), DispatchError> {
954		let proposal_len = proposal.encoded_size();
955		ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
956		let proposal_weight = proposal.get_dispatch_info().call_weight;
957		ensure!(
958			proposal_weight.all_lte(T::MaxProposalWeight::get()),
959			Error::<T, I>::WrongProposalWeight
960		);
961
962		let proposal_hash = T::Hashing::hash_of(&proposal);
963		ensure!(!<ProposalOf<T, I>>::contains_key(proposal_hash), Error::<T, I>::DuplicateProposal);
964
965		let active_proposals =
966			<Proposals<T, I>>::try_mutate(|proposals| -> Result<usize, DispatchError> {
967				proposals.try_push(proposal_hash).map_err(|_| Error::<T, I>::TooManyProposals)?;
968				Ok(proposals.len())
969			})?;
970
971		let cost = T::Consideration::new(&who, active_proposals as u32 - 1)?;
972		if !cost.is_none() {
973			<CostOf<T, I>>::insert(proposal_hash, (who.clone(), cost));
974		}
975
976		let index = ProposalCount::<T, I>::get();
977
978		<ProposalCount<T, I>>::mutate(|i| *i += 1);
979		<ProposalOf<T, I>>::insert(proposal_hash, proposal);
980		let votes = {
981			let end = frame_system::Pallet::<T>::block_number() + T::MotionDuration::get();
982			Votes { index, threshold, ayes: vec![], nays: vec![], end }
983		};
984		<Voting<T, I>>::insert(proposal_hash, votes);
985
986		Self::deposit_event(Event::Proposed {
987			account: who,
988			proposal_index: index,
989			proposal_hash,
990			threshold,
991		});
992		Ok((proposal_len as u32, active_proposals as u32))
993	}
994
995	/// Add an aye or nay vote for the member to the given proposal, returns true if it's the first
996	/// vote of the member in the motion
997	pub fn do_vote(
998		who: T::AccountId,
999		proposal: T::Hash,
1000		index: ProposalIndex,
1001		approve: bool,
1002	) -> Result<bool, DispatchError> {
1003		let mut voting = Voting::<T, I>::get(&proposal).ok_or(Error::<T, I>::ProposalMissing)?;
1004		ensure!(voting.index == index, Error::<T, I>::WrongIndex);
1005
1006		let position_yes = voting.ayes.iter().position(|a| a == &who);
1007		let position_no = voting.nays.iter().position(|a| a == &who);
1008
1009		// Detects first vote of the member in the motion
1010		let is_account_voting_first_time = position_yes.is_none() && position_no.is_none();
1011
1012		if approve {
1013			if position_yes.is_none() {
1014				voting.ayes.push(who.clone());
1015			} else {
1016				return Err(Error::<T, I>::DuplicateVote.into())
1017			}
1018			if let Some(pos) = position_no {
1019				voting.nays.swap_remove(pos);
1020			}
1021		} else {
1022			if position_no.is_none() {
1023				voting.nays.push(who.clone());
1024			} else {
1025				return Err(Error::<T, I>::DuplicateVote.into())
1026			}
1027			if let Some(pos) = position_yes {
1028				voting.ayes.swap_remove(pos);
1029			}
1030		}
1031
1032		let yes_votes = voting.ayes.len() as MemberCount;
1033		let no_votes = voting.nays.len() as MemberCount;
1034		Self::deposit_event(Event::Voted {
1035			account: who,
1036			proposal_hash: proposal,
1037			voted: approve,
1038			yes: yes_votes,
1039			no: no_votes,
1040		});
1041
1042		Voting::<T, I>::insert(&proposal, voting);
1043
1044		Ok(is_account_voting_first_time)
1045	}
1046
1047	/// Close a vote that is either approved, disapproved or whose voting period has ended.
1048	pub fn do_close(
1049		proposal_hash: T::Hash,
1050		index: ProposalIndex,
1051		proposal_weight_bound: Weight,
1052		length_bound: u32,
1053	) -> DispatchResultWithPostInfo {
1054		let voting = Voting::<T, I>::get(&proposal_hash).ok_or(Error::<T, I>::ProposalMissing)?;
1055		ensure!(voting.index == index, Error::<T, I>::WrongIndex);
1056
1057		let mut no_votes = voting.nays.len() as MemberCount;
1058		let mut yes_votes = voting.ayes.len() as MemberCount;
1059		let seats = Members::<T, I>::get().len() as MemberCount;
1060		let approved = yes_votes >= voting.threshold;
1061		let disapproved = seats.saturating_sub(no_votes) < voting.threshold;
1062		// Allow (dis-)approving the proposal as soon as there are enough votes.
1063		if approved {
1064			let (proposal, len) = Self::validate_and_get_proposal(
1065				&proposal_hash,
1066				length_bound,
1067				proposal_weight_bound,
1068			)?;
1069			Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1070			let (proposal_weight, proposal_count) =
1071				Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1072			return Ok((
1073				Some(
1074					T::WeightInfo::close_early_approved(len as u32, seats, proposal_count)
1075						.saturating_add(proposal_weight),
1076				),
1077				Pays::Yes,
1078			)
1079				.into())
1080		} else if disapproved {
1081			Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1082			let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1083			return Ok((
1084				Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)),
1085				Pays::No,
1086			)
1087				.into())
1088		}
1089
1090		// Only allow actual closing of the proposal after the voting period has ended.
1091		ensure!(frame_system::Pallet::<T>::block_number() >= voting.end, Error::<T, I>::TooEarly);
1092
1093		let prime_vote = Prime::<T, I>::get().map(|who| voting.ayes.iter().any(|a| a == &who));
1094
1095		// default voting strategy.
1096		let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats);
1097
1098		let abstentions = seats - (yes_votes + no_votes);
1099		match default {
1100			true => yes_votes += abstentions,
1101			false => no_votes += abstentions,
1102		}
1103		let approved = yes_votes >= voting.threshold;
1104
1105		if approved {
1106			let (proposal, len) = Self::validate_and_get_proposal(
1107				&proposal_hash,
1108				length_bound,
1109				proposal_weight_bound,
1110			)?;
1111			Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1112			let (proposal_weight, proposal_count) =
1113				Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal);
1114			Ok((
1115				Some(
1116					T::WeightInfo::close_approved(len as u32, seats, proposal_count)
1117						.saturating_add(proposal_weight),
1118				),
1119				Pays::Yes,
1120			)
1121				.into())
1122		} else {
1123			Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes });
1124			let proposal_count = Self::do_disapprove_proposal(proposal_hash);
1125			Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into())
1126		}
1127	}
1128
1129	/// Ensure that the right proposal bounds were passed and get the proposal from storage.
1130	///
1131	/// Checks the length in storage via `storage::read` which adds an extra `size_of::<u32>() == 4`
1132	/// to the length.
1133	fn validate_and_get_proposal(
1134		hash: &T::Hash,
1135		length_bound: u32,
1136		weight_bound: Weight,
1137	) -> Result<(<T as Config<I>>::Proposal, usize), DispatchError> {
1138		let key = ProposalOf::<T, I>::hashed_key_for(hash);
1139		// read the length of the proposal storage entry directly
1140		let proposal_len =
1141			storage::read(&key, &mut [0; 0], 0).ok_or(Error::<T, I>::ProposalMissing)?;
1142		ensure!(proposal_len <= length_bound, Error::<T, I>::WrongProposalLength);
1143		let proposal = ProposalOf::<T, I>::get(hash).ok_or(Error::<T, I>::ProposalMissing)?;
1144		let proposal_weight = proposal.get_dispatch_info().call_weight;
1145		ensure!(proposal_weight.all_lte(weight_bound), Error::<T, I>::WrongProposalWeight);
1146		Ok((proposal, proposal_len as usize))
1147	}
1148
1149	/// Weight:
1150	/// If `approved`:
1151	/// - the weight of `proposal` preimage.
1152	/// - two events deposited.
1153	/// - two removals, one mutation.
1154	/// - computation and i/o `O(P + L)` where:
1155	///   - `P` is number of active proposals,
1156	///   - `L` is the encoded length of `proposal` preimage.
1157	///
1158	/// If not `approved`:
1159	/// - one event deposited.
1160	/// Two removals, one mutation.
1161	/// Computation and i/o `O(P)` where:
1162	/// - `P` is number of active proposals
1163	fn do_approve_proposal(
1164		seats: MemberCount,
1165		yes_votes: MemberCount,
1166		proposal_hash: T::Hash,
1167		proposal: <T as Config<I>>::Proposal,
1168	) -> (Weight, u32) {
1169		Self::deposit_event(Event::Approved { proposal_hash });
1170
1171		let dispatch_weight = proposal.get_dispatch_info().call_weight;
1172		let origin = RawOrigin::Members(yes_votes, seats).into();
1173		let result = proposal.dispatch(origin);
1174		Self::deposit_event(Event::Executed {
1175			proposal_hash,
1176			result: result.map(|_| ()).map_err(|e| e.error),
1177		});
1178		// default to the dispatch info weight for safety
1179		let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); // P1
1180
1181		let proposal_count = Self::remove_proposal(proposal_hash);
1182		(proposal_weight, proposal_count)
1183	}
1184
1185	/// Removes a proposal from the pallet, and deposit the `Disapproved` event.
1186	pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 {
1187		// disapproved
1188		Self::deposit_event(Event::Disapproved { proposal_hash });
1189		Self::remove_proposal(proposal_hash)
1190	}
1191
1192	// Removes a proposal from the pallet, cleaning up votes and the vector of proposals.
1193	fn remove_proposal(proposal_hash: T::Hash) -> u32 {
1194		// remove proposal and vote
1195		ProposalOf::<T, I>::remove(&proposal_hash);
1196		Voting::<T, I>::remove(&proposal_hash);
1197		let num_proposals = Proposals::<T, I>::mutate(|proposals| {
1198			proposals.retain(|h| h != &proposal_hash);
1199			proposals.len() + 1 // calculate weight based on original length
1200		});
1201		num_proposals as u32
1202	}
1203
1204	/// Ensure the correctness of the state of this pallet.
1205	///
1206	/// The following expectation must always apply.
1207	///
1208	/// ## Expectations:
1209	///
1210	/// Looking at proposals:
1211	///
1212	/// * Each hash of a proposal that is stored inside `Proposals` must have a
1213	/// call mapped to it inside the `ProposalOf` storage map.
1214	/// * `ProposalCount` must always be more or equal to the number of
1215	/// proposals inside the `Proposals` storage value. The reason why
1216	/// `ProposalCount` can be more is because when a proposal is removed the
1217	/// count is not deducted.
1218	/// * Count of `ProposalOf` should match the count of `Proposals`
1219	///
1220	/// Looking at votes:
1221	/// * The sum of aye and nay votes for a proposal can never exceed
1222	///  `MaxMembers`.
1223	/// * The proposal index inside the `Voting` storage map must be unique.
1224	/// * All proposal hashes inside `Voting` must exist in `Proposals`.
1225	///
1226	/// Looking at members:
1227	/// * The members count must never exceed `MaxMembers`.
1228	/// * All the members must be sorted by value.
1229	///
1230	/// Looking at prime account:
1231	/// * The prime account must be a member of the collective.
1232	#[cfg(any(feature = "try-runtime", test))]
1233	fn do_try_state() -> Result<(), TryRuntimeError> {
1234		Proposals::<T, I>::get().into_iter().try_for_each(
1235			|proposal| -> Result<(), TryRuntimeError> {
1236				ensure!(
1237					ProposalOf::<T, I>::get(proposal).is_some(),
1238					"Proposal hash from `Proposals` is not found inside the `ProposalOf` mapping."
1239				);
1240				Ok(())
1241			},
1242		)?;
1243
1244		ensure!(
1245			Proposals::<T, I>::get().into_iter().count() <= ProposalCount::<T, I>::get() as usize,
1246			"The actual number of proposals is greater than `ProposalCount`"
1247		);
1248		ensure!(
1249			Proposals::<T, I>::get().into_iter().count() == <ProposalOf<T, I>>::iter_keys().count(),
1250			"Proposal count inside `Proposals` is not equal to the proposal count in `ProposalOf`"
1251		);
1252
1253		Proposals::<T, I>::get().into_iter().try_for_each(
1254			|proposal| -> Result<(), TryRuntimeError> {
1255				if let Some(votes) = Voting::<T, I>::get(proposal) {
1256					let ayes = votes.ayes.len();
1257					let nays = votes.nays.len();
1258
1259					ensure!(
1260						ayes.saturating_add(nays) <= T::MaxMembers::get() as usize,
1261						"The sum of ayes and nays is greater than `MaxMembers`"
1262					);
1263				}
1264				Ok(())
1265			},
1266		)?;
1267
1268		let mut proposal_indices = vec![];
1269		Proposals::<T, I>::get().into_iter().try_for_each(
1270			|proposal| -> Result<(), TryRuntimeError> {
1271				if let Some(votes) = Voting::<T, I>::get(proposal) {
1272					let proposal_index = votes.index;
1273					ensure!(
1274						!proposal_indices.contains(&proposal_index),
1275						"The proposal index is not unique."
1276					);
1277					proposal_indices.push(proposal_index);
1278				}
1279				Ok(())
1280			},
1281		)?;
1282
1283		<Voting<T, I>>::iter_keys().try_for_each(
1284			|proposal_hash| -> Result<(), TryRuntimeError> {
1285				ensure!(
1286					Proposals::<T, I>::get().contains(&proposal_hash),
1287					"`Proposals` doesn't contain the proposal hash from the `Voting` storage map."
1288				);
1289				Ok(())
1290			},
1291		)?;
1292
1293		ensure!(
1294			Members::<T, I>::get().len() <= T::MaxMembers::get() as usize,
1295			"The member count is greater than `MaxMembers`."
1296		);
1297
1298		ensure!(
1299			Members::<T, I>::get().windows(2).all(|members| members[0] <= members[1]),
1300			"The members are not sorted by value."
1301		);
1302
1303		if let Some(prime) = Prime::<T, I>::get() {
1304			ensure!(Members::<T, I>::get().contains(&prime), "Prime account is not a member.");
1305		}
1306
1307		Ok(())
1308	}
1309}
1310
1311impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> {
1312	/// Update the members of the collective. Votes are updated and the prime is reset.
1313	///
1314	/// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but
1315	///       the weight estimations rely on it to estimate dispatchable weight.
1316	///
1317	/// ## Complexity
1318	/// - `O(MP + N)`
1319	///   - where `M` old-members-count (governance-bounded)
1320	///   - where `N` new-members-count (governance-bounded)
1321	///   - where `P` proposals-count
1322	fn change_members_sorted(
1323		_incoming: &[T::AccountId],
1324		outgoing: &[T::AccountId],
1325		new: &[T::AccountId],
1326	) {
1327		if new.len() > T::MaxMembers::get() as usize {
1328			log::error!(
1329				target: LOG_TARGET,
1330				"New members count ({}) exceeds maximum amount of members expected ({}).",
1331				new.len(),
1332				T::MaxMembers::get(),
1333			);
1334		}
1335		// remove accounts from all current voting in motions.
1336		let mut outgoing = outgoing.to_vec();
1337		outgoing.sort();
1338		for h in Proposals::<T, I>::get().into_iter() {
1339			<Voting<T, I>>::mutate(h, |v| {
1340				if let Some(mut votes) = v.take() {
1341					votes.ayes = votes
1342						.ayes
1343						.into_iter()
1344						.filter(|i| outgoing.binary_search(i).is_err())
1345						.collect();
1346					votes.nays = votes
1347						.nays
1348						.into_iter()
1349						.filter(|i| outgoing.binary_search(i).is_err())
1350						.collect();
1351					*v = Some(votes);
1352				}
1353			});
1354		}
1355		Members::<T, I>::put(new);
1356		Prime::<T, I>::kill();
1357	}
1358
1359	fn set_prime(prime: Option<T::AccountId>) {
1360		Prime::<T, I>::set(prime);
1361	}
1362
1363	fn get_prime() -> Option<T::AccountId> {
1364		Prime::<T, I>::get()
1365	}
1366}
1367
1368impl<T: Config<I>, I: 'static> InitializeMembers<T::AccountId> for Pallet<T, I> {
1369	fn initialize_members(members: &[T::AccountId]) {
1370		if !members.is_empty() {
1371			assert!(<Members<T, I>>::get().is_empty(), "Members are already initialized!");
1372			let mut members = members.to_vec();
1373			members.sort();
1374			<Members<T, I>>::put(members);
1375		}
1376	}
1377}
1378
1379/// Ensure that the origin `o` represents at least `n` members. Returns `Ok` or an `Err`
1380/// otherwise.
1381pub fn ensure_members<OuterOrigin, AccountId, I>(
1382	o: OuterOrigin,
1383	n: MemberCount,
1384) -> result::Result<MemberCount, &'static str>
1385where
1386	OuterOrigin: Into<result::Result<RawOrigin<AccountId, I>, OuterOrigin>>,
1387{
1388	match o.into() {
1389		Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n),
1390		_ => Err("bad origin: expected to be a threshold number of members"),
1391	}
1392}
1393
1394pub struct EnsureMember<AccountId, I: 'static>(PhantomData<(AccountId, I)>);
1395impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone> EnsureOrigin<O>
1396	for EnsureMember<AccountId, I>
1397where
1398	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1399{
1400	type Success = AccountId;
1401	fn try_origin(o: O) -> Result<Self::Success, O> {
1402		match o.caller().try_into() {
1403			Ok(RawOrigin::Member(id)) => return Ok(id.clone()),
1404			_ => (),
1405		}
1406
1407		Err(o)
1408	}
1409
1410	#[cfg(feature = "runtime-benchmarks")]
1411	fn try_successful_origin() -> Result<O, ()> {
1412		let zero_account_id =
1413			AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
1414				.expect("infinite length input; no invalid inputs for type; qed");
1415		Ok(O::from(RawOrigin::Member(zero_account_id)))
1416	}
1417}
1418
1419impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, I, AccountId: Decode + Clone, T>
1420	EnsureOriginWithArg<O, T> for EnsureMember<AccountId, I>
1421where
1422	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1423{
1424	type Success = <Self as EnsureOrigin<O>>::Success;
1425	fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1426		<Self as EnsureOrigin<O>>::try_origin(o)
1427	}
1428	#[cfg(feature = "runtime-benchmarks")]
1429	fn try_successful_origin(_: &T) -> Result<O, ()> {
1430		<Self as EnsureOrigin<O>>::try_successful_origin()
1431	}
1432}
1433
1434pub struct EnsureMembers<AccountId, I: 'static, const N: u32>(PhantomData<(AccountId, I)>);
1435impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32> EnsureOrigin<O>
1436	for EnsureMembers<AccountId, I, N>
1437where
1438	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1439{
1440	type Success = (MemberCount, MemberCount);
1441	fn try_origin(o: O) -> Result<Self::Success, O> {
1442		match o.caller().try_into() {
1443			Ok(RawOrigin::Members(n, m)) if *n >= N => return Ok((*n, *m)),
1444			_ => (),
1445		}
1446
1447		Err(o)
1448	}
1449
1450	#[cfg(feature = "runtime-benchmarks")]
1451	fn try_successful_origin() -> Result<O, ()> {
1452		Ok(O::from(RawOrigin::Members(N, N)))
1453	}
1454}
1455
1456impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, T>
1457	EnsureOriginWithArg<O, T> for EnsureMembers<AccountId, I, N>
1458where
1459	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1460{
1461	type Success = <Self as EnsureOrigin<O>>::Success;
1462	fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1463		<Self as EnsureOrigin<O>>::try_origin(o)
1464	}
1465	#[cfg(feature = "runtime-benchmarks")]
1466	fn try_successful_origin(_: &T) -> Result<O, ()> {
1467		<Self as EnsureOrigin<O>>::try_successful_origin()
1468	}
1469}
1470
1471pub struct EnsureProportionMoreThan<AccountId, I: 'static, const N: u32, const D: u32>(
1472	PhantomData<(AccountId, I)>,
1473);
1474impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1475	EnsureOrigin<O> for EnsureProportionMoreThan<AccountId, I, N, D>
1476where
1477	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1478{
1479	type Success = ();
1480	fn try_origin(o: O) -> Result<Self::Success, O> {
1481		match o.caller().try_into() {
1482			Ok(RawOrigin::Members(n, m)) if n * D > N * m => return Ok(()),
1483			_ => (),
1484		}
1485
1486		Err(o)
1487	}
1488
1489	#[cfg(feature = "runtime-benchmarks")]
1490	fn try_successful_origin() -> Result<O, ()> {
1491		Ok(O::from(RawOrigin::Members(1u32, 0u32)))
1492	}
1493}
1494
1495impl<
1496		O: OriginTrait + From<RawOrigin<AccountId, I>>,
1497		AccountId,
1498		I,
1499		const N: u32,
1500		const D: u32,
1501		T,
1502	> EnsureOriginWithArg<O, T> for EnsureProportionMoreThan<AccountId, I, N, D>
1503where
1504	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1505{
1506	type Success = <Self as EnsureOrigin<O>>::Success;
1507	fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1508		<Self as EnsureOrigin<O>>::try_origin(o)
1509	}
1510	#[cfg(feature = "runtime-benchmarks")]
1511	fn try_successful_origin(_: &T) -> Result<O, ()> {
1512		<Self as EnsureOrigin<O>>::try_successful_origin()
1513	}
1514}
1515
1516pub struct EnsureProportionAtLeast<AccountId, I: 'static, const N: u32, const D: u32>(
1517	PhantomData<(AccountId, I)>,
1518);
1519impl<O: OriginTrait + From<RawOrigin<AccountId, I>>, AccountId, I, const N: u32, const D: u32>
1520	EnsureOrigin<O> for EnsureProportionAtLeast<AccountId, I, N, D>
1521where
1522	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1523{
1524	type Success = ();
1525	fn try_origin(o: O) -> Result<Self::Success, O> {
1526		match o.caller().try_into() {
1527			Ok(RawOrigin::Members(n, m)) if n * D >= N * m => return Ok(()),
1528			_ => (),
1529		};
1530
1531		Err(o)
1532	}
1533
1534	#[cfg(feature = "runtime-benchmarks")]
1535	fn try_successful_origin() -> Result<O, ()> {
1536		Ok(O::from(RawOrigin::Members(0u32, 0u32)))
1537	}
1538}
1539
1540impl<
1541		O: OriginTrait + From<RawOrigin<AccountId, I>>,
1542		AccountId,
1543		I,
1544		const N: u32,
1545		const D: u32,
1546		T,
1547	> EnsureOriginWithArg<O, T> for EnsureProportionAtLeast<AccountId, I, N, D>
1548where
1549	for<'a> &'a O::PalletsOrigin: TryInto<&'a RawOrigin<AccountId, I>>,
1550{
1551	type Success = <Self as EnsureOrigin<O>>::Success;
1552	fn try_origin(o: O, _: &T) -> Result<Self::Success, O> {
1553		<Self as EnsureOrigin<O>>::try_origin(o)
1554	}
1555	#[cfg(feature = "runtime-benchmarks")]
1556	fn try_successful_origin(_: &T) -> Result<O, ()> {
1557		<Self as EnsureOrigin<O>>::try_successful_origin()
1558	}
1559}