Skip to content

Commit

Permalink
feat(pallets): boilerplate for pallet-communities
Browse files Browse the repository at this point in the history
  • Loading branch information
pandres95 committed Sep 4, 2023
1 parent 8231f56 commit cf966be
Show file tree
Hide file tree
Showing 7 changed files with 474 additions and 0 deletions.
41 changes: 41 additions & 0 deletions pallets/communities/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[package]
name = "pallet-communities"
version = "4.0.0-dev"
description = "FRAME pallet template for defining custom runtime logic."
authors = ["Substrate DevHub <https://github.com/substrate-developer-hub>"]
homepage = "https://substrate.io"
edition = "2021"
license = "MIT-0"
publish = false
repository = "https://github.com/substrate-developer-hub/substrate-node-template/"

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = [
"derive",
] }
scale-info = { version = "2.5.0", default-features = false, features = [
"derive",
] }
frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }

[dev-dependencies]
sp-core = { version = "21.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
sp-io = { version = "23.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }
sp-runtime = { version = "24.0.0", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v1.0.0" }

[features]
default = ["std"]
std = [
"codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"scale-info/std",
]
runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"]
try-runtime = ["frame-support/try-runtime"]
116 changes: 116 additions & 0 deletions pallets/communities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Communities Pallet

Part of the People Local Interactions Protocol, this pallet enables people to unite and
create local communities that share a common interest or economic activity. In simpler
terms, it can be considered a DAO Factory.

## Overview

The Communities pallet provides functionality for managing communities, facilitating its
participants to have governance over the community entity (and its associated account), and
running economic activities:

- [ ] Community registration and removal.
- [ ] Validating a community challenge.
- [ ] Handling a community-governed treasury account.
- [ ] Enrolling/removing members from a community.
- [ ] Promoting/demoting members within the community.
- [ ] Running proposals to enable community governance.
- [ ] Issue governance tokens.
- [ ] Issue economic (sufficient) tokens.

## Terminology

- **Community:** An entity comprised of _members_ —each one defined by their [`AccountId`][1]— with a given _description_ who can vote on _proposals_ and actively take decisions on behalf of it. Communities are given a _treasury account_ and can issue _governance_ and _economic_ tokens. It is required that a community contributes to the network to be active and operate within it.
- **Community Description:** A set of metadata used to identify a community distinctively. Typically, a name, a list of locations (given as a list of one or more [`H3Index`][2]), and a list of URL links.
- **Community Status:** A community can be either `awaiting` or `active` depending on whether the community has proven via a challenge it's actively contributing to the network with infrastructure provisioning (i.e. a [collator][3] node) or by depositing funds.
- **Validity Challenge:** A proof that a community is actively contributing to the network. The mechanisms for challenge verification are usually checked via an off-chain worker. Still, it's possible for a trusted origin to manually mark a community challenge as passed, effectively changing the status of the community to `active`.
- **Admin:** An [`AccountId`][1] registered into the community that is set as such. Can call [privileged functions](#privileged-functions) within the community.
- **Member:** An [`AccountId`][1] registered into the community as such. Can have a rank within it and vote in the community's polls.
- **Member Rank:** Members could have a rank within the community. This can determine a voting weight depending on the community's voting mechanism.
- **Proposal:** A poll with an optionally set duration that executes a [call][4] dispatch if approved when it's closed.
- **Treasury Account:** A keyless [`AccountId`][1] generated on behalf of the community. Can receive [payments][5], transfers, or payment [fees][6]. It can transfer funds via a privileged call executed by the community _admin_ or a call dispatched from a proposal.
- **Governance Token:** A [non-sufficient fungible asset][7] issued and administered by the _Treasury Account_ of the community. Customarily, it's given among community members and can be used to vote depending on the voting mechanism set by the community.
- **Economic Token:** A [sufficient fungible asset][7] issued and administered by the _Treasury Account_ of the community. Generally used for monetary purposes, it can be transferred among members and non-members of the community, used to pay network fees, and for [payments][5] and its corresponding [fees][6].

## Goals

The _"communities"_ are designed to facilitate the following use cases:

- Enable entities (i.e. DAOs) or local-bound groups of people (physical communities) that share common interests to create markets.
- Allow _communities_ can receive taxes (as in [payment fees][5]) and be self-sustainable.
- Let such _communities_ to sovereignly decide how to spend those gathered funds by running and voting on proposals.

## Lifecycle

```ignore
[ ] --> [Awaiting] --> [Active]
register set_metadata set_metadata
fulfill_challenge add_member
force_complete_challenge remove_member
promote_member
demote_member
issue_token
open_proposal
vote_proposal
close_proposal
assets_transfer
balance_transfer
set_admin
set_voting_mechanism
force_increase_economic_token_limit
```

## Implementations

> TODO: Define which traits we are defining/implementing.
## Interface

### Permissionless Functions

- `register`: Registers a new community, taking an [existential deposit][8] used to create the community account.

### Permissioned Functions

Calling these functions requires being a member of the community.

- `fulfill_challenge`: Submit the challenge proof to validate the contribution status of the community.
- `add_member`: Enroll an account as a community member. In theory, any community member should be able to add a member. However, this can be changed to ensure it is a privileged function.
- `open_proposal`: Creates a proposal to be voted by the community. At this point, there can only be a single proposal at a time.
- `vote_proposal`: Adds a vote into a community proposal.

### Privileged Functions

These functions can be called either by the community _admin_ or dispatched through an approved proposal.

- `set_metadata`: Sets some [`CommunityMetadata`] to describe the community.
- `remove_member`: Removes an account as a community member. While enrolling a member into the community can be an action taken by any member, the decision to remove a member should not be taken arbitrarily by any community member.
- `promote_member`: Increases the rank of a member in the community.
- `demote_member`: Decreases the rank of a member in the community.
- `issue_token`: Creates a token that is either governance (only one per community allowed) or economic. While the first economic token is _"free"_," further ones would be subject to network-wide referenda.
- `close_proposal`: Forcefully closes a proposal, dispatching the call when approved.
- `assets_transfer`: Transfers an amount of a given asset from the treasury account to a beneficiary.
- `balance_transfer`: Transfers funds from the treasury account to a beneficiary.
- `set_admin`: Sets an [AccountId][1] of the _admin_ of the community. Ensures that the specified account is a member of the community.
- `set_voting_mechanism`: Transfers funds from the treasury account to a beneficiary.

### Root Functions

- `force_complete_challenge`: Marks a challenge as passed. This can lead to the activation of a community if all challenges are passed.
- `force_increase_economic_token_limit`: Increases the amount of economic tokens a community can issue.

### Public Functions

## License

Unlicense

[1]: https://paritytech.github.io/substrate/master/frame_system/pallet/trait.Config.html#associatedtype.AccountId
[2]: https://h3geo.org/docs/highlights/indexing
[3]: https://docs.substrate.io/reference/glossary/#collator
[4]: https://docs.substrate.io/reference/glossary/#call
[5]: https://github.com/virto-network/virto-node/tree/master/pallets/payments
[6]: https://github.com/virto-network/virto-node/pull/282
[7]: https://paritytech.github.io/substrate/master/pallet_assets/index.html#terminology
[8]: https://docs.substrate.io/reference/glossary/#existential-deposit
35 changes: 35 additions & 0 deletions pallets/communities/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Benchmarking setup for pallet-communities
#![cfg(feature = "runtime-benchmarks")]
use super::*;

#[allow(unused)]
use crate::Pallet as Template;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;

#[benchmarks]
mod benchmarks {
use super::*;

#[benchmark]
fn do_something() {
let value = 100u32.into();
let caller: T::AccountId = whitelisted_caller();
#[extrinsic_call]
do_something(RawOrigin::Signed(caller), value);

assert_eq!(Something::<T>::get(), Some(value));
}

#[benchmark]
fn cause_error() {
Something::<T>::put(100u32);
let caller: T::AccountId = whitelisted_caller();
#[extrinsic_call]
cause_error(RawOrigin::Signed(caller));

assert_eq!(Something::<T>::get(), Some(101u32));
}

impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test);
}
108 changes: 108 additions & 0 deletions pallets/communities/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#![cfg_attr(not(feature = "std"), no_std)]

/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use weights::*;

#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;

#[pallet::pallet]
pub struct Pallet<T>(_);

/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type representing the weight of this pallet
type WeightInfo: WeightInfo;
}

// The pallet's runtime storage items.
// https://docs.substrate.io/main-docs/build/runtime-storage/
#[pallet::storage]
#[pallet::getter(fn something)]
// Learn more about declaring storage items:
// https://docs.substrate.io/main-docs/build/runtime-storage/#declaring-storage-items
pub type Something<T> = StorageValue<_, u32>;

// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Event documentation should end with an array that provides descriptive names for event
/// parameters. [something, who]
SomethingStored { something: u32, who: T::AccountId },
}

// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// Error names should be descriptive.
NoneValue,
/// Errors should have helpful documentation associated with them.
StorageOverflow,
}

