Skip to content

Commit

Permalink
[Communities] Governance V0: Enqueue and Execute calls (#313)
Browse files Browse the repository at this point in the history
* feat(pallets/communities): define MemberRank trait, for MembershipPassport

* feat(pallets/communities): add structures and storage for handling governance

* feat(pallets/communities): add governance (polls/proposals) handling functions

* feat(pallets/communities): define open_proposal/execute_call

* change(pallets/communities): explictly state call origin when creating proposal

* fix(pallets/communities): lint

* change(communities:functions/origin): use CheckedConversion to replace as_origin!
  • Loading branch information
pandres95 authored and olanod committed Nov 20, 2023
1 parent 1de3dc9 commit 4fc82df
Show file tree
Hide file tree
Showing 24 changed files with 925 additions and 57 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pallets/communities/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "polkado

pallet-assets = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
pallet-preimage = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
pallet-scheduler = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }

[features]
default = ["std"]
Expand Down
9 changes: 5 additions & 4 deletions pallets/communities/src/functions/getters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ impl<T: Config> Pallet<T> {
T::PalletId::get().into_sub_account_truncating(community_id)
}

pub(crate) fn get_community_admin(community_id: &CommunityIdOf<T>) -> Result<AccountIdOf<T>, DispatchError> {
let community = Self::community(community_id).ok_or(Error::<T>::CommunityDoesNotExist)?;

Ok(community.admin)
pub(crate) fn get_community_admin(community_id: &CommunityIdOf<T>) -> Option<AccountIdOf<T>> {
match GovernanceStrategy::<T>::get(community_id) {
Some(CommunityGovernanceStrategy::AdminBased(admin)) => Some(admin),
_ => None,
}
}
}
4 changes: 4 additions & 0 deletions pallets/communities/src/functions/governance/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub(self) use super::*;

mod poll;
mod proposals;
136 changes: 136 additions & 0 deletions pallets/communities/src/functions/governance/poll.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use super::*;
use sp_runtime::Permill;

impl<T: Config> Pallet<T> {
pub(crate) fn do_initiate_poll(community_id: &CommunityIdOf<T>) -> DispatchResult {
Poll::<T>::insert(community_id, CommunityPoll::default());

Ok(())
}

pub(crate) fn do_vote_in_poll(
who: &AccountIdOf<T>,
community_id: &CommunityIdOf<T>,
vote: CommunityPollVote<T>,
) -> DispatchResult {
Poll::<T>::try_mutate(community_id, |value| {
let Some(mut poll) = value.clone() else {
return Err(Error::<T>::PollAlreadyClosed)?;
};

match vote {
CommunityPollVote::Aye(weight) => {
poll.ayes = poll
.ayes
.saturating_add(Self::get_vote_weight(who, community_id, weight)?)
}
CommunityPollVote::Nay(weight) => {
poll.ayes = poll
.nays
.saturating_add(Self::get_vote_weight(who, community_id, weight)?)
}
}

*value = Some(poll);

Ok(())
})
}

#[allow(dead_code)]
pub(crate) fn do_close_poll(community_id: &CommunityIdOf<T>) -> DispatchResult {
let poll_outcome = Self::do_calculate_poll_outcome(community_id)?;
let proposal = Self::do_deequeue_proposal(community_id)?;

match poll_outcome {
// Schedule the approved proposal
PollOutcome::Approved => Self::do_execute_proposal(proposal),
// Do nothing
PollOutcome::Rejected => Ok(()),
}?;

Ok(())
}
}

