#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::boxed::Box;
use codec::{Decode, Encode, MaxEncodedLen};
use core::{fmt::Debug, marker::PhantomData};
use scale_info::TypeInfo;
use sp_arithmetic::traits::{Saturating, Zero};
use sp_runtime::RuntimeDebug;
use frame_support::{
defensive,
dispatch::DispatchResultWithPostInfo,
ensure, impl_ensure_origin_with_arg_ignoring_arg,
traits::{
tokens::Balance as BalanceTrait, EnsureOrigin, EnsureOriginWithArg, Get, RankedMembers,
RankedMembersSwapHandler,
},
BoundedVec, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod migration;
pub mod weights;
pub use pallet::*;
pub use weights::*;
#[derive(Encode, Decode, Eq, PartialEq, Copy, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub enum Wish {
Retention,
Promotion,
}
pub type Evidence<T, I> = BoundedVec<u8, <T as Config<I>>::EvidenceSize>;
#[derive(
Encode,
Decode,
CloneNoBound,
EqNoBound,
PartialEqNoBound,
RuntimeDebugNoBound,
TypeInfo,
MaxEncodedLen,
)]
#[scale_info(skip_type_params(Ranks))]
pub struct ParamsType<
Balance: Clone + Eq + PartialEq + Debug,
BlockNumber: Clone + Eq + PartialEq + Debug,
Ranks: Get<u32>,
> {
pub active_salary: BoundedVec<Balance, Ranks>,
pub passive_salary: BoundedVec<Balance, Ranks>,
pub demotion_period: BoundedVec<BlockNumber, Ranks>,
pub min_promotion_period: BoundedVec<BlockNumber, Ranks>,
pub offboard_timeout: BlockNumber,
}
impl<
Balance: Default + Copy + Eq + Debug,
BlockNumber: Default + Copy + Eq + Debug,
Ranks: Get<u32>,
> Default for ParamsType<Balance, BlockNumber, Ranks>
{
fn default() -> Self {
Self {
active_salary: Default::default(),
passive_salary: Default::default(),
demotion_period: Default::default(),
min_promotion_period: Default::default(),
offboard_timeout: BlockNumber::default(),
}
}
}
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub struct MemberStatus<BlockNumber> {
is_active: bool,
last_promotion: BlockNumber,
last_proof: BlockNumber,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
dispatch::Pays,
pallet_prelude::*,
traits::{tokens::GetSalary, EnsureOrigin},
};
use frame_system::{ensure_root, pallet_prelude::*};
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
type WeightInfo: WeightInfo;
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Members: RankedMembers<
AccountId = <Self as frame_system::Config>::AccountId,
Rank = u16,
>;
type Balance: BalanceTrait;
type ParamsOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type InductOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type ApproveOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = RankOf<Self, I>>;
type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = RankOf<Self, I>>;
type FastPromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = RankOf<Self, I>>;
#[pallet::constant]
type EvidenceSize: Get<u32>;
#[pallet::constant]
type MaxRank: Get<u32>;
}
pub type ParamsOf<T, I> =
ParamsType<<T as Config<I>>::Balance, BlockNumberFor<T>, <T as Config<I>>::MaxRank>;
pub type PartialParamsOf<T, I> = ParamsType<
Option<<T as Config<I>>::Balance>,
Option<BlockNumberFor<T>>,
<T as Config<I>>::MaxRank,
>;
pub type MemberStatusOf<T> = MemberStatus<BlockNumberFor<T>>;
pub type RankOf<T, I> = <<T as Config<I>>::Members as RankedMembers>::Rank;
#[pallet::storage]
pub(super) type Params<T: Config<I>, I: 'static = ()> =
StorageValue<_, ParamsOf<T, I>, ValueQuery>;
#[pallet::storage]
pub(super) type Member<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::AccountId, MemberStatusOf<T>, OptionQuery>;
#[pallet::storage]
pub(super) type MemberEvidence<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::AccountId, (Wish, Evidence<T, I>), OptionQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
ParamsChanged { params: ParamsOf<T, I> },
ActiveChanged { who: T::AccountId, is_active: bool },
Inducted { who: T::AccountId },
Offboarded { who: T::AccountId },
Promoted { who: T::AccountId, to_rank: RankOf<T, I> },
Demoted { who: T::AccountId, to_rank: RankOf<T, I> },
Proven { who: T::AccountId, at_rank: RankOf<T, I> },
Requested { who: T::AccountId, wish: Wish },
EvidenceJudged {
who: T::AccountId,
wish: Wish,
evidence: Evidence<T, I>,
old_rank: u16,
new_rank: Option<u16>,
},
Imported { who: T::AccountId, rank: RankOf<T, I> },
Swapped { who: T::AccountId, new_who: T::AccountId },
}
#[pallet::error]
pub enum Error<T, I = ()> {
Unranked,
Ranked,
UnexpectedRank,
InvalidRank,
NoPermission,
NothingDoing,
AlreadyInducted,
NotTracked,
TooSoon,
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::weight(T::WeightInfo::bump_offboard().max(T::WeightInfo::bump_demote()))]
#[pallet::call_index(0)]
pub fn bump(origin: OriginFor<T>, who: T::AccountId) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
let params = Params::<T, I>::get();
let demotion_period = if rank == 0 {
params.offboard_timeout
} else {
let rank_index = Self::rank_to_index(rank).ok_or(Error::<T, I>::InvalidRank)?;
params.demotion_period[rank_index]
};
if demotion_period.is_zero() {
return Err(Error::<T, I>::NothingDoing.into())
}
let demotion_block = member.last_proof.saturating_add(demotion_period);
let now = frame_system::Pallet::<T>::block_number();
if now >= demotion_block {
T::Members::demote(&who)?;
let maybe_to_rank = T::Members::rank_of(&who);
Self::dispose_evidence(who.clone(), rank, maybe_to_rank);
let event = if let Some(to_rank) = maybe_to_rank {
member.last_proof = now;
Member::<T, I>::insert(&who, &member);
Event::<T, I>::Demoted { who, to_rank }
} else {
Member::<T, I>::remove(&who);
Event::<T, I>::Offboarded { who }
};
Self::deposit_event(event);
return Ok(Pays::No.into())
}
Err(Error::<T, I>::NothingDoing.into())
}
#[pallet::weight(T::WeightInfo::set_params())]
#[pallet::call_index(1)]
pub fn set_params(origin: OriginFor<T>, params: Box<ParamsOf<T, I>>) -> DispatchResult {
T::ParamsOrigin::ensure_origin_or_root(origin)?;
Params::<T, I>::put(params.as_ref());
Self::deposit_event(Event::<T, I>::ParamsChanged { params: *params });
Ok(())
}
#[pallet::weight(T::WeightInfo::set_active())]
#[pallet::call_index(2)]
pub fn set_active(origin: OriginFor<T>, is_active: bool) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(
T::Members::rank_of(&who).map_or(false, |r| !r.is_zero()),
Error::<T, I>::Unranked
);
let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
member.is_active = is_active;
Member::<T, I>::insert(&who, &member);
Self::deposit_event(Event::<T, I>::ActiveChanged { who, is_active });
Ok(())
}
#[pallet::weight(T::WeightInfo::approve())]
#[pallet::call_index(3)]
pub fn approve(
origin: OriginFor<T>,
who: T::AccountId,
at_rank: RankOf<T, I>,
) -> DispatchResult {
match T::ApproveOrigin::try_origin(origin) {
Ok(allow_rank) => ensure!(allow_rank >= at_rank, Error::<T, I>::NoPermission),
Err(origin) => ensure_root(origin)?,
}
ensure!(at_rank > 0, Error::<T, I>::InvalidRank);
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
ensure!(rank == at_rank, Error::<T, I>::UnexpectedRank);
let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
member.last_proof = frame_system::Pallet::<T>::block_number();
Member::<T, I>::insert(&who, &member);
Self::dispose_evidence(who.clone(), at_rank, Some(at_rank));
Self::deposit_event(Event::<T, I>::Proven { who, at_rank });
Ok(())
}
#[pallet::weight(T::WeightInfo::induct())]
#[pallet::call_index(4)]
pub fn induct(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
match T::InductOrigin::try_origin(origin) {
Ok(_) => {},
Err(origin) => ensure_root(origin)?,
}
ensure!(!Member::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
ensure!(T::Members::rank_of(&who).is_none(), Error::<T, I>::Ranked);
T::Members::induct(&who)?;
let now = frame_system::Pallet::<T>::block_number();
Member::<T, I>::insert(
&who,
MemberStatus { is_active: true, last_promotion: now, last_proof: now },
);
Self::deposit_event(Event::<T, I>::Inducted { who });
Ok(())
}
#[pallet::weight(T::WeightInfo::promote())]
#[pallet::call_index(5)]
pub fn promote(
origin: OriginFor<T>,
who: T::AccountId,
to_rank: RankOf<T, I>,
) -> DispatchResult {
match T::PromoteOrigin::try_origin(origin) {
Ok(allow_rank) => ensure!(allow_rank >= to_rank, Error::<T, I>::NoPermission),
Err(origin) => ensure_root(origin)?,
}
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
ensure!(
rank.checked_add(1).map_or(false, |i| i == to_rank),
Error::<T, I>::UnexpectedRank
);
let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
let now = frame_system::Pallet::<T>::block_number();
let params = Params::<T, I>::get();
let rank_index = Self::rank_to_index(to_rank).ok_or(Error::<T, I>::InvalidRank)?;
let min_period = params.min_promotion_period[rank_index];
ensure!(
member.last_promotion.saturating_add(min_period) <= now,
Error::<T, I>::TooSoon,
);
T::Members::promote(&who)?;
member.last_promotion = now;
member.last_proof = now;
Member::<T, I>::insert(&who, &member);
Self::dispose_evidence(who.clone(), rank, Some(to_rank));
Self::deposit_event(Event::<T, I>::Promoted { who, to_rank });
Ok(())
}
#[pallet::weight(T::WeightInfo::promote_fast(*to_rank as u32))]
#[pallet::call_index(10)]
pub fn promote_fast(
origin: OriginFor<T>,
who: T::AccountId,
to_rank: RankOf<T, I>,
) -> DispatchResult {
match T::FastPromoteOrigin::try_origin(origin) {
Ok(allow_rank) => ensure!(allow_rank >= to_rank, Error::<T, I>::NoPermission),
Err(origin) => ensure_root(origin)?,
}
ensure!(to_rank as u32 <= T::MaxRank::get(), Error::<T, I>::InvalidRank);
let curr_rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
ensure!(to_rank > curr_rank, Error::<T, I>::UnexpectedRank);
let mut member = Member::<T, I>::get(&who).ok_or(Error::<T, I>::NotTracked)?;
let now = frame_system::Pallet::<T>::block_number();
member.last_promotion = now;
member.last_proof = now;
for rank in (curr_rank + 1)..=to_rank {
T::Members::promote(&who)?;
Member::<T, I>::insert(&who, &member);
Self::dispose_evidence(who.clone(), rank.saturating_sub(1), Some(rank));
Self::deposit_event(Event::<T, I>::Promoted { who: who.clone(), to_rank: rank });
}
Ok(())
}
#[pallet::weight(T::WeightInfo::offboard())]
#[pallet::call_index(6)]
pub fn offboard(origin: OriginFor<T>, who: T::AccountId) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
ensure!(T::Members::rank_of(&who).is_none(), Error::<T, I>::Ranked);
ensure!(Member::<T, I>::contains_key(&who), Error::<T, I>::NotTracked);
Member::<T, I>::remove(&who);
MemberEvidence::<T, I>::remove(&who);
Self::deposit_event(Event::<T, I>::Offboarded { who });
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::submit_evidence())]
#[pallet::call_index(7)]
pub fn submit_evidence(
origin: OriginFor<T>,
wish: Wish,
evidence: Evidence<T, I>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
ensure!(Member::<T, I>::contains_key(&who), Error::<T, I>::NotTracked);
let replaced = MemberEvidence::<T, I>::contains_key(&who);
MemberEvidence::<T, I>::insert(&who, (wish, evidence));
Self::deposit_event(Event::<T, I>::Requested { who, wish });
Ok(if replaced { Pays::Yes } else { Pays::No }.into())
}
#[pallet::weight(T::WeightInfo::import())]
#[pallet::call_index(8)]
pub fn import(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
ensure!(!Member::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::Unranked)?;
let now = frame_system::Pallet::<T>::block_number();
Member::<T, I>::insert(
&who,
MemberStatus { is_active: true, last_promotion: 0u32.into(), last_proof: now },
);
Self::deposit_event(Event::<T, I>::Imported { who, rank });
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::set_partial_params())]
#[pallet::call_index(9)]
pub fn set_partial_params(
origin: OriginFor<T>,
partial_params: Box<PartialParamsOf<T, I>>,
) -> DispatchResult {
T::ParamsOrigin::ensure_origin_or_root(origin)?;
let params = Params::<T, I>::mutate(|p| {
Self::set_partial_params_slice(&mut p.active_salary, partial_params.active_salary);
Self::set_partial_params_slice(
&mut p.passive_salary,
partial_params.passive_salary,
);
Self::set_partial_params_slice(
&mut p.demotion_period,
partial_params.demotion_period,
);
Self::set_partial_params_slice(
&mut p.min_promotion_period,
partial_params.min_promotion_period,
);
if let Some(new_offboard_timeout) = partial_params.offboard_timeout {
p.offboard_timeout = new_offboard_timeout;
}
p.clone()
});
Self::deposit_event(Event::<T, I>::ParamsChanged { params });
Ok(())
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub(crate) fn set_partial_params_slice<S>(
base_slice: &mut BoundedVec<S, <T as Config<I>>::MaxRank>,
new_slice: BoundedVec<Option<S>, <T as Config<I>>::MaxRank>,
) {
for (base_element, new_element) in base_slice.iter_mut().zip(new_slice) {
if let Some(element) = new_element {
*base_element = element;
}
}
}
pub(crate) fn rank_to_index(rank: RankOf<T, I>) -> Option<usize> {
match TryInto::<usize>::try_into(rank) {
Ok(r) if r as u32 <= <T as Config<I>>::MaxRank::get() && r > 0 => Some(r - 1),
_ => return None,
}
}
fn dispose_evidence(who: T::AccountId, old_rank: u16, new_rank: Option<u16>) {
if let Some((wish, evidence)) = MemberEvidence::<T, I>::take(&who) {
let e = Event::<T, I>::EvidenceJudged { who, wish, evidence, old_rank, new_rank };
Self::deposit_event(e);
}
}
}
impl<T: Config<I>, I: 'static> GetSalary<RankOf<T, I>, T::AccountId, T::Balance> for Pallet<T, I> {
fn get_salary(rank: RankOf<T, I>, who: &T::AccountId) -> T::Balance {
let index = match Self::rank_to_index(rank) {
Some(i) => i,
None => return Zero::zero(),
};
let member = match Member::<T, I>::get(who) {
Some(m) => m,
None => return Zero::zero(),
};
let params = Params::<T, I>::get();
let salary =
if member.is_active { params.active_salary } else { params.passive_salary };
salary[index]
}
}
}
pub struct EnsureInducted<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
for EnsureInducted<T, I, MIN_RANK>
{
type Success = T::AccountId;
fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
let who = <frame_system::EnsureSigned<_> as EnsureOrigin<_>>::try_origin(o)?;
match T::Members::rank_of(&who) {
Some(rank) if rank >= MIN_RANK && Member::<T, I>::contains_key(&who) => Ok(who),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
if T::Members::rank_of(&who).is_none() {
T::Members::induct(&who).map_err(|_| ())?;
}
for _ in 0..MIN_RANK {
if T::Members::rank_of(&who).ok_or(())? < MIN_RANK {
T::Members::promote(&who).map_err(|_| ())?;
}
}
Ok(frame_system::RawOrigin::Signed(who).into())
}
}
impl_ensure_origin_with_arg_ignoring_arg! {
impl< { T: Config<I>, I: 'static, const MIN_RANK: u16, A } >
EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureInducted<T, I, MIN_RANK>
{}
}
impl<T: Config<I>, I: 'static> RankedMembersSwapHandler<T::AccountId, u16> for Pallet<T, I> {
fn swapped(old: &T::AccountId, new: &T::AccountId, _rank: u16) {
if old == new {
defensive!("Should not try to swap with self");
return
}
if !Member::<T, I>::contains_key(old) {
defensive!("Should not try to swap non-member");
return
}
if Member::<T, I>::contains_key(new) {
defensive!("Should not try to overwrite existing member");
return
}
if let Some(member) = Member::<T, I>::take(old) {
Member::<T, I>::insert(new, member);
}
if let Some(we) = MemberEvidence::<T, I>::take(old) {
MemberEvidence::<T, I>::insert(new, we);
}
Self::deposit_event(Event::<T, I>::Swapped { who: old.clone(), new_who: new.clone() });
}
}
#[cfg(feature = "runtime-benchmarks")]
impl<T: Config<I>, I: 'static>
pallet_ranked_collective::BenchmarkSetup<<T as frame_system::Config>::AccountId> for Pallet<T, I>
{
fn ensure_member(who: &<T as frame_system::Config>::AccountId) {
Self::import(frame_system::RawOrigin::Signed(who.clone()).into()).unwrap();
}
}