// This file is part of Substrate. // Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! 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`. //! The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight //! calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. //! //! A "prime" member may be set to help determine the default vote behavior based on chain //! config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any //! abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then //! abstentions will first follow the majority of the collective voting, and then the prime //! member. //! //! 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 scale_info::TypeInfo; use sp_io::storage; use sp_runtime::{traits::Hash, RuntimeDebug}; use sp_std::{marker::PhantomData, prelude::*, result}; use frame_support::{ codec::{Decode, Encode}, dispatch::{DispatchError, DispatchResultWithPostInfo, Dispatchable, PostDispatchInfo}, ensure, traits::{ Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers, StorageVersion, }, weights::{GetDispatchInfo, Weight}, }; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod migrations; pub mod weights; pub use pallet::*; pub use weights::WeightInfo; /// 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; /// Default voting strategy when a member is inactive. pub trait DefaultVote { /// Get the default voting strategy, given: /// /// - Whether the prime member voted Aye. /// - Raw number of yes votes. /// - Raw number of no votes. /// - Total number of member count. fn default_vote( prime_vote: Option, yes_votes: MemberCount, no_votes: MemberCount, len: MemberCount, ) -> bool; } /// Set the prime member's vote as the default vote. pub struct PrimeDefaultVote; impl DefaultVote for PrimeDefaultVote { fn default_vote( prime_vote: Option, _yes_votes: MemberCount, _no_votes: MemberCount, _len: MemberCount, ) -> bool { prime_vote.unwrap_or(false) } } /// First see if yes vote are over majority of the whole collective. If so, set the default vote /// as yes. Otherwise, use the prime member's vote as the default vote. pub struct MoreThanMajorityThenPrimeDefaultVote; impl DefaultVote for MoreThanMajorityThenPrimeDefaultVote { fn default_vote( prime_vote: Option, yes_votes: MemberCount, _no_votes: MemberCount, len: MemberCount, ) -> bool { let more_than_majority = yes_votes * 2 > len; more_than_majority || prime_vote.unwrap_or(false) } } /// Origin for the collective module. #[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo)] #[scale_info(skip_type_params(I))] 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(PhantomData), } impl GetBacking for RawOrigin { fn get_backing(&self) -> Option { match self { RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }), _ => None, } } } /// Info for keeping track of a motion being voted on. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] 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, } #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] #[pallet::storage_version(STORAGE_VERSION)] #[pallet::without_storage_info] pub struct Pallet(PhantomData<(T, I)>); #[pallet::config] pub trait Config: frame_system::Config { /// The outer origin type. type Origin: From>; /// The outer call dispatch type. type Proposal: Parameter + Dispatchable>::Origin, PostInfo = PostDispatchInfo> + From> + GetDispatchInfo; /// The outer event type. type Event: From> + IsType<::Event>; /// The time-out for council motions. type MotionDuration: Get; /// Maximum number of proposals allowed to be active in parallel. type MaxProposals: Get; /// The maximum number of members supported by the pallet. Used for weight estimation. /// /// NOTE: /// + Benchmarks will need to be re-run and weights adjusted if this changes. /// + This pallet assumes that dependents keep to the limit without enforcing it. type MaxMembers: Get; /// Default vote strategy of this collective. type DefaultVote: DefaultVote; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::genesis_config] pub struct GenesisConfig, I: 'static = ()> { pub phantom: PhantomData, pub members: Vec, } #[cfg(feature = "std")] impl, I: 'static> Default for GenesisConfig { fn default() -> Self { Self { phantom: Default::default(), members: Default::default() } } } #[pallet::genesis_build] impl, I: 'static> GenesisBuild for GenesisConfig { fn build(&self) { use sp_std::collections::btree_set::BTreeSet; let members_set: BTreeSet<_> = self.members.iter().collect(); assert_eq!( members_set.len(), self.members.len(), "Members cannot contain duplicate accounts." ); Pallet::::initialize_members(&self.members) } } /// Origin for the collective pallet. #[pallet::origin] pub type Origin = RawOrigin<::AccountId, I>; /// The hashes of the active proposals. #[pallet::storage] #[pallet::getter(fn proposals)] pub type Proposals, I: 'static = ()> = StorageValue<_, BoundedVec, ValueQuery>; /// Actual proposal for a given hash, if it's current. #[pallet::storage] #[pallet::getter(fn proposal_of)] pub type ProposalOf, I: 'static = ()> = StorageMap<_, Identity, T::Hash, >::Proposal, OptionQuery>; /// Votes on a given proposal, if it is ongoing. #[pallet::storage] #[pallet::getter(fn voting)] pub type Voting, I: 'static = ()> = StorageMap<_, Identity, T::Hash, Votes, OptionQuery>; /// Proposals so far. #[pallet::storage] #[pallet::getter(fn proposal_count)] pub type ProposalCount, I: 'static = ()> = StorageValue<_, u32, ValueQuery>; /// The current members of the collective. This is stored sorted (just by value). #[pallet::storage] #[pallet::getter(fn members)] pub type Members, I: 'static = ()> = StorageValue<_, Vec, ValueQuery>; /// The prime member that helps determine the default vote behavior in case of absentations. #[pallet::storage] #[pallet::getter(fn prime)] pub type Prime, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// A motion (given hash) has been proposed (by given account) with a threshold (given /// `MemberCount`). Proposed { account: T::AccountId, proposal_index: ProposalIndex, proposal_hash: T::Hash, threshold: 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 { account: T::AccountId, proposal_hash: T::Hash, voted: bool, yes: MemberCount, no: MemberCount, }, /// A motion was approved by the required threshold. Approved { proposal_hash: T::Hash }, /// A motion was not approved by the required threshold. Disapproved { proposal_hash: T::Hash }, /// A motion was executed; result will be `Ok` if it returned without error. Executed { proposal_hash: T::Hash, result: DispatchResult }, /// A single member did some action; result will be `Ok` if it returned without error. MemberExecuted { proposal_hash: T::Hash, result: DispatchResult }, /// A proposal was closed because its threshold was reached or after its duration was up. Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount }, } /// Old name generated by `decl_event`. #[deprecated(note = "use `Event` instead")] pub type RawEvent = Event; #[pallet::error] pub enum Error { /// 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 was made too early, before the end of the voting. TooEarly, /// There can only be a maximum of `MaxProposals` active proposals. TooManyProposals, /// The given weight bound for the proposal was too low. WrongProposalWeight, /// The given length bound for the proposal was too low. WrongProposalLength, } // Note that councillor operations are assigned to the operational class. #[pallet::call] impl, I: 'static> Pallet { /// 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. /// - `old_count`: The upper bound for the previous number of members in storage. Used for /// weight estimation. /// /// Requires root origin. /// /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but /// the weight estimations rely on it to estimate dispatchable weight. /// /// # WARNING: /// /// The `pallet-collective` can also be managed by logic outside of the pallet through the /// implementation of the trait [`ChangeMembers`]. /// Any call to `set_members` must be careful that the member set doesn't get out of sync /// with other logic managing the member set. /// /// # /// ## Weight /// - `O(MP + N)` where: /// - `M` old-members-count (code- and governance-bounded) /// - `N` new-members-count (code- and governance-bounded) /// - `P` proposals-count (code-bounded) /// - DB: /// - 1 storage mutation (codec `O(M)` read, `O(N)` write) for reading and writing the /// members /// - 1 storage read (codec `O(P)`) for reading the proposals /// - `P` storage mutations (codec `O(M)`) for updating the votes for each proposal /// - 1 storage write (codec `O(1)`) for deleting the old `prime` and setting the new one /// # #[pallet::weight(( T::WeightInfo::set_members( *old_count, // M new_members.len() as u32, // N T::MaxProposals::get() // P ), DispatchClass::Operational ))] pub fn set_members( origin: OriginFor, new_members: Vec, prime: Option, old_count: MemberCount, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; if new_members.len() > T::MaxMembers::get() as usize { log::error!( target: "runtime::collective", "New members count ({}) exceeds maximum amount of members expected ({}).", new_members.len(), T::MaxMembers::get(), ); } let old = Members::::get(); if old.len() > old_count as usize { log::warn!( target: "runtime::collective", "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})", old_count, old.len(), ); } let mut new_members = new_members; new_members.sort(); >::set_members_sorted(&new_members, &old); Prime::::set(prime); Ok(Some(T::WeightInfo::set_members( old.len() as u32, // M new_members.len() as u32, // N T::MaxProposals::get(), // P )) .into()) } /// Dispatch a proposal from a member using the `Member` origin. /// /// Origin must be a member of the collective. /// /// # /// ## Weight /// - `O(M + P)` where `M` members-count (code-bounded) and `P` complexity of dispatching /// `proposal` /// - DB: 1 read (codec `O(M)`) + DB access of `proposal` /// - 1 event /// # #[pallet::weight(( T::WeightInfo::execute( *length_bound, // B T::MaxMembers::get(), // M ).saturating_add(proposal.get_dispatch_info().weight), // P DispatchClass::Operational ))] pub fn execute( origin: OriginFor, proposal: Box<>::Proposal>, #[pallet::compact] length_bound: u32, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let members = Self::members(); ensure!(members.contains(&who), Error::::NotMember); let proposal_len = proposal.using_encoded(|x| x.len()); ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); let proposal_hash = T::Hashing::hash_of(&proposal); let result = proposal.dispatch(RawOrigin::Member(who).into()); Self::deposit_event(Event::MemberExecuted { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); Ok(get_result_weight(result) .map(|w| { T::WeightInfo::execute( proposal_len as u32, // B members.len() as u32, // M ) .saturating_add(w) // P }) .into()) } /// Add a new proposal to either be voted on or executed directly. /// /// Requires the sender to be member. /// /// `threshold` determines whether `proposal` is executed directly (`threshold < 2`) /// or put up for voting. /// /// # /// ## Weight /// - `O(B + M + P1)` or `O(B + M + P2)` where: /// - `B` is `proposal` size in bytes (length-fee-bounded) /// - `M` is members-count (code- and governance-bounded) /// - branching is influenced by `threshold` where: /// - `P1` is proposal execution complexity (`threshold < 2`) /// - `P2` is proposals-count (code-bounded) (`threshold >= 2`) /// - DB: /// - 1 storage read `is_member` (codec `O(M)`) /// - 1 storage read `ProposalOf::contains_key` (codec `O(1)`) /// - DB accesses influenced by `threshold`: /// - EITHER storage accesses done by `proposal` (`threshold < 2`) /// - OR proposal insertion (`threshold <= 2`) /// - 1 storage mutation `Proposals` (codec `O(P2)`) /// - 1 storage mutation `ProposalCount` (codec `O(1)`) /// - 1 storage write `ProposalOf` (codec `O(B)`) /// - 1 storage write `Voting` (codec `O(M)`) /// - 1 event /// # #[pallet::weight(( if *threshold < 2 { T::WeightInfo::propose_execute( *length_bound, // B T::MaxMembers::get(), // M ).saturating_add(proposal.get_dispatch_info().weight) // P1 } else { T::WeightInfo::propose_proposed( *length_bound, // B T::MaxMembers::get(), // M T::MaxProposals::get(), // P2 ) }, DispatchClass::Operational ))] pub fn propose( origin: OriginFor, #[pallet::compact] threshold: MemberCount, proposal: Box<>::Proposal>, #[pallet::compact] length_bound: u32, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let members = Self::members(); ensure!(members.contains(&who), Error::::NotMember); let proposal_len = proposal.using_encoded(|x| x.len()); ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); 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 result = proposal.dispatch(RawOrigin::Members(1, seats).into()); Self::deposit_event(Event::Executed { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); Ok(get_result_weight(result) .map(|w| { T::WeightInfo::propose_execute( proposal_len as u32, // B members.len() as u32, // M ) .saturating_add(w) // P1 }) .into()) } else { let active_proposals = >::try_mutate(|proposals| -> Result { proposals .try_push(proposal_hash) .map_err(|_| Error::::TooManyProposals)?; Ok(proposals.len()) })?; let index = Self::proposal_count(); >::mutate(|i| *i += 1); >::insert(proposal_hash, *proposal); let votes = { let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); Votes { index, threshold, ayes: vec![], nays: vec![], end } }; >::insert(proposal_hash, votes); Self::deposit_event(Event::Proposed { account: who, proposal_index: index, proposal_hash, threshold, }); Ok(Some(T::WeightInfo::propose_proposed( proposal_len as u32, // B members.len() as u32, // M active_proposals as u32, // P2 )) .into()) } } /// Add an aye or nay vote for the sender to the given proposal. /// /// Requires the sender to be a member. /// /// Transaction fees will be waived if the member is voting on any particular proposal /// for the first time and the call is successful. Subsequent vote changes will charge a /// fee. /// # /// ## Weight /// - `O(M)` where `M` is members-count (code- and governance-bounded) /// - DB: /// - 1 storage read `Members` (codec `O(M)`) /// - 1 storage mutation `Voting` (codec `O(M)`) /// - 1 event /// # #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))] pub fn vote( origin: OriginFor, proposal: T::Hash, #[pallet::compact] index: ProposalIndex, approve: bool, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let members = Self::members(); ensure!(members.contains(&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); // Detects first vote of the member in the motion let is_account_voting_first_time = position_yes.is_none() && position_no.is_none(); if approve { if position_yes.is_none() { voting.ayes.push(who.clone()); } else { return Err(Error::::DuplicateVote.into()) } if let Some(pos) = position_no { voting.nays.swap_remove(pos); } } else { if position_no.is_none() { voting.nays.push(who.clone()); } else { return Err(Error::::DuplicateVote.into()) } 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(Event::Voted { account: who, proposal_hash: proposal, voted: approve, yes: yes_votes, no: no_votes, }); Voting::::insert(&proposal, voting); if is_account_voting_first_time { Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into()) } else { Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into()) } } /// Close a vote that is either approved, disapproved or whose voting period has ended. /// /// May be called by any signed account in order to finish voting and close the proposal. /// /// If called before the end of the voting period it will only close the vote if it is /// has enough votes to be approved or disapproved. /// /// If called after the end of the voting period abstentions are counted as rejections /// unless there is a prime member set and the prime member cast an approval. /// /// If the close operation completes successfully with disapproval, the transaction fee will /// be waived. Otherwise execution of the approved operation will be charged to the caller. /// /// + `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed /// proposal. /// + `length_bound`: The upper bound for the length of the proposal in storage. Checked via /// `storage::read` so it is `size_of::() == 4` larger than the pure length. /// /// # /// ## Weight /// - `O(B + M + P1 + P2)` where: /// - `B` is `proposal` size in bytes (length-fee-bounded) /// - `M` is members-count (code- and governance-bounded) /// - `P1` is the complexity of `proposal` preimage. /// - `P2` is proposal-count (code-bounded) /// - DB: /// - 2 storage reads (`Members`: codec `O(M)`, `Prime`: codec `O(1)`) /// - 3 mutations (`Voting`: codec `O(M)`, `ProposalOf`: codec `O(B)`, `Proposals`: codec /// `O(P2)`) /// - any mutations done while executing `proposal` (`P1`) /// - up to 3 events /// # #[pallet::weight(( { let b = *length_bound; let m = T::MaxMembers::get(); let p1 = *proposal_weight_bound; let p2 = T::MaxProposals::get(); T::WeightInfo::close_early_approved(b, m, p2) .max(T::WeightInfo::close_early_disapproved(m, p2)) .max(T::WeightInfo::close_approved(b, m, p2)) .max(T::WeightInfo::close_disapproved(m, p2)) .saturating_add(p1) }, DispatchClass::Operational ))] pub fn close( origin: OriginFor, proposal_hash: T::Hash, #[pallet::compact] index: ProposalIndex, #[pallet::compact] proposal_weight_bound: Weight, #[pallet::compact] length_bound: u32, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; let voting = Self::voting(&proposal_hash).ok_or(Error::::ProposalMissing)?; ensure!(voting.index == index, Error::::WrongIndex); 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 approved = yes_votes >= voting.threshold; let disapproved = seats.saturating_sub(no_votes) < voting.threshold; // Allow (dis-)approving the proposal as soon as there are enough votes. if approved { let (proposal, len) = Self::validate_and_get_proposal( &proposal_hash, length_bound, proposal_weight_bound, )?; Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); let (proposal_weight, proposal_count) = Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); return Ok(( Some( T::WeightInfo::close_early_approved(len as u32, seats, proposal_count) .saturating_add(proposal_weight), ), Pays::Yes, ) .into()) } else if disapproved { Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); let proposal_count = Self::do_disapprove_proposal(proposal_hash); return Ok(( Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)), Pays::No, ) .into()) } // Only allow actual closing of the proposal after the voting period has ended. ensure!( frame_system::Pallet::::block_number() >= voting.end, Error::::TooEarly ); let prime_vote = Self::prime().map(|who| voting.ayes.iter().any(|a| a == &who)); // default voting strategy. let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats); let abstentions = seats - (yes_votes + no_votes); match default { true => yes_votes += abstentions, false => no_votes += abstentions, } let approved = yes_votes >= voting.threshold; if approved { let (proposal, len) = Self::validate_and_get_proposal( &proposal_hash, length_bound, proposal_weight_bound, )?; Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); let (proposal_weight, proposal_count) = Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); Ok(( Some( T::WeightInfo::close_approved(len as u32, seats, proposal_count) .saturating_add(proposal_weight), ), Pays::Yes, ) .into()) } else { Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); let proposal_count = Self::do_disapprove_proposal(proposal_hash); Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into()) } } /// Disapprove a proposal, close, and remove it from the system, regardless of its current /// state. /// /// Must be called by the Root origin. /// /// Parameters: /// * `proposal_hash`: The hash of the proposal that should be disapproved. /// /// # /// Complexity: O(P) where P is the number of max proposals /// DB Weight: /// * Reads: Proposals /// * Writes: Voting, Proposals, ProposalOf /// # #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))] pub fn disapprove_proposal( origin: OriginFor, proposal_hash: T::Hash, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; let proposal_count = Self::do_disapprove_proposal(proposal_hash); Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into()) } } } /// Return the weight of a dispatch call result as an `Option`. /// /// Will return the weight regardless of what the state of the result is. fn get_result_weight(result: DispatchResultWithPostInfo) -> Option { match result { Ok(post_info) => post_info.actual_weight, Err(err) => err.post_info.actual_weight, } } impl, I: 'static> Pallet { /// Check whether `who` is a member of the collective. pub fn is_member(who: &T::AccountId) -> bool { // Note: The dispatchables *do not* use this to check membership so make sure // to update those if this is changed. Self::members().contains(who) } /// Ensure that the right proposal bounds were passed and get the proposal from storage. /// /// Checks the length in storage via `storage::read` which adds an extra `size_of::() == 4` /// to the length. fn validate_and_get_proposal( hash: &T::Hash, length_bound: u32, weight_bound: Weight, ) -> Result<(>::Proposal, usize), DispatchError> { let key = ProposalOf::::hashed_key_for(hash); // read the length of the proposal storage entry directly let proposal_len = storage::read(&key, &mut [0; 0], 0).ok_or(Error::::ProposalMissing)?; ensure!(proposal_len <= length_bound, Error::::WrongProposalLength); let proposal = ProposalOf::::get(hash).ok_or(Error::::ProposalMissing)?; let proposal_weight = proposal.get_dispatch_info().weight; ensure!(proposal_weight <= weight_bound, Error::::WrongProposalWeight); Ok((proposal, proposal_len as usize)) } /// 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 do_approve_proposal( seats: MemberCount, yes_votes: MemberCount, proposal_hash: T::Hash, proposal: >::Proposal, ) -> (Weight, u32) { Self::deposit_event(Event::Approved { proposal_hash }); let dispatch_weight = proposal.get_dispatch_info().weight; let origin = RawOrigin::Members(yes_votes, seats).into(); let result = proposal.dispatch(origin); Self::deposit_event(Event::Executed { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); // default to the dispatch info weight for safety let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); // P1 let proposal_count = Self::remove_proposal(proposal_hash); (proposal_weight, proposal_count) } fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 { // disapproved Self::deposit_event(Event::Disapproved { proposal_hash }); Self::remove_proposal(proposal_hash) } // Removes a proposal from the pallet, cleaning up votes and the vector of proposals. fn remove_proposal(proposal_hash: T::Hash) -> u32 { // remove proposal and vote ProposalOf::::remove(&proposal_hash); Voting::::remove(&proposal_hash); let num_proposals = Proposals::::mutate(|proposals| { proposals.retain(|h| h != &proposal_hash); proposals.len() + 1 // calculate weight based on original length }); num_proposals as u32 } } impl, I: 'static> ChangeMembers for Pallet { /// Update the members of the collective. Votes are updated and the prime is reset. /// /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but /// the weight estimations rely on it to estimate dispatchable weight. /// /// # /// ## Weight /// - `O(MP + N)` /// - where `M` old-members-count (governance-bounded) /// - where `N` new-members-count (governance-bounded) /// - where `P` proposals-count /// - DB: /// - 1 storage read (codec `O(P)`) for reading the proposals /// - `P` storage mutations for updating the votes (codec `O(M)`) /// - 1 storage write (codec `O(N)`) for storing the new members /// - 1 storage write (codec `O(1)`) for deleting the old prime /// # fn change_members_sorted( _incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId], ) { if new.len() > T::MaxMembers::get() as usize { log::error!( target: "runtime::collective", "New members count ({}) exceeds maximum amount of members expected ({}).", new.len(), T::MaxMembers::get(), ); } // remove accounts from all current voting in motions. let mut outgoing = outgoing.to_vec(); outgoing.sort(); 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); } fn get_prime() -> Option { Prime::::get() } } impl, I: 'static> InitializeMembers for Pallet { 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(PhantomData<(AccountId, I)>); impl< O: Into, O>> + From>, I, AccountId: Decode, > 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 { let zero_account_id = AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) .expect("infinite length input; no invalid inputs for type; qed"); O::from(RawOrigin::Member(zero_account_id)) } } pub struct EnsureMembers(PhantomData<(AccountId, I)>); impl< O: Into, O>> + From>, AccountId, I, const N: u32, > 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 => Ok((n, m)), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Members(N, N)) } } pub struct EnsureProportionMoreThan( PhantomData<(AccountId, I)>, ); impl< O: Into, O>> + From>, AccountId, I, const N: u32, const D: u32, > 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 > N * m => Ok(()), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Members(1u32, 0u32)) } } pub struct EnsureProportionAtLeast( PhantomData<(AccountId, I)>, ); impl< O: Into, O>> + From>, AccountId, I, const N: u32, const D: u32, > 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 >= N * m => Ok(()), r => Err(O::from(r)), }) } #[cfg(feature = "runtime-benchmarks")] fn successful_origin() -> O { O::from(RawOrigin::Members(0u32, 0u32)) } }