From cf966bee22ecccb812fba0a726400904a5b989fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20Dorado=20Su=C3=A1rez?= Date: Mon, 4 Sep 2023 02:59:46 -0500 Subject: [PATCH] feat(pallets): boilerplate for pallet-communities --- pallets/communities/Cargo.toml | 41 +++++++++ pallets/communities/README.md | 116 ++++++++++++++++++++++++ pallets/communities/src/benchmarking.rs | 35 +++++++ pallets/communities/src/lib.rs | 108 ++++++++++++++++++++++ pallets/communities/src/mock.rs | 57 ++++++++++++ pallets/communities/src/tests.rs | 27 ++++++ pallets/communities/src/weights.rs | 90 ++++++++++++++++++ 7 files changed, 474 insertions(+) create mode 100644 pallets/communities/Cargo.toml create mode 100644 pallets/communities/README.md create mode 100644 pallets/communities/src/benchmarking.rs create mode 100644 pallets/communities/src/lib.rs create mode 100644 pallets/communities/src/mock.rs create mode 100644 pallets/communities/src/tests.rs create mode 100644 pallets/communities/src/weights.rs diff --git a/pallets/communities/Cargo.toml b/pallets/communities/Cargo.toml new file mode 100644 index 00000000..6b786ed8 --- /dev/null +++ b/pallets/communities/Cargo.toml @@ -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 "] +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"] diff --git a/pallets/communities/README.md b/pallets/communities/README.md new file mode 100644 index 00000000..a443db98 --- /dev/null +++ b/pallets/communities/README.md @@ -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 diff --git a/pallets/communities/src/benchmarking.rs b/pallets/communities/src/benchmarking.rs new file mode 100644 index 00000000..4cb5a7d2 --- /dev/null +++ b/pallets/communities/src/benchmarking.rs @@ -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::::get(), Some(value)); + } + + #[benchmark] + fn cause_error() { + Something::::put(100u32); + let caller: T::AccountId = whitelisted_caller(); + #[extrinsic_call] + cause_error(RawOrigin::Signed(caller)); + + assert_eq!(Something::::get(), Some(101u32)); + } + + impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/communities/src/lib.rs b/pallets/communities/src/lib.rs new file mode 100644 index 00000000..9550d3d5 --- /dev/null +++ b/pallets/communities/src/lib.rs @@ -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: +/// +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(_); + + /// 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> + IsType<::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 = 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 { + /// 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 { + /// 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 Pallet { + /// 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, 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. + >::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) -> DispatchResult { + let _who = ensure_signed(origin)?; + + // Read a value from storage. + match >::get() { + // Return an error if the value has not been set. + None => return Err(Error::::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::::StorageOverflow)?; + // Update the value in storage with the incremented result. + >::put(new); + Ok(()) + }, + } + } + } +} diff --git a/pallets/communities/src/mock.rs b/pallets/communities/src/mock.rs new file mode 100644 index 00000000..1b7fcbb0 --- /dev/null +++ b/pallets/communities/src/mock.rs @@ -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; + +// 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; + 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::::default() + .build_storage() + .unwrap() + .into() +} diff --git a/pallets/communities/src/tests.rs b/pallets/communities/src/tests.rs new file mode 100644 index 00000000..fc8c03cc --- /dev/null +++ b/pallets/communities/src/tests.rs @@ -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::::NoneValue + ); + }); +} diff --git a/pallets/communities/src/weights.rs b/pallets/communities/src/weights.rs new file mode 100644 index 00000000..878158ad --- /dev/null +++ b/pallets/communities/src/weights.rs @@ -0,0 +1,90 @@ + +//! Autogenerated weights for pallet_communities +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-04-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Alexs-MacBook-Pro-2.local`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ../../target/release/node-template +// benchmark +// pallet +// --chain +// dev +// --pallet +// pallet_communities +// --extrinsic +// * +// --steps=50 +// --repeat=20 +// --wasm-execution=compiled +// --output +// pallets/template/src/weights.rs +// --template +// ../../.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_communities. +pub trait WeightInfo { + fn do_something() -> Weight; + fn cause_error() -> Weight; +} + +/// Weights for pallet_communities using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Communities Something (r:0 w:1) + /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn do_something() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Communities Something (r:1 w:1) + /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn cause_error() -> Weight { + // Proof Size summary in bytes: + // Measured: `32` + // Estimated: `1489` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(6_000_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Communities Something (r:0 w:1) + /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn do_something() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Communities Something (r:1 w:1) + /// Proof: Communities Something (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + fn cause_error() -> Weight { + // Proof Size summary in bytes: + // Measured: `32` + // Estimated: `1489` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(6_000_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +}