Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Salary pallet #13378

Merged
merged 46 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e3a5bb2
More drafting
gavofyork Oct 7, 2022
3ff9ce8
Merge remote-tracking branch 'origin/master' into gav-paymaster
gavofyork Oct 7, 2022
07ab87a
Paymaster pallet
gavofyork Oct 11, 2022
88e11ce
Merge remote-tracking branch 'origin/master' into gav-paymaster
gavofyork Nov 30, 2022
8200850
Merge remote-tracking branch 'origin/master' into gav-paymaster
gavofyork Feb 12, 2023
958d34f
Fix build
gavofyork Feb 13, 2023
91cb1f5
More tests
gavofyork Feb 13, 2023
c53d372
Rename
gavofyork Feb 13, 2023
0806559
Rename
gavofyork Feb 13, 2023
5cadabf
Renaming
gavofyork Feb 13, 2023
712118b
Revert old changes
gavofyork Feb 13, 2023
07b785c
Multi-phase payouts to avoid bank-runs
gavofyork Feb 15, 2023
7fb95d0
Tests
gavofyork Feb 15, 2023
5c38814
Tests
gavofyork Feb 16, 2023
bfdf333
Allow payment to be targeted elsewhere
gavofyork Feb 16, 2023
807a3c9
Proper ssync payment failure handling
gavofyork Feb 16, 2023
cf1f365
Test for repayment
gavofyork Feb 22, 2023
d829078
Docs
gavofyork Feb 22, 2023
7162964
Impl RankedMembers for RankedCollective
gavofyork Feb 26, 2023
2e299f4
Implement Pay for Pot (i.e. basic account).
gavofyork Feb 26, 2023
5995eeb
Benchmarks
gavofyork Feb 26, 2023
c9c2ce8
Weights
gavofyork Feb 26, 2023
16bc55a
Introduce Salary benchmark into node
gavofyork Feb 26, 2023
2fc7fab
Fix warning
gavofyork Feb 26, 2023
575f426
Merge branch 'master' of https://github.com/paritytech/substrate into…
Feb 26, 2023
42a7927
".git/.scripts/commands/bench/bench.sh" pallet dev pallet_salary
Feb 26, 2023
7dc1529
Update primitives/arithmetic/src/traits.rs
gavofyork Feb 27, 2023
366267c
Update frame/salary/src/lib.rs
gavofyork Feb 27, 2023
26f1ab0
Update lib.rs
gavofyork Feb 27, 2023
872a047
Update frame/salary/src/lib.rs
gavofyork Feb 27, 2023
e2fd134
Docs
gavofyork Feb 27, 2023
8d8a5bf
Update frame/salary/src/lib.rs
gavofyork Feb 27, 2023
9849a65
Update frame/salary/src/lib.rs
gavofyork Feb 27, 2023
0b4aafa
Fix
gavofyork Feb 27, 2023
03199ae
Merge branch 'gav-paymaster' of github.com:paritytech/substrate into …
gavofyork Feb 27, 2023
a21b1bc
Fixes
gavofyork Feb 27, 2023
4c85f02
Fixes
gavofyork Feb 27, 2023
d5696c1
Move some salary traits stuff to a shared location
gavofyork Mar 1, 2023
ddd4d3e
Fix
gavofyork Mar 2, 2023
3704efe
Update frame/salary/src/lib.rs
gavofyork Mar 3, 2023
93a845d
Update frame/salary/src/lib.rs
gavofyork Mar 3, 2023
e5c0733
Mul floor
gavofyork Mar 3, 2023
5d24050
Merge branch 'gav-paymaster' of github.com:paritytech/substrate into …
gavofyork Mar 3, 2023
b3d160e
Fix warnings
gavofyork Mar 3, 2023
b9623a4
Fix test
gavofyork Mar 3, 2023
c08c010
Docs
gavofyork Mar 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ members = [
"frame/recovery",
"frame/referenda",
"frame/remark",
"frame/salary",
"frame/scheduler",
"frame/scored-pool",
"frame/session",
Expand Down
2 changes: 2 additions & 0 deletions frame/ranked-collective/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,5 @@ pub mod pallet {
}
}
}

// TODO: Impl `RankedMembers` for this pallet.
49 changes: 49 additions & 0 deletions frame/salary/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
name = "pallet-salary"
version = "4.0.0-dev"
authors = ["Parity Technologies <[email protected]>"]
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 changes: 3 additions & 0 deletions frame/salary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Salary

Make periodic payment to members of a ranked collective according to rank.
45 changes: 45 additions & 0 deletions frame/salary/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.

//! Staking pallet benchmarking.

use super::*;
#[allow(unused_imports)]
use crate::Pallet as RankedCollective;

use frame_benchmarking::{account, benchmarks_instance_pallet, whitelisted_caller};
use frame_support::{assert_ok, dispatch::UnfilteredDispatchable};
use frame_system::RawOrigin as SystemOrigin;

const SEED: u32 = 0;

fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_last_event(generic_event.into());
}

