-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pallets): boilerplate for pallet-communities
- Loading branch information
Showing
7 changed files
with
474 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
}, | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
}); | ||
} |
Oops, something went wrong.