Proxy voting (#2137)

* Proxy voting in democracy

* Proxy voting for council elections

* Bump and build

* Kill proxy when account dead

* Rebuild wasm

* Fix democraxy delegation locking

* Update srml/council/src/seats.rs

Co-Authored-By: gavofyork <github@gavwood.com>

* Update to use mutate
This commit is contained in:
Gav Wood
2019-03-29 16:07:04 +01:00
committed by GitHub
parent 5c9f306eb3
commit 086d55397b
5 changed files with 189 additions and 41 deletions
+1
View File
@@ -218,6 +218,7 @@ pub trait Derive: Sized {
fn derive<Iter: Iterator<Item=DeriveJunction>>(&self, _path: Iter) -> Option<Self> { None }
}
#[cfg(feature = "std")]
const PREFIX: &[u8] = b"SS58PRE";
#[cfg(feature = "std")]
+1 -1
View File
@@ -58,7 +58,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("node"),
impl_name: create_runtime_str!("substrate-node"),
authoring_version: 10,
spec_version: 50,
spec_version: 51,
impl_version: 51,
apis: RUNTIME_API_VERSIONS,
};
+67 -23
View File
@@ -103,31 +103,16 @@ decl_module! {
/// Set candidate approvals. Approval slots stay valid as long as candidates in those slots
/// are registered.
fn set_approvals(origin, votes: Vec<bool>, #[compact] index: VoteIndex) {
fn set_approvals(origin, votes: Vec<bool>, #[compact] index: VoteIndex) -> Result {
let who = ensure_signed(origin)?;
let candidates = Self::candidates();
Self::do_set_approvals(who, votes, index)
}
ensure!(!Self::presentation_active(), "no approval changes during presentation period");
ensure!(index == Self::vote_index(), "incorrect vote index");
ensure!(!candidates.len().is_zero(), "amount of candidates to receive approval votes should be non-zero");
// Prevent a vote from voters that provide a list of votes that exceeds the candidates length
// since otherwise an attacker may be able to submit a very long list of `votes` that far exceeds
// the amount of candidates and waste more computation than a reasonable voting bond would cover.
ensure!(candidates.len() >= votes.len(), "amount of candidate approval votes cannot exceed amount of candidates");
if !<LastActiveOf<T>>::exists(&who) {
// not yet a voter - deduct bond.
// NOTE: this must be the last potential bailer, since it changes state.
T::Currency::reserve(&who, Self::voting_bond())?;
<Voters<T>>::put({
let mut v = Self::voters();
v.push(who.clone());
v
});
}
<LastActiveOf<T>>::insert(&who, index);
<ApprovalsOf<T>>::insert(&who, votes);
/// Set candidate approvals from a proxy. Approval slots stay valid as long as candidates in those slots
/// are registered.
fn proxy_set_approvals(origin, votes: Vec<bool>, #[compact] index: VoteIndex) -> Result {
let who = <democracy::Module<T>>::proxy(ensure_signed(origin)?).ok_or("not a proxy")?;
Self::do_set_approvals(who, votes, index)
}
/// Remove a voter. For it not to be a bond-consuming no-op, all approved candidate indices
@@ -468,6 +453,31 @@ impl<T: Trait> Module<T> {
<LastActiveOf<T>>::remove(voter);
}
// Actually do the voting.
fn do_set_approvals(who: T::AccountId, votes: Vec<bool>, index: VoteIndex) -> Result {
let candidates = Self::candidates();
ensure!(!Self::presentation_active(), "no approval changes during presentation period");
ensure!(index == Self::vote_index(), "incorrect vote index");
ensure!(!candidates.is_empty(), "amount of candidates to receive approval votes should be non-zero");
// Prevent a vote from voters that provide a list of votes that exceeds the candidates length
// since otherwise an attacker may be able to submit a very long list of `votes` that far exceeds
// the amount of candidates and waste more computation than a reasonable voting bond would cover.
ensure!(candidates.len() >= votes.len(), "amount of candidate approval votes cannot exceed amount of candidates");
if !<LastActiveOf<T>>::exists(&who) {
// not yet a voter - deduct bond.
// NOTE: this must be the last potential bailer, since it changes state.
T::Currency::reserve(&who, Self::voting_bond())?;
<Voters<T>>::mutate(|mut v| v.push(who.clone()));
}
<LastActiveOf<T>>::insert(&who, index);
<ApprovalsOf<T>>::insert(&who, votes);
Ok(())
}
/// Close the voting, snapshot the staking and the number of seats that are actually up for grabs.
fn start_tally() {
let active_council = Self::active_council();
@@ -741,6 +751,40 @@ mod tests {
});
}
#[test]
fn proxy_voting_should_work() {
with_externalities(&mut new_test_ext(false), || {
System::set_block_number(1);
assert_ok!(Council::submit_candidacy(Origin::signed(5), 0));
Democracy::force_proxy(1, 11);
Democracy::force_proxy(2, 12);
Democracy::force_proxy(3, 13);
Democracy::force_proxy(4, 14);
assert_ok!(Council::proxy_set_approvals(Origin::signed(11), vec![true], 0));
assert_ok!(Council::proxy_set_approvals(Origin::signed(14), vec![true], 0));
assert_eq!(Council::approvals_of(1), vec![true]);
assert_eq!(Council::approvals_of(4), vec![true]);
assert_eq!(Council::voters(), vec![1, 4]);
assert_ok!(Council::submit_candidacy(Origin::signed(2), 1));
assert_ok!(Council::submit_candidacy(Origin::signed(3), 2));
assert_ok!(Council::proxy_set_approvals(Origin::signed(12), vec![false, true, true], 0));
assert_ok!(Council::proxy_set_approvals(Origin::signed(13), vec![false, true, true], 0));
assert_eq!(Council::approvals_of(1), vec![true]);
assert_eq!(Council::approvals_of(4), vec![true]);
assert_eq!(Council::approvals_of(2), vec![false, true, true]);
assert_eq!(Council::approvals_of(3), vec![false, true, true]);
assert_eq!(Council::voters(), vec![1, 4, 2, 3]);
});
}
#[test]
fn setting_any_approval_vote_count_without_any_candidate_count_should_not_work() {
with_externalities(&mut new_test_ext(false), || {
+120 -17
View File
@@ -24,7 +24,8 @@ use primitives::traits::{Zero, As, Bounded};
use parity_codec::{Encode, Decode};
use srml_support::{StorageValue, StorageMap, Parameter, Dispatchable, IsSubType, EnumerableStorageMap};
use srml_support::{decl_module, decl_storage, decl_event, ensure};
use srml_support::traits::{Currency, ReservableCurrency, LockableCurrency, WithdrawReason, LockIdentifier};
use srml_support::traits::{Currency, ReservableCurrency, LockableCurrency, WithdrawReason, LockIdentifier,
OnFreeBalanceZero};
use srml_support::dispatch::Result;
use system::ensure_signed;
@@ -119,16 +120,16 @@ decl_module! {
/// Vote in a referendum. If `vote.is_aye()`, the vote is to enact the proposal;
/// otherwise it is a vote to keep the status quo.
fn vote(origin, #[compact] ref_index: ReferendumIndex, vote: Vote) {
fn vote(origin, #[compact] ref_index: ReferendumIndex, vote: Vote) -> Result {
let who = ensure_signed(origin)?;
ensure!(vote.multiplier() <= Self::max_lock_periods(), "vote has too great a strength");
ensure!(Self::is_active_referendum(ref_index), "vote given for invalid referendum.");
ensure!(!T::Currency::total_balance(&who).is_zero(),
"transactor must have balance to signal approval.");
if !<VoteOf<T>>::exists(&(ref_index, who.clone())) {
<VotersFor<T>>::mutate(ref_index, |voters| voters.push(who.clone()));
}
<VoteOf<T>>::insert(&(ref_index, who), vote);
Self::do_vote(who, ref_index, vote)
}
/// Vote in a referendum on behalf of a stash. If `vote.is_aye()`, the vote is to enact the proposal;
/// otherwise it is a vote to keep the status quo.
fn proxy_vote(origin, #[compact] ref_index: ReferendumIndex, vote: Vote) -> Result {
let who = Self::proxy(ensure_signed(origin)?).ok_or("not a proxy")?;
Self::do_vote(who, ref_index, vote)
}
/// Start a referendum.
@@ -147,10 +148,9 @@ decl_module! {
}
/// Cancel a proposal queued for enactment.
pub fn cancel_queued(#[compact] when: T::BlockNumber, #[compact] which: u32) -> Result {
pub fn cancel_queued(#[compact] when: T::BlockNumber, #[compact] which: u32) {
let which = which as usize;
<DispatchQueue<T>>::mutate(when, |items| if items.len() > which { items[which] = None });
Ok(())
}
fn on_finalize(n: T::BlockNumber) {
@@ -159,20 +159,46 @@ decl_module! {
}
}
/// Specify a proxy. Called by the stash.
fn set_proxy(origin, proxy: T::AccountId) {
let who = ensure_signed(origin)?;
ensure!(!<Proxy<T>>::exists(&proxy), "already a proxy");
<Proxy<T>>::insert(proxy, who)
}
/// Clear the proxy. Called by the proxy.
fn resign_proxy(origin) {
let who = ensure_signed(origin)?;
<Proxy<T>>::remove(who);
}
/// Clear the proxy. Called by the stash.
fn remove_proxy(origin, proxy: T::AccountId) {
let who = ensure_signed(origin)?;
ensure!(&Self::proxy(&proxy).ok_or("not a proxy")? == &who, "wrong proxy");
<Proxy<T>>::remove(proxy);
}
/// Delegate vote.
pub fn delegate(origin, to: T::AccountId, lock_periods: LockPeriods) -> Result {
pub fn delegate(origin, to: T::AccountId, lock_periods: LockPeriods) {
let who = ensure_signed(origin)?;
<Delegations<T>>::insert(who.clone(), (to.clone(), lock_periods.clone()));
// Currency is locked indefinitely as long as it's delegated.
T::Currency::extend_lock(DEMOCRACY_ID, &who, Bounded::max_value(), T::BlockNumber::max_value(), WithdrawReason::Transfer.into());
Self::deposit_event(RawEvent::Delegated(who, to));
Ok(())
}
/// Undelegate vote.
fn undelegate(origin) -> Result {
fn undelegate(origin) {
let who = ensure_signed(origin)?;
<Delegations<T>>::remove(who.clone());
ensure!(<Delegations<T>>::exists(&who), "not delegated");
let d = <Delegations<T>>::take(&who);
// Indefinite lock is reduced to the maximum voting lock that could be possible.
let lock_period = Self::public_delay();
let now = <system::Module<T>>::block_number();
let locked_until = now + lock_period * T::BlockNumber::sa(d.1 as u64);
T::Currency::set_lock(DEMOCRACY_ID, &who, Bounded::max_value(), locked_until, WithdrawReason::Transfer.into());
Self::deposit_event(RawEvent::Undelegated(who));
Ok(())
}
}
}
@@ -236,6 +262,9 @@ decl_storage! {
/// `voters_for`, then you can also check for simple existence with `VoteOf::exists` first.
pub VoteOf get(vote_of): map (ReferendumIndex, T::AccountId) => Vote;
/// Who is able to vote for whom. Value is the fund-holding account, key is the vote-transaction-sending account.
pub Proxy get(proxy): map T::AccountId => Option<T::AccountId>;
/// Get the account (and lock periods) to which another account is delegating vote.
pub Delegations get(delegations): linked_map T::AccountId => (T::AccountId, LockPeriods);
}
@@ -340,6 +369,11 @@ impl<T: Trait> Module<T> {
// Exposed mutables.
#[cfg(feature = "std")]
pub fn force_proxy(stash: T::AccountId, proxy: T::AccountId) {
<Proxy<T>>::insert(proxy, stash)
}
/// Start a referendum. Can be called directly by the council.
pub fn internal_start_referendum(proposal: T::Proposal, threshold: VoteThreshold, delay: T::BlockNumber) -> result::Result<ReferendumIndex, &'static str> {
<Module<T>>::inject_referendum(<system::Module<T>>::block_number() + <Module<T>>::voting_period(), proposal, threshold, delay)
@@ -353,6 +387,17 @@ impl<T: Trait> Module<T> {
// private.
/// Actually enact a vote, if legit.
fn do_vote(who: T::AccountId, ref_index: ReferendumIndex, vote: Vote) -> Result {
ensure!(vote.multiplier() <= Self::max_lock_periods(), "vote has too great a strength");
ensure!(Self::is_active_referendum(ref_index), "vote given for invalid referendum.");
if !<VoteOf<T>>::exists(&(ref_index, who.clone())) {
<VotersFor<T>>::mutate(ref_index, |voters| voters.push(who.clone()));
}
<VoteOf<T>>::insert(&(ref_index, who), vote);
Ok(())
}
/// Start a referendum
fn inject_referendum(
end: T::BlockNumber,
@@ -464,6 +509,12 @@ impl<T: Trait> Module<T> {
}
}
impl<T: Trait> OnFreeBalanceZero<T::AccountId> for Module<T> {
fn on_free_balance_zero(who: &T::AccountId) {
<Proxy<T>>::remove(who)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -631,6 +682,58 @@ mod tests {
});
}
#[test]
fn proxy_should_work() {
with_externalities(&mut new_test_ext(), || {
assert_eq!(Democracy::proxy(10), None);
assert_ok!(Democracy::set_proxy(Origin::signed(1), 10));
assert_eq!(Democracy::proxy(10), Some(1));
// Can't set when already set.
assert_noop!(Democracy::set_proxy(Origin::signed(2), 10), "already a proxy");
// But this works because 11 isn't proxying.
assert_ok!(Democracy::set_proxy(Origin::signed(2), 11));
assert_eq!(Democracy::proxy(10), Some(1));
assert_eq!(Democracy::proxy(11), Some(2));
// 2 cannot fire 1's proxy:
assert_noop!(Democracy::remove_proxy(Origin::signed(2), 10), "wrong proxy");
// 1 fires his proxy:
assert_ok!(Democracy::remove_proxy(Origin::signed(1), 10));
assert_eq!(Democracy::proxy(10), None);
assert_eq!(Democracy::proxy(11), Some(2));
// 11 resigns:
assert_ok!(Democracy::resign_proxy(Origin::signed(11)));
assert_eq!(Democracy::proxy(10), None);
assert_eq!(Democracy::proxy(11), None);
});
}
#[test]
fn single_proposal_should_work_with_proxy() {
with_externalities(&mut new_test_ext(), || {
System::set_block_number(1);
assert_ok!(propose_set_balance(1, 2, 1));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
System::set_block_number(2);
let r = 0;
assert_ok!(Democracy::set_proxy(Origin::signed(1), 10));
assert_ok!(Democracy::proxy_vote(Origin::signed(10), r, AYE));
assert_eq!(Democracy::referendum_count(), 1);
assert_eq!(Democracy::voters_for(r), vec![1]);
assert_eq!(Democracy::vote_of((r, 1)), AYE);
assert_eq!(Democracy::tally(r), (10, 0, 10));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
assert_eq!(Balances::free_balance(&42), 2);
});
}
#[test]
fn single_proposal_should_work_with_delegation() {
with_externalities(&mut new_test_ext(), || {