// This file is part of Substrate. // Copyright (C) 2019-2020 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Phragmén Election Module. //! //! An election module based on sequential phragmen. //! //! ### Term and Round //! //! The election happens in _rounds_: every `N` blocks, all previous members are retired and a new //! set is elected (which may or may not have an intersection with the previous set). Each round //! lasts for some number of blocks defined by `TermDuration` storage item. The words _term_ and //! _round_ can be used interchangeably in this context. //! //! `TermDuration` might change during a round. This can shorten or extend the length of the round. //! The next election round's block number is never stored but rather always checked on the fly. //! Based on the current block number and `TermDuration`, the condition `BlockNumber % TermDuration //! == 0` being satisfied will always trigger a new election round. //! //! ### Voting //! //! Voters can vote for any set of the candidates by providing a list of account ids. Invalid votes //! (voting for non-candidates) are ignored during election. Yet, a voter _might_ vote for a future //! candidate. Voters reserve a bond as they vote. Each vote defines a `value`. This amount is //! locked from the account of the voter and indicates the weight of the vote. Voters can update //! their votes at any time by calling `vote()` again. This keeps the bond untouched but can //! optionally change the locked `value`. After a round, votes are kept and might still be valid for //! further rounds. A voter is responsible for calling `remove_voter` once they are done to have //! their bond back and remove the lock. //! //! Voters also report other voters as being defunct to earn their bond. A voter is defunct once all //! of the candidates that they have voted for are neither a valid candidate anymore nor a member. //! Upon reporting, if the target voter is actually defunct, the reporter will be rewarded by the //! voting bond of the target. The target will lose their bond and get removed. If the target is not //! defunct, the reporter is slashed and removed. To prevent being reported, voters should manually //! submit a `remove_voter()` as soon as they are in the defunct state. //! //! ### Candidacy and Members //! //! Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy //! back. A candidate can end up in one of the below situations: //! - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve and they //! are automatically counted as a candidate for the next election. //! - **Runner-up**: Runners-up are the best candidates immediately after the winners. The number //! of runners_up to keep is configurable. Runners-up are used, in order that they are elected, //! as replacements when a candidate is kicked by `[remove_member]`, or when an active member //! renounces their candidacy. Runners are automatically counted as a candidate for the next //! election. //! - **Loser**: Any of the candidate who are not a winner are left as losers. A loser might be an //! _outgoing member or runner_, meaning that they are an active member who failed to keep their //! spot. An outgoing will always lose their bond. //! //! ##### Renouncing candidacy. //! //! All candidates, elected or not, can renounce their candidacy. A call to [`Module::renounce_candidacy`] //! will always cause the candidacy bond to be refunded. //! //! Note that with the members being the default candidates for the next round and votes persisting //! in storage, the election system is entirely stable given no further input. This means that if //! the system has a particular set of candidates `C` and voters `V` that lead to a set of members //! `M` being elected, as long as `V` and `C` don't remove their candidacy and votes, `M` will keep //! being re-elected at the end of each round. //! //! ### Module Information //! //! - [`election_sp_phragmen::Config`](./trait.Config.html) //! - [`Call`](./enum.Call.html) //! - [`Module`](./struct.Module.html) #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode}; use frame_support::{ decl_error, decl_event, decl_module, decl_storage, dispatch::{DispatchResultWithPostInfo, WithPostDispatchInfo}, ensure, storage::{IterableStorageMap, StorageMap}, traits::{ BalanceStatus, ChangeMembers, Contains, ContainsLengthBound, Currency, CurrencyToVote, Get, InitializeMembers, LockIdentifier, LockableCurrency, OnUnbalanced, ReservableCurrency, WithdrawReasons, }, weights::Weight, }; use frame_system::{ensure_root, ensure_signed}; use sp_npos_elections::{ElectionResult, ExtendedBalance}; use sp_runtime::{ traits::{Saturating, StaticLookup, Zero}, DispatchError, Perbill, RuntimeDebug, }; use sp_std::prelude::*; mod benchmarking; pub mod weights; pub use weights::WeightInfo; /// The maximum votes allowed per voter. pub const MAXIMUM_VOTE: usize = 16; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type NegativeImbalanceOf = <::Currency as Currency<::AccountId>>::NegativeImbalance; /// An indication that the renouncing account currently has which of the below roles. #[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)] pub enum Renouncing { /// A member is renouncing. Member, /// A runner-up is renouncing. RunnerUp, /// A candidate is renouncing, while the given total number of candidates exists. Candidate(#[codec(compact)] u32), } /// Information needed to prove the defunct-ness of a voter. #[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)] pub struct DefunctVoter { /// the voter's who's being challenged for being defunct pub who: AccountId, /// The number of votes that `who` has placed. #[codec(compact)] pub vote_count: u32, /// The number of current active candidates. #[codec(compact)] pub candidate_count: u32 } pub trait Config: frame_system::Config { /// The overarching event type.c type Event: From> + Into<::Event>; /// Identifier for the elections-phragmen pallet's lock type ModuleId: Get; /// The currency that people are electing with. type Currency: LockableCurrency + ReservableCurrency; /// What to do when the members change. type ChangeMembers: ChangeMembers; /// What to do with genesis members type InitializeMembers: InitializeMembers; /// Convert a balance into a number used for election calculation. /// This must fit into a `u64` but is allowed to be sensibly lossy. type CurrencyToVote: CurrencyToVote>; /// How much should be locked up in order to submit one's candidacy. type CandidacyBond: Get>; /// How much should be locked up in order to be able to submit votes. type VotingBond: Get>; /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) type LoserCandidate: OnUnbalanced>; /// Handler for the unbalanced reduction when a reporter has submitted a bad defunct report. type BadReport: OnUnbalanced>; /// Handler for the unbalanced reduction when a member has been kicked. type KickedMember: OnUnbalanced>; /// Number of members to elect. type DesiredMembers: Get; /// Number of runners_up to keep. type DesiredRunnersUp: Get; /// How long each seat is kept. This defines the next block number at which an election /// round will happen. If set to zero, no elections are ever triggered and the module will /// be in passive mode. type TermDuration: Get; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } decl_storage! { trait Store for Module as PhragmenElection { // ---- State /// The current elected membership. Sorted based on account id. pub Members get(fn members): Vec<(T::AccountId, BalanceOf)>; /// The current runners_up. Sorted based on low to high merit (worse to best). pub RunnersUp get(fn runners_up): Vec<(T::AccountId, BalanceOf)>; /// The total number of vote rounds that have happened, excluding the upcoming one. pub ElectionRounds get(fn election_rounds): u32 = Zero::zero(); /// Votes and locked stake of a particular voter. /// /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash pub Voting get(fn voting): map hasher(twox_64_concat) T::AccountId => (BalanceOf, Vec); /// The present candidate list. Sorted based on account-id. A current member or runner-up /// can never enter this vector and is always implicitly assumed to be a candidate. pub Candidates get(fn candidates): Vec; } add_extra_genesis { config(members): Vec<(T::AccountId, BalanceOf)>; build(|config: &GenesisConfig| { assert!( config.members.len() as u32 <= T::DesiredMembers::get(), "Cannot accept more than DesiredMembers genesis member", ); let members = config.members.iter().map(|(ref member, ref stake)| { // make sure they have enough stake assert!( T::Currency::free_balance(member) >= *stake, "Genesis member does not have enough stake", ); // reserve candidacy bond and set as members. T::Currency::reserve(&member, T::CandidacyBond::get()) .expect("Genesis member does not have enough balance to be a candidate"); // Note: all members will only vote for themselves, hence they must be given exactly // their own stake as total backing. Any sane election should behave as such. // Nonetheless, stakes will be updated for term 1 onwards according to the election. Members::::mutate(|members| { match members.binary_search_by(|(a, _b)| a.cmp(member)) { Ok(_) => panic!("Duplicate member in elections phragmen genesis: {}", member), Err(pos) => members.insert(pos, (member.clone(), *stake)), } }); // set self-votes to make persistent. >::vote( T::Origin::from(Some(member.clone()).into()), vec![member.clone()], *stake, ).expect("Genesis member could not vote."); member.clone() }).collect::>(); // report genesis members to upstream, if any. T::InitializeMembers::initialize_members(&members); }) } } decl_error! { pub enum Error for Module { /// Cannot vote when no candidates or members exist. UnableToVote, /// Must vote for at least one candidate. NoVotes, /// Cannot vote more than candidates. TooManyVotes, /// Cannot vote more than maximum allowed. MaximumVotesExceeded, /// Cannot vote with stake less than minimum balance. LowBalance, /// Voter can not pay voting bond. UnableToPayBond, /// Must be a voter. MustBeVoter, /// Cannot report self. ReportSelf, /// Duplicated candidate submission. DuplicatedCandidate, /// Member cannot re-submit candidacy. MemberSubmit, /// Runner cannot re-submit candidacy. RunnerSubmit, /// Candidate does not have enough funds. InsufficientCandidateFunds, /// Not a member. NotMember, /// The provided count of number of candidates is incorrect. InvalidCandidateCount, /// The provided count of number of votes is incorrect. InvalidVoteCount, /// The renouncing origin presented a wrong `Renouncing` parameter. InvalidRenouncing, /// Prediction regarding replacement after member removal is wrong. InvalidReplacement, } } decl_module! { pub struct Module for enum Call where origin: T::Origin { type Error = Error; fn deposit_event() = default; const CandidacyBond: BalanceOf = T::CandidacyBond::get(); const VotingBond: BalanceOf = T::VotingBond::get(); const DesiredMembers: u32 = T::DesiredMembers::get(); const DesiredRunnersUp: u32 = T::DesiredRunnersUp::get(); const TermDuration: T::BlockNumber = T::TermDuration::get(); const ModuleId: LockIdentifier = T::ModuleId::get(); /// Vote for a set of candidates for the upcoming round of election. This can be called to /// set the initial votes, or update already existing votes. /// /// Upon initial voting, `value` units of `who`'s balance is locked and a bond amount is /// reserved. /// /// The `votes` should: /// - not be empty. /// - be less than the number of possible candidates. Note that all current members and /// runners-up are also automatically candidates for the next round. /// /// It is the responsibility of the caller to not place all of their balance into the lock /// and keep some for further transactions. /// /// # /// Base weight: 47.93 µs /// State reads: /// - Candidates.len() + Members.len() + RunnersUp.len() /// - Voting (is_voter) /// - Lock /// - [AccountBalance(who) (unreserve + total_balance)] /// State writes: /// - Voting /// - Lock /// - [AccountBalance(who) (unreserve -- only when creating a new voter)] /// # #[weight = T::WeightInfo::vote(votes.len() as u32)] fn vote( origin, votes: Vec, #[compact] value: BalanceOf, ) { let who = ensure_signed(origin)?; ensure!(votes.len() <= MAXIMUM_VOTE, Error::::MaximumVotesExceeded); ensure!(!votes.is_empty(), Error::::NoVotes); let candidates_count = >::decode_len().unwrap_or(0); let members_count = >::decode_len().unwrap_or(0); let runners_up_count = >::decode_len().unwrap_or(0); // addition is valid: candidates, members and runners-up will never overlap. let allowed_votes = candidates_count + members_count + runners_up_count; ensure!(!allowed_votes.is_zero(), Error::::UnableToVote); ensure!(votes.len() <= allowed_votes, Error::::TooManyVotes); ensure!(value > T::Currency::minimum_balance(), Error::::LowBalance); // first time voter. Reserve bond. if !Self::is_voter(&who) { T::Currency::reserve(&who, T::VotingBond::get()) .map_err(|_| Error::::UnableToPayBond)?; } // Amount to be locked up. let locked_balance = value.min(T::Currency::total_balance(&who)); // lock T::Currency::set_lock( T::ModuleId::get(), &who, locked_balance, WithdrawReasons::except(WithdrawReasons::TRANSACTION_PAYMENT), ); Voting::::insert(&who, (locked_balance, votes)); } /// Remove `origin` as a voter. This removes the lock and returns the bond. /// /// # /// Base weight: 36.8 µs /// All state access is from do_remove_voter. /// State reads: /// - Voting /// - [AccountData(who)] /// State writes: /// - Voting /// - Locks /// - [AccountData(who)] /// # #[weight = T::WeightInfo::remove_voter()] fn remove_voter(origin) { let who = ensure_signed(origin)?; ensure!(Self::is_voter(&who), Error::::MustBeVoter); Self::do_remove_voter(&who, true); } /// Report `target` for being an defunct voter. In case of a valid report, the reporter is /// rewarded by the bond amount of `target`. Otherwise, the reporter itself is removed and /// their bond is slashed. /// /// A defunct voter is defined to be: /// - a voter whose current submitted votes are all invalid. i.e. all of them are no /// longer a candidate nor an active member or a runner-up. /// /// /// The origin must provide the number of current candidates and votes of the reported target /// for the purpose of accurate weight calculation. /// /// # /// No Base weight based on min square analysis. /// Complexity of candidate_count: 1.755 µs /// Complexity of vote_count: 18.51 µs /// State reads: /// - Voting(reporter) /// - Candidate.len() /// - Voting(Target) /// - Candidates, Members, RunnersUp (is_defunct_voter) /// State writes: /// - Lock(reporter || target) /// - [AccountBalance(reporter)] + AccountBalance(target) /// - Voting(reporter || target) /// Note: the db access is worse with respect to db, which is when the report is correct. /// # #[weight = T::WeightInfo::report_defunct_voter_correct( defunct.candidate_count, defunct.vote_count, )] fn report_defunct_voter( origin, defunct: DefunctVoter<::Source>, ) -> DispatchResultWithPostInfo { let reporter = ensure_signed(origin)?; let target = T::Lookup::lookup(defunct.who)?; ensure!(reporter != target, Error::::ReportSelf); ensure!(Self::is_voter(&reporter), Error::::MustBeVoter); let DefunctVoter { candidate_count, vote_count, .. } = defunct; ensure!( >::decode_len().unwrap_or(0) as u32 <= candidate_count, Error::::InvalidCandidateCount, ); let (_, votes) = >::get(&target); // indirect way to ensure target is a voter. We could call into `::contains()`, but it // would have the same effect with one extra db access. Note that votes cannot be // submitted with length 0. Hence, a non-zero length means that the target is a voter. ensure!(votes.len() > 0, Error::::MustBeVoter); // ensure that the size of votes that need to be searched is correct. ensure!( votes.len() as u32 <= vote_count, Error::::InvalidVoteCount, ); let valid = Self::is_defunct_voter(&votes); let maybe_refund = if valid { // reporter will get the voting bond of the target T::Currency::repatriate_reserved(&target, &reporter, T::VotingBond::get(), BalanceStatus::Free)?; // remove the target. They are defunct. Self::do_remove_voter(&target, false); None } else { // slash the bond of the reporter. let imbalance = T::Currency::slash_reserved(&reporter, T::VotingBond::get()).0; T::BadReport::on_unbalanced(imbalance); // remove the reporter. Self::do_remove_voter(&reporter, false); Some(T::WeightInfo::report_defunct_voter_incorrect( defunct.candidate_count, defunct.vote_count, )) }; Self::deposit_event(RawEvent::VoterReported(target, reporter, valid)); Ok(maybe_refund.into()) } /// Submit oneself for candidacy. /// /// A candidate will either: /// - Lose at the end of the term and forfeit their deposit. /// - Win and become a member. Members will eventually get their stash back. /// - Become a runner-up. Runners-ups are reserved members in case one gets forcefully /// removed. /// /// # /// Base weight = 33.33 µs /// Complexity of candidate_count: 0.375 µs /// State reads: /// - Candidates /// - Members /// - RunnersUp /// - [AccountBalance(who)] /// State writes: /// - [AccountBalance(who)] /// - Candidates /// # #[weight = T::WeightInfo::submit_candidacy(*candidate_count)] fn submit_candidacy(origin, #[compact] candidate_count: u32) { let who = ensure_signed(origin)?; let actual_count = >::decode_len().unwrap_or(0); ensure!( actual_count as u32 <= candidate_count, Error::::InvalidCandidateCount, ); let is_candidate = Self::is_candidate(&who); ensure!(is_candidate.is_err(), Error::::DuplicatedCandidate); // assured to be an error, error always contains the index. let index = is_candidate.unwrap_err(); ensure!(!Self::is_member(&who), Error::::MemberSubmit); ensure!(!Self::is_runner_up(&who), Error::::RunnerSubmit); T::Currency::reserve(&who, T::CandidacyBond::get()) .map_err(|_| Error::::InsufficientCandidateFunds)?; >::mutate(|c| c.insert(index, who)); } /// Renounce one's intention to be a candidate for the next election round. 3 potential /// outcomes exist: /// - `origin` is a candidate and not elected in any set. In this case, the bond is /// unreserved, returned and origin is removed as a candidate. /// - `origin` is a current runner-up. In this case, the bond is unreserved, returned and /// origin is removed as a runner-up. /// - `origin` is a current member. In this case, the bond is unreserved and origin is /// removed as a member, consequently not being a candidate for the next round anymore. /// Similar to [`remove_voter`], if replacement runners exists, they are immediately used. /// /// If a candidate is renouncing: /// Base weight: 17.28 µs /// Complexity of candidate_count: 0.235 µs /// State reads: /// - Candidates /// - [AccountBalance(who) (unreserve)] /// State writes: /// - Candidates /// - [AccountBalance(who) (unreserve)] /// If member is renouncing: /// Base weight: 46.25 µs /// State reads: /// - Members, RunnersUp (remove_and_replace_member), /// - [AccountData(who) (unreserve)] /// State writes: /// - Members, RunnersUp (remove_and_replace_member), /// - [AccountData(who) (unreserve)] /// If runner is renouncing: /// Base weight: 46.25 µs /// State reads: /// - RunnersUp (remove_and_replace_member), /// - [AccountData(who) (unreserve)] /// State writes: /// - RunnersUp (remove_and_replace_member), /// - [AccountData(who) (unreserve)] /// #[weight = match *renouncing { Renouncing::Candidate(count) => T::WeightInfo::renounce_candidacy_candidate(count), Renouncing::Member => T::WeightInfo::renounce_candidacy_members(), Renouncing::RunnerUp => T::WeightInfo::renounce_candidacy_runners_up(), }] fn renounce_candidacy(origin, renouncing: Renouncing) { let who = ensure_signed(origin)?; match renouncing { Renouncing::Member => { // returns NoMember error in case of error. let _ = Self::remove_and_replace_member(&who)?; T::Currency::unreserve(&who, T::CandidacyBond::get()); Self::deposit_event(RawEvent::MemberRenounced(who)); }, Renouncing::RunnerUp => { let mut runners_up_with_stake = Self::runners_up(); if let Some(index) = runners_up_with_stake .iter() .position(|(ref r, ref _s)| r == &who) { runners_up_with_stake.remove(index); // unreserve the bond T::Currency::unreserve(&who, T::CandidacyBond::get()); // update storage. >::put(runners_up_with_stake); } else { Err(Error::::InvalidRenouncing)?; } } Renouncing::Candidate(count) => { let mut candidates = Self::candidates(); ensure!(count >= candidates.len() as u32, Error::::InvalidRenouncing); if let Some(index) = candidates.iter().position(|x| *x == who) { candidates.remove(index); // unreserve the bond T::Currency::unreserve(&who, T::CandidacyBond::get()); // update storage. >::put(candidates); } else { Err(Error::::InvalidRenouncing)?; } } }; } /// Remove a particular member from the set. This is effective immediately and the bond of /// the outgoing member is slashed. /// /// If a runner-up is available, then the best runner-up will be removed and replaces the /// outgoing member. Otherwise, a new phragmen election is started. /// /// Note that this does not affect the designated block number of the next election. /// /// # /// If we have a replacement: /// - Base weight: 50.93 µs /// - State reads: /// - RunnersUp.len() /// - Members, RunnersUp (remove_and_replace_member) /// - State writes: /// - Members, RunnersUp (remove_and_replace_member) /// Else, since this is a root call and will go into phragmen, we assume full block for now. /// # #[weight = if *has_replacement { T::WeightInfo::remove_member_with_replacement() } else { T::BlockWeights::get().max_block }] fn remove_member( origin, who: ::Source, has_replacement: bool, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; let will_have_replacement = >::decode_len().unwrap_or(0) > 0; if will_have_replacement != has_replacement { // In both cases, we will change more weight than neede. Refund and abort. return Err(Error::::InvalidReplacement.with_weight( // refund. The weight value comes from a benchmark which is special to this. // 5.751 µs T::WeightInfo::remove_member_wrong_refund() )); } // else, prediction was correct. Self::remove_and_replace_member(&who).map(|had_replacement| { let (imbalance, _) = T::Currency::slash_reserved(&who, T::CandidacyBond::get()); T::KickedMember::on_unbalanced(imbalance); Self::deposit_event(RawEvent::MemberKicked(who.clone())); if !had_replacement { // if we end up here, we will charge a full block weight. Self::do_phragmen(); } // no refund needed. None.into() }).map_err(|e| e.into()) } /// What to do at the end of each block. Checks if an election needs to happen or not. fn on_initialize(n: T::BlockNumber) -> Weight { // returns the correct weight. Self::end_block(n) } } } decl_event!( pub enum Event where Balance = BalanceOf, ::AccountId, { /// A new term with \[new_members\]. This indicates that enough candidates existed to run the /// election, not that enough have has been elected. The inner value must be examined for /// this purpose. A `NewTerm(\[\])` indicates that some candidates got their bond slashed and /// none were elected, whilst `EmptyTerm` means that no candidates existed to begin with. NewTerm(Vec<(AccountId, Balance)>), /// No (or not enough) candidates existed for this round. This is different from /// `NewTerm(\[\])`. See the description of `NewTerm`. EmptyTerm, /// Internal error happened while trying to perform election. ElectionError, /// A \[member\] has been removed. This should always be followed by either `NewTerm` or /// `EmptyTerm`. MemberKicked(AccountId), /// A candidate was slashed due to failing to obtain a seat as member or runner-up CandidateSlashed(AccountId, Balance), /// A seat holder (member or runner-up) was slashed due to failing to retaining their position. SeatHolderSlashed(AccountId, Balance), /// A \[member\] has renounced their candidacy. MemberRenounced(AccountId), /// A voter was reported with the the report being successful or not. /// \[voter, reporter, success\] VoterReported(AccountId, AccountId, bool), } ); impl Module { /// Attempts to remove a member `who`. If a runner-up exists, it is used as the replacement and /// Ok(true). is returned. /// /// Otherwise, `Ok(false)` is returned to signal the caller. /// /// If a replacement exists, `Members` and `RunnersUp` storage is updated, where the first /// element of `RunnersUp` is used as the replacement and `Ok(true)` is returned. Else, /// `Ok(false)` is returned with no storage updated. /// /// Note that this function _will_ call into `T::ChangeMembers` in case any change happens /// (`Ok(true)`). /// /// If replacement exists, this will read and write from/into both `Members` and `RunnersUp`. fn remove_and_replace_member(who: &T::AccountId) -> Result { let mut members_with_stake = Self::members(); if let Ok(index) = members_with_stake.binary_search_by(|(ref m, ref _s)| m.cmp(who)) { members_with_stake.remove(index); let next_up = >::mutate(|runners_up| runners_up.pop()); let maybe_replacement = next_up.and_then(|(replacement, stake)| members_with_stake.binary_search_by(|(ref m, ref _s)| m.cmp(&replacement)) .err() .map(|index| { members_with_stake.insert(index, (replacement.clone(), stake)); replacement }) ); >::put(&members_with_stake); let members = members_with_stake.into_iter().map(|m| m.0).collect::>(); let result = Ok(maybe_replacement.is_some()); let old = [who.clone()]; match maybe_replacement { Some(new) => T::ChangeMembers::change_members_sorted(&[new], &old, &members), None => T::ChangeMembers::change_members_sorted(&[], &old, &members), } result } else { Err(Error::::NotMember)? } } /// Check if `who` is a candidate. It returns the insert index if the element does not exists as /// an error. /// /// O(LogN) given N candidates. fn is_candidate(who: &T::AccountId) -> Result<(), usize> { Self::candidates().binary_search(who).map(|_| ()) } /// Check if `who` is a voter. It may or may not be a _current_ one. /// /// State: O(1). fn is_voter(who: &T::AccountId) -> bool { Voting::::contains_key(who) } /// Check if `who` is currently an active member. /// /// O(LogN) given N members. Since members are limited, O(1). fn is_member(who: &T::AccountId) -> bool { Self::members().binary_search_by(|(a, _b)| a.cmp(who)).is_ok() } /// Check if `who` is currently an active runner-up. /// /// O(LogN) given N runners-up. Since runners-up are limited, O(1). fn is_runner_up(who: &T::AccountId) -> bool { Self::runners_up().iter().position(|(a, _b)| a == who).is_some() } /// Returns number of desired members. fn desired_members() -> u32 { T::DesiredMembers::get() } /// Returns number of desired runners up. fn desired_runners_up() -> u32 { T::DesiredRunnersUp::get() } /// Returns the term duration fn term_duration() -> T::BlockNumber { T::TermDuration::get() } /// Get the members' account ids. fn members_ids() -> Vec { Self::members().into_iter().map(|(m, _)| m).collect::>() } /// The the runners' up account ids. fn runners_up_ids() -> Vec { Self::runners_up().into_iter().map(|(r, _)| r).collect::>() } /// Check if `votes` will correspond to a defunct voter. As no origin is part of the inputs, /// this function does not check the origin at all. /// /// O(NLogM) with M candidates and `who` having voted for `N` of them. /// Reads Members, RunnersUp, Candidates and Voting(who) from database. fn is_defunct_voter(votes: &[T::AccountId]) -> bool { votes.iter().all(|v| !Self::is_member(v) && !Self::is_runner_up(v) && !Self::is_candidate(v).is_ok() ) } /// Remove a certain someone as a voter. /// /// This will clean always clean the storage associated with the voter, and remove the balance /// lock. Optionally, it would also return the reserved voting bond if indicated by `unreserve`. /// If unreserve is true, has 3 storage reads and 1 reads. /// /// DB access: Voting and Lock are always written to, if unreserve, then 1 read and write added. fn do_remove_voter(who: &T::AccountId, unreserve: bool) { // remove storage and lock. Voting::::remove(who); T::Currency::remove_lock(T::ModuleId::get(), who); if unreserve { T::Currency::unreserve(who, T::VotingBond::get()); } } /// Check there's nothing to do this block. /// /// Runs phragmen election and cleans all the previous candidate state. The voter state is NOT /// cleaned and voters must themselves submit a transaction to retract. fn end_block(block_number: T::BlockNumber) -> Weight { if !Self::term_duration().is_zero() { if (block_number % Self::term_duration()).is_zero() { Self::do_phragmen(); return T::BlockWeights::get().max_block; } } 0 } /// Run the phragmen election with all required side processes and state updates, if election /// succeeds. Else, it will emit an `ElectionError` event. /// /// Calls the appropriate [`ChangeMembers`] function variant internally. /// /// Reads: O(C + V*E) where C = candidates, V voters and E votes per voter exits. /// Writes: O(M + R) with M desired members and R runners_up. fn do_phragmen() { let desired_seats = Self::desired_members() as usize; let desired_runners_up = Self::desired_runners_up() as usize; let num_to_elect = desired_runners_up + desired_seats; let mut candidates = Self::candidates(); // candidates who explicitly called `submit_candidacy`. Only these folks are at risk of // losing their bond. let exposed_candidates = candidates.clone(); // current members are always a candidate for the next round as well. // this is guaranteed to not create any duplicates. candidates.append(&mut Self::members_ids()); // previous runners_up are also always candidates for the next round. candidates.append(&mut Self::runners_up_ids()); if candidates.len().is_zero() { Self::deposit_event(RawEvent::EmptyTerm); return; } // helper closures to deal with balance/stake. let total_issuance = T::Currency::total_issuance(); let to_votes = |b: BalanceOf| T::CurrencyToVote::to_vote(b, total_issuance); let to_balance = |e: ExtendedBalance| T::CurrencyToVote::to_currency(e, total_issuance); // used for prime election. let voters_and_stakes = Voting::::iter() .map(|(voter, (stake, votes))| (voter, stake, votes)) .collect::>(); // used for phragmen. let voters_and_votes = voters_and_stakes.iter() .cloned() .map(|(voter, stake, votes)| { (voter, to_votes(stake), votes)} ) .collect::>(); let _ = sp_npos_elections::seq_phragmen::( num_to_elect, candidates, voters_and_votes.clone(), None, ).map(|ElectionResult { winners, assignments: _ }| { // this is already sorted by id. let old_members_ids_sorted = >::take().into_iter() .map(|(m, _)| m) .collect::>(); // this one needs a sort by id. let mut old_runners_up_ids_sorted = >::take().into_iter() .map(|(r, _)| r) .collect::>(); old_runners_up_ids_sorted.sort(); // filter out those who end up with no backing stake. let new_set_with_stake = winners .into_iter() .filter_map(|(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) }) .collect::)>>(); // OPTIMISATION NOTE: we could bail out here if `new_set.len() == 0`. There isn't much // left to do. Yet, re-arranging the code would require duplicating the slashing of // exposed candidates, cleaning any previous members, and so on. For now, in favour of // readability and veracity, we keep it simple. // split new set into winners and runners up. let split_point = desired_seats.min(new_set_with_stake.len()); let mut new_members_sorted_by_id = (&new_set_with_stake[..split_point]).to_vec(); // save the runners up as-is. They are sorted based on desirability. // save the members, sorted based on account id. new_members_sorted_by_id.sort_by(|i, j| i.0.cmp(&j.0)); // Now we select a prime member using a [Borda count](https://en.wikipedia.org/wiki/Borda_count). // We weigh everyone's vote for that new member by a multiplier based on the order // of the votes. i.e. the first person a voter votes for gets a 16x multiplier, // the next person gets a 15x multiplier, an so on... (assuming `MAXIMUM_VOTE` = 16) let mut prime_votes: Vec<_> = new_members_sorted_by_id.iter().map(|c| (&c.0, BalanceOf::::zero())).collect(); for (_, stake, votes) in voters_and_stakes.into_iter() { for (vote_multiplier, who) in votes.iter() .enumerate() .map(|(vote_position, who)| ((MAXIMUM_VOTE - vote_position) as u32, who)) { if let Ok(i) = prime_votes.binary_search_by_key(&who, |k| k.0) { prime_votes[i].1 = prime_votes[i].1.saturating_add( stake.saturating_mul(vote_multiplier.into()) ); } } } // We then select the new member with the highest weighted stake. In the case of // a tie, the last person in the list with the tied score is selected. This is // the person with the "highest" account id based on the sort above. let prime = prime_votes.into_iter().max_by_key(|x| x.1).map(|x| x.0.clone()); // new_members_sorted_by_id is sorted by account id. let new_members_ids_sorted = new_members_sorted_by_id .iter() .map(|(m, _)| m.clone()) .collect::>(); let new_runners_up_sorted_by_rank = &new_set_with_stake[split_point..] .into_iter() .cloned() .rev() .collect::)>>(); // new_runners_up remains sorted by desirability. let mut new_runners_up_ids_sorted = new_runners_up_sorted_by_rank .iter() .map(|(r, _)| r.clone()) .collect::>(); new_runners_up_ids_sorted.sort(); // report member changes. We compute diff because we need the outgoing list. let (incoming, outgoing) = T::ChangeMembers::compute_members_diff( &new_members_ids_sorted, &old_members_ids_sorted, ); T::ChangeMembers::change_members_sorted( &incoming, &outgoing, &new_members_ids_sorted, ); T::ChangeMembers::set_prime(prime); // outgoing members who are no longer a runner-up lose their bond. let mut to_burn_bond = outgoing .iter() .filter(|o| new_runners_up_ids_sorted.binary_search(o).is_err()) .cloned() .collect::>(); // compute the outgoing of runners up as well and append them to the `to_burn_bond`, if // they are not members. { let (_, outgoing) = T::ChangeMembers::compute_members_diff( &new_runners_up_ids_sorted, &old_runners_up_ids_sorted, ); // none of the ones computed to be outgoing must still be in the list. debug_assert!(outgoing.iter().all(|o| !new_runners_up_ids_sorted.contains(o))); to_burn_bond.extend( outgoing .iter() .filter(|o| new_members_ids_sorted.binary_search(o).is_err()) .cloned() .collect::>() ); } // Burn loser bond. members list is sorted. O(NLogM) (N candidates, M members) // runner up list is also sorted. O(NLogK) given K runner ups. Overall: O(NLogM + N*K) // both the member and runner counts are bounded. exposed_candidates.into_iter().for_each(|c| { // any candidate who is not a member and not a runner up. if new_members_ids_sorted.binary_search(&c).is_err() && new_runners_up_ids_sorted.binary_search(&c).is_err() { let (imbalance, _) = T::Currency::slash_reserved(&c, T::CandidacyBond::get()); Self::deposit_event(RawEvent::CandidateSlashed(c, T::CandidacyBond::get())); T::LoserCandidate::on_unbalanced(imbalance); } }); // Burn outgoing bonds to_burn_bond.into_iter().for_each(|x| { let (imbalance, _) = T::Currency::slash_reserved(&x, T::CandidacyBond::get()); Self::deposit_event(RawEvent::SeatHolderSlashed(x, T::CandidacyBond::get())); T::LoserCandidate::on_unbalanced(imbalance); }); >::put(&new_members_sorted_by_id); >::put(new_runners_up_sorted_by_rank); Self::deposit_event(RawEvent::NewTerm(new_members_sorted_by_id.clone().to_vec())); // clean candidates. >::kill(); ElectionRounds::mutate(|v| *v += 1); }).map_err(|e| { frame_support::debug::error!("elections-phragmen: failed to run election [{:?}].", e); Self::deposit_event(RawEvent::ElectionError); }); } } impl Contains for Module { fn contains(who: &T::AccountId) -> bool { Self::is_member(who) } fn sorted_members() -> Vec { Self::members_ids() } // A special function to populate members in this pallet for passing Origin // checks in runtime benchmarking. #[cfg(feature = "runtime-benchmarks")] fn add(who: &T::AccountId) { Members::::mutate(|members| { match members.binary_search_by(|(a, _b)| a.cmp(who)) { Ok(_) => (), Err(pos) => members.insert(pos, (who.clone(), BalanceOf::::default())), } }) } } impl ContainsLengthBound for Module { fn min_len() -> usize { 0 } /// Implementation uses a parameter type so calling is cost-free. fn max_len() -> usize { Self::desired_members() as usize } } #[cfg(test)] mod tests { use super::*; use frame_support::{assert_ok, assert_noop, assert_err_with_weight, parameter_types}; use substrate_test_utils::assert_eq_uvec; use sp_core::H256; use sp_runtime::{ testing::Header, BuildStorage, DispatchResult, traits::{BlakeTwo256, IdentityLookup, Block as BlockT}, }; use crate as elections_phragmen; parameter_types! { pub const BlockHashCount: u64 = 250; pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(1024); } impl frame_system::Config for Test { type BaseCallFilter = (); type BlockWeights = (); type BlockLength = (); type DbWeight = (); type Origin = Origin; type Index = u64; type BlockNumber = u64; type Call = Call; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; type Event = Event; type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = (); type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); } parameter_types! { pub const ExistentialDeposit: u64 = 1; } impl pallet_balances::Config for Test { type Balance = u64; type Event = Event; type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = frame_system::Module; type MaxLocks = (); type WeightInfo = (); } parameter_types! { pub const CandidacyBond: u64 = 3; } frame_support::parameter_types! { pub static VotingBond: u64 = 2; pub static DesiredMembers: u32 = 2; pub static DesiredRunnersUp: u32 = 2; pub static TermDuration: u64 = 5; pub static Members: Vec = vec![]; pub static Prime: Option = None; } pub struct TestChangeMembers; impl ChangeMembers for TestChangeMembers { fn change_members_sorted(incoming: &[u64], outgoing: &[u64], new: &[u64]) { // new, incoming, outgoing must be sorted. let mut new_sorted = new.to_vec(); new_sorted.sort(); assert_eq!(new, &new_sorted[..]); let mut incoming_sorted = incoming.to_vec(); incoming_sorted.sort(); assert_eq!(incoming, &incoming_sorted[..]); let mut outgoing_sorted = outgoing.to_vec(); outgoing_sorted.sort(); assert_eq!(outgoing, &outgoing_sorted[..]); // incoming and outgoing must be disjoint for x in incoming.iter() { assert!(outgoing.binary_search(x).is_err()); } let mut old_plus_incoming = MEMBERS.with(|m| m.borrow().to_vec()); old_plus_incoming.extend_from_slice(incoming); old_plus_incoming.sort(); let mut new_plus_outgoing = new.to_vec(); new_plus_outgoing.extend_from_slice(outgoing); new_plus_outgoing.sort(); assert_eq!(old_plus_incoming, new_plus_outgoing, "change members call is incorrect!"); MEMBERS.with(|m| *m.borrow_mut() = new.to_vec()); PRIME.with(|p| *p.borrow_mut() = None); } fn set_prime(who: Option) { PRIME.with(|p| *p.borrow_mut() = who); } } parameter_types!{ pub const ElectionsPhragmenModuleId: LockIdentifier = *b"phrelect"; } impl Config for Test { type ModuleId = ElectionsPhragmenModuleId; type Event = Event; type Currency = Balances; type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; type ChangeMembers = TestChangeMembers; type InitializeMembers = (); type CandidacyBond = CandidacyBond; type VotingBond = VotingBond; type TermDuration = TermDuration; type DesiredMembers = DesiredMembers; type DesiredRunnersUp = DesiredRunnersUp; type LoserCandidate = (); type KickedMember = (); type BadReport = (); type WeightInfo = (); } pub type Block = sp_runtime::generic::Block; pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; frame_support::construct_runtime!( pub enum Test where Block = Block, NodeBlock = Block, UncheckedExtrinsic = UncheckedExtrinsic { System: frame_system::{Module, Call, Event}, Balances: pallet_balances::{Module, Call, Event, Config}, Elections: elections_phragmen::{Module, Call, Event, Config}, } ); pub struct ExtBuilder { genesis_members: Vec<(u64, u64)>, balance_factor: u64, voter_bond: u64, term_duration: u64, desired_runners_up: u32, desired_members: u32, } impl Default for ExtBuilder { fn default() -> Self { Self { genesis_members: vec![], balance_factor: 1, voter_bond: 2, term_duration: 5, desired_runners_up: 0, desired_members: 2, } } } impl ExtBuilder { pub fn voter_bond(mut self, fee: u64) -> Self { self.voter_bond = fee; self } pub fn desired_runners_up(mut self, count: u32) -> Self { self.desired_runners_up = count; self } pub fn term_duration(mut self, duration: u64) -> Self { self.term_duration = duration; self } pub fn genesis_members(mut self, members: Vec<(u64, u64)>) -> Self { self.genesis_members = members; self } pub fn desired_members(mut self, count: u32) -> Self { self.desired_members = count; self } pub fn balance_factor(mut self, factor: u64) -> Self { self.balance_factor = factor; self } fn set_constants(&self) { VOTING_BOND.with(|v| *v.borrow_mut() = self.voter_bond); TERM_DURATION.with(|v| *v.borrow_mut() = self.term_duration); DESIRED_RUNNERS_UP.with(|v| *v.borrow_mut() = self.desired_runners_up); DESIRED_MEMBERS.with(|m| *m.borrow_mut() = self.desired_members); MEMBERS.with(|m| *m.borrow_mut() = self.genesis_members.iter().map(|(m, _)| m.clone()).collect::>()); } pub fn build_and_execute(self, test: impl FnOnce() -> ()) { self.set_constants(); let mut ext: sp_io::TestExternalities = GenesisConfig { pallet_balances: Some(pallet_balances::GenesisConfig::{ balances: vec![ (1, 10 * self.balance_factor), (2, 20 * self.balance_factor), (3, 30 * self.balance_factor), (4, 40 * self.balance_factor), (5, 50 * self.balance_factor), (6, 60 * self.balance_factor) ], }), elections_phragmen: Some(elections_phragmen::GenesisConfig:: { members: self.genesis_members }), }.build_storage().unwrap().into(); ext.execute_with(pre_conditions); ext.execute_with(test); ext.execute_with(post_conditions) } } fn all_voters() -> Vec { Voting::::iter().map(|(v, _)| v).collect::>() } fn balances(who: &u64) -> (u64, u64) { (Balances::free_balance(who), Balances::reserved_balance(who)) } fn has_lock(who: &u64) -> u64 { let lock = Balances::locks(who)[0].clone(); assert_eq!(lock.id, ElectionsPhragmenModuleId::get()); lock.amount } fn intersects(a: &[T], b: &[T]) -> bool { a.iter().any(|e| b.contains(e)) } fn ensure_members_sorted() { let mut members = Elections::members().clone(); members.sort(); assert_eq!(Elections::members(), members); } fn ensure_candidates_sorted() { let mut candidates = Elections::candidates().clone(); candidates.sort(); assert_eq!(Elections::candidates(), candidates); } fn locked_stake_of(who: &u64) -> u64 { Voting::::get(who).0 } fn ensure_members_has_approval_stake() { // we filter members that have no approval state. This means that even we have more seats // than candidates, we will never ever chose a member with no votes. assert!( Elections::members().iter().chain( Elections::runners_up().iter() ).all(|(_, s)| *s != u64::zero()) ); } fn ensure_member_candidates_runners_up_disjoint() { // members, candidates and runners-up must always be disjoint sets. assert!(!intersects(&Elections::members_ids(), &Elections::candidates())); assert!(!intersects(&Elections::members_ids(), &Elections::runners_up_ids())); assert!(!intersects(&Elections::candidates(), &Elections::runners_up_ids())); } fn pre_conditions() { System::set_block_number(1); ensure_members_sorted(); ensure_candidates_sorted(); } fn post_conditions() { ensure_members_sorted(); ensure_candidates_sorted(); ensure_member_candidates_runners_up_disjoint(); ensure_members_has_approval_stake(); } fn submit_candidacy(origin: Origin) -> DispatchResult { Elections::submit_candidacy(origin, Elections::candidates().len() as u32) } fn vote(origin: Origin, votes: Vec, stake: u64) -> DispatchResult { // historical note: helper function was created in a period of time in which the API of vote // call was changing. Currently it is a wrapper for the original call and does not do much. // Nonetheless, totally harmless. ensure_signed(origin.clone()).expect("vote origin must be signed"); Elections::vote(origin, votes, stake) } fn votes_of(who: &u64) -> Vec { Voting::::get(who).1 } fn defunct_for(who: u64) -> DefunctVoter { DefunctVoter { who, candidate_count: Elections::candidates().len() as u32, vote_count: votes_of(&who).len() as u32 } } #[test] fn params_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::desired_members(), 2); assert_eq!(Elections::term_duration(), 5); assert_eq!(Elections::election_rounds(), 0); assert!(Elections::members().is_empty()); assert!(Elections::runners_up().is_empty()); assert!(Elections::candidates().is_empty()); assert_eq!(>::decode_len(), None); assert!(Elections::is_candidate(&1).is_err()); assert!(all_voters().is_empty()); assert!(votes_of(&1).is_empty()); }); } #[test] fn genesis_members_should_work() { ExtBuilder::default().genesis_members(vec![(1, 10), (2, 20)]).build_and_execute(|| { System::set_block_number(1); assert_eq!(Elections::members(), vec![(1, 10), (2, 20)]); assert_eq!(Elections::voting(1), (10, vec![1])); assert_eq!(Elections::voting(2), (20, vec![2])); // they will persist since they have self vote. System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![1, 2]); }) } #[test] fn genesis_members_unsorted_should_work() { ExtBuilder::default().genesis_members(vec![(2, 20), (1, 10)]).build_and_execute(|| { System::set_block_number(1); assert_eq!(Elections::members(), vec![(1, 10), (2, 20)]); assert_eq!(Elections::voting(1), (10, vec![1])); assert_eq!(Elections::voting(2), (20, vec![2])); // they will persist since they have self vote. System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![1, 2]); }) } #[test] #[should_panic = "Genesis member does not have enough stake"] fn genesis_members_cannot_over_stake_0() { // 10 cannot lock 20 as their stake and extra genesis will panic. ExtBuilder::default() .genesis_members(vec![(1, 20), (2, 20)]) .build_and_execute(|| {}); } #[test] #[should_panic] fn genesis_members_cannot_over_stake_1() { // 10 cannot reserve 20 as voting bond and extra genesis will panic. ExtBuilder::default() .voter_bond(20) .genesis_members(vec![(1, 10), (2, 20)]) .build_and_execute(|| {}); } #[test] #[should_panic = "Duplicate member in elections phragmen genesis: 2"] fn genesis_members_cannot_be_duplicate() { ExtBuilder::default() .desired_members(3) .genesis_members(vec![(1, 10), (2, 10), (2, 10)]) .build_and_execute(|| {}); } #[test] #[should_panic = "Cannot accept more than DesiredMembers genesis member"] fn genesis_members_cannot_too_many() { ExtBuilder::default() .genesis_members(vec![(1, 10), (2, 10), (3, 30)]) .desired_members(2) .build_and_execute(|| {}); } #[test] fn term_duration_zero_is_passive() { ExtBuilder::default() .term_duration(0) .build_and_execute(|| { assert_eq!(Elections::term_duration(), 0); assert_eq!(Elections::desired_members(), 2); assert_eq!(Elections::election_rounds(), 0); assert!(Elections::members_ids().is_empty()); assert!(Elections::runners_up().is_empty()); assert!(Elections::candidates().is_empty()); System::set_block_number(5); Elections::end_block(System::block_number()); assert!(Elections::members_ids().is_empty()); assert!(Elections::runners_up().is_empty()); assert!(Elections::candidates().is_empty()); }); } #[test] fn simple_candidate_submission_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::candidates(), Vec::::new()); assert!(Elections::is_candidate(&1).is_err()); assert!(Elections::is_candidate(&2).is_err()); assert_eq!(balances(&1), (10, 0)); assert_ok!(submit_candidacy(Origin::signed(1))); assert_eq!(balances(&1), (7, 3)); assert_eq!(Elections::candidates(), vec![1]); assert!(Elections::is_candidate(&1).is_ok()); assert!(Elections::is_candidate(&2).is_err()); assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(Origin::signed(2))); assert_eq!(balances(&2), (17, 3)); assert_eq!(Elections::candidates(), vec![1, 2]); assert!(Elections::is_candidate(&1).is_ok()); assert!(Elections::is_candidate(&2).is_ok()); }); } #[test] fn simple_candidate_submission_with_no_votes_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::candidates(), Vec::::new()); assert_ok!(submit_candidacy(Origin::signed(1))); assert_ok!(submit_candidacy(Origin::signed(2))); assert!(Elections::is_candidate(&1).is_ok()); assert!(Elections::is_candidate(&2).is_ok()); assert_eq!(Elections::candidates(), vec![1, 2]); assert!(Elections::members_ids().is_empty()); assert!(Elections::runners_up().is_empty()); System::set_block_number(5); Elections::end_block(System::block_number()); assert!(Elections::is_candidate(&1).is_err()); assert!(Elections::is_candidate(&2).is_err()); assert!(Elections::candidates().is_empty()); assert!(Elections::members_ids().is_empty()); assert!(Elections::runners_up().is_empty()); }); } #[test] fn dupe_candidate_submission_should_not_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::candidates(), Vec::::new()); assert_ok!(submit_candidacy(Origin::signed(1))); assert_eq!(Elections::candidates(), vec![1]); assert_noop!( submit_candidacy(Origin::signed(1)), Error::::DuplicatedCandidate, ); }); } #[test] fn member_candidacy_submission_should_not_work() { // critically important to make sure that outgoing candidates and losers are not mixed up. ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![5]); assert!(Elections::runners_up().is_empty()); assert!(Elections::candidates().is_empty()); assert_noop!( submit_candidacy(Origin::signed(5)), Error::::MemberSubmit, ); }); } #[test] fn runner_candidate_submission_should_not_work() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(2), vec![5, 4], 20)); assert_ok!(vote(Origin::signed(1), vec![3], 10)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![3]); assert_noop!( submit_candidacy(Origin::signed(3)), Error::::RunnerSubmit, ); }); } #[test] fn poor_candidate_submission_should_not_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::candidates(), Vec::::new()); assert_noop!( submit_candidacy(Origin::signed(7)), Error::::InsufficientCandidateFunds, ); }); } #[test] fn simple_voting_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::candidates(), Vec::::new()); assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_eq!(balances(&2), (18, 2)); assert_eq!(has_lock(&2), 20); }); } #[test] fn can_vote_with_custom_stake() { ExtBuilder::default().build_and_execute(|| { assert_eq!(Elections::candidates(), Vec::::new()); assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(2), vec![5], 12)); assert_eq!(balances(&2), (18, 2)); assert_eq!(has_lock(&2), 12); }); } #[test] fn can_update_votes_and_stake() { ExtBuilder::default().build_and_execute(|| { assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_eq!(balances(&2), (18, 2)); assert_eq!(has_lock(&2), 20); assert_eq!(locked_stake_of(&2), 20); // can update; different stake; different lock and reserve. assert_ok!(vote(Origin::signed(2), vec![5, 4], 15)); assert_eq!(balances(&2), (18, 2)); assert_eq!(has_lock(&2), 15); assert_eq!(locked_stake_of(&2), 15); }); } #[test] fn cannot_vote_for_no_candidate() { ExtBuilder::default().build_and_execute(|| { assert_noop!( vote(Origin::signed(2), vec![], 20), Error::::NoVotes, ); }); } #[test] fn can_vote_for_old_members_even_when_no_new_candidates() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(2), vec![4, 5], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert!(Elections::candidates().is_empty()); assert_ok!(vote(Origin::signed(3), vec![4, 5], 10)); }); } #[test] fn prime_works() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(1), vec![4, 3], 10)); assert_ok!(vote(Origin::signed(2), vec![4], 20)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert!(Elections::candidates().is_empty()); assert_ok!(vote(Origin::signed(3), vec![4, 5], 10)); assert_eq!(PRIME.with(|p| *p.borrow()), Some(4)); }); } #[test] fn prime_votes_for_exiting_members_are_removed() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(1), vec![4, 3], 10)); assert_ok!(vote(Origin::signed(2), vec![4], 20)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(3))); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![3, 5]); assert!(Elections::candidates().is_empty()); assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); }); } #[test] fn cannot_vote_for_more_than_candidates_and_members_and_runners() { ExtBuilder::default() .desired_runners_up(1) .balance_factor(10) .build_and_execute( || { // when we have only candidates assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_noop!( // content of the vote is irrelevant. vote(Origin::signed(1), vec![9, 99, 999, 9999], 5), Error::::TooManyVotes, ); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); // now we have 2 members, 1 runner-up, and 1 new candidate assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(1), vec![9, 99, 999, 9999], 5)); assert_noop!( vote(Origin::signed(1), vec![9, 99, 999, 9_999, 99_999], 5), Error::::TooManyVotes, ); }); } #[test] fn cannot_vote_for_less_than_ed() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_noop!( vote(Origin::signed(2), vec![4], 1), Error::::LowBalance, ); }) } #[test] fn can_vote_for_more_than_total_balance_but_moot() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(2), vec![4, 5], 30)); // you can lie but won't get away with it. assert_eq!(locked_stake_of(&2), 20); assert_eq!(has_lock(&2), 20); }); } #[test] fn remove_voter_should_work() { ExtBuilder::default().voter_bond(8).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_ok!(vote(Origin::signed(3), vec![5], 30)); assert_eq_uvec!(all_voters(), vec![2, 3]); assert_eq!(locked_stake_of(&2), 20); assert_eq!(locked_stake_of(&3), 30); assert_eq!(votes_of(&2), vec![5]); assert_eq!(votes_of(&3), vec![5]); assert_ok!(Elections::remove_voter(Origin::signed(2))); assert_eq_uvec!(all_voters(), vec![3]); assert!(votes_of(&2).is_empty()); assert_eq!(locked_stake_of(&2), 0); assert_eq!(balances(&2), (20, 0)); assert_eq!(Balances::locks(&2).len(), 0); }); } #[test] fn non_voter_remove_should_not_work() { ExtBuilder::default().build_and_execute(|| { assert_noop!(Elections::remove_voter(Origin::signed(3)), Error::::MustBeVoter); }); } #[test] fn dupe_remove_should_fail() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_ok!(Elections::remove_voter(Origin::signed(2))); assert!(all_voters().is_empty()); assert_noop!(Elections::remove_voter(Origin::signed(2)), Error::::MustBeVoter); }); } #[test] fn removed_voter_should_not_be_counted() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_voter(Origin::signed(4))); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![3, 5]); }); } #[test] fn reporter_must_be_voter() { ExtBuilder::default().build_and_execute(|| { assert_noop!( Elections::report_defunct_voter(Origin::signed(1), defunct_for(2)), Error::::MustBeVoter, ); }); } #[test] fn reporter_must_provide_lengths() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); // both are defunct. assert_ok!(vote(Origin::signed(5), vec![99, 999, 9999], 50)); assert_ok!(vote(Origin::signed(4), vec![999], 40)); // 3 candidates! incorrect candidate length. assert_noop!( Elections::report_defunct_voter(Origin::signed(4), DefunctVoter { who: 5, candidate_count: 2, vote_count: 3, }), Error::::InvalidCandidateCount, ); // 3 votes! incorrect vote length assert_noop!( Elections::report_defunct_voter(Origin::signed(4), DefunctVoter { who: 5, candidate_count: 3, vote_count: 2, }), Error::::InvalidVoteCount, ); // correct. assert_ok!(Elections::report_defunct_voter(Origin::signed(4), DefunctVoter { who: 5, candidate_count: 3, vote_count: 3, })); }); } #[test] fn reporter_can_overestimate_length() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); // both are defunct. assert_ok!(vote(Origin::signed(5), vec![99], 50)); assert_ok!(vote(Origin::signed(4), vec![999], 40)); // 2 candidates! overestimation is okay. assert_ok!(Elections::report_defunct_voter(Origin::signed(4), defunct_for(5))); }); } #[test] fn can_detect_defunct_voter() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(6))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(2), vec![4, 5], 20)); assert_ok!(vote(Origin::signed(6), vec![6], 30)); // will be soon a defunct voter. assert_ok!(vote(Origin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![6]); assert!(Elections::candidates().is_empty()); // all of them have a member or runner-up that they voted for. assert_eq!(Elections::is_defunct_voter(&votes_of(&5)), false); assert_eq!(Elections::is_defunct_voter(&votes_of(&4)), false); assert_eq!(Elections::is_defunct_voter(&votes_of(&2)), false); assert_eq!(Elections::is_defunct_voter(&votes_of(&6)), false); // defunct assert_eq!(Elections::is_defunct_voter(&votes_of(&3)), true); assert_ok!(submit_candidacy(Origin::signed(1))); assert_ok!(vote(Origin::signed(1), vec![1], 10)); // has a candidate voted for. assert_eq!(Elections::is_defunct_voter(&votes_of(&1)), false); }); } #[test] fn report_voter_should_work_and_earn_reward() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(2), vec![4, 5], 20)); // will be soon a defunct voter. assert_ok!(vote(Origin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert!(Elections::candidates().is_empty()); assert_eq!(balances(&3), (28, 2)); assert_eq!(balances(&5), (45, 5)); assert_ok!(Elections::report_defunct_voter(Origin::signed(5), defunct_for(3))); assert!(System::events().iter().any(|event| { event.event == Event::elections_phragmen(RawEvent::VoterReported(3, 5, true)) })); assert_eq!(balances(&3), (28, 0)); assert_eq!(balances(&5), (47, 5)); }); } #[test] fn report_voter_should_slash_when_bad_report() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert!(Elections::candidates().is_empty()); assert_eq!(balances(&4), (35, 5)); assert_eq!(balances(&5), (45, 5)); assert_ok!(Elections::report_defunct_voter(Origin::signed(5), defunct_for(4))); assert!(System::events().iter().any(|event| { event.event == Event::elections_phragmen(RawEvent::VoterReported(4, 5, false)) })); assert_eq!(balances(&4), (35, 5)); assert_eq!(balances(&5), (45, 3)); }); } #[test] fn simple_voting_rounds_should_work() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_ok!(vote(Origin::signed(4), vec![4], 15)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert_eq!(votes_of(&2), vec![5]); assert_eq!(votes_of(&3), vec![3]); assert_eq!(votes_of(&4), vec![4]); assert_eq!(Elections::candidates(), vec![3, 4, 5]); assert_eq!(>::decode_len().unwrap(), 3); assert_eq!(Elections::election_rounds(), 0); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members(), vec![(3, 30), (5, 20)]); assert!(Elections::runners_up().is_empty()); assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert!(Elections::candidates().is_empty()); assert_eq!(>::decode_len(), None); assert_eq!(Elections::election_rounds(), 1); }); } #[test] fn empty_term() { ExtBuilder::default().build_and_execute(|| { // no candidates, no nothing. System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!( System::events().iter().last().unwrap().event, Event::elections_phragmen(RawEvent::EmptyTerm), ) }) } #[test] fn all_outgoing() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!( System::events().iter().last().unwrap().event, Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)])), ); assert_eq!(Elections::members(), vec![(4, 40), (5, 50)]); assert_eq!(Elections::runners_up(), vec![]); assert_ok!(Elections::remove_voter(Origin::signed(5))); assert_ok!(Elections::remove_voter(Origin::signed(4))); System::set_block_number(10); Elections::end_block(System::block_number()); assert_eq!( System::events().iter().last().unwrap().event, Event::elections_phragmen(RawEvent::NewTerm(vec![])), ); // outgoing have lost their bond. assert_eq!(balances(&4), (37, 0)); assert_eq!(balances(&5), (47, 0)); }); } #[test] fn defunct_voter_will_be_counted() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); // This guy's vote is pointless for this round. assert_ok!(vote(Origin::signed(3), vec![4], 30)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members(), vec![(5, 50)]); assert_eq!(Elections::election_rounds(), 1); // but now it has a valid target. assert_ok!(submit_candidacy(Origin::signed(4))); System::set_block_number(10); Elections::end_block(System::block_number()); // candidate 4 is affected by an old vote. assert_eq!(Elections::members(), vec![(4, 30), (5, 50)]); assert_eq!(Elections::election_rounds(), 2); assert_eq_uvec!(all_voters(), vec![3, 5]); }); } #[test] fn only_desired_seats_are_chosen() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![2], 20)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::election_rounds(), 1); assert_eq!(Elections::members_ids(), vec![4, 5]); }); } #[test] fn phragmen_should_not_self_vote() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); System::set_block_number(5); Elections::end_block(System::block_number()); assert!(Elections::candidates().is_empty()); assert_eq!(Elections::election_rounds(), 1); assert!(Elections::members_ids().is_empty()); assert_eq!( System::events().iter().last().unwrap().event, Event::elections_phragmen(RawEvent::NewTerm(vec![])), ) }); } #[test] fn runners_up_should_be_kept() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![3], 20)); assert_ok!(vote(Origin::signed(3), vec![2], 30)); assert_ok!(vote(Origin::signed(4), vec![5], 40)); assert_ok!(vote(Origin::signed(5), vec![4], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); // sorted based on account id. assert_eq!(Elections::members_ids(), vec![4, 5]); // sorted based on merit (least -> most) assert_eq!(Elections::runners_up_ids(), vec![3, 2]); // runner ups are still locked. assert_eq!(balances(&4), (35, 5)); assert_eq!(balances(&5), (45, 5)); assert_eq!(balances(&3), (25, 5)); }); } #[test] fn runners_up_should_be_next_candidates() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![2], 20)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members(), vec![(4, 40), (5, 50)]); assert_eq!(Elections::runners_up(), vec![(2, 20), (3, 30)]); assert_ok!(vote(Origin::signed(5), vec![5], 15)); System::set_block_number(10); Elections::end_block(System::block_number()); assert_eq!(Elections::members(), vec![(3, 30), (4, 40)]); assert_eq!(Elections::runners_up(), vec![(5, 15), (2, 20)]); }); } #[test] fn runners_up_lose_bond_once_outgoing() { ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![2], 20)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![2]); assert_eq!(balances(&2), (15, 5)); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(3), vec![3], 30)); System::set_block_number(10); Elections::end_block(System::block_number()); assert_eq!(Elections::runners_up_ids(), vec![3]); assert_eq!(balances(&2), (15, 2)); }); } #[test] fn members_lose_bond_once_outgoing() { ExtBuilder::default().build_and_execute(|| { assert_eq!(balances(&5), (50, 0)); assert_ok!(submit_candidacy(Origin::signed(5))); assert_eq!(balances(&5), (47, 3)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_eq!(balances(&5), (45, 5)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![5]); assert_ok!(Elections::remove_voter(Origin::signed(5))); assert_eq!(balances(&5), (47, 3)); System::set_block_number(10); Elections::end_block(System::block_number()); assert!(Elections::members_ids().is_empty()); assert_eq!(balances(&5), (47, 0)); }); } #[test] fn losers_will_lose_the_bond() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(4), vec![5], 40)); assert_eq!(balances(&5), (47, 3)); assert_eq!(balances(&3), (27, 3)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![5]); // winner assert_eq!(balances(&5), (47, 3)); // loser assert_eq!(balances(&3), (27, 0)); }); } #[test] fn current_members_are_always_next_candidate() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![2], 20)); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_voter(Origin::signed(4))); // 5 will persist as candidates despite not being in the list. assert_eq!(Elections::candidates(), vec![2, 3]); System::set_block_number(10); Elections::end_block(System::block_number()); // 4 removed; 5 and 3 are the new best. assert_eq!(Elections::members_ids(), vec![3, 5]); }); } #[test] fn election_state_is_uninterrupted() { // what I mean by uninterrupted: // given no input or stimulants the same members are re-elected. ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(2), vec![2], 20)); let check_at_block = |b: u32| { System::set_block_number(b.into()); Elections::end_block(System::block_number()); // we keep re-electing the same folks. assert_eq!(Elections::members(), vec![(4, 40), (5, 50)]); assert_eq!(Elections::runners_up(), vec![(2, 20), (3, 30)]); // no new candidates but old members and runners-up are always added. assert!(Elections::candidates().is_empty()); assert_eq!(Elections::election_rounds(), b / 5); assert_eq_uvec!(all_voters(), vec![2, 3, 4, 5]); }; // this state will always persist when no further input is given. check_at_block(5); check_at_block(10); check_at_block(15); check_at_block(20); }); } #[test] fn remove_members_triggers_election() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); // a new candidate assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_member(Origin::root(), 4, false)); assert_eq!(balances(&4), (35, 2)); // slashed assert_eq!(Elections::election_rounds(), 2); // new election round assert_eq!(Elections::members_ids(), vec![3, 5]); // new members }); } #[test] fn remove_member_should_indicate_replacement() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); // no replacement yet. assert_err_with_weight!( Elections::remove_member(Origin::root(), 4, true), Error::::InvalidReplacement, Some(33489000), // only thing that matters for now is that it is NOT the full block. ); }); ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![3]); // there is a replacement! and this one needs a weight refund. assert_err_with_weight!( Elections::remove_member(Origin::root(), 4, false), Error::::InvalidReplacement, Some(33489000) // only thing that matters for now is that it is NOT the full block. ); }); } #[test] fn seats_should_be_released_when_no_vote() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(2), vec![3], 20)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_eq!(>::decode_len().unwrap(), 3); assert_eq!(Elections::election_rounds(), 0); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![3, 5]); assert_eq!(Elections::election_rounds(), 1); assert_ok!(Elections::remove_voter(Origin::signed(2))); assert_ok!(Elections::remove_voter(Origin::signed(3))); assert_ok!(Elections::remove_voter(Origin::signed(4))); assert_ok!(Elections::remove_voter(Origin::signed(5))); // meanwhile, no one cares to become a candidate again. System::set_block_number(10); Elections::end_block(System::block_number()); assert!(Elections::members_ids().is_empty()); assert_eq!(Elections::election_rounds(), 2); }); } #[test] fn incoming_outgoing_are_reported() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_ok!(submit_candidacy(Origin::signed(1))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(submit_candidacy(Origin::signed(3))); // 5 will change their vote and becomes an `outgoing` assert_ok!(vote(Origin::signed(5), vec![4], 8)); // 4 will stay in the set assert_ok!(vote(Origin::signed(4), vec![4], 40)); // 3 will become a winner assert_ok!(vote(Origin::signed(3), vec![3], 30)); // these two are losers. assert_ok!(vote(Origin::signed(2), vec![2], 20)); assert_ok!(vote(Origin::signed(1), vec![1], 10)); System::set_block_number(10); Elections::end_block(System::block_number()); // 3, 4 are new members, must still be bonded, nothing slashed. assert_eq!(Elections::members(), vec![(3, 30), (4, 48)]); assert_eq!(balances(&3), (25, 5)); assert_eq!(balances(&4), (35, 5)); // 1 is a loser, slashed by 3. assert_eq!(balances(&1), (5, 2)); // 5 is an outgoing loser. will also get slashed. assert_eq!(balances(&5), (45, 2)); assert!(System::events().iter().any(|event| { event.event == Event::elections_phragmen(RawEvent::NewTerm(vec![(4, 40), (5, 50)])) })); }) } #[test] fn invalid_votes_are_moot() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![10], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq_uvec!(Elections::members_ids(), vec![3, 4]); assert_eq!(Elections::election_rounds(), 1); }); } #[test] fn members_are_sorted_based_on_id_runners_on_merit() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![3], 20)); assert_ok!(vote(Origin::signed(3), vec![2], 30)); assert_ok!(vote(Origin::signed(4), vec![5], 40)); assert_ok!(vote(Origin::signed(5), vec![4], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); // id: low -> high. assert_eq!(Elections::members(), vec![(4, 50), (5, 40)]); // merit: low -> high. assert_eq!(Elections::runners_up(), vec![(3, 20), (2, 30)]); }); } #[test] fn candidates_are_sorted() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_eq!(Elections::candidates(), vec![3, 5]); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::Candidate(4))); assert_eq!(Elections::candidates(), vec![2, 4, 5]); }) } #[test] fn runner_up_replacement_maintains_members_order() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![2], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![2, 4]); assert_ok!(Elections::remove_member(Origin::root(), 2, true)); assert_eq!(Elections::members_ids(), vec![4, 5]); }); } #[test] fn can_renounce_candidacy_member_with_runners_bond_is_refunded() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![2, 3]); assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Member)); assert_eq!(balances(&4), (38, 2)); // 2 is voting bond. assert_eq!(Elections::members_ids(), vec![3, 5]); assert_eq!(Elections::runners_up_ids(), vec![2]); }) } #[test] fn can_renounce_candidacy_member_without_runners_bond_is_refunded() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert!(Elections::runners_up_ids().is_empty()); assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Member)); assert_eq!(balances(&4), (38, 2)); // 2 is voting bond. // no replacement assert_eq!(Elections::members_ids(), vec![5]); assert!(Elections::runners_up_ids().is_empty()); }) } #[test] fn can_renounce_candidacy_runner() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(5), vec![4], 50)); assert_ok!(vote(Origin::signed(4), vec![5], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![2, 3]); assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::RunnerUp)); assert_eq!(balances(&3), (28, 2)); // 2 is voting bond. assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![2]); }) } #[test] fn runner_up_replacement_works_when_out_of_order() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![5], 20)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(5), vec![2], 50)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![2, 4]); assert_eq!(Elections::runners_up_ids(), vec![5, 3]); assert_ok!(Elections::renounce_candidacy(Origin::signed(3), Renouncing::RunnerUp)); assert_eq!(Elections::members_ids(), vec![2, 4]); assert_eq!(Elections::runners_up_ids(), vec![5]); }); } #[test] fn can_renounce_candidacy_candidate() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_eq!(balances(&5), (47, 3)); assert_eq!(Elections::candidates(), vec![5]); assert_ok!(Elections::renounce_candidacy(Origin::signed(5), Renouncing::Candidate(1))); assert_eq!(balances(&5), (50, 0)); assert!(Elections::candidates().is_empty()); }) } #[test] fn wrong_renounce_candidacy_should_fail() { ExtBuilder::default().build_and_execute(|| { assert_noop!( Elections::renounce_candidacy(Origin::signed(5), Renouncing::Candidate(0)), Error::::InvalidRenouncing, ); assert_noop!( Elections::renounce_candidacy(Origin::signed(5), Renouncing::Member), Error::::NotMember, ); assert_noop!( Elections::renounce_candidacy(Origin::signed(5), Renouncing::RunnerUp), Error::::InvalidRenouncing, ); }) } #[test] fn non_member_renounce_member_should_fail() { ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![3]); assert_noop!( Elections::renounce_candidacy(Origin::signed(3), Renouncing::Member), Error::::NotMember, ); }) } #[test] fn non_runner_up_renounce_runner_up_should_fail() { ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4, 5]); assert_eq!(Elections::runners_up_ids(), vec![3]); assert_noop!( Elections::renounce_candidacy(Origin::signed(4), Renouncing::RunnerUp), Error::::InvalidRenouncing, ); }) } #[test] fn wrong_candidate_count_renounce_should_fail() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_noop!( Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(2)), Error::::InvalidRenouncing, ); assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(3))); }) } #[test] fn renounce_candidacy_count_can_overestimate() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); // while we have only 3 candidates. assert_ok!(Elections::renounce_candidacy(Origin::signed(4), Renouncing::Candidate(4))); }) } #[test] fn behavior_with_dupe_candidate() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { >::put(vec![1, 1, 2, 3, 4]); assert_ok!(vote(Origin::signed(5), vec![1], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![1, 4]); assert_eq!(Elections::runners_up_ids(), vec![2, 3]); assert!(Elections::candidates().is_empty()); }) } #[test] fn unsorted_runners_up_are_detected() { ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); assert_ok!(vote(Origin::signed(4), vec![4], 5)); assert_ok!(vote(Origin::signed(3), vec![3], 15)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![5]); assert_eq!(Elections::runners_up_ids(), vec![4, 3]); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(2), vec![2], 10)); System::set_block_number(10); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![5]); assert_eq!(Elections::runners_up_ids(), vec![2, 3]); // 4 is outgoing runner-up. Slash candidacy bond. assert_eq!(balances(&4), (35, 2)); // 3 stays. assert_eq!(balances(&3), (25, 5)); }) } #[test] fn member_to_runner_up_wont_slash() { ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4]); assert_eq!(Elections::runners_up_ids(), vec![2, 3]); assert_eq!(balances(&4), (35, 5)); assert_eq!(balances(&3), (25, 5)); assert_eq!(balances(&2), (15, 5)); // this guy will shift everyone down. assert_ok!(submit_candidacy(Origin::signed(5))); assert_ok!(vote(Origin::signed(5), vec![5], 50)); System::set_block_number(10); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![5]); assert_eq!(Elections::runners_up_ids(), vec![3, 4]); // 4 went from member to runner-up -- don't slash. assert_eq!(balances(&4), (35, 5)); // 3 stayed runner-up -- don't slash. assert_eq!(balances(&3), (25, 5)); // 2 was removed -- slash. assert_eq!(balances(&2), (15, 2)); }); } #[test] fn runner_up_to_member_wont_slash() { ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| { assert_ok!(submit_candidacy(Origin::signed(4))); assert_ok!(submit_candidacy(Origin::signed(3))); assert_ok!(submit_candidacy(Origin::signed(2))); assert_ok!(vote(Origin::signed(4), vec![4], 40)); assert_ok!(vote(Origin::signed(3), vec![3], 30)); assert_ok!(vote(Origin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![4]); assert_eq!(Elections::runners_up_ids(), vec![2, 3]); assert_eq!(balances(&4), (35, 5)); assert_eq!(balances(&3), (25, 5)); assert_eq!(balances(&2), (15, 5)); // swap some votes. assert_ok!(vote(Origin::signed(4), vec![2], 40)); assert_ok!(vote(Origin::signed(2), vec![4], 20)); System::set_block_number(10); Elections::end_block(System::block_number()); assert_eq!(Elections::members_ids(), vec![2]); assert_eq!(Elections::runners_up_ids(), vec![4, 3]); // 2 went from runner to member, don't slash assert_eq!(balances(&2), (15, 5)); // 4 went from member to runner, don't slash assert_eq!(balances(&4), (35, 5)); // 3 stayed the same assert_eq!(balances(&3), (25, 5)); }); } }