#![cfg_attr(not(feature = "std"), no_std)]
#![recursion_limit = "128"]
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_arithmetic::traits::Saturating;
use sp_runtime::{
traits::{Convert, StaticLookup},
ArithmeticError::Overflow,
Perbill, RuntimeDebug,
};
use sp_std::{marker::PhantomData, prelude::*};
use frame_support::{
dispatch::{DispatchError, DispatchResultWithPostInfo, PostDispatchInfo},
ensure, impl_ensure_origin_with_arg_ignoring_arg,
traits::{EnsureOrigin, EnsureOriginWithArg, PollStatus, Polling, RankedMembers, VoteTally},
CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
pub type MemberIndex = u32;
pub type Rank = u16;
pub type Votes = u32;
#[derive(
CloneNoBound,
PartialEqNoBound,
EqNoBound,
RuntimeDebugNoBound,
TypeInfo,
Encode,
Decode,
MaxEncodedLen,
)]
#[scale_info(skip_type_params(T, I, M))]
#[codec(mel_bound())]
pub struct Tally<T, I, M: GetMaxVoters> {
bare_ayes: MemberIndex,
ayes: Votes,
nays: Votes,
dummy: PhantomData<(T, I, M)>,
}
impl<T: Config<I>, I: 'static, M: GetMaxVoters> Tally<T, I, M> {
pub fn from_parts(bare_ayes: MemberIndex, ayes: Votes, nays: Votes) -> Self {
Tally { bare_ayes, ayes, nays, dummy: PhantomData }
}
}
pub type TallyOf<T, I = ()> = Tally<T, I, Pallet<T, I>>;
pub type PollIndexOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Index;
pub type ClassOf<T, I = ()> = <<T as Config<I>>::Polls as Polling<TallyOf<T, I>>>::Class;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
impl<T: Config<I>, I: 'static, M: GetMaxVoters<Class = ClassOf<T, I>>>
VoteTally<Votes, ClassOf<T, I>> for Tally<T, I, M>
{
fn new(_: ClassOf<T, I>) -> Self {
Self { bare_ayes: 0, ayes: 0, nays: 0, dummy: PhantomData }
}
fn ayes(&self, _: ClassOf<T, I>) -> Votes {
self.bare_ayes
}
fn support(&self, class: ClassOf<T, I>) -> Perbill {
Perbill::from_rational(self.bare_ayes, M::get_max_voters(class))
}
fn approval(&self, _: ClassOf<T, I>) -> Perbill {
Perbill::from_rational(self.ayes, 1.max(self.ayes + self.nays))
}
#[cfg(feature = "runtime-benchmarks")]
fn unanimity(class: ClassOf<T, I>) -> Self {
Self {
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: ClassOf<T, I>) -> 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: ClassOf<T, I>) -> 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;
Self { bare_ayes: ayes, ayes, nays, dummy: PhantomData }
}
#[cfg(feature = "runtime-benchmarks")]
fn setup(class: ClassOf<T, I>, 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::<T, I>::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);
}
}
}
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct MemberRecord {
rank: Rank,
}
impl MemberRecord {
pub fn new(rank: Rank) -> Self {
Self { rank }
}
}
#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum VoteRecord {
Aye(Votes),
Nay(Votes),
}
impl From<(bool, Votes)> for VoteRecord {
fn from((aye, votes): (bool, Votes)) -> Self {
match aye {
true => VoteRecord::Aye(votes),
false => VoteRecord::Nay(votes),
}
}
}
pub struct Unit;
impl Convert<Rank, Votes> for Unit {
fn convert(_: Rank) -> Votes {
1
}
}
pub struct Linear;
impl Convert<Rank, Votes> for Linear {
fn convert(r: Rank) -> Votes {
(r + 1) as Votes
}
}
pub struct Geometric;
impl Convert<Rank, Votes> for Geometric {
fn convert(r: Rank) -> Votes {
let v = (r + 1) as Votes;
v * (v + 1) / 2
}
}
pub trait GetMaxVoters {
type Class;
fn get_max_voters(c: Self::Class) -> MemberIndex;
}
impl<T: Config<I>, I: 'static> GetMaxVoters for Pallet<T, I> {
type Class = ClassOf<T, I>;
fn get_max_voters(c: Self::Class) -> MemberIndex {
MemberCount::<T, I>::get(T::MinRankOfClass::convert(c))
}
}
pub struct EnsureRanked<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
for EnsureRanked<T, I, MIN_RANK>
{
type Success = Rank;
fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
let who = <frame_system::EnsureSigned<_> as EnsureOrigin<_>>::try_origin(o)?;
match Members::<T, I>::get(&who) {
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(rank),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
<EnsureRankedMember<T, I, MIN_RANK> as EnsureOrigin<_>>::try_successful_origin()
}
}
impl_ensure_origin_with_arg_ignoring_arg! {
impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureRanked<T, I, MIN_RANK>
{}
}
pub struct EnsureOfRank<T, I>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static> EnsureOriginWithArg<T::RuntimeOrigin, Rank> for EnsureOfRank<T, I> {
type Success = (T::AccountId, Rank);
fn try_origin(o: T::RuntimeOrigin, min_rank: &Rank) -> Result<Self::Success, T::RuntimeOrigin> {
let who = <frame_system::EnsureSigned<_> as EnsureOrigin<_>>::try_origin(o)?;
match Members::<T, I>::get(&who) {
Some(MemberRecord { rank, .. }) if rank >= *min_rank => Ok((who, rank)),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin(min_rank: &Rank) -> Result<T::RuntimeOrigin, ()> {
let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
crate::Pallet::<T, I>::do_add_member_to_rank(who.clone(), *min_rank)
.expect("Could not add members for benchmarks");
Ok(frame_system::RawOrigin::Signed(who).into())
}
}
pub struct EnsureMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
for EnsureMember<T, I, MIN_RANK>
{
type Success = T::AccountId;
fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
let who = <frame_system::EnsureSigned<_> as EnsureOrigin<_>>::try_origin(o)?;
match Members::<T, I>::get(&who) {
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok(who),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
<EnsureRankedMember<T, I, MIN_RANK> as EnsureOrigin<_>>::try_successful_origin()
}
}
impl_ensure_origin_with_arg_ignoring_arg! {
impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureMember<T, I, MIN_RANK>
{}
}
pub struct EnsureRankedMember<T, I, const MIN_RANK: u16>(PhantomData<(T, I)>);
impl<T: Config<I>, I: 'static, const MIN_RANK: u16> EnsureOrigin<T::RuntimeOrigin>
for EnsureRankedMember<T, I, MIN_RANK>
{
type Success = (T::AccountId, Rank);
fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
let who = <frame_system::EnsureSigned<_> as EnsureOrigin<_>>::try_origin(o)?;
match Members::<T, I>::get(&who) {
Some(MemberRecord { rank, .. }) if rank >= MIN_RANK => Ok((who, rank)),
_ => Err(frame_system::RawOrigin::Signed(who).into()),
}
}
#[cfg(feature = "runtime-benchmarks")]
fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
let who = frame_benchmarking::account::<T::AccountId>("successful_origin", 0, 0);
crate::Pallet::<T, I>::do_add_member_to_rank(who.clone(), MIN_RANK)
.expect("Could not add members for benchmarks");
Ok(frame_system::RawOrigin::Signed(who).into())
}
}
impl_ensure_origin_with_arg_ignoring_arg! {
impl<{ T: Config<I>, I: 'static, const MIN_RANK: u16, A }>
EnsureOriginWithArg<T::RuntimeOrigin, A> for EnsureRankedMember<T, I, MIN_RANK>
{}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{pallet_prelude::*, storage::KeyLenOf};
use frame_system::pallet_prelude::*;
#[pallet::pallet]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
type WeightInfo: WeightInfo;
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
type PromoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
type DemoteOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Rank>;
type Polls: Polling<TallyOf<Self, I>, Votes = Votes, Moment = BlockNumberFor<Self>>;
type MinRankOfClass: Convert<ClassOf<Self, I>, Rank>;
type VoteWeight: Convert<Rank, Votes>;
}
#[pallet::storage]
pub type MemberCount<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, Rank, MemberIndex, ValueQuery>;
#[pallet::storage]
pub type Members<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, T::AccountId, MemberRecord>;
#[pallet::storage]
pub type IdToIndex<T: Config<I>, I: 'static = ()> =
StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, T::AccountId, MemberIndex>;
#[pallet::storage]
pub type IndexToId<T: Config<I>, I: 'static = ()> =
StorageDoubleMap<_, Twox64Concat, Rank, Twox64Concat, MemberIndex, T::AccountId>;
#[pallet::storage]
pub type Voting<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
_,
Blake2_128Concat,
PollIndexOf<T, I>,
Twox64Concat,
T::AccountId,
VoteRecord,
>;
#[pallet::storage]
pub type VotingCleanup<T: Config<I>, I: 'static = ()> =
StorageMap<_, Blake2_128Concat, PollIndexOf<T, I>, BoundedVec<u8, KeyLenOf<Voting<T, I>>>>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
MemberAdded { who: T::AccountId },
RankChanged { who: T::AccountId, rank: Rank },
MemberRemoved { who: T::AccountId, rank: Rank },
Voted { who: T::AccountId, poll: PollIndexOf<T, I>, vote: VoteRecord, tally: TallyOf<T, I> },
}
#[pallet::error]
pub enum Error<T, I = ()> {
AlreadyMember,
NotMember,
NotPolling,
Ongoing,
NoneRemaining,
Corruption,
RankTooLow,
InvalidWitness,
NoPermission,
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::add_member())]
pub fn add_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
let _ = T::PromoteOrigin::ensure_origin(origin)?;
let who = T::Lookup::lookup(who)?;
Self::do_add_member(who)
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::promote_member(0))]
pub fn promote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
let max_rank = T::PromoteOrigin::ensure_origin(origin)?;
let who = T::Lookup::lookup(who)?;
Self::do_promote_member(who, Some(max_rank))
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::demote_member(0))]
pub fn demote_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
let who = T::Lookup::lookup(who)?;
Self::do_demote_member(who, Some(max_rank))
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::remove_member(*min_rank as u32))]
pub fn remove_member(
origin: OriginFor<T>,
who: AccountIdLookupOf<T>,
min_rank: Rank,
) -> DispatchResultWithPostInfo {
let max_rank = T::DemoteOrigin::ensure_origin(origin)?;
let who = T::Lookup::lookup(who)?;
let MemberRecord { rank, .. } = Self::ensure_member(&who)?;
ensure!(min_rank >= rank, Error::<T, I>::InvalidWitness);
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
for r in 0..=rank {
Self::remove_from_rank(&who, r)?;
}
Members::<T, I>::remove(&who);
Self::deposit_event(Event::MemberRemoved { who, rank });
Ok(PostDispatchInfo {
actual_weight: Some(T::WeightInfo::remove_member(rank as u32)),
pays_fee: Pays::Yes,
})
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::vote())]
pub fn vote(
origin: OriginFor<T>,
poll: PollIndexOf<T, I>,
aye: bool,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let record = Self::ensure_member(&who)?;
use VoteRecord::*;
let mut pays = Pays::Yes;
let (tally, vote) = T::Polls::try_access_poll(
poll,
|mut status| -> Result<(TallyOf<T, I>, VoteRecord), DispatchError> {
match status {
PollStatus::None | PollStatus::Completed(..) =>
Err(Error::<T, I>::NotPolling)?,
PollStatus::Ongoing(ref mut tally, class) => {
match Voting::<T, I>::get(&poll, &who) {
Some(Aye(votes)) => {
tally.bare_ayes.saturating_dec();
tally.ayes.saturating_reduce(votes);
},
Some(Nay(votes)) => tally.nays.saturating_reduce(votes),
None => pays = Pays::No,
}
let min_rank = T::MinRankOfClass::convert(class);
let votes = Self::rank_to_votes(record.rank, min_rank)?;
let vote = VoteRecord::from((aye, votes));
match aye {
true => {
tally.bare_ayes.saturating_inc();
tally.ayes.saturating_accrue(votes);
},
false => tally.nays.saturating_accrue(votes),
}
Voting::<T, I>::insert(&poll, &who, &vote);
Ok((tally.clone(), vote))
},
}
},
)?;
Self::deposit_event(Event::Voted { who, poll, vote, tally });
Ok(pays.into())
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::cleanup_poll(*max))]
pub fn cleanup_poll(
origin: OriginFor<T>,
poll_index: PollIndexOf<T, I>,
max: u32,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
ensure!(T::Polls::as_ongoing(poll_index).is_none(), Error::<T, I>::Ongoing);
let r = Voting::<T, I>::clear_prefix(
poll_index,
max,
VotingCleanup::<T, I>::take(poll_index).as_ref().map(|c| &c[..]),
);
if r.unique == 0 {
return Ok(Pays::Yes.into())
}
if let Some(cursor) = r.maybe_cursor {
VotingCleanup::<T, I>::insert(poll_index, BoundedVec::truncate_from(cursor));
}
Ok(PostDispatchInfo {
actual_weight: Some(T::WeightInfo::cleanup_poll(r.unique)),
pays_fee: Pays::No,
})
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
fn ensure_member(who: &T::AccountId) -> Result<MemberRecord, DispatchError> {
Members::<T, I>::get(who).ok_or(Error::<T, I>::NotMember.into())
}
fn rank_to_votes(rank: Rank, min: Rank) -> Result<Votes, DispatchError> {
let excess = rank.checked_sub(min).ok_or(Error::<T, I>::RankTooLow)?;
Ok(T::VoteWeight::convert(excess))
}
fn remove_from_rank(who: &T::AccountId, rank: Rank) -> DispatchResult {
let last_index = MemberCount::<T, I>::get(rank).saturating_sub(1);
let index = IdToIndex::<T, I>::get(rank, &who).ok_or(Error::<T, I>::Corruption)?;
if index != last_index {
let last =
IndexToId::<T, I>::get(rank, last_index).ok_or(Error::<T, I>::Corruption)?;
IdToIndex::<T, I>::insert(rank, &last, index);
IndexToId::<T, I>::insert(rank, index, &last);
}
MemberCount::<T, I>::mutate(rank, |r| r.saturating_dec());
Ok(())
}
pub fn do_add_member(who: T::AccountId) -> DispatchResult {
ensure!(!Members::<T, I>::contains_key(&who), Error::<T, I>::AlreadyMember);
let index = MemberCount::<T, I>::get(0);
let count = index.checked_add(1).ok_or(Overflow)?;
Members::<T, I>::insert(&who, MemberRecord { rank: 0 });
IdToIndex::<T, I>::insert(0, &who, index);
IndexToId::<T, I>::insert(0, index, &who);
MemberCount::<T, I>::insert(0, count);
Self::deposit_event(Event::MemberAdded { who });
Ok(())
}
pub fn do_promote_member(
who: T::AccountId,
maybe_max_rank: Option<Rank>,
) -> DispatchResult {
let record = Self::ensure_member(&who)?;
let rank = record.rank.checked_add(1).ok_or(Overflow)?;
if let Some(max_rank) = maybe_max_rank {
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
}
let index = MemberCount::<T, I>::get(rank);
MemberCount::<T, I>::insert(rank, index.checked_add(1).ok_or(Overflow)?);
IdToIndex::<T, I>::insert(rank, &who, index);
IndexToId::<T, I>::insert(rank, index, &who);
Members::<T, I>::insert(&who, MemberRecord { rank });
Self::deposit_event(Event::RankChanged { who, rank });
Ok(())
}
fn do_demote_member(who: T::AccountId, maybe_max_rank: Option<Rank>) -> DispatchResult {
let mut record = Self::ensure_member(&who)?;
let rank = record.rank;
if let Some(max_rank) = maybe_max_rank {
ensure!(max_rank >= rank, Error::<T, I>::NoPermission);
}
Self::remove_from_rank(&who, rank)?;
let maybe_rank = rank.checked_sub(1);
match maybe_rank {
None => {
Members::<T, I>::remove(&who);
Self::deposit_event(Event::MemberRemoved { who, rank: 0 });
},
Some(rank) => {
record.rank = rank;
Members::<T, I>::insert(&who, &record);
Self::deposit_event(Event::RankChanged { who, rank });
},
}
Ok(())
}
pub fn do_add_member_to_rank(who: T::AccountId, rank: Rank) -> DispatchResult {
Self::do_add_member(who.clone())?;
for _ in 0..rank {
Self::do_promote_member(who.clone(), None)?;
}
Ok(())
}
pub fn as_rank(
o: &<T::RuntimeOrigin as frame_support::traits::OriginTrait>::PalletsOrigin,
) -> Option<u16> {
use frame_support::traits::CallerTrait;
o.as_signed().and_then(Self::rank_of)
}
}
impl<T: Config<I>, I: 'static> RankedMembers for Pallet<T, I> {
type AccountId = T::AccountId;
type Rank = Rank;
fn min_rank() -> Self::Rank {
0
}
fn rank_of(who: &Self::AccountId) -> Option<Self::Rank> {
Some(Self::ensure_member(&who).ok()?.rank)
}
fn induct(who: &Self::AccountId) -> DispatchResult {
Self::do_add_member(who.clone())
}
fn promote(who: &Self::AccountId) -> DispatchResult {
Self::do_promote_member(who.clone(), None)
}
fn demote(who: &Self::AccountId) -> DispatchResult {
Self::do_demote_member(who.clone(), None)
}
}
}