diff --git a/Cargo.lock b/Cargo.lock index a6c2bc1fd318..a334d1b9a4ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11554,6 +11554,30 @@ dependencies = [ "sp-runtime 31.0.1", ] +[[package]] +name = "pallet-distribution" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "pallet-preimage", + "pallet-scheduler", + "pallet-sudo", + "pallet-timestamp", + "pallet-transaction-payment", + "pallet-transaction-payment-rpc-runtime-api", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", + "sp-std 14.0.0", +] + [[package]] name = "pallet-election-provider-e2e-test" version = "1.0.0" @@ -12296,6 +12320,29 @@ dependencies = [ "sp-staking", ] +[[package]] +name = "pallet-opf" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "pallet-conviction-voting", + "pallet-distribution", + "pallet-sudo", + "pallet-timestamp", + "pallet-transaction-payment", + "pallet-transaction-payment-rpc-runtime-api", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", +] + [[package]] name = "pallet-paged-list" version = "0.6.0" @@ -15420,6 +15467,7 @@ dependencies = [ "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", + "pallet-distribution", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", @@ -15447,6 +15495,7 @@ dependencies = [ "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", + "pallet-opf", "pallet-paged-list", "pallet-parameters", "pallet-preimage", @@ -15726,6 +15775,7 @@ dependencies = [ "pallet-contracts", "pallet-default-config-example", "pallet-democracy", + "pallet-distribution", "pallet-example-authorization-tx-extension", "pallet-example-offchain-worker", "pallet-example-single-block-migrations", @@ -15733,6 +15783,7 @@ dependencies = [ "pallet-grandpa", "pallet-multisig", "pallet-nfts", + "pallet-opf", "pallet-preimage", "pallet-proxy", "pallet-referenda", diff --git a/Cargo.toml b/Cargo.toml index e451529431ba..4f5017642843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -341,6 +341,7 @@ members = [ "substrate/frame/core-fellowship", "substrate/frame/delegated-staking", "substrate/frame/democracy", + "substrate/frame/distribution", "substrate/frame/election-provider-multi-phase", "substrate/frame/election-provider-multi-phase/test-staking-e2e", "substrate/frame/election-provider-support", @@ -389,6 +390,7 @@ members = [ "substrate/frame/nomination-pools/test-transfer-stake", "substrate/frame/offences", "substrate/frame/offences/benchmarking", + "substrate/frame/opf", "substrate/frame/paged-list", "substrate/frame/paged-list/fuzzer", "substrate/frame/parameters", @@ -918,6 +920,7 @@ pallet-default-config-example = { path = "substrate/frame/examples/default-confi pallet-delegated-staking = { path = "substrate/frame/delegated-staking", default-features = false } pallet-democracy = { path = "substrate/frame/democracy", default-features = false } pallet-dev-mode = { path = "substrate/frame/examples/dev-mode", default-features = false } +pallet-distribution = { path = "substrate/frame/distribution", default-features = false } pallet-election-provider-multi-phase = { path = "substrate/frame/election-provider-multi-phase", default-features = false } pallet-election-provider-support-benchmarking = { path = "substrate/frame/election-provider-support/benchmarking", default-features = false } pallet-elections-phragmen = { path = "substrate/frame/elections-phragmen", default-features = false } @@ -956,6 +959,7 @@ pallet-nomination-pools-benchmarking = { path = "substrate/frame/nomination-pool pallet-nomination-pools-runtime-api = { path = "substrate/frame/nomination-pools/runtime-api", default-features = false } pallet-offences = { path = "substrate/frame/offences", default-features = false } pallet-offences-benchmarking = { path = "substrate/frame/offences/benchmarking", default-features = false } +pallet-opf = { path = "substrate/frame/opf", default-features = false } pallet-paged-list = { path = "substrate/frame/paged-list", default-features = false } pallet-parachain-template = { path = "templates/parachain/pallets/template", default-features = false } pallet-parameters = { path = "substrate/frame/parameters", default-features = false } diff --git a/docs/sdk/Cargo.toml b/docs/sdk/Cargo.toml index 0c39367eeed3..903842c361b5 100644 --- a/docs/sdk/Cargo.toml +++ b/docs/sdk/Cargo.toml @@ -97,6 +97,8 @@ pallet-scheduler = { workspace = true, default-features = true } pallet-referenda = { workspace = true, default-features = true } pallet-broker = { workspace = true, default-features = true } pallet-babe = { workspace = true, default-features = true } +pallet-distribution = { workspace = true, default-features = true } +pallet-opf = { workspace = true, default-features = true } pallet-grandpa = { workspace = true, default-features = true } # Primitives diff --git a/prdoc/pr_5162.prdoc b/prdoc/pr_5162.prdoc new file mode 100644 index 000000000000..d7afba9311c6 --- /dev/null +++ b/prdoc/pr_5162.prdoc @@ -0,0 +1,23 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: "Optimistic Project Funding" + +doc: + - audience: Runtime Dev + description: | + the combination of the OPF pallet and the DISTRIBUTION pallet handles the Optimistic Project Funding. + It allows users to nominate projects (whitelisted in OpenGov) with their DOT. This mechanism will be funded with a constant stream of DOT taken directly from inflation and distributed to projects based on the proportion of DOT that has nominated them. + The project rewards distribution is handled by the **Distribution Pallet** + +crates: + - name: pallet-distribution + bump: patch + - name: pallet-opf + bump: patch + - name: frame-support + bump: none + - name: polkadot-sdk + bump: patch + - name: polkadot + bump: none \ No newline at end of file diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 12e8dc3e5077..fefe31b29619 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -2245,6 +2245,44 @@ impl pallet_broker::Config for Runtime { type PriceAdapter = pallet_broker::CenterTargetPrice; } +parameter_types! { + pub const PotId: PalletId = PalletId(*b"py/potid"); + pub const Period:BlockNumber = 5 * MINUTES; + pub const MaxProjects:u32 = 50; + pub const EpochDurationBlocks: BlockNumber = EPOCH_DURATION_IN_BLOCKS; +} + +impl pallet_distribution::Config for Runtime { + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type NativeBalance = Balances; + type PotId = PotId; + type RuntimeHoldReason = RuntimeHoldReason; + type Scheduler = Scheduler; + type BufferPeriod = Period; + type MaxProjects = MaxProjects; + type EpochDurationBlocks = EpochDurationBlocks; + type BlockNumberProvider = System; + type Preimages = Preimage; + type WeightInfo = pallet_distribution::weights::SubstrateWeight; +} + +parameter_types! { + pub const MaxWhitelistedProjects: u32 = 64; + pub const TemporaryRewards: Balance = 100000 * DOLLARS; + pub const TotalPeriod:BlockNumber = 30 * DAYS; + pub const LockPeriod:BlockNumber = 10 * DAYS; +} + +impl pallet_opf::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type VoteLockingPeriod = LockPeriod; + type VotingPeriod = TotalPeriod; + type MaxWhitelistedProjects = MaxWhitelistedProjects; + type TemporaryRewards = TemporaryRewards; + type WeightInfo = pallet_opf::weights::SubstrateWeight; +} + parameter_types! { pub const MixnetNumCoverToCurrentBlocks: BlockNumber = 3; pub const MixnetNumRequestsToCurrentBlocks: BlockNumber = 3; @@ -2590,6 +2628,12 @@ mod runtime { #[runtime::pallet_index(81)] pub type VerifySignature = pallet_verify_signature::Pallet; + + #[runtime::pallet_index(82)] + pub type Distribution = pallet_distribution::Pallet; + + #[runtime::pallet_index(83)] + pub type OptimisticProjectFunding = pallet_opf::Pallet; } impl TryFrom for pallet_revive::Call { @@ -2849,6 +2893,8 @@ mod benches { [pallet_safe_mode, SafeMode] [pallet_example_mbm, PalletExampleMbms] [pallet_asset_conversion_ops, AssetConversionMigration] + [pallet_distribution, Distribution] + [pallet_opf, OptimisticProjectFunding] [pallet_verify_signature, VerifySignature] ); } diff --git a/substrate/frame/distribution/Cargo.toml b/substrate/frame/distribution/Cargo.toml new file mode 100644 index 000000000000..d8ec83545c54 --- /dev/null +++ b/substrate/frame/distribution/Cargo.toml @@ -0,0 +1,89 @@ +[package] +authors.workspace = true +description = "FRAME pallet to distribute funds to whitelisted projects" +edition.workspace = true +homepage = "https://substrate.io" +license = "Apache-2.0" +name = "pallet-distribution" +readme = "README.md" +repository.workspace = true +version = "0.1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, default-features = false } +frame-benchmarking = { optional = true, workspace = true, default-features = false } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +log = { workspace = true } +pallet-scheduler = { workspace = true, default-features = false } +scale-info = { features = [ + "derive", +], workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-io = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } +sp-std = { workspace = true, default-features = false } + +[dev-dependencies] +pallet-assets = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +pallet-preimage = { workspace = true, default-features = true } +pallet-scheduler = { workspace = true, default-features = true } +pallet-sudo = { workspace = true, default-features = true } +pallet-timestamp = { workspace = true, default-features = true } +pallet-transaction-payment = { workspace = true, default-features = true } +pallet-transaction-payment-rpc-runtime-api = { workspace = true } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", + "pallet-scheduler/runtime-benchmarks", + "pallet-sudo/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "pallet-transaction-payment/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-preimage/std", + "pallet-scheduler/std", + "pallet-sudo/std", + "pallet-timestamp/std", + "pallet-transaction-payment-rpc-runtime-api/std", + "pallet-transaction-payment/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-assets/try-runtime", + "pallet-balances/try-runtime", + "pallet-preimage/try-runtime", + "pallet-scheduler/try-runtime", + "pallet-sudo/try-runtime", + "pallet-timestamp/try-runtime", + "pallet-transaction-payment/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/distribution/README.md b/substrate/frame/distribution/README.md new file mode 100644 index 000000000000..25af1e720ad7 --- /dev/null +++ b/substrate/frame/distribution/README.md @@ -0,0 +1,32 @@ +# Distribution Pallet +## Overview + +The **Distribution Pallet** handles the distribution of whitelisted projects rewards. + +For now only one reward distribution pattern has been implemented, but the pallet could be extended +to offer to the user claiming rewards for a project, a choice between more than one distribution pattern. + +The **Distribution Pallet** receives a list of Whitelisted/Nominated Projects with +their respective calculated rewards. For each project, it will create a corresponding +spend that will be stored until the project reward can be claimed. +At the moment, the reward claim period start corresponds to: +[beginning of an ***Epoch_Block*** + ***BufferPeriod***] (The ***BufferPeriod*** can be configured in the runtime). + +### Terminology + +- **PotId:** Pot containing the funds used to pay the rewards. +- **BufferPeriod:** minimum required buffer time period between project nomination and reward claim. + +## Interface + +### Dispatchable Functions + +#### Public + +These calls can be made from any externally held account capable of creating +a signed extrinsic. + +Basic actions: +- `claim_reward_for` - From this extrinsic any user can claim a reward for a nominated/whitelisted project. + +License: Apache-2.0 diff --git a/substrate/frame/distribution/src/benchmarking.rs b/substrate/frame/distribution/src/benchmarking.rs new file mode 100644 index 000000000000..62c92f21c548 --- /dev/null +++ b/substrate/frame/distribution/src/benchmarking.rs @@ -0,0 +1,115 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Distribution pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use crate::Pallet as Distribution; +use frame_benchmarking::{ + v1::{account, BenchmarkError}, + v2::*, +}; +use frame_support::ensure; +use frame_system::RawOrigin; +use sp_runtime::traits::One; + +const SEED: u32 = 0; + +fn run_to_block(n: frame_system::pallet_prelude::BlockNumberFor) { + while T::BlockNumberProvider::current_block_number() < n { + crate::Pallet::::on_finalize(T::BlockNumberProvider::current_block_number()); + frame_system::Pallet::::on_finalize(T::BlockNumberProvider::current_block_number()); + frame_system::Pallet::::set_block_number( + T::BlockNumberProvider::current_block_number() + One::one(), + ); + frame_system::Pallet::::on_initialize(T::BlockNumberProvider::current_block_number()); + crate::Pallet::::on_initialize(T::BlockNumberProvider::current_block_number()); + } +} + +fn create_project(project_id: AccountIdOf, amount: BalanceOf) { + let submission_block = T::BlockNumberProvider::current_block_number(); + let project: types::ProjectInfo = ProjectInfo { project_id, submission_block, amount }; + Projects::::mutate(|value| { + let mut val = value.clone(); + let _ = val.try_push(project); + *value = val; + }); +} + +/*fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +}*/ + +fn create_parameters(n: u32) -> (AccountIdOf, BalanceOf) { + let project_id = account("project", n, SEED); + let value: BalanceOf = T::NativeBalance::minimum_balance() * 100u32.into() * (n + 1).into(); + let _ = T::NativeBalance::set_balance(&project_id, value); + (project_id, value) +} + +fn setup_pot_account() -> AccountIdOf { + let pot_account = Distribution::::pot_account(); + let value = T::NativeBalance::minimum_balance().saturating_mul(1_000_000_000u32.into()); + let _ = T::NativeBalance::set_balance(&pot_account, value); + pot_account +} + +fn add_projects(r: u32) -> Result<(), &'static str> { + for i in 0..r { + let (project_id, amount) = create_parameters::(i); + create_project::(project_id, amount); + } + + Ok(()) +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn claim_reward_for(r: Linear<1, { T::MaxProjects::get() }>) -> Result<(), BenchmarkError> { + /* setup initial state */ + add_projects::(r)?; + + ensure!(>::get().len() as u32 == r, "Project list setting failed !!"); + let index: usize = (r - 1).try_into().unwrap(); + let project = &>::get()[index]; + let _pot = setup_pot_account::(); + let caller: T::AccountId = whitelisted_caller(); + let epoch = T::EpochDurationBlocks::get(); + let mut when = T::BlockNumberProvider::current_block_number().saturating_add(epoch); + run_to_block::(when); + /* execute extrinsic or function */ + + let project_id = &project.project_id; + let spend = >::get(&project_id); + when = when.saturating_add(spend.unwrap().valid_from); + run_to_block::(when); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), project_id.clone()); + + Ok(()) + } + + impl_benchmark_test_suite!(Distribution, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/substrate/frame/distribution/src/functions.rs b/substrate/frame/distribution/src/functions.rs new file mode 100644 index 000000000000..07f6a614a7a0 --- /dev/null +++ b/substrate/frame/distribution/src/functions.rs @@ -0,0 +1,129 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helper functions for Distribution pallet. + +pub use super::*; +impl Pallet { + pub fn pot_account() -> AccountIdOf { + // Get Pot account + T::PotId::get().into_account_truncating() + } + /// Funds transfer from the Pot to a project account + pub fn spend(amount: BalanceOf, beneficiary: AccountIdOf) -> DispatchResult { + // Get Pot account + let pot_account: AccountIdOf = Self::pot_account(); + + //Operate the transfer + T::NativeBalance::transfer(&pot_account, &beneficiary, amount, Preservation::Preserve)?; + + Ok(()) + } + + /// Series of checks on the Pot, to ensure that we have enough funds + /// before executing a Spend --> used in tests. + pub fn pot_check(spend: BalanceOf) -> DispatchResult { + // Get Pot account + let pot_account = Self::pot_account(); + + // Check that the Pot as enough funds for the transfer + let balance = T::NativeBalance::balance(&pot_account); + let minimum_balance = T::NativeBalance::minimum_balance(); + let remaining_balance = balance.saturating_sub(spend); + + ensure!(remaining_balance > minimum_balance, Error::::InsufficientPotReserves); + ensure!(balance > spend, Error::::InsufficientPotReserves); + Ok(()) + } + + pub fn schedule_enactment(project: ProjectId, call: BoundedCallOf) -> DispatchResult { + let infos = Spends::::get(&project).ok_or(Error::::InexistentSpend)?; + let when = infos.valid_from; + T::Scheduler::schedule_named( + (DISTRIBUTION_ID, "enactment", project).using_encoded(sp_io::hashing::blake2_256), + DispatchTime::At(when), + None, + 63, + RawOrigin::Root.into(), + call, + )?; + + Ok(()) + } + + // Done in begin_block + // At the beginning of every Epoch, populate the `Spends` storage from the `Projects` storage + // (populated by an external process/pallet) make sure that there is enough funds before + // creating a new `SpendInfo`, and `ProjectInfo` corresponding to a created `SpendInfo` + // should be removed from the `Projects` storage. This is also a good place to Reserve the + // funds for created `SpendInfos`. the function will be use in a hook. + + pub fn begin_block() -> DispatchResult { + let mut projects = Projects::::get().into_inner(); + + if projects.len() > 0 { + // Reserve funds for the project + let pot = Self::pot_account(); + let balance = T::NativeBalance::balance(&pot); + let minimum_balance = T::NativeBalance::minimum_balance(); + + projects = projects + .iter() + .filter(|project| { + // check if the pot has enough fund for the Spend + // Check that the Pot as enough funds for the transfer + let remaining_balance = balance.saturating_sub(project.amount); + + // we check that holding the necessary amount cannot fail + if remaining_balance > minimum_balance && balance > project.amount { + // Create a new Spend + let new_spend = SpendInfo::::new(&project); + let _ = T::NativeBalance::hold( + &HoldReason::FundsReserved.into(), + &pot, + project.amount, + ) + .expect("Funds Reserve Failed"); + + // Emmit an event + let now = T::BlockNumberProvider::current_block_number(); + Self::deposit_event(Event::SpendCreated { + when: now, + amount: new_spend.amount, + project_id: project.project_id.clone(), + }); + } + return false; + }) + .map(|x| x.clone()) + .collect(); + } + + // Update project storage + let mut bounded = BoundedVec::, T::MaxProjects>::new(); + Projects::::mutate(|val| { + for p in projects { + // The number of elements in projects is ALWAYS + // egual or below T::MaxProjects at this point. + let _ = bounded.try_push(p); + } + *val = bounded; + }); + + Ok(()) + } +} diff --git a/substrate/frame/distribution/src/lib.rs b/substrate/frame/distribution/src/lib.rs new file mode 100644 index 000000000000..855fcb0694ca --- /dev/null +++ b/substrate/frame/distribution/src/lib.rs @@ -0,0 +1,271 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Distribution pallet. +//! +//! The Distribution Pallet handles the distribution of whitelisted projects rewards. +//! For now only one reward distribution pattern has been implemented, +//! but the pallet could be extended to offer to the user claiming rewards for a project, +//! a choice between more than one distribution pattern. +//! +//! ## Overview +//! +//! The Distribution Pallet receives a list of Whitelisted/Nominated Projects with their respective +//! calculated rewards. For each project, it will create a corresponding spend that will be stored +//! until the project reward can be claimed. At the moment, the reward claim period start +//! corresponds to: [beginning of an Epoch_Block + BufferPeriod] (The BufferPeriod can be configured +//! in the runtime). +//! +//! ### Terminology +//! +//! - **PotId:** Pot containing the funds used to pay the rewards. +//! - **BufferPeriod:** Minimum required buffer time period between project nomination and reward +//! claim. +//! +//! ## Interface +//! +//! ### Permissionless Functions +//! +//! * `pot_account`: Output the pot account_id. +//! +//! * `spend`: Funds transfer from the Pot to a project account. +//! +//! ### Privileged Functions +//! +//! * `claim_reward_for`: Claim a reward for a nominated/whitelisted project. +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; +mod functions; +mod types; +pub use pallet_scheduler as Schedule; +pub use types::*; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; +pub mod weights; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeCall: Parameter + + Dispatchable + + From> + + IsType<::RuntimeCall> + + From>; + + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Type to access the Balances Pallet. + type NativeBalance: fungible::Inspect + + fungible::Mutate + + fungible::hold::Inspect + + fungible::hold::Mutate + + fungible::freeze::Inspect + + fungible::freeze::Mutate; + + /// Provider for the block number. + type BlockNumberProvider: BlockNumberProvider>; + + /// Treasury account Id + #[pallet::constant] + type PotId: Get; + + /// The preimage provider. + type Preimages: QueryPreimage + StorePreimage; + + /// The Scheduler. + type Scheduler: ScheduleAnon< + BlockNumberFor, + CallOf, + PalletsOriginOf, + Hasher = Self::Hashing, + > + ScheduleNamed< + BlockNumberFor, + CallOf, + PalletsOriginOf, + Hasher = Self::Hashing, + >; + type RuntimeHoldReason: From; + + /// This the minimum required buffer time period between project nomination + /// and payment/reward_claim from the treasury. + #[pallet::constant] + type BufferPeriod: Get>; + + /// Maximum number projects that can be accepted by this pallet + #[pallet::constant] + type MaxProjects: Get; + + /// Epoch duration in blocks + #[pallet::constant] + type EpochDurationBlocks: Get>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// A reason for placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// Funds are held for a given buffer time before payment + #[codec(index = 0)] + FundsReserved, + } + + /// Spends that still have to be claimed. + #[pallet::storage] + pub(super) type Spends = + CountedStorageMap<_, Twox64Concat, ProjectId, SpendInfo, OptionQuery>; + + /// List of whitelisted projects to be rewarded + #[pallet::storage] + pub type Projects = + StorageValue<_, BoundedVec, T::MaxProjects>, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Reward successfully claimed + RewardClaimed { when: BlockNumberFor, amount: BalanceOf, project_id: ProjectId }, + + /// A Spend was created + SpendCreated { when: BlockNumberFor, amount: BalanceOf, project_id: ProjectId }, + + /// Not yet in the claiming period + NotClaimingPeriod { project_id: ProjectId, claiming_period: BlockNumberFor }, + } + + #[pallet::error] + pub enum Error { + /// Not enough Funds in the Pot + InsufficientPotReserves, + /// The funds transfer operation failed + TransferFailed, + /// Spend or Spend index does not exists + InexistentSpend, + /// No valid Account_id found + NoValidAccount, + /// No project available for funding + NoProjectAvailable, + /// The Funds transfer failed + FailedSpendOperation, + /// Still not in claiming period + NotClaimingPeriod, + /// Funds locking failed + FundsReserveFailed, + /// An invalid result was returned + InvalidResult, + } + + /*#[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + Self::begin_block(n) + } + }*/ + + #[pallet::call] + impl Pallet { + /// OPF Reward Claim logic + /// + /// ## Dispatch Origin + /// + /// Must be signed + /// + /// ## Details + /// + /// From this extrinsic any user can claim a reward for a nominated/whitelisted project. + /// + /// ### Parameters + /// - `project_id`: The account that will receive the reward. + /// + /// ### Errors + /// - [`Error::::InexistentSpend`]:Spend or Spend index does not exists + /// - [`Error::::NoValidAccount`]: No valid Account_id found + /// - [`Error::::NotClaimingPeriod`]: Still not in claiming period + /// + /// ## Events + /// Emits [`Event::::RewardClaimed`] if successful for a positive approval. + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::claim_reward_for(T::MaxProjects::get()))] + #[transactional] + pub fn claim_reward_for(origin: OriginFor, project_id: ProjectId) -> DispatchResult { + let _caller = ensure_signed(origin)?; + Self::begin_block()?; + let now = T::BlockNumberProvider::current_block_number(); + let info = Spends::::get(&project_id).ok_or(Error::::InexistentSpend)?; + if now >= info.valid_from { + let call0: ::RuntimeCall = + (Call::::execute_claim { project_id: project_id.clone() }).into(); + let call1: CallOf = call0.clone().into(); + let call = T::Preimages::bound(call1)?; + Self::schedule_enactment(project_id, call)?; + Ok(()) + } else{ + Self::deposit_event(Event::NotClaimingPeriod { + project_id, + claiming_period: info.valid_from, + }); + Ok(()) + } + + + + } + + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::claim_reward_for(T::MaxProjects::get()))] + #[transactional] + pub fn execute_claim(origin: OriginFor, project_id: ProjectId) -> DispatchResult { + let _caller = ensure_signed(origin)?; + let now = T::BlockNumberProvider::current_block_number(); + let pot = Self::pot_account(); + let info = Spends::::get(&project_id).ok_or(Error::::InexistentSpend)?; + + // Unlock the funds + T::NativeBalance::release( + &HoldReason::FundsReserved.into(), + &pot, + info.amount, + Precision::Exact, + )?; + // transfer the funds + Self::spend(info.amount, project_id.clone())?; + + Self::deposit_event(Event::RewardClaimed { + when: now, + amount: info.amount, + project_id, + }); + + Ok(()) + } + } +} diff --git a/substrate/frame/distribution/src/mock.rs b/substrate/frame/distribution/src/mock.rs new file mode 100644 index 000000000000..3e1cfbb2d745 --- /dev/null +++ b/substrate/frame/distribution/src/mock.rs @@ -0,0 +1,132 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for Distribution pallet. + +use crate as pallet_distribution; +pub use frame_support::{ + derive_impl, parameter_types, + traits::{ConstU128, ConstU16, ConstU32, EqualPrivilegeOnly, ConstU64, OnFinalize, OnInitialize}, + weights::Weight,PalletId, +}; +pub use sp_core::H256; +pub use sp_runtime::{ + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, + BuildStorage, +}; +pub use frame_system::EnsureRoot; +pub type Block = frame_system::mocking::MockBlock; +pub type Balance = u64; +pub type AccountId = u64; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub struct Test { + System: frame_system, + Balances: pallet_balances, + Preimage: pallet_preimage, + Scheduler: pallet_scheduler, + Distribution: pallet_distribution, + } +); + + +parameter_types! { + pub MaxWeight: Weight = Weight::from_parts(2_000_000_000_000, u64::MAX); +} +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = AccountId; + type AccountData = pallet_balances::AccountData; + type Block = Block; + type Lookup = IdentityLookup; +} +impl pallet_preimage::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type Consideration = (); +} +impl pallet_scheduler::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaxWeight; + type ScheduleOrigin = EnsureRoot; + type MaxScheduledPerBlock = ConstU32<100>; + type WeightInfo = (); + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type Preimages = Preimage; +} +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +parameter_types! { + pub const PotId: PalletId = PalletId(*b"py/potid"); + pub const Period: u32 = 1; + pub const MaxProjects:u32 = 50; + pub const EpochDurationBlocks:u32 = 5; +} +impl pallet_distribution::Config for Test { + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type NativeBalance = Balances; + type PotId = PotId; + type RuntimeHoldReason = RuntimeHoldReason; + type Scheduler = Scheduler; + type BufferPeriod = Period; + type MaxProjects = MaxProjects; + type EpochDurationBlocks = EpochDurationBlocks; + type BlockNumberProvider = System; + type Preimages = Preimage; + type WeightInfo = (); +} +//Define some accounts and use them +pub const ALICE: AccountId = 10; +pub const BOB: AccountId = 11; +pub const DAVE: AccountId = 12; +pub const EVE: AccountId = 13; +pub const BSX: Balance = 100_000_000_000; + +pub fn expect_events(e: Vec) { + e.into_iter().for_each(frame_system::Pallet::::assert_has_event); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let pot_account = PotId::get().into_account_truncating(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (ALICE, 200_000 * BSX), + (BOB, 200_000 * BSX), + (DAVE, 150_000 * BSX), + (EVE, 150_000 * BSX), + (pot_account, 150_000_000 * BSX), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/substrate/frame/distribution/src/tests.rs b/substrate/frame/distribution/src/tests.rs new file mode 100644 index 000000000000..d4430b25664d --- /dev/null +++ b/substrate/frame/distribution/src/tests.rs @@ -0,0 +1,213 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for Distribution pallet. + +pub use super::*; +use crate::mock::*; +use frame_support::{assert_noop, assert_ok}; + +pub fn next_block() { + System::set_block_number(::BlockNumberProvider::current_block_number() + 1); + AllPalletsWithSystem::on_initialize( + ::BlockNumberProvider::current_block_number(), + ); +} + +pub fn run_to_block(n: BlockNumberFor) { + while ::BlockNumberProvider::current_block_number() < n { + if ::BlockNumberProvider::current_block_number() > 1 { + AllPalletsWithSystem::on_finalize( + ::BlockNumberProvider::current_block_number(), + ); + } + next_block(); + } +} + +pub fn create_project(project_id: AccountId, amount: u128) { + let submission_block = ::BlockNumberProvider::current_block_number(); + let project: types::ProjectInfo = ProjectInfo { project_id, submission_block, amount: amount.try_into().unwrap() }; + Projects::::mutate(|value| { + let mut val = value.clone(); + let _ = val.try_push(project); + *value = val; + }); +} + +#[test] +fn spends_creation_works_but_not_executed_before_claim_period() { + new_test_ext().execute_with(|| { + // Add 3 projects + let amount1 = 1_000_000 * BSX; + let amount2 = 1_200_000 * BSX; + let amount3 = 2_000_000 * BSX; + create_project(ALICE, amount1.into()); + create_project(BOB, amount2.into()); + create_project(DAVE, amount3.into()); + + // The Spends Storage should be empty + assert_eq!(Spends::::count(), 0); + + // Move to epoch block => Warning: We set the system block at 1 in mock.rs, so now = + // Epoch_Block + 1 + let now = ::BlockNumberProvider::current_block_number() + .saturating_add(::EpochDurationBlocks::get().into()); + run_to_block(now); + let valid_from = now.saturating_add(::BufferPeriod::get().into()); + + // The 3 Spends are known + let alice_spend: types::SpendInfo = SpendInfo { + amount: amount1, + valid_from, + whitelisted_project: Some(ALICE), + claimed: false, + }; + + let bob_spend: types::SpendInfo = SpendInfo { + amount: amount2, + valid_from, + whitelisted_project: Some(BOB), + claimed: false, + }; + + let dave_spend: types::SpendInfo = SpendInfo { + amount: amount3, + valid_from, + whitelisted_project: Some(DAVE), + claimed: false, + }; + + let _=Distribution::claim_reward_for(RawOrigin::Signed(EVE).into(), ALICE); + let _=Distribution::claim_reward_for(RawOrigin::Signed(EVE).into(), BOB); + let _=Distribution::claim_reward_for(RawOrigin::Signed(EVE).into(), DAVE); + + // List of Spends actually created & stored + let list0: Vec<_> = Spends::::iter_keys().collect(); + let list: Vec<_> = list0.into_iter().map(|x| Spends::::get(x)).collect(); + + expect_events(vec![ + RuntimeEvent::Distribution(Event::NotClaimingPeriod { + project_id: list[0].clone().unwrap().whitelisted_project.unwrap(), + claiming_period: list[0].clone().unwrap().valid_from, + }), + RuntimeEvent::Distribution(Event::NotClaimingPeriod { + project_id: list[1].clone().unwrap().whitelisted_project.unwrap(), + claiming_period: list[1].clone().unwrap().valid_from, + }), + RuntimeEvent::Distribution(Event::NotClaimingPeriod { + project_id: list[2].clone().unwrap().whitelisted_project.unwrap(), + claiming_period: list[2].clone().unwrap().valid_from, + }), + ]); + + assert_eq!(Spends::::contains_key(ALICE),true); + assert_eq!(Spends::::get(ALICE),Some(alice_spend)); + assert_eq!(Spends::::get(BOB),Some(bob_spend)); + assert_eq!(Spends::::get(DAVE),Some(dave_spend)); + }) +} + +#[test] +fn funds_are_locked() { + new_test_ext().execute_with(|| { + // Add 3 projects + let amount1 = 1_000_000 * BSX; + let amount2 = 1_200_000 * BSX; + let amount3 = 2_000_000 * BSX; + create_project(ALICE, amount1.into()); + create_project(BOB, amount2.into()); + create_project(DAVE, amount3.into()); + + // The Spends Storage should be empty + assert_eq!(Spends::::count(), 0); + + // Move to epoch block => Warning: We set the system block at 1 in mock.rs, so now = + // Epoch_Block + 1 + let now = ::BlockNumberProvider::current_block_number() + .saturating_add(::EpochDurationBlocks::get().into()); + run_to_block(now); + + let total_on_hold = amount1.saturating_add(amount2).saturating_add(amount3); + let pot_account = Distribution::pot_account(); + let hold = + <::NativeBalance as fungible::hold::Inspect>::balance_on_hold( + &HoldReason::FundsReserved.into(), + &pot_account, + ); + assert_eq!(total_on_hold, hold); + }) +} + +#[test] +fn not_enough_funds_in_pot() { + new_test_ext().execute_with(|| { + // Add 3 projects + let amount1 = 50_000_000 * BSX; + let amount2 = 60_200_000 * BSX; + let amount3 = 70_000_000 * BSX; + create_project(ALICE, amount1.into()); + create_project(BOB, amount2.into()); + create_project(DAVE, amount3.into()); + + let total = amount1.saturating_add(amount2.saturating_add(amount3)); + assert_noop!(Distribution::pot_check(total), Error::::InsufficientPotReserves); + }) +} + +#[test] +fn funds_claim_works() { + new_test_ext().execute_with(|| { + // Add 3 projects + let amount1 = 1_000_000 * BSX; + let amount2 = 1_200_000 * BSX; + let amount3 = 2_000_000 * BSX; + create_project(ALICE, amount1.into()); + create_project(BOB, amount2.into()); + create_project(DAVE, amount3.into()); + + // The Spends Storage should be empty + assert_eq!(Spends::::count(), 0); + + assert_eq!(Projects::::get().len(), 3); + + // Move to epoch block => Warning: We set the system block at 1 in mock.rs, so now = + // Epoch_Block + 1 + let mut now = ::BlockNumberProvider::current_block_number() + .saturating_add(::EpochDurationBlocks::get().into()); + run_to_block(now); + + let project = Spends::::get(ALICE).unwrap(); + let project_id = project.whitelisted_project.unwrap(); + let balance_0 = + <::NativeBalance as fungible::Inspect>::balance(&project_id); + now = now.saturating_add(project.valid_from); + run_to_block(now); + + // Spend is in storage + //assert!(Spends::::get(ALICE).is_some()); + + assert_ok!(Distribution::claim_reward_for(RawOrigin::Signed(EVE).into(), project_id,)); + let balance_1 = + <::NativeBalance as fungible::Inspect>::balance(&project_id); + + assert!(balance_1 > balance_0); + assert_eq!(Projects::::get().len(), 0); + // Spend has been removed from storage + assert!(!Spends::::get(0).is_some()); + }) +} diff --git a/substrate/frame/distribution/src/types.rs b/substrate/frame/distribution/src/types.rs new file mode 100644 index 000000000000..2ddfa70a8e39 --- /dev/null +++ b/substrate/frame/distribution/src/types.rs @@ -0,0 +1,113 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types & Imports for Distribution pallet. + +pub use super::*; + +pub use frame_support::{ + pallet_prelude::*, + traits::{ + fungible, + fungible::{Inspect, Mutate, MutateHold}, + fungibles, + schedule::{ + v3::{Anon as ScheduleAnon, Named as ScheduleNamed}, + DispatchTime, MaybeHashed, + }, + tokens::{Precision, Preservation}, + Bounded, DefensiveOption, EnsureOrigin, LockIdentifier, OriginTrait, QueryPreimage, + StorePreimage, + }, + transactional, PalletId, Serialize, +}; +pub use frame_system::{pallet_prelude::*, RawOrigin}; +pub use scale_info::prelude::vec::Vec; +pub use sp_runtime::traits::{ + AccountIdConversion, BlockNumberProvider, Convert, Dispatchable, Saturating, StaticLookup, Zero, +}; +pub use sp_std::boxed::Box; +pub use weights::WeightInfo; + +pub type BalanceOf = <::NativeBalance as fungible::Inspect< + ::AccountId, +>>::Balance; +pub type AccountIdOf = ::AccountId; +/// A reward index. +pub type SpendIndex = u32; +pub type CallOf = ::RuntimeCall; +pub type BoundedCallOf = Bounded, ::Hashing>; +pub type ProjectId = AccountIdOf; +pub type PalletsOriginOf = + <::RuntimeOrigin as OriginTrait>::PalletsOrigin; +pub const DISTRIBUTION_ID: LockIdentifier = *b"distribu"; + +/// The state of the payment claim. +#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo, Default)] +pub enum SpendState { + /// Unclaimed + #[default] + Unclaimed, + /// Claimed & Paid. + Completed, + /// Claimed but Failed. + Failed, +} + +//Processed Reward status +#[derive(Encode, Decode, Clone, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct SpendInfo { + /// The asset amount of the spend. + pub amount: BalanceOf, + /// The block number from which the spend can be claimed(24h after SpendStatus Creation). + pub valid_from: BlockNumberFor, + /// Corresponding project id + pub whitelisted_project: Option>, + /// Has it been claimed? + pub claimed: bool, +} + +impl SpendInfo { + pub fn new(whitelisted: &ProjectInfo) -> Self { + let amount = whitelisted.amount; + let whitelisted_project = Some(whitelisted.project_id.clone()); + let claimed = false; + let valid_from = + >::block_number().saturating_add(T::BufferPeriod::get()); + + let spend = SpendInfo { amount, valid_from, whitelisted_project, claimed }; + + //Add it to the Spends storage + Spends::::insert(whitelisted.project_id.clone(), spend.clone()); + + spend + } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct ProjectInfo { + /// AcountId that will receive the payment. + pub project_id: ProjectId, + + /// Block at which the project was submitted for reward distribution + pub submission_block: BlockNumberFor, + + /// Amount to be lock & pay for this project + pub amount: BalanceOf, +} diff --git a/substrate/frame/distribution/src/weights.rs b/substrate/frame/distribution/src/weights.rs new file mode 100644 index 000000000000..c2c0eda2849d --- /dev/null +++ b/substrate/frame/distribution/src/weights.rs @@ -0,0 +1,109 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//! Autogenerated weights for `pallet_distribution` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-10-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Kazu-Rog`, CPU: `AMD Ryzen 9 4900HS with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/substrate-node +// benchmark +// pallet +// --chain +// dev +// --pallet +// pallet_distribution +// --extrinsic +// * +// --steps +// 50 +// --repeat +// 20 +// --output +// substrate/frame/distribution/src/weights.rs +// --wasm-execution +// compiled +// --heap-pages +// 4096 +// --template +// substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_distribution`. +pub trait WeightInfo { + fn claim_reward_for(r: u32, ) -> Weight; +} + +/// Weights for `pallet_distribution` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Distribution::Spends` (r:1 w:1) + /// Proof: `Distribution::Spends` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `Distribution::CounterForSpends` (r:1 w:1) + /// Proof: `Distribution::CounterForSpends` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 50]`. + fn claim_reward_for(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `864 + r * (11 ±0)` + // Estimated: `6196` + // Minimum execution time: 111_649_000 picoseconds. + Weight::from_parts(115_740_782, 6196) + // Standard Error: 22_665 + .saturating_add(Weight::from_parts(467_124, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Distribution::Spends` (r:1 w:1) + /// Proof: `Distribution::Spends` (`max_values`: None, `max_size`: Some(94), added: 2569, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `Distribution::CounterForSpends` (r:1 w:1) + /// Proof: `Distribution::CounterForSpends` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 50]`. + fn claim_reward_for(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `864 + r * (11 ±0)` + // Estimated: `6196` + // Minimum execution time: 111_649_000 picoseconds. + Weight::from_parts(115_740_782, 6196) + // Standard Error: 22_665 + .saturating_add(Weight::from_parts(467_124, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } +} diff --git a/substrate/frame/opf/Cargo.toml b/substrate/frame/opf/Cargo.toml new file mode 100644 index 000000000000..f70f522ac8a4 --- /dev/null +++ b/substrate/frame/opf/Cargo.toml @@ -0,0 +1,86 @@ +[package] +authors.workspace = true +description = "Optimist Project Funding - pallet allowing users to nominate projects to be funded, by locking their DOTS." +edition.workspace = true +homepage = "https://substrate.io" +license = "Apache-2.0" +name = "pallet-opf" +readme = "README.md" +repository.workspace = true +version = "0.1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, default-features = false } +frame-benchmarking = { optional = true, workspace = true, default-features = false } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +log = { workspace = true } +pallet-conviction-voting = { workspace = true, default-features = false } +pallet-distribution = { workspace = true, default-features = false } +scale-info = { features = [ + "derive", +], workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-io = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } + +[dev-dependencies] +pallet-assets = { workspace = true, default-features = true } +pallet-balances = { workspace = true, default-features = true } +pallet-sudo = { workspace = true, default-features = true } +pallet-timestamp = { workspace = true, default-features = true } +pallet-transaction-payment = { workspace = true, default-features = true } +pallet-transaction-payment-rpc-runtime-api = { workspace = true, default-features = true } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-conviction-voting/runtime-benchmarks", + "pallet-distribution/runtime-benchmarks", + "pallet-sudo/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "pallet-transaction-payment/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-conviction-voting/std", + "pallet-distribution/std", + "pallet-sudo/std", + "pallet-timestamp/std", + "pallet-transaction-payment-rpc-runtime-api/std", + "pallet-transaction-payment/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-assets/try-runtime", + "pallet-balances/try-runtime", + "pallet-conviction-voting/try-runtime", + "pallet-distribution/try-runtime", + "pallet-sudo/try-runtime", + "pallet-timestamp/try-runtime", + "pallet-transaction-payment/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/opf/README.md b/substrate/frame/opf/README.md new file mode 100644 index 000000000000..d2dc05a08a58 --- /dev/null +++ b/substrate/frame/opf/README.md @@ -0,0 +1,51 @@ +# OPF Pallet +## Overview + +The **OPF Pallet** handles the Optimistic Project Funding. +It allows users to nominate projects (whitelisted in OpenGov) with their DOT. +This mechanism will be funded with a constant stream of DOT taken directly from inflation +and distributed to projects based on the proportion of DOT that has nominated them. +The project rewards distribution is handled by the **Distribution Pallet** +The voting round timeline is described below for someone voting for a project with no conviction round_0 and +for another project with a conviction of 1x in round_1: + +``` +|----------Voting_Round_0-----------|----------Voting_Round_1-----------| +|----user_votes----|--funds0_locked-|----user_votes----|--funds1_locked-|--funds1_unlocked-| +|------------------|--Distribution--|------------------|--Distribution--| + +``` + + +**Relevant Links:** +- *Full description of the mechanism that was approved*: https://docs.google.com/document/d/1cl6CpWyqX7NCshV0aYT5a8ZTm75PWcLrEBcfk2I1tAA/edit#heading=h.hh40wjcakxp9 + +- *Polkadot's economics Forum post*: https://forum.polkadot.network/t/polkadots-economics-tools-to-shape-the-forseeable-future/8708?u=lolmcshizz + +- *Project discussion TG*: https://t.me/parachainstaking + +### Terminology + +- **MaxWhitelistedProjects:** Maximum number of Whitelisted projects that can be handled by the pallet. +- **VoteLockingPeriod:** Period during which voting is disabled. +- **VotingPeriod:** Period during which voting is enabled. +- **TemporaryRewards:** For test purposes only ⇒ used as a substitute for the inflation portion used for the rewards. + +## Interface + +### Dispatchable Functions + +#### Public + +These calls can be made from any externally held account capable of creating +a signed extrinsic. + +**Basic actions:** +- `vote` - This extrinsic allows users to [vote for/nominate] a whitelisted project using their funds. +- `remove_vote` - This extrinsic allows users to remove a cast vote, as long as it is within the vote-casting period. + The user can add a conviction to the amount appointed to the vote. + With a conviction of x2 for example, one additional funds locking period will be added after the end of the round, + as shown in the diagram above. +- `unlock_funds` - This extrinsic allows the user to unlock his funds, provided that the funds locking period has ended. + +License: Apache-2.0 diff --git a/substrate/frame/opf/src/benchmarking.rs b/substrate/frame/opf/src/benchmarking.rs new file mode 100644 index 000000000000..1b265ad0cd9c --- /dev/null +++ b/substrate/frame/opf/src/benchmarking.rs @@ -0,0 +1,169 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! OPF pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] +use super::*; + +use crate::Pallet as Opf; +//use pallet_distribution as Distribution; +use frame_benchmarking::{ + v1::{account, BenchmarkError}, + v2::*, +}; +use frame_support::ensure; +use frame_system::RawOrigin; +use sp_runtime::traits::One; + +const SEED: u32 = 0; + +fn run_to_block(n: frame_system::pallet_prelude::BlockNumberFor) { + while T::BlockNumberProvider::current_block_number() < n { + crate::Pallet::::on_finalize(T::BlockNumberProvider::current_block_number()); + frame_system::Pallet::::on_finalize(T::BlockNumberProvider::current_block_number()); + frame_system::Pallet::::set_block_number( + T::BlockNumberProvider::current_block_number() + One::one(), + ); + frame_system::Pallet::::on_initialize(T::BlockNumberProvider::current_block_number()); + crate::Pallet::::on_initialize(T::BlockNumberProvider::current_block_number()); + } +} + +fn on_idle_full_block() { + let remaining_weight = ::BlockWeights::get().max_block; + let when = T::BlockNumberProvider::current_block_number(); + frame_system::Pallet::::on_idle(when, remaining_weight); + crate::Pallet::::on_idle(when, remaining_weight); +} + +fn add_whitelisted_project(n: u32) -> Result<(), &'static str> { + for _i in 0..n { + let project_id = account("project", n, SEED); + WhiteListedProjectAccounts::::mutate(|value| { + let mut val = value.clone(); + let _ = val.try_push(project_id); + *value = val; + }) + } + + Ok(()) +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn vote(r: Linear<1, { T::MaxWhitelistedProjects::get() }>) -> Result<(), BenchmarkError> { + add_whitelisted_project::(r)?; + ensure!( + WhiteListedProjectAccounts::::get().len() as u32 == r, + "Project_id not set up correctly." + ); + + on_idle_full_block::(); + let when = T::BlockNumberProvider::current_block_number() + One::one(); + run_to_block::(when); + + ensure!(VotingRounds::::get(0).is_some(), "Round not created!"); + let caller_balance = T::NativeBalance::minimum_balance() * 10000u32.into(); + let caller: T::AccountId = whitelisted_caller(); + let _ = T::NativeBalance::mint_into(&caller, caller_balance); + let account = WhiteListedProjectAccounts::::get()[(r - 1) as usize].clone(); + let value: BalanceOf = T::NativeBalance::minimum_balance() * 100u32.into() * (r).into(); + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), account, value, true, Conviction::Locked1x); + + Ok(()) + } + + #[benchmark] + fn remove_vote( + r: Linear<1, { T::MaxWhitelistedProjects::get() }>, + ) -> Result<(), BenchmarkError> { + add_whitelisted_project::(r)?; + ensure!( + WhiteListedProjectAccounts::::get().len() as u32 == r, + "Project_id not set up correctly." + ); + + on_idle_full_block::(); + let when = T::BlockNumberProvider::current_block_number() + One::one(); + run_to_block::(when); + + ensure!(VotingRounds::::get(0).is_some(), "Round not created!"); + let caller_balance = T::NativeBalance::minimum_balance() * 10000u32.into(); + let caller: T::AccountId = whitelisted_caller(); + let _ = T::NativeBalance::mint_into(&caller, caller_balance); + let account = WhiteListedProjectAccounts::::get()[(r - 1) as usize].clone(); + let value: BalanceOf = T::NativeBalance::minimum_balance() * 100u32.into() * (r).into(); + Opf::::vote( + RawOrigin::Signed(caller.clone()).into(), + account.clone(), + value, + true, + Conviction::Locked1x, + )?; + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), account); + + Ok(()) + } + + #[benchmark] + fn unlock_funds( + r: Linear<1, { T::MaxWhitelistedProjects::get() }>, + ) -> Result<(), BenchmarkError> { + add_whitelisted_project::(r)?; + ensure!( + WhiteListedProjectAccounts::::get().len() as u32 == r, + "Project_id not set up correctly." + ); + + on_idle_full_block::(); + let mut when = T::BlockNumberProvider::current_block_number() + One::one(); + run_to_block::(when); + + ensure!(VotingRounds::::get(0).is_some(), "Round not created!"); + let caller_balance = T::NativeBalance::minimum_balance() * 1000000u32.into(); + let caller: T::AccountId = whitelisted_caller(); + let _ = T::NativeBalance::mint_into(&caller, caller_balance); + let account = WhiteListedProjectAccounts::::get()[(r - 1) as usize].clone(); + let value: BalanceOf = T::NativeBalance::minimum_balance() * 100u32.into() * (r).into(); + Opf::::vote( + RawOrigin::Signed(caller.clone()).into(), + account.clone(), + value, + true, + Conviction::Locked1x, + )?; + + when = Votes::::get(account.clone(), caller.clone()).unwrap().funds_unlock_block; + + run_to_block::(when); + on_idle_full_block::(); + + #[extrinsic_call] + _(RawOrigin::Signed(caller.clone()), account); + + Ok(()) + } + + impl_benchmark_test_suite!(Opf, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/substrate/frame/opf/src/functions.rs b/substrate/frame/opf/src/functions.rs new file mode 100644 index 000000000000..2dc6ced797f8 --- /dev/null +++ b/substrate/frame/opf/src/functions.rs @@ -0,0 +1,322 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Helper functions for OPF pallet. + +pub use super::*; +impl Pallet { + // Helper function for voting action. Existing votes are over-written, and Hold is adjusted + pub fn try_vote( + voter_id: VoterId, + project: ProjectId, + amount: BalanceOf, + is_fund: bool, + conviction: Conviction, + ) -> DispatchResult { + if !ProjectFunds::::contains_key(&project) { + let bounded = BoundedVec::, ConstU32<2>>::try_from(vec![ + BalanceOf::::zero(), + BalanceOf::::zero(), + ]) + .expect("It works"); + ProjectFunds::::insert(&project, bounded); + } + + let projects = WhiteListedProjectAccounts::::get(); + let conviction_fund = amount.saturating_add( + amount.saturating_mul(>::from(conviction).into()), + ); + + // Check that Project is whiteListed + ensure!(projects.contains(&project), Error::::NotWhitelistedProject); + + // Create vote infos and store/adjust them + let round_number = VotingRoundNumber::::get().saturating_sub(1); + let mut round = VotingRounds::::get(round_number).ok_or(Error::::NoRoundFound)?; + if is_fund { + round.total_positive_votes_amount = + round.total_positive_votes_amount.saturating_add(conviction_fund); + } else { + round.total_negative_votes_amount = + round.total_negative_votes_amount.saturating_add(conviction_fund); + } + + VotingRounds::::mutate(round_number, |val| { + *val = Some(round.clone()); + }); + + let mut new_vote = VoteInfo { + amount, + round: round.clone(), + is_fund, + conviction, + funds_unlock_block: round.round_ending_block, + }; + + // Update Funds unlock block according to the selected conviction + new_vote.funds_unlock(); + if Votes::::contains_key(&project, &voter_id) { + let old_vote = Votes::::get(&project, &voter_id).ok_or(Error::::NoVoteData)?; + let old_amount = old_vote.amount; + let old_conviction = old_vote.conviction; + let old_conviction_amount = old_amount.saturating_add( + old_amount.saturating_mul(>::from(old_conviction).into()), + ); + ProjectFunds::::mutate(&project, |val| { + let mut val0 = val.clone().into_inner(); + if is_fund { + val0[0] = val0[0 as usize] + .saturating_add(conviction_fund) + .saturating_sub(old_conviction_amount); + } else { + val0[1] = val0[1 as usize] + .saturating_add(conviction_fund) + .saturating_sub(old_conviction_amount); + } + *val = BoundedVec::, ConstU32<2>>::try_from(val0).expect("It works"); + }); + + Votes::::mutate(&project, &voter_id, |value| { + *value = Some(new_vote); + }); + + // Adjust locked amount + let total_hold = T::NativeBalance::total_balance_on_hold(&voter_id); + let new_hold = total_hold.saturating_sub(old_amount).saturating_add(amount); + T::NativeBalance::set_on_hold(&HoldReason::FundsReserved.into(), &voter_id, new_hold)?; + } else { + Votes::::insert(&project, &voter_id, new_vote); + ProjectFunds::::mutate(&project, |val| { + let mut val0 = val.clone().into_inner(); + if is_fund { + val0[0] = val0[0 as usize].saturating_add(conviction_fund); + } else { + val0[1] = val0[1 as usize].saturating_add(conviction_fund); + } + *val = BoundedVec::, ConstU32<2>>::try_from(val0).expect("It works"); + }); + // Lock the necessary amount + T::NativeBalance::hold(&HoldReason::FundsReserved.into(), &voter_id, amount)?; + } + + Ok(()) + } + + // Voting Period checks + pub fn period_check() -> DispatchResult { + // Get current voting round & check if we are in voting period or not + let current_round_index = VotingRoundNumber::::get().saturating_sub(1); + let round = VotingRounds::::get(current_round_index).ok_or(Error::::NoRoundFound)?; + let now = T::BlockNumberProvider::current_block_number(); + ensure!(now < round.voting_locked_block, Error::::VotePeriodClosed); + ensure!(now < round.round_ending_block, Error::::VotingRoundOver); + Ok(()) + } + + // Helper function for complete vote data removal from storage. + pub fn try_remove_vote(voter_id: VoterId, project: ProjectId) -> DispatchResult { + if Votes::::contains_key(&project, &voter_id) { + let infos = Votes::::get(&project, &voter_id).ok_or(Error::::NoVoteData)?; + let amount = infos.amount; + let conviction = infos.conviction; + let is_fund = infos.is_fund; + + let conviction_fund = amount.saturating_add( + amount.saturating_mul(>::from(conviction).into()), + ); + + // Update Round infos + let round_number = VotingRoundNumber::::get().saturating_sub(1); + let mut round = VotingRounds::::get(round_number).ok_or(Error::::NoRoundFound)?; + if is_fund { + round.total_positive_votes_amount = + round.total_positive_votes_amount.saturating_sub(conviction_fund); + } else { + round.total_negative_votes_amount = + round.total_negative_votes_amount.saturating_sub(conviction_fund); + } + + VotingRounds::::mutate(round_number, |val| { + *val = Some(round.clone()); + }); + + // Update ProjectFund Storage + ProjectFunds::::mutate(&project, |val| { + let mut val0 = val.clone().into_inner(); + if is_fund { + val0[0] = val0[0 as usize].saturating_sub(conviction_fund); + } else { + val0[1] = val0[1 as usize].saturating_sub(conviction_fund); + } + *val = BoundedVec::, ConstU32<2>>::try_from(val0).expect("It works"); + }); + + // Remove Vote Infos + Votes::::remove(&project, &voter_id); + + T::NativeBalance::release( + &HoldReason::FundsReserved.into(), + &voter_id, + amount, + Precision::Exact, + )?; + } + Ok(()) + } + + // The total reward to be distributed is a portion or inflation, determined in another pallet + // Reward calculation is executed within VotingLocked period --> "VotingLockBlock == + // EpochBeginningBlock" + pub fn calculate_rewards(total_reward: BalanceOf) -> DispatchResult { + let projects = WhiteListedProjectAccounts::::get(); + let round_number = VotingRoundNumber::::get().saturating_sub(1); + let round = VotingRounds::::get(round_number).ok_or(Error::::NoRoundFound)?; + if projects.clone().len() > 0 as usize { + let total_positive_votes_amount = round.total_positive_votes_amount; + let total_negative_votes_amount = round.total_negative_votes_amount; + + let total_votes_amount = + total_positive_votes_amount.saturating_sub(total_negative_votes_amount); + + // for each project, calculate the percentage of votes, the amount to be distributed, + // and then populate the storage Projects in pallet_distribution + for project in projects { + if ProjectFunds::::contains_key(&project) { + let funds = ProjectFunds::::get(&project); + let project_positive_reward = funds[0]; + let project_negative_reward = funds[1]; + + let project_reward = + project_positive_reward.saturating_sub(project_negative_reward); + + if !project_reward.is_zero() { + let project_percentage = + Percent::from_rational(project_reward, total_votes_amount); + let final_amount = project_percentage * total_reward; + + // Send calculated reward for distribution + let now = T::BlockNumberProvider::current_block_number() + .checked_add(&T::BufferPeriod::get()) + .ok_or(Error::::InvalidResult)?; + let project_info = ProjectInfo { + project_id: project.clone(), + submission_block: now, + amount: final_amount, + }; + + let mut rewarded = Distribution::Projects::::get(); + rewarded + .try_push(project_info.clone()) + .map_err(|_| Error::::MaximumProjectsNumber)?; + + Distribution::Projects::::mutate(|value| { + *value = rewarded; + }); + + let when = T::BlockNumberProvider::current_block_number(); + Self::deposit_event(Event::::ProjectFundingAccepted { + project_id: project, + when, + round_number, + amount: project_info.amount, + }) + } else { + // remove unfunded project from whitelisted storage + Self::remove_unfunded_project(project.clone())?; + let when = T::BlockNumberProvider::current_block_number(); + Self::deposit_event(Event::::ProjectFundingRejected { + when, + project_id: project, + }); + } + } + } + } + + Ok(()) + } + + pub fn remove_unfunded_project(project_id: ProjectId) -> DispatchResult { + WhiteListedProjectAccounts::::mutate(|value| { + let mut val = value.clone(); + val.retain(|x| *x != project_id); + *value = val; + }); + let when = T::BlockNumberProvider::current_block_number(); + + Self::deposit_event(Event::::ProjectUnlisted { when, project_id }); + + Ok(()) + } + + // To be executed in a hook, on_initialize + pub fn on_idle_function(now: BlockNumberFor, limit: Weight) -> Weight { + let mut meter = WeightMeter::with_limit(limit); + let max_block_weight = T::DbWeight::get().reads_writes(14, 8); + + if meter.try_consume(max_block_weight).is_err() { + return meter.consumed(); + } + let mut round_index = VotingRoundNumber::::get(); + + // No active round? + if round_index == 0 { + // Start the first voting round + let _round0 = VotingRoundInfo::::new(); + round_index = VotingRoundNumber::::get(); + } + + let current_round_index = round_index.saturating_sub(1); + + let round_infos = VotingRounds::::get(current_round_index).expect("InvalidResult"); + let voting_locked_block = round_infos.voting_locked_block; + let round_ending_block = round_infos.round_ending_block; + + // Conditions for distribution preparations are: + // - We are within voting_round period + // - We are past the voting_round_lock block + + if now == voting_locked_block { + // Emmit event + Self::deposit_event(Event::::VoteActionLocked { + when: now, + round_number: round_infos.round_number, + }); + // prepare reward distribution + // for now we are using the temporary-constant reward. + let _ = Self::calculate_rewards(T::TemporaryRewards::get()) + .map_err(|_| Error::::FailedRewardCalculation); + } + + // Create a new round when we reach the end of the current round. + if now == round_ending_block { + let _new_round = VotingRoundInfo::::new(); + // Clear WhiteListedProjectAccounts storage + WhiteListedProjectAccounts::::kill(); + // Clear Votes storage + Votes::::drain(); + // Clear ProjectFunds storage + ProjectFunds::::drain(); + // Emmit events + Self::deposit_event(Event::::VotingRoundEnded { + when: now, + round_number: round_infos.round_number, + }); + } + + meter.consumed() + } +} diff --git a/substrate/frame/opf/src/lib.rs b/substrate/frame/opf/src/lib.rs new file mode 100644 index 000000000000..415824f8abfa --- /dev/null +++ b/substrate/frame/opf/src/lib.rs @@ -0,0 +1,366 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! OPF pallet. +//! +//! The OPF Pallet handles the Optimistic Project Funding. +//! It allows users to nominate projects (whitelisted in OpenGov) with their DOT. +//! +//! ## Overview +//! +//! This mechanism will be funded with a constant stream of DOT taken directly from inflation +//! and distributed to projects based on the proportion of DOT that has nominated them. +//! The project rewards distribution is handled by the Distribution Pallet. +//! +//! ### Terminology +//! +//! - **MaxWhitelistedProjects:** Maximum number of Whitelisted projects that can be handled by the +//! pallet. +//! - **VoteLockingPeriod:** Period during which voting is disabled. +//! - **VotingPeriod:**Period during which voting is enabled. +//! - **TemporaryRewards:**For test purposes only ⇒ used as a substitute for the inflation portion +//! used for the rewards. +//! +//! ## Interface +//! +//! ### Permissionless Functions +//! +//! ### Privileged Functions +//! +//! * `vote`: Allows users to [vote for/nominate] a whitelisted project using their funds. +//! * `remove_vote`: Allows users to remove a casted vote. +//! * `unlock_funds`: Allows users to unlock funds related to a specific project. + +#![cfg_attr(not(feature = "std"), no_std)] + +// Re-export all pallet parts, this is needed to properly import the pallet into the runtime. +pub use pallet::*; +pub mod functions; +mod types; +pub use pallet_distribution as Distribution; +pub use types::*; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; +pub mod weights; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Distribution::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The minimum duration for which votes are locked + #[pallet::constant] + type VoteLockingPeriod: Get>; + + /// The maximum number of whitelisted projects per nomination round + #[pallet::constant] + type MaxWhitelistedProjects: Get; + + /// Time during which it is possible to cast a vote or change an existing vote. + #[pallet::constant] + type VotingPeriod: Get>; + + /// Used for Pallet testing only. Represents the Total Reward distributed + type TemporaryRewards: Get>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// Number of Voting Rounds executed so far + #[pallet::storage] + pub type VotingRoundNumber = StorageValue<_, u32, ValueQuery>; + + /// Returns Infos about a Voting Round agains the Voting Round index + #[pallet::storage] + pub type VotingRounds = + StorageMap<_, Twox64Concat, RoundIndex, VotingRoundInfo, OptionQuery>; + + /// Returns a list of Whitelisted Project accounts + #[pallet::storage] + pub type WhiteListedProjectAccounts = + StorageValue<_, BoundedVec, T::MaxWhitelistedProjects>, ValueQuery>; + + /// Returns (positive_funds,negative_funds) of Whitelisted Project accounts + #[pallet::storage] + pub type ProjectFunds = StorageMap< + _, + Twox64Concat, + ProjectId, + BoundedVec, ConstU32<2>>, + ValueQuery, + >; + + /// Returns Votes Infos against (project_id, voter_id) key + #[pallet::storage] + pub type Votes = StorageDoubleMap< + _, + Blake2_128Concat, + ProjectId, + Twox64Concat, + VoterId, + VoteInfo, + OptionQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Reward successfully claimed + RewardsAssigned { when: BlockNumberFor }, + + /// User's vote successfully submitted + VoteCasted { who: VoterId, when: BlockNumberFor, project_id: ProjectId }, + + /// User's vote successfully removed + VoteRemoved { who: VoterId, when: BlockNumberFor, project_id: ProjectId }, + + /// Project removed from whitelisted projects list + ProjectUnlisted { when: BlockNumberFor, project_id: ProjectId }, + + /// Project Funding Accepted by voters + ProjectFundingAccepted { + project_id: ProjectId, + when: BlockNumberFor, + round_number: u32, + amount: BalanceOf, + }, + + /// Project Funding rejected by voters + ProjectFundingRejected { when: BlockNumberFor, project_id: ProjectId }, + + /// A new voting round started + VotingRoundStarted { when: BlockNumberFor, round_number: u32 }, + + /// The users voting period ended. Reward calculation will start. + VoteActionLocked { when: BlockNumberFor, round_number: u32 }, + + /// The voting round ended + VotingRoundEnded { when: BlockNumberFor, round_number: u32 }, + + /// User's funds unlocked + FundsUnlocked { when: BlockNumberFor, amount: BalanceOf, project_id: ProjectId }, + } + + #[pallet::error] + pub enum Error { + /// This account is not connected to any WhiteListed Project. + NotWhitelistedProject, + + /// There are no whitelisted project + NoWhitelistedProject, + + /// The voting action failed. + VoteFailed, + + /// No such voting data + NoVoteData, + + /// An invalid result was returned + InvalidResult, + + /// Maximum number of projects submission for distribution as been reached + MaximumProjectsNumber, + + /// This voting round does not exists + NoRoundFound, + + /// Voting period closed for this round + VotePeriodClosed, + + /// Not enough funds to vote, you need to decrease your stake + NotEnoughFunds, + + /// The reward calculation failed due to an internal error + FailedRewardCalculation, + + /// Voting round is over + VotingRoundOver, + + /// User's funds still cannot be unlocked + FundsUnlockNotPermitted, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Weight: see `begin_block` + fn on_idle(n: BlockNumberFor, remaining_weight: Weight) -> Weight { + Self::on_idle_function(n, remaining_weight) + } + } + + #[pallet::call] + impl Pallet { + /// OPF voting logic + /// + /// ## Dispatch Origin + /// + /// Must be signed + /// + /// ## Details + /// + /// This extrinsic allows users to [vote for/nominate] a whitelisted project using their + /// funds. As a first implementation, the `conviction` parameter was not included for + /// simplicity, but /// should be in the next iteration of the pallet. The amount defined + /// by the user is locked and released only when the project reward is /// sent for + /// distribution, or when the project is not dimmed fundable. Users can edit an existing + /// vote within the vote-casting period. Then, during the vote-locked period, rewards are + /// calculated based on the total user amount attributed to each project by the user’s + /// votes. + /// + /// ### Parameters + /// - `project_id`: The account that will receive the reward. + /// - `amount`: Amount that will be locked in user’s balance to nominate a project. + /// - `is_fund`: Parameter that defines if user’s vote is in favor (*true*), or against + /// (*false*) + /// the project funding. + + /// ### Errors + /// - [`Error::::NotEnoughFunds`]: The user does not have enough balance to cast a vote + /// + /// ## Events + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::vote(T::MaxWhitelistedProjects::get()))] + #[transactional] + pub fn vote( + origin: OriginFor, + project_id: ProjectId, + #[pallet::compact] amount: BalanceOf, + is_fund: bool, + conviction: Conviction, + ) -> DispatchResult { + let voter = ensure_signed(origin)?; + // Get current voting round & check if we are in voting period or not + Self::period_check()?; + // Check that voter has enough funds to vote + let voter_balance = T::NativeBalance::total_balance(&voter); + ensure!(voter_balance > amount, Error::::NotEnoughFunds); + + // Check the total amount locked in other projects + let voter_holds = BalanceOf::::zero(); + let projects = WhiteListedProjectAccounts::::get(); + for project in projects { + if let Some(infos) = Votes::::get(&project, &voter) { + voter_holds.saturating_add(infos.amount); + } + } + + let available_funds = voter_balance.saturating_sub(voter_holds); + ensure!(available_funds > amount, Error::::NotEnoughFunds); + + // Vote action executed + + Self::try_vote(voter.clone(), project_id.clone(), amount, is_fund, conviction)?; + + let when = T::BlockNumberProvider::current_block_number(); + + Self::deposit_event(Event::::VoteCasted { who: voter, when, project_id }); + + Ok(()) + } + + /// OPF vote removal logic + /// + /// ## Dispatch Origin + /// + /// Must be signed + /// + /// ## Details + /// + /// This extrinsic allows users to remove a casted vote, as long as it is within the + /// vote-casting period. + /// + /// ### Parameters + /// - `project_id`: The account that will receive the reward. + /// + /// ### Errors + /// - [`Error::::NotEnoughFunds`]: The user does not have enough balance to cast a vote + /// + /// ## Events + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::remove_vote(T::MaxWhitelistedProjects::get()))] + #[transactional] + pub fn remove_vote(origin: OriginFor, project_id: ProjectId) -> DispatchResult { + let voter = ensure_signed(origin)?; + // Get current voting round & check if we are in voting period or not + Self::period_check()?; + // Removal action executed + Self::try_remove_vote(voter.clone(), project_id.clone())?; + + let when = T::BlockNumberProvider::current_block_number(); + Self::deposit_event(Event::::VoteRemoved { who: voter, when, project_id }); + + Ok(()) + } + + /// User's funds unlock + /// + /// ## Dispatch Origin + /// + /// Must be signed + /// + /// ## Details + /// + /// This extrinsic allows users to unlock funds related to a specific project, + /// provided the locking period (which is dependant of the conviction) has ended. + /// + /// ### Parameters + /// - `project_id`: The account that will receive the reward. + /// + /// ### Errors + /// - [`Error::::NotEnoughFunds`]: The user does not have enough balance to cast a vote + /// + /// ## Events + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::unlock_funds(T::MaxWhitelistedProjects::get()))] + #[transactional] + pub fn unlock_funds(origin: OriginFor, project: ProjectId) -> DispatchResult { + let voter = ensure_signed(origin)?; + let infos = Votes::::get(&project, &voter).ok_or(Error::::NoVoteData)?; + let amount = infos.amount; + let now = T::BlockNumberProvider::current_block_number(); + ensure!(now >= infos.funds_unlock_block, Error::::FundsUnlockNotPermitted); + // release voter's funds + T::NativeBalance::release( + &HoldReason::FundsReserved.into(), + &voter, + amount, + Precision::Exact, + )?; + + Self::deposit_event(Event::::FundsUnlocked { + when: now, + amount, + project_id: project, + }); + Ok(()) + } + } +} diff --git a/substrate/frame/opf/src/mock.rs b/substrate/frame/opf/src/mock.rs new file mode 100644 index 000000000000..59a93f5317d1 --- /dev/null +++ b/substrate/frame/opf/src/mock.rs @@ -0,0 +1,156 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test environment for OPF pallet. +use crate as pallet_opf; +pub use frame_support::{ + derive_impl, parameter_types, + traits::{ConstU128, ConstU16, ConstU32, ConstU64, OnFinalize, OnInitialize}, + PalletId, +}; +pub use sp_core::H256; +pub use sp_runtime::{ + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub type Block = frame_system::mocking::MockBlock; +pub type Balance = u128; +pub type AccountId = u64; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub struct Test { + System: frame_system, + Balances: pallet_balances, + Distribution: pallet_distribution, + Opf: pallet_opf, + } +); + +// Feel free to remove more items from this, as they are the same as +// `frame_system::config_preludes::TestDefaultConfig`. We have only listed the full `type` list here +// for verbosity. Same for `pallet_balances::Config`. +// https://paritytech.github.io/polkadot-sdk/master/frame_support/attr.derive_impl.html +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +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 = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = ConstU16<42>; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = ConstU32<10>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<10>; +} + +parameter_types! { + pub const PotId: PalletId = PalletId(*b"py/potid"); + pub const Period: u32 = 1; + pub const MaxProjects:u32 = 50; + pub const EpochDurationBlocks:u32 = 5; +} +impl pallet_distribution::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type NativeBalance = Balances; + type PotId = PotId; + type RuntimeHoldReason = RuntimeHoldReason; + type BufferPeriod = Period; + type MaxProjects = MaxProjects; + type EpochDurationBlocks = EpochDurationBlocks; + type BlockNumberProvider = System; + type WeightInfo = (); +} + +parameter_types! { + pub const MaxWhitelistedProjects: u32 = 5; + pub const TemporaryRewards: Balance = 100_000; + pub const VoteLockingPeriod:u32 = 10; + pub const VotingPeriod:u32 = 30; +} +impl pallet_opf::Config for Test { + type RuntimeEvent = RuntimeEvent; + type VoteLockingPeriod = VoteLockingPeriod; + type VotingPeriod = VotingPeriod; + type MaxWhitelistedProjects = MaxWhitelistedProjects; + type TemporaryRewards = TemporaryRewards; + type WeightInfo = (); +} + +//Define some accounts and use them +pub const ALICE: AccountId = 10; +pub const BOB: AccountId = 11; +pub const DAVE: AccountId = 12; +pub const EVE: AccountId = 13; +pub const BSX: Balance = 100_000_000_000; + +pub fn expect_events(e: Vec) { + e.into_iter().for_each(frame_system::Pallet::::assert_has_event); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let pot_account = PotId::get().into_account_truncating(); + + pallet_balances::GenesisConfig:: { + balances: vec![ + (ALICE, 200_000 * BSX), + (BOB, 200_000 * BSX), + (DAVE, 150_000 * BSX), + (EVE, 150_000 * BSX), + (pot_account, 150_000_000 * BSX), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/substrate/frame/opf/src/tests.rs b/substrate/frame/opf/src/tests.rs new file mode 100644 index 000000000000..c6dcfaf1fb08 --- /dev/null +++ b/substrate/frame/opf/src/tests.rs @@ -0,0 +1,412 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for OPF pallet. + +pub use super::*; +use crate::mock::*; +use frame_support::{assert_noop, assert_ok, traits::OnIdle}; + +pub fn next_block() { + System::set_block_number( + ::BlockNumberProvider::current_block_number() + 1, + ); + AllPalletsWithSystem::on_initialize( + ::BlockNumberProvider::current_block_number(), + ); + AllPalletsWithSystem::on_idle( + ::BlockNumberProvider::current_block_number(), + Weight::MAX, + ); +} + +pub fn run_to_block(n: BlockNumberFor) { + while ::BlockNumberProvider::current_block_number() < n { + if ::BlockNumberProvider::current_block_number() > 1 { + AllPalletsWithSystem::on_finalize( + ::BlockNumberProvider::current_block_number(), + ); + } + next_block(); + } +} + +pub fn create_project_list() { + const MAX_NUMBER: u64 = ::MaxWhitelistedProjects::get() as u64; + let mut bounded_vec = BoundedVec::::MaxWhitelistedProjects>::new(); + for i in 0..MAX_NUMBER { + let _ = bounded_vec.try_push(i + 100); + } + WhiteListedProjectAccounts::::mutate(|value| { + *value = bounded_vec; + }); +} + +#[test] +fn first_round_creation_works() { + new_test_ext().execute_with(|| { + // Creating whitelisted projects list succeeds + create_project_list(); + let project_list = WhiteListedProjectAccounts::::get(); + let max_number: u64 = ::MaxWhitelistedProjects::get() as u64; + assert_eq!(project_list.len(), max_number as usize); + + // First round is created + next_block(); + let voting_period = ::VotingPeriod::get(); + let voting_lock_period = ::VoteLockingPeriod::get(); + let now = + ::BlockNumberProvider::current_block_number(); + + let round_ending_block = now.saturating_add(voting_period.into()); + let voting_locked_block = round_ending_block.saturating_sub(voting_lock_period.into()); + + let first_round_info: VotingRoundInfo = VotingRoundInfo { + round_number: 0, + round_starting_block: now, + voting_locked_block, + round_ending_block, + total_positive_votes_amount: 0, + total_negative_votes_amount: 0, + }; + + // The righ event was emitted + expect_events(vec![RuntimeEvent::Opf(Event::VotingRoundStarted { + when: now, + round_number: 0, + })]); + + // The storage infos are correct + let round_info = VotingRounds::::get(0).unwrap(); + assert_eq!(first_round_info, round_info); + }) +} + +#[test] +fn voting_action_works() { + new_test_ext().execute_with(|| { + create_project_list(); + next_block(); + + // Bob nominate project_102 with an amount of 1000*BSX + assert_ok!(Opf::vote( + RawOrigin::Signed(BOB).into(), + 102, + 1000 * BSX, + true, + Conviction::Locked1x + )); + + // expected event is emitted + let voting_period = ::VotingPeriod::get(); + let voting_lock_period = ::VoteLockingPeriod::get(); + let now = + ::BlockNumberProvider::current_block_number(); + let round_ending_block = now.saturating_add(voting_period.into()); + let voting_locked_block = round_ending_block.saturating_sub(voting_lock_period.into()); + + let first_round_info: VotingRoundInfo = VotingRoundInfo { + round_number: 0, + round_starting_block: now, + voting_locked_block, + round_ending_block, + total_positive_votes_amount: 1000 * 2 * BSX, + total_negative_votes_amount: 0, + }; + + expect_events(vec![RuntimeEvent::Opf(Event::VoteCasted { + who: BOB, + when: now, + project_id: 102, + })]); + + let funds_unlock_block = round_ending_block.saturating_add(voting_lock_period.into()); + // The storage infos are correct + let first_vote_info: VoteInfo = VoteInfo { + amount: 1000 * BSX, + round: first_round_info, + is_fund: true, + conviction: Conviction::Locked1x, + funds_unlock_block, + }; + let vote_info = Votes::::get(102, BOB).unwrap(); + assert_eq!(first_vote_info, vote_info); + + // Voter's funds are locked + let locked_balance = + <::NativeBalance as fungible::hold::Inspect< + u64, + >>::balance_on_hold(&pallet_distribution::HoldReason::FundsReserved.into(), &BOB); + assert!(locked_balance > Zero::zero()); + }) +} + +#[test] +fn rewards_calculation_works() { + new_test_ext().execute_with(|| { + create_project_list(); + next_block(); + + // Bob nominate project_101 with an amount of 1000*BSX with a conviction x2 => equivalent to + // 3000*BSX locked + assert_ok!(Opf::vote( + RawOrigin::Signed(BOB).into(), + 101, + 1000 * BSX, + true, + Conviction::Locked2x + )); + let mut p1 = ProjectFunds::::get(101); + println!("the reward is: {:?}", p1); + + // Alice nominate project_101 with an amount of 5000*BSX with conviction 1x => equivalent to + // 10000*BSX locked + assert_ok!(Opf::vote( + RawOrigin::Signed(ALICE).into(), + 101, + 5000 * BSX, + true, + Conviction::Locked1x + )); + p1 = ProjectFunds::::get(101); + println!("the reward is: {:?}", p1); + + // DAVE vote against project_102 with an amount of 3000*BSX with conviction 1x => equivalent + // to 6000*BSX locked + assert_ok!(Opf::vote( + RawOrigin::Signed(DAVE).into(), + 102, + 3000 * BSX, + false, + Conviction::Locked1x + )); + // Eve nominate project_102 with an amount of 5000*BSX with conviction 1x => equivalent to + // 10000*BSX locked + assert_ok!(Opf::vote( + RawOrigin::Signed(EVE).into(), + 102, + 5000 * BSX, + true, + Conviction::Locked1x + )); + + let round_info = VotingRounds::::get(0).unwrap(); + + run_to_block(round_info.voting_locked_block); + let mut now = + ::BlockNumberProvider::current_block_number(); + + assert_eq!(now, round_info.voting_locked_block); + + // The right events are emitted + expect_events(vec![RuntimeEvent::Opf(Event::VoteActionLocked { + when: now, + round_number: 0, + })]); + + // The total equivalent amount voted is 17000 + // Project 101: 13000 -> ~76.5%; Project 102: 4000 -> ~23.5% + // Distributed to project 101 -> 44%*100_000; Distributed to project 102 -> 55%*100_000 + + assert_eq!(pallet_distribution::Projects::::get().len() == 2, true); + let rewards = pallet_distribution::Projects::::get(); + assert_eq!(rewards[0].project_id, 101); + assert_eq!(rewards[1].project_id, 102); + //assert_eq!(rewards[0].amount > rewards[1].amount, true); + //assert_eq!(rewards[0].amount, 76000); + assert_eq!(rewards[1].amount, 23000); + + // New round is properly started + run_to_block(round_info.round_ending_block); + now = round_info.round_ending_block; + expect_events(vec![RuntimeEvent::Opf(Event::VotingRoundEnded { + when: now, + round_number: 0, + })]); + let new_round_number = VotingRoundNumber::::get() - 1; + assert_eq!(new_round_number, 1); + let next_round = VotingRounds::::get(1); + assert_eq!(next_round.is_some(), true); + + now = now.saturating_add(::VoteLockingPeriod::get().into()); + // Unlock funds + run_to_block(now); + assert_ok!(Opf::unlock_funds(RawOrigin::Signed(ALICE).into(), 101)); + }) +} + +#[test] +fn vote_removal_works() { + new_test_ext().execute_with(|| { + create_project_list(); + next_block(); + + // Bob nominate project_102 with an amount of 1000 equivalent to + // 2000 locked + assert_ok!(Opf::vote(RawOrigin::Signed(BOB).into(), 101, 1000, true, Conviction::Locked1x)); + + // Eve nominate project_101 with an amount of 5000 with conviction 1x => equivalent to + // 10000 locked + assert_ok!(Opf::vote(RawOrigin::Signed(EVE).into(), 101, 5000, true, Conviction::Locked1x)); + + // ProjectFund is correctly updated + let project_fund_before = ProjectFunds::::get(101); + assert_eq!(project_fund_before[0], 12000); + + // Voter's funds are locked + let locked_balance0 = + <::NativeBalance as fungible::hold::Inspect< + u64, + >>::balance_on_hold(&pallet_distribution::HoldReason::FundsReserved.into(), &BOB); + + // Vote is in storage and balance is locked + assert!(locked_balance0 > Zero::zero()); + assert_eq!(Votes::::get(101, BOB).is_some(), true); + + // Bob removes his vote + assert_ok!(Opf::remove_vote(RawOrigin::Signed(BOB).into(), 101,)); + + let locked_balance1 = + <::NativeBalance as fungible::hold::Inspect< + u64, + >>::balance_on_hold(&pallet_distribution::HoldReason::FundsReserved.into(), &BOB); + + // No more votes in storage and balance is unlocked + assert_eq!(Votes::::get(101, BOB).is_some(), false); + assert_eq!(locked_balance1, Zero::zero()); + + // ProjectFund is correctly updated + let project_fund_after = ProjectFunds::::get(101); + assert_eq!(project_fund_after[0], 10000); + }) +} + +#[test] +fn not_enough_funds_to_vote() { + new_test_ext().execute_with(|| { + create_project_list(); + next_block(); + let balance_plus = < + ::NativeBalance as fungible::Inspect>::balance(&BOB)+100; + + // Bob vote with wrong amount + assert_noop!( + Opf::vote(RawOrigin::Signed(BOB).into(), 101, balance_plus, true, Conviction::Locked1x), + Error::::NotEnoughFunds + ); + }) +} + +#[test] +fn voting_action_locked() { + new_test_ext().execute_with(|| { + create_project_list(); + next_block(); + + let now = + ::BlockNumberProvider::current_block_number(); + + // Bob nominate project_101 with an amount of 1000 and conviction 3 => 3000 locked + assert_ok!(Opf::vote(RawOrigin::Signed(BOB).into(), 101, 1000, true, Conviction::Locked3x)); + + expect_events(vec![RuntimeEvent::Opf(Event::VoteCasted { + who: BOB, + when: now, + project_id: 101, + })]); + + // Bob nominate project_103 with an amount of 5000 + assert_ok!(Opf::vote(RawOrigin::Signed(BOB).into(), 103, 5000, true, Conviction::Locked1x)); + + // Voter's funds are locked + let locked_balance0 = + <::NativeBalance as fungible::hold::Inspect< + u64, + >>::balance_on_hold(&pallet_distribution::HoldReason::FundsReserved.into(), &BOB); + assert!(locked_balance0 > Zero::zero()); + + let round_info = VotingRounds::::get(0).unwrap(); + run_to_block(round_info.voting_locked_block); + + // Bob cannot edit his vote for project 101 + assert_noop!( + Opf::vote(RawOrigin::Signed(BOB).into(), 101, 2000, true, Conviction::Locked2x), + Error::::VotePeriodClosed + ); + }) +} + +#[test] +fn vote_move_works() { + new_test_ext().execute_with(|| { + create_project_list(); + next_block(); + + let now = + ::BlockNumberProvider::current_block_number(); + + // Bob nominate project_101 with an amount of 1000 with a conviction of 2 => amount+amount*2 + // is the amount allocated to the project + assert_ok!(Opf::vote(RawOrigin::Signed(BOB).into(), 101, 1000, true, Conviction::Locked2x)); + + expect_events(vec![RuntimeEvent::Opf(Event::VoteCasted { + who: BOB, + when: now, + project_id: 101, + })]); + + // 3000 is allocated to project 101 + let mut funds = ProjectFunds::::get(101); + assert_eq!(funds[0], 3000); + + // Bob nominate project_103 with an amount of 5000 with a conviction of 1 => amount+amount*1 + // is the amount allocated to the project + assert_ok!(Opf::vote(RawOrigin::Signed(BOB).into(), 103, 5000, true, Conviction::Locked1x)); + + // 10000 is allocated to project 103 + funds = ProjectFunds::::get(103); + assert_eq!(funds[0], 10000); + + // Voter's funds are locked + let mut locked_balance0 = + <::NativeBalance as fungible::hold::Inspect< + u64, + >>::balance_on_hold(&pallet_distribution::HoldReason::FundsReserved.into(), &BOB); + assert!(locked_balance0 > Zero::zero()); + assert_eq!(locked_balance0, 6000); + println!("locked: {:?}", locked_balance0); + + // Bob changes amount in project_103 to 4500 + assert_ok!(Opf::vote(RawOrigin::Signed(BOB).into(), 103, 4500, true, Conviction::Locked2x)); + + // Allocated amount to project 103 is now 13500 + funds = ProjectFunds::::get(103); + assert_eq!(funds[0], 13500); + + // Storage was correctly updated + let vote_info = Votes::::get(103, BOB).unwrap(); + + locked_balance0 = + <::NativeBalance as fungible::hold::Inspect< + u64, + >>::balance_on_hold(&pallet_distribution::HoldReason::FundsReserved.into(), &BOB); + + assert_eq!(4500, vote_info.amount); + assert_eq!(Conviction::Locked2x, vote_info.conviction); + assert_eq!(locked_balance0, 5500); + }) +} diff --git a/substrate/frame/opf/src/types.rs b/substrate/frame/opf/src/types.rs new file mode 100644 index 000000000000..a59abb490e3b --- /dev/null +++ b/substrate/frame/opf/src/types.rs @@ -0,0 +1,139 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types and imports for OPF pallet. + +pub use super::*; + +pub use frame_support::{ + pallet_prelude::*, + traits::{ + fungible, fungibles, + tokens::{Precision, Preservation}, + DefensiveOption, EnsureOrigin, + }, + transactional, + weights::WeightMeter, + PalletId, Serialize, +}; +pub use frame_system::{pallet_prelude::*, RawOrigin}; +pub use pallet_conviction_voting::Conviction; +pub use pallet_distribution::{ + fungible::InspectHold, AccountIdOf, BalanceOf, HoldReason, Inspect, Mutate, MutateHold, + ProjectId, ProjectInfo, +}; +pub use scale_info::prelude::vec; +pub use sp_runtime::{ + traits::{ + AccountIdConversion, BlockNumberProvider, CheckedAdd, CheckedSub, Convert, Saturating, + StaticLookup, Zero, + }, + Percent, +}; +pub use weights::WeightInfo; + +pub type RoundIndex = u32; +pub type VoterId = AccountIdOf; + +#[derive(Encode, Decode, Clone, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct VoteInfo { + /// The amount of stake/slash placed on this vote. + pub amount: BalanceOf, + + /// Round at which the vote was casted + pub round: VotingRoundInfo, + + /// Whether the vote is "fund" / "not fund" + pub is_fund: bool, + + pub conviction: Conviction, + + pub funds_unlock_block: BlockNumberFor, +} + +// If no conviction, user's funds are released at the end of the voting round +impl VoteInfo { + pub fn funds_unlock(&mut self) { + let conviction_coeff = >::from(self.conviction); + let funds_unlock_block = self + .round + .round_ending_block + .saturating_add(T::VoteLockingPeriod::get().saturating_mul(conviction_coeff.into())); + self.funds_unlock_block = funds_unlock_block; + } +} + +impl Default for VoteInfo { + // Dummy vote infos used to handle errors + fn default() -> Self { + // get round number + let round = VotingRounds::::get(0).expect("Round 0 exists"); + let amount = Zero::zero(); + let is_fund = false; + let conviction = Conviction::None; + let funds_unlock_block = round.round_ending_block; + VoteInfo { amount, round, is_fund, conviction, funds_unlock_block } + } +} + +/// Voting rounds are periodically created inside a hook on_initialize (use poll in the future) +#[derive(Encode, Decode, Clone, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct VotingRoundInfo { + pub round_number: u32, + pub round_starting_block: BlockNumberFor, + pub voting_locked_block: BlockNumberFor, + pub round_ending_block: BlockNumberFor, + pub total_positive_votes_amount: BalanceOf, + pub total_negative_votes_amount: BalanceOf, +} + +impl VotingRoundInfo { + pub fn new() -> Self { + let round_starting_block = T::BlockNumberProvider::current_block_number(); + let round_ending_block = round_starting_block + .clone() + .checked_add(&T::VotingPeriod::get()) + .expect("Invalid Result"); + let voting_locked_block = round_ending_block + .checked_sub(&T::VoteLockingPeriod::get()) + .expect("Invalid Result"); + + let round_number = VotingRoundNumber::::get(); + let new_number = round_number.checked_add(1).expect("Invalid Result"); + VotingRoundNumber::::put(new_number); + let total_positive_votes_amount = BalanceOf::::zero(); + let total_negative_votes_amount = BalanceOf::::zero(); + + Pallet::::deposit_event(Event::::VotingRoundStarted { + when: round_starting_block, + round_number, + }); + + let round_infos = VotingRoundInfo { + round_number, + round_starting_block, + voting_locked_block, + round_ending_block, + total_positive_votes_amount, + total_negative_votes_amount, + }; + VotingRounds::::insert(round_number, round_infos.clone()); + round_infos + } +} diff --git a/substrate/frame/opf/src/weights.rs b/substrate/frame/opf/src/weights.rs new file mode 100644 index 000000000000..feb63bdff96b --- /dev/null +++ b/substrate/frame/opf/src/weights.rs @@ -0,0 +1,191 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//! Autogenerated weights for `pallet_opf` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-10-20, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Kazu-Rog`, CPU: `AMD Ryzen 9 4900HS with Radeon Graphics` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/substrate-node +// benchmark +// pallet +// --chain +// dev +// --pallet +// pallet_opf +// --extrinsic +// * +// --steps +// 50 +// --repeat +// 20 +// --output +// substrate/frame/opf/src/weights.rs +// --wasm-execution +// compiled +// --heap-pages +// 4096 +// --template +// substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_opf`. +pub trait WeightInfo { + fn vote(r: u32, ) -> Weight; + fn remove_vote(r: u32, ) -> Weight; + fn unlock_funds(r: u32, ) -> Weight; +} + +/// Weights for `pallet_opf` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `OptimisticProjectFunding::VotingRoundNumber` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::VotingRoundNumber` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::VotingRounds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::VotingRounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::WhiteListedProjectAccounts` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::WhiteListedProjectAccounts` (`max_values`: Some(1), `max_size`: Some(2050), added: 2545, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::Votes` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::Votes` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::ProjectFunds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::ProjectFunds` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 64]`. + fn vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `311 + r * (32 ±0)` + // Estimated: `3820` + // Minimum execution time: 91_001_000 picoseconds. + Weight::from_parts(88_815_411, 3820) + // Standard Error: 4_901 + .saturating_add(Weight::from_parts(3_871_245, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `OptimisticProjectFunding::VotingRoundNumber` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::VotingRoundNumber` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::VotingRounds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::VotingRounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::Votes` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::Votes` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::ProjectFunds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::ProjectFunds` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 64]`. + fn remove_vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `658` + // Estimated: `3820` + // Minimum execution time: 79_960_000 picoseconds. + Weight::from_parts(81_690_153, 3820) + // Standard Error: 1_217 + .saturating_add(Weight::from_parts(1_572, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `OptimisticProjectFunding::Votes` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::Votes` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 64]`. + fn unlock_funds(_r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `523` + // Estimated: `3820` + // Minimum execution time: 70_792_000 picoseconds. + Weight::from_parts(97_718_659, 3820) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `OptimisticProjectFunding::VotingRoundNumber` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::VotingRoundNumber` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::VotingRounds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::VotingRounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::WhiteListedProjectAccounts` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::WhiteListedProjectAccounts` (`max_values`: Some(1), `max_size`: Some(2050), added: 2545, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::Votes` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::Votes` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::ProjectFunds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::ProjectFunds` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 64]`. + fn vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `311 + r * (32 ±0)` + // Estimated: `3820` + // Minimum execution time: 91_001_000 picoseconds. + Weight::from_parts(88_815_411, 3820) + // Standard Error: 4_901 + .saturating_add(Weight::from_parts(3_871_245, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `OptimisticProjectFunding::VotingRoundNumber` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::VotingRoundNumber` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::VotingRounds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::VotingRounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::Votes` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::Votes` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) + /// Storage: `OptimisticProjectFunding::ProjectFunds` (r:1 w:1) + /// Proof: `OptimisticProjectFunding::ProjectFunds` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 64]`. + fn remove_vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `658` + // Estimated: `3820` + // Minimum execution time: 79_960_000 picoseconds. + Weight::from_parts(81_690_153, 3820) + // Standard Error: 1_217 + .saturating_add(Weight::from_parts(1_572, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `OptimisticProjectFunding::Votes` (r:1 w:0) + /// Proof: `OptimisticProjectFunding::Votes` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// The range of component `r` is `[1, 64]`. + fn unlock_funds(_r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `523` + // Estimated: `3820` + // Minimum execution time: 70_792_000 picoseconds. + Weight::from_parts(97_718_659, 3820) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 28d6a2c3fb01..c1f4b463f964 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -85,6 +85,7 @@ std = [ "pallet-delegated-staking?/std", "pallet-democracy?/std", "pallet-dev-mode?/std", + "pallet-distribution?/std", "pallet-election-provider-multi-phase?/std", "pallet-election-provider-support-benchmarking?/std", "pallet-elections-phragmen?/std", @@ -112,6 +113,7 @@ std = [ "pallet-nomination-pools?/std", "pallet-offences-benchmarking?/std", "pallet-offences?/std", + "pallet-opf?/std", "pallet-paged-list?/std", "pallet-parameters?/std", "pallet-preimage?/std", @@ -278,6 +280,7 @@ runtime-benchmarks = [ "pallet-core-fellowship?/runtime-benchmarks", "pallet-delegated-staking?/runtime-benchmarks", "pallet-democracy?/runtime-benchmarks", + "pallet-distribution?/runtime-benchmarks", "pallet-election-provider-multi-phase?/runtime-benchmarks", "pallet-election-provider-support-benchmarking?/runtime-benchmarks", "pallet-elections-phragmen?/runtime-benchmarks", @@ -301,6 +304,7 @@ runtime-benchmarks = [ "pallet-nomination-pools?/runtime-benchmarks", "pallet-offences-benchmarking?/runtime-benchmarks", "pallet-offences?/runtime-benchmarks", + "pallet-opf?/runtime-benchmarks", "pallet-paged-list?/runtime-benchmarks", "pallet-parameters?/runtime-benchmarks", "pallet-preimage?/runtime-benchmarks", @@ -412,6 +416,7 @@ try-runtime = [ "pallet-delegated-staking?/try-runtime", "pallet-democracy?/try-runtime", "pallet-dev-mode?/try-runtime", + "pallet-distribution?/try-runtime", "pallet-election-provider-multi-phase?/try-runtime", "pallet-elections-phragmen?/try-runtime", "pallet-fast-unstake?/try-runtime", @@ -434,6 +439,7 @@ try-runtime = [ "pallet-node-authorization?/try-runtime", "pallet-nomination-pools?/try-runtime", "pallet-offences?/try-runtime", + "pallet-opf?/try-runtime", "pallet-paged-list?/try-runtime", "pallet-parameters?/try-runtime", "pallet-preimage?/try-runtime", @@ -541,7 +547,9 @@ with-tracing = [ "sp-tracing?/with-tracing", "sp-tracing?/with-tracing", ] -runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-fixtures", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"] + +runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-distribution", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-opf", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-fixtures", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"] + runtime = [ "frame-benchmarking", "frame-benchmarking-pallet-pov", @@ -1013,6 +1021,11 @@ path = "../substrate/frame/examples/dev-mode" default-features = false optional = true +[dependencies.pallet-distribution] +path = "../substrate/frame/distribution" +default-features = false +optional = true + [dependencies.pallet-election-provider-multi-phase] path = "../substrate/frame/election-provider-multi-phase" default-features = false @@ -1148,6 +1161,11 @@ path = "../substrate/frame/offences/benchmarking" default-features = false optional = true +[dependencies.pallet-opf] +path = "../substrate/frame/opf" +default-features = false +optional = true + [dependencies.pallet-paged-list] path = "../substrate/frame/paged-list" default-features = false diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index 2216864fad0f..443f0d3b8fc9 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -435,6 +435,10 @@ pub use pallet_democracy; #[cfg(feature = "pallet-dev-mode")] pub use pallet_dev_mode; +/// FRAME pallet to distribute funds to whitelisted projects. +#[cfg(feature = "pallet-distribution")] +pub use pallet_distribution; + /// PALLET two phase election providers. #[cfg(feature = "pallet-election-provider-multi-phase")] pub use pallet_election_provider_multi_phase; @@ -543,6 +547,11 @@ pub use pallet_offences; #[cfg(feature = "pallet-offences-benchmarking")] pub use pallet_offences_benchmarking; +/// Optimist Project Funding - pallet allowing users to nominate projects to be funded, by +/// locking their DOTS. +#[cfg(feature = "pallet-opf")] +pub use pallet_opf; + /// FRAME pallet that provides a paged list data structure. #[cfg(feature = "pallet-paged-list")] pub use pallet_paged_list;