referrerpolicy=no-referrer-when-downgrade

pallet_membership/
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//! # Membership Module
19//!
20//! Allows control of membership of a set of `AccountId`s, useful for managing membership of a
21//! collective. A prime member may be set
22
23// Ensure we're `no_std` when compiling for Wasm.
24#![cfg_attr(not(feature = "std"), no_std)]
25
26extern crate alloc;
27
28use alloc::vec::Vec;
29use frame_support::{
30	traits::{ChangeMembers, Contains, ContainsLengthBound, Get, InitializeMembers, SortedMembers},
31	BoundedVec,
32};
33use sp_runtime::traits::{StaticLookup, UniqueSaturatedInto};
34
35pub mod migrations;
36pub mod weights;
37
38#[cfg(test)]
39mod mock;
40
41#[cfg(feature = "runtime-benchmarks")]
42pub mod benchmarking;
43
44#[cfg(test)]
45mod tests;
46
47pub use pallet::*;
48pub use weights::WeightInfo;
49
50const LOG_TARGET: &str = "runtime::membership";
51
52type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
53
54#[frame_support::pallet]
55pub mod pallet {
56	use super::*;
57	use frame_support::pallet_prelude::*;
58	use frame_system::pallet_prelude::*;
59
60	/// The in-code storage version.
61	const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
62
63	#[pallet::pallet]
64	#[pallet::storage_version(STORAGE_VERSION)]
65	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
66
67	#[pallet::config]
68	pub trait Config<I: 'static = ()>: frame_system::Config {
69		/// The overarching event type.
70		#[allow(deprecated)]
71		type RuntimeEvent: From<Event<Self, I>>
72			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
73
74		/// Required origin for adding a member (though can always be Root).
75		type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>;
76
77		/// Required origin for removing a member (though can always be Root).
78		type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin>;
79
80		/// Required origin for adding and removing a member in a single action.
81		type SwapOrigin: EnsureOrigin<Self::RuntimeOrigin>;
82
83		/// Required origin for resetting membership.
84		type ResetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
85
86		/// Required origin for setting or resetting the prime member.
87		type PrimeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
88
89		/// The receiver of the signal for when the membership has been initialized. This happens
90		/// pre-genesis and will usually be the same as `MembershipChanged`. If you need to do
91		/// something different on initialization, then you can change this accordingly.
92		type MembershipInitialized: InitializeMembers<Self::AccountId>;
93
94		/// The receiver of the signal for when the membership has changed.
95		type MembershipChanged: ChangeMembers<Self::AccountId>;
96
97		/// The maximum number of members that this membership can have.
98		///
99		/// This is used for benchmarking. Re-run the benchmarks if this changes.
100		///
101		/// This is enforced in the code; the membership size can not exceed this limit.
102		type MaxMembers: Get<u32>;
103
104		/// Weight information for extrinsics in this pallet.
105		type WeightInfo: WeightInfo;
106	}
107
108	/// The current membership, stored as an ordered Vec.
109	#[pallet::storage]
110	pub type Members<T: Config<I>, I: 'static = ()> =
111		StorageValue<_, BoundedVec<T::AccountId, T::MaxMembers>, ValueQuery>;
112
113	/// The current prime member, if one exists.
114	#[pallet::storage]
115	pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
116
117	#[pallet::genesis_config]
118	#[derive(frame_support::DefaultNoBound)]
119	pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
120		pub members: BoundedVec<T::AccountId, T::MaxMembers>,
121		#[serde(skip)]
122		pub phantom: PhantomData<I>,
123	}
124
125	#[pallet::genesis_build]
126	impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
127		fn build(&self) {
128			use alloc::collections::btree_set::BTreeSet;
129			let members_set: BTreeSet<_> = self.members.iter().collect();
130			assert_eq!(
131				members_set.len(),
132				self.members.len(),
133				"Members cannot contain duplicate accounts."
134			);
135
136			let mut members = self.members.clone();
137			members.sort();
138			T::MembershipInitialized::initialize_members(&members);
139			Members::<T, I>::put(members);
140		}
141	}
142
143	#[pallet::event]
144	#[pallet::generate_deposit(pub(super) fn deposit_event)]
145	pub enum Event<T: Config<I>, I: 'static = ()> {
146		/// The given member was added; see the transaction for who.
147		MemberAdded,
148		/// The given member was removed; see the transaction for who.
149		MemberRemoved,
150		/// Two members were swapped; see the transaction for who.
151		MembersSwapped,
152		/// The membership was reset; see the transaction for who the new set is.
153		MembersReset,
154		/// One of the members' keys changed.
155		KeyChanged,
156		/// Phantom member, never used.
157		Dummy { _phantom_data: PhantomData<(T::AccountId, <T as Config<I>>::RuntimeEvent)> },
158	}
159
160	#[pallet::error]
161	pub enum Error<T, I = ()> {
162		/// Already a member.
163		AlreadyMember,
164		/// Not a member.
165		NotMember,
166		/// Too many members.
167		TooManyMembers,
168	}
169
170	#[pallet::call]
171	impl<T: Config<I>, I: 'static> Pallet<T, I> {
172		/// Add a member `who` to the set.
173		///
174		/// May only be called from `T::AddOrigin`.
175		#[pallet::call_index(0)]
176		#[pallet::weight(T::WeightInfo::add_member(T::MaxMembers::get()))]
177		pub fn add_member(
178			origin: OriginFor<T>,
179			who: AccountIdLookupOf<T>,
180		) -> DispatchResultWithPostInfo {
181			T::AddOrigin::ensure_origin(origin)?;
182			let who = T::Lookup::lookup(who)?;
183
184			let mut members = Members::<T, I>::get();
185			let init_length = members.len();
186			let location = members.binary_search(&who).err().ok_or(Error::<T, I>::AlreadyMember)?;
187			members
188				.try_insert(location, who.clone())
189				.map_err(|_| Error::<T, I>::TooManyMembers)?;
190
191			Members::<T, I>::put(&members);
192
193			T::MembershipChanged::change_members_sorted(&[who], &[], &members[..]);
194
195			Self::deposit_event(Event::MemberAdded);
196
197			Ok(Some(T::WeightInfo::add_member(init_length as u32)).into())
198		}
199
200		/// Remove a member `who` from the set.
201		///
202		/// May only be called from `T::RemoveOrigin`.
203		#[pallet::call_index(1)]
204		#[pallet::weight(T::WeightInfo::remove_member(T::MaxMembers::get()))]
205		pub fn remove_member(
206			origin: OriginFor<T>,
207			who: AccountIdLookupOf<T>,
208		) -> DispatchResultWithPostInfo {
209			T::RemoveOrigin::ensure_origin(origin)?;
210			let who = T::Lookup::lookup(who)?;
211
212			let mut members = Members::<T, I>::get();
213			let init_length = members.len();
214			let location = members.binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?;
215			members.remove(location);
216
217			Members::<T, I>::put(&members);
218
219			T::MembershipChanged::change_members_sorted(&[], &[who], &members[..]);
220			Self::rejig_prime(&members);
221
222			Self::deposit_event(Event::MemberRemoved);
223			Ok(Some(T::WeightInfo::remove_member(init_length as u32)).into())
224		}
225
226		/// Swap out one member `remove` for another `add`.
227		///
228		/// May only be called from `T::SwapOrigin`.
229		///
230		/// Prime membership is *not* passed from `remove` to `add`, if extant.
231		#[pallet::call_index(2)]
232		#[pallet::weight(T::WeightInfo::swap_member(T::MaxMembers::get()))]
233		pub fn swap_member(
234			origin: OriginFor<T>,
235			remove: AccountIdLookupOf<T>,
236			add: AccountIdLookupOf<T>,
237		) -> DispatchResultWithPostInfo {
238			T::SwapOrigin::ensure_origin(origin)?;
239			let remove = T::Lookup::lookup(remove)?;
240			let add = T::Lookup::lookup(add)?;
241
242			if remove == add {
243				return Ok(().into());
244			}
245
246			let mut members = Members::<T, I>::get();
247			let location = members.binary_search(&remove).ok().ok_or(Error::<T, I>::NotMember)?;
248			members.binary_search(&add).err().ok_or(Error::<T, I>::AlreadyMember)?;
249			members[location] = add.clone();
250			members.sort();
251
252			Members::<T, I>::put(&members);
253
254			T::MembershipChanged::change_members_sorted(&[add], &[remove], &members[..]);
255			Self::rejig_prime(&members);
256
257			Self::deposit_event(Event::MembersSwapped);
258			Ok(Some(T::WeightInfo::swap_member(members.len() as u32)).into())
259		}
260
261		/// Change the membership to a new set, disregarding the existing membership. Be nice and
262		/// pass `members` pre-sorted.
263		///
264		/// May only be called from `T::ResetOrigin`.
265		#[pallet::call_index(3)]
266		#[pallet::weight(T::WeightInfo::reset_members(members.len().unique_saturated_into()))]
267		pub fn reset_members(origin: OriginFor<T>, members: Vec<T::AccountId>) -> DispatchResult {
268			T::ResetOrigin::ensure_origin(origin)?;
269
270			let mut members: BoundedVec<T::AccountId, T::MaxMembers> =
271				BoundedVec::try_from(members).map_err(|_| Error::<T, I>::TooManyMembers)?;
272			members.sort();
273			Members::<T, I>::mutate(|m| {
274				T::MembershipChanged::set_members_sorted(&members[..], m);
275				Self::rejig_prime(&members);
276				*m = members;
277			});
278
279			Self::deposit_event(Event::MembersReset);
280			Ok(())
281		}
282
283		/// Swap out the sending member for some other key `new`.
284		///
285		/// May only be called from `Signed` origin of a current member.
286		///
287		/// Prime membership is passed from the origin account to `new`, if extant.
288		#[pallet::call_index(4)]
289		#[pallet::weight(T::WeightInfo::change_key(T::MaxMembers::get()))]
290		pub fn change_key(
291			origin: OriginFor<T>,
292			new: AccountIdLookupOf<T>,
293		) -> DispatchResultWithPostInfo {
294			let remove = ensure_signed(origin)?;
295			let new = T::Lookup::lookup(new)?;
296
297			if remove == new {
298				return Ok(().into());
299			}
300
301			let mut members = Members::<T, I>::get();
302			let members_length = members.len() as u32;
303			let location = members.binary_search(&remove).ok().ok_or(Error::<T, I>::NotMember)?;
304			members.binary_search(&new).err().ok_or(Error::<T, I>::AlreadyMember)?;
305			members[location] = new.clone();
306			members.sort();
307
308			Members::<T, I>::put(&members);
309
310			T::MembershipChanged::change_members_sorted(
311				&[new.clone()],
312				&[remove.clone()],
313				&members[..],
314			);
315
316			if Prime::<T, I>::get() == Some(remove) {
317				Prime::<T, I>::put(&new);
318				T::MembershipChanged::set_prime(Some(new));
319			}
320
321			Self::deposit_event(Event::KeyChanged);
322			Ok(Some(T::WeightInfo::change_key(members_length)).into())
323		}
324
325		/// Set the prime member. Must be a current member.
326		///
327		/// May only be called from `T::PrimeOrigin`.
328		#[pallet::call_index(5)]
329		#[pallet::weight(T::WeightInfo::set_prime(T::MaxMembers::get()))]
330		pub fn set_prime(
331			origin: OriginFor<T>,
332			who: AccountIdLookupOf<T>,
333		) -> DispatchResultWithPostInfo {
334			T::PrimeOrigin::ensure_origin(origin)?;
335			let who = T::Lookup::lookup(who)?;
336			let members = Members::<T, I>::get();
337			members.binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?;
338			Prime::<T, I>::put(&who);
339			T::MembershipChanged::set_prime(Some(who));
340			Ok(Some(T::WeightInfo::set_prime(members.len() as u32)).into())
341		}
342
343		/// Remove the prime member if it exists.
344		///
345		/// May only be called from `T::PrimeOrigin`.
346		#[pallet::call_index(6)]
347		#[pallet::weight(T::WeightInfo::clear_prime())]
348		pub fn clear_prime(origin: OriginFor<T>) -> DispatchResult {
349			T::PrimeOrigin::ensure_origin(origin)?;
350			Prime::<T, I>::kill();
351			T::MembershipChanged::set_prime(None);
352			Ok(())
353		}
354	}
355}
356
357impl<T: Config<I>, I: 'static> Pallet<T, I> {
358	/// The current membership, stored as an ordered `Vec`.
359	pub fn members() -> BoundedVec<T::AccountId, T::MaxMembers> {
360		Members::<T, I>::get()
361	}
362
363	/// The current prime member, if one exists.
364	pub fn prime() -> Option<T::AccountId> {
365		Prime::<T, I>::get()
366	}
367
368	fn rejig_prime(members: &[T::AccountId]) {
369		if let Some(prime) = Prime::<T, I>::get() {
370			match members.binary_search(&prime) {
371				Ok(_) => T::MembershipChanged::set_prime(Some(prime)),
372				Err(_) => Prime::<T, I>::kill(),
373			}
374		}
375	}
376}
377
378impl<T: Config<I>, I: 'static> Contains<T::AccountId> for Pallet<T, I> {
379	fn contains(t: &T::AccountId) -> bool {
380		Members::<T, I>::get().binary_search(t).is_ok()
381	}
382}
383
384impl<T: Config> ContainsLengthBound for Pallet<T> {
385	fn min_len() -> usize {
386		0
387	}
388
389	/// Implementation uses a parameter type so calling is cost-free.
390	fn max_len() -> usize {
391		T::MaxMembers::get() as usize
392	}
393}
394
395impl<T: Config<I>, I: 'static> SortedMembers<T::AccountId> for Pallet<T, I> {
396	fn sorted_members() -> Vec<T::AccountId> {
397		Members::<T, I>::get().to_vec()
398	}
399
400	fn count() -> usize {
401		Members::<T, I>::decode_len().unwrap_or(0)
402	}
403
404	#[cfg(feature = "runtime-benchmarks")]
405	fn add(new_member: &T::AccountId) {
406		use frame_support::{assert_ok, traits::EnsureOrigin};
407		let new_member_lookup = T::Lookup::unlookup(new_member.clone());
408
409		if let Ok(origin) = T::AddOrigin::try_successful_origin() {
410			assert_ok!(Pallet::<T, I>::add_member(origin, new_member_lookup,));
411		} else {
412			log::error!(target: LOG_TARGET, "Failed to add `{new_member:?}` in `SortedMembers::add`.")
413		}
414	}
415}