From 982a85b9b802a856a0a8ddbd0a4f55b834750e2d Mon Sep 17 00:00:00 2001 From: Lauro Gripa Date: Thu, 4 Jan 2024 08:37:34 -0300 Subject: [PATCH] Fix vote weights of ranked members in the Society pallet (#2758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes a bug in the tally accrual of approvals/rejections for Candidates and Defender. The issue happened because: - The `match maybe_old` is reducing `weight` from the tally: ``` Some(Vote { approve: true, weight }) => tally.approvals.saturating_reduce(weight), Some(Vote { approve: false, weight }) => tally.rejections.saturating_reduce(weight), ``` - But `match approval` is accruing only `1` to the tally: ``` true => tally.approvals.saturating_accrue(1), false => tally.rejections.saturating_accrue(1), ``` This way, if a member is rank 1 his vote is going to have weight 1 when accruing but weight 4 when reducing from the tally. For example, let's say: - There's a Candidate with 0 approvals and 12 rejections; - A ranked Member votes against the Candidate; - The tally changes to 0 approvals and 13 rejections (should be 16); - The Member changes his vote to an approval; - Now tally changes to 1 approvals and 9 rejections, removing the accrued approvals from other Members; - If the Member keeps changing his vote, it wipes the tally clean. So this PR changes `match approval` to accrue `weight` instead of just `1` and changes the tests: - Fixes `challenges_work`. This test started failing after the fix. The reason is that the test assumes that all Members have equal weights to their votes, but Member 10 is ranked, so his vote should have weight 4 against the Defender. So instead of using Member 10, I added Member 50 of rank 0 to keep the same logic; - Improves `votes_are_working`. Added some assertions to check if the tally is correct even after a ranked Member changes his vote a couple times; - Fixes `waive_repay_works`. Unrelated to the bug, but this test was yielding a false positive. The test is ranking up Member 20, but asserting the rank of Member 10, which is already ranked up. --------- Co-authored-by: Bastian Köcher Co-authored-by: command-bot <> --- substrate/frame/society/src/lib.rs | 4 +- substrate/frame/society/src/tests.rs | 83 +++++++++++++++++++--------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/substrate/frame/society/src/lib.rs b/substrate/frame/society/src/lib.rs index ca8d96e193c84..99dd580a4035e 100644 --- a/substrate/frame/society/src/lib.rs +++ b/substrate/frame/society/src/lib.rs @@ -1433,8 +1433,8 @@ impl, I: 'static> Pallet { let weight_root = rank + 1; let weight = weight_root * weight_root; match approve { - true => tally.approvals.saturating_accrue(1), - false => tally.rejections.saturating_accrue(1), + true => tally.approvals.saturating_accrue(weight), + false => tally.rejections.saturating_accrue(weight), } Vote { approve, weight } } diff --git a/substrate/frame/society/src/tests.rs b/substrate/frame/society/src/tests.rs index ea2afef3b32b5..2163575a86080 100644 --- a/substrate/frame/society/src/tests.rs +++ b/substrate/frame/society/src/tests.rs @@ -720,7 +720,7 @@ fn unvouch_works() { // But their pick doesn't resign (yet). conclude_intake(false, None); // Voting still happening and voucher cannot unvouch. - assert_eq!(candidacies(), vec![(20, candidacy(1, 100, Vouch(10, 0), 0, 1))]); + assert_eq!(candidacies(), vec![(20, candidacy(1, 100, Vouch(10, 0), 0, 4))]); assert_eq!(Members::::get(10).unwrap().vouching, Some(VouchingStatus::Vouching)); // Candidate gives in and resigns. @@ -836,72 +836,72 @@ fn founder_and_head_cannot_be_removed() { fn challenges_work() { EnvBuilder::new().execute(|| { // Add some members - place_members([20, 30, 40]); + place_members([20, 30, 40, 50]); // Votes are empty - assert_eq!(DefenderVotes::::get(0, 10), None); assert_eq!(DefenderVotes::::get(0, 20), None); assert_eq!(DefenderVotes::::get(0, 30), None); assert_eq!(DefenderVotes::::get(0, 40), None); + assert_eq!(DefenderVotes::::get(0, 50), None); // Check starting point - assert_eq!(members(), vec![10, 20, 30, 40]); + assert_eq!(members(), vec![10, 20, 30, 40, 50]); assert_eq!(Defending::::get(), None); - // 30 will be challenged during the challenge rotation + // 40 will be challenged during the challenge rotation next_challenge(); - assert_eq!(Defending::::get().unwrap().0, 30); + assert_eq!(Defending::::get().unwrap().0, 40); // They can always free vote for themselves - assert_ok!(Society::defender_vote(Origin::signed(30), true)); + assert_ok!(Society::defender_vote(Origin::signed(40), true)); // If no one else votes, nothing happens next_challenge(); - assert_eq!(members(), vec![10, 20, 30, 40]); + assert_eq!(members(), vec![10, 20, 30, 40, 50]); // Reset votes for last challenge assert_ok!(Society::cleanup_challenge(Origin::signed(0), 0, 10)); - // New challenge period - assert_eq!(Defending::::get().unwrap().0, 30); + // New challenge period, 20 is challenged + assert_eq!(Defending::::get().unwrap().0, 20); // Non-member cannot vote assert_noop!(Society::defender_vote(Origin::signed(1), true), Error::::NotMember); // 3 people say accept, 1 reject - assert_ok!(Society::defender_vote(Origin::signed(10), true)); assert_ok!(Society::defender_vote(Origin::signed(20), true)); assert_ok!(Society::defender_vote(Origin::signed(30), true)); - assert_ok!(Society::defender_vote(Origin::signed(40), false)); + assert_ok!(Society::defender_vote(Origin::signed(40), true)); + assert_ok!(Society::defender_vote(Origin::signed(50), false)); next_challenge(); - // 30 survives - assert_eq!(members(), vec![10, 20, 30, 40]); + // 20 survives + assert_eq!(members(), vec![10, 20, 30, 40, 50]); // Reset votes for last challenge assert_ok!(Society::cleanup_challenge(Origin::signed(0), 1, 10)); // Votes are reset - assert_eq!(DefenderVotes::::get(0, 10), None); assert_eq!(DefenderVotes::::get(0, 20), None); assert_eq!(DefenderVotes::::get(0, 30), None); assert_eq!(DefenderVotes::::get(0, 40), None); + assert_eq!(DefenderVotes::::get(0, 50), None); - // One more time - assert_eq!(Defending::::get().unwrap().0, 30); + // One more time, 40 is challenged + assert_eq!(Defending::::get().unwrap().0, 40); // 2 people say accept, 2 reject - assert_ok!(Society::defender_vote(Origin::signed(10), true)); assert_ok!(Society::defender_vote(Origin::signed(20), true)); - assert_ok!(Society::defender_vote(Origin::signed(30), false)); + assert_ok!(Society::defender_vote(Origin::signed(30), true)); assert_ok!(Society::defender_vote(Origin::signed(40), false)); + assert_ok!(Society::defender_vote(Origin::signed(50), false)); next_challenge(); - // 30 is suspended - assert_eq!(members(), vec![10, 20, 40]); + // 40 is suspended + assert_eq!(members(), vec![10, 20, 30, 50]); assert_eq!( - SuspendedMembers::::get(30), - Some(MemberRecord { rank: 0, strikes: 0, vouching: None, index: 2 }) + SuspendedMembers::::get(40), + Some(MemberRecord { rank: 0, strikes: 0, vouching: None, index: 3 }) ); // Reset votes for last challenge assert_ok!(Society::cleanup_challenge(Origin::signed(0), 2, 10)); - // New defender is chosen - assert_eq!(Defending::::get().unwrap().0, 20); + // New defender is chosen, 30 is challenged + assert_eq!(Defending::::get().unwrap().0, 30); // Votes are reset - assert_eq!(DefenderVotes::::get(0, 10), None); assert_eq!(DefenderVotes::::get(0, 20), None); assert_eq!(DefenderVotes::::get(0, 30), None); assert_eq!(DefenderVotes::::get(0, 40), None); + assert_eq!(DefenderVotes::::get(0, 50), None); }); } @@ -1044,6 +1044,9 @@ fn vouching_handles_removed_member_with_candidate() { fn votes_are_working() { EnvBuilder::new().execute(|| { place_members([20]); + // Member 10 is rank 1 and Member 20 is rank 0 + assert_eq!(Members::::get(10).unwrap().rank, 1); + assert_eq!(Members::::get(20).unwrap().rank, 0); // Users make bids of various amounts assert_ok!(Society::bid(RuntimeOrigin::signed(50), 500)); assert_ok!(Society::bid(RuntimeOrigin::signed(40), 400)); @@ -1061,6 +1064,32 @@ fn votes_are_working() { assert_eq!(Votes::::get(30, 20), Some(Vote { approve: true, weight: 1 })); assert_eq!(Votes::::get(40, 10), Some(Vote { approve: true, weight: 4 })); assert_eq!(Votes::::get(50, 10), None); + + // Votes have correct weights on the tally + assert_eq!( + Candidates::::get(30).unwrap().tally, + Tally { approvals: 5, rejections: 0 } + ); + assert_eq!( + Candidates::::get(40).unwrap().tally, + Tally { approvals: 4, rejections: 0 } + ); + // Member 10 changes his vote for Candidate 30 + assert_ok!(Society::vote(Origin::signed(10), 30, false)); + // Assert the tally calculation is correct + assert_eq!( + Candidates::::get(30).unwrap().tally, + Tally { approvals: 1, rejections: 4 } + ); + // Member 10 changes his vote again + assert_ok!(Society::vote(Origin::signed(10), 30, true)); + // Assert the tally is still correct + assert_eq!( + Candidates::::get(30).unwrap().tally, + Tally { approvals: 5, rejections: 0 } + ); + + // Finish intake conclude_intake(false, None); // Cleanup the candidacy assert_ok!(Society::cleanup_candidacy(Origin::signed(0), 30, 10)); @@ -1240,7 +1269,7 @@ fn waive_repay_works() { Payouts::::get(20), PayoutRecord { paid: 0, payouts: vec![].try_into().unwrap() } ); - assert_eq!(Members::::get(10).unwrap().rank, 1); + assert_eq!(Members::::get(20).unwrap().rank, 1); assert_eq!(Balances::free_balance(20), 50); }); }