Delegation voting (#1827)

* feat: add on-chain delegation voting

* fix: add recursion limit to delegated_votes

* fix: tests

* fix: count votes before limit exceeded
This commit is contained in:
Marcio Diaz
2019-03-18 18:33:36 +01:00
committed by GitHub
parent 530c12b4dc
commit 648b11e118
4 changed files with 239 additions and 7 deletions
+2 -2
View File
@@ -58,8 +58,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: create_runtime_str!("node"),
impl_name: create_runtime_str!("substrate-node"),
authoring_version: 10,
spec_version: 36,
impl_version: 39,
spec_version: 37,
impl_version: 40,
apis: RUNTIME_API_VERSIONS,
};
+237 -5
View File
@@ -22,7 +22,7 @@ use rstd::prelude::*;
use rstd::result;
use primitives::traits::{Zero, As, Bounded};
use parity_codec::{Encode, Decode};
use srml_support::{StorageValue, StorageMap, Parameter, Dispatchable, IsSubType};
use srml_support::{StorageValue, StorageMap, Parameter, Dispatchable, IsSubType, EnumerableStorageMap};
use srml_support::{decl_module, decl_storage, decl_event, ensure};
use srml_support::traits::{Currency, LockableCurrency, WithdrawReason, ArithmeticType, LockIdentifier};
use srml_support::dispatch::Result;
@@ -40,6 +40,8 @@ pub type ReferendumIndex = u32;
/// A number of lock periods.
pub type LockPeriods = i8;
const MAX_RECURSION_LIMIT: u32 = 16;
/// A number of lock periods, plus a vote, one way or the other.
#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "std", derive(Debug))]
@@ -156,6 +158,22 @@ decl_module! {
runtime_io::print(e);
}
}
/// Delegate vote.
pub fn delegate(origin, to: T::AccountId, lock_periods: LockPeriods) -> Result {
let who = ensure_signed(origin)?;
<Delegations<T>>::insert(who.clone(), (to.clone(), lock_periods.clone()));
Self::deposit_event(RawEvent::Delegated(who, to));
Ok(())
}
/// Undelegate vote.
fn undelegate(origin) -> Result {
let who = ensure_signed(origin)?;
<Delegations<T>>::remove(who.clone());
Self::deposit_event(RawEvent::Undelegated(who));
Ok(())
}
}
}
@@ -217,6 +235,9 @@ decl_storage! {
/// voter when called with the referendum (you'll get the default `Vote` value otherwise). If you don't want to check
/// `voters_for`, then you can also check for simple existence with `VoteOf::exists` first.
pub VoteOf get(vote_of): map (ReferendumIndex, T::AccountId) => Vote;
/// Get the account (and lock periods) to which another account is delegating vote.
pub Delegations get(delegations): linked_map T::AccountId => (T::AccountId, LockPeriods);
}
}
@@ -230,6 +251,8 @@ decl_event!(
NotPassed(ReferendumIndex),
Cancelled(ReferendumIndex),
Executed(ReferendumIndex, bool),
Delegated(AccountId, AccountId),
Undelegated(AccountId),
}
);
@@ -268,10 +291,9 @@ impl<T: Trait> Module<T> {
/// Get the voters for the current proposal.
pub fn tally(ref_index: ReferendumIndex) -> (BalanceOf<T>, BalanceOf<T>, BalanceOf<T>) {
Self::voters_for(ref_index).iter()
let (approve, against, capital): (BalanceOf<T>, BalanceOf<T>, BalanceOf<T>) = Self::voters_for(ref_index).iter()
.map(|voter| (
T::Currency::total_balance(voter),
Self::vote_of((ref_index, voter.clone())),
T::Currency::total_balance(voter), Self::vote_of((ref_index, voter.clone()))
))
.map(|(bal, vote)|
if vote.is_aye() {
@@ -279,7 +301,42 @@ impl<T: Trait> Module<T> {
} else {
(Zero::zero(), bal * BalanceOf::<T>::sa(vote.multiplier() as u64), bal)
}
).fold((Zero::zero(), Zero::zero(), Zero::zero()), |(a, b, c), (d, e, f)| (a + d, b + e, c + f))
).fold((Zero::zero(), Zero::zero(), Zero::zero()), |(a, b, c), (d, e, f)| (a + d, b + e, c + f));
let (del_approve, del_against, del_capital) = Self::tally_delegation(ref_index);
(approve + del_approve, against + del_against, capital + del_capital)
}
/// Get the delegated voters for the current proposal.
/// I think this goes into a worker once https://github.com/paritytech/substrate/issues/1458 is done.
fn tally_delegation(ref_index: ReferendumIndex) -> (BalanceOf<T>, BalanceOf<T>, BalanceOf<T>) {
Self::voters_for(ref_index).iter()
.fold((Zero::zero(), Zero::zero(), Zero::zero()), |(approve_acc, against_acc, capital_acc), voter| {
let vote = Self::vote_of((ref_index, voter.clone()));
let (votes, balance) = Self::delegated_votes(ref_index, voter.clone(), vote.multiplier(), MAX_RECURSION_LIMIT);
if vote.is_aye() {
(approve_acc + votes, against_acc, capital_acc + balance)
} else {
(approve_acc, against_acc + votes, capital_acc + balance)
}
})
}
fn delegated_votes(
ref_index: ReferendumIndex,
to: T::AccountId,
min_lock_periods: LockPeriods,
recursion_limit: u32,
) -> (BalanceOf<T>, BalanceOf<T>) {
if recursion_limit == 0 { return (Zero::zero(), Zero::zero()); }
<Delegations<T>>::enumerate()
.filter(|(delegator, (delegate, _))| *delegate == to && !<VoteOf<T>>::exists(&(ref_index, delegator.clone())))
.fold((Zero::zero(), Zero::zero()), |(votes_acc, balance_acc), (delegator, (_delegate, periods))| {
let lock_periods = if min_lock_periods <= periods { min_lock_periods } else { periods };
let balance = T::Currency::total_balance(&delegator);
let votes = T::Currency::total_balance(&delegator) * BalanceOf::<T>::sa(lock_periods as u64);
let (del_votes, del_balance) = Self::delegated_votes(ref_index, delegator, lock_periods, recursion_limit - 1);
(votes_acc + votes + del_votes, balance_acc + balance + del_balance)
})
}
// Exposed mutables.
@@ -564,6 +621,158 @@ mod tests {
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(), || {
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;
// Delegate vote.
assert_ok!(Democracy::delegate(Origin::signed(2), 1, 100));
assert_ok!(Democracy::vote(Origin::signed(1), r, AYE));
assert_eq!(Democracy::referendum_count(), 1);
assert_eq!(Democracy::voters_for(r), vec![1]);
assert_eq!(Democracy::vote_of((r, 1)), AYE);
// Delegated vote is counted.
assert_eq!(Democracy::tally(r), (30, 0, 30));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
assert_eq!(Balances::free_balance(&42), 2);
});
}
#[test]
fn single_proposal_should_work_with_cyclic_delegation() {
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;
// Check behaviour with cycle.
assert_ok!(Democracy::delegate(Origin::signed(2), 1, 100));
assert_ok!(Democracy::delegate(Origin::signed(3), 2, 100));
assert_ok!(Democracy::delegate(Origin::signed(1), 3, 100));
assert_ok!(Democracy::vote(Origin::signed(1), r, AYE));
assert_eq!(Democracy::referendum_count(), 1);
assert_eq!(Democracy::voters_for(r), vec![1]);
// Delegated vote is counted.
assert_eq!(Democracy::tally(r), (60, 0, 60));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
assert_eq!(Balances::free_balance(&42), 2);
});
}
#[test]
/// If transactor already voted, delegated vote is overwriten.
fn single_proposal_should_work_with_vote_and_delegation() {
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::vote(Origin::signed(1), r, AYE));
// Vote.
assert_ok!(Democracy::vote(Origin::signed(2), r, AYE));
// Delegate vote.
assert_ok!(Democracy::delegate(Origin::signed(2), 1, 100));
assert_eq!(Democracy::referendum_count(), 1);
assert_eq!(Democracy::voters_for(r), vec![1, 2]);
assert_eq!(Democracy::vote_of((r, 1)), AYE);
// Delegated vote is not counted.
assert_eq!(Democracy::tally(r), (30, 0, 30));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
assert_eq!(Balances::free_balance(&42), 2);
});
}
#[test]
fn single_proposal_should_work_with_undelegation() {
with_externalities(&mut new_test_ext(), || {
System::set_block_number(1);
assert_ok!(propose_set_balance(1, 2, 1));
// Delegate and undelegate vote.
assert_ok!(Democracy::delegate(Origin::signed(2), 1, 100));
assert_ok!(Democracy::undelegate(Origin::signed(2)));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
System::set_block_number(2);
let r = 0;
assert_ok!(Democracy::vote(Origin::signed(1), r, AYE));
assert_eq!(Democracy::referendum_count(), 1);
assert_eq!(Democracy::voters_for(r), vec![1]);
assert_eq!(Democracy::vote_of((r, 1)), AYE);
// Delegated vote is not counted.
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]
/// If transactor voted, delegated vote is overwriten.
fn single_proposal_should_work_with_delegation_and_vote() {
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::vote(Origin::signed(1), r, AYE));
// Delegate vote.
assert_ok!(Democracy::delegate(Origin::signed(2), 1, 100));
// Vote.
assert_ok!(Democracy::vote(Origin::signed(2), r, AYE));
assert_eq!(Democracy::referendum_count(), 1);
assert_eq!(Democracy::voters_for(r), vec![1, 2]);
assert_eq!(Democracy::vote_of((r, 1)), AYE);
// Delegated vote is not counted.
assert_eq!(Democracy::tally(r), (30, 0, 30));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
@@ -774,6 +983,29 @@ mod tests {
});
}
#[test]
fn lock_voting_should_work_with_delegation() {
with_externalities(&mut new_test_ext_with_public_delay(1), || {
System::set_block_number(1);
let r = Democracy::inject_referendum(1, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0).unwrap();
assert_ok!(Democracy::vote(Origin::signed(1), r, Vote::new(false, 6)));
assert_ok!(Democracy::vote(Origin::signed(2), r, Vote::new(true, 5)));
assert_ok!(Democracy::vote(Origin::signed(3), r, Vote::new(true, 4)));
assert_ok!(Democracy::vote(Origin::signed(4), r, Vote::new(true, 3)));
assert_ok!(Democracy::delegate(Origin::signed(5), 2, 2));
assert_ok!(Democracy::vote(Origin::signed(6), r, Vote::new(false, 1)));
assert_eq!(Democracy::tally(r), (440, 120, 210));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
System::set_block_number(2);
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
assert_eq!(Balances::free_balance(&42), 2);
});
}
#[test]
fn controversial_low_turnout_voting_should_work() {
with_externalities(&mut new_test_ext(), || {