Files
pezkuwi-sdk/bizinikiwi/pezframe/salary/src/lib.rs
T
pezkuwichain 3139ffa25e fix: Complete snowbridge pezpallet rebrand and critical bug fixes
- 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
2025-12-16 09:57:23 +03:00

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();
}
}