// This file is part of Substrate. // Copyright (C) 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 [`Config::TermDuration`]. The words _term_ and //! _round_ can be used interchangeably in this context. //! //! [`Config::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 [`Config::TermDuration`], the condition //! `BlockNumber % TermDuration == 0` being satisfied will always trigger a new election round. //! //! ### Bonds and Deposits //! //! Both voting and being a candidate requires deposits to be taken, in exchange for the data that //! needs to be kept on-chain. The terms *bond* and *deposit* can be used interchangeably in this //! context. //! //! Bonds will be unreserved only upon adhering to the protocol laws. Failing to do so will cause in //! the bond to slashed. //! //! ### Voting //! //! Voters can vote for a limited number of the candidates by providing a list of account ids, //! bounded by [`Config::MaxVotesPerVoter`]. Invalid votes (voting for non-candidates) and duplicate //! votes 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 can update the vote targets (which might update the deposit) or //! update the vote's stake ([`Voter::stake`]). 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. //! //! See [`Call::vote`], [`Call::remove_voter`]. //! //! ### Defunct Voter //! //! A voter is defunct once all of the candidates that they have voted for are not a valid candidate //! (as seen further below, members and runners-up are also always candidates). Defunct voters can //! be removed via a root call ([`Call::clean_defunct_voters`]). Upon being removed, their bond is //! returned. This is an administrative operation and can be called only by the root origin in the //! case of state bloat. //! //! ### Candidacy and Members //! //! Candidates also reserve a bond as they submit candidacy. A candidate can end up in one of the //! below situations: //! - **Members**: 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. The number of desired //! members is set by [`Config::DesiredMembers`]. //! - **Runner-up**: Runners-up are the best candidates immediately after the winners. The number //! of runners up to keep is set by [`Config::DesiredRunnersUp`]. Runners-up are used, in the //! same order as they are elected, as replacements when a candidate is kicked by //! [`Call::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 member/runner-up are left as losers. A loser //! might be an _outgoing member or runner-up_, meaning that they are an active member who //! failed to keep their spot. **An outgoing candidate/member/runner-up will always lose their //! bond**. //! //! #### Renouncing candidacy. //! //! All candidates, elected or not, can renounce their candidacy. A call to //! [`Call::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 //! //! - [`Config`] //! - [`Call`] //! - [`Module`] #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode}; use frame_support::{ traits::{ defensive_prelude::*, ChangeMembers, Contains, ContainsLengthBound, Currency, Get, InitializeMembers, LockIdentifier, LockableCurrency, OnUnbalanced, ReservableCurrency, SortedMembers, WithdrawReasons, }, weights::Weight, }; use log; use scale_info::TypeInfo; use sp_npos_elections::{ElectionResult, ExtendedBalance}; use sp_runtime::{ traits::{Saturating, StaticLookup, Zero}, DispatchError, Perbill, RuntimeDebug, }; use sp_staking::currency_to_vote::CurrencyToVote; use sp_std::{cmp::Ordering, prelude::*}; #[cfg(any(feature = "try-runtime", test))] use sp_runtime::TryRuntimeError; mod benchmarking; pub mod weights; pub use weights::WeightInfo; /// All migrations. pub mod migrations; const LOG_TARGET: &str = "runtime::elections-phragmen"; 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, TypeInfo)] 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), } /// An active voter. #[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, TypeInfo)] pub struct Voter { /// The members being backed. pub votes: Vec, /// The amount of stake placed on this vote. pub stake: Balance, /// The amount of deposit reserved for this vote. /// /// To be unreserved upon removal. pub deposit: Balance, } impl Default for Voter { fn default() -> Self { Self { votes: vec![], stake: Default::default(), deposit: Default::default() } } } /// A holder of a seat as either a member or a runner-up. #[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq, TypeInfo)] pub struct SeatHolder { /// The holder. pub who: AccountId, /// The total backing stake. pub stake: Balance, /// The amount of deposit held on-chain. /// /// To be unreserved upon renouncing, or slashed upon being a loser. pub deposit: Balance, } pub use pallet::*; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] #[pallet::without_storage_info] pub struct Pallet(_); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Identifier for the elections-phragmen pallet's lock #[pallet::constant] type PalletId: 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. #[pallet::constant] type CandidacyBond: Get>; /// Base deposit associated with voting. /// /// This should be sensibly high to economically ensure the pallet cannot be attacked by /// creating a gigantic number of votes. #[pallet::constant] type VotingBondBase: Get>; /// The amount of bond that need to be locked for each vote (32 bytes). #[pallet::constant] type VotingBondFactor: 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 member has been kicked. type KickedMember: OnUnbalanced>; /// Number of members to elect. #[pallet::constant] type DesiredMembers: Get; /// Number of runners_up to keep. #[pallet::constant] 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. #[pallet::constant] type TermDuration: Get>; /// The maximum number of candidates in a phragmen election. /// /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and /// consider how it will impact `T::WeightInfo::election_phragmen`. /// /// When this limit is reached no more candidates are accepted in the election. #[pallet::constant] type MaxCandidates: Get; /// The maximum number of voters to allow in a phragmen election. /// /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and /// consider how it will impact `T::WeightInfo::election_phragmen`. /// /// When the limit is reached the new voters are ignored. #[pallet::constant] type MaxVoters: Get; /// Maximum numbers of votes per voter. /// /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and /// consider how it will impact `T::WeightInfo::election_phragmen`. #[pallet::constant] type MaxVotesPerVoter: Get; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } #[pallet::hooks] impl Hooks> for Pallet { /// What to do at the end of each block. /// /// Checks if an election needs to happen or not. fn on_initialize(n: BlockNumberFor) -> Weight { let term_duration = T::TermDuration::get(); if !term_duration.is_zero() && (n % term_duration).is_zero() { Self::do_phragmen() } else { Weight::zero() } } fn integrity_test() { let block_weight = T::BlockWeights::get().max_block; // mind the order. let election_weight = T::WeightInfo::election_phragmen( T::MaxCandidates::get(), T::MaxVoters::get(), T::MaxVotesPerVoter::get() * T::MaxVoters::get(), ); let to_seconds = |w: &Weight| { w.ref_time() as f32 / frame_support::weights::constants::WEIGHT_REF_TIME_PER_SECOND as f32 }; log::debug!( target: LOG_TARGET, "election weight {}s ({:?}) // chain's block weight {}s ({:?})", to_seconds(&election_weight), election_weight, to_seconds(&block_weight), block_weight, ); assert!( election_weight.all_lt(block_weight), "election weight {}s ({:?}) will exceed a {}s chain's block weight ({:?}) (MaxCandidates {}, MaxVoters {}, MaxVotesPerVoter {} -- tweak these parameters)", election_weight, to_seconds(&election_weight), to_seconds(&block_weight), block_weight, T::MaxCandidates::get(), T::MaxVoters::get(), T::MaxVotesPerVoter::get(), ); } #[cfg(feature = "try-runtime")] fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> { Self::do_try_state() } } #[pallet::call] impl Pallet { /// 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 deposit amount is /// reserved. The deposit is based on the number of votes and can be updated over time. /// /// 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. /// /// If `value` is more than `who`'s free balance, then the maximum of the two is used. /// /// The dispatch origin of this call must be signed. /// /// ### Warning /// /// It is the responsibility of the caller to **NOT** place all of their balance into the /// lock and keep some for further operations. #[pallet::call_index(0)] #[pallet::weight( T::WeightInfo::vote_more(votes.len() as u32) .max(T::WeightInfo::vote_less(votes.len() as u32)) .max(T::WeightInfo::vote_equal(votes.len() as u32)) )] pub fn vote( origin: OriginFor, votes: Vec, #[pallet::compact] value: BalanceOf, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; ensure!( votes.len() <= T::MaxVotesPerVoter::get() as usize, 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); // can never submit a vote of there are no members, and cannot submit more votes than // all potential vote targets. // addition is valid: candidates, members and runners-up will never overlap. let allowed_votes = candidates_count.saturating_add(members_count).saturating_add(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); // Reserve bond. let new_deposit = Self::deposit_of(votes.len()); let Voter { deposit: old_deposit, .. } = >::get(&who); match new_deposit.cmp(&old_deposit) { Ordering::Greater => { // Must reserve a bit more. let to_reserve = new_deposit - old_deposit; T::Currency::reserve(&who, to_reserve) .map_err(|_| Error::::UnableToPayBond)?; }, Ordering::Equal => {}, Ordering::Less => { // Must unreserve a bit. let to_unreserve = old_deposit - new_deposit; let _remainder = T::Currency::unreserve(&who, to_unreserve); debug_assert!(_remainder.is_zero()); }, }; // Amount to be locked up. let locked_stake = value.min(T::Currency::free_balance(&who)); T::Currency::set_lock(T::PalletId::get(), &who, locked_stake, WithdrawReasons::all()); Voting::::insert(&who, Voter { votes, deposit: new_deposit, stake: locked_stake }); Ok(None::.into()) } /// Remove `origin` as a voter. /// /// This removes the lock and returns the deposit. /// /// The dispatch origin of this call must be signed and be a voter. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::remove_voter())] pub fn remove_voter(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(Self::is_voter(&who), Error::::MustBeVoter); Self::do_remove_voter(&who); Ok(()) } /// Submit oneself for candidacy. A fixed amount of deposit is recorded. /// /// All candidates are wiped at the end of the term. They either become a member/runner-up, /// or leave the system while their deposit is slashed. /// /// The dispatch origin of this call must be signed. /// /// ### Warning /// /// Even if a candidate ends up being a member, they must call [`Call::renounce_candidacy`] /// to get their deposit back. Losing the spot in an election will always lead to a slash. /// /// The number of current candidates must be provided as witness data. /// ## Complexity /// O(C + log(C)) where C is candidate_count. #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::submit_candidacy(*candidate_count))] pub fn submit_candidacy( origin: OriginFor, #[pallet::compact] candidate_count: u32, ) -> DispatchResult { let who = ensure_signed(origin)?; let actual_count = >::decode_len().unwrap_or(0) as u32; ensure!(actual_count <= candidate_count, Error::::InvalidWitnessData); ensure!( actual_count <= ::MaxCandidates::get(), Error::::TooManyCandidates ); let index = Self::is_candidate(&who).err().ok_or(Error::::DuplicatedCandidate)?; ensure!(!Self::is_member(&who), Error::::MemberSubmit); ensure!(!Self::is_runner_up(&who), Error::::RunnerUpSubmit); T::Currency::reserve(&who, T::CandidacyBond::get()) .map_err(|_| Error::::InsufficientCandidateFunds)?; >::mutate(|c| c.insert(index, (who, T::CandidacyBond::get()))); Ok(()) } /// 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 deposit is /// unreserved, returned and origin is removed as a candidate. /// - `origin` is a current runner-up. In this case, the deposit is unreserved, returned and /// origin is removed as a runner-up. /// - `origin` is a current member. In this case, the deposit is unreserved and origin is /// removed as a member, consequently not being a candidate for the next round anymore. /// Similar to [`remove_member`](Self::remove_member), if replacement runners exists, they /// are immediately used. If the prime is renouncing, then no prime will exist until the /// next round. /// /// The dispatch origin of this call must be signed, and have one of the above roles. /// The type of renouncing must be provided as witness data. /// /// ## Complexity /// - Renouncing::Candidate(count): O(count + log(count)) /// - Renouncing::Member: O(1) /// - Renouncing::RunnerUp: O(1) #[pallet::call_index(3)] #[pallet::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(), })] pub fn renounce_candidacy(origin: OriginFor, renouncing: Renouncing) -> DispatchResult { let who = ensure_signed(origin)?; match renouncing { Renouncing::Member => { let _ = Self::remove_and_replace_member(&who, false) .map_err(|_| Error::::InvalidRenouncing)?; Self::deposit_event(Event::Renounced { candidate: who }); }, Renouncing::RunnerUp => { >::try_mutate::<_, Error, _>(|runners_up| { let index = runners_up .iter() .position(|SeatHolder { who: r, .. }| r == &who) .ok_or(Error::::InvalidRenouncing)?; // can't fail anymore. let SeatHolder { deposit, .. } = runners_up.remove(index); let _remainder = T::Currency::unreserve(&who, deposit); debug_assert!(_remainder.is_zero()); Self::deposit_event(Event::Renounced { candidate: who }); Ok(()) })?; }, Renouncing::Candidate(count) => { >::try_mutate::<_, Error, _>(|candidates| { ensure!(count >= candidates.len() as u32, Error::::InvalidWitnessData); let index = candidates .binary_search_by(|(c, _)| c.cmp(&who)) .map_err(|_| Error::::InvalidRenouncing)?; let (_removed, deposit) = candidates.remove(index); let _remainder = T::Currency::unreserve(&who, deposit); debug_assert!(_remainder.is_zero()); Self::deposit_event(Event::Renounced { candidate: who }); Ok(()) })?; }, }; Ok(()) } /// 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, if `rerun_election` is `true`, a new phragmen election is /// started, else, nothing happens. /// /// If `slash_bond` is set to true, the bond of the member being removed is slashed. Else, /// it is returned. /// /// The dispatch origin of this call must be root. /// /// Note that this does not affect the designated block number of the next election. /// /// ## Complexity /// - Check details of remove_and_replace_member() and do_phragmen(). #[pallet::call_index(4)] #[pallet::weight(if *rerun_election { T::WeightInfo::remove_member_without_replacement() } else { T::WeightInfo::remove_member_with_replacement() })] pub fn remove_member( origin: OriginFor, who: AccountIdLookupOf, slash_bond: bool, rerun_election: bool, ) -> DispatchResult { ensure_root(origin)?; let who = T::Lookup::lookup(who)?; let _ = Self::remove_and_replace_member(&who, slash_bond)?; Self::deposit_event(Event::MemberKicked { member: who }); if rerun_election { Self::do_phragmen(); } // no refund needed. Ok(()) } /// Clean all voters who are defunct (i.e. they do not serve any purpose at all). The /// deposit of the removed voters are returned. /// /// This is an root function to be used only for cleaning the state. /// /// The dispatch origin of this call must be root. /// /// ## Complexity /// - Check is_defunct_voter() details. #[pallet::call_index(5)] #[pallet::weight(T::WeightInfo::clean_defunct_voters(*num_voters, *num_defunct))] pub fn clean_defunct_voters( origin: OriginFor, num_voters: u32, num_defunct: u32, ) -> DispatchResult { let _ = ensure_root(origin)?; >::iter() .take(num_voters as usize) .filter(|(_, x)| Self::is_defunct_voter(&x.votes)) .take(num_defunct as usize) .for_each(|(dv, _)| Self::do_remove_voter(&dv)); Ok(()) } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// 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 { new_members: Vec<(::AccountId, BalanceOf)> }, /// 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 { member: ::AccountId }, /// Someone has renounced their candidacy. Renounced { candidate: ::AccountId }, /// A candidate was slashed by amount due to failing to obtain a seat as member or /// runner-up. /// /// Note that old members and runners-up are also candidates. CandidateSlashed { candidate: ::AccountId, amount: BalanceOf }, /// A seat holder was slashed by amount by being forcefully removed from the set. SeatHolderSlashed { seat_holder: ::AccountId, amount: BalanceOf, }, } #[pallet::error] pub enum Error { /// 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, /// Duplicated candidate submission. DuplicatedCandidate, /// Too many candidates have been created. TooManyCandidates, /// Member cannot re-submit candidacy. MemberSubmit, /// Runner cannot re-submit candidacy. RunnerUpSubmit, /// Candidate does not have enough funds. InsufficientCandidateFunds, /// Not a member. NotMember, /// The provided count of number of candidates is incorrect. InvalidWitnessData, /// 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, } /// The current elected members. /// /// Invariant: Always sorted based on account id. #[pallet::storage] #[pallet::getter(fn members)] pub type Members = StorageValue<_, Vec>>, ValueQuery>; /// The current reserved runners-up. /// /// Invariant: Always sorted based on rank (worse to best). Upon removal of a member, the /// last (i.e. _best_) runner-up will be replaced. #[pallet::storage] #[pallet::getter(fn runners_up)] pub type RunnersUp = StorageValue<_, Vec>>, ValueQuery>; /// The present candidate list. A current member or runner-up can never enter this vector /// and is always implicitly assumed to be a candidate. /// /// Second element is the deposit. /// /// Invariant: Always sorted based on account id. #[pallet::storage] #[pallet::getter(fn candidates)] pub type Candidates = StorageValue<_, Vec<(T::AccountId, BalanceOf)>, ValueQuery>; /// The total number of vote rounds that have happened, excluding the upcoming one. #[pallet::storage] #[pallet::getter(fn election_rounds)] pub type ElectionRounds = StorageValue<_, u32, ValueQuery>; /// Votes and locked stake of a particular voter. /// /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash. #[pallet::storage] #[pallet::getter(fn voting)] pub type Voting = StorageMap<_, Twox64Concat, T::AccountId, Voter>, ValueQuery>; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { pub members: Vec<(T::AccountId, BalanceOf)>, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { assert!( self.members.len() as u32 <= T::DesiredMembers::get(), "Cannot accept more than DesiredMembers genesis member", ); let members = self .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.", ); // 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(|m| m.who.cmp(member)) { Ok(_) => { panic!( "Duplicate member in elections-phragmen genesis: {:?}", member ) }, Err(pos) => members.insert( pos, SeatHolder { who: member.clone(), stake: *stake, deposit: Zero::zero(), }, ), } }); // set self-votes to make persistent. Genesis voters don't have any bond, nor do // they have any lock. NOTE: this means that we will still try to remove a lock // once this genesis voter is removed, and for now it is okay because // remove_lock is noop if lock is not there. >::insert( &member, Voter { votes: vec![member.clone()], stake: *stake, deposit: Zero::zero() }, ); member.clone() }) .collect::>(); // report genesis members to upstream, if any. T::InitializeMembers::initialize_members(&members); } } } impl Pallet { /// The deposit value of `count` votes. fn deposit_of(count: usize) -> BalanceOf { T::VotingBondBase::get() .saturating_add(T::VotingBondFactor::get().saturating_mul((count as u32).into())) } /// Attempts to remove a member `who`. If a runner-up exists, it is used as the replacement. /// /// Returns: /// /// - `Ok(true)` if the member was removed and a replacement was found. /// - `Ok(false)` if the member was removed and but no replacement was found. /// - `Err(_)` if the member was no found. /// /// Both `Members` and `RunnersUp` storage is updated accordingly. `T::ChangeMember` is called /// if needed. If `slash` is true, the deposit of the potentially removed member is slashed, /// else, it is unreserved. /// /// ### Note: Prime preservation /// /// This function attempts to preserve the prime. If the removed members is not the prime, it is /// set again via [`Config::ChangeMembers`]. fn remove_and_replace_member(who: &T::AccountId, slash: bool) -> Result { // closure will return: // - `Ok(Option(replacement))` if member was removed and replacement was replaced. // - `Ok(None)` if member was removed but no replacement was found // - `Err(_)` if who is not a member. let maybe_replacement = >::try_mutate::<_, Error, _>(|members| { let remove_index = members .binary_search_by(|m| m.who.cmp(who)) .map_err(|_| Error::::NotMember)?; // we remove the member anyhow, regardless of having a runner-up or not. let removed = members.remove(remove_index); // slash or unreserve if slash { let (imbalance, _remainder) = T::Currency::slash_reserved(who, removed.deposit); debug_assert!(_remainder.is_zero()); T::LoserCandidate::on_unbalanced(imbalance); Self::deposit_event(Event::SeatHolderSlashed { seat_holder: who.clone(), amount: removed.deposit, }); } else { T::Currency::unreserve(who, removed.deposit); } let maybe_next_best = >::mutate(|r| r.pop()).map(|next_best| { // defensive-only: Members and runners-up are disjoint. This will always be err and // give us an index to insert. if let Err(index) = members.binary_search_by(|m| m.who.cmp(&next_best.who)) { members.insert(index, next_best.clone()); } else { // overlap. This can never happen. If so, it seems like our intended replacement // is already a member, so not much more to do. log::error!(target: LOG_TARGET, "A member seems to also be a runner-up."); } next_best }); Ok(maybe_next_best) })?; let remaining_member_ids_sorted = Self::members().into_iter().map(|x| x.who).collect::>(); let outgoing = &[who.clone()]; let maybe_current_prime = T::ChangeMembers::get_prime(); let return_value = match maybe_replacement { // member ids are already sorted, other two elements have one item. Some(incoming) => { T::ChangeMembers::change_members_sorted( &[incoming.who], outgoing, &remaining_member_ids_sorted[..], ); true }, None => { T::ChangeMembers::change_members_sorted( &[], outgoing, &remaining_member_ids_sorted[..], ); false }, }; // if there was a prime before and they are not the one being removed, then set them // again. if let Some(current_prime) = maybe_current_prime { if ¤t_prime != who { T::ChangeMembers::set_prime(Some(current_prime)); } } Ok(return_value) } /// Check if `who` is a candidate. It returns the insert index if the element does not exists as /// an error. fn is_candidate(who: &T::AccountId) -> Result<(), usize> { Self::candidates().binary_search_by(|c| c.0.cmp(who)).map(|_| ()) } /// Check if `who` is a voter. It may or may not be a _current_ one. fn is_voter(who: &T::AccountId) -> bool { Voting::::contains_key(who) } /// Check if `who` is currently an active member. fn is_member(who: &T::AccountId) -> bool { Self::members().binary_search_by(|m| m.who.cmp(who)).is_ok() } /// Check if `who` is currently an active runner-up. fn is_runner_up(who: &T::AccountId) -> bool { Self::runners_up().iter().any(|r| &r.who == who) } /// Get the members' account ids. pub(crate) fn members_ids() -> Vec { Self::members().into_iter().map(|m| m.who).collect::>() } /// Get a concatenation of previous members and runners-up and their deposits. /// /// These accounts are essentially treated as candidates. fn implicit_candidates_with_deposit() -> Vec<(T::AccountId, BalanceOf)> { // invariant: these two are always without duplicates. Self::members() .into_iter() .map(|m| (m.who, m.deposit)) .chain(Self::runners_up().into_iter().map(|r| (r.who, r.deposit))) .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_err() }) } /// Remove a certain someone as a voter. fn do_remove_voter(who: &T::AccountId) { let Voter { deposit, .. } = >::take(who); // remove storage, lock and unreserve. T::Currency::remove_lock(T::PalletId::get(), who); // NOTE: we could check the deposit amount before removing and skip if zero, but it will be // a noop anyhow. let _remainder = T::Currency::unreserve(who, deposit); debug_assert!(_remainder.is_zero()); } /// 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. fn do_phragmen() -> Weight { let desired_seats = T::DesiredMembers::get() as usize; let desired_runners_up = T::DesiredRunnersUp::get() as usize; let num_to_elect = desired_runners_up + desired_seats; let mut candidates_and_deposit = Self::candidates(); // add all the previous members and runners-up as candidates as well. candidates_and_deposit.append(&mut Self::implicit_candidates_with_deposit()); if candidates_and_deposit.len().is_zero() { Self::deposit_event(Event::EmptyTerm); return T::DbWeight::get().reads(3) } // All of the new winners that come out of phragmen will thus have a deposit recorded. let candidate_ids = candidates_and_deposit.iter().map(|(x, _)| x).cloned().collect::>(); // 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); let mut num_edges: u32 = 0; let max_voters = ::MaxVoters::get() as usize; // used for prime election. let mut voters_and_stakes = Vec::new(); match Voting::::iter().try_for_each(|(voter, Voter { stake, votes, .. })| { if voters_and_stakes.len() < max_voters { voters_and_stakes.push((voter, stake, votes)); Ok(()) } else { Err(()) } }) { Ok(_) => (), Err(_) => { log::error!( target: LOG_TARGET, "Failed to run election. Number of voters exceeded", ); Self::deposit_event(Event::ElectionError); return T::DbWeight::get().reads(3 + max_voters as u64) }, } // used for phragmen. let voters_and_votes = voters_and_stakes .iter() .cloned() .map(|(voter, stake, votes)| { num_edges = num_edges.saturating_add(votes.len() as u32); (voter, to_votes(stake), votes) }) .collect::>(); let weight_candidates = candidates_and_deposit.len() as u32; let weight_voters = voters_and_votes.len() as u32; let weight_edges = num_edges; let _ = sp_npos_elections::seq_phragmen(num_to_elect, candidate_ids, voters_and_votes, None) .map(|ElectionResult:: { winners, assignments: _ }| { // this is already sorted by id. let old_members_ids_sorted = >::take() .into_iter() .map(|m| m.who) .collect::>(); // this one needs a sort by id. let mut old_runners_up_ids_sorted = >::take() .into_iter() .map(|r| r.who) .collect::>(); old_runners_up_ids_sorted.sort(); // filter out those who end up with no backing stake. let mut new_set_with_stake = winners .into_iter() .filter_map( |(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) }, ) .collect::)>>(); // OPTIMIZATION 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 favor 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.drain(..split_point).collect::>(); new_members_sorted_by_id.sort_by(|i, j| i.0.cmp(&j.0)); // all the rest will be runners-up new_set_with_stake.reverse(); let new_runners_up_sorted_by_rank = new_set_with_stake; 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(); // 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 `T::MaxVotesPerVoter` = 16) let mut prime_votes = 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)| { ((T::MaxVotesPerVoter::get() as usize - 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::>(); // report member changes. We compute diff because we need the outgoing list. let (incoming, outgoing) = T::ChangeMembers::compute_members_diff_sorted( &new_members_ids_sorted, &old_members_ids_sorted, ); T::ChangeMembers::change_members_sorted( &incoming, &outgoing, &new_members_ids_sorted, ); T::ChangeMembers::set_prime(prime); // All candidates/members/runners-up who are no longer retaining a position as a // seat holder will lose their bond. candidates_and_deposit.iter().for_each(|(c, d)| { 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, *d); T::LoserCandidate::on_unbalanced(imbalance); Self::deposit_event(Event::CandidateSlashed { candidate: c.clone(), amount: *d, }); } }); // write final values to storage. let deposit_of_candidate = |x: &T::AccountId| -> BalanceOf { // defensive-only. This closure is used against the new members and new // runners-up, both of which are phragmen winners and thus must have // deposit. candidates_and_deposit .iter() .find_map(|(c, d)| if c == x { Some(*d) } else { None }) .defensive_unwrap_or_default() }; // fetch deposits from the one recorded one. This will make sure that a // candidate who submitted candidacy before a change to candidacy deposit will // have the correct amount recorded. >::put( new_members_sorted_by_id .iter() .map(|(who, stake)| SeatHolder { deposit: deposit_of_candidate(who), who: who.clone(), stake: *stake, }) .collect::>(), ); >::put( new_runners_up_sorted_by_rank .into_iter() .map(|(who, stake)| SeatHolder { deposit: deposit_of_candidate(&who), who, stake, }) .collect::>(), ); // clean candidates. >::kill(); Self::deposit_event(Event::NewTerm { new_members: new_members_sorted_by_id }); >::mutate(|v| *v += 1); }) .map_err(|e| { log::error!(target: LOG_TARGET, "Failed to run election [{:?}].", e,); Self::deposit_event(Event::ElectionError); }); T::WeightInfo::election_phragmen(weight_candidates, weight_voters, weight_edges) } } impl Contains for Pallet { fn contains(who: &T::AccountId) -> bool { Self::is_member(who) } } impl SortedMembers for Pallet { 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(|m| m.who.cmp(who)) { Ok(_) => (), Err(pos) => { let s = SeatHolder { who: who.clone(), stake: Default::default(), deposit: Default::default(), }; members.insert(pos, s) }, }) } } impl ContainsLengthBound for Pallet { fn min_len() -> usize { 0 } /// Implementation uses a parameter type so calling is cost-free. fn max_len() -> usize { T::DesiredMembers::get() as usize } } #[cfg(any(feature = "try-runtime", test))] impl Pallet { fn do_try_state() -> Result<(), TryRuntimeError> { Self::try_state_members()?; Self::try_state_runners_up()?; Self::try_state_candidates()?; Self::try_state_candidates_runners_up_disjoint()?; Self::try_state_members_disjoint()?; Self::try_state_members_approval_stake() } /// [`Members`] state checks. Invariants: /// - Members are always sorted based on account ID. fn try_state_members() -> Result<(), TryRuntimeError> { let mut members = Members::::get().clone(); members.sort_by_key(|m| m.who.clone()); if Members::::get() == members { Ok(()) } else { Err("try_state checks: Members must be always sorted by account ID".into()) } } // [`RunnersUp`] state checks. Invariants: // - Elements are sorted based on weight (worst to best). fn try_state_runners_up() -> Result<(), TryRuntimeError> { let mut sorted = RunnersUp::::get(); // worst stake first sorted.sort_by(|a, b| a.stake.cmp(&b.stake)); if RunnersUp::::get() == sorted { Ok(()) } else { Err("try_state checks: Runners Up must always be sorted by stake (worst to best)" .into()) } } // [`Candidates`] state checks. Invariants: // - Always sorted based on account ID. fn try_state_candidates() -> Result<(), TryRuntimeError> { let mut candidates = Candidates::::get().clone(); candidates.sort_by_key(|(c, _)| c.clone()); if Candidates::::get() == candidates { Ok(()) } else { Err("try_state checks: Candidates must be always sorted by account ID".into()) } } // [`Candidates`] and [`RunnersUp`] state checks. Invariants: // - Candidates and runners-ups sets are disjoint. fn try_state_candidates_runners_up_disjoint() -> Result<(), TryRuntimeError> { match Self::intersects(&Self::candidates_ids(), &Self::runners_up_ids()) { true => Err("Candidates and runners up sets should always be disjoint".into()), false => Ok(()), } } // [`Members`], [`Candidates`] and [`RunnersUp`] state checks. Invariants: // - Members and candidates sets are disjoint; // - Members and runners-ups sets are disjoint. fn try_state_members_disjoint() -> Result<(), TryRuntimeError> { match Self::intersects(&Pallet::::members_ids(), &Self::candidates_ids()) && Self::intersects(&Pallet::::members_ids(), &Self::runners_up_ids()) { true => Err("Members set should be disjoint from candidates and runners-up sets".into()), false => Ok(()), } } // [`Members`], [`RunnersUp`] and approval stake state checks. Invariants: // - Selected members should have approval stake; // - Selected RunnersUp should have approval stake. fn try_state_members_approval_stake() -> Result<(), TryRuntimeError> { match Members::::get() .iter() .chain(RunnersUp::::get().iter()) .all(|s| s.stake != BalanceOf::::zero()) { true => Ok(()), false => Err("Members and RunnersUp must have approval stake".into()), } } fn intersects(a: &[P], b: &[P]) -> bool { a.iter().any(|e| b.contains(e)) } fn candidates_ids() -> Vec { Pallet::::candidates().iter().map(|(x, _)| x).cloned().collect::>() } fn runners_up_ids() -> Vec { Pallet::::runners_up().into_iter().map(|r| r.who).collect::>() } } #[cfg(test)] mod tests { use super::*; use crate as elections_phragmen; use frame_support::{ assert_noop, assert_ok, dispatch::DispatchResultWithPostInfo, parameter_types, traits::{ConstU32, ConstU64, OnInitialize}, }; use frame_system::ensure_signed; use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, BuildStorage, }; use substrate_test_utils::assert_eq_uvec; impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); type BlockLength = (); type DbWeight = (); type RuntimeOrigin = RuntimeOrigin; type Nonce = u64; type RuntimeCall = RuntimeCall; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = u64; type Lookup = IdentityLookup; type Block = Block; type RuntimeEvent = RuntimeEvent; type BlockHashCount = ConstU64<250>; type Version = (); type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type SS58Prefix = (); type OnSetCode = (); type MaxConsumers = ConstU32<16>; } impl pallet_balances::Config for Test { type Balance = u64; type RuntimeEvent = RuntimeEvent; type DustRemoval = (); type ExistentialDeposit = ConstU64<1>; type AccountStore = frame_system::Pallet; type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); type FreezeIdentifier = (); type MaxFreezes = (); type RuntimeHoldReason = (); type RuntimeFreezeReason = (); type MaxHolds = (); } frame_support::parameter_types! { pub static VotingBondBase: u64 = 2; pub static VotingBondFactor: u64 = 0; pub static CandidacyBond: u64 = 3; pub static DesiredMembers: u32 = 2; pub static DesiredRunnersUp: u32 = 0; 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); } fn get_prime() -> Option { PRIME.with(|p| *p.borrow()) } } parameter_types! { pub const ElectionsPhragmenPalletId: LockIdentifier = *b"phrelect"; pub const PhragmenMaxVoters: u32 = 1000; pub const PhragmenMaxCandidates: u32 = 100; } impl Config for Test { type PalletId = ElectionsPhragmenPalletId; type RuntimeEvent = RuntimeEvent; type Currency = Balances; type CurrencyToVote = (); type ChangeMembers = TestChangeMembers; type InitializeMembers = (); type CandidacyBond = CandidacyBond; type VotingBondBase = VotingBondBase; type VotingBondFactor = VotingBondFactor; type TermDuration = TermDuration; type DesiredMembers = DesiredMembers; type DesiredRunnersUp = DesiredRunnersUp; type LoserCandidate = (); type KickedMember = (); type WeightInfo = (); type MaxVoters = PhragmenMaxVoters; type MaxVotesPerVoter = ConstU32<16>; type MaxCandidates = PhragmenMaxCandidates; } pub type Block = sp_runtime::generic::Block; pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; frame_support::construct_runtime!( pub enum Test { System: frame_system::{Pallet, Call, Event}, Balances: pallet_balances::{Pallet, Call, Event, Config}, Elections: elections_phragmen::{Pallet, Call, Event, Config}, } ); pub struct ExtBuilder { balance_factor: u64, genesis_members: Vec<(u64, u64)>, } impl Default for ExtBuilder { fn default() -> Self { Self { balance_factor: 1, genesis_members: vec![] } } } impl ExtBuilder { pub fn voter_bond(self, bond: u64) -> Self { VOTING_BOND_BASE.with(|v| *v.borrow_mut() = bond); self } pub fn voter_bond_factor(self, bond: u64) -> Self { VOTING_BOND_FACTOR.with(|v| *v.borrow_mut() = bond); self } pub fn desired_runners_up(self, count: u32) -> Self { DESIRED_RUNNERS_UP.with(|v| *v.borrow_mut() = count); self } pub fn term_duration(self, duration: u64) -> Self { TERM_DURATION.with(|v| *v.borrow_mut() = duration); self } pub fn genesis_members(mut self, members: Vec<(u64, u64)>) -> Self { MEMBERS.with(|m| *m.borrow_mut() = members.iter().map(|(m, _)| *m).collect::>()); self.genesis_members = members; self } pub fn desired_members(self, count: u32) -> Self { DESIRED_MEMBERS.with(|m| *m.borrow_mut() = count); self } pub fn balance_factor(mut self, factor: u64) -> Self { self.balance_factor = factor; self } pub fn build_and_execute(self, test: impl FnOnce() -> ()) { sp_tracing::try_init_simple(); MEMBERS.with(|m| { *m.borrow_mut() = self.genesis_members.iter().map(|(m, _)| *m).collect::>() }); let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { balances: 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: elections_phragmen::GenesisConfig:: { members: self.genesis_members, }, } .build_storage() .unwrap() .into(); ext.execute_with(pre_conditions); ext.execute_with(test); #[cfg(feature = "try-runtime")] ext.execute_with(|| { assert_ok!(>::try_state( System::block_number() )); }); } } fn candidate_ids() -> Vec { Elections::candidates().into_iter().map(|(c, _)| c).collect::>() } fn candidate_deposit(who: &u64) -> u64 { Elections::candidates() .into_iter() .find_map(|(c, d)| if c == *who { Some(d) } else { None }) .unwrap_or_default() } fn voter_deposit(who: &u64) -> u64 { Elections::voting(who).deposit } fn runners_up_ids() -> Vec { Elections::runners_up().into_iter().map(|r| r.who).collect::>() } fn members_ids() -> Vec { Elections::members_ids() } fn members_and_stake() -> Vec<(u64, u64)> { Elections::members().into_iter().map(|m| (m.who, m.stake)).collect::>() } fn runners_up_and_stake() -> Vec<(u64, u64)> { Elections::runners_up() .into_iter() .map(|r| (r.who, r.stake)) .collect::>() } 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 { Balances::locks(who) .get(0) .cloned() .map(|lock| { assert_eq!(lock.id, ElectionsPhragmenPalletId::get()); lock.amount }) .unwrap_or_default() } fn locked_stake_of(who: &u64) -> u64 { Voting::::get(who).stake } fn pre_conditions() { System::set_block_number(1); Elections::do_try_state().unwrap(); } fn submit_candidacy(origin: RuntimeOrigin) -> sp_runtime::DispatchResult { Elections::submit_candidacy(origin, Elections::candidates().len() as u32) } fn vote(origin: RuntimeOrigin, votes: Vec, stake: u64) -> DispatchResultWithPostInfo { // 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).votes } #[test] fn params_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(::DesiredMembers::get(), 2); assert_eq!(::DesiredRunnersUp::get(), 0); assert_eq!(::VotingBondBase::get(), 2); assert_eq!(::VotingBondFactor::get(), 0); assert_eq!(::CandidacyBond::get(), 3); assert_eq!(::TermDuration::get(), 5); assert_eq!(Elections::election_rounds(), 0); assert!(Elections::members().is_empty()); assert!(Elections::runners_up().is_empty()); assert!(candidate_ids().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![ SeatHolder { who: 1, stake: 10, deposit: 0 }, SeatHolder { who: 2, stake: 20, deposit: 0 } ] ); assert_eq!( Elections::voting(1), Voter { stake: 10u64, votes: vec![1], deposit: 0 } ); assert_eq!( Elections::voting(2), Voter { stake: 20u64, votes: vec![2], deposit: 0 } ); // they will persist since they have self vote. System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![1, 2]); }) } #[test] fn genesis_voters_can_remove_lock() { ExtBuilder::default() .genesis_members(vec![(1, 10), (2, 20)]) .build_and_execute(|| { System::set_block_number(1); assert_eq!( Elections::voting(1), Voter { stake: 10u64, votes: vec![1], deposit: 0 } ); assert_eq!( Elections::voting(2), Voter { stake: 20u64, votes: vec![2], deposit: 0 } ); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(1))); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); assert_eq!(Elections::voting(1), Default::default()); assert_eq!(Elections::voting(2), Default::default()); }) } #[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![ SeatHolder { who: 1, stake: 10, deposit: 0 }, SeatHolder { who: 2, stake: 20, deposit: 0 }, ] ); assert_eq!( Elections::voting(1), Voter { stake: 10u64, votes: vec![1], deposit: 0 } ); assert_eq!( Elections::voting(2), Voter { stake: 20u64, votes: vec![2], deposit: 0 } ); // they will persist since they have self vote. System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(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 = "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!(::TermDuration::get(), 0); assert_eq!(::DesiredMembers::get(), 2); assert_eq!(Elections::election_rounds(), 0); assert!(members_ids().is_empty()); assert!(Elections::runners_up().is_empty()); assert!(candidate_ids().is_empty()); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert!(members_ids().is_empty()); assert!(Elections::runners_up().is_empty()); assert!(candidate_ids().is_empty()); }); } #[test] fn simple_candidate_submission_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(candidate_ids(), 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(RuntimeOrigin::signed(1))); assert_eq!(balances(&1), (7, 3)); assert_eq!(candidate_ids(), 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(RuntimeOrigin::signed(2))); assert_eq!(balances(&2), (17, 3)); assert_eq!(candidate_ids(), vec![1, 2]); assert!(Elections::is_candidate(&1).is_ok()); assert!(Elections::is_candidate(&2).is_ok()); assert_eq!(candidate_deposit(&1), 3); assert_eq!(candidate_deposit(&2), 3); assert_eq!(candidate_deposit(&3), 0); }); } #[test] fn updating_candidacy_bond_works() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_eq!(Elections::candidates(), vec![(5, 3)]); // a runtime upgrade changes the bond. CANDIDACY_BOND.with(|v| *v.borrow_mut() = 4); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_eq!(Elections::candidates(), vec![(4, 4), (5, 3)]); // once elected, they each hold their candidacy bond, no more. System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(balances(&4), (34, 6)); assert_eq!(balances(&5), (45, 5)); assert_eq!( Elections::members(), vec![ SeatHolder { who: 4, stake: 34, deposit: 4 }, SeatHolder { who: 5, stake: 45, deposit: 3 }, ] ); }) } #[test] fn candidates_are_always_sorted() { ExtBuilder::default().build_and_execute(|| { assert_eq!(candidate_ids(), Vec::::new()); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_eq!(candidate_ids(), vec![3]); assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); assert_eq!(candidate_ids(), vec![1, 3]); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_eq!(candidate_ids(), vec![1, 2, 3]); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_eq!(candidate_ids(), vec![1, 2, 3, 4]); }); } #[test] fn dupe_candidate_submission_should_not_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(candidate_ids(), Vec::::new()); assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); assert_eq!(candidate_ids(), vec![1]); assert_noop!( submit_candidacy(RuntimeOrigin::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(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![5]); assert!(Elections::runners_up().is_empty()); assert!(candidate_ids().is_empty()); assert_noop!(submit_candidacy(RuntimeOrigin::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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 4], 20)); assert_ok!(vote(RuntimeOrigin::signed(1), vec![3], 10)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![3]); assert_noop!(submit_candidacy(RuntimeOrigin::signed(3)), Error::::RunnerUpSubmit); }); } #[test] fn poor_candidate_submission_should_not_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(candidate_ids(), Vec::::new()); assert_noop!( submit_candidacy(RuntimeOrigin::signed(7)), Error::::InsufficientCandidateFunds, ); }); } #[test] fn simple_voting_should_work() { ExtBuilder::default().build_and_execute(|| { assert_eq!(candidate_ids(), Vec::::new()); assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); assert_eq!(balances(&2), (18, 2)); assert_eq!(has_lock(&2), 18); }); } #[test] fn can_vote_with_custom_stake() { ExtBuilder::default().build_and_execute(|| { assert_eq!(candidate_ids(), Vec::::new()); assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); // User only locks up to their free balance. assert_eq!(balances(&2), (18, 2)); assert_eq!(has_lock(&2), 18); assert_eq!(locked_stake_of(&2), 18); // can update; different stake; different lock and reserve. assert_ok!(vote(RuntimeOrigin::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 updated_voting_bond_works() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_eq!(balances(&2), (20, 0)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 5)); assert_eq!(balances(&2), (18, 2)); assert_eq!(voter_deposit(&2), 2); // a runtime upgrade lowers the voting bond to 1. This guy still un-reserves 2 when // leaving. VOTING_BOND_BASE.with(|v| *v.borrow_mut() = 1); // proof that bond changed. assert_eq!(balances(&1), (10, 0)); assert_ok!(vote(RuntimeOrigin::signed(1), vec![5], 5)); assert_eq!(balances(&1), (9, 1)); assert_eq!(voter_deposit(&1), 1); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); assert_eq!(balances(&2), (20, 0)); }) } #[test] fn voting_reserves_bond_per_vote() { ExtBuilder::default().voter_bond_factor(1).build_and_execute(|| { assert_eq!(balances(&2), (20, 0)); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); // initial vote. assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 10)); // 2 + 1 assert_eq!(balances(&2), (17, 3)); assert_eq!(Elections::voting(&2).deposit, 3); assert_eq!(has_lock(&2), 10); assert_eq!(locked_stake_of(&2), 10); // can update; different stake; different lock and reserve. assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 4], 15)); // 2 + 2 assert_eq!(balances(&2), (16, 4)); assert_eq!(Elections::voting(&2).deposit, 4); assert_eq!(has_lock(&2), 15); assert_eq!(locked_stake_of(&2), 15); // stay at two votes with different stake. assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 3], 18)); // 2 + 2 assert_eq!(balances(&2), (16, 4)); assert_eq!(Elections::voting(&2).deposit, 4); assert_eq!(has_lock(&2), 16); assert_eq!(locked_stake_of(&2), 16); // back to 1 vote. assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 12)); // 2 + 1 assert_eq!(balances(&2), (17, 3)); assert_eq!(Elections::voting(&2).deposit, 3); assert_eq!(has_lock(&2), 12); assert_eq!(locked_stake_of(&2), 12); }); } #[test] fn cannot_vote_for_no_candidate() { ExtBuilder::default().build_and_execute(|| { assert_noop!(vote(RuntimeOrigin::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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![4, 5], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert!(candidate_ids().is_empty()); assert_ok!(vote(RuntimeOrigin::signed(3), vec![4, 5], 10)); }); } #[test] fn prime_works() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(1), vec![4, 3], 10)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert!(candidate_ids().is_empty()); assert_ok!(vote(RuntimeOrigin::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(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(1), vec![4, 3], 10)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(4), Renouncing::Candidate(3) )); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![3, 5]); assert!(candidate_ids().is_empty()); assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); }); } #[test] fn prime_is_kept_if_other_members_leave() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Member)); assert_eq!(members_ids(), vec![5]); assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); }) } #[test] fn prime_is_gone_if_renouncing() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Member)); assert_eq!(members_ids(), vec![4]); assert_eq!(PRIME.with(|p| *p.borrow()), None); }) } #[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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_noop!( // content of the vote is irrelevant. vote(RuntimeOrigin::signed(1), vec![9, 99, 999, 9999], 5), Error::::TooManyVotes, ); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); // now we have 2 members, 1 runner-up, and 1 new candidate assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(1), vec![9, 99, 999, 9999], 5)); assert_noop!( vote(RuntimeOrigin::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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_noop!(vote(RuntimeOrigin::signed(2), vec![4], 1), Error::::LowBalance); }) } #[test] fn can_vote_for_more_than_free_balance_but_moot() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); // User has 100 free and 50 reserved. assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 150)); assert_ok!(Balances::reserve(&2, 50)); // User tries to vote with 150 tokens. assert_ok!(vote(RuntimeOrigin::signed(2), vec![4, 5], 150)); // We truncate to only their free balance, after reserving additional for voting. assert_eq!(locked_stake_of(&2), 98); assert_eq!(has_lock(&2), 98); }); } #[test] fn remove_voter_should_work() { ExtBuilder::default().voter_bond(8).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![5], 30)); assert_eq_uvec!(all_voters(), vec![2, 3]); assert_eq!(balances(&2), (12, 8)); assert_eq!(locked_stake_of(&2), 12); assert_eq!(balances(&3), (22, 8)); assert_eq!(locked_stake_of(&3), 22); assert_eq!(votes_of(&2), vec![5]); assert_eq!(votes_of(&3), vec![5]); assert_ok!(Elections::remove_voter(RuntimeOrigin::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(RuntimeOrigin::signed(3)), Error::::MustBeVoter ); }); } #[test] fn dupe_remove_should_fail() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); assert!(all_voters().is_empty()); assert_noop!( Elections::remove_voter(RuntimeOrigin::signed(2)), Error::::MustBeVoter ); }); } #[test] fn removed_voter_should_not_be_counted() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![3, 5]); }); } #[test] fn simple_voting_rounds_should_work() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 15)); assert_ok!(vote(RuntimeOrigin::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!(candidate_ids(), vec![3, 4, 5]); assert_eq!(>::decode_len().unwrap(), 3); assert_eq!(Elections::election_rounds(), 0); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(balances(&3), (25, 5)); // votes for 5 assert_eq!(balances(&2), (18, 2)); assert_eq!(members_and_stake(), vec![(3, 25), (5, 18)]); assert!(Elections::runners_up().is_empty()); assert_eq_uvec!(all_voters(), vec![2, 3, 4]); assert!(candidate_ids().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::on_initialize(System::block_number()); System::assert_last_event(RuntimeEvent::Elections(super::Event::EmptyTerm)); }) } #[test] fn all_outgoing() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); System::set_block_number(5); Elections::on_initialize(System::block_number()); System::assert_last_event(RuntimeEvent::Elections(super::Event::NewTerm { new_members: vec![(4, 35), (5, 45)], })); assert_eq!(members_and_stake(), vec![(4, 35), (5, 45)]); assert_eq!(runners_up_and_stake(), vec![]); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(5))); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); System::set_block_number(10); Elections::on_initialize(System::block_number()); System::assert_last_event(RuntimeEvent::Elections(super::Event::NewTerm { new_members: 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(RuntimeOrigin::signed(5))); // This guy's vote is pointless for this round. assert_ok!(vote(RuntimeOrigin::signed(3), vec![4], 30)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_and_stake(), vec![(5, 45)]); assert_eq!(Elections::election_rounds(), 1); // but now it has a valid target. assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); System::set_block_number(10); Elections::on_initialize(System::block_number()); // candidate 4 is affected by an old vote. assert_eq!(members_and_stake(), vec![(4, 28), (5, 45)]); 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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(Elections::election_rounds(), 1); assert_eq!(members_ids(), vec![4, 5]); }); } #[test] fn phragmen_should_not_self_vote() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert!(candidate_ids().is_empty()); assert_eq!(Elections::election_rounds(), 1); assert!(members_ids().is_empty()); System::assert_last_event(RuntimeEvent::Elections(super::Event::NewTerm { new_members: vec![], })); }); } #[test] fn runners_up_should_be_kept() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![3], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![2], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); // sorted based on account id. assert_eq!(members_ids(), vec![4, 5]); // sorted based on merit (least -> most) assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_and_stake(), vec![(4, 35), (5, 45)]); assert_eq!(runners_up_and_stake(), vec![(2, 15), (3, 25)]); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 10)); System::set_block_number(10); Elections::on_initialize(System::block_number()); assert_eq!(members_and_stake(), vec![(3, 25), (4, 35)]); assert_eq!(runners_up_and_stake(), vec![(5, 10), (2, 15)]); }); } #[test] fn runners_up_lose_bond_once_outgoing() { ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![2]); assert_eq!(balances(&2), (15, 5)); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); System::set_block_number(10); Elections::on_initialize(System::block_number()); assert_eq!(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(RuntimeOrigin::signed(5))); assert_eq!(balances(&5), (47, 3)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_eq!(balances(&5), (45, 5)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![5]); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(5))); assert_eq!(balances(&5), (47, 3)); System::set_block_number(10); Elections::on_initialize(System::block_number()); assert!(members_ids().is_empty()); assert_eq!(balances(&5), (47, 0)); }); } #[test] fn candidates_lose_the_bond_when_outgoing() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); assert_eq!(balances(&5), (47, 3)); assert_eq!(balances(&3), (27, 3)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); // 5 will persist as candidates despite not being in the list. assert_eq!(candidate_ids(), vec![2, 3]); System::set_block_number(10); Elections::on_initialize(System::block_number()); // 4 removed; 5 and 3 are the new best. assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); let check_at_block = |b: u32| { System::set_block_number(b.into()); Elections::on_initialize(System::block_number()); // we keep re-electing the same folks. assert_eq!(members_and_stake(), vec![(4, 35), (5, 45)]); assert_eq!(runners_up_and_stake(), vec![(2, 15), (3, 25)]); // no new candidates but old members and runners-up are always added. assert!(candidate_ids().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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(Elections::election_rounds(), 1); // a new candidate assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(Elections::remove_member(RuntimeOrigin::root(), 4, true, true)); assert_eq!(balances(&4), (35, 2)); // slashed assert_eq!(Elections::election_rounds(), 2); // new election round assert_eq!(members_ids(), vec![3, 5]); // new members }); } #[test] fn seats_should_be_released_when_no_vote() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![3], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_eq!(>::decode_len().unwrap(), 3); assert_eq!(Elections::election_rounds(), 0); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![3, 5]); assert_eq!(Elections::election_rounds(), 1); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(3))); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(5))); // meanwhile, no one cares to become a candidate again. System::set_block_number(10); Elections::on_initialize(System::block_number()); assert!(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(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); // 5 will change their vote and becomes an `outgoing` assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 8)); // 4 will stay in the set assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); // 3 will become a winner assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); // these two are losers. assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); assert_ok!(vote(RuntimeOrigin::signed(1), vec![1], 10)); System::set_block_number(10); Elections::on_initialize(System::block_number()); // 3, 4 are new members, must still be bonded, nothing slashed. assert_eq!(members_and_stake(), vec![(3, 25), (4, 43)]); 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)); System::assert_has_event(RuntimeEvent::Elections(super::Event::NewTerm { new_members: vec![(4, 35), (5, 45)], })); }) } #[test] fn invalid_votes_are_moot() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![10], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq_uvec!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![3], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![2], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); // id: low -> high. assert_eq!(members_and_stake(), vec![(4, 45), (5, 35)]); // merit: low -> high. assert_eq!(runners_up_and_stake(), vec![(3, 15), (2, 25)]); }); } #[test] fn runner_up_replacement_maintains_members_order() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![2], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![2, 4]); assert_ok!(Elections::remove_member(RuntimeOrigin::root(), 2, true, false)); assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![2, 3]); assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Member)); assert_eq!(balances(&4), (38, 2)); // 2 is voting bond. assert_eq!(members_ids(), vec![3, 5]); assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert!(runners_up_ids().is_empty()); assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Member)); assert_eq!(balances(&4), (38, 2)); // 2 is voting bond. // no replacement assert_eq!(members_ids(), vec![5]); assert!(runners_up_ids().is_empty()); }) } #[test] fn can_renounce_candidacy_runner_up() { ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![2, 3]); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(3), Renouncing::RunnerUp )); assert_eq!(balances(&3), (28, 2)); // 2 is voting bond. assert_eq!(members_ids(), vec![4, 5]); assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(5), vec![2], 50)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![2, 4]); assert_eq!(runners_up_ids(), vec![5, 3]); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(3), Renouncing::RunnerUp )); assert_eq!(members_ids(), vec![2, 4]); assert_eq!(runners_up_ids(), vec![5]); }); } #[test] fn can_renounce_candidacy_candidate() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_eq!(balances(&5), (47, 3)); assert_eq!(candidate_ids(), vec![5]); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(5), Renouncing::Candidate(1) )); assert_eq!(balances(&5), (50, 0)); assert!(candidate_ids().is_empty()); }) } #[test] fn wrong_renounce_candidacy_should_fail() { ExtBuilder::default().build_and_execute(|| { assert_noop!( Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Candidate(0)), Error::::InvalidRenouncing, ); assert_noop!( Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Member), Error::::InvalidRenouncing, ); assert_noop!( Elections::renounce_candidacy(RuntimeOrigin::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(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![3]); assert_noop!( Elections::renounce_candidacy(RuntimeOrigin::signed(3), Renouncing::Member), Error::::InvalidRenouncing, ); }) } #[test] fn non_runner_up_renounce_runner_up_should_fail() { ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![3]); assert_noop!( Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::RunnerUp), Error::::InvalidRenouncing, ); }) } #[test] fn wrong_candidate_count_renounce_should_fail() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_noop!( Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Candidate(2)), Error::::InvalidWitnessData, ); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(4), Renouncing::Candidate(3) )); }) } #[test] fn renounce_candidacy_count_can_overestimate() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); // while we have only 3 candidates. assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(4), Renouncing::Candidate(4) )); }) } #[test] fn unsorted_runners_up_are_detected() { ExtBuilder::default() .desired_runners_up(2) .desired_members(1) .build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 5)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 15)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![5]); assert_eq!(runners_up_ids(), vec![4, 3]); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 10)); System::set_block_number(10); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![5]); assert_eq!(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(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4]); assert_eq!(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(RuntimeOrigin::signed(5))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); System::set_block_number(10); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![5]); assert_eq!(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(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4]); assert_eq!(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(RuntimeOrigin::signed(4), vec![2], 40)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 20)); System::set_block_number(10); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![2]); assert_eq!(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)); }); } #[test] fn remove_and_replace_member_works() { let setup = || { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![4, 5]); assert_eq!(runners_up_ids(), vec![3]); }; // member removed, replacement found. ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { setup(); assert_eq!(Elections::remove_and_replace_member(&4, false), Ok(true)); assert_eq!(members_ids(), vec![3, 5]); assert_eq!(runners_up_ids().len(), 0); }); // member removed, no replacement found. ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { setup(); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(3), Renouncing::RunnerUp )); assert_eq!(Elections::remove_and_replace_member(&4, false), Ok(false)); assert_eq!(members_ids(), vec![5]); assert_eq!(runners_up_ids().len(), 0); }); // wrong member to remove. ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { setup(); assert!(matches!(Elections::remove_and_replace_member(&2, false), Err(_))); }); } #[test] fn no_desired_members() { // not interested in anything ExtBuilder::default() .desired_members(0) .desired_runners_up(0) .build_and_execute(|| { assert_eq!(Elections::candidates().len(), 0); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_eq!(Elections::candidates().len(), 3); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids().len(), 0); assert_eq!(runners_up_ids().len(), 0); assert_eq!(all_voters().len(), 3); assert_eq!(Elections::candidates().len(), 0); }); // not interested in members ExtBuilder::default() .desired_members(0) .desired_runners_up(2) .build_and_execute(|| { assert_eq!(Elections::candidates().len(), 0); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_eq!(Elections::candidates().len(), 3); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids().len(), 0); assert_eq!(runners_up_ids(), vec![3, 4]); assert_eq!(all_voters().len(), 3); assert_eq!(Elections::candidates().len(), 0); }); // not interested in runners-up ExtBuilder::default() .desired_members(2) .desired_runners_up(0) .build_and_execute(|| { assert_eq!(Elections::candidates().len(), 0); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_eq!(Elections::candidates().len(), 3); assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![3, 4]); assert_eq!(runners_up_ids().len(), 0); assert_eq!(all_voters().len(), 3); assert_eq!(Elections::candidates().len(), 0); }); } #[test] fn dupe_vote_is_moot() { ExtBuilder::default().desired_members(1).build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); // all these duplicate votes will not cause 2 to win. assert_ok!(vote(RuntimeOrigin::signed(1), vec![2, 2, 2, 2], 5)); assert_ok!(vote(RuntimeOrigin::signed(2), vec![2, 2, 2, 2], 20)); assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); System::set_block_number(5); Elections::on_initialize(System::block_number()); assert_eq!(members_ids(), vec![3]); }) } #[test] fn remove_defunct_voter_works() { ExtBuilder::default().build_and_execute(|| { assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); // defunct assert_ok!(vote(RuntimeOrigin::signed(5), vec![5, 4], 5)); // defunct assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 5)); // ok assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 5)); // ok assert_ok!(vote(RuntimeOrigin::signed(2), vec![3, 4], 5)); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(5), Renouncing::Candidate(3) )); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(4), Renouncing::Candidate(2) )); assert_ok!(Elections::renounce_candidacy( RuntimeOrigin::signed(3), Renouncing::Candidate(1) )); assert_ok!(Elections::clean_defunct_voters(RuntimeOrigin::root(), 4, 2)); }) } }