Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
Promote/demote by rank
  • Loading branch information
gavofyork committed May 31, 2022
commit 0ee3e2808dff49df9bf6e2baf37ad2ab65bb0bdc
66 changes: 60 additions & 6 deletions frame/ranked-collective/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,28 @@ impl<T: Config<I>, I: 'static> GetMaxVoters for Pallet<T, I> {
pub struct EnsureRanked<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
for EnsureRanked<T, I, MIN_RANK>
{
type Success = Rank;

fn try_origin(o: T::Origin) -> Result<Self::Success, T::Origin> {
let who = frame_system::EnsureSigned::try_origin(o)?;
match Members::<T, I>::get(&who) {
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(rank),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}

#[cfg(feature = "runtime-benchmarks")]
fn successful_origin() -> T::Origin {
let who = IndexToId::<T, I>::get(MIN_RANK, 0)
.expect("Must be at least one member at rank for a successful origin");
frame_system::RawOrigin::Signed(who).into()
}
}

pub struct EnsureMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
for EnsureMember<T, I, MIN_RANK>
{
type Success = T::AccountId;

Expand All @@ -239,6 +261,28 @@ impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
}
}

pub struct EnsureRankedMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::Origin>
for EnsureRankedMember<T, I, MIN_RANK>
{
type Success = (T::AccountId, Rank);

fn try_origin(o: T::Origin) -> Result<Self::Success, T::Origin> {
let who = frame_system::EnsureSigned::try_origin(o)?;
match Members::<T, I>::get(&who) {
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok((who, rank)),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}

#[cfg(feature = "runtime-benchmarks")]
fn successful_origin() -> T::Origin {
let who = IndexToId::<T, I>::get(MIN_RANK, 0)
.expect("Must be at least one member at rank for a successful origin");
frame_system::RawOrigin::Signed(who).into()
}
}

#[frame_support::pallet]
pub mod pallet {
use super::*;
Expand All @@ -258,8 +302,13 @@ pub mod pallet {
/// The outer event type.
type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>;

/// The origin required to add, promote or remove a member.
type AdminOrigin: EnsureOrigin<Self::Origin>;
/// The origin required to add or promote a mmember. The success value indicates the
/// maximum rank *to which* the promotion may be.
type PromoteOrigin: EnsureOrigin<Self::Origin, Success = Rank>;

/// The origin required to demote or remove a member. The success value indicates the
/// maximum rank *from which* the demotion/removal may be.
type DemoteOrigin: EnsureOrigin<Self::Origin, Success = Rank>;

/// The polling system used for our voting.
type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = Self::BlockNumber>;
Expand Down Expand Up @@ -345,6 +394,8 @@ pub mod pallet {
RankTooLow,
/// The information provided is incorrect.
InvalidWitness,
/// The origin is not sufficiently privileged to do the operation.
NoPermission,
}

#[pallet::call]
Expand All @@ -358,7 +409,7 @@ pub mod pallet {
/// Weight: `O(1)`
#[pallet::weight(T::WeightInfo::add_member())]
pub fn add_member(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
let _ = T::PromoteOrigin::ensure_origin(origin)?;
ensure!(!Members::<T, I>::contains_key(&who), Error::<T, I>::AlreadyMember);
let index = MemberCount::<T, I>::get(0);
let count = index.checked_add(1).ok_or(Overflow)?;
Expand All @@ -380,9 +431,10 @@ pub mod pallet {
/// Weight: `O(1)`
#[pallet::weight(T::WeightInfo::promote_member(0))]
pub fn promote_member(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
let max_rank = T::PromoteOrigin::ensure_origin(origin)?;
let record = Self::ensure_member(&who)?;
let rank = record.rank.checked_add(1).ok_or(Overflow)?;
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
let index = MemberCount::<T, I>::get(rank);
MemberCount::<T, I>::insert(rank, index.checked_add(1).ok_or(Overflow)?);
IdToIndex::<T, I>::insert(rank, &who, index);
Expand All @@ -402,9 +454,10 @@ pub mod pallet {
/// Weight: `O(1)`, less if the member's index is highest in its rank.
#[pallet::weight(T::WeightInfo::demote_member(0))]
pub fn demote_member(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
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);
Expand Down Expand Up @@ -435,9 +488,10 @@ pub mod pallet {
who: T::AccountId,
min_rank: Rank,
) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin(origin)?;
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
ensure!(min_rank >= rank, Error::<T, I>::InvalidWitness);
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);

for r in 0..=rank {
Self::remove_from_rank(&who, r)?;
Expand Down
67 changes: 65 additions & 2 deletions frame/ranked-collective/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use std::collections::BTreeMap;

use frame_support::{
assert_noop, assert_ok, parameter_types,
traits::{ConstU32, ConstU64, Everything, Polling},
traits::{ConstU32, ConstU64, Everything, Polling, ConstU16, EitherOf, MapSuccess, ReduceBy}, error::BadOrigin,
};
use sp_core::H256;
use sp_runtime::{
Expand Down Expand Up @@ -169,7 +169,16 @@ impl Polling<TallyOf<Test>> for TestPolls {
impl Config for Test {
type WeightInfo = ();
type Event = Event;
type AdminOrigin = frame_system::EnsureRoot<Self::AccountId>;
type PromoteOrigin = EitherOf<
// Members can promote up to the rank of 2 below them.
MapSuccess<EnsureRanked<Test, (), 2>, ReduceBy<ConstU16<2>>>,
frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>
>;
type DemoteOrigin = EitherOf<
// Members can demote up to the rank of 3 below them.
MapSuccess<EnsureRanked<Test, (), 3>, ReduceBy<ConstU16<3>>>,
frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>
>;
type Polls = TestPolls;
type MinRankOfClass = Identity;
type VoteWeight = Geometric;
Expand Down Expand Up @@ -295,6 +304,59 @@ fn promote_demote_works() {
});
}

#[test]
fn promote_demote_by_rank_works() {
new_test_ext().execute_with(|| {
assert_ok!(Club::add_member(Origin::root(), 1));
for _ in 0..7 { assert_ok!(Club::promote_member(Origin::root(), 1)); };

// #1 can add #2 and promote to rank 1
assert_ok!(Club::add_member(Origin::signed(1), 2));
assert_ok!(Club::promote_member(Origin::signed(1), 2));
// #2 as rank 1 cannot do anything privileged
assert_noop!(Club::add_member(Origin::signed(2), 3), BadOrigin);

assert_ok!(Club::promote_member(Origin::signed(1), 2));
// #2 as rank 2 can add #3.
assert_ok!(Club::add_member(Origin::signed(2), 3));

// #2 as rank 2 cannot promote #3 to rank 1
assert_noop!(Club::promote_member(Origin::signed(2), 3), Error::<Test>::NoPermission);

// #1 as rank 7 can promote #2 only up to rank 5 and once there cannot demote them.
assert_ok!(Club::promote_member(Origin::signed(1), 2));
assert_ok!(Club::promote_member(Origin::signed(1), 2));
assert_ok!(Club::promote_member(Origin::signed(1), 2));
assert_noop!(Club::promote_member(Origin::signed(1), 2), Error::<Test>::NoPermission);
assert_noop!(Club::demote_member(Origin::signed(1), 2), Error::<Test>::NoPermission);

// #2 as rank 5 can promote #3 only up to rank 3 and once there cannot demote them.
assert_ok!(Club::promote_member(Origin::signed(2), 3));
assert_ok!(Club::promote_member(Origin::signed(2), 3));
assert_ok!(Club::promote_member(Origin::signed(2), 3));
assert_noop!(Club::promote_member(Origin::signed(2), 3), Error::<Test>::NoPermission);
assert_noop!(Club::demote_member(Origin::signed(2), 3), Error::<Test>::NoPermission);

// #2 can add #4 & #5 as rank 0 and #6 & #7 as rank 1.
assert_ok!(Club::add_member(Origin::signed(2), 4));
assert_ok!(Club::add_member(Origin::signed(2), 5));
assert_ok!(Club::add_member(Origin::signed(2), 6));
assert_ok!(Club::promote_member(Origin::signed(2), 6));
assert_ok!(Club::add_member(Origin::signed(2), 7));
assert_ok!(Club::promote_member(Origin::signed(2), 7));

// #3 as rank 3 can demote/remove #4 & #5 but not #6 & #7
assert_ok!(Club::demote_member(Origin::signed(3), 4));
assert_ok!(Club::remove_member(Origin::signed(3), 5, 0));
assert_noop!(Club::demote_member(Origin::signed(3), 6), Error::<Test>::NoPermission);
assert_noop!(Club::remove_member(Origin::signed(3), 7, 1), Error::<Test>::NoPermission);

// #2 as rank 5 can demote/remove #6 & #7
assert_ok!(Club::demote_member(Origin::signed(2), 6));
assert_ok!(Club::remove_member(Origin::signed(2), 7, 1));
});
}

#[test]
fn voting_works() {
new_test_ext().execute_with(|| {
Expand Down Expand Up @@ -387,3 +449,4 @@ fn ensure_ranked_works() {
assert_eq!(Rank4::try_origin(Origin::signed(3)).unwrap_err().as_signed().unwrap(), 3);
});
}

3 changes: 2 additions & 1 deletion frame/support/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ mod dispatch;
pub use dispatch::EnsureOneOf;
pub use dispatch::{
AsEnsureOriginWithArg, DispatchableWithStorageLayer, EitherOf, EitherOfDiverse, EnsureOrigin,
EnsureOriginWithArg, NeverEnsureOrigin, OriginTrait, UnfilteredDispatchable,
EnsureOriginWithArg, NeverEnsureOrigin, OriginTrait, UnfilteredDispatchable, MapSuccess, TryMapSuccess,
ReduceBy,
};

mod voting;
Expand Down
55 changes: 54 additions & 1 deletion frame/support/src/traits/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@

//! Traits for dealing with dispatching calls and the origin from which they are dispatched.

use std::marker::PhantomData;

use crate::dispatch::{DispatchResultWithPostInfo, Parameter, RawOrigin};
use sp_arithmetic::traits::{Zero, CheckedSub};
use sp_runtime::{
traits::{BadOrigin, Member},
traits::{BadOrigin, Member, TryMorph, Morph},
Either,
};

use super::TypedGet;

/// Some sort of check on the origin is performed by this object.
pub trait EnsureOrigin<OuterOrigin> {
/// A return type.
Expand Down Expand Up @@ -102,6 +107,54 @@ impl<OuterOrigin, Argument, EO: EnsureOrigin<OuterOrigin>>
}
}

/// A derivative `EnsureOrigin` implementation. It mutates the `Success` result of an `Original`
/// implementation with a given `Mutator`.
pub struct MapSuccess<Original, Mutator>(PhantomData<(Original, Mutator)>);
impl<
O,
Original: EnsureOrigin<O>,
Mutator: Morph<Original::Success>,
> EnsureOrigin<O> for MapSuccess<Original, Mutator> {
type Success = Mutator::Outcome;
fn try_origin(o: O) -> Result<Mutator::Outcome, O> {
Ok(Mutator::morph(Original::try_origin(o)?))
}
}

/// A derivative `EnsureOrigin` implementation. It mutates the `Success` result of an `Original`
/// implementation with a given `Mutator`, allowing the possibility of an error to be returned
/// from the mutator.
///
/// NOTE: This is strictly worse performance than `MapSuccess` since it clones the original origin
/// value. If possible, use `MapSuccess` instead.
pub struct TryMapSuccess<Orig, Mutator>(PhantomData<(Orig, Mutator)>);
impl<
O: Clone,
Original: EnsureOrigin<O>,
Mutator: TryMorph<Original::Success>,
> EnsureOrigin<O> for TryMapSuccess<Original, Mutator> {
type Success = Mutator::Outcome;
fn try_origin(o: O) -> Result<Mutator::Outcome, O> {
let orig = o.clone();
Mutator::try_morph(Original::try_origin(o)?).map_err(|()| orig)
}
}

pub struct ReduceBy<N>(PhantomData<N>);
impl<N: TypedGet> TryMorph<N::Type> for ReduceBy<N> where N::Type: CheckedSub {
type Outcome = N::Type;
fn try_morph(r: N::Type) -> Result<N::Type, ()> {
r.checked_sub(&N::get()).ok_or(())
}
}
impl<N: TypedGet> Morph<N::Type> for ReduceBy<N> where N::Type: CheckedSub + Zero {
type Outcome = N::Type;
fn morph(r: N::Type) -> N::Type {
r.checked_sub(&N::get()).unwrap_or(Zero::zero())
}
}


/// Type that can be dispatched with an origin but without checking the origin filter.
///
/// Implemented for pallet dispatchable type by `decl_module` and for runtime dispatchable by
Expand Down
37 changes: 37 additions & 0 deletions primitives/runtime/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,43 @@ where
}
}

/// Extensible conversion trait. Generic over only source type, with destination type being
/// associated.
pub trait Morph<A> {
/// The type into which `A` is mutated.
type Outcome;

/// Make conversion.
fn morph(a: A) -> Self::Outcome;
}

/// A structure that performs identity conversion.
impl<T> Morph<T> for Identity {
type Outcome = T;
fn morph(a: T) -> T {
a
}
}


/// Extensible conversion trait. Generic over only source type, with destination type being
/// associated.
pub trait TryMorph<A> {
/// The type into which `A` is mutated.
type Outcome;

/// Make conversion.
fn try_morph(a: A) -> Result<Self::Outcome, ()>;
}

/// A structure that performs identity conversion.
impl<T> TryMorph<T> for Identity {
type Outcome = T;
fn try_morph(a: T) -> Result<T, ()> {
Ok(a)
}
}

/// Extensible conversion trait. Generic over both source and destination types.
pub trait Convert<A, B> {
/// Make conversion.
Expand Down