impl<T: Config> Pallet<T> {
fn get_vote_weight(
who: &AccountIdOf<T>,
community_id: &CommunityIdOf<T>,
_input_weight: VoteWeightFor<T>,
) -> Result<VoteWeightFor<T>, DispatchError> {
let governance_strategy = Self::governance_strategy(community_id).ok_or(Error::<T>::CommunityDoesNotExist)?;

match governance_strategy {
CommunityGovernanceStrategy::AdminBased(admin) => {
if *who == admin {
Ok(1u32.into())
} else {
Ok(0u32.into())
}
}
CommunityGovernanceStrategy::MemberCountPoll { min: _ } => Ok(1u32.into()),
CommunityGovernanceStrategy::AssetWeighedPoll {
asset_id: _,
num: _,
denum: _,
} => todo!(),
CommunityGovernanceStrategy::RankedWeighedPoll { num: _, denum: _ } => {
// use crate::traits::rank::GetRank;

// let membership = Self::ensure_member(community_id, who)?;
// let rank = <MembershipPassportOf<T> as
// GetRank<T::MembershipRank>>::rank_of(&membership);

// Ok(input_weight.max(rank.into()))
todo!()
}
}
}

fn do_calculate_poll_outcome(community_id: &CommunityIdOf<T>) -> Result<PollOutcome, DispatchError> {
let governance_strategy = Self::governance_strategy(community_id).ok_or(Error::<T>::CommunityDoesNotExist)?;
let poll = Self::poll(community_id).ok_or(Error::<T>::PollAlreadyClosed)?;

let (ayes, nays) = (poll.ayes, poll.nays);

match governance_strategy {
CommunityGovernanceStrategy::AdminBased(_) => Ok(PollOutcome::Approved),
CommunityGovernanceStrategy::MemberCountPoll { min } => {
if ayes.saturating_add(nays) >= min {
if ayes > nays {
Ok(PollOutcome::Approved)
} else {
Ok(PollOutcome::Rejected)
}
} else {
Err(Error::<T>::CannotClosePoll.into())
}
}
CommunityGovernanceStrategy::AssetWeighedPoll {
asset_id: _,
num,
denum,
} => {
let criteria_fraction = Permill::from_rational(num, denum);
let poll_fraction = Permill::from_rational(ayes, ayes.saturating_add(nays));

if poll_fraction >= criteria_fraction {
Ok(PollOutcome::Approved)
} else {
Ok(PollOutcome::Rejected)
}
}
CommunityGovernanceStrategy::RankedWeighedPoll { num, denum } => {
let criteria_fraction = Permill::from_rational(num, denum);
let poll_fraction = Permill::from_rational(ayes, ayes.saturating_add(nays));

if poll_fraction >= criteria_fraction {
Ok(PollOutcome::Approved)
} else {
Ok(PollOutcome::Rejected)
}
}
}
}
}
58 changes: 58 additions & 0 deletions pallets/communities/src/functions/governance/proposals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use super::*;
pub(self) use frame_support::traits::{schedule::v3::Anon, StorePreimage};
pub(self) use sp_runtime::traits::Zero;

