3139ffa25e
- snowbridge-pezpallet-* → pezsnowbridge-pezpallet-* (201 refs) - pallet/ directories → pezpallet/ (4 locations) - Fixed pezpallet.rs self-include recursion bug - Fixed sc-chain-spec hardcoded crate name in derive macro - Reverted .pezpallet_by_name() to .pallet_by_name() (subxt API) - Added BizinikiwiConfig type alias for zombienet tests - Deleted obsolete session state files Verified: pezsnowbridge-pezpallet-*, pezpallet-staking, pezpallet-staking-async, pezframe-benchmarking-cli all pass cargo check
479 lines
17 KiB
Rust
479 lines
17 KiB
Rust
// This file is part of Bizinikiwi.
|
|
|
|
// Copyright (C) Parity Technologies (UK) Ltd.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Make periodic payment to members of a ranked collective according to rank.
|
|
|
|
#![cfg_attr(not(feature = "std"), no_std)]
|
|
|
|
use core::marker::PhantomData;
|
|
use frame::{
|
|
prelude::*,
|
|
traits::tokens::{GetSalary, Pay, PaymentStatus},
|
|
};
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
#[cfg(feature = "runtime-benchmarks")]
|
|
mod benchmarking;
|
|
pub mod weights;
|
|
|
|
pub use pezpallet::*;
|
|
pub use weights::WeightInfo;
|
|
|
|
/// Payroll cycle.
|
|
pub type Cycle = u32;
|
|
|
|
/// The status of the pezpallet instance.
|
|
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
|
pub struct StatusType<CycleIndex, BlockNumber, Balance> {
|
|
/// The index of the "current cycle" (i.e. the last cycle being processed).
|
|
cycle_index: CycleIndex,
|
|
/// The first block of the "current cycle" (i.e. the last cycle being processed).
|
|
cycle_start: BlockNumber,
|
|
/// The total budget available for all payments in the current cycle.
|
|
budget: Balance,
|
|
/// The total amount of the payments registered in the current cycle.
|
|
total_registrations: Balance,
|
|
/// The total amount of unregistered payments which have been made in the current cycle.
|
|
total_unregistered_paid: Balance,
|
|
}
|
|
|
|
/// The state of a specific payment claim.
|
|
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
|
pub enum ClaimState<Balance, Id> {
|
|
/// No claim recorded.
|
|
Nothing,
|
|
/// Amount reserved when last active.
|
|
Registered(Balance),
|
|
/// Amount attempted to be paid when last active as well as the identity of the payment.
|
|
Attempted { registered: Option<Balance>, id: Id, amount: Balance },
|
|
}
|
|
|
|
use ClaimState::*;
|
|
|
|
/// The status of a single payee/claimant.
|
|
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
|
|
pub struct ClaimantStatus<CycleIndex, Balance, Id> {
|
|
/// The most recent cycle in which the claimant was active.
|
|
last_active: CycleIndex,
|
|
/// The state of the payment/claim with in the above cycle.
|
|
status: ClaimState<Balance, Id>,
|
|
}
|
|
|
|
#[frame::pezpallet]
|
|
pub mod pezpallet {
|
|
use super::*;
|
|
#[pezpallet::pezpallet]
|
|
pub struct Pezpallet<T, I = ()>(PhantomData<(T, I)>);
|
|
|
|
#[pezpallet::config]
|
|
pub trait Config<I: 'static = ()>: pezframe_system::Config {
|
|
/// Weight information for extrinsics in this pezpallet.
|
|
type WeightInfo: WeightInfo;
|
|
|
|
/// The runtime event type.
|
|
#[allow(deprecated)]
|
|
type RuntimeEvent: From<Event<Self, I>>
|
|
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
|
|
|
/// Means by which we can make payments to accounts. This also defines the currency and the
|
|
/// balance which we use to denote that currency.
|
|
type Paymaster: Pay<Beneficiary = <Self as pezframe_system::Config>::AccountId, AssetKind = ()>;
|
|
|
|
/// The current membership of payees.
|
|
type Members: RankedMembers<AccountId = <Self as pezframe_system::Config>::AccountId>;
|
|
|
|
/// The maximum payout to be made for a single period to an active member of the given rank.
|
|
///
|
|
/// The benchmarks require that this be non-zero for some rank at most 255.
|
|
type Salary: GetSalary<
|
|
<Self::Members as RankedMembers>::Rank,
|
|
Self::AccountId,
|
|
<Self::Paymaster as Pay>::Balance,
|
|
>;
|
|
|
|
/// The number of blocks within a cycle which accounts have to register their intent to
|
|
/// claim.
|
|
///
|
|
/// The number of blocks between sequential payout cycles is the sum of this and
|
|
/// `PayoutPeriod`.
|
|
#[pezpallet::constant]
|
|
type RegistrationPeriod: Get<BlockNumberFor<Self>>;
|
|
|
|
/// The number of blocks within a cycle which accounts have to claim the payout.
|
|
///
|
|
/// The number of blocks between sequential payout cycles is the sum of this and
|
|
/// `RegistrationPeriod`.
|
|
#[pezpallet::constant]
|
|
type PayoutPeriod: Get<BlockNumberFor<Self>>;
|
|
|
|
/// The total budget per cycle.
|
|
///
|
|
/// This may change over the course of a cycle without any problem.
|
|
#[pezpallet::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>>;
|
|
|
|
/// The overall status of the system.
|
|
#[pezpallet::storage]
|
|
pub type Status<T: Config<I>, I: 'static = ()> = StorageValue<_, StatusOf<T, I>, OptionQuery>;
|
|
|
|
/// The status of a claimant.
|
|
#[pezpallet::storage]
|
|
pub type Claimant<T: Config<I>, I: 'static = ()> =
|
|
StorageMap<_, Twox64Concat, T::AccountId, ClaimantStatusOf<T, I>, OptionQuery>;
|
|
|
|
#[pezpallet::event]
|
|
#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
|
|
pub enum Event<T: Config<I>, I: 'static = ()> {
|
|
/// A member is inducted into the payroll.
|
|
Inducted { who: T::AccountId },
|
|
/// A member registered for a payout.
|
|
Registered { who: T::AccountId, amount: BalanceOf<T, I> },
|
|
/// A payment happened.
|
|
Paid {
|
|
who: T::AccountId,
|
|
beneficiary: T::AccountId,
|
|
amount: BalanceOf<T, I>,
|
|
id: <T::Paymaster as Pay>::Id,
|
|
},
|
|
/// The next cycle begins.
|
|
CycleStarted { index: CycleIndexOf<T> },
|
|
/// A member swapped their account.
|
|
Swapped { who: T::AccountId, new_who: T::AccountId },
|
|
}
|
|
|
|
#[pezpallet::error]
|
|
pub enum Error<T, I = ()> {
|
|
/// The salary system has already been started.
|
|
AlreadyStarted,
|
|
/// The account is not a ranked member.
|
|
NotMember,
|
|
/// The account is already inducted.
|
|
AlreadyInducted,
|
|
// The account is not yet inducted into the system.
|
|
NotInducted,
|
|
/// The member does not have a current valid claim.
|
|
NoClaim,
|
|
/// The member's claim is zero.
|
|
ClaimZero,
|
|
/// Current cycle's registration period is over.
|
|
TooLate,
|
|
/// Current cycle's payment period is not yet begun.
|
|
TooEarly,
|
|
/// Cycle is not yet over.
|
|
NotYet,
|
|
/// The payout cycles have not yet started.
|
|
NotStarted,
|
|
/// There is no budget left for the payout.
|
|
Bankrupt,
|
|
/// There was some issue with the mechanism of payment.
|
|
PayError,
|
|
/// The payment has neither failed nor succeeded yet.
|
|
Inconclusive,
|
|
/// The cycle is after that in which the payment was made.
|
|
NotCurrent,
|
|
}
|
|
|
|
#[pezpallet::call]
|
|
impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
|
|
/// Start the first payout cycle.
|
|
///
|
|
/// - `origin`: A `Signed` origin of an account.
|
|
#[pezpallet::weight(T::WeightInfo::init())]
|
|
#[pezpallet::call_index(0)]
|
|
pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
|
ensure_signed(origin)?;
|
|
let now = pezframe_system::Pezpallet::<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())
|
|
}
|
|
|
|
/// Move to next payout cycle, assuming that the present block is now within that cycle.
|
|
///
|
|
/// - `origin`: A `Signed` origin of an account.
|
|
#[pezpallet::weight(T::WeightInfo::bump())]
|
|
#[pezpallet::call_index(1)]
|
|
pub fn bump(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
|
|
ensure_signed(origin)?;
|
|
let now = pezframe_system::Pezpallet::<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())
|
|
}
|
|
|
|
/// Induct oneself into the payout system.
|
|
#[pezpallet::weight(T::WeightInfo::induct())]
|
|
#[pezpallet::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;
|
|
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())
|
|
}
|
|
|
|
/// Register for a payout.
|
|
///
|
|
/// Will only work if we are in the first `RegistrationPeriod` blocks since the cycle
|
|
/// started.
|
|
///
|
|
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
|
|
#[pezpallet::weight(T::WeightInfo::register())]
|
|
#[pezpallet::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 = pezframe_system::Pezpallet::<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())
|
|
}
|
|
|
|
/// Request a payout.
|
|
///
|
|
/// Will only work if we are after the first `RegistrationPeriod` blocks since the cycle
|
|
/// started but by no more than `PayoutPeriod` blocks.
|
|
///
|
|
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
|
|
#[pezpallet::weight(T::WeightInfo::payout())]
|
|
#[pezpallet::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())
|
|
}
|
|
|
|
/// Request a payout to a secondary account.
|
|
///
|
|
/// Will only work if we are after the first `RegistrationPeriod` blocks since the cycle
|
|
/// started but by no more than `PayoutPeriod` blocks.
|
|
///
|
|
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
|
|
/// - `beneficiary`: The account to receive payment.
|
|
#[pezpallet::weight(T::WeightInfo::payout_other())]
|
|
#[pezpallet::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())
|
|
}
|
|
|
|
/// Update a payment's status; if it failed, alter the state so the payment can be retried.
|
|
///
|
|
/// This must be called within the same cycle as the failed payment. It will fail with
|
|
/// `Event::NotCurrent` otherwise.
|
|
///
|
|
/// - `origin`: A `Signed` origin of an account which is a member of `Members` who has
|
|
/// received a payment this cycle.
|
|
#[pezpallet::weight(T::WeightInfo::check_payment())]
|
|
#[pezpallet::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 => {
|
|
// Payment failed: we reset back to the status prior to payment.
|
|
if let Some(amount) = registered {
|
|
// Account registered; this makes it simple to roll back and allow retry.
|
|
claimant.status = ClaimState::Registered(amount);
|
|
} else {
|
|
// Account didn't register; we set it to `Nothing` but must decrement
|
|
// the `last_active` also to ensure a retry works.
|
|
claimant.last_active.saturating_reduce(1u32.into());
|
|
claimant.status = ClaimState::Nothing;
|
|
// Since it is not registered, we must walk back our counter for what has
|
|
// been paid.
|
|
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> Pezpallet<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 = pezframe_system::Pezpallet::<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 => {
|
|
// Registered for this cycle. Pay accordingly.
|
|
let payout = if status.total_registrations <= status.budget {
|
|
// Can pay in full.
|
|
unpaid
|
|
} else {
|
|
// Must be reduced pro-rata
|
|
Perbill::from_rational(status.budget, status.total_registrations)
|
|
.mul_floor(unpaid)
|
|
};
|
|
(payout, Some(unpaid))
|
|
},
|
|
Nothing | Attempted { .. } | Registered(_)
|
|
if claimant.last_active < status.cycle_index =>
|
|
{
|
|
// Not registered for this cycle (or stale registration from previous cycle).
|
|
// Pay from whatever is left.
|
|
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 Pezpallet<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 {
|
|
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>
|
|
pezpallet_ranked_collective::BenchmarkSetup<<T as pezframe_system::Config>::AccountId> for Pezpallet<T, I>
|
|
{
|
|
fn ensure_member(who: &<T as pezframe_system::Config>::AccountId) {
|
|
Self::init(pezframe_system::RawOrigin::Signed(who.clone()).into()).unwrap();
|
|
Self::induct(pezframe_system::RawOrigin::Signed(who.clone()).into()).unwrap();
|
|
}
|
|
}
|