benchmarks_instance_pallet! {
add_member {
let origin = T::PromoteOrigin::successful_origin();
let call = Call::<T, I>::add_member { };
}: { call.dispatch_bypass_filter(origin)? }
verify {
assert_eq!(MemberCount::<T, I>::get(0), 1);
assert_last_event::<T, I>(Event::MemberAdded { who }.into());
}

impl_benchmark_test_suite!(RankedCollective, crate::tests::new_test_ext(), crate::tests::Test);
}
245 changes: 245 additions & 0 deletions frame/salary/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// 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_runtime::traits::{AtLeast32BitUnsigned, CheckedSub, Convert};
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};

use frame_support::{
dispatch::DispatchResultWithPostInfo, ensure, traits::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;

// Can be implemented by Pot pallet with a fixed Currency 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: AtLeast32BitUnsigned + FullCodec + MaxEncodedLen + TypeInfo + Debug;
/// 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;
/// The amount of currency with which we have to make payments in this period. It may be a fixed
/// value or reduce as calls to `pay` are made. It should be called once prior to the series of
/// payments to determine the overall budget and then not again until the next series of
/// payments are to be made.
fn budget() -> Self::Balance;
/// 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, ()>;
}

#[derive(Encode, Decode, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen, RuntimeDebug)]
pub struct StatusType<CycleIndex, BlockNumber, Balance> {
cycle_index: CycleIndex,
cycle_start: BlockNumber,
remaining_budget: Balance,
}

#[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.
type ActiveSalaryForRank: Convert<
<Self::Members as RankedMembers>::Rank,
<Self::Paymaster as Pay>::Balance,
>;

/// The number of blocks between sequential payout cycles.
#[pallet::constant]
type CyclePeriod: Get<Self::BlockNumber>;
}

pub type CycleIndexOf<T> = <T as frame_system::Config>::BlockNumber;
pub type StatusOf<T, I> = StatusType<
CycleIndexOf<T>,
<T as frame_system::Config>::BlockNumber,
<<T as Config<I>>::Paymaster as Pay>::Balance,
>;

/// The current payout cycle, the block nunber at which it started and the remaining balance in
/// this cycle's budget.
#[pallet::storage]
pub(super) type Status<T: Config<I>, I: 'static = ()> =
StorageValue<_, StatusOf<T, I>, OptionQuery>;

/// The most recent cycle index which was paid to a member. None implies that a member has not
/// yet been paid.
#[pallet::storage]
pub(super) type LastClaim<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::AccountId, CycleIndexOf<T>, 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 payment happened.
Paid { who: T::AccountId, id: <T::Paymaster as Pay>::Id },
/// The next cycle begins.
CycleStarted { index: CycleIndexOf<T>, budget: <T::Paymaster as Pay>::Balance },
}

#[pallet::error]
pub enum Error<T, I = ()> {
/// 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,
/// 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,
}

#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Induct oneself into the payout system.
#[pallet::weight(T::WeightInfo::add_member())]
#[pallet::call_index(0)]
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!(!LastClaim::<T, I>::contains_key(&who), Error::<T, I>::AlreadyInducted);

LastClaim::<T, I>::insert(&who, cycle_index);

Self::deposit_event(Event::<T, I>::Inducted { who });
Ok(Pays::No.into())
}

/// Request a payout.
///
/// - `origin`: A `Signed` origin of an account which is a member of `Members`.
#[pallet::weight(T::WeightInfo::add_member())]
#[pallet::call_index(1)]
pub fn payout(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let rank = T::Members::rank_of(&who).ok_or(Error::<T, I>::NotMember)?;
let payout = T::ActiveSalaryForRank::convert(rank);
ensure!(!payout.is_zero(), Error::<T, I>::ClaimZero);
let last_claim = Self::last_claim(&who)?;
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
ensure!(last_claim < status.cycle_index, Error::<T, I>::NoClaim);
status.remaining_budget =
status.remaining_budget.checked_sub(&payout).ok_or(Error::<T, I>::Bankrupt)?;

let id = T::Paymaster::pay(&who, payout).map_err(|()| Error::<T, I>::PayError)?;
LastClaim::<T, I>::insert(&who, status.cycle_index);
Status::<T, I>::put(&status);

Self::deposit_event(Event::<T, I>::Paid { who, id });
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 which is a member of `Members`.
#[pallet::weight(T::WeightInfo::add_member())]
#[pallet::call_index(2)]
pub fn next_cycle(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
let now = frame_system::Pallet::<T>::block_number();
let mut status = match Status::<T, I>::get() {
// Not first time... (move along start block and bump index)
Some(mut status) => {
status.cycle_start.saturating_accrue(T::CyclePeriod::get());
ensure!(now >= status.cycle_start, Error::<T, I>::NotYet);
status.cycle_index.saturating_inc();
status
},
// First time... (initialize)
None => StatusType {
cycle_index: Zero::zero(),
cycle_start: now,
remaining_budget: Zero::zero(),
},
};
status.remaining_budget = T::Paymaster::budget();

Status::<T, I>::put(&status);

Self::deposit_event(Event::<T, I>::CycleStarted {
index: status.cycle_index,
budget: status.remaining_budget,
});
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_claim(who: &T::AccountId) -> Result<CycleIndexOf<T>, DispatchError> {
LastClaim::<T, I>::get(&who).ok_or(Error::<T, I>::NotInducted.into())
}
}
}
Loading