diff --git a/substrate/srml/council/src/seats.rs b/substrate/srml/council/src/seats.rs index bff476695b0e785ed374ffc9894ef9db437f3803..54579e356e3b5c394b537db754901ce86eeb5722 100644 --- a/substrate/srml/council/src/seats.rs +++ b/substrate/srml/council/src/seats.rs @@ -94,9 +94,16 @@ decl_module! { fn set_approvals(origin, votes: Vec<bool>, index: Compact<VoteIndex>) { let who = ensure_signed(origin)?; let index: VoteIndex = index.into(); + let candidates = Self::candidates(); ensure!(!Self::presentation_active(), "no approval changes during presentation period"); ensure!(index == Self::vote_index(), "incorrect vote index"); + ensure!(!candidates.len().is_zero(), "amount of candidates to receive approval votes should be non-zero"); + // Prevent a vote from voters that provide a list of votes that exceeds the candidates length + // since otherise an attacker may be able to submit a very long list of `votes` that far exceeds + // the amount of candidates and waste more computation than a reasonable voting bond would cover. + ensure!(candidates.len() >= votes.len(), "amount of candidate approval votes cannot exceed amount of candidates"); + if !<LastActiveOf<T>>::exists(&who) { // not yet a voter - deduct bond. // NOTE: this must be the last potential bailer, since it changes state. @@ -132,7 +139,7 @@ decl_module! { ensure!(Self::voter_last_active(&reporter).is_some(), "reporter must be a voter"); let last_active = Self::voter_last_active(&who).ok_or("target for inactivity cleanup must be active")?; ensure!(assumed_vote_index == Self::vote_index(), "vote index not current"); - ensure!(last_active < assumed_vote_index - Self::inactivity_grace_period(), "cannot reap during grace perid"); + ensure!(assumed_vote_index > last_active + Self::inactivity_grace_period(), "cannot reap during grace period"); let voters = Self::voters(); let reporter_index: u32 = reporter_index.into(); let reporter_index = reporter_index as usize; @@ -157,7 +164,7 @@ decl_module! { voters ); if valid { - // This only fails if `who` doesn't exist, which it clearly must do since its the origin. + // This only fails if `reporter` doesn't exist, which it clearly must do since its the origin. // Still, it's no more harmful to propagate any error at this point. <balances::Module<T>>::repatriate_reserved(&who, &reporter, Self::voting_bond())?; Self::deposit_event(RawEvent::VoterReaped(who, reporter)); @@ -225,6 +232,7 @@ decl_module! { ) -> Result { let who = ensure_signed(origin)?; let total = total.into(); + ensure!(!total.is_zero(), "stake deposited to present winner and be added to leaderboard should be non-zero"); let index: VoteIndex = index.into(); let candidate = <balances::Module<T>>::lookup(candidate)?; @@ -324,7 +332,7 @@ decl_storage! { pub CarryCount get(carry_count) config(): u32 = 2; /// How long to give each top candidate to present themselves after the vote ends. pub PresentationDuration get(presentation_duration) config(): T::BlockNumber = T::BlockNumber::sa(1000); - /// How many votes need to go by after a voter's last vote before they can be reaped if their + /// How many vote indexes need to go by after a target voter's last vote before they can be reaped if their /// approvals are moot. pub InactiveGracePeriod get(inactivity_grace_period) config(inactive_grace_period): VoteIndex = 1; /// How often (in blocks) to check for new votes. @@ -336,13 +344,16 @@ decl_storage! { // permanent state (always relevant, changes only at the finalisation of voting) /// The current council. When there's a vote going on, this should still be used for executive - /// matters. + /// matters. The block number (second element in the tuple) is the block that their position is + /// active until (calculated by the sum of the block number when the council member was elected + /// and their term duration). pub ActiveCouncil get(active_council) config(): Vec<(T::AccountId, T::BlockNumber)>; /// The total number of votes that have happened or are in progress. pub VoteCount get(vote_index): VoteIndex; // persistent state (always relevant, changes constantly) - /// The last cleared vote index that this voter was last active at. + /// A list of votes for each voter, respecting the last cleared vote index that this voter was + /// last active at. pub ApprovalsOf get(approvals_of): map T::AccountId => Vec<bool>; /// The vote index and list slot that the candidate `who` was registered or `None` if they are not /// currently registered. @@ -459,8 +470,9 @@ impl<T: Trait> Module<T> { let desired_seats = Self::desired_seats() as usize; let number = <system::Module<T>>::block_number(); let expiring = active_council.iter().take_while(|i| i.1 == number).map(|i| i.0.clone()).collect::<Vec<_>>(); - if active_council.len() - expiring.len() < desired_seats { - let empty_seats = desired_seats - (active_council.len() - expiring.len()); + let retaining_seats = active_council.len() - expiring.len(); + if retaining_seats < desired_seats { + let empty_seats = desired_seats - retaining_seats; <NextFinalise<T>>::put((number + Self::presentation_duration(), empty_seats as u32, expiring)); let voters = Self::voters(); @@ -558,6 +570,7 @@ mod tests { assert_eq!(Council::voting_bond(), 3); assert_eq!(Council::present_slash_per_voter(), 1); assert_eq!(Council::presentation_duration(), 2); + assert_eq!(Council::inactivity_grace_period(), 1); assert_eq!(Council::voting_period(), 4); assert_eq!(Council::term_duration(), 5); assert_eq!(Council::desired_seats(), 2); @@ -723,6 +736,29 @@ mod tests { }); } + #[test] + fn setting_any_approval_vote_count_without_any_candidate_count_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + + assert_eq!(Council::candidates().len(), 0); + + assert_noop!(Council::set_approvals(Origin::signed(4), vec![], 0.into()), "amount of candidates to receive approval votes should be non-zero"); + }); + } + + #[test] + fn setting_an_approval_vote_count_more_than_candidate_count_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(1); + + assert_ok!(Council::submit_candidacy(Origin::signed(5), 0.into())); + assert_eq!(Council::candidates().len(), 1); + + assert_noop!(Council::set_approvals(Origin::signed(4), vec![true, true], 0.into()), "amount of candidate approval votes cannot exceed amount of candidates"); + }); + } + #[test] fn resubmitting_voting_should_work() { with_externalities(&mut new_test_ext(false), || { @@ -735,6 +771,7 @@ mod tests { assert_ok!(Council::submit_candidacy(Origin::signed(2), 1.into())); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2.into())); + assert_eq!(Council::candidates().len(), 3); assert_ok!(Council::set_approvals(Origin::signed(4), vec![true, false, true], 0.into())); assert_eq!(Council::approvals_of(4), vec![true, false, true]); @@ -749,6 +786,7 @@ mod tests { assert_ok!(Council::submit_candidacy(Origin::signed(5), 0.into())); assert_ok!(Council::submit_candidacy(Origin::signed(2), 1.into())); assert_ok!(Council::submit_candidacy(Origin::signed(3), 2.into())); + assert_eq!(Council::candidates().len(), 3); assert_ok!(Council::set_approvals(Origin::signed(1), vec![true], 0.into())); assert_ok!(Council::set_approvals(Origin::signed(2), vec![false, true, true], 0.into())); @@ -853,6 +891,19 @@ mod tests { }); } + #[test] + fn presentations_with_zero_staked_deposit_should_not_work() { + with_externalities(&mut new_test_ext(false), || { + System::set_block_number(4); + assert_ok!(Council::submit_candidacy(Origin::signed(2), 0.into())); + assert_ok!(Council::set_approvals(Origin::signed(2), vec![true], 0.into())); + assert_ok!(Council::end_block(System::block_number())); + + System::set_block_number(6); + assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 0.into(), 0.into()), "stake deposited to present winner and be added to leaderboard should be non-zero"); + }); + } + #[test] fn double_presentations_should_be_punished() { with_externalities(&mut new_test_ext(false), || { @@ -1057,9 +1108,15 @@ mod tests { assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30.into(), 1.into())); assert_ok!(Council::end_block(System::block_number())); + assert_eq!(Council::vote_index(), 2); + assert_eq!(Council::inactivity_grace_period(), 1); + assert_eq!(Council::voting_period(), 4); + assert_eq!(Council::voter_last_active(4), Some(0)); + assert_ok!(Council::reap_inactive_voter(Origin::signed(4), (Council::voters().iter().position(|&i| i == 4).unwrap() as u32).into(), - 2.into(), (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), + 2.into(), + (Council::voters().iter().position(|&i| i == 2).unwrap() as u32).into(), 2.into() )); @@ -1119,6 +1176,14 @@ mod tests { assert_ok!(Council::present_winner(Origin::signed(4), 3.into(), 30.into(), 0.into())); assert_ok!(Council::present_winner(Origin::signed(4), 4.into(), 40.into(), 0.into())); assert_ok!(Council::present_winner(Origin::signed(4), 5.into(), 50.into(), 0.into())); + + assert_eq!(Council::leaderboard(), Some(vec![ + (30, 3), + (40, 4), + (50, 5), + (60, 1) + ])); + assert_noop!(Council::present_winner(Origin::signed(4), 2.into(), 20.into(), 0.into()), "candidate not worthy of leaderboard"); }); } @@ -1240,6 +1305,8 @@ mod tests { System::set_block_number(6); assert!(Council::presentation_active()); assert_ok!(Council::present_winner(Origin::signed(4), 1.into(), 60.into(), 0.into())); + // leaderboard length is the empty seats plus the carry count (i.e. 5 + 2), where those + // to be carried are the lowest and stored in lowest indexes assert_eq!(Council::leaderboard(), Some(vec![ (0, 0), (0, 0),