-
thiolliere authored
* warn * Apply suggestions from code review Co-authored-by:
joe petrowski <25483142+joepetrowski@users.noreply.github.com> * fmt Co-authored-by:
joe petrowski <25483142+joepetrowski@users.noreply.github.com>
1ae674dd
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
lib.rs 35.54 KiB
// This file is part of Substrate.
// Copyright (C) 2017-2021 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_core::u32_trait::Value as U32;
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<bool>,
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<bool>,
_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<bool>,
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<AccountId, I> {
/// 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<I>),
}
impl<AccountId, I> GetBacking for RawOrigin<AccountId, I> {
fn get_backing(&self) -> Option<Backing> {
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<AccountId, BlockNumber> {
/// 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<AccountId>,
/// The current set of voters that rejected it.
nays: Vec<AccountId>,
/// 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)]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
/// The outer origin type.
type Origin: From<RawOrigin<Self::AccountId, I>>;
/// The outer call dispatch type.
type Proposal: Parameter
+ Dispatchable<Origin = <Self as Config<I>>::Origin, PostInfo = PostDispatchInfo>
+ From<frame_system::Call<Self>>
+ GetDispatchInfo;
/// The outer event type.
type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>;
/// The time-out for council motions.
type MotionDuration: Get<Self::BlockNumber>;
/// Maximum number of proposals allowed to be active in parallel.
type MaxProposals: Get<ProposalIndex>;
/// 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<MemberCount>;
/// 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<T: Config<I>, I: 'static = ()> {
pub phantom: PhantomData<I>,
pub members: Vec<T::AccountId>,
}
#[cfg(feature = "std")]
impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> {
fn default() -> Self {
Self { phantom: Default::default(), members: Default::default() }
}
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> {
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::<T, I>::initialize_members(&self.members)
}
}
/// Origin for the collective pallet.
#[pallet::origin]
pub type Origin<T, I = ()> = RawOrigin<<T as frame_system::Config>::AccountId, I>;
/// The hashes of the active proposals.
#[pallet::storage]
#[pallet::getter(fn proposals)]
pub type Proposals<T: Config<I>, I: 'static = ()> =
StorageValue<_, BoundedVec<T::Hash, T::MaxProposals>, ValueQuery>;
/// Actual proposal for a given hash, if it's current.
#[pallet::storage]
#[pallet::getter(fn proposal_of)]
pub type ProposalOf<T: Config<I>, I: 'static = ()> =
StorageMap<_, Identity, T::Hash, <T as Config<I>>::Proposal, OptionQuery>;
/// Votes on a given proposal, if it is ongoing.
#[pallet::storage]
#[pallet::getter(fn voting)]
pub type Voting<T: Config<I>, I: 'static = ()> =
StorageMap<_, Identity, T::Hash, Votes<T::AccountId, T::BlockNumber>, OptionQuery>;
/// Proposals so far.
#[pallet::storage]
#[pallet::getter(fn proposal_count)]
pub type ProposalCount<T: Config<I>, 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<T: Config<I>, I: 'static = ()> =
StorageValue<_, Vec<T::AccountId>, ValueQuery>;
/// The prime member that helps determine the default vote behavior in case of absentations.
#[pallet::storage]
#[pallet::getter(fn prime)]
pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
/// A motion (given hash) has been proposed (by given account) with a threshold (given
/// `MemberCount`).
/// \[account, proposal_index, proposal_hash, threshold\]
Proposed(T::AccountId, ProposalIndex, T::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`).
/// \[account, proposal_hash, voted, yes, no\]
Voted(T::AccountId, T::Hash, bool, MemberCount, MemberCount),
/// A motion was approved by the required threshold.
/// \[proposal_hash\]
Approved(T::Hash),
/// A motion was not approved by the required threshold.
/// \[proposal_hash\]
Disapproved(T::Hash),
/// A motion was executed; result will be `Ok` if it returned without error.
/// \[proposal_hash, result\]
Executed(T::Hash, DispatchResult),
/// A single member did some action; result will be `Ok` if it returned without error.
/// \[proposal_hash, result\]
MemberExecuted(T::Hash, DispatchResult),
/// A proposal was closed because its threshold was reached or after its duration was up.
/// \[proposal_hash, yes, no\]
Closed(T::Hash, MemberCount, MemberCount),
}
/// Old name generated by `decl_event`.
#[deprecated(note = "use `Event` instead")]
pub type RawEvent<T, I = ()> = Event<T, I>;
#[pallet::error]
pub enum Error<T, I = ()> {
/// 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<T: Config<I>, I: 'static> Pallet<T, I> {
/// 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>
/// ## 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
/// # </weight>
#[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<T>,
new_members: Vec<T::AccountId>,
prime: Option<T::AccountId>,
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::<T, I>::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();
<Self as ChangeMembers<T::AccountId>>::set_members_sorted(&new_members, &old);
Prime::<T, I>::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>
/// ## 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
/// # </weight>
#[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<T>,
proposal: Box<<T as Config<I>>::Proposal>,
#[pallet::compact] length_bound: u32,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let members = Self::members();
ensure!(members.contains(&who), Error::<T, I>::NotMember);
let proposal_len = proposal.using_encoded(|x| x.len());
ensure!(proposal_len <= length_bound as usize, Error::<T, I>::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.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>
/// ## 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
/// # </weight>
#[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<T>,
#[pallet::compact] threshold: MemberCount,
proposal: Box<<T as Config<I>>::Proposal>,
#[pallet::compact] length_bound: u32,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let members = Self::members();
ensure!(members.contains(&who), Error::<T, I>::NotMember);
let proposal_len = proposal.using_encoded(|x| x.len());
ensure!(proposal_len <= length_bound as usize, Error::<T, I>::WrongProposalLength);
let proposal_hash = T::Hashing::hash_of(&proposal);
ensure!(
!<ProposalOf<T, I>>::contains_key(proposal_hash),
Error::<T, I>::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.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 =
<Proposals<T, I>>::try_mutate(|proposals| -> Result<usize, DispatchError> {
proposals
.try_push(proposal_hash)
.map_err(|_| Error::<T, I>::TooManyProposals)?;
Ok(proposals.len())
})?;
let index = Self::proposal_count();
<ProposalCount<T, I>>::mutate(|i| *i += 1);
<ProposalOf<T, I>>::insert(proposal_hash, *proposal);
let votes = {
let end = frame_system::Pallet::<T>::block_number() + T::MotionDuration::get();
Votes { index, threshold, ayes: vec![], nays: vec![], end }
};
<Voting<T, I>>::insert(proposal_hash, votes);
Self::deposit_event(Event::Proposed(who, 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>
/// ## 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
/// # </weight>
#[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))]
pub fn vote(
origin: OriginFor<T>,
proposal: T::Hash,
#[pallet::compact] index: ProposalIndex,
approve: bool,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let members = Self::members();
ensure!(members.contains(&who), Error::<T, I>::NotMember);
let mut voting = Self::voting(&proposal).ok_or(Error::<T, I>::ProposalMissing)?;
ensure!(voting.index == index, Error::<T, I>::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::<T, I>::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::<T, I>::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(who, proposal, approve, yes_votes, no_votes));
Voting::<T, I>::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::<u32>() == 4` larger than the pure length.
///
/// # <weight>
/// ## 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
/// # </weight>
#[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<T>,
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::<T, I>::ProposalMissing)?;
ensure!(voting.index == index, Error::<T, I>::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_votes, 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_votes, 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::<T>::block_number() >= voting.end,
Error::<T, I>::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_votes, 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_votes, 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.
///
/// # <weight>
/// Complexity: O(P) where P is the number of max proposals
/// DB Weight:
/// * Reads: Proposals
/// * Writes: Voting, Proposals, ProposalOf
/// # </weight>
#[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))]
pub fn disapprove_proposal(
origin: OriginFor<T>,
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<Weight> {
match result {
Ok(post_info) => post_info.actual_weight,
Err(err) => err.post_info.actual_weight,
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// 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::<u32>() == 4`
/// to the length.
fn validate_and_get_proposal(
hash: &T::Hash,
length_bound: u32,
weight_bound: Weight,
) -> Result<(<T as Config<I>>::Proposal, usize), DispatchError> {
let key = ProposalOf::<T, I>::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::<T, I>::ProposalMissing)?;
ensure!(proposal_len <= length_bound, Error::<T, I>::WrongProposalLength);
let proposal = ProposalOf::<T, I>::get(hash).ok_or(Error::<T, I>::ProposalMissing)?;
let proposal_weight = proposal.get_dispatch_info().weight;
ensure!(proposal_weight <= weight_bound, Error::<T, I>::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: <T as Config<I>>::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.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::<T, I>::remove(&proposal_hash);
Voting::<T, I>::remove(&proposal_hash);
let num_proposals = Proposals::<T, I>::mutate(|proposals| {
proposals.retain(|h| h != &proposal_hash);
proposals.len() + 1 // calculate weight based on original length
});
num_proposals as u32
}
}
impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> {
/// 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>
/// ## 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
/// # </weight>
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() {
<Voting<T, I>>::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::<T, I>::put(new);
Prime::<T, I>::kill();
}
fn set_prime(prime: Option<T::AccountId>) {
Prime::<T, I>::set(prime);
}
fn get_prime() -> Option<T::AccountId> {
Prime::<T, I>::get()
}
}
impl<T: Config<I>, I: 'static> InitializeMembers<T::AccountId> for Pallet<T, I> {
fn initialize_members(members: &[T::AccountId]) {
if !members.is_empty() {
assert!(<Members<T, I>>::get().is_empty(), "Members are already initialized!");
<Members<T, I>>::put(members);
}
}
}
/// Ensure that the origin `o` represents at least `n` members. Returns `Ok` or an `Err`
/// otherwise.
pub fn ensure_members<OuterOrigin, AccountId, I>(
o: OuterOrigin,
n: MemberCount,
) -> result::Result<MemberCount, &'static str>
where
OuterOrigin: Into<result::Result<RawOrigin<AccountId, I>, 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<AccountId, I: 'static>(PhantomData<(AccountId, I)>);
impl<
O: Into<Result<RawOrigin<AccountId, I>, O>> + From<RawOrigin<AccountId, I>>,
AccountId: Default,
I,
> EnsureOrigin<O> for EnsureMember<AccountId, I>
{
type Success = AccountId;
fn try_origin(o: O) -> Result<Self::Success, O> {
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<N: U32, AccountId, I: 'static>(PhantomData<(N, AccountId, I)>);
impl<
O: Into<Result<RawOrigin<AccountId, I>, O>> + From<RawOrigin<AccountId, I>>,
N: U32,
AccountId,
I,
> EnsureOrigin<O> for EnsureMembers<N, AccountId, I>
{
type Success = (MemberCount, MemberCount);
fn try_origin(o: O) -> Result<Self::Success, O> {
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<N: U32, D: U32, AccountId, I: 'static>(
PhantomData<(N, D, AccountId, I)>,
);
impl<
O: Into<Result<RawOrigin<AccountId, I>, O>> + From<RawOrigin<AccountId, I>>,
N: U32,
D: U32,
AccountId,
I,
> EnsureOrigin<O> for EnsureProportionMoreThan<N, D, AccountId, I>
{
type Success = ();
fn try_origin(o: O) -> Result<Self::Success, O> {
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<N: U32, D: U32, AccountId, I: 'static>(
PhantomData<(N, D, AccountId, I)>,
);
impl<
O: Into<Result<RawOrigin<AccountId, I>, O>> + From<RawOrigin<AccountId, I>>,
N: U32,
D: U32,
AccountId,
I,
> EnsureOrigin<O> for EnsureProportionAtLeast<N, D, AccountId, I>
{
type Success = ();
fn try_origin(o: O) -> Result<Self::Success, O> {
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))
}
}