mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-11 11:51:12 +00:00
Technical Committee (#3041)
* Add copy of council seats as elections module. * Split council into collective and elections modules. Make collective instanceable. * Propagate changes to the runtime and fix origin/event * insert_ref and put_ref to avoid copies. * Add tests * Fix up collective's tests * One more test * Fix elections module tests * Missed merge line * Minor fix * Test fixes * Line widths * Line widths * Rntime version * Remove comment * Deduplicate * Bump runtime again * Fix test
This commit is contained in:
Generated
+22
-5
@@ -2295,9 +2295,10 @@ dependencies = [
|
||||
"srml-aura 2.0.0",
|
||||
"srml-authorship 0.1.0",
|
||||
"srml-balances 2.0.0",
|
||||
"srml-collective 2.0.0",
|
||||
"srml-contracts 2.0.0",
|
||||
"srml-council 2.0.0",
|
||||
"srml-democracy 2.0.0",
|
||||
"srml-elections 2.0.0",
|
||||
"srml-executive 2.0.0",
|
||||
"srml-finality-tracker 2.0.0",
|
||||
"srml-grandpa 2.0.0",
|
||||
@@ -3642,6 +3643,23 @@ dependencies = [
|
||||
"substrate-primitives 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-collective"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parity-codec 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sr-io 2.0.0",
|
||||
"sr-primitives 2.0.0",
|
||||
"sr-std 2.0.0",
|
||||
"srml-balances 2.0.0",
|
||||
"srml-support 2.0.0",
|
||||
"srml-system 2.0.0",
|
||||
"substrate-primitives 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-contracts"
|
||||
version = "2.0.0"
|
||||
@@ -3667,10 +3685,9 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-council"
|
||||
name = "srml-democracy"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parity-codec 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -3678,16 +3695,16 @@ dependencies = [
|
||||
"sr-primitives 2.0.0",
|
||||
"sr-std 2.0.0",
|
||||
"srml-balances 2.0.0",
|
||||
"srml-democracy 2.0.0",
|
||||
"srml-support 2.0.0",
|
||||
"srml-system 2.0.0",
|
||||
"substrate-primitives 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "srml-democracy"
|
||||
name = "srml-elections"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"hex-literal 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parity-codec 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
||||
@@ -69,8 +69,9 @@ members = [
|
||||
"srml/aura",
|
||||
"srml/balances",
|
||||
"srml/contracts",
|
||||
"srml/council",
|
||||
"srml/collective",
|
||||
"srml/democracy",
|
||||
"srml/elections",
|
||||
"srml/example",
|
||||
"srml/executive",
|
||||
"srml/finality-tracker",
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
use primitives::{ed25519, sr25519, Pair, crypto::UncheckedInto};
|
||||
use node_primitives::{AccountId, AuraId, Balance};
|
||||
use node_runtime::{
|
||||
AuraConfig, BalancesConfig, ContractsConfig, CouncilSeatsConfig, DemocracyConfig,
|
||||
GrandpaConfig, IndicesConfig, SessionConfig, StakingConfig, SudoConfig,
|
||||
GrandpaConfig, BalancesConfig, ContractsConfig, ElectionsConfig, DemocracyConfig, CouncilConfig,
|
||||
AuraConfig, IndicesConfig, SessionConfig, StakingConfig, SudoConfig, TechnicalCommitteeConfig,
|
||||
SystemConfig, TimestampConfig, WASM_BINARY, Perbill, SessionKeys, StakerStatus, DAYS, DOLLARS,
|
||||
MILLICENTS, SECS_PER_BLOCK,
|
||||
};
|
||||
@@ -130,8 +130,16 @@ fn staging_testnet_config_genesis() -> GenesisConfig {
|
||||
invulnerables: initial_authorities.iter().map(|x| x.1.clone()).collect(),
|
||||
}),
|
||||
democracy: Some(DemocracyConfig::default()),
|
||||
council_seats: Some(CouncilSeatsConfig {
|
||||
active_council: vec![],
|
||||
collective_Instance1: Some(CouncilConfig {
|
||||
members: vec![],
|
||||
phantom: Default::default(),
|
||||
}),
|
||||
collective_Instance2: Some(TechnicalCommitteeConfig {
|
||||
members: vec![],
|
||||
phantom: Default::default(),
|
||||
}),
|
||||
elections: Some(ElectionsConfig {
|
||||
members: vec![],
|
||||
presentation_duration: 1 * DAYS,
|
||||
term_duration: 28 * DAYS,
|
||||
desired_seats: 0,
|
||||
@@ -228,7 +236,7 @@ pub fn testnet_genesis(
|
||||
const ENDOWMENT: Balance = 10_000_000 * DOLLARS;
|
||||
const STASH: Balance = 100 * DOLLARS;
|
||||
|
||||
let council_desired_seats = (endowed_accounts.len() / 2 - initial_authorities.len()) as u32;
|
||||
let desired_seats = (endowed_accounts.len() / 2 - initial_authorities.len()) as u32;
|
||||
|
||||
GenesisConfig {
|
||||
system: Some(SystemConfig {
|
||||
@@ -257,13 +265,21 @@ pub fn testnet_genesis(
|
||||
invulnerables: initial_authorities.iter().map(|x| x.1.clone()).collect(),
|
||||
}),
|
||||
democracy: Some(DemocracyConfig::default()),
|
||||
council_seats: Some(CouncilSeatsConfig {
|
||||
active_council: endowed_accounts.iter()
|
||||
collective_Instance1: Some(CouncilConfig {
|
||||
members: vec![],
|
||||
phantom: Default::default(),
|
||||
}),
|
||||
collective_Instance2: Some(TechnicalCommitteeConfig {
|
||||
members: vec![],
|
||||
phantom: Default::default(),
|
||||
}),
|
||||
elections: Some(ElectionsConfig {
|
||||
members: endowed_accounts.iter()
|
||||
.filter(|&endowed| initial_authorities.iter().find(|&(_, controller, ..)| controller == endowed).is_none())
|
||||
.map(|a| (a.clone(), 1000000)).collect(),
|
||||
presentation_duration: 10,
|
||||
term_duration: 1000000,
|
||||
desired_seats: council_desired_seats,
|
||||
desired_seats: desired_seats,
|
||||
}),
|
||||
timestamp: Some(TimestampConfig {
|
||||
minimum_period: 2, // 2*2=4 second block time.
|
||||
|
||||
@@ -343,7 +343,9 @@ mod tests {
|
||||
invulnerables: vec![alice(), bob(), charlie()],
|
||||
}),
|
||||
democracy: Some(Default::default()),
|
||||
council_seats: Some(Default::default()),
|
||||
collective_Instance1: Some(Default::default()),
|
||||
collective_Instance2: Some(Default::default()),
|
||||
elections: Some(Default::default()),
|
||||
timestamp: Some(Default::default()),
|
||||
contracts: Some(ContractsConfig {
|
||||
current_schedule: Default::default(),
|
||||
|
||||
@@ -20,8 +20,9 @@ aura = { package = "srml-aura", path = "../../srml/aura", default-features = fal
|
||||
authorship = { package = "srml-authorship", path = "../../srml/authorship", default-features = false }
|
||||
balances = { package = "srml-balances", path = "../../srml/balances", default-features = false }
|
||||
contracts = { package = "srml-contracts", path = "../../srml/contracts", default-features = false }
|
||||
council = { package = "srml-council", path = "../../srml/council", default-features = false }
|
||||
collective = { package = "srml-collective", path = "../../srml/collective", default-features = false }
|
||||
democracy = { package = "srml-democracy", path = "../../srml/democracy", default-features = false }
|
||||
elections = { package = "srml-elections", path = "../../srml/elections", default-features = false }
|
||||
executive = { package = "srml-executive", path = "../../srml/executive", default-features = false }
|
||||
finality-tracker = { package = "srml-finality-tracker", path = "../../srml/finality-tracker", default-features = false }
|
||||
grandpa = { package = "srml-grandpa", path = "../../srml/grandpa", default-features = false }
|
||||
@@ -56,8 +57,9 @@ std = [
|
||||
"authorship/std",
|
||||
"balances/std",
|
||||
"contracts/std",
|
||||
"council/std",
|
||||
"collective/std",
|
||||
"democracy/std",
|
||||
"elections/std",
|
||||
"executive/std",
|
||||
"finality-tracker/std",
|
||||
"grandpa/std",
|
||||
|
||||
@@ -40,9 +40,7 @@ use runtime_primitives::traits::{
|
||||
BlakeTwo256, Block as BlockT, DigestFor, NumberFor, StaticLookup, Convert,
|
||||
};
|
||||
use version::RuntimeVersion;
|
||||
use council::{motions as council_motions, VoteIndex};
|
||||
#[cfg(feature = "std")]
|
||||
use council::seats as council_seats;
|
||||
use elections::VoteIndex;
|
||||
#[cfg(any(feature = "std", test))]
|
||||
use version::NativeVersion;
|
||||
use substrate_primitives::OpaqueMetadata;
|
||||
@@ -71,8 +69,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
|
||||
// and set impl_version to equal spec_version. If only runtime
|
||||
// implementation changes and behavior does not, then leave spec_version as
|
||||
// is and increment impl_version.
|
||||
spec_version: 107,
|
||||
impl_version: 107,
|
||||
spec_version: 108,
|
||||
impl_version: 108,
|
||||
apis: RUNTIME_API_VERSIONS,
|
||||
};
|
||||
|
||||
@@ -265,14 +263,22 @@ impl democracy::Trait for Runtime {
|
||||
type VotingPeriod = VotingPeriod;
|
||||
type EmergencyVotingPeriod = EmergencyVotingPeriod;
|
||||
type MinimumDeposit = MinimumDeposit;
|
||||
type ExternalOrigin = council_motions::EnsureProportionAtLeast<_1, _2, AccountId>;
|
||||
type ExternalMajorityOrigin = council_motions::EnsureProportionAtLeast<_2, _3, AccountId>;
|
||||
type EmergencyOrigin = council_motions::EnsureProportionAtLeast<_1, _1, AccountId>;
|
||||
type CancellationOrigin = council_motions::EnsureProportionAtLeast<_2, _3, AccountId>;
|
||||
type VetoOrigin = council_motions::EnsureMember<AccountId>;
|
||||
type ExternalOrigin = collective::EnsureProportionAtLeast<_1, _2, AccountId, CouncilInstance>;
|
||||
type ExternalMajorityOrigin = collective::EnsureProportionAtLeast<_2, _3, AccountId, CouncilInstance>;
|
||||
type ExternalPushOrigin = collective::EnsureProportionAtLeast<_2, _3, AccountId, TechnicalInstance>;
|
||||
type EmergencyOrigin = collective::EnsureProportionAtLeast<_1, _1, AccountId, CouncilInstance>;
|
||||
type CancellationOrigin = collective::EnsureProportionAtLeast<_2, _3, AccountId, CouncilInstance>;
|
||||
type VetoOrigin = collective::EnsureMember<AccountId, CouncilInstance>;
|
||||
type CooloffPeriod = CooloffPeriod;
|
||||
}
|
||||
|
||||
type CouncilInstance = collective::Instance1;
|
||||
impl collective::Trait<CouncilInstance> for Runtime {
|
||||
type Origin = Origin;
|
||||
type Proposal = Call;
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const CandidacyBond: Balance = 10 * DOLLARS;
|
||||
pub const VotingBond: Balance = 1 * DOLLARS;
|
||||
@@ -281,28 +287,30 @@ parameter_types! {
|
||||
pub const CarryCount: u32 = 6;
|
||||
// one additional vote should go by before an inactive voter can be reaped.
|
||||
pub const InactiveGracePeriod: VoteIndex = 1;
|
||||
pub const CouncilVotingPeriod: BlockNumber = 2 * DAYS;
|
||||
pub const ElectionsVotingPeriod: BlockNumber = 2 * DAYS;
|
||||
pub const DecayRatio: u32 = 0;
|
||||
}
|
||||
|
||||
impl council::Trait for Runtime {
|
||||
impl elections::Trait for Runtime {
|
||||
type Event = Event;
|
||||
type Currency = Balances;
|
||||
type BadPresentation = ();
|
||||
type BadReaper = ();
|
||||
type BadVoterIndex = ();
|
||||
type LoserCandidate = ();
|
||||
type OnMembersChanged = CouncilMotions;
|
||||
type ChangeMembers = Council;
|
||||
type CandidacyBond = CandidacyBond;
|
||||
type VotingBond = VotingBond;
|
||||
type VotingFee = VotingFee;
|
||||
type PresentSlashPerVoter = PresentSlashPerVoter;
|
||||
type CarryCount = CarryCount;
|
||||
type InactiveGracePeriod = InactiveGracePeriod;
|
||||
type CouncilVotingPeriod = CouncilVotingPeriod;
|
||||
type VotingPeriod = ElectionsVotingPeriod;
|
||||
type DecayRatio = DecayRatio;
|
||||
}
|
||||
|
||||
impl council::motions::Trait for Runtime {
|
||||
type TechnicalInstance = collective::Instance2;
|
||||
impl collective::Trait<TechnicalInstance> for Runtime {
|
||||
type Origin = Origin;
|
||||
type Proposal = Call;
|
||||
type Event = Event;
|
||||
@@ -317,8 +325,8 @@ parameter_types! {
|
||||
|
||||
impl treasury::Trait for Runtime {
|
||||
type Currency = Balances;
|
||||
type ApproveOrigin = council_motions::EnsureMembers<_4, AccountId>;
|
||||
type RejectOrigin = council_motions::EnsureMembers<_2, AccountId>;
|
||||
type ApproveOrigin = collective::EnsureMembers<_4, AccountId, CouncilInstance>;
|
||||
type RejectOrigin = collective::EnsureMembers<_2, AccountId, CouncilInstance>;
|
||||
type Event = Event;
|
||||
type MintedForSpending = ();
|
||||
type ProposalRejection = ();
|
||||
@@ -406,9 +414,9 @@ construct_runtime!(
|
||||
Session: session::{Module, Call, Storage, Event, Config<T>},
|
||||
Staking: staking::{default, OfflineWorker},
|
||||
Democracy: democracy::{Module, Call, Storage, Config, Event<T>},
|
||||
Council: council::{Module, Call, Storage, Event<T>},
|
||||
CouncilMotions: council_motions::{Module, Call, Storage, Event<T>, Origin<T>},
|
||||
CouncilSeats: council_seats::{Config<T>},
|
||||
Council: collective::<Instance1>::{Module, Call, Storage, Origin<T>, Event<T>, Config<T>},
|
||||
TechnicalCommittee: collective::<Instance2>::{Module, Call, Storage, Origin<T>, Event<T>, Config<T>},
|
||||
Elections: elections::{Module, Call, Storage, Event<T>, Config<T>},
|
||||
FinalityTracker: finality_tracker::{Module, Call, Inherent},
|
||||
Grandpa: grandpa::{Module, Call, Storage, Config, Event},
|
||||
Treasury: treasury::{Module, Call, Storage, Event<T>},
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use primitives::{
|
||||
KeyTypeId,
|
||||
traits::IdentityLookup,
|
||||
testing::{UINT_DUMMY_KEY, Header, UintAuthorityId},
|
||||
testing::{Header, UintAuthorityId},
|
||||
};
|
||||
use srml_support::{impl_outer_origin, parameter_types};
|
||||
use runtime_io;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "srml-council"
|
||||
name = "srml-collective"
|
||||
version = "2.0.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
@@ -13,7 +13,6 @@ rstd = { package = "sr-std", path = "../../core/sr-std", default-features = fals
|
||||
runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false }
|
||||
primitives = { package = "sr-primitives", path = "../../core/sr-primitives", default-features = false }
|
||||
srml-support = { path = "../support", default-features = false }
|
||||
democracy = { package = "srml-democracy", path = "../democracy", default-features = false }
|
||||
system = { package = "srml-system", path = "../system", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -31,6 +30,5 @@ std = [
|
||||
"runtime_io/std",
|
||||
"srml-support/std",
|
||||
"primitives/std",
|
||||
"democracy/std",
|
||||
"system/std",
|
||||
]
|
||||
@@ -0,0 +1,728 @@
|
||||
// Copyright 2017-2019 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collective system: Members of a set of account IDs can make their collective feelings known
|
||||
//! through dispatched calls from one of two specialised origins.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![recursion_limit="128"]
|
||||
|
||||
use rstd::{prelude::*, result};
|
||||
use substrate_primitives::u32_trait::Value as U32;
|
||||
use primitives::traits::{Hash, EnsureOrigin};
|
||||
use srml_support::{
|
||||
dispatch::{Dispatchable, Parameter}, codec::{Encode, Decode}, traits::ChangeMembers,
|
||||
StorageValue, StorageMap, decl_module, decl_event, decl_storage, ensure
|
||||
};
|
||||
use system::{self, ensure_signed, ensure_root};
|
||||
|
||||
/// 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<I=DefaultInstance>: system::Trait {
|
||||
/// The outer origin type.
|
||||
type Origin: From<RawOrigin<Self::AccountId, I>>;
|
||||
|
||||
/// The outer call dispatch type.
|
||||
type Proposal: Parameter + Dispatchable<Origin=<Self as Trait<I>>::Origin>;
|
||||
|
||||
/// The outer event type.
|
||||
type Event: From<Event<Self, I>> + Into<<Self as system::Trait>::Event>;
|
||||
}
|
||||
|
||||
/// Origin for the collective module.
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
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(rstd::marker::PhantomData<I>),
|
||||
}
|
||||
|
||||
/// Origin for the collective module.
|
||||
pub type Origin<T, I> = RawOrigin<<T as system::Trait>::AccountId, I>;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
/// Info for keeping track of a motion being voted on.
|
||||
pub struct Votes<AccountId> {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Trait<I>, I: Instance=DefaultInstance> as Collective {
|
||||
/// The hashes of the active proposals.
|
||||
pub Proposals get(proposals): Vec<T::Hash>;
|
||||
/// Actual proposal for a given hash, if it's current.
|
||||
pub ProposalOf get(proposal_of): map T::Hash => Option<<T as Trait<I>>::Proposal>;
|
||||
/// Votes on a given proposal, if it is ongoing.
|
||||
pub Voting get(voting): map T::Hash => Option<Votes<T::AccountId>>;
|
||||
/// Proposals so far.
|
||||
pub ProposalCount get(proposal_count): u32;
|
||||
/// The current members of the collective. This is stored sorted (just by value).
|
||||
pub Members get(members) config(): Vec<T::AccountId>;
|
||||
}
|
||||
add_extra_genesis {
|
||||
config(phantom): rstd::marker::PhantomData<I>;
|
||||
}
|
||||
}
|
||||
|
||||
decl_event!(
|
||||
pub enum Event<T, I> where
|
||||
<T as system::Trait>::Hash,
|
||||
<T as system::Trait>::AccountId,
|
||||
Phantom = rstd::marker::PhantomData<T>
|
||||
{
|
||||
/// Dummy to manage the fact we have instancing.
|
||||
_Phantom(Phantom),
|
||||
/// 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),
|
||||
}
|
||||
);
|
||||
|
||||
decl_module! {
|
||||
pub struct Module<T: Trait<I>, I: Instance=DefaultInstance> for enum Call where origin: <T as system::Trait>::Origin {
|
||||
fn deposit_event<T, I>() = default;
|
||||
|
||||
/// Set the collective's membership manually to `new_members`. Be nice to the chain and
|
||||
/// provide it pre-sorted.
|
||||
///
|
||||
/// Requires root origin.
|
||||
fn set_members(origin, new_members: Vec<T::AccountId>) {
|
||||
ensure_root(origin)?;
|
||||
|
||||
// stable sorting since they will generally be provided sorted.
|
||||
let mut old_members = <Members<T, I>>::get();
|
||||
old_members.sort();
|
||||
let mut new_members = new_members;
|
||||
new_members.sort();
|
||||
let mut old_iter = old_members.iter();
|
||||
let mut new_iter = new_members.iter();
|
||||
let mut incoming = vec![];
|
||||
let mut outgoing = vec![];
|
||||
let mut old_i = old_iter.next();
|
||||
let mut new_i = new_iter.next();
|
||||
loop {
|
||||
match (old_i, new_i) {
|
||||
(None, None) => break,
|
||||
(Some(old), Some(new)) if old == new => {
|
||||
old_i = old_iter.next();
|
||||
new_i = new_iter.next();
|
||||
}
|
||||
(Some(old), Some(new)) if old < new => {
|
||||
outgoing.push(old.clone());
|
||||
old_i = old_iter.next();
|
||||
}
|
||||
(Some(old), None) => {
|
||||
outgoing.push(old.clone());
|
||||
old_i = old_iter.next();
|
||||
}
|
||||
(_, Some(new)) => {
|
||||
incoming.push(new.clone());
|
||||
new_i = new_iter.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::change_members(&incoming, &outgoing, &new_members);
|
||||
}
|
||||
|
||||
/// Dispatch a proposal from a member using the `Member` origin.
|
||||
///
|
||||
/// Origin must be a member of the collective.
|
||||
fn execute(origin, proposal: Box<<T as Trait<I>>::Proposal>) {
|
||||
let who = ensure_signed(origin)?;
|
||||
ensure!(Self::is_member(&who), "proposer not a member");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// # <weight>
|
||||
/// - Bounded storage reads and writes.
|
||||
/// - Argument `threshold` has bearing on weight.
|
||||
/// # </weight>
|
||||
fn propose(origin, #[compact] threshold: MemberCount, proposal: Box<<T as Trait<I>>::Proposal>) {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(Self::is_member(&who), "proposer not a member");
|
||||
|
||||
let proposal_hash = T::Hashing::hash_of(&proposal);
|
||||
|
||||
ensure!(!<ProposalOf<T, I>>::exists(proposal_hash), "duplicate proposals not allowed");
|
||||
|
||||
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();
|
||||
<ProposalCount<I>>::mutate(|i| *i += 1);
|
||||
<Proposals<T, I>>::mutate(|proposals| proposals.push(proposal_hash));
|
||||
<ProposalOf<T, I>>::insert(proposal_hash, *proposal);
|
||||
let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![] };
|
||||
<Voting<T, I>>::insert(proposal_hash, votes);
|
||||
|
||||
Self::deposit_event(RawEvent::Proposed(who, index, proposal_hash, threshold));
|
||||
}
|
||||
}
|
||||
|
||||
/// # <weight>
|
||||
/// - Bounded storage read and writes.
|
||||
/// - Will be slightly heavier if the proposal is approved / disapproved after the vote.
|
||||
/// # </weight>
|
||||
fn vote(origin, proposal: T::Hash, #[compact] index: ProposalIndex, approve: bool) {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(Self::is_member(&who), "voter not a member");
|
||||
|
||||
let mut voting = Self::voting(&proposal).ok_or("proposal must exist")?;
|
||||
ensure!(voting.index == index, "mismatched index");
|
||||
|
||||
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 {
|
||||
return Err("duplicate vote ignored")
|
||||
}
|
||||
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("duplicate vote ignored")
|
||||
}
|
||||
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 {
|
||||
if approved {
|
||||
Self::deposit_event(RawEvent::Approved(proposal));
|
||||
|
||||
// execute motion, assuming it exists.
|
||||
if let Some(p) = <ProposalOf<T, I>>::take(&proposal) {
|
||||
let origin = RawOrigin::Members(voting.threshold, seats).into();
|
||||
let ok = p.dispatch(origin).is_ok();
|
||||
Self::deposit_event(RawEvent::Executed(proposal, ok));
|
||||
}
|
||||
} else {
|
||||
// disapproved
|
||||
Self::deposit_event(RawEvent::Disapproved(proposal));
|
||||
}
|
||||
|
||||
// remove vote
|
||||
<Voting<T, I>>::remove(&proposal);
|
||||
<Proposals<T, I>>::mutate(|proposals| proposals.retain(|h| h != &proposal));
|
||||
} else {
|
||||
// update voting
|
||||
<Voting<T, I>>::insert(&proposal, voting);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait<I>, I: Instance> Module<T, I> {
|
||||
pub fn is_member(who: &T::AccountId) -> bool {
|
||||
Self::members().contains(who)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait<I>, I: Instance> ChangeMembers<T::AccountId> for Module<T, I> {
|
||||
fn change_members(_incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId]) {
|
||||
// remove accounts from all current voting in motions.
|
||||
let mut old = outgoing.to_vec();
|
||||
old.sort_unstable();
|
||||
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| old.binary_search(i).is_err())
|
||||
.collect();
|
||||
votes.nays = votes.nays.into_iter()
|
||||
.filter(|i| old.binary_search(i).is_err())
|
||||
.collect();
|
||||
*v = Some(votes);
|
||||
}
|
||||
);
|
||||
}
|
||||
<Members<T, I>>::put_ref(new);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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=DefaultInstance>(rstd::marker::PhantomData<(AccountId, I)>);
|
||||
impl<
|
||||
O: Into<Result<RawOrigin<AccountId, I>, O>> + From<RawOrigin<AccountId, I>>,
|
||||
AccountId,
|
||||
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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureMembers<N: U32, AccountId, I=DefaultInstance>(rstd::marker::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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureProportionMoreThan<N: U32, D: U32, AccountId, I=DefaultInstance>(
|
||||
rstd::marker::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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureProportionAtLeast<N: U32, D: U32, AccountId, I=DefaultInstance>(
|
||||
rstd::marker::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(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use srml_support::{Hashable, assert_ok, assert_noop, parameter_types};
|
||||
use system::{EventRecord, Phase};
|
||||
use hex_literal::hex;
|
||||
use runtime_io::with_externalities;
|
||||
use substrate_primitives::{H256, Blake2Hasher};
|
||||
use primitives::{
|
||||
traits::{BlakeTwo256, IdentityLookup, Block as BlockT}, testing::Header, BuildStorage
|
||||
};
|
||||
use crate as collective;
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
}
|
||||
impl system::Trait for Test {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Header = Header;
|
||||
type Event = Event;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
}
|
||||
impl Trait<Instance1> for Test {
|
||||
type Origin = Origin;
|
||||
type Proposal = Call;
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
pub type Block = primitives::generic::Block<Header, UncheckedExtrinsic>;
|
||||
pub type UncheckedExtrinsic = primitives::generic::UncheckedMortalCompactExtrinsic<u32, u64, Call, ()>;
|
||||
|
||||
srml_support::construct_runtime!(
|
||||
pub enum Test where
|
||||
Block = Block,
|
||||
NodeBlock = Block,
|
||||
UncheckedExtrinsic = UncheckedExtrinsic
|
||||
{
|
||||
System: system::{Module, Call, Event},
|
||||
Collective: collective::<Instance1>::{Module, Call, Event<T>, Origin<T>, Config<T>},
|
||||
}
|
||||
);
|
||||
|
||||
fn make_ext() -> runtime_io::TestExternalities<Blake2Hasher> {
|
||||
GenesisConfig {
|
||||
collective_Instance1: Some(collective::GenesisConfig {
|
||||
members: vec![1, 2, 3],
|
||||
phantom: Default::default(),
|
||||
}),
|
||||
}.build_storage().unwrap().0.into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_basic_environment_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
assert_eq!(Collective::members(), vec![1, 2, 3]);
|
||||
assert_eq!(Collective::proposals(), Vec::<H256>::new());
|
||||
});
|
||||
}
|
||||
|
||||
fn make_proposal(value: u64) -> Call {
|
||||
Call::System(system::Call::remark(value.encode()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_of_old_voters_votes_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = make_proposal(42);
|
||||
let hash = BlakeTwo256::hash_of(&proposal);
|
||||
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
|
||||
assert_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] })
|
||||
);
|
||||
Collective::change_members(&[4], &[1], &[2, 3, 4]);
|
||||
assert_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![] })
|
||||
);
|
||||
|
||||
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] })
|
||||
);
|
||||
Collective::change_members(&[], &[3], &[2, 4]);
|
||||
assert_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![] })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_of_old_voters_votes_works_with_set_members() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = make_proposal(42);
|
||||
let hash = BlakeTwo256::hash_of(&proposal);
|
||||
assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_ok!(Collective::vote(Origin::signed(2), hash.clone(), 0, true));
|
||||
assert_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] })
|
||||
);
|
||||
assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 3, 4]));
|
||||
assert_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![] })
|
||||
);
|
||||
|
||||
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] })
|
||||
);
|
||||
assert_ok!(Collective::set_members(Origin::ROOT, vec![2, 4]));
|
||||
assert_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![] })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propose_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = make_proposal(42);
|
||||
let hash = proposal.blake2_256().into();
|
||||
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![] })
|
||||
);
|
||||
|
||||
assert_eq!(System::events(), vec![
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
3,
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_ignoring_non_collective_proposals_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = make_proposal(42);
|
||||
assert_noop!(
|
||||
Collective::propose(Origin::signed(42), 3, Box::new(proposal.clone())),
|
||||
"proposer not a member"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_ignoring_non_collective_votes_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
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), "voter not a member");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_ignoring_bad_index_collective_vote_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
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), "mismatched index");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_revoting_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
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_eq!(
|
||||
Collective::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![] })
|
||||
);
|
||||
assert_noop!(Collective::vote(Origin::signed(1), hash.clone(), 0, true), "duplicate vote ignored");
|
||||
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] })
|
||||
);
|
||||
assert_noop!(Collective::vote(Origin::signed(1), hash.clone(), 0, false), "duplicate vote ignored");
|
||||
|
||||
assert_eq!(System::events(), vec![
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
2,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Voted(
|
||||
1,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
false,
|
||||
0,
|
||||
1,
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_disapproval_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
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::Finalization,
|
||||
event: Event::collective_Instance1(
|
||||
RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
3,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Voted(
|
||||
2,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
false,
|
||||
1,
|
||||
1,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Disapproved(
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_approval_works() {
|
||||
with_externalities(&mut make_ext(), || {
|
||||
System::set_block_number(1);
|
||||
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::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
2,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Voted(
|
||||
2,
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
true,
|
||||
2,
|
||||
0,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Approved(
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Finalization,
|
||||
event: Event::collective_Instance1(RawEvent::Executed(
|
||||
hex!["10b209e55d0f37cd45574674bba42519a29bf0ccf3c85c3c773fcbacab820bb4"].into(),
|
||||
false,
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
// Copyright 2017-2019 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Council voting system.
|
||||
|
||||
use rstd::{prelude::*, result};
|
||||
use substrate_primitives::u32_trait::Value as U32;
|
||||
use primitives::traits::{Hash, EnsureOrigin};
|
||||
use srml_support::{
|
||||
dispatch::{Dispatchable, Parameter}, codec::{Encode, Decode},
|
||||
StorageValue, StorageMap, decl_module, decl_event, decl_storage, ensure
|
||||
};
|
||||
use super::{Trait as CouncilTrait, Module as Council, OnMembersChanged};
|
||||
use system::{self, ensure_signed};
|
||||
|
||||
/// Simple index type for proposal counting.
|
||||
pub type ProposalIndex = u32;
|
||||
/// A number of council members.
|
||||
///
|
||||
/// This also serves as a number of voting members, and since for motions, each council member may
|
||||
/// vote exactly once, therefore also the number of votes for any given motion.
|
||||
pub type MemberCount = u32;
|
||||
|
||||
pub trait Trait: CouncilTrait {
|
||||
/// The outer origin type.
|
||||
type Origin: From<RawOrigin<Self::AccountId>>;
|
||||
|
||||
/// The outer call dispatch type.
|
||||
type Proposal: Parameter + Dispatchable<Origin=<Self as Trait>::Origin>;
|
||||
|
||||
/// The outer event type.
|
||||
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
|
||||
}
|
||||
|
||||
/// Origin for the council module.
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
pub enum RawOrigin<AccountId> {
|
||||
/// It has been condoned by a given number of council members from a given total.
|
||||
Members(MemberCount, MemberCount),
|
||||
/// It has been condoned by a single council member.
|
||||
Member(AccountId),
|
||||
}
|
||||
|
||||
/// Origin for the council module.
|
||||
pub type Origin<T> = RawOrigin<<T as system::Trait>::AccountId>;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
/// Info for keeping track of a motion being voted on.
|
||||
pub struct Votes<AccountId> {
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
decl_storage! {
|
||||
trait Store for Module<T: Trait> as CouncilMotions {
|
||||
/// The hashes of the active proposals.
|
||||
pub Proposals get(proposals): Vec<T::Hash>;
|
||||
/// Actual proposal for a given hash, if it's current.
|
||||
pub ProposalOf get(proposal_of): map T::Hash => Option<<T as Trait>::Proposal>;
|
||||
/// Votes on a given proposal, if it is ongoing.
|
||||
pub Voting get(voting): map T::Hash => Option<Votes<T::AccountId>>;
|
||||
/// Proposals so far.
|
||||
pub ProposalCount get(proposal_count): u32;
|
||||
}
|
||||
}
|
||||
|
||||
decl_event!(
|
||||
pub enum Event<T> where <T as system::Trait>::Hash, <T as system::Trait>::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 councillor did some action; `bool` is true if returned without error.
|
||||
MemberExecuted(Hash, bool),
|
||||
}
|
||||
);
|
||||
|
||||
decl_module! {
|
||||
pub struct Module<T: Trait> for enum Call where origin: <T as system::Trait>::Origin {
|
||||
fn deposit_event<T>() = default;
|
||||
|
||||
/// Dispatch a proposal from a councilor using the `Member` origin.
|
||||
///
|
||||
/// Origin must be a council member.
|
||||
fn execute(origin, proposal: Box<<T as Trait>::Proposal>) {
|
||||
let who = ensure_signed(origin)?;
|
||||
ensure!(Self::is_councillor(&who), "proposer not on council");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// # <weight>
|
||||
/// - Bounded storage reads and writes.
|
||||
/// - Argument `threshold` has bearing on weight.
|
||||
/// # </weight>
|
||||
fn propose(origin, #[compact] threshold: MemberCount, proposal: Box<<T as Trait>::Proposal>) {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(Self::is_councillor(&who), "proposer not on council");
|
||||
|
||||
let proposal_hash = T::Hashing::hash_of(&proposal);
|
||||
|
||||
ensure!(!<ProposalOf<T>>::exists(proposal_hash), "duplicate proposals not allowed");
|
||||
|
||||
if threshold < 2 {
|
||||
let seats = <Council<T>>::active_council().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();
|
||||
ProposalCount::mutate(|i| *i += 1);
|
||||
<Proposals<T>>::mutate(|proposals| proposals.push(proposal_hash));
|
||||
<ProposalOf<T>>::insert(proposal_hash, *proposal);
|
||||
let votes = Votes { index, threshold, ayes: vec![who.clone()], nays: vec![] };
|
||||
<Voting<T>>::insert(proposal_hash, votes);
|
||||
|
||||
Self::deposit_event(RawEvent::Proposed(who, index, proposal_hash, threshold));
|
||||
}
|
||||
}
|
||||
|
||||
/// # <weight>
|
||||
/// - Bounded storage read and writes.
|
||||
/// - Will be slightly heavier if the proposal is approved / disapproved after the vote.
|
||||
/// # </weight>
|
||||
fn vote(origin, proposal: T::Hash, #[compact] index: ProposalIndex, approve: bool) {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(Self::is_councillor(&who), "voter not on council");
|
||||
|
||||
let mut voting = Self::voting(&proposal).ok_or("proposal must exist")?;
|
||||
ensure!(voting.index == index, "mismatched index");
|
||||
|
||||
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 {
|
||||
return Err("duplicate vote ignored")
|
||||
}
|
||||
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("duplicate vote ignored")
|
||||
}
|
||||
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 = <Council<T>>::active_council().len() as MemberCount;
|
||||
let approved = yes_votes >= voting.threshold;
|
||||
let disapproved = seats.saturating_sub(no_votes) < voting.threshold;
|
||||
if approved || disapproved {
|
||||
if approved {
|
||||
Self::deposit_event(RawEvent::Approved(proposal));
|
||||
|
||||
// execute motion, assuming it exists.
|
||||
if let Some(p) = <ProposalOf<T>>::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
|
||||
Self::deposit_event(RawEvent::Disapproved(proposal));
|
||||
}
|
||||
|
||||
// remove vote
|
||||
<Voting<T>>::remove(&proposal);
|
||||
<Proposals<T>>::mutate(|proposals| proposals.retain(|h| h != &proposal));
|
||||
} else {
|
||||
// update voting
|
||||
<Voting<T>>::insert(&proposal, voting);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
pub fn is_councillor(who: &T::AccountId) -> bool {
|
||||
<Council<T>>::active_council().iter()
|
||||
.any(|&(ref a, _)| a == who)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> OnMembersChanged<T::AccountId> for Module<T> {
|
||||
fn on_members_changed(_new: &[T::AccountId], old: &[T::AccountId]) {
|
||||
// remove accounts from all current voting in motions.
|
||||
let mut old = old.to_vec();
|
||||
old.sort_unstable();
|
||||
for h in Self::proposals().into_iter() {
|
||||
<Voting<T>>::mutate(h, |v|
|
||||
if let Some(mut votes) = v.take() {
|
||||
votes.ayes = votes.ayes.into_iter()
|
||||
.filter(|i| old.binary_search(i).is_err())
|
||||
.collect();
|
||||
votes.nays = votes.nays.into_iter()
|
||||
.filter(|i| old.binary_search(i).is_err())
|
||||
.collect();
|
||||
*v = Some(votes);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that the origin `o` represents at least `n` council members. Returns `Ok` or an `Err`
|
||||
/// otherwise.
|
||||
pub fn ensure_council_members<OuterOrigin, AccountId>(o: OuterOrigin, n: MemberCount)
|
||||
-> result::Result<MemberCount, &'static str>
|
||||
where OuterOrigin: Into<result::Result<RawOrigin<AccountId>, OuterOrigin>>
|
||||
{
|
||||
match o.into() {
|
||||
Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n),
|
||||
_ => Err("bad origin: expected to be a threshold number of council members"),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureMember<AccountId>(::rstd::marker::PhantomData<AccountId>);
|
||||
impl<
|
||||
O: Into<Result<RawOrigin<AccountId>, O>> + From<RawOrigin<AccountId>>,
|
||||
AccountId
|
||||
> EnsureOrigin<O> for EnsureMember<AccountId> {
|
||||
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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureMembers<N: U32, AccountId>(::rstd::marker::PhantomData<(N, AccountId)>);
|
||||
impl<
|
||||
O: Into<Result<RawOrigin<AccountId>, O>> + From<RawOrigin<AccountId>>,
|
||||
N: U32,
|
||||
AccountId,
|
||||
> EnsureOrigin<O> for EnsureMembers<N, AccountId> {
|
||||
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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureProportionMoreThan<N: U32, D: U32, AccountId>(
|
||||
::rstd::marker::PhantomData<(N, D, AccountId)>
|
||||
);
|
||||
impl<
|
||||
O: Into<Result<RawOrigin<AccountId>, O>> + From<RawOrigin<AccountId>>,
|
||||
N: U32,
|
||||
D: U32,
|
||||
AccountId,
|
||||
> EnsureOrigin<O> for EnsureProportionMoreThan<N, D, AccountId> {
|
||||
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)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnsureProportionAtLeast<N: U32, D: U32, AccountId>(
|
||||
::rstd::marker::PhantomData<(N, D, AccountId)>
|
||||
);
|
||||
impl<
|
||||
O: Into<Result<RawOrigin<AccountId>, O>> + From<RawOrigin<AccountId>>,
|
||||
N: U32,
|
||||
D: U32,
|
||||
AccountId,
|
||||
> EnsureOrigin<O> for EnsureProportionAtLeast<N, D, AccountId> {
|
||||
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(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::RawEvent;
|
||||
use crate::tests::*;
|
||||
use crate::tests::{Call, Origin, Event as OuterEvent};
|
||||
use primitives::traits::BlakeTwo256;
|
||||
use srml_support::{Hashable, assert_ok, assert_noop};
|
||||
use system::{EventRecord, Phase};
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
fn motions_basic_environment_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
assert_eq!(Balances::free_balance(&42), 0);
|
||||
assert_eq!(CouncilMotions::proposals(), Vec::<H256>::new());
|
||||
});
|
||||
}
|
||||
|
||||
fn set_balance_proposal(value: u64) -> Call {
|
||||
Call::Balances(balances::Call::set_balance(42, value.into(), 0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_of_old_voters_votes_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash = BlakeTwo256::hash_of(&proposal);
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_ok!(CouncilMotions::vote(Origin::signed(2), hash.clone(), 0, true));
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![] })
|
||||
);
|
||||
CouncilMotions::on_members_changed(&[], &[1]);
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![] })
|
||||
);
|
||||
|
||||
let proposal = set_balance_proposal(69);
|
||||
let hash = BlakeTwo256::hash_of(&proposal);
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(2), 2, Box::new(proposal.clone())));
|
||||
assert_ok!(CouncilMotions::vote(Origin::signed(3), hash.clone(), 1, false));
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3] })
|
||||
);
|
||||
CouncilMotions::on_members_changed(&[], &[3]);
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![] })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn propose_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash = proposal.blake2_256().into();
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_eq!(CouncilMotions::proposals(), vec![hash]);
|
||||
assert_eq!(CouncilMotions::proposal_of(&hash), Some(proposal));
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 3, ayes: vec![1], nays: vec![] })
|
||||
);
|
||||
|
||||
assert_eq!(System::events(), vec![
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
3,
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_ignoring_non_council_proposals_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
assert_noop!(
|
||||
CouncilMotions::propose(Origin::signed(42), 3, Box::new(proposal.clone())),
|
||||
"proposer not on council"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_ignoring_non_council_votes_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash: H256 = proposal.blake2_256().into();
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_noop!(CouncilMotions::vote(Origin::signed(42), hash.clone(), 0, true), "voter not on council");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_ignoring_bad_index_council_vote_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(3);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash: H256 = proposal.blake2_256().into();
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_noop!(CouncilMotions::vote(Origin::signed(2), hash.clone(), 1, true), "mismatched index");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_revoting_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash: H256 = proposal.blake2_256().into();
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 2, Box::new(proposal.clone())));
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![] })
|
||||
);
|
||||
assert_noop!(CouncilMotions::vote(Origin::signed(1), hash.clone(), 0, true), "duplicate vote ignored");
|
||||
assert_ok!(CouncilMotions::vote(Origin::signed(1), hash.clone(), 0, false));
|
||||
assert_eq!(
|
||||
CouncilMotions::voting(&hash),
|
||||
Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1] })
|
||||
);
|
||||
assert_noop!(CouncilMotions::vote(Origin::signed(1), hash.clone(), 0, false), "duplicate vote ignored");
|
||||
|
||||
assert_eq!(System::events(), vec![
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
2,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Voted(
|
||||
1,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
false,
|
||||
0,
|
||||
1,
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_disapproval_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash: H256 = proposal.blake2_256().into();
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 3, Box::new(proposal.clone())));
|
||||
assert_ok!(CouncilMotions::vote(Origin::signed(2), hash.clone(), 0, false));
|
||||
|
||||
assert_eq!(System::events(), vec![
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(
|
||||
RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
3,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Voted(
|
||||
2,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
false,
|
||||
1,
|
||||
1,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Disapproved(
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motions_approval_works() {
|
||||
with_externalities(&mut ExtBuilder::default().with_council(true).build(), || {
|
||||
System::set_block_number(1);
|
||||
let proposal = set_balance_proposal(42);
|
||||
let hash: H256 = proposal.blake2_256().into();
|
||||
assert_ok!(CouncilMotions::propose(Origin::signed(1), 2, Box::new(proposal.clone())));
|
||||
assert_ok!(CouncilMotions::vote(Origin::signed(2), hash.clone(), 0, true));
|
||||
|
||||
assert_eq!(System::events(), vec![
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Proposed(
|
||||
1,
|
||||
0,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
2,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Voted(
|
||||
2,
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
true,
|
||||
2,
|
||||
0,
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Approved(
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
)),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::ApplyExtrinsic(0),
|
||||
event: OuterEvent::motions(RawEvent::Executed(
|
||||
hex!["cd0b662a49f004093b80600415cf4126399af0d27ed6c185abeb1469c17eb5bf"].into(),
|
||||
false,
|
||||
)),
|
||||
topics: vec![],
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,11 @@ pub trait Trait: system::Trait + Sized {
|
||||
/// a majority-carries referendum.
|
||||
type ExternalMajorityOrigin: EnsureOrigin<Self::Origin>;
|
||||
|
||||
/// Origin from which the next referendum proposed by the external majority may be immediately
|
||||
/// tabled to vote asynchronously in a similar manner to the emergency origin. It remains a
|
||||
/// majority-carries vote.
|
||||
type ExternalPushOrigin: EnsureOrigin<Self::Origin>;
|
||||
|
||||
/// Origin from which emergency referenda may be scheduled.
|
||||
type EmergencyOrigin: EnsureOrigin<Self::Origin>;
|
||||
|
||||
@@ -443,12 +448,7 @@ decl_module! {
|
||||
// resubmission in the case of a mistakenly low `vote_period`; better to just let the
|
||||
// referendum take place with the lowest valid value.
|
||||
let period = voting_period.max(T::EmergencyVotingPeriod::get());
|
||||
Self::inject_referendum(
|
||||
now + period,
|
||||
*proposal,
|
||||
threshold,
|
||||
delay,
|
||||
).map(|_| ())?;
|
||||
Self::inject_referendum(now + period, *proposal, threshold, delay).map(|_| ())?;
|
||||
}
|
||||
|
||||
/// Schedule an emergency cancellation of a referendum. Cannot happen twice to the same
|
||||
@@ -488,6 +488,31 @@ decl_module! {
|
||||
<NextExternal<T>>::put((*proposal, VoteThreshold::SimpleMajority));
|
||||
}
|
||||
|
||||
/// Schedule the currently externally-proposed majority-carries referendum to be tabled
|
||||
/// immediately. If there is no externally-proposed referendum currently, or if there is one
|
||||
/// but it is not a majority-carries referendum then it fails.
|
||||
///
|
||||
/// - `proposal_hash`: The hash of the current external proposal.
|
||||
/// - `voting_period`: The period that is allowed for voting on this proposal.
|
||||
/// - `delay`: The number of block after voting has ended in approval and this should be
|
||||
/// enacted. Increased to `EmergencyVotingPeriod` if too low.
|
||||
fn external_push(origin,
|
||||
proposal_hash: T::Hash,
|
||||
voting_period: T::BlockNumber,
|
||||
delay: T::BlockNumber
|
||||
) {
|
||||
T::ExternalPushOrigin::ensure_origin(origin)?;
|
||||
let (proposal, threshold) = <NextExternal<T>>::get().ok_or("no proposal made")?;
|
||||
ensure!(threshold == VoteThreshold::SimpleMajority, "next external proposal not simple majority");
|
||||
ensure!(proposal_hash == T::Hashing::hash_of(&proposal), "invalid hash");
|
||||
|
||||
<NextExternal<T>>::kill();
|
||||
let now = <system::Module<T>>::block_number();
|
||||
// We don't consider it an error if `vote_period` is too low, like `emergency_propose`.
|
||||
let period = voting_period.max(T::EmergencyVotingPeriod::get());
|
||||
Self::inject_referendum(now + period, proposal, threshold, delay).map(|_| ())?;
|
||||
}
|
||||
|
||||
/// Veto and blacklist the external proposal hash.
|
||||
fn veto_external(origin, proposal_hash: T::Hash) {
|
||||
let who = T::VetoOrigin::ensure_origin(origin)?;
|
||||
@@ -734,7 +759,7 @@ impl<T: Trait> Module<T> {
|
||||
<Proxy<T>>::insert(proxy, stash)
|
||||
}
|
||||
|
||||
/// Start a referendum. Can be called directly by the council.
|
||||
/// Start a referendum.
|
||||
pub fn internal_start_referendum(
|
||||
proposal: T::Proposal,
|
||||
threshold: VoteThreshold,
|
||||
@@ -748,7 +773,7 @@ impl<T: Trait> Module<T> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove a referendum. Can be called directly by the council.
|
||||
/// Remove a referendum.
|
||||
pub fn internal_cancel_referendum(ref_index: ReferendumIndex) {
|
||||
Self::deposit_event(RawEvent::Cancelled(ref_index));
|
||||
<Module<T>>::clear_referendum(ref_index);
|
||||
@@ -1038,6 +1063,7 @@ mod tests {
|
||||
type EmergencyOrigin = EnsureSignedBy<One, u64>;
|
||||
type ExternalOrigin = EnsureSignedBy<Two, u64>;
|
||||
type ExternalMajorityOrigin = EnsureSignedBy<Three, u64>;
|
||||
type ExternalPushOrigin = EnsureSignedBy<Five, u64>;
|
||||
type CancellationOrigin = EnsureSignedBy<Four, u64>;
|
||||
type VetoOrigin = EnsureSignedBy<OneToFive, u64>;
|
||||
type CooloffPeriod = CooloffPeriod;
|
||||
@@ -1403,6 +1429,46 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_push_referendum_works() {
|
||||
with_externalities(&mut new_test_ext(), || {
|
||||
System::set_block_number(0);
|
||||
let h = BlakeTwo256::hash_of(&set_balance_proposal(2));
|
||||
assert_noop!(Democracy::external_push(Origin::signed(5), h, 3, 2), "no proposal made");
|
||||
assert_ok!(Democracy::external_propose_majority(
|
||||
Origin::signed(3),
|
||||
Box::new(set_balance_proposal(2))
|
||||
));
|
||||
assert_noop!(Democracy::external_push(Origin::signed(1), h, 3, 2), "Invalid origin");
|
||||
assert_ok!(Democracy::external_push(Origin::signed(5), h, 0, 0));
|
||||
assert_eq!(
|
||||
Democracy::referendum_info(0),
|
||||
Some(ReferendumInfo {
|
||||
end: 1,
|
||||
proposal: set_balance_proposal(2),
|
||||
threshold: VoteThreshold::SimpleMajority,
|
||||
delay: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_push_referendum_fails_when_no_simple_majority() {
|
||||
with_externalities(&mut new_test_ext(), || {
|
||||
System::set_block_number(0);
|
||||
let h = BlakeTwo256::hash_of(&set_balance_proposal(2));
|
||||
assert_ok!(Democracy::external_propose(
|
||||
Origin::signed(2),
|
||||
Box::new(set_balance_proposal(2))
|
||||
));
|
||||
assert_noop!(
|
||||
Democracy::external_push(Origin::signed(5), h, 3, 2),
|
||||
"next external proposal not simple majority"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_for_should_work() {
|
||||
with_externalities(&mut new_test_ext(), || {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "srml-elections"
|
||||
version = "2.0.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", optional = true }
|
||||
safe-mix = { version = "1.0", default-features = false}
|
||||
parity-codec = { version = "4.1.1", default-features = false, features = ["derive"] }
|
||||
substrate-primitives = { path = "../../core/primitives", default-features = false }
|
||||
rstd = { package = "sr-std", path = "../../core/sr-std", default-features = false }
|
||||
runtime_io = { package = "sr-io", path = "../../core/sr-io", default-features = false }
|
||||
primitives = { package = "sr-primitives", path = "../../core/sr-primitives", default-features = false }
|
||||
srml-support = { path = "../support", default-features = false }
|
||||
system = { package = "srml-system", path = "../system", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.2.0"
|
||||
balances = { package = "srml-balances", path = "../balances" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"safe-mix/std",
|
||||
"parity-codec/std",
|
||||
"substrate-primitives/std",
|
||||
"rstd/std",
|
||||
"serde",
|
||||
"runtime_io/std",
|
||||
"srml-support/std",
|
||||
"primitives/std",
|
||||
"system/std",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -648,7 +648,7 @@ decl_module! {
|
||||
fn deposit_event<T>() = default;
|
||||
|
||||
/// Take the origin account as a stash and lock up `value` of its balance. `controller` will
|
||||
/// be the account that controls it.
|
||||
/// be the account that controls it.
|
||||
///
|
||||
/// The dispatch origin for this call must be _Signed_ by the stash account.
|
||||
///
|
||||
@@ -693,7 +693,7 @@ decl_module! {
|
||||
}
|
||||
|
||||
/// Add some extra amount that have appeared in the stash `free_balance` into the balance up
|
||||
/// for staking.
|
||||
/// for staking.
|
||||
///
|
||||
/// Use this if there are additional funds in your stash account that you wish to bond.
|
||||
///
|
||||
|
||||
@@ -52,10 +52,9 @@ thread_local! {
|
||||
pub struct TestSessionHandler;
|
||||
impl session::SessionHandler<AccountId> for TestSessionHandler {
|
||||
fn on_new_session<Ks: OpaqueKeys>(_changed: bool, validators: &[(AccountId, Ks)]) {
|
||||
SESSION.with(|x| {
|
||||
let v = validators.iter().map(|(ref a, _)| a).cloned().collect::<Vec<_>>();
|
||||
SESSION.with(|x|
|
||||
*x.borrow_mut() = (validators.iter().map(|x| x.0.clone()).collect(), HashSet::new())
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
fn on_disabled(validator_index: usize) {
|
||||
|
||||
@@ -870,10 +870,12 @@ macro_rules! decl_module {
|
||||
) { $( $impl:tt )* }
|
||||
) => {
|
||||
$(#[doc = $doc_attr])*
|
||||
#[allow(unreachable_code)]
|
||||
$vis fn $name(
|
||||
$origin: $origin_ty $(, $param: $param_ty )*
|
||||
) -> $crate::dispatch::Result {
|
||||
{ $( $impl )* }
|
||||
// May be unreachable.
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
@@ -1285,9 +1287,11 @@ macro_rules! impl_outer_dispatch {
|
||||
}
|
||||
$(
|
||||
impl $crate::dispatch::IsSubType<$camelcase, $runtime> for $call_type {
|
||||
#[allow(unreachable_patterns)]
|
||||
fn is_aux_sub_type(&self) -> Option<&$crate::dispatch::CallableCallFor<$camelcase, $runtime>> {
|
||||
match *self {
|
||||
$call_type::$camelcase(ref r) => Some(r),
|
||||
// May be unreachable
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
//! Abstract storage to use on HashedStorage trait
|
||||
|
||||
use crate::codec;
|
||||
use crate::codec::{self, Encode};
|
||||
use crate::rstd::prelude::{Vec, Box};
|
||||
#[cfg(feature = "std")]
|
||||
use crate::storage::unhashed::generator::UnhashedStorage;
|
||||
@@ -184,6 +184,13 @@ pub trait StorageValue<T: codec::Codec> {
|
||||
storage.put(Self::key(), val)
|
||||
}
|
||||
|
||||
/// Store a value under this key into the provided storage instance; this can take any reference
|
||||
/// type that derefs to `T` (and has `Encode` implemented).
|
||||
/// Store a value under this key into the provided storage instance.
|
||||
fn put_ref<Arg: ?Sized + Encode, S: HashedStorage<Twox128>>(val: &Arg, storage: &mut S) where T: AsRef<Arg> {
|
||||
val.using_encoded(|b| storage.put_raw(Self::key(), b))
|
||||
}
|
||||
|
||||
/// Mutate this value
|
||||
fn mutate<R, F: FnOnce(&mut Self::Query) -> R, S: HashedStorage<Twox128>>(f: F, storage: &mut S) -> R;
|
||||
|
||||
@@ -236,6 +243,17 @@ pub trait StorageMap<K: codec::Codec, V: codec::Codec> {
|
||||
storage.put(&Self::key_for(key)[..], val);
|
||||
}
|
||||
|
||||
/// Store a value under this key into the provided storage instance; this can take any reference
|
||||
/// type that derefs to `T` (and has `Encode` implemented).
|
||||
/// Store a value under this key into the provided storage instance.
|
||||
fn insert_ref<Arg: ?Sized + Encode, S: HashedStorage<Twox128>>(
|
||||
key: &K,
|
||||
val: &Arg,
|
||||
storage: &mut S
|
||||
) where V: AsRef<Arg> {
|
||||
val.using_encoded(|b| storage.put_raw(&Self::key_for(key)[..], b))
|
||||
}
|
||||
|
||||
/// Remove the value under a key.
|
||||
fn remove<S: HashedStorage<Self::Hasher>>(key: &K, storage: &mut S) {
|
||||
storage.kill(&Self::key_for(key)[..]);
|
||||
|
||||
@@ -149,6 +149,10 @@ pub trait StorageValue<T: Codec> {
|
||||
/// Store a value under this key into the provided storage instance.
|
||||
fn put<Arg: Borrow<T>>(val: Arg);
|
||||
|
||||
/// Store a value under this key into the provided storage instance; this can take any reference
|
||||
/// type that derefs to `T` (and has `Encode` implemented).
|
||||
fn put_ref<Arg: ?Sized + Encode>(val: &Arg) where T: AsRef<Arg>;
|
||||
|
||||
/// Mutate the value
|
||||
fn mutate<R, F: FnOnce(&mut Self::Query) -> R>(f: F) -> R;
|
||||
|
||||
@@ -180,6 +184,9 @@ impl<T: Codec, U> StorageValue<T> for U where U: hashed::generator::StorageValue
|
||||
fn put<Arg: Borrow<T>>(val: Arg) {
|
||||
U::put(val.borrow(), &mut RuntimeStorage)
|
||||
}
|
||||
fn put_ref<Arg: ?Sized + Encode>(val: &Arg) where T: AsRef<Arg> {
|
||||
U::put_ref(val, &mut RuntimeStorage)
|
||||
}
|
||||
fn mutate<R, F: FnOnce(&mut Self::Query) -> R>(f: F) -> R {
|
||||
U::mutate(f, &mut RuntimeStorage)
|
||||
}
|
||||
@@ -216,6 +223,10 @@ pub trait StorageMap<K: Codec, V: Codec> {
|
||||
/// Store a value to be associated with the given key from the map.
|
||||
fn insert<KeyArg: Borrow<K>, ValArg: Borrow<V>>(key: KeyArg, val: ValArg);
|
||||
|
||||
/// Store a value under this key into the provided storage instance; this can take any reference
|
||||
/// type that derefs to `T` (and has `Encode` implemented).
|
||||
fn insert_ref<KeyArg: Borrow<K>, ValArg: ?Sized + Encode>(key: KeyArg, val: &ValArg) where V: AsRef<ValArg>;
|
||||
|
||||
/// Remove the value under a key.
|
||||
fn remove<KeyArg: Borrow<K>>(key: KeyArg);
|
||||
|
||||
@@ -249,6 +260,10 @@ impl<K: Codec, V: Codec, U> StorageMap<K, V> for U where U: hashed::generator::S
|
||||
U::insert(key.borrow(), val.borrow(), &mut RuntimeStorage)
|
||||
}
|
||||
|
||||
fn insert_ref<KeyArg: Borrow<K>, ValArg: ?Sized + Encode>(key: KeyArg, val: &ValArg) where V: AsRef<ValArg> {
|
||||
U::insert_ref(key.borrow(), val, &mut RuntimeStorage)
|
||||
}
|
||||
|
||||
fn remove<KeyArg: Borrow<K>>(key: KeyArg) {
|
||||
U::remove(key.borrow(), &mut RuntimeStorage)
|
||||
}
|
||||
|
||||
@@ -630,3 +630,13 @@ bitmask! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for type that can handle incremental changes to a set of account IDs.
|
||||
pub trait ChangeMembers<AccountId> {
|
||||
/// A number of members `_incoming` just joined the set and replaced some `_outgoing` ones. The
|
||||
/// new set is thus given by `_new`.
|
||||
fn change_members(_incoming: &[AccountId], _outgoing: &[AccountId], _new: &[AccountId]);
|
||||
}
|
||||
|
||||
impl<T> ChangeMembers<T> for () {
|
||||
fn change_members(_incoming: &[T], _outgoing: &[T], _new_set: &[T]) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user