// Dispatchable functions allows users to interact with the pallet and invoke state changes.
// These functions materialize as "extrinsics", which are often compared to transactions.
// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
#[pallet::call]
impl<T: Config> Pallet<T> {
/// An example dispatchable that takes a singles value as a parameter, writes the value to
/// storage and emits an event. This function must be dispatched by a signed extrinsic.
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::do_something())]
pub fn do_something(origin: OriginFor<T>, something: u32) -> DispatchResult {
// Check that the extrinsic was signed and get the signer.
// This function will return an error if the extrinsic is not signed.
// https://docs.substrate.io/main-docs/build/origins/
let who = ensure_signed(origin)?;

// Update storage.
<Something<T>>::put(something);

// Emit an event.
Self::deposit_event(Event::SomethingStored { something, who });
// Return a successful DispatchResultWithPostInfo
Ok(())
}

/// An example dispatchable that may throw a custom error.
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::cause_error())]
pub fn cause_error(origin: OriginFor<T>) -> DispatchResult {
let _who = ensure_signed(origin)?;

// Read a value from storage.
match <Something<T>>::get() {
// Return an error if the value has not been set.
None => return Err(Error::<T>::NoneValue.into()),
Some(old) => {
// Increment the value read from storage; will error in the event of overflow.
let new = old.checked_add(1).ok_or(Error::<T>::StorageOverflow)?;
// Update the value in storage with the incremented result.
<Something<T>>::put(new);
Ok(())
},
}
}
}
}
57 changes: 57 additions & 0 deletions pallets/communities/src/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate as pallet_communities;
use frame_support::traits::{ConstU16, ConstU64};
use sp_core::H256;
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};

