referrerpolicy=no-referrer-when-downgrade

pallet_core_fellowship/
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//! Additional logic for the Core Fellowship. This determines salary, registers activity/passivity
19//! and handles promotion and demotion periods.
20//!
21//! This only handles members of non-zero rank.
22//!
23//! # Process Flow
24//!
25//! - Begin with a call to `induct`, where some privileged origin (perhaps a pre-existing member of
26//!   `rank > 1`) is able to make a candidate from an account and introduce it to be tracked in this
27//!   pallet in order to allow evidence to be submitted and promotion voted on.
28//! - The candidate then calls `submit_evidence` to apply for their promotion to rank 1.
29//! - A `PromoteOrigin` of at least rank 1 calls `promote` on the candidate to elevate it to rank 1.
30//! - Some time later but before rank 1's `demotion_period` elapses, candidate calls
31//!   `submit_evidence` with evidence of their efforts to apply for approval to stay at rank 1.
32//! - An `ApproveOrigin` of at least rank 1 calls `approve` on the candidate to avoid imminent
33//!   demotion and keep it at rank 1.
34//! - These last two steps continue until the candidate is ready to apply for a promotion, at which
35//!   point the previous two steps are repeated with a higher rank.
36//! - If the member fails to get an approval within the `demotion_period` then anyone may call
37//!   `bump` to demote the candidate by one rank.
38//! - If a candidate fails to be promoted to a member within the `offboard_timeout` period, then
39//!   anyone may call `bump` to remove the account's candidacy.
40//! - Pre-existing members may call `import_member` on themselves (formerly `import`) to have their
41//!   rank recognised and be inducted into this pallet (to gain a salary and allow for eventual
42//!   promotion).
43//! - If, externally to this pallet, a member or candidate has their rank removed completely, then
44//!   `offboard` may be called to remove them entirely from this pallet.
45//!
46//! Note there is a difference between having a rank of 0 (whereby the account is a *candidate*) and
47//! having no rank at all (whereby we consider it *unranked*). An account can be demoted from rank
48//! 0 to become unranked. This process is called being offboarded and there is an extrinsic to do
49//! this explicitly when external factors to this pallet have caused the tracked account to become
50//! unranked. At rank 0, there is not a "demotion" period after which the account may be bumped to
51//! become offboarded but rather an "offboard timeout".
52//!
53//! Candidates may be introduced (i.e. an account to go from unranked to rank of 0) by an origin
54//! of a different privilege to that for promotion. This allows the possibility for even a single
55//! existing member to introduce a new candidate without payment.
56//!
57//! Only tracked/ranked accounts may submit evidence for their proof and promotion. Candidates
58//! cannot be approved - they must proceed only to promotion prior to the offboard timeout elapsing.
59
60#![cfg_attr(not(feature = "std"), no_std)]
61
62extern crate alloc;
63
64use alloc::boxed::Box;
65use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
66use core::{fmt::Debug, marker::PhantomData};
67use scale_info::TypeInfo;
68use sp_arithmetic::traits::{Saturating, Zero};
69
70use frame_support::{
71	defensive,
72	dispatch::DispatchResultWithPostInfo,
73	ensure, impl_ensure_origin_with_arg_ignoring_arg,
74	traits::{
75		tokens::Balance as BalanceTrait, EnsureOrigin, EnsureOriginWithArg, Get, RankedMembers,
76		RankedMembersSwapHandler,
77	},
78	BoundedVec, CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound,
79};
80
81#[cfg(test)]
82mod tests;
83
84#[cfg(feature = "runtime-benchmarks")]
85mod benchmarking;
86pub mod migration;
87pub mod weights;
88
89pub use pallet::*;
90pub use weights::*;
91
92/// The desired outcome for which evidence is presented.
93#[derive(
94	Encode,
95	Decode,
96	DecodeWithMemTracking,
97	Eq,
98	PartialEq,
99	Copy,
100	Clone,
101	TypeInfo,
102	MaxEncodedLen,
103	Debug,
104)]
105pub enum Wish {
106	/// Member wishes only to retain their current rank.
107	Retention,
108	/// Member wishes to be promoted.
109	Promotion,
110}
111
112/// A piece of evidence to underpin a [Wish].
113///
114/// From the pallet's perspective, this is just a blob of data without meaning. The fellows can
115/// decide how to concretely utilise it. This could be an IPFS hash, a URL or structured data.
116pub type Evidence<T, I> = BoundedVec<u8, <T as Config<I>>::EvidenceSize>;
117
118/// The status of the pallet instance.
119#[derive(
120	Encode,
121	Decode,
122	DecodeWithMemTracking,
123	CloneNoBound,
124	EqNoBound,
125	PartialEqNoBound,
126	DebugNoBound,
127	TypeInfo,
128	MaxEncodedLen,
129)]
130#[scale_info(skip_type_params(Ranks))]
131pub struct ParamsType<
132	Balance: Clone + Eq + PartialEq + Debug,
133	BlockNumber: Clone + Eq + PartialEq + Debug,
134	Ranks: Get<u32>,
135> {
136	/// The amounts to be paid when a member of a given rank (-1) is active.
137	pub active_salary: BoundedVec<Balance, Ranks>,
138	/// The amounts to be paid when a member of a given rank (-1) is passive.
139	pub passive_salary: BoundedVec<Balance, Ranks>,
140	/// The period between which unproven members become demoted.
141	pub demotion_period: BoundedVec<BlockNumber, Ranks>,
142	/// The period between which members must wait before they may proceed to this rank.
143	pub min_promotion_period: BoundedVec<BlockNumber, Ranks>,
144	/// Amount by which an account can remain at rank 0 (candidate before being offboard entirely).
145	pub offboard_timeout: BlockNumber,
146}
147
148impl<
149		Balance: Default + Copy + Eq + Debug,
150		BlockNumber: Default + Copy + Eq + Debug,
151		Ranks: Get<u32>,
152	> Default for ParamsType<Balance, BlockNumber, Ranks>
153{
154	fn default() -> Self {
155		Self {
156			active_salary: Default::default(),
157			passive_salary: Default::default(),
158			demotion_period: Default::default(),
159			min_promotion_period: Default::default(),
160			offboard_timeout: BlockNumber::default(),
161		}
162	}
163}
164
165pub struct ConvertU16ToU32<Inner>(PhantomData<Inner>);
166impl<Inner: Get<u16>> Get<u32> for ConvertU16ToU32<Inner> {
167	fn get() -> u32 {
168		Inner::get() as u32
169	}
170}
171
172/// The status of a single member.
173#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, Debug)]
174pub struct MemberStatus<BlockNumber> {
175	/// Are they currently active?
176	pub is_active: bool,
177	/// The block number at which we last promoted them.
178	pub last_promotion: BlockNumber,
179	/// The last time a member was demoted, promoted or proved their rank.
180	pub last_proof: BlockNumber,
181}
182
183#[frame_support::pallet]
184pub mod pallet {
185	use super::*;
186	use frame_support::{
187		dispatch::Pays,
188		pallet_prelude::*,
189		traits::{tokens::GetSalary, EnsureOrigin},
190	};
191	use frame_system::{ensure_root, pallet_prelude::*};
192	use sp_runtime::traits::BlockNumberProvider;
193	/// The in-code storage version.
194	const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
195
196	#[pallet::pallet]
197	#[pallet::storage_version(STORAGE_VERSION)]
198	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
199
200	#[pallet::config]
201	pub trait Config<I: 'static = ()>: frame_system::Config {
202		/// Weight information for extrinsics in this pallet.
203		type WeightInfo: WeightInfo;
204
205		/// The runtime event type.
206		#[allow(deprecated)]
207		type RuntimeEvent: From<Event<Self, I>>
208			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
209
210		/// The current membership of the fellowship.
211		type Members: RankedMembers<
212			AccountId = <Self as frame_system::Config>::AccountId,
213			Rank = u16,
214		>;
215
216		/// The type in which salaries/budgets are measured.
217		type Balance: BalanceTrait;
218
219		/// The origin which has permission update the parameters.
220		type ParamsOrigin: EnsureOrigin<Self::RuntimeOrigin>;
221
222		/// The origin which has permission to move a candidate into being tracked in this pallet.
223		/// Generally a very low-permission, such as a pre-existing member of rank 1 or above.
224		///
225		/// This allows the candidate to deposit evidence for their request to be promoted to a
226		/// member.
227		type InductOrigin: EnsureOrigin<Self::RuntimeOrigin>;
228
229		/// The origin which has permission to issue a proof that a member may retain their rank.
230		/// The `Success` value is the maximum rank of members it is able to prove.
231		type ApproveOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = RankOf<Self, I>>;
232
233		/// The origin which has permission to promote a member. The `Success` value is the maximum
234		/// rank to which it can promote.
235		type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = RankOf<Self, I>>;
236
237		/// The origin that has permission to "fast" promote a member by ignoring promotion periods
238		/// and skipping ranks. The `Success` value is the maximum rank to which it can promote.
239		type FastPromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = RankOf<Self, I>>;
240
241		/// The maximum size in bytes submitted evidence is allowed to be.
242		#[pallet::constant]
243		type EvidenceSize: Get<u32>;
244
245		/// Represents the highest possible rank in this pallet.
246		///
247		/// Increasing this value is supported, but decreasing it may lead to a broken state.
248		#[pallet::constant]
249		type MaxRank: Get<u16>;
250
251		/// Provides the current block number.
252		///
253		/// This is usually `cumulus_pallet_parachain_system::RelaychainDataProvider` if a
254		/// parachain, or `frame_system::Pallet` if a solo- or relaychain.
255		type BlockNumberProvider: BlockNumberProvider;
256	}
257
258	pub type BlockNumberFor<T, I = ()> =
259		<<T as Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
260	pub type ParamsOf<T, I> = ParamsType<
261		<T as Config<I>>::Balance,
262		BlockNumberFor<T, I>,
263		ConvertU16ToU32<<T as Config<I>>::MaxRank>,
264	>;
265	pub type PartialParamsOf<T, I> = ParamsType<
266		Option<<T as Config<I>>::Balance>,
267		Option<BlockNumberFor<T, I>>,
268		ConvertU16ToU32<<T as Config<I>>::MaxRank>,
269	>;
270	pub type MemberStatusOf<T, I> = MemberStatus<BlockNumberFor<T, I>>;
271	pub type RankOf<T, I> = <<T as Config<I>>::Members as RankedMembers>::Rank;
272
273	/// The overall status of the system.
274	#[pallet::storage]
275	pub type Params<T: Config<I>, I: 'static = ()> = StorageValue<_, ParamsOf<T, I>, ValueQuery>;
276
277	/// The status of a claimant.
278	#[pallet::storage]
279	pub type Member<T: Config<I>, I: 'static = ()> =
280		StorageMap<_, Twox64Concat, T::AccountId, MemberStatusOf<T, I>, OptionQuery>;
281
282	/// Some evidence together with the desired outcome for which it was presented.
283	#[pallet::storage]
284	pub type MemberEvidence<T: Config<I>, I: 'static = ()> =
285		StorageMap<_, Twox64Concat, T::AccountId, (Wish, Evidence<T, I>), OptionQuery>;
286
287	#[pallet::event]
288	#[pallet::generate_deposit(pub(super) fn deposit_event)]
289	pub enum Event<T: Config<I>, I: 'static = ()> {
290		/// Parameters for the pallet have changed.
291		ParamsChanged { params: ParamsOf<T, I> },
292		/// Member activity flag has been set.
293		ActiveChanged { who: T::AccountId, is_active: bool },
294		/// Member has begun being tracked in this pallet.
295		Inducted { who: T::AccountId },
296		/// Member has been removed from being tracked in this pallet (i.e. because rank is now
297		/// zero).
298		Offboarded { who: T::AccountId },
299		/// Member has been promoted to the given rank.
300		Promoted { who: T::AccountId, to_rank: RankOf<T, I> },
301		/// Member has been demoted to the given (non-zero) rank.
302		Demoted { who: T::AccountId, to_rank: RankOf<T, I> },
303		/// Member has been proven at their current rank, postponing auto-demotion.
304		Proven { who: T::AccountId, at_rank: RankOf<T, I> },
305		/// Member has stated evidence of their efforts their request for rank.
306		Requested { who: T::AccountId, wish: Wish },
307		/// Some submitted evidence was judged and removed. There may or may not have been a change
308		/// to the rank, but in any case, `last_proof` is reset.
309		EvidenceJudged {
310			/// The member/candidate.
311			who: T::AccountId,
312			/// The desired outcome for which the evidence was presented.
313			wish: Wish,
314			/// The evidence of efforts.
315			evidence: Evidence<T, I>,
316			/// The old rank, prior to this change.
317			old_rank: u16,
318			/// New rank. If `None` then candidate record was removed entirely.
319			new_rank: Option<u16>,
320		},
321		/// Pre-ranked account has been inducted at their current rank.
322		Imported { who: T::AccountId, rank: RankOf<T, I> },
323		/// A member had its AccountId swapped.
324		Swapped { who: T::AccountId, new_who: T::AccountId },
325	}
326
327	#[pallet::error]
328	pub enum Error<T, I = ()> {
329		/// Member's rank is too low.
330		Unranked,
331		/// Member's rank is not zero.
332		Ranked,
333		/// Member's rank is not as expected - generally means that the rank provided to the call
334		/// does not agree with the state of the system.
335		UnexpectedRank,
336		/// The given rank is invalid - this generally means it's not between 1 and `RANK_COUNT`.
337		InvalidRank,
338		/// The origin does not have enough permission to do this operation.
339		NoPermission,
340		/// No work needs to be done at present for this member.
341		NothingDoing,
342		/// The candidate has already been inducted. This should never happen since it would
343		/// require a candidate (rank 0) to already be tracked in the pallet.
344		AlreadyInducted,
345		/// The candidate has not been inducted, so cannot be offboarded from this pallet.
346		NotTracked,
347		/// Operation cannot be done yet since not enough time has passed.
348		TooSoon,
349	}
350
351	#[pallet::call]
352	impl<T: Config<I>, I: 'static> Pallet<T, I> {
353		/// Bump the state of a member.
354		///
355		/// This will demote a member whose `last_proof` is now beyond their rank's
356		/// `demotion_period`.
357		///
358		/// - `origin`: A `Signed` origin of an account.
359		/// - `who`: A member account whose state is to be updated.
360		#[pallet::weight(T::WeightInfo::bump_offboard().max(T::WeightInfo::bump_demote()))]
361		#[pallet::call_index(0)]
362		pub fn bump(origin: OriginFor<T>, who: T::AccountId) -> DispatchResultWithPostInfo {
363			ensure_signed(origin)?;
364			let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
365			let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
366
367			let params = Params::<T, I>::get();
368			let demotion_period = if rank == 0 {
369				params.offboard_timeout
370			} else {
371				let rank_index = Self::rank_to_index(rank).ok_or(Error::<T, I>::InvalidRank)?;
372				params.demotion_period[rank_index]
373			};
374
375			if demotion_period.is_zero() {
376				return Err(Error::<T, I>::NothingDoing.into());
377			}
378
379			let demotion_block = member.last_proof.saturating_add(demotion_period);
380
381			// Ensure enough time has passed.
382			let now = T::BlockNumberProvider::current_block_number();
383			if now >= demotion_block {
384				T::Members::demote(&who)?;
385				let maybe_to_rank = T::Members::rank_of(&who);
386				Self::dispose_evidence(who.clone(), rank, maybe_to_rank);
387				let event = if let Some(to_rank) = maybe_to_rank {
388					member.last_proof = now;
389					Member::<T, I>::insert(&who, &member);
390					Event::<T, I>::Demoted { who, to_rank }
391				} else {
392					Member::<T, I>::remove(&who);
393					Event::<T, I>::Offboarded { who }
394				};
395				Self::deposit_event(event);
396				return Ok(Pays::No.into());
397			}
398
399			Err(Error::<T, I>::NothingDoing.into())
400		}
401
402		/// Set the parameters.
403		///
404		/// - `origin`: An origin complying with `ParamsOrigin` or root.
405		/// - `params`: The new parameters for the pallet.
406		#[pallet::weight(T::WeightInfo::set_params())]
407		#[pallet::call_index(1)]
408		pub fn set_params(origin: OriginFor<T>, params: Box<ParamsOf<T, I>>) -> DispatchResult {
409			T::ParamsOrigin::ensure_origin_or_root(origin)?;
410
411			Params::<T, I>::put(params.as_ref());
412			Self::deposit_event(Event::<T, I>::ParamsChanged { params: *params });
413
414			Ok(())
415		}
416
417		/// Set whether a member is active or not.
418		///
419		/// - `origin`: A `Signed` origin of a member's account.
420		/// - `is_active`: `true` iff the member is active.
421		#[pallet::weight(T::WeightInfo::set_active())]
422		#[pallet::call_index(2)]
423		pub fn set_active(origin: OriginFor<T>, is_active: bool) -> DispatchResult {
424			let who = ensure_signed(origin)?;
425			ensure!(
426				T::Members::rank_of(&who).map_or(false, |r| !r.is_zero()),
427				Error::<T, I>::Unranked
428			);
429			let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
430			member.is_active = is_active;
431			Member::<T, I>::insert(&who, &member);
432			Self::deposit_event(Event::<T, I>::ActiveChanged { who, is_active });
433			Ok(())
434		}
435
436		/// Approve a member to continue at their rank.
437		///
438		/// This resets `last_proof` to the current block, thereby delaying any automatic demotion.
439		///
440		/// `who` must already be tracked by this pallet for this to have an effect.
441		///
442		/// - `origin`: An origin which satisfies `ApproveOrigin` or root.
443		/// - `who`: A member (i.e. of non-zero rank).
444		/// - `at_rank`: The rank of member.
445		#[pallet::weight(T::WeightInfo::approve())]
446		#[pallet::call_index(3)]
447		pub fn approve(
448			origin: OriginFor<T>,
449			who: T::AccountId,
450			at_rank: RankOf<T, I>,
451		) -> DispatchResult {
452			match T::ApproveOrigin::try_origin(origin) {
453				Ok(allow_rank) => ensure!(allow_rank >= at_rank, Error::<T, I>::NoPermission),
454				Err(origin) => ensure_root(origin)?,
455			}
456			ensure!(at_rank > 0, Error::<T, I>::InvalidRank);
457			let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
458			ensure!(rank == at_rank, Error::<T, I>::UnexpectedRank);
459			let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
460
461			member.last_proof = T::BlockNumberProvider::current_block_number();
462			Member::<T, I>::insert(&who, &member);
463
464			Self::dispose_evidence(who.clone(), at_rank, Some(at_rank));
465			Self::deposit_event(Event::<T, I>::Proven { who, at_rank });
466
467			Ok(())
468		}
469
470		/// Introduce a new and unranked candidate (rank zero).
471		///
472		/// - `origin`: An origin which satisfies `InductOrigin` or root.
473		/// - `who`: The account ID of the candidate to be inducted and become a member.
474		#[pallet::weight(T::WeightInfo::induct())]
475		#[pallet::call_index(4)]
476		pub fn induct(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
477			match T::InductOrigin::try_origin(origin) {
478				Ok(_) => {},
479				Err(origin) => ensure_root(origin)?,
480			}
481			ensure!(!Member::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
482			ensure!(T::Members::rank_of(&who).is_none(), Error::<T, I>::Ranked);
483
484			T::Members::induct(&who)?;
485			let now = T::BlockNumberProvider::current_block_number();
486			Member::<T, I>::insert(
487				&who,
488				MemberStatus { is_active: true, last_promotion: now, last_proof: now },
489			);
490			Self::deposit_event(Event::<T, I>::Inducted { who });
491			Ok(())
492		}
493
494		/// Increment the rank of a ranked and tracked account.
495		///
496		/// - `origin`: An origin which satisfies `PromoteOrigin` with a `Success` result of
497		///   `to_rank` or more or root.
498		/// - `who`: The account ID of the member to be promoted to `to_rank`.
499		/// - `to_rank`: One more than the current rank of `who`.
500		#[pallet::weight(T::WeightInfo::promote())]
501		#[pallet::call_index(5)]
502		pub fn promote(
503			origin: OriginFor<T>,
504			who: T::AccountId,
505			to_rank: RankOf<T, I>,
506		) -> DispatchResult {
507			match T::PromoteOrigin::try_origin(origin) {
508				Ok(allow_rank) => ensure!(allow_rank >= to_rank, Error::<T, I>::NoPermission),
509				Err(origin) => ensure_root(origin)?,
510			}
511			let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
512			ensure!(
513				rank.checked_add(1).map_or(false, |i| i == to_rank),
514				Error::<T, I>::UnexpectedRank
515			);
516
517			let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
518			let now = T::BlockNumberProvider::current_block_number();
519
520			let params = Params::<T, I>::get();
521			let rank_index = Self::rank_to_index(to_rank).ok_or(Error::<T, I>::InvalidRank)?;
522			let min_period = params.min_promotion_period[rank_index];
523			// Ensure enough time has passed.
524			ensure!(
525				member.last_promotion.saturating_add(min_period) <= now,
526				Error::<T, I>::TooSoon,
527			);
528
529			T::Members::promote(&who)?;
530			member.last_promotion = now;
531			member.last_proof = now;
532			Member::<T, I>::insert(&who, &member);
533			Self::dispose_evidence(who.clone(), rank, Some(to_rank));
534
535			Self::deposit_event(Event::<T, I>::Promoted { who, to_rank });
536
537			Ok(())
538		}
539
540		/// Fast promotions can skip ranks and ignore the `min_promotion_period`.
541		///
542		/// This is useful for out-of-band promotions, hence it has its own `FastPromoteOrigin` to
543		/// be (possibly) more restrictive than `PromoteOrigin`. Note that the member must already
544		/// be inducted.
545		#[pallet::weight(T::WeightInfo::promote_fast(*to_rank as u32))]
546		#[pallet::call_index(10)]
547		pub fn promote_fast(
548			origin: OriginFor<T>,
549			who: T::AccountId,
550			to_rank: RankOf<T, I>,
551		) -> DispatchResult {
552			match T::FastPromoteOrigin::try_origin(origin) {
553				Ok(allow_rank) => ensure!(allow_rank >= to_rank, Error::<T, I>::NoPermission),
554				Err(origin) => ensure_root(origin)?,
555			}
556			ensure!(to_rank <= T::MaxRank::get(), Error::<T, I>::InvalidRank);
557			let curr_rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
558			ensure!(to_rank > curr_rank, Error::<T, I>::UnexpectedRank);
559
560			let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
561			let now = T::BlockNumberProvider::current_block_number();
562			member.last_promotion = now;
563			member.last_proof = now;
564
565			for rank in (curr_rank + 1)..=to_rank {
566				T::Members::promote(&who)?;
567
568				// NOTE: We could factor this out, but it would destroy our invariants:
569				Member::<T, I>::insert(&who, &member);
570
571				Self::dispose_evidence(who.clone(), rank.saturating_sub(1), Some(rank));
572				Self::deposit_event(Event::<T, I>::Promoted { who: who.clone(), to_rank: rank });
573			}
574
575			Ok(())
576		}
577
578		/// Stop tracking a prior member who is now not a ranked member of the collective.
579		///
580		/// - `origin`: A `Signed` origin of an account.
581		/// - `who`: The ID of an account which was tracked in this pallet but which is now not a
582		///   ranked member of the collective.
583		#[pallet::weight(T::WeightInfo::offboard())]
584		#[pallet::call_index(6)]
585		pub fn offboard(origin: OriginFor<T>, who: T::AccountId) -> DispatchResultWithPostInfo {
586			ensure_signed(origin)?;
587			ensure!(T::Members::rank_of(&who).is_none(), Error::<T, I>::Ranked);
588			ensure!(Member::<T, I>::contains_key(&who), Error::<T, I>::NotTracked);
589			Member::<T, I>::remove(&who);
590			MemberEvidence::<T, I>::remove(&who);
591			Self::deposit_event(Event::<T, I>::Offboarded { who });
592			Ok(Pays::No.into())
593		}
594
595		/// Provide evidence that a rank is deserved.
596		///
597		/// This is free as long as no evidence for the forthcoming judgement is already submitted.
598		/// Evidence is cleared after an outcome (either demotion, promotion of approval).
599		///
600		/// - `origin`: A `Signed` origin of an inducted and ranked account.
601		/// - `wish`: The stated desire of the member.
602		/// - `evidence`: A dump of evidence to be considered. This should generally be either a
603		///   Markdown-encoded document or a series of 32-byte hashes which can be found on a
604		///   decentralised content-based-indexing system such as IPFS.
605		#[pallet::weight(T::WeightInfo::submit_evidence())]
606		#[pallet::call_index(7)]
607		pub fn submit_evidence(
608			origin: OriginFor<T>,
609			wish: Wish,
610			evidence: Evidence<T, I>,
611		) -> DispatchResultWithPostInfo {
612			let who = ensure_signed(origin)?;
613			ensure!(Member::<T, I>::contains_key(&who), Error::<T, I>::NotTracked);
614			let replaced = MemberEvidence::<T, I>::contains_key(&who);
615			MemberEvidence::<T, I>::insert(&who, (wish, evidence));
616			Self::deposit_event(Event::<T, I>::Requested { who, wish });
617			Ok(if replaced { Pays::Yes } else { Pays::No }.into())
618		}
619
620		/// Introduce an already-ranked individual of the collective into this pallet.
621		///
622		/// The rank may still be zero. This resets `last_proof` to the current block and
623		/// `last_promotion` will be set to zero, thereby delaying any automatic demotion but
624		/// allowing immediate promotion.
625		///
626		/// - `origin`: A signed origin of a ranked, but not tracked, account.
627		#[pallet::weight(T::WeightInfo::import())]
628		#[pallet::call_index(8)]
629		#[deprecated = "Use `import_member` instead"]
630		#[allow(deprecated)] // Otherwise FRAME will complain about using something deprecated.
631		pub fn import(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
632			let who = ensure_signed(origin)?;
633			Self::do_import(who)?;
634
635			Ok(Pays::No.into()) // Successful imports are free
636		}
637
638		/// Introduce an already-ranked individual of the collective into this pallet.
639		///
640		/// The rank may still be zero. Can be called by anyone on any collective member - including
641		/// the sender.
642		///
643		/// This resets `last_proof` to the current block and `last_promotion` will be set to zero,
644		/// thereby delaying any automatic demotion but allowing immediate promotion.
645		///
646		/// - `origin`: A signed origin of a ranked, but not tracked, account.
647		/// - `who`: The account ID of the collective member to be inducted.
648		#[pallet::weight(T::WeightInfo::set_partial_params())]
649		#[pallet::call_index(11)]
650		pub fn import_member(
651			origin: OriginFor<T>,
652			who: T::AccountId,
653		) -> DispatchResultWithPostInfo {
654			ensure_signed(origin)?;
655			Self::do_import(who)?;
656
657			Ok(Pays::No.into()) // Successful imports are free
658		}
659
660		/// Set the parameters partially.
661		///
662		/// - `origin`: An origin complying with `ParamsOrigin` or root.
663		/// - `partial_params`: The new parameters for the pallet.
664		///
665		/// This update config with multiple arguments without duplicating
666		/// the fields that does not need to update (set to None).
667		#[pallet::weight(T::WeightInfo::set_partial_params())]
668		#[pallet::call_index(9)]
669		pub fn set_partial_params(
670			origin: OriginFor<T>,
671			partial_params: Box<PartialParamsOf<T, I>>,
672		) -> DispatchResult {
673			T::ParamsOrigin::ensure_origin_or_root(origin)?;
674			let params = Params::<T, I>::mutate(|p| {
675				Self::set_partial_params_slice(&mut p.active_salary, partial_params.active_salary);
676				Self::set_partial_params_slice(
677					&mut p.passive_salary,
678					partial_params.passive_salary,
679				);
680				Self::set_partial_params_slice(
681					&mut p.demotion_period,
682					partial_params.demotion_period,
683				);
684				Self::set_partial_params_slice(
685					&mut p.min_promotion_period,
686					partial_params.min_promotion_period,
687				);
688				if let Some(new_offboard_timeout) = partial_params.offboard_timeout {
689					p.offboard_timeout = new_offboard_timeout;
690				}
691				p.clone()
692			});
693			Self::deposit_event(Event::<T, I>::ParamsChanged { params });
694			Ok(())
695		}
696	}
697
698	impl<T: Config<I>, I: 'static> Pallet<T, I> {
699		/// Partially update the base slice with a new slice
700		///
701		/// Only elements in the base slice which has a new value in the new slice will be updated.
702		pub(crate) fn set_partial_params_slice<S>(
703			base_slice: &mut BoundedVec<S, ConvertU16ToU32<T::MaxRank>>,
704			new_slice: BoundedVec<Option<S>, ConvertU16ToU32<T::MaxRank>>,
705		) {
706			for (base_element, new_element) in base_slice.iter_mut().zip(new_slice) {
707				if let Some(element) = new_element {
708					*base_element = element;
709				}
710			}
711		}
712
713		/// Import `who` into the core-fellowship pallet.
714		///
715		/// `who` must be a member of the collective but *not* already imported.
716		pub(crate) fn do_import(who: T::AccountId) -> DispatchResult {
717			ensure!(!Member::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
718			let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
719
720			let now = T::BlockNumberProvider::current_block_number();
721			Member::<T, I>::insert(
722				&who,
723				MemberStatus { is_active: true, last_promotion: 0u32.into(), last_proof: now },
724			);
725			Self::deposit_event(Event::<T, I>::Imported { who, rank });
726
727			Ok(())
728		}
729
730		/// Convert a rank into a `0..RANK_COUNT` index suitable for the arrays in Params.
731		///
732		/// Rank 1 becomes index 0, rank `RANK_COUNT` becomes index `RANK_COUNT - 1`. Any rank not
733		/// in the range `1..=RANK_COUNT` is `None`.
734		pub(crate) fn rank_to_index(rank: RankOf<T, I>) -> Option<usize> {
735			if rank == 0 || rank > T::MaxRank::get() {
736				None
737			} else {
738				Some((rank - 1) as usize)
739			}
740		}
741
742		fn dispose_evidence(who: T::AccountId, old_rank: u16, new_rank: Option<u16>) {
743			if let Some((wish, evidence)) = MemberEvidence::<T, I>::take(&who) {
744				let e = Event::<T, I>::EvidenceJudged { who, wish, evidence, old_rank, new_rank };
745				Self::deposit_event(e);
746			}
747		}
748	}
749
750	impl<T: Config<I>, I: 'static> GetSalary<RankOf<T, I>, T::AccountId, T::Balance> for Pallet<T, I> {
751		fn get_salary(rank: RankOf<T, I>, who: &T::AccountId) -> T::Balance {
752			let index = match Self::rank_to_index(rank) {
753				Some(i) => i,
754				None => return Zero::zero(),
755			};
756			let member = match Member::<T, I>::get(who) {
757				Some(m) => m,
758				None => return Zero::zero(),
759			};
760			let params = Params::<T, I>::get();
761			let salary =
762				if member.is_active { params.active_salary } else { params.passive_salary };
763			salary[index]
764		}
765	}
766}
767
768/// Guard to ensure that the given origin is inducted into this pallet with a given minimum rank.
769/// The account ID of the member is the `Success` value.
770pub struct EnsureInducted<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
771impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
772	for EnsureInducted<T, I, MIN_RANK>
773{
774	type Success = T::AccountId;
775
776	fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
777		let who = <frame_system::EnsureSigned<_> as EnsureOrigin<_>>::try_origin(o)?;
778		match T::Members::rank_of(&who) {
779			Some(rank) if rank >= MIN_RANK && Member::<T, I>::contains_key(&who) => Ok(who),
780			_ => Err(frame_system::RawOrigin::Signed(who).into()),
781		}
782	}
783
784	#[cfg(feature = "runtime-benchmarks")]
785	fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
786		let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
787		if T::Members::rank_of(&who).is_none() {
788			T::Members::induct(&who).map_err(|_| ())?;
789		}
790		for _ in 0..MIN_RANK {
791			if T::Members::rank_of(&who).ok_or(())? < MIN_RANK {
792				T::Members::promote(&who).map_err(|_| ())?;
793			}
794		}
795		Ok(frame_system::RawOrigin::Signed(who).into())
796	}
797}
798
799impl_ensure_origin_with_arg_ignoring_arg! {
800	impl< { T: Config<I>, I: 'static, const MIN_RANK: u16, A } >
801		EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureInducted<T, I, MIN_RANK>
802	{}
803}
804
805impl<T: Config<I>, I: 'static> RankedMembersSwapHandler<T::AccountId, u16> for Pallet<T, I> {
806	fn swapped(old: &T::AccountId, new: &T::AccountId, _rank: u16) {
807		if old == new {
808			defensive!("Should not try to swap with self");
809			return;
810		}
811		if !Member::<T, I>::contains_key(old) {
812			defensive!("Should not try to swap non-member");
813			return;
814		}
815		if Member::<T, I>::contains_key(new) {
816			defensive!("Should not try to overwrite existing member");
817			return;
818		}
819
820		if let Some(member) = Member::<T, I>::take(old) {
821			Member::<T, I>::insert(new, member);
822		}
823		if let Some(we) = MemberEvidence::<T, I>::take(old) {
824			MemberEvidence::<T, I>::insert(new, we);
825		}
826
827		Self::deposit_event(Event::<T, I>::Swapped { who: old.clone(), new_who: new.clone() });
828	}
829}
830
831#[cfg(feature = "runtime-benchmarks")]
832impl<T: Config<I>, I: 'static>
833	pallet_ranked_collective::BenchmarkSetup<<T as frame_system::Config>::AccountId> for Pallet<T, I>
834{
835	fn ensure_member(who: &<T as frame_system::Config>::AccountId) {
836		#[allow(deprecated)]
837		Self::import(frame_system::RawOrigin::Signed(who.clone()).into()).unwrap();
838	}
839}