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