type Block = frame_system::mocking::MockBlock<Test>;

// Configure a mock runtime to test the pallet.
frame_support::construct_runtime!(
pub enum Test
{
System: frame_system,
Communities: pallet_communities,
}
);

impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ConstU16<42>;
type OnSetCode = ();
type MaxConsumers = frame_support::traits::ConstU32<16>;
}

impl pallet_communities::Config for Test {
type RuntimeEvent = RuntimeEvent;
type WeightInfo = ();
}

// Build genesis storage according to the mock runtime.
pub fn new_test_ext() -> sp_io::TestExternalities {
frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap()
.into()
}
27 changes: 27 additions & 0 deletions pallets/communities/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::{mock::*, Error, Event};
use frame_support::{assert_noop, assert_ok};

#[test]
fn it_works_for_default_value() {
new_test_ext().execute_with(|| {
// Go past genesis block so events get deposited
System::set_block_number(1);
// Dispatch a signed extrinsic.
assert_ok!(Communities::do_something(RuntimeOrigin::signed(1), 42));
// Read pallet storage and assert an expected result.
assert_eq!(Communities::something(), Some(42));
// Assert that the correct event was deposited
System::assert_last_event(Event::SomethingStored { something: 42, who: 1 }.into());
});
}

#[test]
fn correct_error_for_none_value() {
new_test_ext().execute_with(|| {
// Ensure the expected error is thrown when no value is present.
assert_noop!(
Communities::cause_error(RuntimeOrigin::signed(1)),
Error::<Test>::NoneValue
);
});
}
Loading

0 comments on commit cf966be

Please sign in to comment.