mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-16 18:55:42 +00:00
c5c5b35da3
* Improve and simplify `decl_event!` - RawEvent is not required anymore to be written in the defintion - Events with and without parameters are now supported everywhere * Make `decl_event!` more flexible with the automatic naming of generic parameters The macro will now automatically derive the name of a generic parameter from the trait type name, if no explicit name is given. `where Balance = <T as Trait>::Balance` can be simplified to `where <T as Trait>::Balance`. * Adapts to latest refactoring changes
371 lines
13 KiB
Rust
371 lines
13 KiB
Rust
// Copyright 2017 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::*;
|
|
use rstd::result;
|
|
use substrate_primitives::u32_trait::Value as U32;
|
|
use primitives::traits::{Hash, EnsureOrigin, MaybeSerializeDebug, OnFinalise};
|
|
use srml_support::dispatch::{Result, Dispatchable, Parameter};
|
|
use srml_support::{StorageValue, StorageMap};
|
|
use super::{Trait as CouncilTrait, Module as Council};
|
|
use system::{self, ensure_signed};
|
|
|
|
/// Simple index type for proposal counting.
|
|
pub type ProposalIndex = u32;
|
|
|
|
pub trait Trait: CouncilTrait + MaybeSerializeDebug {
|
|
/// The outer origin type.
|
|
type Origin: From<Origin>;
|
|
|
|
/// The outer call dispatch type.
|
|
type Proposal: Parameter + Dispatchable<Origin=<Self as Trait>::Origin> + MaybeSerializeDebug;
|
|
|
|
/// 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 Origin {
|
|
/// It has been condoned by a given number of council members.
|
|
Members(u32),
|
|
}
|
|
|
|
/// Event for this module.
|
|
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 u32).
|
|
Proposed(AccountId, ProposalIndex, Hash, u32),
|
|
/// A motion (given hash) has been voted on by given account, leaving
|
|
/// a tally (yes votes and no votes given as u32s respectively).
|
|
Voted(AccountId, Hash, bool, u32, u32),
|
|
/// 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),
|
|
}
|
|
);
|
|
|
|
decl_module! {
|
|
#[cfg_attr(feature = "std", serde(bound(deserialize = "<T as Trait>::Proposal: ::serde::de::DeserializeOwned")))]
|
|
pub struct Module<T: Trait> for enum Call where origin: <T as system::Trait>::Origin {
|
|
fn propose(origin, threshold: u32, proposal: Box<<T as Trait>::Proposal>) -> Result;
|
|
fn vote(origin, proposal: T::Hash, index: ProposalIndex, approve: bool) -> Result;
|
|
}
|
|
}
|
|
|
|
decl_storage! {
|
|
trait Store for Module<T: Trait> as CouncilMotions {
|
|
/// The (hashes of) the active proposals.
|
|
pub Proposals get(proposals): default Vec<T::Hash>;
|
|
/// Actual proposal for a given hash, if it's current.
|
|
pub ProposalOf get(proposal_of): map [ T::Hash => <T as Trait>::Proposal ];
|
|
/// Votes for a given proposal: (required_yes_votes, yes_voters, no_voters).
|
|
pub Voting get(voting): map [ T::Hash => (ProposalIndex, u32, Vec<T::AccountId>, Vec<T::AccountId>) ];
|
|
/// Proposals so far.
|
|
pub ProposalCount get(proposal_count): default u32;
|
|
}
|
|
}
|
|
|
|
impl<T: Trait> Module<T> {
|
|
|
|
/// Deposit one of this module's events.
|
|
fn deposit_event(event: Event<T>) {
|
|
<system::Module<T>>::deposit_event(<T as Trait>::Event::from(event).into());
|
|
}
|
|
|
|
pub fn is_councillor(who: &T::AccountId) -> bool {
|
|
<Council<T>>::active_council().iter()
|
|
.any(|&(ref a, _)| a == who)
|
|
}
|
|
|
|
// Dispatch
|
|
fn propose(origin: <T as system::Trait>::Origin, threshold: u32, proposal: Box<<T as Trait>::Proposal>) -> Result {
|
|
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 ok = proposal.dispatch(Origin::Members(1).into()).is_ok();
|
|
Self::deposit_event(RawEvent::Executed(proposal_hash, ok));
|
|
} else {
|
|
let index = Self::proposal_count();
|
|
<ProposalCount<T>>::mutate(|i| *i += 1);
|
|
<Proposals<T>>::mutate(|proposals| proposals.push(proposal_hash));
|
|
<ProposalOf<T>>::insert(proposal_hash, *proposal);
|
|
<Voting<T>>::insert(proposal_hash, (index, threshold, vec![who.clone()], vec![]));
|
|
|
|
Self::deposit_event(RawEvent::Proposed(who, index, proposal_hash, threshold));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn vote(origin: <T as system::Trait>::Origin, proposal: T::Hash, index: ProposalIndex, approve: bool) -> Result {
|
|
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.0 == index, "mismatched index");
|
|
|
|
let position_yes = voting.2.iter().position(|a| a == &who);
|
|
let position_no = voting.3.iter().position(|a| a == &who);
|
|
|
|
if approve {
|
|
if position_yes.is_none() {
|
|
voting.2.push(who.clone());
|
|
} else {
|
|
return Err("duplicate vote ignored")
|
|
}
|
|
if let Some(pos) = position_no {
|
|
voting.3.swap_remove(pos);
|
|
}
|
|
} else {
|
|
if position_no.is_none() {
|
|
voting.3.push(who.clone());
|
|
} else {
|
|
return Err("duplicate vote ignored")
|
|
}
|
|
if let Some(pos) = position_yes {
|
|
voting.2.swap_remove(pos);
|
|
}
|
|
}
|
|
|
|
let yes_votes = voting.2.len() as u32;
|
|
let no_votes = voting.3.len() as u32;
|
|
Self::deposit_event(RawEvent::Voted(who, proposal, approve, yes_votes, no_votes));
|
|
|
|
let threshold = voting.1;
|
|
let potential_votes = <Council<T>>::active_council().len() as u32;
|
|
let approved = yes_votes >= threshold;
|
|
let disapproved = potential_votes.saturating_sub(no_votes) < 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 ok = p.dispatch(Origin::Members(threshold).into()).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);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<T: Trait> OnFinalise<T::BlockNumber> for Module<T> {
|
|
fn on_finalise(_n: T::BlockNumber) {
|
|
}
|
|
}
|
|
|
|
/// Ensure that the origin `o` represents at least `n` council members. Returns
|
|
/// `Ok` or an `Err` otherwise.
|
|
pub fn ensure_council_members<OuterOrigin>(o: OuterOrigin, n: u32) -> result::Result<u32, &'static str>
|
|
where OuterOrigin: Into<Option<Origin>>
|
|
{
|
|
match o.into() {
|
|
Some(Origin::Members(x)) if x >= n => Ok(n),
|
|
_ => Err("bad origin: expected to be a threshold number of council members"),
|
|
}
|
|
}
|
|
|
|
pub struct EnsureMembers<N: U32>(::rstd::marker::PhantomData<N>);
|
|
impl<O, N: U32> EnsureOrigin<O> for EnsureMembers<N>
|
|
where O: Into<Option<Origin>>
|
|
{
|
|
type Success = u32;
|
|
fn ensure_origin(o: O) -> result::Result<Self::Success, &'static str> {
|
|
ensure_council_members(o, N::VALUE)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use super::RawEvent;
|
|
use ::tests::*;
|
|
use ::tests::{Call, Origin, Event as OuterEvent};
|
|
use srml_support::Hashable;
|
|
use system::{EventRecord, Phase};
|
|
|
|
#[test]
|
|
fn motions_basic_environment_works() {
|
|
with_externalities(&mut new_test_ext(true), || {
|
|
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(balances::address::Address::Id(42), value, 0))
|
|
}
|
|
|
|
#[test]
|
|
fn motions_propose_works() {
|
|
with_externalities(&mut new_test_ext(true), || {
|
|
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((0, 3, vec![1], Vec::<u64>::new())));
|
|
|
|
assert_eq!(System::events(), vec![
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Proposed(1, 0, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 3))
|
|
}
|
|
]);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn motions_ignoring_non_council_proposals_works() {
|
|
with_externalities(&mut new_test_ext(true), || {
|
|
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 new_test_ext(true), || {
|
|
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 new_test_ext(true), || {
|
|
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 new_test_ext(true), || {
|
|
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((0, 2, vec![1], Vec::<u64>::new())));
|
|
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((0, 2, Vec::<u64>::new(), 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!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 2))
|
|
},
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Voted(1, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false, 0, 1))
|
|
}
|
|
]);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn motions_disapproval_works() {
|
|
with_externalities(&mut new_test_ext(true), || {
|
|
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!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 3))
|
|
},
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Voted(2, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false, 1, 1))
|
|
},
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Disapproved(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into()))
|
|
}
|
|
]);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn motions_approval_works() {
|
|
with_externalities(&mut new_test_ext(true), || {
|
|
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!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), 2))
|
|
},
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Voted(2, hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), true, 2, 0))
|
|
},
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Approved(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into()))
|
|
},
|
|
EventRecord {
|
|
phase: Phase::ApplyExtrinsic(0),
|
|
event: OuterEvent::motions(RawEvent::Executed(hex!["a900ca23832b1f42a5d4af5d0ece88da63fbb4049cc00bac3f741eabb5a79c45"].into(), false))
|
|
}
|
|
]);
|
|
});
|
|
}
|
|
}
|