#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode, MaxEncodedLen};
use core::marker::PhantomData;
use scale_info::TypeInfo;
use sp_arithmetic::traits::{Saturating, Zero};
use sp_runtime::{Perbill, RuntimeDebug};
use frame_support::{
defensive,
dispatch::DispatchResultWithPostInfo,
ensure,
traits::{
tokens::{GetSalary, Pay, PaymentStatus},
RankedMembers, RankedMembersSwapHandler,
},
};
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
pub type Cycle = u32;
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub struct StatusType<CycleIndex, BlockNumber, Balance> {
cycle_index: CycleIndex,
cycle_start: BlockNumber,
budget: Balance,
total_registrations: Balance,
total_unregistered_paid: Balance,
}
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub enum ClaimState<Balance, Id> {
Nothing,
Registered(Balance),
Attempted { registered: Option<Balance>, id: Id, amount: Balance },
}
use ClaimState::*;
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub struct ClaimantStatus<CycleIndex, Balance, Id> {
last_active: CycleIndex,
status: ClaimState<Balance, Id>,
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{dispatch::Pays, pallet_prelude::*};
use frame_system::pallet_prelude::*;
#[pallet::pallet]
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 Paymaster: Pay<Beneficiary = <Self as frame_system::Config>::AccountId, AssetKind = ()>;
type Members: RankedMembers<AccountId = <Self as frame_system::Config>::AccountId>;
type Salary: GetSalary<
<Self::Members as RankedMembers>::Rank,
Self::AccountId,
<Self::Paymaster as Pay>::Balance,
>;
#[pallet::constant]
type RegistrationPeriod: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type PayoutPeriod: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type Budget: Get<BalanceOf<Self, I>>;
}
pub type CycleIndexOf<T> = BlockNumberFor<T>;
pub type BalanceOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Balance;
pub type IdOf<T, I> = <<T as Config<I>>::Paymaster as Pay>::Id;
pub type StatusOf<T, I> = StatusType<CycleIndexOf<T>, BlockNumberFor<T>, BalanceOf<T, I>>;
pub type ClaimantStatusOf<T, I> = ClaimantStatus<CycleIndexOf<T>, BalanceOf<T, I>, IdOf<T, I>>;
#[pallet::storage]
pub(super) type Status<T: Config<I>, I: 'static = ()> =
StorageValue<_, StatusOf<T, I>, OptionQuery>;
#[pallet::storage]
pub(super) type Claimant<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::AccountId, ClaimantStatusOf<T, I>, OptionQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
Inducted { who: T::AccountId },
Registered { who: T::AccountId, amount: BalanceOf<T, I> },
Paid {
who: T::AccountId,
beneficiary: T::AccountId,
amount: BalanceOf<T, I>,
id: <T::Paymaster as Pay>::Id,
},
CycleStarted { index: CycleIndexOf<T> },
Swapped { who: T::AccountId, new_who: T::AccountId },
}
#[pallet::error]
pub enum Error<T, I = ()> {
AlreadyStarted,
NotMember,
AlreadyInducted,
NotInducted,
NoClaim,
ClaimZero,
TooLate,
TooEarly,
NotYet,
NotStarted,
Bankrupt,
PayError,
Inconclusive,
NotCurrent,
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::weight(T::WeightInfo::init())]
#[pallet::call_index(0)]
pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
let now = frame_system::Pallet::<T>::block_number();
ensure!(!Status::<T, I>::exists(), Error::<T, I>::AlreadyStarted);
let status = StatusType {
cycle_index: Zero::zero(),
cycle_start: now,
budget: T::Budget::get(),
total_registrations: Zero::zero(),
total_unregistered_paid: Zero::zero(),
};
Status::<T, I>::put(&status);
Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::bump())]
#[pallet::call_index(1)]
pub fn bump(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
let now = frame_system::Pallet::<T>::block_number();
let cycle_period = Self::cycle_period();
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
status.cycle_start.saturating_accrue(cycle_period);
ensure!(now >= status.cycle_start, Error::<T, I>::NotYet);
status.cycle_index.saturating_inc();
status.budget = T::Budget::get();
status.total_registrations = Zero::zero();
status.total_unregistered_paid = Zero::zero();
Status::<T, I>::put(&status);
Self::deposit_event(Event::<T, I>::CycleStarted { index: status.cycle_index });
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::induct())]
#[pallet::call_index(2)]
pub fn induct(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let cycle_index = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?.cycle_index;
let _ = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
ensure!(!Claimant::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);
Claimant::<T, I>::insert(
&who,
ClaimantStatus { last_active: cycle_index, status: Nothing },
);
Self::deposit_event(Event::<T, I>::Inducted { who });
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::register())]
#[pallet::call_index(3)]
pub fn register(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
let now = frame_system::Pallet::<T>::block_number();
ensure!(
now < status.cycle_start + T::RegistrationPeriod::get(),
Error::<T, I>::TooLate
);
ensure!(claimant.last_active < status.cycle_index, Error::<T, I>::NoClaim);
let payout = T::Salary::get_salary(rank, &who);
ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
claimant.last_active = status.cycle_index;
claimant.status = Registered(payout);
status.total_registrations.saturating_accrue(payout);
Claimant::<T, I>::insert(&who, &claimant);
Status::<T, I>::put(&status);
Self::deposit_event(Event::<T, I>::Registered { who, amount: payout });
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::payout())]
#[pallet::call_index(4)]
pub fn payout(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Self::do_payout(who.clone(), who)?;
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::payout_other())]
#[pallet::call_index(5)]
pub fn payout_other(
origin: OriginFor<T>,
beneficiary: T::AccountId,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Self::do_payout(who, beneficiary)?;
Ok(Pays::No.into())
}
#[pallet::weight(T::WeightInfo::check_payment())]
#[pallet::call_index(6)]
pub fn check_payment(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
ensure!(claimant.last_active == status.cycle_index, Error::<T, I>::NotCurrent);
let (id, registered, amount) = match claimant.status {
Attempted { id, registered, amount } => (id, registered, amount),
_ => return Err(Error::<T, I>::NoClaim.into()),
};
match T::Paymaster::check_payment(id) {
PaymentStatus::Failure => {
if let Some(amount) = registered {
claimant.status = ClaimState::Registered(amount);
} else {
claimant.last_active.saturating_reduce(1u32.into());
claimant.status = ClaimState::Nothing;
status.total_unregistered_paid.saturating_reduce(amount);
}
},
PaymentStatus::Success => claimant.status = ClaimState::Nothing,
_ => return Err(Error::<T, I>::Inconclusive.into()),
}
Claimant::<T, I>::insert(&who, &claimant);
Status::<T, I>::put(&status);
Ok(Pays::No.into())
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn status() -> Option<StatusOf<T, I>> {
Status::<T, I>::get()
}
pub fn last_active(who: &T::AccountId) -> Result<CycleIndexOf<T>, DispatchError> {
Ok(Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?.last_active)
}
pub fn cycle_period() -> BlockNumberFor<T> {
T::RegistrationPeriod::get() + T::PayoutPeriod::get()
}
fn do_payout(who: T::AccountId, beneficiary: T::AccountId) -> DispatchResult {
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
let mut claimant = Claimant::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted)?;
let now = frame_system::Pallet::<T>::block_number();
ensure!(
now >= status.cycle_start + T::RegistrationPeriod::get(),
Error::<T, I>::TooEarly,
);
let (payout, registered) = match claimant.status {
Registered(unpaid) if claimant.last_active == status.cycle_index => {
let payout = if status.total_registrations <= status.budget {
unpaid
} else {
Perbill::from_rational(status.budget, status.total_registrations)
.mul_floor(unpaid)
};
(payout, Some(unpaid))
},
Nothing | Attempted { .. } if claimant.last_active < status.cycle_index => {
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
let ideal_payout = T::Salary::get_salary(rank, &who);
let pot = status
.budget
.saturating_sub(status.total_registrations)
.saturating_sub(status.total_unregistered_paid);
let payout = ideal_payout.min(pot);
ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
status.total_unregistered_paid.saturating_accrue(payout);
(payout, None)
},
_ => return Err(Error::<T, I>::NoClaim.into()),
};
claimant.last_active = status.cycle_index;
let id =
T::Paymaster::pay(&beneficiary, (), payout).map_err(|_| Error::<T, I>::PayError)?;
claimant.status = Attempted { registered, id, amount: payout };
Claimant::<T, I>::insert(&who, &claimant);
Status::<T, I>::put(&status);
Self::deposit_event(Event::<T, I>::Paid { who, beneficiary, amount: payout, id });
Ok(())
}
}
}
impl<T: Config<I>, I: 'static>
RankedMembersSwapHandler<T::AccountId, <T::Members as RankedMembers>::Rank> for Pallet<T, I>
{
fn swapped(
who: &T::AccountId,
new_who: &T::AccountId,
_rank: <T::Members as RankedMembers>::Rank,
) {
if who == new_who {
defensive!("Should not try to swap with self");
return
}
if Claimant::<T, I>::contains_key(new_who) {
defensive!("Should not try to overwrite existing claimant");
return
}
let Some(claimant) = Claimant::<T, I>::take(who) else {
frame_support::defensive!("Claimant should exist when swapping");
return;
};
Claimant::<T, I>::insert(new_who, claimant);
Self::deposit_event(Event::<T, I>::Swapped { who: who.clone(), new_who: new_who.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::init(frame_system::RawOrigin::Signed(who.clone()).into()).unwrap();
Self::induct(frame_system::RawOrigin::Signed(who.clone()).into()).unwrap();
}
}