Introduce default-setting prime for collective (#5137)

* Introduce default-setting prime for collective

* Docs.

* Elections phragmen supports prime

* Fix

* Membership supports prime

* Fix

* Update frame/collective/src/lib.rs

Co-Authored-By: Shawn Tabrizi <shawntabrizi@gmail.com>

Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
This commit is contained in:
Gavin Wood
2020-03-05 15:57:03 +01:00
committed by GitHub
parent d3208aa7bc
commit 0573f1408d
7 changed files with 399 additions and 50 deletions
+12
View File
@@ -68,6 +68,7 @@ use impls::{CurrencyToVoteHandler, Author, LinearWeightToFee, TargetedFeeAdjustm
/// Constant values used within the runtime.
pub mod constants;
use constants::{time::*, currency::*};
use frame_system::Trait;
// Make the WASM binary available.
#[cfg(feature = "std")]
@@ -330,11 +331,16 @@ impl pallet_democracy::Trait for Runtime {
type Slash = Treasury;
}
parameter_types! {
pub const CouncilMotionDuration: BlockNumber = 5 * DAYS;
}
type CouncilCollective = pallet_collective::Instance1;
impl pallet_collective::Trait<CouncilCollective> for Runtime {
type Origin = Origin;
type Proposal = Call;
type Event = Event;
type MotionDuration = CouncilMotionDuration;
}
parameter_types! {
@@ -360,11 +366,16 @@ impl pallet_elections_phragmen::Trait for Runtime {
type TermDuration = TermDuration;
}
parameter_types! {
pub const TechnicalMotionDuration: BlockNumber = 5 * DAYS;
}
type TechnicalCollective = pallet_collective::Instance2;
impl pallet_collective::Trait<TechnicalCollective> for Runtime {
type Origin = Origin;
type Proposal = Call;
type Event = Event;
type MotionDuration = TechnicalMotionDuration;
}
impl pallet_membership::Trait<pallet_membership::Instance1> for Runtime {
@@ -373,6 +384,7 @@ impl pallet_membership::Trait<pallet_membership::Instance1> for Runtime {
type RemoveOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>;
type SwapOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>;
type ResetOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>;
type PrimeOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>;
type MembershipInitialized = TechnicalCommittee;
type MembershipChanged = TechnicalCommittee;
}
+232 -47
View File
@@ -18,7 +18,20 @@
//! through dispatched calls from one of two specialized origins.
//!
//! The membership can be provided in one of two ways: either directly, using the Root-dispatchable
//! function `set_members`, or indirectly, through implementing the `ChangeMembers`
//! function `set_members`, or indirectly, through implementing the `ChangeMembers`.
//!
//! A "prime" member may be set allowing their vote to act as the default vote in case of any
//! abstentions after the voting period.
//!
//! Voting happens through motions comprising a proposal (i.e. a curried dispatchable) plus a
//! number of approvals required for it to pass and be called. Motions are open for members to
//! vote on for a minimum period given by `MotionDuration`. As soon as the needed number of
//! approvals is given, the motion is closed and executed. If the number of approvals is not reached
//! during the voting period, then `close` may be called by any account in order to force the end
//! the motion explicitly. If a prime member is defined then their vote is used in place of any
//! abstentions and the proposal is executed if there are enough approvals counting the new votes.
//!
//! If there are not, or if no prime is set, then the motion is dropped without being executed.
#![cfg_attr(not(feature = "std"), no_std)]
#![recursion_limit="128"]
@@ -30,7 +43,7 @@ use sp_runtime::traits::{Hash, EnsureOrigin};
use frame_support::weights::SimpleDispatchInfo;
use frame_support::{
dispatch::{Dispatchable, Parameter}, codec::{Encode, Decode},
traits::{ChangeMembers, InitializeMembers}, decl_module, decl_event,
traits::{Get, ChangeMembers, InitializeMembers}, decl_module, decl_event,
decl_storage, decl_error, ensure,
};
use frame_system::{self as system, ensure_signed, ensure_root};
@@ -53,6 +66,9 @@ pub trait Trait<I=DefaultInstance>: frame_system::Trait {
/// The outer event type.
type Event: From<Event<Self, I>> + Into<<Self as frame_system::Trait>::Event>;
/// The time-out for council motions.
type MotionDuration: Get<Self::BlockNumber>;
}
/// Origin for the collective module.
@@ -71,7 +87,7 @@ pub type Origin<T, I=DefaultInstance> = RawOrigin<<T as frame_system::Trait>::Ac
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
/// Info for keeping track of a motion being voted on.
pub struct Votes<AccountId> {
pub struct Votes<AccountId, BlockNumber> {
/// The proposal's unique index.
index: ProposalIndex,
/// The number of approval votes that are needed to pass the motion.
@@ -80,6 +96,8 @@ pub struct Votes<AccountId> {
ayes: Vec<AccountId>,
/// The current set of voters that rejected it.
nays: Vec<AccountId>,
/// The hard end time of this vote.
end: BlockNumber,
}
decl_storage! {
@@ -91,11 +109,14 @@ decl_storage! {
map hasher(blake2_256) T::Hash => Option<<T as Trait<I>>::Proposal>;
/// Votes on a given proposal, if it is ongoing.
pub Voting get(fn voting):
map hasher(blake2_256) T::Hash => Option<Votes<T::AccountId>>;
map hasher(blake2_256) T::Hash => Option<Votes<T::AccountId, T::BlockNumber>>;
/// Proposals so far.
pub ProposalCount get(fn proposal_count): u32;
/// The current members of the collective. This is stored sorted (just by value).
pub Members get(fn members): Vec<T::AccountId>;
/// The member who provides the default vote for any other members that do not vote before
/// the timeout. If None, then no member has that privilege.
pub Prime get(fn prime): Option<T::AccountId>;
}
add_extra_genesis {
config(phantom): sp_std::marker::PhantomData<I>;
@@ -123,6 +144,8 @@ decl_event! {
Executed(Hash, bool),
/// A single member did some action; `bool` is true if returned without error.
MemberExecuted(Hash, bool),
/// A proposal was closed after its duration was up.
Closed(Hash, MemberCount, MemberCount),
}
}
@@ -140,6 +163,8 @@ decl_error! {
DuplicateVote,
/// Members are already initialized!
AlreadyInitialized,
/// The close call is made too early, before the end of the voting.
TooEarly,
}
}
@@ -152,19 +177,21 @@ decl_module! {
fn deposit_event() = default;
/// Set the collective's membership manually to `new_members`. Be nice to the chain and
/// provide it pre-sorted.
/// Set the collective's membership.
///
/// - `new_members`: The new member list. Be nice to the chain and
// provide it sorted.
/// - `prime`: The prime member whose vote sets the default.
///
/// Requires root origin.
#[weight = SimpleDispatchInfo::FixedOperational(100_000)]
fn set_members(origin, new_members: Vec<T::AccountId>) {
fn set_members(origin, new_members: Vec<T::AccountId>, prime: Option<T::AccountId>) {
ensure_root(origin)?;
let mut new_members = new_members;
new_members.sort();
<Members<T, I>>::mutate(|m| {
<Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members[..], m);
*m = new_members;
});
let old = Members::<T, I>::get();
<Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members[..], &old);
Prime::<T, I>::set(prime);
}
/// Dispatch a proposal from a member using the `Member` origin.
@@ -202,7 +229,8 @@ decl_module! {
<ProposalCount<I>>::mutate(|i| *i += 1);
<Proposals<T, I>>::mutate(|proposals| proposals.push(proposal_hash));
<ProposalOf<T, I>>::insert(proposal_hash, *proposal);
let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![] };
let end = system::Module::<T>::block_number() + T::MotionDuration::get();
let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![], end };
<Voting<T, I>>::insert(proposal_hash, votes);
Self::deposit_event(RawEvent::Proposed(who, index, proposal_hash, threshold));
@@ -249,32 +277,55 @@ decl_module! {
Self::deposit_event(RawEvent::Voted(who, proposal, approve, yes_votes, no_votes));
let seats = Self::members().len() as MemberCount;
let approved = yes_votes >= voting.threshold;
let disapproved = seats.saturating_sub(no_votes) < voting.threshold;
if approved || disapproved {
if approved {
Self::deposit_event(RawEvent::Approved(proposal));
// execute motion, assuming it exists.
if let Some(p) = <ProposalOf<T, I>>::take(&proposal) {
let origin = RawOrigin::Members(voting.threshold, seats).into();
let ok = p.dispatch(origin).is_ok();
Self::deposit_event(RawEvent::Executed(proposal, ok));
}
} else {
// disapproved
<ProposalOf<T, I>>::remove(&proposal);
Self::deposit_event(RawEvent::Disapproved(proposal));
}
// remove vote
<Voting<T, I>>::remove(&proposal);
<Proposals<T, I>>::mutate(|proposals| proposals.retain(|h| h != &proposal));
Self::finalize_proposal(approved, seats, voting, proposal);
} else {
// update voting
<Voting<T, I>>::insert(&proposal, voting);
Voting::<T, I>::insert(&proposal, voting);
}
}
/// May be called by any signed account after the voting duration has ended in order to
/// finish voting and close the proposal.
///
/// Abstentions are counted as rejections unless there is a prime member set and the prime
/// member cast an approval.
///
/// - the weight of `proposal` preimage.
/// - up to three events deposited.
/// - one read, two removals, one mutation. (plus three static reads.)
/// - computation and i/o `O(P + L + M)` where:
/// - `M` is number of members,
/// - `P` is number of active proposals,
/// - `L` is the encoded length of `proposal` preimage.
#[weight = SimpleDispatchInfo::FixedOperational(200_000)]
fn close(origin, proposal: T::Hash, #[compact] index: ProposalIndex) {
let _ = ensure_signed(origin)?;
let voting = Self::voting(&proposal).ok_or(Error::<T, I>::ProposalMissing)?;
ensure!(voting.index == index, Error::<T, I>::WrongIndex);
ensure!(system::Module::<T>::block_number() >= voting.end, Error::<T, I>::TooEarly);
// default to true only if there's a prime and they voted in favour.
let default = Self::prime().map_or(
false,
|who| voting.ayes.iter().any(|a| a == &who),
);
let mut no_votes = voting.nays.len() as MemberCount;
let mut yes_votes = voting.ayes.len() as MemberCount;
let seats = Self::members().len() as MemberCount;
let abstentions = seats - (yes_votes + no_votes);
match default {
true => yes_votes += abstentions,
false => no_votes += abstentions,
}
Self::deposit_event(RawEvent::Closed(proposal, yes_votes, no_votes));
Self::finalize_proposal(yes_votes >= voting.threshold, seats, voting, proposal);
}
}
}
@@ -282,10 +333,54 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
pub fn is_member(who: &T::AccountId) -> bool {
Self::members().contains(who)
}
/// Weight:
/// If `approved`:
/// - the weight of `proposal` preimage.
/// - two events deposited.
/// - two removals, one mutation.
/// - computation and i/o `O(P + L)` where:
/// - `P` is number of active proposals,
/// - `L` is the encoded length of `proposal` preimage.
///
/// If not `approved`:
/// - one event deposited.
/// Two removals, one mutation.
/// Computation and i/o `O(P)` where:
/// - `P` is number of active proposals
fn finalize_proposal(
approved: bool,
seats: MemberCount,
voting: Votes<T::AccountId, T::BlockNumber>,
proposal: T::Hash,
) {
if approved {
Self::deposit_event(RawEvent::Approved(proposal));
// execute motion, assuming it exists.
if let Some(p) = ProposalOf::<T, I>::take(&proposal) {
let origin = RawOrigin::Members(voting.threshold, seats).into();
let ok = p.dispatch(origin).is_ok();
Self::deposit_event(RawEvent::Executed(proposal, ok));
}
} else {
// disapproved
ProposalOf::<T, I>::remove(&proposal);
Self::deposit_event(RawEvent::Disapproved(proposal));
}
// remove vote
Voting::<T, I>::remove(&proposal);
Proposals::<T, I>::mutate(|proposals| proposals.retain(|h| h != &proposal));
}
}
impl<T: Trait<I>, I: Instance> ChangeMembers<T::AccountId> for Module<T, I> {
fn change_members_sorted(_incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId]) {
fn change_members_sorted(
_incoming: &[T::AccountId],
outgoing: &[T::AccountId],
new: &[T::AccountId],
) {
// remove accounts from all current voting in motions.
let mut outgoing = outgoing.to_vec();
outgoing.sort_unstable();
@@ -302,7 +397,12 @@ impl<T: Trait<I>, I: Instance> ChangeMembers<T::AccountId> for Module<T, I> {
}
);
}
<Members<T, I>>::put(new);
Members::<T, I>::put(new);
Prime::<T, I>::kill();
}
fn set_prime(prime: Option<T::AccountId>) {
Prime::<T, I>::set(prime);
}
}
@@ -415,6 +515,7 @@ mod tests {
pub const MaximumBlockWeight: Weight = 1024;
pub const MaximumBlockLength: u32 = 2 * 1024;
pub const AvailableBlockRatio: Perbill = Perbill::one();
pub const MotionDuration: u64 = 3;
}
impl frame_system::Trait for Test {
type Origin = Origin;
@@ -441,11 +542,13 @@ mod tests {
type Origin = Origin;
type Proposal = Call;
type Event = Event;
type MotionDuration = MotionDuration;
}
impl Trait for Test {
type Origin = Origin;
type Proposal = Call;
type Event = Event;
type MotionDuration = MotionDuration;
}
pub type Block = sp_runtime::generic::Block<Header, UncheckedExtrinsic>;
@@ -486,22 +589,101 @@ mod tests {
Call::System(frame_system::Call::remark(value.encode()))
}
#[test]
fn close_works() {
make_ext().execute_with(|| {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash = BlakeTwo256::hash_of(&proposal);
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
System::set_block_number(3);
assert_noop!(
Collective::close(Origin::signed(4), hash.clone(), 0),
Error::<Test, Instance1>::TooEarly
);
System::set_block_number(4);
assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0));
let record = |event| EventRecord { phase: Phase::Finalization, event, topics: vec![] };
assert_eq!(System::events(), vec![
record(Event::collective_Instance1(RawEvent::Proposed(1, 0, hash.clone(), 3))),
record(Event::collective_Instance1(RawEvent::Voted(2, hash.clone(), true, 2, 0))),
record(Event::collective_Instance1(RawEvent::Closed(hash.clone(), 2, 1))),
record(Event::collective_Instance1(RawEvent::Disapproved(hash.clone())))
]);
});
}
#[test]
fn close_with_prime_works() {
make_ext().execute_with(|| {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash = BlakeTwo256::hash_of(&proposal);
assert_ok!(Collective::set_members(Origin::ROOT, vec![1, 2, 3], Some(3)));
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
System::set_block_number(4);
assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0));
let record = |event| EventRecord { phase: Phase::Finalization, event, topics: vec![] };
assert_eq!(System::events(), vec![
record(Event::collective_Instance1(RawEvent::Proposed(1, 0, hash.clone(), 3))),
record(Event::collective_Instance1(RawEvent::Voted(2, hash.clone(), true, 2, 0))),
record(Event::collective_Instance1(RawEvent::Closed(hash.clone(), 2, 1))),
record(Event::collective_Instance1(RawEvent::Disapproved(hash.clone())))
]);
});
}
#[test]
fn close_with_voting_prime_works() {
make_ext().execute_with(|| {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash = BlakeTwo256::hash_of(&proposal);
assert_ok!(Collective::set_members(Origin::ROOT, vec![1, 2, 3], Some(1)));
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
System::set_block_number(4);
assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0));
let record = |event| EventRecord { phase: Phase::Finalization, event, topics: vec![] };
assert_eq!(System::events(), vec![
record(Event::collective_Instance1(RawEvent::Proposed(1, 0, hash.clone(), 3))),
record(Event::collective_Instance1(RawEvent::Voted(2, hash.clone(), true, 2, 0))),
record(Event::collective_Instance1(RawEvent::Closed(hash.clone(), 3, 0))),
record(Event::collective_Instance1(RawEvent::Approved(hash.clone()))),
record(Event::collective_Instance1(RawEvent::Executed(hash.clone(), false)))
]);
});
}
#[test]
fn removal_of_old_voters_votes_works() {
make_ext().execute_with(|| {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash = BlakeTwo256::hash_of(&proposal);
let end = 4;
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] })
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end })
);
Collective::change_members_sorted(&[4], &[1], &[2, 3, 4]);
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![] })
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end })
);
let proposal = make_proposal(69);
@@ -510,12 +692,12 @@ mod tests {
assert_ok!(Collective::vote(Origin::signed(3), hash.clone(), 1, false));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3] })
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end })
);
Collective::change_members_sorted(&[], &[3], &[2, 4]);
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![] })
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end })
);
});
}
@@ -526,16 +708,17 @@ mod tests {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash = BlakeTwo256::hash_of(&proposal);
let end = 4;
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] })
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end })
);
assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 3, 4]));
assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 3, 4], None));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![] })
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end })
);
let proposal = make_proposal(69);
@@ -544,12 +727,12 @@ mod tests {
assert_ok!(Collective::vote(Origin::signed(3), hash.clone(), 1, false));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3] })
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end })
);
assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 4]));
assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 4], None));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![] })
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end })
);
});
}
@@ -560,12 +743,13 @@ mod tests {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash = proposal.blake2_256().into();
let end = 4;
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
assert_eq!(Collective::proposals(), vec![hash]);
assert_eq!(Collective::proposal_of(&hash), Some(proposal));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 3, ayes: vec![1], nays: vec![] })
Some(Votes { index: 0, threshold: 3, ayes: vec![1], nays: vec![], end })
);
assert_eq!(System::events(), vec![
@@ -629,10 +813,11 @@ mod tests {
System::set_block_number(1);
let proposal = make_proposal(42);
let hash: H256 = proposal.blake2_256().into();
let end = 4;
assert_ok!(Collective::propose(Origin::signed(1), 2, Box::new(proposal.clone())));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![] })
Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![], end })
);
assert_noop!(
Collective::vote(Origin::signed(1), hash.clone(), 0, true),
@@ -641,7 +826,7 @@ mod tests {
assert_ok!(Collective::vote(Origin::signed(1), hash.clone(), 0, false));
assert_eq!(
Collective::voting(&hash),
Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1] })
Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1], end })
);
assert_noop!(
Collective::vote(Origin::signed(1), hash.clone(), 0, false),
@@ -690,6 +690,7 @@ impl<T: Trait> Module<T> {
// split new set into winners and runners up.
let split_point = desired_seats.min(new_set_with_stake.len());
let mut new_members = (&new_set_with_stake[..split_point]).to_vec();
let most_popular = new_members.first().map(|x| x.0.clone());
// save the runners up as-is. They are sorted based on desirability.
// sort and save the members.
@@ -722,6 +723,7 @@ impl<T: Trait> Module<T> {
&outgoing.clone(),
&new_members_ids,
);
T::ChangeMembers::set_prime(most_popular);
// outgoing candidates lose their bond.
let mut to_burn_bond = outgoing.to_vec();
@@ -864,6 +866,7 @@ mod tests {
thread_local! {
pub static MEMBERS: RefCell<Vec<u64>> = RefCell::new(vec![]);
pub static PRIME: RefCell<Option<u64>> = RefCell::new(None);
}
pub struct TestChangeMembers;
@@ -898,6 +901,11 @@ mod tests {
assert_eq!(old_plus_incoming, new_plus_outgoing);
MEMBERS.with(|m| *m.borrow_mut() = new.to_vec());
PRIME.with(|p| *p.borrow_mut() = None);
}
fn set_prime(who: Option<u64>) {
PRIME.with(|p| *p.borrow_mut() = who);
}
}
@@ -1250,6 +1258,30 @@ mod tests {
});
}
#[test]
fn prime_works() {
ExtBuilder::default().build().execute_with(|| {
assert_ok!(Elections::submit_candidacy(Origin::signed(3)));
assert_ok!(Elections::submit_candidacy(Origin::signed(4)));
assert_ok!(Elections::submit_candidacy(Origin::signed(5)));
assert_ok!(Elections::vote(Origin::signed(1), vec![4, 3], 10));
assert_ok!(Elections::vote(Origin::signed(2), vec![4], 20));
assert_ok!(Elections::vote(Origin::signed(3), vec![3], 30));
assert_ok!(Elections::vote(Origin::signed(4), vec![4], 40));
assert_ok!(Elections::vote(Origin::signed(5), vec![5], 50));
System::set_block_number(5);
assert_ok!(Elections::end_block(System::block_number()));
assert_eq!(Elections::members_ids(), vec![4, 5]);
assert_eq!(Elections::candidates(), vec![]);
assert_ok!(Elections::vote(Origin::signed(3), vec![4, 5], 10));
assert_eq!(PRIME.with(|p| *p.borrow()), Some(4));
});
}
#[test]
fn cannot_vote_for_more_than_candidates() {
ExtBuilder::default().build().execute_with(|| {
+99 -3
View File
@@ -17,7 +17,7 @@
//! # Membership Module
//!
//! Allows control of membership of a set of `AccountId`s, useful for managing membership of of a
//! collective.
//! collective. A prime member may be set.
// Ensure we're `no_std` when compiling for Wasm.
#![cfg_attr(not(feature = "std"), no_std)]
@@ -47,6 +47,9 @@ pub trait Trait<I=DefaultInstance>: frame_system::Trait {
/// Required origin for resetting membership.
type ResetOrigin: EnsureOrigin<Self::Origin>;
/// Required origin for setting or resetting the prime member.
type PrimeOrigin: EnsureOrigin<Self::Origin>;
/// The receiver of the signal for when the membership has been initialized. This happens pre-
/// genesis and will usually be the same as `MembershipChanged`. If you need to do something
/// different on initialization, then you can change this accordingly.
@@ -60,6 +63,9 @@ decl_storage! {
trait Store for Module<T: Trait<I>, I: Instance=DefaultInstance> as Membership {
/// The current membership, stored as an ordered Vec.
Members get(fn members): Vec<T::AccountId>;
/// The current prime member, if one exists.
Prime get(fn prime): Option<T::AccountId>;
}
add_extra_genesis {
config(members): Vec<T::AccountId>;
@@ -144,6 +150,7 @@ decl_module! {
<Members<T, I>>::put(&members);
T::MembershipChanged::change_members_sorted(&[], &[who], &members[..]);
Self::rejig_prime(&members);
Self::deposit_event(RawEvent::MemberRemoved);
}
@@ -151,6 +158,8 @@ decl_module! {
/// Swap out one member `remove` for another `add`.
///
/// May only be called from `SwapOrigin` or root.
///
/// Prime membership is *not* passed from `remove` to `add`, if extant.
#[weight = SimpleDispatchInfo::FixedNormal(50_000)]
fn swap_member(origin, remove: T::AccountId, add: T::AccountId) {
T::SwapOrigin::try_origin(origin)
@@ -171,6 +180,7 @@ decl_module! {
&[remove],
&members[..],
);
Self::rejig_prime(&members);
Self::deposit_event(RawEvent::MembersSwapped);
}
@@ -189,15 +199,19 @@ decl_module! {
members.sort();
<Members<T, I>>::mutate(|m| {
T::MembershipChanged::set_members_sorted(&members[..], m);
Self::rejig_prime(&members);
*m = members;
});
Self::deposit_event(RawEvent::MembersReset);
}
/// Swap out the sending member for some other key `new`.
///
/// May only be called from `Signed` origin of a current member.
///
/// Prime membership is passed from the origin account to `new`, if extant.
#[weight = SimpleDispatchInfo::FixedNormal(50_000)]
fn change_key(origin, new: T::AccountId) {
let remove = ensure_signed(origin)?;
@@ -211,14 +225,51 @@ decl_module! {
<Members<T, I>>::put(&members);
T::MembershipChanged::change_members_sorted(
&[new],
&[remove],
&[new.clone()],
&[remove.clone()],
&members[..],
);
if Prime::<T, I>::get() == Some(remove) {
Prime::<T, I>::put(&new);
T::MembershipChanged::set_prime(Some(new));
}
}
Self::deposit_event(RawEvent::KeyChanged);
}
/// Set the prime member. Must be a current member.
#[weight = SimpleDispatchInfo::FixedNormal(50_000)]
fn set_prime(origin, who: T::AccountId) {
T::PrimeOrigin::try_origin(origin)
.map(|_| ())
.or_else(ensure_root)?;
Self::members().binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?;
Prime::<T, I>::put(&who);
T::MembershipChanged::set_prime(Some(who));
}
/// Remove the prime member if it exists.
#[weight = SimpleDispatchInfo::FixedNormal(50_000)]
fn clear_prime(origin) {
T::PrimeOrigin::try_origin(origin)
.map(|_| ())
.or_else(ensure_root)?;
Prime::<T, I>::kill();
T::MembershipChanged::set_prime(None);
}
}
}
impl<T: Trait<I>, I: Instance> Module<T, I> {
fn rejig_prime(members: &[T::AccountId]) {
if let Some(prime) = Prime::<T, I>::get() {
match members.binary_search(&prime) {
Ok(_) => T::MembershipChanged::set_prime(Some(prime)),
Err(_) => Prime::<T, I>::kill(),
}
}
}
}
@@ -283,6 +334,7 @@ mod tests {
thread_local! {
static MEMBERS: RefCell<Vec<u64>> = RefCell::new(vec![]);
static PRIME: RefCell<Option<u64>> = RefCell::new(None);
}
pub struct TestChangeMembers;
@@ -297,6 +349,10 @@ mod tests {
assert_eq!(old_plus_incoming, new_plus_outgoing);
MEMBERS.with(|m| *m.borrow_mut() = new.to_vec());
PRIME.with(|p| *p.borrow_mut() = None);
}
fn set_prime(who: Option<u64>) {
PRIME.with(|p| *p.borrow_mut() = who);
}
}
impl InitializeMembers<u64> for TestChangeMembers {
@@ -311,6 +367,7 @@ mod tests {
type RemoveOrigin = EnsureSignedBy<Two, u64>;
type SwapOrigin = EnsureSignedBy<Three, u64>;
type ResetOrigin = EnsureSignedBy<Four, u64>;
type PrimeOrigin = EnsureSignedBy<Five, u64>;
type MembershipInitialized = TestChangeMembers;
type MembershipChanged = TestChangeMembers;
}
@@ -337,6 +394,21 @@ mod tests {
});
}
#[test]
fn prime_member_works() {
new_test_ext().execute_with(|| {
assert_noop!(Membership::set_prime(Origin::signed(4), 20), BadOrigin);
assert_noop!(Membership::set_prime(Origin::signed(5), 15), Error::<Test, _>::NotMember);
assert_ok!(Membership::set_prime(Origin::signed(5), 20));
assert_eq!(Membership::prime(), Some(20));
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
assert_ok!(Membership::clear_prime(Origin::signed(5)));
assert_eq!(Membership::prime(), None);
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
});
}
#[test]
fn add_member_works() {
new_test_ext().execute_with(|| {
@@ -353,9 +425,12 @@ mod tests {
new_test_ext().execute_with(|| {
assert_noop!(Membership::remove_member(Origin::signed(5), 20), BadOrigin);
assert_noop!(Membership::remove_member(Origin::signed(2), 15), Error::<Test, _>::NotMember);
assert_ok!(Membership::set_prime(Origin::signed(5), 20));
assert_ok!(Membership::remove_member(Origin::signed(2), 20));
assert_eq!(Membership::members(), vec![10, 30]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members());
assert_eq!(Membership::prime(), None);
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
});
}
@@ -365,11 +440,19 @@ mod tests {
assert_noop!(Membership::swap_member(Origin::signed(5), 10, 25), BadOrigin);
assert_noop!(Membership::swap_member(Origin::signed(3), 15, 25), Error::<Test, _>::NotMember);
assert_noop!(Membership::swap_member(Origin::signed(3), 10, 30), Error::<Test, _>::AlreadyMember);
assert_ok!(Membership::set_prime(Origin::signed(5), 20));
assert_ok!(Membership::swap_member(Origin::signed(3), 20, 20));
assert_eq!(Membership::members(), vec![10, 20, 30]);
assert_eq!(Membership::prime(), Some(20));
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
assert_ok!(Membership::set_prime(Origin::signed(5), 10));
assert_ok!(Membership::swap_member(Origin::signed(3), 10, 25));
assert_eq!(Membership::members(), vec![20, 25, 30]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members());
assert_eq!(Membership::prime(), None);
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
});
}
@@ -385,11 +468,14 @@ mod tests {
#[test]
fn change_key_works() {
new_test_ext().execute_with(|| {
assert_ok!(Membership::set_prime(Origin::signed(5), 10));
assert_noop!(Membership::change_key(Origin::signed(3), 25), Error::<Test, _>::NotMember);
assert_noop!(Membership::change_key(Origin::signed(10), 20), Error::<Test, _>::AlreadyMember);
assert_ok!(Membership::change_key(Origin::signed(10), 40));
assert_eq!(Membership::members(), vec![20, 30, 40]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members());
assert_eq!(Membership::prime(), Some(40));
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
});
}
@@ -405,10 +491,20 @@ mod tests {
#[test]
fn reset_members_works() {
new_test_ext().execute_with(|| {
assert_ok!(Membership::set_prime(Origin::signed(5), 20));
assert_noop!(Membership::reset_members(Origin::signed(1), vec![20, 40, 30]), BadOrigin);
assert_ok!(Membership::reset_members(Origin::signed(4), vec![20, 40, 30]));
assert_eq!(Membership::members(), vec![20, 30, 40]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members());
assert_eq!(Membership::prime(), Some(20));
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
assert_ok!(Membership::reset_members(Origin::signed(4), vec![10, 40, 30]));
assert_eq!(Membership::members(), vec![10, 30, 40]);
assert_eq!(MEMBERS.with(|m| m.borrow().clone()), Membership::members());
assert_eq!(Membership::prime(), None);
assert_eq!(PRIME.with(|m| *m.borrow()), Membership::prime());
});
}
}
@@ -91,6 +91,14 @@ impl<T: FullCodec, G: StorageValue<T>> storage::StorageValue<T> for G {
unhashed::put(&Self::storage_value_final_key(), &val)
}
fn set(maybe_val: Self::Query) {
if let Some(val) = G::from_query_to_optional_value(maybe_val) {
unhashed::put(&Self::storage_value_final_key(), &val)
} else {
unhashed::kill(&Self::storage_value_final_key())
}
}
fn kill() {
unhashed::kill(&Self::storage_value_final_key())
}
@@ -73,6 +73,10 @@ pub trait StorageValue<T: FullCodec> {
/// Store a value under this key into the provided storage instance.
fn put<Arg: EncodeLike<T>>(val: Arg);
/// Store a value under this key into the provided storage instance; this uses the query
/// type rather than the underlying value.
fn set(val: Self::Query);
/// Mutate the value
fn mutate<R, F: FnOnce(&mut Self::Query) -> R>(f: F) -> R;
+12
View File
@@ -808,6 +808,8 @@ impl WithdrawReasons {
pub trait ChangeMembers<AccountId: Clone + Ord> {
/// A number of members `incoming` just joined the set and replaced some `outgoing` ones. The
/// new set is given by `new`, and need not be sorted.
///
/// This resets any previous value of prime.
fn change_members(incoming: &[AccountId], outgoing: &[AccountId], mut new: Vec<AccountId>) {
new.sort_unstable();
Self::change_members_sorted(incoming, outgoing, &new[..]);
@@ -817,6 +819,8 @@ pub trait ChangeMembers<AccountId: Clone + Ord> {
/// new set is thus given by `sorted_new` and **must be sorted**.
///
/// NOTE: This is the only function that needs to be implemented in `ChangeMembers`.
///
/// This resets any previous value of prime.
fn change_members_sorted(
incoming: &[AccountId],
outgoing: &[AccountId],
@@ -825,6 +829,8 @@ pub trait ChangeMembers<AccountId: Clone + Ord> {
/// Set the new members; they **must already be sorted**. This will compute the diff and use it to
/// call `change_members_sorted`.
///
/// This resets any previous value of prime.
fn set_members_sorted(new_members: &[AccountId], old_members: &[AccountId]) {
let (incoming, outgoing) = Self::compute_members_diff(new_members, old_members);
Self::change_members_sorted(&incoming[..], &outgoing[..], &new_members);
@@ -865,14 +871,20 @@ pub trait ChangeMembers<AccountId: Clone + Ord> {
}
(incoming, outgoing)
}
/// Set the prime member.
fn set_prime(_prime: Option<AccountId>) {}
}
impl<T: Clone + Ord> ChangeMembers<T> for () {
fn change_members(_: &[T], _: &[T], _: Vec<T>) {}
fn change_members_sorted(_: &[T], _: &[T], _: &[T]) {}
fn set_members_sorted(_: &[T], _: &[T]) {}
fn set_prime(_: Option<T>) {}
}
/// Trait for type that can handle the initialization of account IDs at genesis.
pub trait InitializeMembers<AccountId> {
/// Initialize the members to the given `members`.