// Copyright 2017-2020 Parity Technologies (UK) Ltd. // This file is part of Substrate. // Substrate is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Substrate is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . //! Collective system: Members of a set of account IDs can make their collective feelings known //! 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`. //! //! 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"] use sp_std::{prelude::*, result}; use sp_core::u32_trait::Value as U32; use sp_runtime::RuntimeDebug; use sp_runtime::traits::Hash; use frame_support::weights::SimpleDispatchInfo; use frame_support::{ dispatch::{Dispatchable, Parameter}, codec::{Encode, Decode}, traits::{Get, ChangeMembers, InitializeMembers, EnsureOrigin}, decl_module, decl_event, decl_storage, decl_error, ensure, }; use frame_system::{self as system, ensure_signed, ensure_root}; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; /// Simple index type for proposal counting. pub type ProposalIndex = u32; /// A number of members. /// /// This also serves as a number of voting members, and since for motions, each member may /// vote exactly once, therefore also the number of votes for any given motion. pub type MemberCount = u32; pub trait Trait: frame_system::Trait { /// The outer origin type. type Origin: From>; /// The outer call dispatch type. type Proposal: Parameter + Dispatchable>::Origin> + From>; /// The outer event type. type Event: From> + Into<::Event>; /// The time-out for council motions. type MotionDuration: Get; } /// Origin for the collective module. #[derive(PartialEq, Eq, Clone, RuntimeDebug)] pub enum RawOrigin { /// It has been condoned by a given number of members of the collective from a given total. Members(MemberCount, MemberCount), /// It has been condoned by a single member of the collective. Member(AccountId), /// Dummy to manage the fact we have instancing. _Phantom(sp_std::marker::PhantomData), } /// Origin for the collective module. pub type Origin = RawOrigin<::AccountId, I>; #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] /// Info for keeping track of a motion being voted on. pub struct Votes { /// The proposal's unique index. index: ProposalIndex, /// The number of approval votes that are needed to pass the motion. threshold: MemberCount, /// The current set of voters that approved it. ayes: Vec, /// The current set of voters that rejected it. nays: Vec, /// The hard end time of this vote. end: BlockNumber, } decl_storage! { trait Store for Module, I: Instance=DefaultInstance> as Collective { /// The hashes of the active proposals. pub Proposals get(fn proposals): Vec; /// Actual proposal for a given hash, if it's current. pub ProposalOf get(fn proposal_of): map hasher(identity) T::Hash => Option<>::Proposal>; /// Votes on a given proposal, if it is ongoing. pub Voting get(fn voting): map hasher(identity) T::Hash => Option>; /// 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; /// 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; } add_extra_genesis { config(phantom): sp_std::marker::PhantomData; config(members): Vec; build(|config| Module::::initialize_members(&config.members)) } } decl_event! { pub enum Event where ::Hash, ::AccountId, { /// A motion (given hash) has been proposed (by given account) with a threshold (given /// `MemberCount`). Proposed(AccountId, ProposalIndex, Hash, MemberCount), /// A motion (given hash) has been voted on by given account, leaving /// a tally (yes votes and no votes given respectively as `MemberCount`). Voted(AccountId, Hash, bool, MemberCount, MemberCount), /// A motion was approved by the required threshold. Approved(Hash), /// A motion was not approved by the required threshold. Disapproved(Hash), /// A motion was executed; `bool` is true if returned without error. 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), } } decl_error! { pub enum Error for Module, I: Instance> { /// Account is not a member NotMember, /// Duplicate proposals not allowed DuplicateProposal, /// Proposal must exist ProposalMissing, /// Mismatched index WrongIndex, /// Duplicate vote ignored DuplicateVote, /// Members are already initialized! AlreadyInitialized, /// The close call is made too early, before the end of the voting. TooEarly, } } // Note: this module is not benchmarked. The weights are obtained based on the similarity of the // executed logic with other democracy function. Note that councillor operations are assigned to the // operational class. decl_module! { pub struct Module, I: Instance=DefaultInstance> for enum Call where origin: ::Origin { type Error = Error; fn deposit_event() = default; /// 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, prime: Option) { ensure_root(origin)?; let mut new_members = new_members; new_members.sort(); let old = Members::::get(); >::set_members_sorted(&new_members[..], &old); Prime::::set(prime); } /// Dispatch a proposal from a member using the `Member` origin. /// /// Origin must be a member of the collective. #[weight = SimpleDispatchInfo::FixedOperational(100_000)] fn execute(origin, proposal: Box<>::Proposal>) { let who = ensure_signed(origin)?; ensure!(Self::is_member(&who), Error::::NotMember); let proposal_hash = T::Hashing::hash_of(&proposal); let ok = proposal.dispatch(RawOrigin::Member(who).into()).is_ok(); Self::deposit_event(RawEvent::MemberExecuted(proposal_hash, ok)); } /// # /// - Bounded storage reads and writes. /// - Argument `threshold` has bearing on weight. /// # #[weight = SimpleDispatchInfo::FixedOperational(5_000_000)] fn propose(origin, #[compact] threshold: MemberCount, proposal: Box<>::Proposal>) { let who = ensure_signed(origin)?; ensure!(Self::is_member(&who), Error::::NotMember); let proposal_hash = T::Hashing::hash_of(&proposal); ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); if threshold < 2 { let seats = Self::members().len() as MemberCount; let ok = proposal.dispatch(RawOrigin::Members(1, seats).into()).is_ok(); Self::deposit_event(RawEvent::Executed(proposal_hash, ok)); } else { let index = Self::proposal_count(); >::mutate(|i| *i += 1); >::mutate(|proposals| proposals.push(proposal_hash)); >::insert(proposal_hash, *proposal); let end = system::Module::::block_number() + T::MotionDuration::get(); let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![], end }; >::insert(proposal_hash, votes); Self::deposit_event(RawEvent::Proposed(who, index, proposal_hash, threshold)); } } /// # /// - Bounded storage read and writes. /// - Will be slightly heavier if the proposal is approved / disapproved after the vote. /// # #[weight = SimpleDispatchInfo::FixedOperational(200_000)] fn vote(origin, proposal: T::Hash, #[compact] index: ProposalIndex, approve: bool) { let who = ensure_signed(origin)?; ensure!(Self::is_member(&who), Error::::NotMember); let mut voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; ensure!(voting.index == index, Error::::WrongIndex); let position_yes = voting.ayes.iter().position(|a| a == &who); let position_no = voting.nays.iter().position(|a| a == &who); if approve { if position_yes.is_none() { voting.ayes.push(who.clone()); } else { Err(Error::::DuplicateVote)? } if let Some(pos) = position_no { voting.nays.swap_remove(pos); } } else { if position_no.is_none() { voting.nays.push(who.clone()); } else { Err(Error::::DuplicateVote)? } if let Some(pos) = position_yes { voting.ayes.swap_remove(pos); } } let yes_votes = voting.ayes.len() as MemberCount; let no_votes = voting.nays.len() as MemberCount; 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 { Self::finalize_proposal(approved, seats, voting, proposal); } else { Voting::::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::::ProposalMissing)?; ensure!(voting.index == index, Error::::WrongIndex); ensure!(system::Module::::block_number() >= voting.end, Error::::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); } } } impl, I: Instance> Module { 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, proposal: T::Hash, ) { if approved { Self::deposit_event(RawEvent::Approved(proposal)); // execute motion, assuming it exists. if let Some(p) = ProposalOf::::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::::remove(&proposal); Self::deposit_event(RawEvent::Disapproved(proposal)); } // remove vote Voting::::remove(&proposal); Proposals::::mutate(|proposals| proposals.retain(|h| h != &proposal)); } } impl, I: Instance> ChangeMembers for Module { 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(); for h in Self::proposals().into_iter() { >::mutate(h, |v| if let Some(mut votes) = v.take() { votes.ayes = votes.ayes.into_iter() .filter(|i| outgoing.binary_search(i).is_err()) .collect(); votes.nays = votes.nays.into_iter() .filter(|i| outgoing.binary_search(i).is_err()) .collect(); *v = Some(votes); } ); } Members::::put(new); Prime::::kill(); } fn set_prime(prime: Option) { Prime::::set(prime); } } impl, I: Instance> InitializeMembers for Module { fn initialize_members(members: &[T::AccountId]) { if !members.is_empty() { assert!(>::get().is_empty(), "Members are already initialized!"); >::put(members); } } } /// Ensure that the origin `o` represents at least `n` members. Returns `Ok` or an `Err` /// otherwise. pub fn ensure_members(o: OuterOrigin, n: MemberCount) -> result::Result where OuterOrigin: Into, OuterOrigin>> { match o.into() { Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n), _ => Err("bad origin: expected to be a threshold number of members"), } } pub struct EnsureMember(sp_std::marker::PhantomData<(AccountId, I)>); impl< O: Into, O>> + From>, AccountId: Default, I, > EnsureOrigin for EnsureMember { type Success = AccountId; fn try_origin(o: O) -> Result { o.into().and_then(|o| match o { RawOrigin::Member(id) => Ok(id), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Member(Default::default())) } } pub struct EnsureMembers(sp_std::marker::PhantomData<(N, AccountId, I)>); impl< O: Into, O>> + From>, N: U32, AccountId, I, > EnsureOrigin for EnsureMembers { type Success = (MemberCount, MemberCount); fn try_origin(o: O) -> Result { o.into().and_then(|o| match o { RawOrigin::Members(n, m) if n >= N::VALUE => Ok((n, m)), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Members(N::VALUE, N::VALUE)) } } pub struct EnsureProportionMoreThan( sp_std::marker::PhantomData<(N, D, AccountId, I)> ); impl< O: Into, O>> + From>, N: U32, D: U32, AccountId, I, > EnsureOrigin for EnsureProportionMoreThan { type Success = (); fn try_origin(o: O) -> Result { o.into().and_then(|o| match o { RawOrigin::Members(n, m) if n * D::VALUE > N::VALUE * m => Ok(()), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Members(1u32, 0u32)) } } pub struct EnsureProportionAtLeast( sp_std::marker::PhantomData<(N, D, AccountId, I)> ); impl< O: Into, O>> + From>, N: U32, D: U32, AccountId, I, > EnsureOrigin for EnsureProportionAtLeast { type Success = (); fn try_origin(o: O) -> Result { o.into().and_then(|o| match o { RawOrigin::Members(n, m) if n * D::VALUE >= N::VALUE * m => Ok(()), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Members(0u32, 0u32)) } } #[cfg(test)] mod tests { use super::*; use frame_support::{Hashable, assert_ok, assert_noop, parameter_types, weights::Weight}; use frame_system::{self as system, EventRecord, Phase}; use hex_literal::hex; use sp_core::H256; use sp_runtime::{ Perbill, traits::{BlakeTwo256, IdentityLookup, Block as BlockT}, testing::Header, BuildStorage, }; use crate as collective; parameter_types! { pub const BlockHashCount: u64 = 250; 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; type Index = u64; type BlockNumber = u64; type Call = (); type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; type Event = Event; type BlockHashCount = BlockHashCount; type MaximumBlockWeight = MaximumBlockWeight; type MaximumBlockLength = MaximumBlockLength; type AvailableBlockRatio = AvailableBlockRatio; type Version = (); type ModuleToIndex = (); type AccountData = (); type OnNewAccount = (); type OnKilledAccount = (); } impl Trait for Test { 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; pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; frame_support::construct_runtime!( pub enum Test where Block = Block, NodeBlock = Block, UncheckedExtrinsic = UncheckedExtrinsic { System: system::{Module, Call, Event}, Collective: collective::::{Module, Call, Event, Origin, Config}, DefaultCollective: collective::{Module, Call, Event, Origin, Config}, } ); pub fn new_test_ext() -> sp_io::TestExternalities { let mut ext: sp_io::TestExternalities = GenesisConfig { collective_Instance1: Some(collective::GenesisConfig { members: vec![1, 2, 3], phantom: Default::default(), }), collective: None, }.build_storage().unwrap().into(); ext.execute_with(|| System::set_block_number(1)); ext } #[test] fn motions_basic_environment_works() { new_test_ext().execute_with(|| { assert_eq!(Collective::members(), vec![1, 2, 3]); assert_eq!(Collective::proposals(), Vec::::new()); }); } fn make_proposal(value: u64) -> Call { Call::System(frame_system::Call::remark(value.encode())) } #[test] fn close_works() { new_test_ext().execute_with(|| { 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::::TooEarly ); System::set_block_number(4); assert_ok!(Collective::close(Origin::signed(4), hash.clone(), 0)); let record = |event| EventRecord { phase: Phase::Initialization, 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() { new_test_ext().execute_with(|| { 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::Initialization, 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() { new_test_ext().execute_with(|| { 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::Initialization, 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() { new_test_ext().execute_with(|| { 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![], 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![], end }) ); let proposal = make_proposal(69); let hash = BlakeTwo256::hash_of(&proposal); assert_ok!(Collective::propose(Origin::signed(2), 2, Box::new(proposal.clone()))); 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], end }) ); Collective::change_members_sorted(&[], &[3], &[2, 4]); assert_eq!( Collective::voting(&hash), Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) ); }); } #[test] fn removal_of_old_voters_votes_works_with_set_members() { new_test_ext().execute_with(|| { 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![], end }) ); 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![], end }) ); let proposal = make_proposal(69); let hash = BlakeTwo256::hash_of(&proposal); assert_ok!(Collective::propose(Origin::signed(2), 2, Box::new(proposal.clone()))); 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], end }) ); 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![], end }) ); }); } #[test] fn propose_works() { new_test_ext().execute_with(|| { 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![], end }) ); assert_eq!(System::events(), vec![ EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Proposed( 1, 0, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), 3, )), topics: vec![], } ]); }); } #[test] fn motions_ignoring_non_collective_proposals_works() { new_test_ext().execute_with(|| { let proposal = make_proposal(42); assert_noop!( Collective::propose(Origin::signed(42), 3, Box::new(proposal.clone())), Error::::NotMember ); }); } #[test] fn motions_ignoring_non_collective_votes_works() { new_test_ext().execute_with(|| { let proposal = make_proposal(42); let hash: H256 = proposal.blake2_256().into(); assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_noop!( Collective::vote(Origin::signed(42), hash.clone(), 0, true), Error::::NotMember, ); }); } #[test] fn motions_ignoring_bad_index_collective_vote_works() { new_test_ext().execute_with(|| { System::set_block_number(3); let proposal = make_proposal(42); let hash: H256 = proposal.blake2_256().into(); assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_noop!( Collective::vote(Origin::signed(2), hash.clone(), 1, true), Error::::WrongIndex, ); }); } #[test] fn motions_revoting_works() { new_test_ext().execute_with(|| { 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![], end }) ); assert_noop!( Collective::vote(Origin::signed(1), hash.clone(), 0, true), Error::::DuplicateVote, ); 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], end }) ); assert_noop!( Collective::vote(Origin::signed(1), hash.clone(), 0, false), Error::::DuplicateVote, ); assert_eq!(System::events(), vec![ EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Proposed( 1, 0, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), 2, )), topics: vec![], }, EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Voted( 1, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), false, 0, 1, )), topics: vec![], } ]); }); } #[test] fn motions_reproposing_disapproved_works() { new_test_ext().execute_with(|| { let proposal = make_proposal(42); let hash: H256 = proposal.blake2_256().into(); assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, false)); assert_eq!(Collective::proposals(), vec![]); assert_ok!(Collective::propose(Origin::signed(1), 2, Box::new(proposal.clone()))); assert_eq!(Collective::proposals(), vec![hash]); }); } #[test] fn motions_disapproval_works() { new_test_ext().execute_with(|| { let proposal = make_proposal(42); let hash: H256 = proposal.blake2_256().into(); assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()))); assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, false)); assert_eq!(System::events(), vec![ EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1( RawEvent::Proposed( 1, 0, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), 3, )), topics: vec![], }, EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Voted( 2, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), false, 1, 1, )), topics: vec![], }, EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Disapproved( hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), )), topics: vec![], } ]); }); } #[test] fn motions_approval_works() { new_test_ext().execute_with(|| { let proposal = make_proposal(42); let hash: H256 = proposal.blake2_256().into(); assert_ok!(Collective::propose(Origin::signed(1), 2, Box::new(proposal.clone()))); assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true)); assert_eq!(System::events(), vec![ EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Proposed( 1, 0, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), 2, )), topics: vec![], }, EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Voted( 2, hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), true, 2, 0, )), topics: vec![], }, EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Approved( hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), )), topics: vec![], }, EventRecord { phase: Phase::Initialization, event: Event::collective_Instance1(RawEvent::Executed( hex!["68eea8f20b542ec656c6ac2d10435ae3bd1729efc34d1354ab85af840aad2d35"].into(), false, )), topics: vec![], } ]); }); } }