impl<T: Config> Pallet<T> {
pub(crate) fn do_create_proposal(
proposer: &AccountIdOf<T>,
community_id: &CommunityIdOf<T>,
call_origin: PalletsOriginOf<T>,
call: RuntimeCallOf<T>,
) -> DispatchResult {
Self::ensure_proposal_origin(community_id, call_origin.clone())?;

let bounded_call = T::Preimage::bound(call).map_err(|_| Error::<T>::CannotEncodeCall)?;

Self::do_enqueue_proposal(
community_id,
CommunityProposal {
proposer: proposer.clone(),
call: bounded_call,
origin: call_origin,
},
)?;

Ok(())
}

#[allow(dead_code)]
pub(crate) fn do_execute_proposal(proposal: CommunityProposal<T>) -> DispatchResult {
T::Scheduler::schedule(
frame_support::traits::schedule::DispatchTime::After(Zero::zero()),
None,
Default::default(),
proposal.origin,
proposal.call,
)
.map(|_| ())
}

fn do_enqueue_proposal(community_id: &CommunityIdOf<T>, proposal: CommunityProposal<T>) -> DispatchResult {
if Proposals::<T>::decode_len(community_id).unwrap_or_default() >= T::MaxProposals::get() as usize {
Err(Error::<T>::ExceededMaxProposals)?;
}

Proposals::<T>::try_append(community_id, proposal).map_err(|_| Error::<T>::CannotEnqueueProposal)?;

Ok(())
}

pub(crate) fn do_deequeue_proposal(community_id: &CommunityIdOf<T>) -> Result<CommunityProposal<T>, DispatchError> {
Proposals::<T>::try_mutate(community_id, |proposals| {
let first_proposal = proposals.first().ok_or(Error::<T>::CannotDequeueProposal)?.clone();
proposals.remove(0);

Ok(first_proposal)
})
}
}
32 changes: 18 additions & 14 deletions pallets/communities/src/functions/membership.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,35 @@ impl<T: Config> Pallet<T> {
pub(crate) fn ensure_origin_member(
origin: OriginFor<T>,
community_id: &CommunityIdOf<T>,
) -> Result<(), DispatchError> {
) -> Result<AccountIdOf<T>, DispatchError> {
let caller = ensure_signed(origin)?;

if Self::membership(community_id, caller).is_none() {
return Err(DispatchError::BadOrigin);
}
Self::membership(community_id, &caller)
.ok_or(DispatchError::BadOrigin)
.map(|_| caller)
}

Ok(())
#[allow(dead_code)]
pub(crate) fn ensure_member(
community_id: &CommunityIdOf<T>,
who: &AccountIdOf<T>,
) -> Result<MembershipPassportOf<T>, DispatchError> {
Self::membership(community_id, who).ok_or(Error::<T>::NotAMember.into())
}

pub(crate) fn ensure_origin_privileged(
origin: OriginFor<T>,
community_id: &CommunityIdOf<T>,
) -> Result<(), DispatchError> {
) -> Result<Option<AccountIdOf<T>>, DispatchError> {
if let Some(caller) = ensure_signed_or_root(origin)? {
if caller != Self::get_community_admin(community_id)? {
return Err(DispatchError::BadOrigin);
if let Some(admin) = Self::get_community_admin(community_id) && admin == caller {
return Ok(Some(admin))
} else {
return Err(DispatchError::BadOrigin)
}
}

Ok(())
Ok(None)
}

pub(crate) fn do_insert_member(community_id: &CommunityIdOf<T>, who: &AccountIdOf<T>) -> DispatchResult {
Expand All @@ -50,11 +58,7 @@ impl<T: Config> Pallet<T> {
return Err(Error::<T>::NotAMember.into());
}

let Some(community_info) = Self::community(community_id) else {
return Err(Error::<T>::CommunityDoesNotExist.into());
};

if community_info.admin == *who {
if let Some(community_admin) = Self::get_community_admin(community_id) && community_admin == *who {
return Err(Error::<T>::CannotRemoveAdmin.into());
}

Expand Down
3 changes: 3 additions & 0 deletions pallets/communities/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ pub(self) use frame_support::traits::tokens::{
fungibles::Mutate as MutateFuns,
};
pub(self) use frame_system::pallet_prelude::*;
pub(self) use sp_runtime::Saturating;
pub(self) use types::*;

mod challenges;
mod getters;
mod governance;
mod membership;
mod origin;
mod registry;
mod treasury;
24 changes: 24 additions & 0 deletions pallets/communities/src/functions/origin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use sp_runtime::traits::CheckedConversion;

use super::*;

impl<T: Config> Pallet<T> {
pub(crate) fn ensure_proposal_origin(
community_id: &CommunityIdOf<T>,
origin: PalletsOriginOf<T>,
) -> DispatchResult {
let community_account_id = Self::get_community_account_id(community_id);

if let Some(o) = origin.clone().checked_into::<frame_system::Origin<T>>() {
match o {
frame_system::Origin::<T>::Signed(account) if account == community_account_id => Ok(()),
_ => Err(Error::<T>::InvalidProposalOrigin.into()),
}
} else {
match origin.checked_into::<pallet::Origin<T>>() {
Some(_) => Ok(()),
None => Err(Error::<T>::InvalidProposalOrigin.into()),
}
}
}
}
5 changes: 2 additions & 3 deletions pallets/communities/src/functions/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ impl<T: Config> Pallet<T> {
}

Info::<T>::insert(
community_id.clone(),
community_id,
CommunityInfo {
admin: who.clone(),
state: Default::default(),
sufficient_asset_id: None,
},
);
GovernanceStrategy::<T>::insert(community_id, CommunityGovernanceStrategy::AdminBased(who.clone()));

Self::do_insert_member(community_id, who)?;

Expand Down
Loading

0 comments on commit 4fc82df

Please sign in to comment.