diff --git a/Cargo.lock b/Cargo.lock index 2f2571bfa85feac6f8f8ed60f8e10cd386343377..a4b1cd6e0e15cf89ab6a5e10faadb6eec51968aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9656,6 +9656,7 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-ranked-collective", "parity-scale-codec", "scale-info", "sp-arithmetic", diff --git a/substrate/frame/core-fellowship/Cargo.toml b/substrate/frame/core-fellowship/Cargo.toml index 0a1706ae22d3c63e3cf4dd5c142db0696488b67b..dc1a0a6c4af8f877aea26e92d7005b42d16b3a84 100644 --- a/substrate/frame/core-fellowship/Cargo.toml +++ b/substrate/frame/core-fellowship/Cargo.toml @@ -27,6 +27,7 @@ sp-core = { path = "../../primitives/core", default-features = false } sp-io = { path = "../../primitives/io", default-features = false } sp-runtime = { path = "../../primitives/runtime", default-features = false } sp-std = { path = "../../primitives/std", default-features = false } +pallet-ranked-collective = { path = "../ranked-collective", default-features = false } [features] default = ["std"] @@ -34,6 +35,7 @@ std = [ "codec/std", "frame-benchmarking?/std", "frame-support/std", + "frame-support/experimental", "frame-system/std", "log/std", "scale-info/std", @@ -42,15 +44,18 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "pallet-ranked-collective/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "pallet-ranked-collective/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "sp-runtime/try-runtime", + "pallet-ranked-collective/try-runtime" ] diff --git a/substrate/frame/core-fellowship/src/lib.rs b/substrate/frame/core-fellowship/src/lib.rs index a0a45c7c594da76c50d6c7bab30a3cae30b4904a..4c824b43bb7b3126b9e324e5bd2ddcc54a23bb63 100644 --- a/substrate/frame/core-fellowship/src/lib.rs +++ b/substrate/frame/core-fellowship/src/lib.rs @@ -47,7 +47,7 @@ //! this explicitly when external factors to this pallet have caused the tracked account to become //! unranked. At rank 0, there is not a "demotion" period after which the account may be bumped to //! become offboarded but rather an "offboard timeout". -//! +//!> //! Candidates may be introduced (i.e. an account to go from unranked to rank of 0) by an origin //! of a different privilege to that for promotion. This allows the possibility for even a single //! existing member to introduce a new candidate without payment. @@ -65,13 +65,16 @@ use sp_runtime::RuntimeDebug; use sp_std::{marker::PhantomData, prelude::*}; use frame_support::{ + defensive, dispatch::DispatchResultWithPostInfo, ensure, impl_ensure_origin_with_arg_ignoring_arg, traits::{ tokens::Balance as BalanceTrait, EnsureOrigin, EnsureOriginWithArg, Get, RankedMembers, + RankedMembersSwapHandler, }, BoundedVec, }; +use pallet_ranked_collective::Rank; #[cfg(test)] mod tests; @@ -249,6 +252,8 @@ pub mod pallet { }, /// Pre-ranked account has been inducted at their current rank. Imported { who: T::AccountId, rank: RankOf<T, I> }, + /// A member had its AccountId swapped. + Swapped { who: T::AccountId, new_who: T::AccountId }, } #[pallet::error] @@ -603,3 +608,29 @@ impl_ensure_origin_with_arg_ignoring_arg! { EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureInducted<T, I, MIN_RANK> {} } + +impl<T: Config<I>, I: 'static> RankedMembersSwapHandler<T::AccountId, Rank> for Pallet<T, I> { + fn swapped(old: &T::AccountId, new: &T::AccountId, _rank: Rank) { + if old == new { + defensive!("Should not try to swap with self"); + return + } + if !Member::<T, I>::contains_key(old) { + defensive!("Should not try to swap non-member"); + return + } + if Member::<T, I>::contains_key(new) { + defensive!("Should not try to overwrite existing member"); + return + } + + if let Some(member) = Member::<T, I>::take(old) { + Member::<T, I>::insert(new, member); + } + if let Some(we) = MemberEvidence::<T, I>::take(old) { + MemberEvidence::<T, I>::insert(new, we); + } + + Self::deposit_event(Event::<T, I>::Swapped { who: old.clone(), new_who: new.clone() }); + } +} diff --git a/substrate/frame/core-fellowship/src/tests/integration.rs b/substrate/frame/core-fellowship/src/tests/integration.rs new file mode 100644 index 0000000000000000000000000000000000000000..ded12aaf1a4d3521733209eb106aa539652f776e --- /dev/null +++ b/substrate/frame/core-fellowship/src/tests/integration.rs @@ -0,0 +1,389 @@ +// 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. + +//! Integration test together with the ranked-collective pallet. + +use std::collections::BTreeMap; + +use frame_support::{ + assert_noop, assert_ok, derive_impl, hypothetically, ord_parameter_types, + pallet_prelude::Weight, + parameter_types, + traits::{ + ConstU16, ConstU32, ConstU64, EitherOf, Everything, IsInVec, MapSuccess, PollStatus, + Polling, TryMapSuccess, + }, +}; +use frame_system::EnsureSignedBy; +use pallet_ranked_collective::{EnsureRanked, Geometric, Rank, Tally, TallyOf, Votes}; +use sp_core::{Get, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, Convert, IdentityLookup, ReduceBy, TryMorphInto}, + BuildStorage, DispatchError, DispatchResult, +}; +type Class = Rank; +use sp_std::cell::RefCell; + +use crate as pallet_core_fellowship; +use crate::*; + +type Block = frame_system::mocking::MockBlock<Test>; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + CoreFellowship: pallet_core_fellowship, + Club: pallet_ranked_collective, + } +); + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1_000_000, u64::max_value())); +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup<Self::AccountId>; + type Block = Block; + 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>; +} + +parameter_types! { + pub static MinRankOfClassDelta: Rank = 0; +} + +thread_local! { + pub static CLUB: RefCell<BTreeMap<u64, u16>> = RefCell::new(BTreeMap::new()); +} + +pub struct TestClub; +impl RankedMembers for TestClub { + type AccountId = u64; + type Rank = u16; + fn min_rank() -> Self::Rank { + 0 + } + fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> { + CLUB.with(|club| club.borrow().get(who).cloned()) + } + fn induct(who: &Self::AccountId) -> DispatchResult { + CLUB.with(|club| club.borrow_mut().insert(*who, 0)); + Ok(()) + } + fn promote(who: &Self::AccountId) -> DispatchResult { + CLUB.with(|club| { + club.borrow_mut().entry(*who).and_modify(|r| *r += 1); + }); + Ok(()) + } + fn demote(who: &Self::AccountId) -> DispatchResult { + CLUB.with(|club| match Self::rank_of(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: u16) { + CLUB.with(|club| club.borrow_mut().insert(who, rank)); +} + +parameter_types! { + pub ZeroToNine: Vec<u64> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + pub EvidenceSize: u32 = 1024; +} +ord_parameter_types! { + pub const One: u64 = 1; +} + +impl Config for Test { + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type Members = TestClub; + type Balance = u64; + type ParamsOrigin = EnsureSignedBy<One, u64>; + type InductOrigin = EnsureInducted<Test, (), 1>; + type ApproveOrigin = TryMapSuccess<EnsureSignedBy<IsInVec<ZeroToNine>, u64>, TryMorphInto<u16>>; + type PromoteOrigin = TryMapSuccess<EnsureSignedBy<IsInVec<ZeroToNine>, u64>, TryMorphInto<u16>>; + type EvidenceSize = EvidenceSize; +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum TestPollState { + Ongoing(TallyOf<Test>, Rank), + Completed(u64, bool), +} +use TestPollState::*; + +parameter_types! { + pub static Polls: BTreeMap<u8, TestPollState> = vec![ + (1, Completed(1, true)), + (2, Completed(2, false)), + (3, Ongoing(Tally::from_parts(0, 0, 0), 1)), + ].into_iter().collect(); +} + +pub struct TestPolls; +impl Polling<TallyOf<Test>> for TestPolls { + type Index = u8; + type Votes = Votes; + type Moment = u64; + type Class = Class; + fn classes() -> Vec<Self::Class> { + vec![0, 1, 2] + } + fn as_ongoing(index: u8) -> Option<(TallyOf<Test>, Self::Class)> { + Polls::get().remove(&index).and_then(|x| { + if let TestPollState::Ongoing(t, c) = x { + Some((t, c)) + } else { + None + } + }) + } + fn access_poll<R>( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>) -> R, + ) -> R { + let mut polls = Polls::get(); + let entry = polls.get_mut(&index); + let r = match entry { + Some(Ongoing(ref mut tally_mut_ref, class)) => + f(PollStatus::Ongoing(tally_mut_ref, *class)), + Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), + None => f(PollStatus::None), + }; + Polls::set(polls); + r + } + fn try_access_poll<R>( + index: Self::Index, + f: impl FnOnce( + PollStatus<&mut TallyOf<Test>, Self::Moment, Self::Class>, + ) -> Result<R, DispatchError>, + ) -> Result<R, DispatchError> { + let mut polls = Polls::get(); + let entry = polls.get_mut(&index); + let r = match entry { + Some(Ongoing(ref mut tally_mut_ref, class)) => + f(PollStatus::Ongoing(tally_mut_ref, *class)), + Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), + None => f(PollStatus::None), + }?; + Polls::set(polls); + Ok(r) + } + + #[cfg(feature = "runtime-benchmarks")] + fn create_ongoing(class: Self::Class) -> Result<Self::Index, ()> { + let mut polls = Polls::get(); + let i = polls.keys().rev().next().map_or(0, |x| x + 1); + polls.insert(i, Ongoing(Tally::new(class), class)); + Polls::set(polls); + Ok(i) + } + + #[cfg(feature = "runtime-benchmarks")] + fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> { + let mut polls = Polls::get(); + match polls.get(&index) { + Some(Ongoing(..)) => {}, + _ => return Err(()), + } + let now = frame_system::Pallet::<Test>::block_number(); + polls.insert(index, Completed(now, approved)); + Polls::set(polls); + Ok(()) + } +} + +/// Convert the tally class into the minimum rank required to vote on the poll. +/// MinRank(Class) = Class - Delta +pub struct MinRankOfClass<Delta>(PhantomData<Delta>); +impl<Delta: Get<Rank>> Convert<Class, Rank> for MinRankOfClass<Delta> { + fn convert(a: Class) -> Rank { + a.saturating_sub(Delta::get()) + } +} + +impl pallet_ranked_collective::Config for Test { + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type PromoteOrigin = EitherOf< + // Root can promote arbitrarily. + frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>, + // Members can promote up to the rank of 2 below them. + MapSuccess<EnsureRanked<Test, (), 2>, ReduceBy<ConstU16<2>>>, + >; + type DemoteOrigin = EitherOf< + // Root can demote arbitrarily. + frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>, + // Members can demote up to the rank of 3 below them. + MapSuccess<EnsureRanked<Test, (), 3>, ReduceBy<ConstU16<3>>>, + >; + type ExchangeOrigin = EitherOf< + // Root can exchange arbitrarily. + frame_system::EnsureRootWithSuccess<Self::AccountId, ConstU16<65535>>, + // Members can exchange up to the rank of 2 below them. + MapSuccess<EnsureRanked<Test, (), 2>, ReduceBy<ConstU16<2>>>, + >; + type Polls = TestPolls; + type MinRankOfClass = MinRankOfClass<MinRankOfClassDelta>; + type VoteWeight = Geometric; + type MemberSwappedHandler = CoreFellowship; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + let params = ParamsType { + active_salary: [10, 20, 30, 40, 50, 60, 70, 80, 90], + passive_salary: [1, 2, 3, 4, 5, 6, 7, 8, 9], + demotion_period: [2, 4, 6, 8, 10, 12, 14, 16, 18], + min_promotion_period: [3, 6, 9, 12, 15, 18, 21, 24, 27], + offboard_timeout: 1, + }; + assert_ok!(CoreFellowship::set_params(signed(1), Box::new(params))); + System::set_block_number(1); + }); + ext +} + +fn signed(who: u64) -> RuntimeOrigin { + RuntimeOrigin::signed(who) +} + +fn assert_last_event(generic_event: <Test as Config>::RuntimeEvent) { + let events = frame_system::Pallet::<Test>::events(); + let system_event: <Test as frame_system::Config>::RuntimeEvent = generic_event.into(); + let frame_system::EventRecord { event, .. } = events.last().expect("Event expected"); + assert_eq!(event, &system_event.into()); +} + +fn evidence(e: u32) -> Evidence<Test, ()> { + e.encode() + .into_iter() + .cycle() + .take(1024) + .collect::<Vec<_>>() + .try_into() + .expect("Static length matches") +} + +#[test] +fn swap_simple_works() { + new_test_ext().execute_with(|| { + for i in 0u16..9 { + let acc = i as u64; + + assert_ok!(Club::add_member(RuntimeOrigin::root(), acc)); + set_rank(acc, i); + assert_ok!(CoreFellowship::import(signed(acc))); + + // Swapping normally works: + assert_ok!(Club::exchange_member(RuntimeOrigin::root(), acc, acc + 10)); + assert_last_event(Event::Swapped { who: acc, new_who: acc + 10 }.into()); + } + }); +} + +/// Exhaustively test that adding member `1` is equivalent to adding member `0` and then swapping. +/// +/// The member also submits evidence before the swap. +#[test] +fn swap_exhaustive_works() { + new_test_ext().execute_with(|| { + let root_add = hypothetically!({ + assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); + set_rank(1, 4); + assert_ok!(CoreFellowship::import(signed(1))); + assert_ok!(CoreFellowship::submit_evidence(signed(1), Wish::Retention, evidence(1))); + + // The events mess up the storage root: + System::reset_events(); + sp_io::storage::root(sp_runtime::StateVersion::V1) + }); + + let root_swap = hypothetically!({ + assert_ok!(Club::add_member(RuntimeOrigin::root(), 0)); + set_rank(0, 4); + assert_ok!(CoreFellowship::import(signed(0))); + assert_ok!(CoreFellowship::submit_evidence(signed(0), Wish::Retention, evidence(1))); + + // Now we swap: + assert_ok!(Club::exchange_member(RuntimeOrigin::root(), 0, 1)); + + System::reset_events(); + sp_io::storage::root(sp_runtime::StateVersion::V1) + }); + + assert_eq!(root_add, root_swap); + }); +} + +#[test] +fn swap_bad_noops() { + new_test_ext().execute_with(|| { + assert_ok!(Club::add_member(RuntimeOrigin::root(), 0)); + set_rank(0, 0); + assert_ok!(CoreFellowship::import(signed(0))); + assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); + set_rank(1, 1); + assert_ok!(CoreFellowship::import(signed(1))); + + // Swapping for another member is a noop: + assert_noop!( + Club::exchange_member(RuntimeOrigin::root(), 0, 1), + pallet_ranked_collective::Error::<Test>::AlreadyMember + ); + // Swapping for the same member is a noop: + assert_noop!( + Club::exchange_member(RuntimeOrigin::root(), 0, 0), + pallet_ranked_collective::Error::<Test>::SameMember + ); + }); +} diff --git a/substrate/frame/core-fellowship/src/tests/mod.rs b/substrate/frame/core-fellowship/src/tests/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..4c657a8e877798f89ef26b5c185bf6349661457b --- /dev/null +++ b/substrate/frame/core-fellowship/src/tests/mod.rs @@ -0,0 +1,21 @@ +// 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. + +//! Tests for this crate. + +mod integration; +mod unit; diff --git a/substrate/frame/core-fellowship/src/tests.rs b/substrate/frame/core-fellowship/src/tests/unit.rs similarity index 99% rename from substrate/frame/core-fellowship/src/tests.rs rename to substrate/frame/core-fellowship/src/tests/unit.rs index 838bd9bbdc80e57a760ea13693bf2c07fdcd62b4..26f88100d08ba8b9e36a706b6646d434b081664f 100644 --- a/substrate/frame/core-fellowship/src/tests.rs +++ b/substrate/frame/core-fellowship/src/tests/unit.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! The crate's tests. +//! The crate's unit tests. use std::collections::BTreeMap; @@ -33,8 +33,8 @@ use sp_runtime::{ }; use sp_std::cell::RefCell; -use super::*; use crate as pallet_core_fellowship; +use crate::*; type Block = frame_system::mocking::MockBlock<Test>; diff --git a/substrate/frame/ranked-collective/src/lib.rs b/substrate/frame/ranked-collective/src/lib.rs index 88631884c5a781520323befe8f802c172a6fa48e..c8291ab46093b18d2e8256eeb2e3a2f6eb87062e 100644 --- a/substrate/frame/ranked-collective/src/lib.rs +++ b/substrate/frame/ranked-collective/src/lib.rs @@ -54,7 +54,10 @@ use sp_std::{marker::PhantomData, prelude::*}; use frame_support::{ dispatch::{DispatchResultWithPostInfo, PostDispatchInfo}, ensure, impl_ensure_origin_with_arg_ignoring_arg, - traits::{EnsureOrigin, EnsureOriginWithArg, PollStatus, Polling, RankedMembers, VoteTally}, + traits::{ + EnsureOrigin, EnsureOriginWithArg, PollStatus, Polling, RankedMembers, + RankedMembersSwapHandler, VoteTally, + }, CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; @@ -402,6 +405,12 @@ pub mod pallet { /// "a rank of at least the poll class". type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>; + /// An external handler that will be notified when two members are swapped. + type MemberSwappedHandler: RankedMembersSwapHandler< + <Pallet<Self, I> as RankedMembers>::AccountId, + <Pallet<Self, I> as RankedMembers>::Rank, + >; + /// Convert a rank_delta into a number of votes the rank gets. /// /// Rank_delta is defined as the number of ranks above the minimum required to take part @@ -679,7 +688,12 @@ pub mod pallet { Self::do_remove_member_from_rank(&who, rank)?; Self::do_add_member_to_rank(new_who.clone(), rank, false)?; - Self::deposit_event(Event::MemberExchanged { who, new_who }); + Self::deposit_event(Event::MemberExchanged { + who: who.clone(), + new_who: new_who.clone(), + }); + T::MemberSwappedHandler::swapped(&who, &new_who, rank); + Ok(()) } } diff --git a/substrate/frame/ranked-collective/src/tests.rs b/substrate/frame/ranked-collective/src/tests.rs index f5e83c48acd05498d25865b44585633fc462c0ff..5c4ef0673c16fa96dca27722606a757ed4d203f9 100644 --- a/substrate/frame/ranked-collective/src/tests.rs +++ b/substrate/frame/ranked-collective/src/tests.rs @@ -173,6 +173,7 @@ impl Config for Test { type Polls = TestPolls; type MinRankOfClass = MinRankOfClass<MinRankOfClassDelta>; type VoteWeight = Geometric; + type MemberSwappedHandler = (); } pub fn new_test_ext() -> sp_io::TestExternalities { @@ -602,3 +603,21 @@ fn exchange_member_works() { ); }); } + +#[test] +fn exchange_member_same_noops() { + new_test_ext().execute_with(|| { + assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); + assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); + + // Swapping the same accounts is a noop: + assert_noop!(Club::exchange_member(RuntimeOrigin::root(), 1, 1), Error::<Test>::SameMember); + // Swapping with a different member is a noop: + assert_noop!( + Club::exchange_member(RuntimeOrigin::root(), 1, 2), + Error::<Test>::AlreadyMember + ); + }); +} diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index a18faee680596340999cd784597825dee00f3391..2a42fca76b3c57623c74e3724b342eadf57737cd 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -37,7 +37,7 @@ pub use members::{AllowAll, DenyAll, Filter}; pub use members::{ AsContains, ChangeMembers, Contains, ContainsLengthBound, ContainsPair, Equals, Everything, EverythingBut, FromContainsPair, InitializeMembers, InsideBoth, IsInVec, Nothing, - RankedMembers, SortedMembers, TheseExcept, + RankedMembers, RankedMembersSwapHandler, SortedMembers, TheseExcept, }; mod validation; diff --git a/substrate/frame/support/src/traits/members.rs b/substrate/frame/support/src/traits/members.rs index d667eaa7e9d8d2b46b39254fccf5e0c6052c9751..892374bdbca472c418ccc744fe7f4e7902d62818 100644 --- a/substrate/frame/support/src/traits/members.rs +++ b/substrate/frame/support/src/traits/members.rs @@ -297,6 +297,16 @@ pub trait RankedMembers { fn demote(who: &Self::AccountId) -> DispatchResult; } +/// Handler that can deal with the swap of two members. +pub trait RankedMembersSwapHandler<AccountId, Rank> { + /// Member `old` was swapped with `new` at `rank`. + fn swapped(who: &AccountId, new_who: &AccountId, rank: Rank); +} + +impl<AccountId, Rank> RankedMembersSwapHandler<AccountId, Rank> for () { + fn swapped(_: &AccountId, _: &AccountId, _: Rank) {} +} + /// Trait for type that can handle the initialization of account IDs at genesis. pub trait InitializeMembers<AccountId> { /// Initialize the members to the given `members`.