diff --git a/frame/ranked-collective/src/lib.rs b/frame/ranked-collective/src/lib.rs index 288fd78d6e718..6296403d2a1ce 100644 --- a/frame/ranked-collective/src/lib.rs +++ b/frame/ranked-collective/src/lib.rs @@ -112,36 +112,39 @@ impl, I: 'static, M: GetMaxVoters> Tally { pub type TallyOf = Tally>; pub type PollIndexOf = <>::Polls as Polling>>::Index; +pub type ClassOf = <>::Polls as Polling>>::Class; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; -impl, I: 'static, M: GetMaxVoters> VoteTally for Tally { - fn new(_: Rank) -> Self { +impl, I: 'static, M: GetMaxVoters>> + VoteTally> for Tally +{ + fn new(_: ClassOf) -> Self { Self { bare_ayes: 0, ayes: 0, nays: 0, dummy: PhantomData } } - fn ayes(&self, _: Rank) -> Votes { + fn ayes(&self, _: ClassOf) -> Votes { self.bare_ayes } - fn support(&self, class: Rank) -> Perbill { + fn support(&self, class: ClassOf) -> Perbill { Perbill::from_rational(self.bare_ayes, M::get_max_voters(class)) } - fn approval(&self, _: Rank) -> Perbill { + fn approval(&self, _: ClassOf) -> Perbill { Perbill::from_rational(self.ayes, 1.max(self.ayes + self.nays)) } #[cfg(feature = "runtime-benchmarks")] - fn unanimity(class: Rank) -> Self { + fn unanimity(class: ClassOf) -> Self { Self { - bare_ayes: M::get_max_voters(class), + bare_ayes: M::get_max_voters(class.clone()), ayes: M::get_max_voters(class), nays: 0, dummy: PhantomData, } } #[cfg(feature = "runtime-benchmarks")] - fn rejection(class: Rank) -> Self { + fn rejection(class: ClassOf) -> Self { Self { bare_ayes: 0, ayes: 0, nays: M::get_max_voters(class), dummy: PhantomData } } #[cfg(feature = "runtime-benchmarks")] - fn from_requirements(support: Perbill, approval: Perbill, class: Rank) -> Self { + fn from_requirements(support: Perbill, approval: Perbill, class: ClassOf) -> Self { let c = M::get_max_voters(class); let ayes = support * c; let nays = ((ayes as u64) * 1_000_000_000u64 / approval.deconstruct() as u64) as u32 - ayes; @@ -149,14 +152,17 @@ impl, I: 'static, M: GetMaxVoters> VoteTally for Tally } #[cfg(feature = "runtime-benchmarks")] - fn setup(class: Rank, granularity: Perbill) { - if M::get_max_voters(class) == 0 { + fn setup(class: ClassOf, granularity: Perbill) { + if M::get_max_voters(class.clone()) == 0 { let max_voters = granularity.saturating_reciprocal_mul(1u32); for i in 0..max_voters { let who: T::AccountId = frame_benchmarking::account("ranked_collective_benchmarking", i, 0); - crate::Pallet::::do_add_member_to_rank(who, class) - .expect("could not add members for benchmarks"); + crate::Pallet::::do_add_member_to_rank( + who, + T::MinRankOfClass::convert(class.clone()), + ) + .expect("could not add members for benchmarks"); } assert_eq!(M::get_max_voters(class), max_voters); } @@ -234,14 +240,17 @@ impl Convert for Geometric { } } -/// Trait for getting the maximum number of voters for a given rank. +/// Trait for getting the maximum number of voters for a given poll class. pub trait GetMaxVoters { - /// Return the maximum number of voters for the rank `r`. - fn get_max_voters(r: Rank) -> MemberIndex; + /// Poll class type. + type Class; + /// Return the maximum number of voters for the poll class `c`. + fn get_max_voters(c: Self::Class) -> MemberIndex; } impl, I: 'static> GetMaxVoters for Pallet { - fn get_max_voters(r: Rank) -> MemberIndex { - MemberCount::::get(r) + type Class = ClassOf; + fn get_max_voters(c: Self::Class) -> MemberIndex { + MemberCount::::get(T::MinRankOfClass::convert(c)) } } @@ -346,7 +355,7 @@ pub mod pallet { /// Convert the tally class into the minimum rank required to vote on the poll. If /// `Polls::Class` is the same type as `Rank`, then `Identity` can be used here to mean /// "a rank of at least the poll class". - type MinRankOfClass: Convert<>>::Class, Rank>; + type MinRankOfClass: Convert, Rank>; /// Convert a rank_delta into a number of votes the rank gets. /// diff --git a/frame/ranked-collective/src/tests.rs b/frame/ranked-collective/src/tests.rs index 91244a90b0b6a..04519bc0f8e22 100644 --- a/frame/ranked-collective/src/tests.rs +++ b/frame/ranked-collective/src/tests.rs @@ -25,10 +25,10 @@ use frame_support::{ parameter_types, traits::{ConstU16, ConstU32, ConstU64, EitherOf, Everything, MapSuccess, Polling}, }; -use sp_core::H256; +use sp_core::{Get, H256}; use sp_runtime::{ testing::Header, - traits::{BlakeTwo256, Identity, IdentityLookup, ReduceBy}, + traits::{BlakeTwo256, IdentityLookup, ReduceBy}, }; use super::*; @@ -36,6 +36,7 @@ use crate as pallet_ranked_collective; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; +type Class = Rank; frame_support::construct_runtime!( pub enum Test where @@ -95,7 +96,7 @@ impl Polling> for TestPolls { type Index = u8; type Votes = Votes; type Moment = u64; - type Class = Rank; + type Class = Class; fn classes() -> Vec { vec![0, 1, 2] } @@ -164,6 +165,19 @@ impl Polling> for TestPolls { } } +/// Convert the tally class into the minimum rank required to vote on the poll. +/// MinRank(Class) = Class - Delta +pub struct MinRankOfClass(PhantomData); +impl> Convert for MinRankOfClass { + fn convert(a: Class) -> Rank { + a.saturating_sub(Delta::get()) + } +} + +parameter_types! { + pub static MinRankOfClassDelta: Rank = 0; +} + impl Config for Test { type WeightInfo = (); type RuntimeEvent = RuntimeEvent; @@ -180,7 +194,7 @@ impl Config for Test { MapSuccess, ReduceBy>>, >; type Polls = TestPolls; - type MinRankOfClass = Identity; + type MinRankOfClass = MinRankOfClass; type VoteWeight = Geometric; } @@ -499,3 +513,43 @@ fn do_add_member_to_rank_works() { assert_eq!(member_count(max_rank + 1), 0); }) } + +#[test] +fn tally_support_correct() { + new_test_ext().execute_with(|| { + // add members, + // rank 1: accounts 1, 2, 3 + // rank 2: accounts 2, 3 + // rank 3: accounts 3. + assert_ok!(Club::add_member(RuntimeOrigin::root(), 1)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 1)); + assert_ok!(Club::add_member(RuntimeOrigin::root(), 2)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 2)); + assert_ok!(Club::add_member(RuntimeOrigin::root(), 3)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); + assert_ok!(Club::promote_member(RuntimeOrigin::root(), 3)); + + // init tally with 1 aye vote. + let tally: TallyOf = Tally::from_parts(1, 1, 0); + + // with minRank(Class) = Class + // for class 3, 100% support. + MinRankOfClassDelta::set(0); + assert_eq!(tally.support(3), Perbill::from_rational(1u32, 1)); + + // with minRank(Class) = (Class - 1) + // for class 3, ~50% support. + MinRankOfClassDelta::set(1); + assert_eq!(tally.support(3), Perbill::from_rational(1u32, 2)); + + // with minRank(Class) = (Class - 2) + // for class 3, ~33% support. + MinRankOfClassDelta::set(2); + assert_eq!(tally.support(3), Perbill::from_rational(1u32, 3)); + + // reset back. + MinRankOfClassDelta::set(0); + }); +}