pezpallet_core_fellowship/
lib.rs

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