referrerpolicy=no-referrer-when-downgrade

pallet_scored_pool/
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//! # Scored Pool Pallet
19//!
20//! The pallet maintains a scored membership pool. Each entity in the
21//! pool can be attributed a `Score`. From this pool a set `Members`
22//! is constructed. This set contains the `MemberCount` highest
23//! scoring entities. Unscored entities are never part of `Members`.
24//!
25//! If an entity wants to be part of the pool a deposit is required.
26//! The deposit is returned when the entity withdraws or when it
27//! is removed by an entity with the appropriate authority.
28//!
29//! Every `Period` blocks the set of `Members` is refreshed from the
30//! highest scoring members in the pool and, no matter if changes
31//! occurred, `T::MembershipChanged::set_members_sorted` is invoked.
32//! On first load `T::MembershipInitialized::initialize_members` is
33//! invoked with the initial `Members` set.
34//!
35//! It is possible to withdraw candidacy/resign your membership at any
36//! time. If an entity is currently a member, this results in removal
37//! from the `Pool` and `Members`; the entity is immediately replaced
38//! by the next highest scoring candidate in the pool, if available.
39//!
40//! - [`Config`]
41//! - [`Call`]
42//! - [`Pallet`]
43//!
44//! ## Interface
45//!
46//! ### Public Functions
47//!
48//! - `submit_candidacy` - Submit candidacy to become a member. Requires a deposit.
49//! - `withdraw_candidacy` - Withdraw candidacy. Deposit is returned.
50//! - `score` - Attribute a quantitative score to an entity.
51//! - `kick` - Remove an entity from the pool and members. Deposit is returned.
52//! - `change_member_count` - Changes the amount of candidates taken into `Members`.
53//!
54//! ## Usage
55//!
56//! ```
57//! use pallet_scored_pool::{self as scored_pool};
58//!
59//! #[frame_support::pallet]
60//! pub mod pallet {
61//! 	use super::*;
62//! 	use frame_support::pallet_prelude::*;
63//! 	use frame_system::pallet_prelude::*;
64//!
65//! 	#[pallet::pallet]
66//! 	pub struct Pallet<T>(_);
67//!
68//! 	#[pallet::config]
69//! 	pub trait Config: frame_system::Config + scored_pool::Config {}
70//!
71//! 	#[pallet::call]
72//! 	impl<T: Config> Pallet<T> {
73//! 		#[pallet::weight({0})]
74//! 		pub fn candidate(origin: OriginFor<T>) -> DispatchResult {
75//! 			let who = ensure_signed(origin)?;
76//!
77//! 			let _ = <scored_pool::Pallet<T>>::submit_candidacy(
78//! 				T::RuntimeOrigin::from(Some(who.clone()).into())
79//! 			);
80//! 			Ok(())
81//! 		}
82//! 	}
83//! }
84//!
85//! # fn main() { }
86//! ```
87//!
88//! ## Dependencies
89//!
90//! This pallet depends on the [System pallet](../frame_system/index.html).
91
92// Ensure we're `no_std` when compiling for Wasm.
93#![cfg_attr(not(feature = "std"), no_std)]
94
95#[cfg(test)]
96// We do not declare all features used by `construct_runtime`
97#[allow(unexpected_cfgs)]
98mod mock;
99
100#[cfg(test)]
101mod tests;
102
103extern crate alloc;
104
105use alloc::vec::Vec;
106use codec::{FullCodec, MaxEncodedLen};
107use core::{cmp::Reverse, fmt::Debug};
108use frame_support::{
109	ensure,
110	traits::{ChangeMembers, Currency, Get, InitializeMembers, ReservableCurrency},
111	BoundedVec,
112};
113pub use pallet::*;
114use sp_runtime::traits::{AtLeast32Bit, StaticLookup, Zero};
115
116type BalanceOf<T, I> =
117	<<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
118type PoolT<T, I> = BoundedVec<
119	(<T as frame_system::Config>::AccountId, Option<<T as Config<I>>::Score>),
120	<T as Config<I>>::MaximumMembers,
121>;
122type MembersT<T, I> =
123	BoundedVec<<T as frame_system::Config>::AccountId, <T as Config<I>>::MaximumMembers>;
124type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
125
126/// The enum is supplied when refreshing the members set.
127/// Depending on the enum variant the corresponding associated
128/// type function will be invoked.
129enum ChangeReceiver {
130	/// Should call `T::MembershipInitialized`.
131	MembershipInitialized,
132	/// Should call `T::MembershipChanged`.
133	MembershipChanged,
134}
135
136#[frame_support::pallet]
137pub mod pallet {
138	use super::*;
139	use frame_support::pallet_prelude::*;
140	use frame_system::pallet_prelude::*;
141
142	#[pallet::pallet]
143	pub struct Pallet<T, I = ()>(_);
144
145	#[pallet::config]
146	pub trait Config<I: 'static = ()>: frame_system::Config {
147		/// The currency used for deposits.
148		type Currency: Currency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
149
150		/// Maximum members length allowed.
151		#[pallet::constant]
152		type MaximumMembers: Get<u32>;
153
154		/// The score attributed to a member or candidate.
155		type Score: AtLeast32Bit
156			+ Clone
157			+ Copy
158			+ Default
159			+ FullCodec
160			+ MaybeSerializeDeserialize
161			+ Debug
162			+ scale_info::TypeInfo
163			+ MaxEncodedLen;
164
165		/// The overarching event type.
166		#[allow(deprecated)]
167		type RuntimeEvent: From<Event<Self, I>>
168			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
169
170		// The deposit which is reserved from candidates if they want to
171		// start a candidacy. The deposit gets returned when the candidacy is
172		// withdrawn or when the candidate is kicked.
173		#[pallet::constant]
174		type CandidateDeposit: Get<BalanceOf<Self, I>>;
175
176		/// Every `Period` blocks the `Members` are filled with the highest scoring
177		/// members in the `Pool`.
178		#[pallet::constant]
179		type Period: Get<BlockNumberFor<Self>>;
180
181		/// The receiver of the signal for when the membership has been initialized.
182		/// This happens pre-genesis and will usually be the same as `MembershipChanged`.
183		/// If you need to do something different on initialization, then you can change
184		/// this accordingly.
185		type MembershipInitialized: InitializeMembers<Self::AccountId>;
186
187		/// The receiver of the signal for when the members have changed.
188		type MembershipChanged: ChangeMembers<Self::AccountId>;
189
190		/// Allows a configurable origin type to set a score to a candidate in the pool.
191		type ScoreOrigin: EnsureOrigin<Self::RuntimeOrigin>;
192
193		/// Required origin for removing a member (though can always be Root).
194		/// Configurable origin which enables removing an entity. If the entity
195		/// is part of the `Members` it is immediately replaced by the next
196		/// highest scoring candidate, if available.
197		type KickOrigin: EnsureOrigin<Self::RuntimeOrigin>;
198	}
199
200	#[pallet::event]
201	#[pallet::generate_deposit(pub(super) fn deposit_event)]
202	pub enum Event<T: Config<I>, I: 'static = ()> {
203		/// The given member was removed. See the transaction for who.
204		MemberRemoved,
205		/// An entity has issued a candidacy. See the transaction for who.
206		CandidateAdded,
207		/// An entity withdrew candidacy. See the transaction for who.
208		CandidateWithdrew,
209		/// The candidacy was forcefully removed for an entity.
210		/// See the transaction for who.
211		CandidateKicked,
212		/// A score was attributed to the candidate.
213		/// See the transaction for who.
214		CandidateScored,
215	}
216
217	/// Error for the scored-pool pallet.
218	#[pallet::error]
219	pub enum Error<T, I = ()> {
220		/// Already a member.
221		AlreadyInPool,
222		/// Index out of bounds.
223		InvalidIndex,
224		/// Index does not match requested account.
225		WrongAccountIndex,
226		/// Number of members exceeds `MaximumMembers`.
227		TooManyMembers,
228	}
229
230	/// The current pool of candidates, stored as an ordered Bounded Vec
231	/// (ordered descending by score, `None` last, highest first).
232	#[pallet::storage]
233	#[pallet::getter(fn pool)]
234	pub(crate) type Pool<T: Config<I>, I: 'static = ()> = StorageValue<_, PoolT<T, I>, ValueQuery>;
235
236	/// A Map of the candidates. The information in this Map is redundant
237	/// to the information in the `Pool`. But the Map enables us to easily
238	/// check if a candidate is already in the pool, without having to
239	/// iterate over the entire pool (the `Pool` is not sorted by
240	/// `T::AccountId`, but by `T::Score` instead).
241	#[pallet::storage]
242	#[pallet::getter(fn candidate_exists)]
243	pub(crate) type CandidateExists<T: Config<I>, I: 'static = ()> =
244		StorageMap<_, Twox64Concat, T::AccountId, bool, ValueQuery>;
245
246	/// The current membership, stored as an ordered Vec.
247	#[pallet::storage]
248	#[pallet::getter(fn members)]
249	pub(crate) type Members<T: Config<I>, I: 'static = ()> =
250		StorageValue<_, MembersT<T, I>, ValueQuery>;
251
252	/// Size of the `Members` set.
253	#[pallet::storage]
254	#[pallet::getter(fn member_count)]
255	pub(crate) type MemberCount<T, I = ()> = StorageValue<_, u32, ValueQuery>;
256
257	#[pallet::genesis_config]
258	#[derive(frame_support::DefaultNoBound)]
259	pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
260		pub pool: PoolT<T, I>,
261		pub member_count: u32,
262	}
263
264	#[pallet::genesis_build]
265	impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
266		fn build(&self) {
267			let mut pool = self.pool.clone();
268
269			// reserve balance for each candidate in the pool.
270			// panicking here is ok, since this just happens one time, pre-genesis.
271			pool.iter().for_each(|(who, _)| {
272				T::Currency::reserve(who, T::CandidateDeposit::get())
273					.expect("balance too low to create candidacy");
274				<CandidateExists<T, I>>::insert(who, true);
275			});
276
277			// Sorts the `Pool` by score in a descending order. Entities which
278			// have a score of `None` are sorted to the end of the bounded vec.
279			pool.sort_by_key(|(_, maybe_score)| Reverse(maybe_score.unwrap_or_default()));
280			<Pallet<T, I>>::update_member_count(self.member_count)
281				.expect("Number of allowed members exceeded");
282			<Pool<T, I>>::put(&pool);
283			<Pallet<T, I>>::refresh_members(pool, ChangeReceiver::MembershipInitialized);
284		}
285	}
286
287	#[pallet::hooks]
288	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
289		/// Every `Period` blocks the `Members` set is refreshed from the
290		/// highest scoring members in the pool.
291		fn on_initialize(n: frame_system::pallet_prelude::BlockNumberFor<T>) -> Weight {
292			if n % T::Period::get() == Zero::zero() {
293				let pool = <Pool<T, I>>::get();
294				<Pallet<T, I>>::refresh_members(pool, ChangeReceiver::MembershipChanged);
295			}
296			Weight::zero()
297		}
298	}
299
300	#[pallet::call]
301	impl<T: Config<I>, I: 'static> Pallet<T, I> {
302		/// Add `origin` to the pool of candidates.
303		///
304		/// This results in `CandidateDeposit` being reserved from
305		/// the `origin` account. The deposit is returned once
306		/// candidacy is withdrawn by the candidate or the entity
307		/// is kicked by `KickOrigin`.
308		///
309		/// The dispatch origin of this function must be signed.
310		///
311		/// The `index` parameter of this function must be set to
312		/// the index of the transactor in the `Pool`.
313		#[pallet::call_index(0)]
314		#[pallet::weight({0})]
315		pub fn submit_candidacy(origin: OriginFor<T>) -> DispatchResult {
316			let who = ensure_signed(origin)?;
317			ensure!(!<CandidateExists<T, I>>::contains_key(&who), Error::<T, I>::AlreadyInPool);
318
319			let deposit = T::CandidateDeposit::get();
320			T::Currency::reserve(&who, deposit)?;
321
322			// can be inserted as last element in pool, since entities with
323			// `None` are always sorted to the end.
324			<Pool<T, I>>::try_append((who.clone(), Option::<<T as Config<I>>::Score>::None))
325				.map_err(|_| Error::<T, I>::TooManyMembers)?;
326
327			<CandidateExists<T, I>>::insert(&who, true);
328
329			Self::deposit_event(Event::<T, I>::CandidateAdded);
330			Ok(())
331		}
332
333		/// An entity withdraws candidacy and gets its deposit back.
334		///
335		/// If the entity is part of the `Members`, then the highest member
336		/// of the `Pool` that is not currently in `Members` is immediately
337		/// placed in the set instead.
338		///
339		/// The dispatch origin of this function must be signed.
340		///
341		/// The `index` parameter of this function must be set to
342		/// the index of the transactor in the `Pool`.
343		#[pallet::call_index(1)]
344		#[pallet::weight({0})]
345		pub fn withdraw_candidacy(origin: OriginFor<T>, index: u32) -> DispatchResult {
346			let who = ensure_signed(origin)?;
347
348			let pool = <Pool<T, I>>::get();
349			Self::ensure_index(&pool, &who, index)?;
350
351			Self::remove_member(pool, who, index)?;
352			Self::deposit_event(Event::<T, I>::CandidateWithdrew);
353			Ok(())
354		}
355
356		/// Kick a member `who` from the set.
357		///
358		/// May only be called from `T::KickOrigin`.
359		///
360		/// The `index` parameter of this function must be set to
361		/// the index of `dest` in the `Pool`.
362		#[pallet::call_index(2)]
363		#[pallet::weight({0})]
364		pub fn kick(
365			origin: OriginFor<T>,
366			dest: AccountIdLookupOf<T>,
367			index: u32,
368		) -> DispatchResult {
369			T::KickOrigin::ensure_origin(origin)?;
370
371			let who = T::Lookup::lookup(dest)?;
372
373			let pool = <Pool<T, I>>::get();
374			Self::ensure_index(&pool, &who, index)?;
375
376			Self::remove_member(pool, who, index)?;
377			Self::deposit_event(Event::<T, I>::CandidateKicked);
378			Ok(())
379		}
380
381		/// Score a member `who` with `score`.
382		///
383		/// May only be called from `T::ScoreOrigin`.
384		///
385		/// The `index` parameter of this function must be set to
386		/// the index of the `dest` in the `Pool`.
387		#[pallet::call_index(3)]
388		#[pallet::weight({0})]
389		pub fn score(
390			origin: OriginFor<T>,
391			dest: AccountIdLookupOf<T>,
392			index: u32,
393			score: T::Score,
394		) -> DispatchResult {
395			T::ScoreOrigin::ensure_origin(origin)?;
396
397			let who = T::Lookup::lookup(dest)?;
398
399			let mut pool = <Pool<T, I>>::get();
400			Self::ensure_index(&pool, &who, index)?;
401
402			pool.remove(index as usize);
403
404			// we binary search the pool (which is sorted descending by score).
405			// if there is already an element with `score`, we insert
406			// right before that. if not, the search returns a location
407			// where we can insert while maintaining order.
408			let item = (who, Some(score));
409			let location = pool
410				.binary_search_by_key(&Reverse(score), |(_, maybe_score)| {
411					Reverse(maybe_score.unwrap_or_default())
412				})
413				.unwrap_or_else(|l| l);
414			pool.try_insert(location, item).map_err(|_| Error::<T, I>::TooManyMembers)?;
415
416			<Pool<T, I>>::put(&pool);
417			Self::deposit_event(Event::<T, I>::CandidateScored);
418			Ok(())
419		}
420
421		/// Dispatchable call to change `MemberCount`.
422		///
423		/// This will only have an effect the next time a refresh happens
424		/// (this happens each `Period`).
425		///
426		/// May only be called from root.
427		#[pallet::call_index(4)]
428		#[pallet::weight({0})]
429		pub fn change_member_count(origin: OriginFor<T>, count: u32) -> DispatchResult {
430			ensure_root(origin)?;
431			Self::update_member_count(count).map_err(Into::into)
432		}
433	}
434}
435
436impl<T: Config<I>, I: 'static> Pallet<T, I> {
437	/// Fetches the `MemberCount` highest scoring members from
438	/// `Pool` and puts them into `Members`.
439	///
440	/// The `notify` parameter is used to deduct which associated
441	/// type function to invoke at the end of the method.
442	fn refresh_members(pool: PoolT<T, I>, notify: ChangeReceiver) {
443		let count = MemberCount::<T, I>::get();
444		let old_members = <Members<T, I>>::get();
445
446		let new_members: Vec<T::AccountId> = pool
447			.into_iter()
448			.filter(|(_, score)| score.is_some())
449			.take(count as usize)
450			.map(|(account_id, _)| account_id)
451			.collect();
452
453		// It's safe to truncate_from at this point since MemberCount
454		// is verified that it does not exceed the MaximumMembers value
455		let mut new_members_bounded: MembersT<T, I> = BoundedVec::truncate_from(new_members);
456
457		new_members_bounded.sort();
458
459		<Members<T, I>>::put(&new_members_bounded);
460
461		match notify {
462			ChangeReceiver::MembershipInitialized =>
463				T::MembershipInitialized::initialize_members(&new_members_bounded),
464			ChangeReceiver::MembershipChanged =>
465				T::MembershipChanged::set_members_sorted(&new_members_bounded[..], &old_members[..]),
466		}
467	}
468
469	/// Removes an entity `remove` at `index` from the `Pool`.
470	///
471	/// If the entity is a member it is also removed from `Members` and
472	/// the deposit is returned.
473	fn remove_member(
474		mut pool: PoolT<T, I>,
475		remove: T::AccountId,
476		index: u32,
477	) -> Result<(), Error<T, I>> {
478		// all callers of this function in this pallet also check
479		// the index for validity before calling this function.
480		// nevertheless we check again here, to assert that there was
481		// no mistake when invoking this sensible function.
482		Self::ensure_index(&pool, &remove, index)?;
483
484		pool.remove(index as usize);
485		<Pool<T, I>>::put(&pool);
486
487		// remove from set, if it was in there
488		let members = <Members<T, I>>::get();
489		if members.binary_search(&remove).is_ok() {
490			Self::refresh_members(pool, ChangeReceiver::MembershipChanged);
491		}
492
493		<CandidateExists<T, I>>::remove(&remove);
494
495		T::Currency::unreserve(&remove, T::CandidateDeposit::get());
496
497		Self::deposit_event(Event::<T, I>::MemberRemoved);
498		Ok(())
499	}
500
501	/// Checks if `index` is a valid number and if the element found
502	/// at `index` in `Pool` is equal to `who`.
503	fn ensure_index(pool: &PoolT<T, I>, who: &T::AccountId, index: u32) -> Result<(), Error<T, I>> {
504		ensure!(index < pool.len() as u32, Error::<T, I>::InvalidIndex);
505
506		let (index_who, _index_score) = &pool[index as usize];
507		ensure!(index_who == who, Error::<T, I>::WrongAccountIndex);
508
509		Ok(())
510	}
511
512	/// Make sure the new member count value does not exceed the MaximumMembers
513	fn update_member_count(new_member_count: u32) -> Result<(), Error<T, I>> {
514		ensure!(new_member_count <= T::MaximumMembers::get(), Error::<T, I>::TooManyMembers);
515		<MemberCount<T, I>>::put(new_member_count);
516		Ok(())
517	}
518}