diff --git a/.gitignore b/.gitignore index 5cd013e054e4f..efb63520e1e1b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ rls*.log *.bin *.iml scripts/ci/node-template-release/Cargo.lock +substrate.code-workspace diff --git a/Cargo.lock b/Cargo.lock index 299f639219437..ed7f9d10d20b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3576,6 +3576,7 @@ dependencies = [ "pallet-referenda", "pallet-remark", "pallet-root-testing", + "pallet-salary", "pallet-scheduler", "pallet-session", "pallet-session-benchmarking", @@ -6388,6 +6389,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" diff --git a/Cargo.toml b/Cargo.toml index 80dd2b90f85c2..523314b07aea2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ members = [ "frame/recovery", "frame/referenda", "frame/remark", + "frame/salary", "frame/scheduler", "frame/scored-pool", "frame/session", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 3df4596aaad90..0c6c660c74af8 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -95,6 +95,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" } @@ -181,6 +182,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", @@ -256,6 +258,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", @@ -314,6 +317,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", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 14b74425f094b..d5fd46a4c09a6 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -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::{ @@ -1570,6 +1571,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 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; + 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; @@ -1765,6 +1789,7 @@ construct_runtime!( Nis: pallet_nis, Uniques: pallet_uniques, Nfts: pallet_nfts, + Salary: pallet_salary, TransactionStorage: pallet_transaction_storage, VoterList: pallet_bags_list::, StateTrieMigration: pallet_state_trie_migration, @@ -1884,6 +1909,7 @@ mod benches { [pallet_referenda, Referenda] [pallet_recovery, Recovery] [pallet_remark, Remark] + [pallet_salary, Salary] [pallet_scheduler, Scheduler] [pallet_glutton, Glutton] [pallet_session, SessionBench::] diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index 30bb280a1a4f8..6b4a51a16ed9f 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -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, }; @@ -465,24 +465,7 @@ pub mod pallet { pub fn demote_member(origin: OriginFor, who: AccountIdLookupOf) -> 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::::NoPermission); - - Self::remove_from_rank(&who, rank)?; - let maybe_rank = rank.checked_sub(1); - match maybe_rank { - None => { - Members::::remove(&who); - Self::deposit_event(Event::MemberRemoved { who, rank: 0 }); - }, - Some(rank) => { - record.rank = rank; - Members::::insert(&who, &record); - Self::deposit_event(Event::RankChanged { who, rank }); - }, - } - Ok(()) + Self::do_demote_member(who, Some(max_rank)) } /// Remove the member entirely. @@ -652,7 +635,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. @@ -674,6 +657,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) -> 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::::NoPermission); + } + + Self::remove_from_rank(&who, rank)?; + let maybe_rank = rank.checked_sub(1); + match maybe_rank { + None => { + Members::::remove(&who); + Self::deposit_event(Event::MemberRemoved { who, rank: 0 }); + }, + Some(rank) => { + record.rank = rank; + Members::::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 { @@ -684,4 +694,29 @@ pub mod pallet { Ok(()) } } + + impl, I: 'static> RankedMembers for Pallet { + type AccountId = T::AccountId; + type Rank = Rank; + + fn min_rank() -> Self::Rank { + 0 + } + + fn rank_of(who: &Self::AccountId) -> Option { + 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) + } + } } diff --git a/frame/salary/Cargo.toml b/frame/salary/Cargo.toml new file mode 100644 index 0000000000000..86cae16bb28b7 --- /dev/null +++ b/frame/salary/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "pallet-salary" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +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"] diff --git a/frame/salary/README.md b/frame/salary/README.md new file mode 100644 index 0000000000000..25c1be0e805d7 --- /dev/null +++ b/frame/salary/README.md @@ -0,0 +1,3 @@ +# Salary + +Make periodic payment to members of a ranked collective according to rank. \ No newline at end of file diff --git a/frame/salary/src/benchmarking.rs b/frame/salary/src/benchmarking.rs new file mode 100644 index 0000000000000..339185b37cb7b --- /dev/null +++ b/frame/salary/src/benchmarking.rs @@ -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, 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::::status().is_some()); + } + + #[benchmark] + fn bump() { + let caller: T::AccountId = whitelisted_caller(); + Salary::::init(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::block_number() + Salary::::cycle_period()); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + + assert_eq!(Salary::::status().unwrap().cycle_index, 1u32.into()); + } + + #[benchmark] + fn induct() { + let caller = whitelisted_caller(); + ensure_member_with_salary::(&caller); + Salary::::init(RawOrigin::Signed(caller.clone()).into()).unwrap(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + + assert!(Salary::::last_active(&caller).is_ok()); + } + + #[benchmark] + fn register() { + let caller = whitelisted_caller(); + ensure_member_with_salary::(&caller); + Salary::::init(RawOrigin::Signed(caller.clone()).into()).unwrap(); + Salary::::induct(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::block_number() + Salary::::cycle_period()); + Salary::::bump(RawOrigin::Signed(caller.clone()).into()).unwrap(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone())); + + assert_eq!(Salary::::last_active(&caller).unwrap(), 1u32.into()); + } + + #[benchmark] + fn payout() { + let caller = whitelisted_caller(); + ensure_member_with_salary::(&caller); + Salary::::init(RawOrigin::Signed(caller.clone()).into()).unwrap(); + Salary::::induct(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::block_number() + Salary::::cycle_period()); + Salary::::bump(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::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::::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::::payout(RawOrigin::Signed(caller.clone()).into()).is_err()); + } + + #[benchmark] + fn payout_other() { + let caller = whitelisted_caller(); + ensure_member_with_salary::(&caller); + Salary::::init(RawOrigin::Signed(caller.clone()).into()).unwrap(); + Salary::::induct(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::block_number() + Salary::::cycle_period()); + Salary::::bump(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::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::::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::::payout(RawOrigin::Signed(caller.clone()).into()).is_err()); + } + + #[benchmark] + fn check_payment() { + let caller = whitelisted_caller(); + ensure_member_with_salary::(&caller); + Salary::::init(RawOrigin::Signed(caller.clone()).into()).unwrap(); + Salary::::induct(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::block_number() + Salary::::cycle_period()); + Salary::::bump(RawOrigin::Signed(caller.clone()).into()).unwrap(); + System::::set_block_number(System::::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::::payout(RawOrigin::Signed(caller.clone()).into()).unwrap(); + let id = match Claimant::::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::::get(&caller).unwrap().status, Attempted { .. })); + } + + impl_benchmark_test_suite! { + Salary, + crate::tests::new_test_ext(), + crate::tests::Test, + } +} diff --git a/frame/salary/src/lib.rs b/frame/salary/src/lib.rs new file mode 100644 index 0000000000000..2169952f1a7f3 --- /dev/null +++ b/frame/salary/src/lib.rs @@ -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; + /// 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(sp_std::marker::PhantomData<(F, A)>); +impl + fungible::Mutate> Pay + for PayFromAccount +{ + type Balance = F::Balance; + type AccountId = A::Type; + type Id = (); + fn pay(who: &Self::AccountId, amount: Self::Balance) -> Result { + >::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) { + >::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 { + /// 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 { + /// 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, 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 { + /// 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, +} + +#[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(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The runtime event type. + type RuntimeEvent: From> + + IsType<::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>; + + /// The current membership of payees. + type Members: RankedMembers::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< + ::Rank, + Self::AccountId, + ::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; + + /// 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; + + /// The total budget per cycle. + /// + /// This may change over the course of a cycle without any problem. + #[pallet::constant] + type Budget: Get>; + } + + pub type CycleIndexOf = ::BlockNumber; + pub type BalanceOf = <>::Paymaster as Pay>::Balance; + pub type IdOf = <>::Paymaster as Pay>::Id; + pub type StatusOf = + StatusType, ::BlockNumber, BalanceOf>; + pub type ClaimantStatusOf = ClaimantStatus, BalanceOf, IdOf>; + + /// The overall status of the system. + #[pallet::storage] + pub(super) type Status, I: 'static = ()> = + StorageValue<_, StatusOf, OptionQuery>; + + /// The status of a claimant. + #[pallet::storage] + pub(super) type Claimant, I: 'static = ()> = + StorageMap<_, Twox64Concat, T::AccountId, ClaimantStatusOf, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, 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 }, + /// A payment happened. + Paid { + who: T::AccountId, + beneficiary: T::AccountId, + amount: BalanceOf, + id: ::Id, + }, + /// The next cycle begins. + CycleStarted { index: CycleIndexOf }, + } + + #[pallet::error] + pub enum Error { + /// 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, I: 'static> Pallet { + /// 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) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); + ensure!(!Status::::exists(), Error::::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::::put(&status); + + Self::deposit_event(Event::::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) -> DispatchResultWithPostInfo { + let _ = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); + let cycle_period = Self::cycle_period(); + let mut status = Status::::get().ok_or(Error::::NotStarted)?; + status.cycle_start.saturating_accrue(cycle_period); + ensure!(now >= status.cycle_start, Error::::NotYet); + status.cycle_index.saturating_inc(); + status.budget = T::Budget::get(); + status.total_registrations = Zero::zero(); + status.total_unregistered_paid = Zero::zero(); + Status::::put(&status); + + Self::deposit_event(Event::::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) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let cycle_index = Status::::get().ok_or(Error::::NotStarted)?.cycle_index; + let _ = T::Members::rank_of(&who).ok_or(Error::::NotMember)?; + ensure!(!Claimant::::contains_key(&who), Error::::AlreadyInducted); + + Claimant::::insert( + &who, + ClaimantStatus { last_active: cycle_index, status: Nothing }, + ); + + Self::deposit_event(Event::::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) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let rank = T::Members::rank_of(&who).ok_or(Error::::NotMember)?; + let mut status = Status::::get().ok_or(Error::::NotStarted)?; + let mut claimant = Claimant::::get(&who).ok_or(Error::::NotInducted)?; + let now = frame_system::Pallet::::block_number(); + ensure!( + now < status.cycle_start + T::RegistrationPeriod::get(), + Error::::TooLate + ); + ensure!(claimant.last_active < status.cycle_index, Error::::NoClaim); + let payout = T::Salary::get_salary(rank, &who); + ensure!(!payout.is_zero(), Error::::ClaimZero); + claimant.last_active = status.cycle_index; + claimant.status = Registered(payout); + status.total_registrations.saturating_accrue(payout); + + Claimant::::insert(&who, &claimant); + Status::::put(&status); + + Self::deposit_event(Event::::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) -> 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, + 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) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let mut status = Status::::get().ok_or(Error::::NotStarted)?; + let mut claimant = Claimant::::get(&who).ok_or(Error::::NotInducted)?; + ensure!(claimant.last_active == status.cycle_index, Error::::NotCurrent); + let (id, registered, amount) = match claimant.status { + Attempted { id, registered, amount } => (id, registered, amount), + _ => return Err(Error::::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::::Inconclusive.into()), + } + Claimant::::insert(&who, &claimant); + Status::::put(&status); + + Ok(Pays::No.into()) + } + } + + impl, I: 'static> Pallet { + pub fn status() -> Option> { + Status::::get() + } + pub fn last_active(who: &T::AccountId) -> Result, DispatchError> { + Ok(Claimant::::get(&who).ok_or(Error::::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::::get().ok_or(Error::::NotStarted)?; + let mut claimant = Claimant::::get(&who).ok_or(Error::::NotInducted)?; + + let now = frame_system::Pallet::::block_number(); + ensure!( + now >= status.cycle_start + T::RegistrationPeriod::get(), + Error::::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::::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::::ClaimZero); + + status.total_unregistered_paid.saturating_accrue(payout); + (payout, None) + }, + _ => return Err(Error::::NoClaim.into()), + }; + + claimant.last_active = status.cycle_index; + + let id = + T::Paymaster::pay(&beneficiary, payout).map_err(|()| Error::::PayError)?; + + claimant.status = Attempted { registered, id, amount: payout }; + + Claimant::::insert(&who, &claimant); + Status::::put(&status); + + Self::deposit_event(Event::::Paid { who, beneficiary, amount: payout, id }); + Ok(()) + } + } +} diff --git a/frame/salary/src/tests.rs b/frame/salary/src/tests.rs new file mode 100644 index 0000000000000..8649291225bc3 --- /dev/null +++ b/frame/salary/src/tests.rs @@ -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; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Salary: pallet_salary::{Pallet, Call, Storage, Event}, + } +); + +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; + 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> = RefCell::new(BTreeMap::new()); + pub static STATUS: RefCell> = RefCell::new(BTreeMap::new()); + pub static LAST_ID: RefCell = 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 { + 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> = 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 { + 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; + 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::().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::::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::::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::::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::::AlreadyInducted); + }); +} + +#[test] +fn unregistered_payment_works() { + new_test_ext().execute_with(|| { + set_rank(1, 1); + assert_noop!(Salary::induct(RuntimeOrigin::signed(1)), Error::::NotStarted); + assert_ok!(Salary::init(RuntimeOrigin::signed(1))); + assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::::NotInducted); + assert_ok!(Salary::induct(RuntimeOrigin::signed(1))); + // No claim on the cycle active during induction. + assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::::TooEarly); + run_to(3); + assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::::NoClaim); + + run_to(6); + assert_ok!(Salary::bump(RuntimeOrigin::signed(1))); + assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::::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::::NoClaim); + run_to(8); + assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::::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::::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::::NoClaim); + run_to(8); + assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::::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::::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::::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::::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::::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::::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::::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::::NoClaim); + run_to(8); + assert_noop!(Salary::bump(RuntimeOrigin::signed(1)), Error::::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::::NotStarted); + assert_ok!(Salary::init(RuntimeOrigin::signed(1))); + assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::::NotInducted); + assert_ok!(Salary::induct(RuntimeOrigin::signed(1))); + // No claim on the cycle active during induction. + assert_noop!(Salary::register(RuntimeOrigin::signed(1)), Error::::NoClaim); + run_to(3); + assert_noop!(Salary::payout(RuntimeOrigin::signed(1)), Error::::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::::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::::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::::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); + }); +} diff --git a/frame/salary/src/weights.rs b/frame/salary/src/weights.rs new file mode 100644 index 0000000000000..f678d086dfcd9 --- /dev/null +++ b/frame/salary/src/weights.rs @@ -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(PhantomData); +impl WeightInfo for SubstrateWeight { + /// 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)) + } +} diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index da8efe6afc483..4b92cfbaee69c 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -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; diff --git a/frame/support/src/traits/members.rs b/frame/support/src/traits/members.rs index f336b460c8f1e..fbba742ebeb43 100644 --- a/frame/support/src/traits/members.rs +++ b/frame/support/src/traits/members.rs @@ -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; + + /// 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 { /// Initialize the members to the given `members`. diff --git a/frame/support/src/traits/tokens.rs b/frame/support/src/traits/tokens.rs index 6055fde2180f0..f8dcf68159f1a 100644 --- a/frame/support/src/traits/tokens.rs +++ b/frame/support/src/traits/tokens.rs @@ -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, }; diff --git a/frame/support/src/traits/tokens/misc.rs b/frame/support/src/traits/tokens/misc.rs index b4bd4640116a7..eff65f3620a32 100644 --- a/frame/support/src/traits/tokens/misc.rs +++ b/frame/support/src/traits/tokens/misc.rs @@ -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 Locker for () { false } } + +/// Retrieve the salary for a member of a particular rank. +pub trait GetSalary { + /// 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(sp_std::marker::PhantomData); +impl> GetSalary for ConvertRank { + fn get_salary(rank: R, _: &A) -> B { + C::convert(rank) + } +} diff --git a/primitives/arithmetic/src/traits.rs b/primitives/arithmetic/src/traits.rs index 91e13a832ea08..84e3f60aa6a7d 100644 --- a/primitives/arithmetic/src/traits.rs +++ b/primitives/arithmetic/src/traits.rs @@ -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` +/// and smaller integers. All other conversions are fallible. +pub trait AtLeast16Bit: BaseArithmetic + From {} + +impl> AtLeast16Bit for T {} + +/// A meta trait for arithmetic. Same as [`AtLeast16Bit `], but also bounded to be unsigned. +pub trait AtLeast16BitUnsigned: AtLeast16Bit + Unsigned {} + +impl 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