Salary pallet (#13378)

* More drafting

* Paymaster pallet

* Fix build

* More tests

* Rename

* Rename

* Renaming

* Revert old changes

* Multi-phase payouts to avoid bank-runs

* Tests

* Tests

* Allow payment to be targeted elsewhere

* Proper ssync payment failure handling

* Test for repayment

* Docs

* Impl RankedMembers for RankedCollective

* Implement Pay for Pot (i.e. basic account).

* Benchmarks

* Weights

* Introduce Salary benchmark into node

* Fix warning

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_salary

* Update primitives/arithmetic/src/traits.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

* Update frame/salary/src/lib.rs

Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>

* Update lib.rs

* Update frame/salary/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Docs

* Update frame/salary/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Update frame/salary/src/lib.rs

Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>

* Fix

* Fixes

* Fixes

* Move some salary traits stuff to a shared location

* Fix

* Update frame/salary/src/lib.rs

Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Update frame/salary/src/lib.rs

Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Mul floor

* Fix warnings

* Fix test

* Docs

---------

Co-authored-by: command-bot <>
Co-authored-by: Jegor Sidorenko <5252494+jsidorenko@users.noreply.github.com>
Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
This commit is contained in:
Gavin Wood
2023-03-03 19:05:01 +00:00
committed by GitHub
parent ac4a23609c
commit e4bf9f2125
17 changed files with 1833 additions and 28 deletions
+1
View File
@@ -25,3 +25,4 @@ rls*.log
*.bin
*.iml
scripts/ci/node-template-release/Cargo.lock
substrate.code-workspace
+18
View File
@@ -3542,6 +3542,7 @@ dependencies = [
"pallet-referenda",
"pallet-remark",
"pallet-root-testing",
"pallet-salary",
"pallet-scheduler",
"pallet-session",
"pallet-session-benchmarking",
@@ -6362,6 +6363,23 @@ dependencies = [
"sp-std",
]
[[package]]
name = "pallet-salary"
version = "4.0.0-dev"
dependencies = [
"frame-benchmarking",
"frame-support",
"frame-system",
"log",
"parity-scale-codec",
"scale-info",
"sp-arithmetic",
"sp-core",
"sp-io",
"sp-runtime",
"sp-std",
]
[[package]]
name = "pallet-scheduler"
version = "4.0.0-dev"
+1
View File
@@ -134,6 +134,7 @@ members = [
"frame/recovery",
"frame/referenda",
"frame/remark",
"frame/salary",
"frame/scheduler",
"frame/scored-pool",
"frame/session",
+4
View File
@@ -96,6 +96,7 @@ pallet-recovery = { version = "4.0.0-dev", default-features = false, path = "../
pallet-referenda = { version = "4.0.0-dev", default-features = false, path = "../../../frame/referenda" }
pallet-remark = { version = "4.0.0-dev", default-features = false, path = "../../../frame/remark" }
pallet-root-testing = { version = "1.0.0-dev", default-features = false, path = "../../../frame/root-testing" }
pallet-salary = { version = "4.0.0-dev", default-features = false, path = "../../../frame/salary" }
pallet-session = { version = "4.0.0-dev", features = [ "historical" ], path = "../../../frame/session", default-features = false }
pallet-session-benchmarking = { version = "4.0.0-dev", path = "../../../frame/session/benchmarking", default-features = false, optional = true }
pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking" }
@@ -183,6 +184,7 @@ std = [
"pallet-staking/std",
"pallet-staking-runtime-api/std",
"pallet-state-trie-migration/std",
"pallet-salary/std",
"sp-session/std",
"pallet-sudo/std",
"frame-support/std",
@@ -258,6 +260,7 @@ runtime-benchmarks = [
"pallet-referenda/runtime-benchmarks",
"pallet-recovery/runtime-benchmarks",
"pallet-remark/runtime-benchmarks",
"pallet-salary/runtime-benchmarks",
"pallet-session-benchmarking/runtime-benchmarks",
"pallet-society/runtime-benchmarks",
"pallet-staking/runtime-benchmarks",
@@ -316,6 +319,7 @@ try-runtime = [
"pallet-referenda/try-runtime",
"pallet-remark/try-runtime",
"pallet-root-testing/try-runtime",
"pallet-salary/try-runtime",
"pallet-session/try-runtime",
"pallet-staking/try-runtime",
"pallet-state-trie-migration/try-runtime",
+30 -4
View File
@@ -32,10 +32,11 @@ use frame_support::{
pallet_prelude::Get,
parameter_types,
traits::{
fungible::ItemOf, tokens::nonfungibles_v2::Inspect, AsEnsureOriginWithArg, ConstBool,
ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse, EqualPrivilegeOnly, Everything,
Imbalance, InstanceFilter, KeyOwnerProofSystem, LockIdentifier, Nothing, OnUnbalanced,
U128CurrencyToVote, WithdrawReasons,
fungible::ItemOf,
tokens::{nonfungibles_v2::Inspect, GetSalary},
AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse,
EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter, KeyOwnerProofSystem,
LockIdentifier, Nothing, OnUnbalanced, U128CurrencyToVote, WithdrawReasons,
},
weights::{
constants::{
@@ -1571,6 +1572,29 @@ impl pallet_uniques::Config for Runtime {
type Locker = ();
}
parameter_types! {
pub const Budget: Balance = 10_000 * DOLLARS;
pub TreasuryAccount: AccountId = Treasury::account_id();
}
pub struct SalaryForRank;
impl GetSalary<u16, AccountId, Balance> for SalaryForRank {
fn get_salary(a: u16, _: &AccountId) -> Balance {
Balance::from(a) * 1000 * DOLLARS
}
}
impl pallet_salary::Config for Runtime {
type WeightInfo = ();
type RuntimeEvent = RuntimeEvent;
type Paymaster = pallet_salary::PayFromAccount<Balances, TreasuryAccount>;
type Members = RankedCollective;
type Salary = SalaryForRank;
type RegistrationPeriod = ConstU32<200>;
type PayoutPeriod = ConstU32<200>;
type Budget = Budget;
}
parameter_types! {
pub Features: PalletFeatures = PalletFeatures::all_enabled();
pub const MaxAttributesPerCall: u32 = 10;
@@ -1766,6 +1790,7 @@ construct_runtime!(
Nis: pallet_nis,
Uniques: pallet_uniques,
Nfts: pallet_nfts,
Salary: pallet_salary,
TransactionStorage: pallet_transaction_storage,
VoterList: pallet_bags_list::<Instance1>,
StateTrieMigration: pallet_state_trie_migration,
@@ -1885,6 +1910,7 @@ mod benches {
[pallet_referenda, Referenda]
[pallet_recovery, Recovery]
[pallet_remark, Remark]
[pallet_salary, Salary]
[pallet_scheduler, Scheduler]
[pallet_glutton, Glutton]
[pallet_session, SessionBench::<Runtime>]
+55 -20
View File
@@ -54,7 +54,7 @@ use frame_support::{
codec::{Decode, Encode, MaxEncodedLen},
dispatch::{DispatchError, DispatchResultWithPostInfo, PostDispatchInfo},
ensure,
traits::{EnsureOrigin, PollStatus, Polling, VoteTally},
traits::{EnsureOrigin, PollStatus, Polling, RankedMembers, VoteTally},
CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
@@ -472,24 +472,7 @@ pub mod pallet {
pub fn demote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
let who = T::Lookup::lookup(who)?;
let mut record = Self::ensure_member(&who)?;
let rank = record.rank;
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
Self::remove_from_rank(&who, rank)?;
let maybe_rank = rank.checked_sub(1);
match maybe_rank {
None => {
Members::<T, I>::remove(&who);
Self::deposit_event(Event::MemberRemoved { who, rank: 0 });
},
Some(rank) => {
record.rank = rank;
Members::<T, I>::insert(&who, &record);
Self::deposit_event(Event::RankChanged { who, rank });
},
}
Ok(())
Self::do_demote_member(who, Some(max_rank))
}
/// Remove the member entirely.
@@ -659,7 +642,7 @@ pub mod pallet {
Ok(())
}
/// Promotes a member in the ranked collective into the next role.
/// Promotes a member in the ranked collective into the next higher rank.
///
/// A `maybe_max_rank` may be provided to check that the member does not get promoted beyond
/// a certain rank. Is `None` is provided, then the rank will be incremented without checks.
@@ -681,6 +664,33 @@ pub mod pallet {
Ok(())
}
/// Demotes a member in the ranked collective into the next lower rank.
///
/// A `maybe_max_rank` may be provided to check that the member does not get demoted from
/// a certain rank. Is `None` is provided, then the rank will be decremented without checks.
fn do_demote_member(who: T::AccountId, maybe_max_rank: Option<Rank>) -> DispatchResult {
let mut record = Self::ensure_member(&who)?;
let rank = record.rank;
if let Some(max_rank) = maybe_max_rank {
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
}
Self::remove_from_rank(&who, rank)?;
let maybe_rank = rank.checked_sub(1);
match maybe_rank {
None => {
Members::<T, I>::remove(&who);
Self::deposit_event(Event::MemberRemoved { who, rank: 0 });
},
Some(rank) => {
record.rank = rank;
Members::<T, I>::insert(&who, &record);
Self::deposit_event(Event::RankChanged { who, rank });
},
}
Ok(())
}
/// Add a member to the rank collective, and continue to promote them until a certain rank
/// is reached.
pub fn do_add_member_to_rank(who: T::AccountId, rank: Rank) -> DispatchResult {
@@ -691,4 +701,29 @@ pub mod pallet {
Ok(())
}
}
impl<T: Config<I>, I: 'static> RankedMembers for Pallet<T, I> {
type AccountId = T::AccountId;
type Rank = Rank;
fn min_rank() -> Self::Rank {
0
}
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
Some(Self::ensure_member(&who).ok()?.rank)
}
fn induct(who: &Self::AccountId) -> DispatchResult {
Self::do_add_member(who.clone())
}
fn promote(who: &Self::AccountId) -> DispatchResult {
Self::do_promote_member(who.clone(), None)
}
fn demote(who: &Self::AccountId) -> DispatchResult {
Self::do_demote_member(who.clone(), None)
}
}
}
+49
View File
@@ -0,0 +1,49 @@
[package]
name = "pallet-salary"
version = "4.0.0-dev"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"
license = "Apache-2.0"
homepage = "https://substrate.io"
repository = "https://github.com/paritytech/substrate/"
description = "Paymaster"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
log = { version = "0.4.16", default-features = false }
scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" }
frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
sp-arithmetic = { version = "6.0.0", default-features = false, path = "../../primitives/arithmetic" }
sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" }
sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" }
sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" }
sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" }
[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"log/std",
"scale-info/std",
"sp-arithmetic/std",
"sp-core/std",
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = ["frame-support/try-runtime"]
+3
View File
@@ -0,0 +1,3 @@
# Salary
Make periodic payment to members of a ranked collective according to rank.
+183
View File
@@ -0,0 +1,183 @@
// This file is part of Substrate.
// Copyright (C) 2020-2022 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.
//! Salary pallet benchmarking.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use crate::Pallet as Salary;
use frame_benchmarking::v2::*;
use frame_system::{Pallet as System, RawOrigin};
use sp_core::Get;
const SEED: u32 = 0;
fn ensure_member_with_salary<T: Config<I>, I: 'static>(who: &T::AccountId) {
// induct if not a member.
if T::Members::rank_of(who).is_none() {
T::Members::induct(who).unwrap();
}
// promote until they have a salary.
for _ in 0..255 {
let r = T::Members::rank_of(who).expect("prior guard ensures `who` is a member; qed");
if !T::Salary::get_salary(r, &who).is_zero() {
break
}
T::Members::promote(who).unwrap();
}
}
#[instance_benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn init() {
let caller: T::AccountId = whitelisted_caller();
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert!(Salary::<T, I>::status().is_some());
}
#[benchmark]
fn bump() {
let caller: T::AccountId = whitelisted_caller();
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert_eq!(Salary::<T, I>::status().unwrap().cycle_index, 1u32.into());
}
#[benchmark]
fn induct() {
let caller = whitelisted_caller();
ensure_member_with_salary::<T, I>(&caller);
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert!(Salary::<T, I>::last_active(&caller).is_ok());
}
#[benchmark]
fn register() {
let caller = whitelisted_caller();
ensure_member_with_salary::<T, I>(&caller);
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert_eq!(Salary::<T, I>::last_active(&caller).unwrap(), 1u32.into());
}
#[benchmark]
fn payout() {
let caller = whitelisted_caller();
ensure_member_with_salary::<T, I>(&caller);
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + T::RegistrationPeriod::get());
let salary = T::Salary::get_salary(T::Members::rank_of(&caller).unwrap(), &caller);
T::Paymaster::ensure_successful(&caller, salary);
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
match Claimant::<T, I>::get(&caller) {
Some(ClaimantStatus { last_active, status: Attempted { id, .. } }) => {
assert_eq!(last_active, 1u32.into());
assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure);
},
_ => panic!("No claim made"),
}
assert!(Salary::<T, I>::payout(RawOrigin::Signed(caller.clone()).into()).is_err());
}
#[benchmark]
fn payout_other() {
let caller = whitelisted_caller();
ensure_member_with_salary::<T, I>(&caller);
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + T::RegistrationPeriod::get());
let salary = T::Salary::get_salary(T::Members::rank_of(&caller).unwrap(), &caller);
let recipient: T::AccountId = account("recipient", 0, SEED);
T::Paymaster::ensure_successful(&recipient, salary);
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), recipient.clone());
match Claimant::<T, I>::get(&caller) {
Some(ClaimantStatus { last_active, status: Attempted { id, .. } }) => {
assert_eq!(last_active, 1u32.into());
assert_ne!(T::Paymaster::check_payment(id), PaymentStatus::Failure);
},
_ => panic!("No claim made"),
}
assert!(Salary::<T, I>::payout(RawOrigin::Signed(caller.clone()).into()).is_err());
}
#[benchmark]
fn check_payment() {
let caller = whitelisted_caller();
ensure_member_with_salary::<T, I>(&caller);
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
Salary::<T, I>::bump(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + T::RegistrationPeriod::get());
let salary = T::Salary::get_salary(T::Members::rank_of(&caller).unwrap(), &caller);
let recipient: T::AccountId = account("recipient", 0, SEED);
T::Paymaster::ensure_successful(&recipient, salary);
Salary::<T, I>::payout(RawOrigin::Signed(caller.clone()).into()).unwrap();
let id = match Claimant::<T, I>::get(&caller).unwrap().status {
Attempted { id, .. } => id,
_ => panic!("No claim made"),
};
T::Paymaster::ensure_concluded(id);
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()));
assert!(!matches!(Claimant::<T, I>::get(&caller).unwrap().status, Attempted { .. }));
}
impl_benchmark_test_suite! {
Salary,
crate::tests::new_test_ext(),
crate::tests::Test,
}
}
+514
View File
@@ -0,0 +1,514 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 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)]
#![recursion_limit = "128"]
use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_arithmetic::traits::{Saturating, Zero};
use sp_core::TypedGet;
use sp_runtime::Perbill;
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};
use frame_support::{
dispatch::DispatchResultWithPostInfo,
ensure,
traits::{
tokens::{fungible, Balance, GetSalary},
RankedMembers,
},
RuntimeDebug,
};
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
/// Payroll cycle.
pub type Cycle = u32;
/// Status for making a payment via the `Pay::pay` trait function.
#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub enum PaymentStatus {
/// Payment is in progress. Nothing to report yet.
InProgress,
/// Payment status is unknowable. It will never be reported successful or failed.
Unknown,
/// Payment happened successfully.
Success,
/// Payment failed. It may safely be retried.
Failure,
}
/// Can be implemented by `PayFromAccount` using a `fungible` impl, but can also be implemented with
/// XCM/MultiAsset and made generic over assets.
pub trait Pay {
/// The type by which we measure units of the currency in which we make payments.
type Balance: Balance;
/// The type by which we identify the individuals to whom a payment may be made.
type AccountId;
/// An identifier given to an individual payment.
type Id: FullCodec + MaxEncodedLen + TypeInfo + Clone + Eq + PartialEq + Debug + Copy;
/// Make a payment and return an identifier for later evaluation of success in some off-chain
/// mechanism (likely an event, but possibly not on this chain).
fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result<Self::Id, ()>;
/// Check how a payment has proceeded. `id` must have been a previously returned by `pay` for
/// the result of this call to be meaningful.
fn check_payment(id: Self::Id) -> PaymentStatus;
/// Ensure that a call to pay with the given parameters will be successful if done immediately
/// after this call. Used in benchmarking code.
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(who: &Self::AccountId, amount: Self::Balance);
/// Ensure that a call to check_payment with the given parameters will return either Success
/// or Failure.
#[cfg(feature = "runtime-benchmarks")]
fn ensure_concluded(id: Self::Id);
}
/// Simple implementation of `Pay` which makes a payment from a "pot" - i.e. a single account.
pub struct PayFromAccount<F, A>(sp_std::marker::PhantomData<(F, A)>);
impl<A: TypedGet, F: fungible::Transfer<A::Type> + fungible::Mutate<A::Type>> Pay
for PayFromAccount<F, A>
{
type Balance = F::Balance;
type AccountId = A::Type;
type Id = ();
fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result<Self::Id, ()> {
<F as fungible::Transfer<_>>::transfer(&A::get(), who, amount, false).map_err(|_| ())?;
Ok(())
}
fn check_payment(_: ()) -> PaymentStatus {
PaymentStatus::Success
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(_: &Self::AccountId, amount: Self::Balance) {
<F as fungible::Mutate<_>>::mint_into(&A::get(), amount).unwrap();
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_concluded(_: Self::Id) {}
}
/// The status of the pallet 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_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{dispatch::Pays, pallet_prelude::*};
use frame_system::pallet_prelude::*;
#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// The runtime event type.
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_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<AccountId = <Self as frame_system::Config>::AccountId>;
/// The current membership of payees.
type Members: RankedMembers<AccountId = <Self as frame_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`.
#[pallet::constant]
type RegistrationPeriod: Get<Self::BlockNumber>;
/// 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`.
#[pallet::constant]
type PayoutPeriod: Get<Self::BlockNumber>;
/// The total budget per cycle.
///
/// This may change over the course of a cycle without any problem.
#[pallet::constant]
type Budget: Get<BalanceOf<Self, I>>;
}
pub type CycleIndexOf<T> = <T as frame_system::Config>::BlockNumber;
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>, <T as frame_system::Config>::BlockNumber, BalanceOf<T, I>>;
pub type ClaimantStatusOf<T, I> = ClaimantStatus<CycleIndexOf<T>, BalanceOf<T, I>, IdOf<T, I>>;
/// The overall status of the system.
#[pallet::storage]
pub(super) type Status<T: Config<I>, I: 'static = ()> =
StorageValue<_, StatusOf<T, I>, OptionQuery>;
/// The status of a claimant.
#[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 = ()> {
/// 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> },
}
#[pallet::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,
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Start the first payout cycle.
///
/// - `origin`: A `Signed` origin of an account.
#[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())
}
/// Move to next payout cycle, assuming that the present block is now within that cycle.
///
/// - `origin`: A `Signed` origin of an account.
#[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())
}
/// Induct oneself into the payout system.
#[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())
}
/// 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`.
#[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())
}
/// 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`.
#[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())
}
/// 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.
#[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())
}
/// 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.
#[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 => {
// 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> 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() -> T::BlockNumber {
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 => {
// 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 { .. } if claimant.last_active < status.cycle_index => {
// Not registered for this 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(())
}
}
}
+641
View File
@@ -0,0 +1,641 @@
// This file is part of Substrate.
// Copyright (C) 2017-2022 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.
//! The crate's tests.
use std::collections::BTreeMap;
use frame_support::{
assert_noop, assert_ok,
pallet_prelude::Weight,
parameter_types,
traits::{tokens::ConvertRank, ConstU32, ConstU64, Everything},
};
use sp_core::H256;
use sp_runtime::{
testing::Header,
traits::{BlakeTwo256, Identity, IdentityLookup},
DispatchResult,
};
use sp_std::cell::RefCell;
use super::*;
use crate as pallet_salary;
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Salary: pallet_salary::{Pallet, Call, Storage, Event<T>},
}
);
parameter_types! {
pub BlockWeights: frame_system::limits::BlockWeights =
frame_system::limits::BlockWeights::simple_max(Weight::from_ref_time(1_000_000));
}
impl frame_system::Config for Test {
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type Index = u64;
type BlockNumber = u64;
type RuntimeCall = RuntimeCall;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
thread_local! {
pub static PAID: RefCell<BTreeMap<u64, u64>> = RefCell::new(BTreeMap::new());
pub static STATUS: RefCell<BTreeMap<u64, PaymentStatus>> = RefCell::new(BTreeMap::new());
pub static LAST_ID: RefCell<u64> = RefCell::new(0u64);
}
fn paid(who: u64) -> u64 {
PAID.with(|p| p.borrow().get(&who).cloned().unwrap_or(0))
}
fn unpay(who: u64, amount: u64) {
PAID.with(|p| p.borrow_mut().entry(who).or_default().saturating_reduce(amount))
}
fn set_status(id: u64, s: PaymentStatus) {
STATUS.with(|m| m.borrow_mut().insert(id, s));
}
pub struct TestPay;
impl Pay for TestPay {
type AccountId = u64;
type Balance = u64;
type Id = u64;
fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result<Self::Id, ()> {
PAID.with(|paid| *paid.borrow_mut().entry(*who).or_default() += amount);
Ok(LAST_ID.with(|lid| {
let x = *lid.borrow();
lid.replace(x + 1);
x
}))
}
fn check_payment(id: Self::Id) -> PaymentStatus {
STATUS.with(|s| s.borrow().get(&id).cloned().unwrap_or(PaymentStatus::Unknown))
}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_successful(_: &Self::AccountId, _: Self::Balance) {}
#[cfg(feature = "runtime-benchmarks")]
fn ensure_concluded(id: Self::Id) {
set_status(id, PaymentStatus::Failure)
}
}
thread_local! {
pub static CLUB: RefCell<BTreeMap<u64, u64>> = RefCell::new(BTreeMap::new());
}
pub struct TestClub;
impl RankedMembers for TestClub {
type AccountId = u64;
type Rank = u64;
fn min_rank() -> Self::Rank {
0
}
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
CLUB.with(|club| club.borrow().get(who).cloned())
}
fn induct(who: &Self::AccountId) -> DispatchResult {
CLUB.with(|club| club.borrow_mut().insert(*who, 0));
Ok(())
}
fn promote(who: &Self::AccountId) -> DispatchResult {
CLUB.with(|club| {
club.borrow_mut().entry(*who).and_modify(|r| *r += 1);
});
Ok(())
}
fn demote(who: &Self::AccountId) -> DispatchResult {
CLUB.with(|club| match club.borrow().get(who) {
None => Err(sp_runtime::DispatchError::Unavailable),
Some(&0) => {
club.borrow_mut().remove(&who);
Ok(())
},
Some(_) => {
club.borrow_mut().entry(*who).and_modify(|x| *x -= 1);
Ok(())
},
})
}
}
fn set_rank(who: u64, rank: u64) {
CLUB.with(|club| club.borrow_mut().insert(who, rank));
}
parameter_types! {
pub static Budget: u64 = 10;
}
impl Config for Test {
type WeightInfo = ();
type RuntimeEvent = RuntimeEvent;
type Paymaster = TestPay;
type Members = TestClub;
type Salary = ConvertRank<Identity>;
type RegistrationPeriod = ConstU64<2>;
type PayoutPeriod = ConstU64<2>;
type Budget = Budget;
}
pub fn new_test_ext() -> sp_io::TestExternalities {
let t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
fn next_block() {
System::set_block_number(System::block_number() + 1);
}
#[allow(dead_code)]
fn run_to(n: u64) {
while System::block_number() < n {
next_block();
}
}
#[test]
fn basic_stuff() {
new_test_ext().execute_with(|| {
assert!(Salary::last_active(&0).is_err());
assert_eq!(Salary::status(), None);
});
}
#[test]
fn can_start() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_eq!(
Salary::status(),
Some(StatusType {
cycle_index: 0,
cycle_start: 1,
budget: 10,
total_registrations: 0,
total_unregistered_paid: 0,
})
);
});
}
#[test]
fn bump_works() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
run_to(4);
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
run_to(5);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_eq!(
Salary::status(),
Some(StatusType {
cycle_index: 1,
cycle_start: 5,
budget: 10,
total_registrations: 0,
total_unregistered_paid: 0
})
);
run_to(8);
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
BUDGET.with(|b| b.replace(5));
run_to(9);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_eq!(
Salary::status(),
Some(StatusType {
cycle_index: 2,
cycle_start: 9,
budget: 5,
total_registrations: 0,
total_unregistered_paid: 0
})
);
});
}
#[test]
fn induct_works() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::NotMember);
set_rank(1, 1);
assert!(Salary::last_active(&1).is_err());
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
assert_eq!(Salary::last_active(&1).unwrap(), 0);
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::AlreadyInducted);
});
}
#[test]
fn unregistered_payment_works() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::NotStarted);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NotInducted);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
// No claim on the cycle active during induction.
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::TooEarly);
run_to(3);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
run_to(6);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::TooEarly);
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
run_to(8);
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
run_to(9);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(11);
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
assert_eq!(paid(1), 1);
assert_eq!(paid(10), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
});
}
#[test]
fn retry_payment_works() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
run_to(6);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
// Payment failed.
unpay(1, 1);
set_status(0, PaymentStatus::Failure);
assert_eq!(paid(1), 0);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
// Can't just retry.
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
// Check status.
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
// Allowed to try again.
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
run_to(8);
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
run_to(9);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(11);
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
assert_eq!(paid(1), 1);
assert_eq!(paid(10), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
});
}
#[test]
fn retry_registered_payment_works() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
run_to(6);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
// Payment failed.
unpay(1, 1);
set_status(0, PaymentStatus::Failure);
assert_eq!(paid(1), 0);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
// Check status.
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
// Allowed to try again.
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
});
}
#[test]
fn retry_payment_later_is_not_allowed() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
run_to(6);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
// Payment failed.
unpay(1, 1);
set_status(0, PaymentStatus::Failure);
assert_eq!(paid(1), 0);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
// Can't just retry.
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
// Next cycle.
run_to(9);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
// Payment did fail but now too late to retry.
assert_noop!(Salary::check_payment(RuntimeOrigin::signed(1)), Error::<Test>::NotCurrent);
// We do get this cycle's payout, but we must wait for the payout period to start.
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::TooEarly);
run_to(11);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
});
}
#[test]
fn retry_payment_later_without_bump_is_allowed() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
run_to(6);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
// Payment failed.
unpay(1, 1);
set_status(0, PaymentStatus::Failure);
// Next cycle.
run_to(9);
// Payment did fail but we can still retry as long as we don't `bump`.
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
});
}
#[test]
fn retry_payment_to_other_works() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
run_to(6);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(7);
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
// Payment failed.
unpay(10, 1);
set_status(0, PaymentStatus::Failure);
// Can't just retry.
assert_noop!(Salary::payout_other(RuntimeOrigin::signed(1), 10), Error::<Test>::NoClaim);
// Check status.
assert_ok!(Salary::check_payment(RuntimeOrigin::signed(1)));
// Allowed to try again.
assert_ok!(Salary::payout_other(RuntimeOrigin::signed(1), 10));
assert_eq!(paid(10), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
assert_noop!(Salary::payout_other(RuntimeOrigin::signed(1), 10), Error::<Test>::NoClaim);
run_to(8);
assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::<Test>::NotYet);
run_to(9);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
run_to(11);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(paid(10), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 1);
});
}
#[test]
fn registered_payment_works() {
new_test_ext().execute_with(|| {
set_rank(1, 1);
assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::<Test>::NotStarted);
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NotInducted);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
// No claim on the cycle active during induction.
assert_noop!(Salary::register(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
run_to(3);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
run_to(5);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
assert_eq!(Salary::status().unwrap().total_registrations, 1);
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 1);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::NoClaim);
run_to(9);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_eq!(Salary::status().unwrap().total_registrations, 0);
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
assert_eq!(Salary::status().unwrap().total_registrations, 1);
run_to(11);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 2);
assert_eq!(Salary::status().unwrap().total_unregistered_paid, 0);
});
}
#[test]
fn zero_payment_fails() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
set_rank(1, 0);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
run_to(7);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::ClaimZero);
});
}
#[test]
fn unregistered_bankrupcy_fails_gracefully() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
set_rank(1, 2);
set_rank(2, 6);
set_rank(3, 12);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
run_to(7);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
assert_eq!(paid(1), 2);
assert_eq!(paid(2), 6);
assert_eq!(paid(3), 2);
});
}
#[test]
fn registered_bankrupcy_fails_gracefully() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
set_rank(1, 2);
set_rank(2, 6);
set_rank(3, 12);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
run_to(5);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(2)));
assert_ok!(Salary::register(RuntimeOrigin::signed(3)));
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
assert_eq!(paid(1), 1);
assert_eq!(paid(2), 3);
assert_eq!(paid(3), 6);
});
}
#[test]
fn mixed_bankrupcy_fails_gracefully() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
set_rank(1, 2);
set_rank(2, 6);
set_rank(3, 12);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
run_to(5);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(2)));
run_to(7);
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(1)));
assert_eq!(paid(1), 2);
assert_eq!(paid(2), 6);
assert_eq!(paid(3), 2);
});
}
#[test]
fn other_mixed_bankrupcy_fails_gracefully() {
new_test_ext().execute_with(|| {
assert_ok!(Salary::init(RuntimeOrigin::signed(1)));
set_rank(1, 2);
set_rank(2, 6);
set_rank(3, 12);
assert_ok!(Salary::induct(RuntimeOrigin::signed(1)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(2)));
assert_ok!(Salary::induct(RuntimeOrigin::signed(3)));
run_to(5);
assert_ok!(Salary::bump(RuntimeOrigin::signed(1)));
assert_ok!(Salary::register(RuntimeOrigin::signed(2)));
assert_ok!(Salary::register(RuntimeOrigin::signed(3)));
run_to(7);
assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::<Test>::ClaimZero);
assert_ok!(Salary::payout(RuntimeOrigin::signed(2)));
assert_ok!(Salary::payout(RuntimeOrigin::signed(3)));
assert_eq!(paid(1), 0);
assert_eq!(paid(2), 3);
assert_eq!(paid(3), 6);
});
}
+276
View File
@@ -0,0 +1,276 @@
// This file is part of Substrate.
// 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.
//! Autogenerated weights for pallet_salary
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2023-02-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz`
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
// target/production/substrate
// benchmark
// pallet
// --steps=50
// --repeat=20
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --heap-pages=4096
// --json-file=/var/lib/gitlab-runner/builds/zyw4fam_/0/parity/mirrors/substrate/.git/.artifacts/bench.json
// --pallet=pallet_salary
// --chain=dev
// --header=./HEADER-APACHE2
// --output=./frame/salary/src/weights.rs
// --template=./.maintain/frame-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use sp_std::marker::PhantomData;
/// Weight functions needed for pallet_salary.
pub trait WeightInfo {
fn init() -> Weight;
fn bump() -> Weight;
fn induct() -> Weight;
fn register() -> Weight;
fn payout() -> Weight;
fn payout_other() -> Weight;
fn check_payment() -> Weight;
}
/// Weights for pallet_salary using the Substrate node and recommended hardware.
pub struct SubstrateWeight<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
fn init() -> Weight {
// Proof Size summary in bytes:
// Measured: `1120`
// Estimated: `551`
// Minimum execution time: 21_058 nanoseconds.
Weight::from_ref_time(21_381_000)
.saturating_add(Weight::from_proof_size(551))
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
fn bump() -> Weight {
// Proof Size summary in bytes:
// Measured: `1234`
// Estimated: `551`
// Minimum execution time: 22_272 nanoseconds.
Weight::from_ref_time(22_923_000)
.saturating_add(Weight::from_proof_size(551))
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: Salary Status (r:1 w:0)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
fn induct() -> Weight {
// Proof Size summary in bytes:
// Measured: `1510`
// Estimated: `5621`
// Minimum execution time: 32_223 nanoseconds.
Weight::from_ref_time(32_663_000)
.saturating_add(Weight::from_proof_size(5621))
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
fn register() -> Weight {
// Proof Size summary in bytes:
// Measured: `1616`
// Estimated: `5621`
// Minimum execution time: 38_279 nanoseconds.
Weight::from_ref_time(38_996_000)
.saturating_add(Weight::from_proof_size(5621))
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
fn payout() -> Weight {
// Proof Size summary in bytes:
// Measured: `2241`
// Estimated: `5621`
// Minimum execution time: 68_868 nanoseconds.
Weight::from_ref_time(70_160_000)
.saturating_add(Weight::from_proof_size(5621))
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
/// Storage: System Account (r:1 w:1)
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
fn payout_other() -> Weight {
// Proof Size summary in bytes:
// Measured: `2189`
// Estimated: `8224`
// Minimum execution time: 68_804 nanoseconds.
Weight::from_ref_time(69_223_000)
.saturating_add(Weight::from_proof_size(8224))
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
fn check_payment() -> Weight {
// Proof Size summary in bytes:
// Measured: `880`
// Estimated: `3104`
// Minimum execution time: 19_027 nanoseconds.
Weight::from_ref_time(19_360_000)
.saturating_add(Weight::from_proof_size(3104))
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
fn init() -> Weight {
// Proof Size summary in bytes:
// Measured: `1120`
// Estimated: `551`
// Minimum execution time: 21_058 nanoseconds.
Weight::from_ref_time(21_381_000)
.saturating_add(Weight::from_proof_size(551))
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
fn bump() -> Weight {
// Proof Size summary in bytes:
// Measured: `1234`
// Estimated: `551`
// Minimum execution time: 22_272 nanoseconds.
Weight::from_ref_time(22_923_000)
.saturating_add(Weight::from_proof_size(551))
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: Salary Status (r:1 w:0)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
fn induct() -> Weight {
// Proof Size summary in bytes:
// Measured: `1510`
// Estimated: `5621`
// Minimum execution time: 32_223 nanoseconds.
Weight::from_ref_time(32_663_000)
.saturating_add(Weight::from_proof_size(5621))
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
fn register() -> Weight {
// Proof Size summary in bytes:
// Measured: `1616`
// Estimated: `5621`
// Minimum execution time: 38_279 nanoseconds.
Weight::from_ref_time(38_996_000)
.saturating_add(Weight::from_proof_size(5621))
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
fn payout() -> Weight {
// Proof Size summary in bytes:
// Measured: `2241`
// Estimated: `5621`
// Minimum execution time: 68_868 nanoseconds.
Weight::from_ref_time(70_160_000)
.saturating_add(Weight::from_proof_size(5621))
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
/// Storage: RankedCollective Members (r:1 w:0)
/// Proof: RankedCollective Members (max_values: None, max_size: Some(42), added: 2517, mode: MaxEncodedLen)
/// Storage: System Account (r:1 w:1)
/// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen)
fn payout_other() -> Weight {
// Proof Size summary in bytes:
// Measured: `2189`
// Estimated: `8224`
// Minimum execution time: 68_804 nanoseconds.
Weight::from_ref_time(69_223_000)
.saturating_add(Weight::from_proof_size(8224))
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: Salary Status (r:1 w:1)
/// Proof: Salary Status (max_values: Some(1), max_size: Some(56), added: 551, mode: MaxEncodedLen)
/// Storage: Salary Claimant (r:1 w:1)
/// Proof: Salary Claimant (max_values: None, max_size: Some(78), added: 2553, mode: MaxEncodedLen)
fn check_payment() -> Weight {
// Proof Size summary in bytes:
// Measured: `880`
// Estimated: `3104`
// Minimum execution time: 19_027 nanoseconds.
Weight::from_ref_time(19_360_000)
.saturating_add(Weight::from_proof_size(3104))
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ pub use members::{AllowAll, DenyAll, Filter};
pub use members::{
AsContains, ChangeMembers, Contains, ContainsLengthBound, ContainsPair, Everything,
EverythingBut, FromContainsPair, InitializeMembers, InsideBoth, IsInVec, Nothing,
SortedMembers, TheseExcept,
RankedMembers, SortedMembers, TheseExcept,
};
mod validation;
@@ -18,6 +18,8 @@
//! Traits for dealing with the idea of membership.
use impl_trait_for_tuples::impl_for_tuples;
use sp_arithmetic::traits::AtLeast16BitUnsigned;
use sp_runtime::DispatchResult;
use sp_std::{marker::PhantomData, prelude::*};
/// A trait for querying whether a type can be said to "contain" a value.
@@ -265,6 +267,28 @@ pub trait ContainsLengthBound {
fn max_len() -> usize;
}
/// Ranked membership data structure.
pub trait RankedMembers {
type AccountId;
type Rank: AtLeast16BitUnsigned;
/// The lowest rank possible in this membership organisation.
fn min_rank() -> Self::Rank;
/// Return the rank of the given ID, or `None` if they are not a member.
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank>;
/// Add a member to the group at the `min_rank()`.
fn induct(who: &Self::AccountId) -> DispatchResult;
/// Promote a member to the next higher rank.
fn promote(who: &Self::AccountId) -> DispatchResult;
/// Demote a member to the next lower rank; demoting beyond the `min_rank` removes the
/// member entirely.
fn demote(who: &Self::AccountId) -> DispatchResult;
}
/// Trait for type that can handle the initialization of account IDs at genesis.
pub trait InitializeMembers<AccountId> {
/// Initialize the members to the given `members`.
+3 -2
View File
@@ -28,6 +28,7 @@ pub mod nonfungibles;
pub mod nonfungibles_v2;
pub use imbalance::Imbalance;
pub use misc::{
AssetId, AttributeNamespace, Balance, BalanceConversion, BalanceStatus, DepositConsequence,
ExistenceRequirement, Locker, WithdrawConsequence, WithdrawReasons,
AssetId, AttributeNamespace, Balance, BalanceConversion, BalanceStatus, ConvertRank,
DepositConsequence, ExistenceRequirement, GetSalary, Locker, WithdrawConsequence,
WithdrawReasons,
};
@@ -20,7 +20,7 @@
use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero};
use sp_core::RuntimeDebug;
use sp_runtime::{ArithmeticError, DispatchError, TokenError};
use sp_runtime::{traits::Convert, ArithmeticError, DispatchError, TokenError};
use sp_std::fmt::Debug;
/// One of a number of consequences of withdrawing a fungible from an account.
@@ -225,3 +225,18 @@ impl<CollectionId, ItemId> Locker<CollectionId, ItemId> for () {
false
}
}
/// Retrieve the salary for a member of a particular rank.
pub trait GetSalary<Rank, AccountId, Balance> {
/// Retrieve the salary for a given rank. The account ID is also supplied in case this changes
/// things.
fn get_salary(rank: Rank, who: &AccountId) -> Balance;
}
/// Adapter for a rank-to-salary `Convert` implementation into a `GetSalary` implementation.
pub struct ConvertRank<C>(sp_std::marker::PhantomData<C>);
impl<A, R, B, C: Convert<R, B>> GetSalary<R, A, B> for ConvertRank<C> {
fn get_salary(rank: R, _: &A) -> B {
C::convert(rank)
}
}
@@ -148,6 +148,20 @@ impl<
{
}
/// A meta trait for arithmetic.
///
/// Arithmetic types do all the usual stuff you'd expect numbers to do. They are guaranteed to
/// be able to represent at least `u16` values without loss, hence the trait implies `From<u16>`
/// and smaller integers. All other conversions are fallible.
pub trait AtLeast16Bit: BaseArithmetic + From<u16> {}
impl<T: BaseArithmetic + From<u16>> AtLeast16Bit for T {}
/// A meta trait for arithmetic. Same as [`AtLeast16Bit `], but also bounded to be unsigned.
pub trait AtLeast16BitUnsigned: AtLeast16Bit + Unsigned {}
impl<T: AtLeast16Bit + Unsigned> AtLeast16BitUnsigned for T {}
/// A meta trait for arithmetic.
///
/// Arithmetic types do all the usual stuff you'd expect numbers to do. They are guaranteed to