diff --git a/Cargo.lock b/Cargo.lock index 2e254e7d11843..45158288298b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4871,6 +4871,7 @@ dependencies = [ "pallet-contracts", "pallet-contracts-primitives", "pallet-contracts-rpc-runtime-api", + "pallet-conviction-voting", "pallet-democracy", "pallet-election-provider-multi-phase", "pallet-elections-phragmen", @@ -4889,6 +4890,7 @@ dependencies = [ "pallet-proxy", "pallet-randomness-collective-flip", "pallet-recovery", + "pallet-referenda", "pallet-scheduler", "pallet-session", "pallet-session-benchmarking", @@ -5672,6 +5674,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-conviction-voting" +version = "4.0.0-dev" +dependencies = [ + "assert_matches", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-scheduler", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-democracy" version = "4.0.0-dev" @@ -6122,6 +6143,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-referenda" +version = "4.0.0-dev" +dependencies = [ + "assert_matches", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-preimage", + "pallet-scheduler", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-scheduler" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 48a36419eb746..a31d8011a9f44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,9 @@ members = [ "frame/authority-discovery", "frame/authorship", "frame/babe", + "frame/bags-list", + "frame/bags-list/fuzzer", + "frame/bags-list/remote-tests", "frame/balances", "frame/beefy", "frame/beefy-mmr", @@ -78,6 +81,7 @@ members = [ "frame/contracts", "frame/contracts/rpc", "frame/contracts/rpc/runtime-api", + "frame/conviction-voting", "frame/democracy", "frame/try-runtime", "frame/election-provider-multi-phase", @@ -104,6 +108,7 @@ members = [ "frame/proxy", "frame/randomness-collective-flip", "frame/recovery", + "frame/referenda", "frame/scheduler", "frame/scored-pool", "frame/session", @@ -133,9 +138,6 @@ members = [ "frame/uniques", "frame/utility", "frame/vesting", - "frame/bags-list", - "frame/bags-list/remote-tests", - "frame/bags-list/fuzzer", "primitives/api", "primitives/api/proc-macro", "primitives/api/test", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 647562caff30c..2aad40b4f121d 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -64,6 +64,7 @@ pallet-collective = { version = "4.0.0-dev", default-features = false, path = ". pallet-contracts = { version = "4.0.0-dev", default-features = false, path = "../../../frame/contracts" } pallet-contracts-primitives = { version = "5.0.0", default-features = false, path = "../../../frame/contracts/common/" } pallet-contracts-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/contracts/rpc/runtime-api/" } +pallet-conviction-voting = { version = "4.0.0-dev", default-features = false, path = "../../../frame/conviction-voting" } pallet-democracy = { version = "4.0.0-dev", default-features = false, path = "../../../frame/democracy" } pallet-election-provider-multi-phase = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-multi-phase" } pallet-elections-phragmen = { version = "5.0.0-dev", default-features = false, path = "../../../frame/elections-phragmen" } @@ -82,9 +83,8 @@ pallet-preimage = { version = "4.0.0-dev", default-features = false, path = "../ pallet-proxy = { version = "4.0.0-dev", default-features = false, path = "../../../frame/proxy" } pallet-randomness-collective-flip = { version = "4.0.0-dev", default-features = false, path = "../../../frame/randomness-collective-flip" } pallet-recovery = { version = "4.0.0-dev", default-features = false, path = "../../../frame/recovery" } -pallet-session = { version = "4.0.0-dev", features = [ - "historical", -], path = "../../../frame/session", default-features = false } +pallet-referenda = { version = "4.0.0-dev", default-features = false, path = "../../../frame/referenda" } +pallet-session = { version = "4.0.0-dev", features = [ "historical" ], path = "../../../frame/session", default-features = false } pallet-session-benchmarking = { version = "4.0.0-dev", path = "../../../frame/session/benchmarking", default-features = false, optional = true } pallet-staking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking" } pallet-staking-reward-curve = { version = "4.0.0-dev", default-features = false, path = "../../../frame/staking/reward-curve" } @@ -125,6 +125,7 @@ std = [ "pallet-contracts/std", "pallet-contracts-primitives/std", "pallet-contracts-rpc-runtime-api/std", + "pallet-conviction-voting/std", "pallet-democracy/std", "pallet-elections-phragmen/std", "frame-executive/std", @@ -170,6 +171,7 @@ std = [ "pallet-utility/std", "sp-version/std", "pallet-society/std", + "pallet-referenda/std", "pallet-recovery/std", "pallet-uniques/std", "pallet-vesting/std", @@ -183,7 +185,6 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", - "pallet-election-provider-multi-phase/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-babe/runtime-benchmarks", @@ -193,7 +194,9 @@ runtime-benchmarks = [ "pallet-child-bounties/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-contracts/runtime-benchmarks", + "pallet-conviction-voting/runtime-benchmarks", "pallet-democracy/runtime-benchmarks", + "pallet-election-provider-multi-phase/runtime-benchmarks", "pallet-elections-phragmen/runtime-benchmarks", "pallet-gilt/runtime-benchmarks", "pallet-grandpa/runtime-benchmarks", @@ -204,9 +207,12 @@ runtime-benchmarks = [ "pallet-membership/runtime-benchmarks", "pallet-mmr/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", + "pallet-offences-benchmarking", "pallet-preimage/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", + "pallet-referenda/runtime-benchmarks", + "pallet-session-benchmarking", "pallet-society/runtime-benchmarks", "pallet-staking/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", @@ -216,8 +222,6 @@ runtime-benchmarks = [ "pallet-utility/runtime-benchmarks", "pallet-uniques/runtime-benchmarks", "pallet-vesting/runtime-benchmarks", - "pallet-offences-benchmarking", - "pallet-session-benchmarking", "frame-system-benchmarking", "hex-literal", ] @@ -234,35 +238,37 @@ try-runtime = [ "pallet-child-bounties/try-runtime", "pallet-collective/try-runtime", "pallet-contracts/try-runtime", + "pallet-conviction-voting/try-runtime", "pallet-democracy/try-runtime", + "pallet-election-provider-multi-phase/try-runtime", "pallet-elections-phragmen/try-runtime", + "pallet-gilt/try-runtime", "pallet-grandpa/try-runtime", + "pallet-identity/try-runtime", "pallet-im-online/try-runtime", "pallet-indices/try-runtime", "pallet-lottery/try-runtime", "pallet-membership/try-runtime", "pallet-mmr/try-runtime", "pallet-multisig/try-runtime", - "pallet-identity/try-runtime", - "pallet-scheduler/try-runtime", "pallet-offences/try-runtime", "pallet-preimage/try-runtime", "pallet-proxy/try-runtime", "pallet-randomness-collective-flip/try-runtime", + "pallet-recovery/try-runtime", + "pallet-referenda/try-runtime", + "pallet-scheduler/try-runtime", "pallet-session/try-runtime", + "pallet-society/try-runtime", "pallet-staking/try-runtime", "pallet-sudo/try-runtime", - "pallet-election-provider-multi-phase/try-runtime", "pallet-timestamp/try-runtime", "pallet-tips/try-runtime", "pallet-transaction-payment/try-runtime", "pallet-treasury/try-runtime", - "pallet-utility/try-runtime", - "pallet-society/try-runtime", - "pallet-recovery/try-runtime", "pallet-uniques/try-runtime", + "pallet-utility/try-runtime", "pallet-vesting/try-runtime", - "pallet-gilt/try-runtime", ] # Make contract callable functions marked as __unstable__ available. Do not enable # on live chains as those are subject to change. diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index e70adc6aa7238..0a13b795919c0 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -698,6 +698,83 @@ impl pallet_bags_list::Config for Runtime { type BagThresholds = BagThresholds; } +parameter_types! { + pub const VoteLockingPeriod: BlockNumber = 30 * DAYS; +} + +impl pallet_conviction_voting::Config for Runtime { + type WeightInfo = pallet_conviction_voting::weights::SubstrateWeight; + type Event = Event; + type Currency = Balances; + type VoteLockingPeriod = VoteLockingPeriod; + type MaxVotes = ConstU32<512>; + type MaxTurnout = frame_support::traits::TotalIssuanceOf; + type Polls = Referenda; +} + +parameter_types! { + pub const AlarmInterval: BlockNumber = 1; + pub const SubmissionDeposit: Balance = 100 * DOLLARS; + pub const UndecidingTimeout: BlockNumber = 28 * DAYS; +} + +pub struct TracksInfo; +impl pallet_referenda::TracksInfo for TracksInfo { + type Id = u8; + type Origin = ::PalletsOrigin; + fn tracks() -> &'static [(Self::Id, pallet_referenda::TrackInfo)] { + static DATA: [(u8, pallet_referenda::TrackInfo); 1] = [( + 0u8, + pallet_referenda::TrackInfo { + name: "root", + max_deciding: 1, + decision_deposit: 10, + prepare_period: 4, + decision_period: 4, + confirm_period: 2, + min_enactment_period: 4, + min_approval: pallet_referenda::Curve::LinearDecreasing { + begin: Perbill::from_percent(100), + delta: Perbill::from_percent(50), + }, + min_turnout: pallet_referenda::Curve::LinearDecreasing { + begin: Perbill::from_percent(100), + delta: Perbill::from_percent(100), + }, + }, + )]; + &DATA[..] + } + fn track_for(id: &Self::Origin) -> Result { + if let Ok(system_origin) = frame_system::RawOrigin::try_from(id.clone()) { + match system_origin { + frame_system::RawOrigin::Root => Ok(0), + _ => Err(()), + } + } else { + Err(()) + } + } +} + +impl pallet_referenda::Config for Runtime { + type WeightInfo = pallet_referenda::weights::SubstrateWeight; + type Call = Call; + type Event = Event; + type Scheduler = Scheduler; + type Currency = pallet_balances::Pallet; + type CancelOrigin = EnsureRoot; + type KillOrigin = EnsureRoot; + type Slash = (); + type Votes = pallet_conviction_voting::VotesOf; + type Tally = pallet_conviction_voting::TallyOf; + type SubmissionDeposit = SubmissionDeposit; + type MaxQueued = ConstU32<100>; + type UndecidingTimeout = UndecidingTimeout; + type AlarmInterval = AlarmInterval; + type Tracks = TracksInfo; +} + parameter_types! { pub const LaunchPeriod: BlockNumber = 28 * 24 * 60 * MINUTES; pub const VotingPeriod: BlockNumber = 28 * 24 * 60 * MINUTES; @@ -1336,6 +1413,8 @@ construct_runtime!( TransactionStorage: pallet_transaction_storage, BagsList: pallet_bags_list, ChildBounties: pallet_child_bounties, + Referenda: pallet_referenda, + ConvictionVoting: pallet_conviction_voting, } ); @@ -1405,6 +1484,7 @@ mod benches { [pallet_bounties, Bounties] [pallet_child_bounties, ChildBounties] [pallet_collective, Council] + [pallet_conviction_voting, ConvictionVoting] [pallet_contracts, Contracts] [pallet_democracy, Democracy] [pallet_election_provider_multi_phase, ElectionProviderMultiPhase] @@ -1421,6 +1501,7 @@ mod benches { [pallet_offences, OffencesBench::] [pallet_preimage, Preimage] [pallet_proxy, Proxy] + [pallet_referenda, Referenda] [pallet_scheduler, Scheduler] [pallet_session, SessionBench::] [pallet_staking, Staking] diff --git a/frame/balances/src/lib.rs b/frame/balances/src/lib.rs index 80e8bf577abe6..37114f385aa7f 100644 --- a/frame/balances/src/lib.rs +++ b/frame/balances/src/lib.rs @@ -1517,7 +1517,7 @@ where .map_err(|_| Error::::LiquidityRestrictions)?; // TODO: This is over-conservative. There may now be other providers, and - // this pallet may not even be a provider. + // this pallet may not even be a provider. let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; let allow_death = allow_death && system::Pallet::::can_dec_provider(transactor); diff --git a/frame/conviction-voting/Cargo.toml b/frame/conviction-voting/Cargo.toml new file mode 100644 index 0000000000000..ab62065c1c546 --- /dev/null +++ b/frame/conviction-voting/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "pallet-conviction-voting" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for conviction voting in referenda" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.126", optional = true, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } +sp-io = { version = "5.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "5.0.0", default-features = false, path = "../../primitives/runtime" } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +assert_matches = "1.3.0" + +[dev-dependencies] +sp-core = { version = "5.0.0", path = "../../primitives/core" } +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-scheduler = { version = "4.0.0-dev", path = "../scheduler" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-io/std", + "frame-benchmarking/std", + "frame-support/std", + "sp-runtime/std", + "frame-system/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/conviction-voting/README.md b/frame/conviction-voting/README.md new file mode 100644 index 0000000000000..5dc5d526d5c23 --- /dev/null +++ b/frame/conviction-voting/README.md @@ -0,0 +1,8 @@ +# Voting Pallet + +- [`assembly::Config`](https://docs.rs/pallet-assembly/latest/pallet_assembly/trait.Config.html) +- [`Call`](https://docs.rs/pallet-assembly/latest/pallet_assembly/enum.Call.html) + +## Overview + +Pallet for voting in referenda. diff --git a/frame/conviction-voting/src/benchmarking.rs b/frame/conviction-voting/src/benchmarking.rs new file mode 100644 index 0000000000000..2beee4f3b49d2 --- /dev/null +++ b/frame/conviction-voting/src/benchmarking.rs @@ -0,0 +1,278 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 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. + +//! ConvictionVoting pallet benchmarking. + +use super::*; + +use assert_matches::assert_matches; +use frame_benchmarking::{account, benchmarks, whitelist_account}; +use frame_support::{ + dispatch::RawOrigin, + traits::{fungible, Currency, Get}, +}; +use sp_runtime::traits::Bounded; +use sp_std::collections::btree_map::BTreeMap; + +use crate::Pallet as ConvictionVoting; + +const SEED: u32 = 0; + +/// Fill all classes as much as possible up to `MaxVotes` and return the Class with the most votes +/// ongoing. +fn fill_voting() -> (ClassOf, BTreeMap, Vec>>) { + let mut r = BTreeMap::, Vec>>::new(); + for class in T::Polls::classes().into_iter() { + for _ in 0..T::MaxVotes::get() { + match T::Polls::create_ongoing(class.clone()) { + Ok(i) => r.entry(class.clone()).or_default().push(i), + Err(()) => break, + } + } + } + let c = r.iter().max_by_key(|(_, ref v)| v.len()).unwrap().0.clone(); + (c, r) +} + +fn funded_account(name: &'static str, index: u32) -> T::AccountId { + let caller: T::AccountId = account(name, index, SEED); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + caller +} + +fn account_vote(b: BalanceOf) -> AccountVote> { + let v = Vote { aye: true, conviction: Conviction::Locked1x }; + + AccountVote::Standard { vote: v, balance: b } +} + +benchmarks! { + where_clause { where T::MaxVotes: core::fmt::Debug } + + vote_new { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + let account_vote = account_vote::(100u32.into()); + + let (class, all_polls) = fill_voting::(); + let polls = &all_polls[&class]; + let r = polls.len() - 1; + // We need to create existing votes + for i in polls.iter().skip(1) { + ConvictionVoting::::vote(RawOrigin::Signed(caller.clone()).into(), *i, account_vote.clone())?; + } + let votes = match VotingFor::::get(&caller, &class) { + Voting::Casting(Casting { votes, .. }) => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r as usize, "Votes were not recorded."); + + let index = polls[0]; + }: vote(RawOrigin::Signed(caller.clone()), index, account_vote) + verify { + assert_matches!( + VotingFor::::get(&caller, &class), + Voting::Casting(Casting { votes, .. }) if votes.len() == (r + 1) as usize + ); + } + + vote_existing { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + let old_account_vote = account_vote::(100u32.into()); + + let (class, all_polls) = fill_voting::(); + let polls = &all_polls[&class]; + let r = polls.len(); + // We need to create existing votes + for i in polls.iter() { + ConvictionVoting::::vote(RawOrigin::Signed(caller.clone()).into(), *i, old_account_vote.clone())?; + } + let votes = match VotingFor::::get(&caller, &class) { + Voting::Casting(Casting { votes, .. }) => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r, "Votes were not recorded."); + + let new_account_vote = account_vote::(200u32.into()); + let index = polls[0]; + }: vote(RawOrigin::Signed(caller.clone()), index, new_account_vote) + verify { + assert_matches!( + VotingFor::::get(&caller, &class), + Voting::Casting(Casting { votes, .. }) if votes.len() == r as usize + ); + } + + remove_vote { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + let old_account_vote = account_vote::(100u32.into()); + + let (class, all_polls) = fill_voting::(); + let polls = &all_polls[&class]; + let r = polls.len(); + // We need to create existing votes + for i in polls.iter() { + ConvictionVoting::::vote(RawOrigin::Signed(caller.clone()).into(), *i, old_account_vote.clone())?; + } + let votes = match VotingFor::::get(&caller, &class) { + Voting::Casting(Casting { votes, .. }) => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r, "Votes were not recorded."); + + let index = polls[0]; + }: _(RawOrigin::Signed(caller.clone()), Some(class.clone()), index) + verify { + assert_matches!( + VotingFor::::get(&caller, &class), + Voting::Casting(Casting { votes, .. }) if votes.len() == (r - 1) as usize + ); + } + + remove_other_vote { + let caller = funded_account::("caller", 0); + let voter = funded_account::("caller", 0); + whitelist_account!(caller); + let old_account_vote = account_vote::(100u32.into()); + + let (class, all_polls) = fill_voting::(); + let polls = &all_polls[&class]; + let r = polls.len(); + // We need to create existing votes + for i in polls.iter() { + ConvictionVoting::::vote(RawOrigin::Signed(voter.clone()).into(), *i, old_account_vote.clone())?; + } + let votes = match VotingFor::::get(&caller, &class) { + Voting::Casting(Casting { votes, .. }) => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r, "Votes were not recorded."); + + let index = polls[0]; + assert!(T::Polls::end_ongoing(index, false).is_ok()); + }: _(RawOrigin::Signed(caller.clone()), voter.clone(), class.clone(), index) + verify { + assert_matches!( + VotingFor::::get(&voter, &class), + Voting::Casting(Casting { votes, .. }) if votes.len() == (r - 1) as usize + ); + } + + delegate { + let r in 0 .. T::MaxVotes::get().min(T::Polls::max_ongoing().1); + + let all_polls = fill_voting::().1; + let class = T::Polls::max_ongoing().0; + let polls = &all_polls[&class]; + let voter = funded_account::("voter", 0); + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + + let delegated_balance: BalanceOf = 1000u32.into(); + let delegate_vote = account_vote::(delegated_balance); + + // We need to create existing delegations + for i in polls.iter().take(r as usize) { + ConvictionVoting::::vote(RawOrigin::Signed(voter.clone()).into(), *i, delegate_vote.clone())?; + } + assert_matches!( + VotingFor::::get(&voter, &class), + Voting::Casting(Casting { votes, .. }) if votes.len() == r as usize + ); + + }: _(RawOrigin::Signed(caller.clone()), class.clone(), voter.clone(), Conviction::Locked1x, delegated_balance) + verify { + assert_matches!(VotingFor::::get(&caller, &class), Voting::Delegating(_)); + } + + undelegate { + let r in 0 .. T::MaxVotes::get().min(T::Polls::max_ongoing().1); + + let all_polls = fill_voting::().1; + let class = T::Polls::max_ongoing().0; + let polls = &all_polls[&class]; + let voter = funded_account::("voter", 0); + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + + let delegated_balance: BalanceOf = 1000u32.into(); + let delegate_vote = account_vote::(delegated_balance); + + ConvictionVoting::::delegate( + RawOrigin::Signed(caller.clone()).into(), + class.clone(), + voter.clone(), + Conviction::Locked1x, + delegated_balance, + )?; + + // We need to create delegations + for i in polls.iter().take(r as usize) { + ConvictionVoting::::vote(RawOrigin::Signed(voter.clone()).into(), *i, delegate_vote.clone())?; + } + assert_matches!( + VotingFor::::get(&voter, &class), + Voting::Casting(Casting { votes, .. }) if votes.len() == r as usize + ); + assert_matches!(VotingFor::::get(&caller, &class), Voting::Delegating(_)); + }: _(RawOrigin::Signed(caller.clone()), class.clone()) + verify { + assert_matches!(VotingFor::::get(&caller, &class), Voting::Casting(_)); + } + + unlock { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + let normal_account_vote = account_vote::(T::Currency::free_balance(&caller) - 100u32.into()); + let big_account_vote = account_vote::(T::Currency::free_balance(&caller)); + + // Fill everything up to the max by filling all classes with votes and voting on them all. + let (class, all_polls) = fill_voting::(); + assert!(all_polls.len() > 0); + for (class, polls) in all_polls.iter() { + assert!(polls.len() > 0); + for i in polls.iter() { + ConvictionVoting::::vote(RawOrigin::Signed(caller.clone()).into(), *i, normal_account_vote.clone())?; + } + } + + let orig_usable = >::reducible_balance(&caller, false); + let polls = &all_polls[&class]; + + // Vote big on the class with the most ongoing votes of them to bump the lock and make it + // hard to recompute when removed. + ConvictionVoting::::vote(RawOrigin::Signed(caller.clone()).into(), polls[0], big_account_vote.clone())?; + let now_usable = >::reducible_balance(&caller, false); + assert_eq!(orig_usable - now_usable, 100u32.into()); + + // Remove the vote + ConvictionVoting::::remove_vote(RawOrigin::Signed(caller.clone()).into(), Some(class.clone()), polls[0])?; + + // We can now unlock on `class` from 200 to 100... + }: _(RawOrigin::Signed(caller.clone()), class, caller.clone()) + verify { + assert_eq!(orig_usable, >::reducible_balance(&caller, false)); + } + + impl_benchmark_test_suite!( + ConvictionVoting, + crate::tests::new_test_ext(), + crate::tests::Test + ); +} diff --git a/frame/conviction-voting/src/conviction.rs b/frame/conviction-voting/src/conviction.rs new file mode 100644 index 0000000000000..129f2771124b5 --- /dev/null +++ b/frame/conviction-voting/src/conviction.rs @@ -0,0 +1,131 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! The conviction datatype. + +use crate::types::Delegations; +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Bounded, CheckedDiv, CheckedMul, Zero}, + RuntimeDebug, +}; +use sp_std::{convert::TryFrom, result::Result}; + +/// A value denoting the strength of conviction of a vote. +#[derive( + Encode, + Decode, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, +)] +pub enum Conviction { + /// 0.1x votes, unlocked. + None, + /// 1x votes, locked for an enactment period following a successful vote. + Locked1x, + /// 2x votes, locked for 2x enactment periods following a successful vote. + Locked2x, + /// 3x votes, locked for 4x... + Locked3x, + /// 4x votes, locked for 8x... + Locked4x, + /// 5x votes, locked for 16x... + Locked5x, + /// 6x votes, locked for 32x... + Locked6x, +} + +impl Default for Conviction { + fn default() -> Self { + Conviction::None + } +} + +impl From for u8 { + fn from(c: Conviction) -> u8 { + match c { + Conviction::None => 0, + Conviction::Locked1x => 1, + Conviction::Locked2x => 2, + Conviction::Locked3x => 3, + Conviction::Locked4x => 4, + Conviction::Locked5x => 5, + Conviction::Locked6x => 6, + } + } +} + +impl TryFrom for Conviction { + type Error = (); + fn try_from(i: u8) -> Result { + Ok(match i { + 0 => Conviction::None, + 1 => Conviction::Locked1x, + 2 => Conviction::Locked2x, + 3 => Conviction::Locked3x, + 4 => Conviction::Locked4x, + 5 => Conviction::Locked5x, + 6 => Conviction::Locked6x, + _ => return Err(()), + }) + } +} + +impl Conviction { + /// The amount of time (in number of periods) that our conviction implies a successful voter's + /// balance should be locked for. + pub fn lock_periods(self) -> u32 { + match self { + Conviction::None => 0, + Conviction::Locked1x => 1, + Conviction::Locked2x => 2, + Conviction::Locked3x => 4, + Conviction::Locked4x => 8, + Conviction::Locked5x => 16, + Conviction::Locked6x => 32, + } + } + + /// The votes of a voter of the given `balance` with our conviction. + pub fn votes + Zero + Copy + CheckedMul + CheckedDiv + Bounded>( + self, + capital: B, + ) -> Delegations { + let votes = match self { + Conviction::None => capital.checked_div(&10u8.into()).unwrap_or_else(Zero::zero), + x => capital.checked_mul(&u8::from(x).into()).unwrap_or_else(B::max_value), + }; + Delegations { votes, capital } + } +} + +impl Bounded for Conviction { + fn min_value() -> Self { + Conviction::None + } + fn max_value() -> Self { + Conviction::Locked6x + } +} diff --git a/frame/conviction-voting/src/lib.rs b/frame/conviction-voting/src/lib.rs new file mode 100644 index 0000000000000..8e7e0d91b1cf4 --- /dev/null +++ b/frame/conviction-voting/src/lib.rs @@ -0,0 +1,632 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! # Voting Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! Pallet for managing actual voting in polls. + +#![recursion_limit = "256"] +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + ensure, + traits::{ + fungible, Currency, Get, LockIdentifier, LockableCurrency, PollStatus, Polling, + ReservableCurrency, WithdrawReasons, + }, +}; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Saturating, Zero}, + ArithmeticError, DispatchError, DispatchResult, Perbill, +}; +use sp_std::prelude::*; + +mod conviction; +mod types; +mod vote; +pub mod weights; +pub use conviction::Conviction; +pub use pallet::*; +pub use types::{Delegations, Tally, UnvoteScope}; +pub use vote::{AccountVote, Casting, Delegating, Vote, Voting}; +pub use weights::WeightInfo; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +const CONVICTION_VOTING_ID: LockIdentifier = *b"pyconvot"; + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; +type VotingOf = Voting< + BalanceOf, + ::AccountId, + ::BlockNumber, + PollIndexOf, + ::MaxVotes, +>; +#[allow(dead_code)] +type DelegatingOf = Delegating< + BalanceOf, + ::AccountId, + ::BlockNumber, +>; +pub type TallyOf = Tally, ::MaxTurnout>; +pub type VotesOf = BalanceOf; +type PollIndexOf = <::Polls as Polling>>::Index; +#[cfg(feature = "runtime-benchmarks")] +type IndexOf = <::Polls as Polling>>::Index; +type ClassOf = <::Polls as Polling>>::Class; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_runtime::DispatchResult; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + // System level stuff. + type Event: From> + IsType<::Event>; + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + /// Currency type with which voting happens. + type Currency: ReservableCurrency + + LockableCurrency + + fungible::Inspect; + + /// The implementation of the logic which conducts polls. + type Polls: Polling, Votes = BalanceOf, Moment = Self::BlockNumber>; + + /// The maximum amount of tokens which may be used for voting. May just be + /// `Currency::total_issuance`, but you might want to reduce this in order to account for + /// funds in the system which are unable to vote (e.g. parachain auction deposits). + type MaxTurnout: Get>; + + /// The maximum number of concurrent votes an account may have. + /// + /// Also used to compute weight, an overly large value can + /// lead to extrinsic with large weight estimation: see `delegate` for instance. + #[pallet::constant] + type MaxVotes: Get; + + /// The minimum period of vote locking. + /// + /// It should be no shorter than enactment period to ensure that in the case of an approval, + /// those successful voters are locked into the consequences that their votes entail. + #[pallet::constant] + type VoteLockingPeriod: Get; + } + + /// All voting for a particular voter in a particular voting class. We store the balance for the + /// number of votes that we have recorded. + #[pallet::storage] + pub type VotingFor = StorageDoubleMap< + _, + Twox64Concat, + T::AccountId, + Twox64Concat, + ClassOf, + VotingOf, + ValueQuery, + >; + + /// The voting classes which have a non-zero lock requirement and the lock amounts which they + /// require. The actual amount locked on behalf of this pallet should always be the maximum of + /// this list. + #[pallet::storage] + pub type ClassLocksFor = + StorageMap<_, Twox64Concat, T::AccountId, Vec<(ClassOf, BalanceOf)>, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// An account has delegated their vote to another account. \[who, target\] + Delegated(T::AccountId, T::AccountId), + /// An \[account\] has cancelled a previous delegation operation. + Undelegated(T::AccountId), + } + + #[pallet::error] + pub enum Error { + /// Poll is not ongoing. + NotOngoing, + /// The given account did not vote on the poll. + NotVoter, + /// The actor has no permission to conduct the action. + NoPermission, + /// The actor has no permission to conduct the action right now but will do in the future. + NoPermissionYet, + /// The account is already delegating. + AlreadyDelegating, + /// The account currently has votes attached to it and the operation cannot succeed until + /// these are removed, either through `unvote` or `reap_vote`. + AlreadyVoting, + /// Too high a balance was provided that the account cannot afford. + InsufficientFunds, + /// The account is not currently delegating. + NotDelegating, + /// Delegation to oneself makes no sense. + Nonsense, + /// Maximum number of votes reached. + MaxVotesReached, + /// The class must be supplied since it is not easily determinable from the state. + ClassNeeded, + /// The class ID supplied is invalid. + BadClass, + } + + #[pallet::call] + impl Pallet { + /// Vote in a poll. If `vote.is_aye()`, the vote is to enact the proposal; + /// otherwise it is a vote to keep the status quo. + /// + /// The dispatch origin of this call must be _Signed_. + /// + /// - `poll_index`: The index of the poll to vote for. + /// - `vote`: The vote configuration. + /// + /// Weight: `O(R)` where R is the number of polls the voter has voted on. + #[pallet::weight(T::WeightInfo::vote_new().max(T::WeightInfo::vote_existing()))] + pub fn vote( + origin: OriginFor, + #[pallet::compact] poll_index: PollIndexOf, + vote: AccountVote>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::try_vote(&who, poll_index, vote) + } + + /// Delegate the voting power (with some given conviction) of the sending account for a + /// particular class of polls. + /// + /// The balance delegated is locked for as long as it's delegated, and thereafter for the + /// time appropriate for the conviction's lock period. + /// + /// The dispatch origin of this call must be _Signed_, and the signing account must either: + /// - be delegating already; or + /// - have no voting activity (if there is, then it will need to be removed/consolidated + /// through `reap_vote` or `unvote`). + /// + /// - `to`: The account whose voting the `target` account's voting power will follow. + /// - `class`: The class of polls to delegate. To delegate multiple classes, multiple calls + /// to this function are required. + /// - `conviction`: The conviction that will be attached to the delegated votes. When the + /// account is undelegated, the funds will be locked for the corresponding period. + /// - `balance`: The amount of the account's balance to be used in delegating. This must not + /// be more than the account's current balance. + /// + /// Emits `Delegated`. + /// + /// Weight: `O(R)` where R is the number of polls the voter delegating to has + /// voted on. Weight is initially charged as if maximum votes, but is refunded later. + // NOTE: weight must cover an incorrect voting of origin with max votes, this is ensure + // because a valid delegation cover decoding a direct voting with max votes. + #[pallet::weight(T::WeightInfo::delegate(T::MaxVotes::get()))] + pub fn delegate( + origin: OriginFor, + class: ClassOf, + to: T::AccountId, + conviction: Conviction, + balance: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let votes = Self::try_delegate(who, class, to, conviction, balance)?; + + Ok(Some(T::WeightInfo::delegate(votes)).into()) + } + + /// Undelegate the voting power of the sending account for a particular class of polls. + /// + /// Tokens may be unlocked following once an amount of time consistent with the lock period + /// of the conviction with which the delegation was issued. + /// + /// The dispatch origin of this call must be _Signed_ and the signing account must be + /// currently delegating. + /// + /// - `class`: The class of polls to remove the delegation from. + /// + /// Emits `Undelegated`. + /// + /// Weight: `O(R)` where R is the number of polls the voter delegating to has + /// voted on. Weight is initially charged as if maximum votes, but is refunded later. + // NOTE: weight must cover an incorrect voting of origin with max votes, this is ensure + // because a valid delegation cover decoding a direct voting with max votes. + #[pallet::weight(T::WeightInfo::undelegate(T::MaxVotes::get().into()))] + pub fn undelegate(origin: OriginFor, class: ClassOf) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let votes = Self::try_undelegate(who, class)?; + Ok(Some(T::WeightInfo::undelegate(votes)).into()) + } + + /// Remove the lock caused prior voting/delegating which has expired within a particluar + /// class. + /// + /// The dispatch origin of this call must be _Signed_. + /// + /// - `class`: The class of polls to unlock. + /// - `target`: The account to remove the lock on. + /// + /// Weight: `O(R)` with R number of vote of target. + #[pallet::weight(T::WeightInfo::unlock())] + pub fn unlock( + origin: OriginFor, + class: ClassOf, + target: T::AccountId, + ) -> DispatchResult { + ensure_signed(origin)?; + Self::update_lock(&class, &target); + Ok(()) + } + + /// Remove a vote for a poll. + /// + /// If: + /// - the poll was cancelled, or + /// - the poll is ongoing, or + /// - the poll has ended such that + /// - the vote of the account was in opposition to the result; or + /// - there was no conviction to the account's vote; or + /// - the account made a split vote + /// ...then the vote is removed cleanly and a following call to `unlock` may result in more + /// funds being available. + /// + /// If, however, the poll has ended and: + /// - it finished corresponding to the vote of the account, and + /// - the account made a standard vote with conviction, and + /// - the lock period of the conviction is not over + /// ...then the lock will be aggregated into the overall account's lock, which may involve + /// *overlocking* (where the two locks are combined into a single lock that is the maximum + /// of both the amount locked and the time is it locked for). + /// + /// The dispatch origin of this call must be _Signed_, and the signer must have a vote + /// registered for poll `index`. + /// + /// - `index`: The index of poll of the vote to be removed. + /// - `class`: Optional parameter, if given it indicates the class of the poll. For polls + /// which have finished or are cancelled, this must be `Some`. + /// + /// Weight: `O(R + log R)` where R is the number of polls that `target` has voted on. + /// Weight is calculated for the maximum number of vote. + #[pallet::weight(T::WeightInfo::remove_vote())] + pub fn remove_vote( + origin: OriginFor, + class: Option>, + index: PollIndexOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::try_remove_vote(&who, index, class, UnvoteScope::Any) + } + + /// Remove a vote for a poll. + /// + /// If the `target` is equal to the signer, then this function is exactly equivalent to + /// `remove_vote`. If not equal to the signer, then the vote must have expired, + /// either because the poll was cancelled, because the voter lost the poll or + /// because the conviction period is over. + /// + /// The dispatch origin of this call must be _Signed_. + /// + /// - `target`: The account of the vote to be removed; this account must have voted for poll + /// `index`. + /// - `index`: The index of poll of the vote to be removed. + /// - `class`: The class of the poll. + /// + /// Weight: `O(R + log R)` where R is the number of polls that `target` has voted on. + /// Weight is calculated for the maximum number of vote. + #[pallet::weight(T::WeightInfo::remove_other_vote())] + pub fn remove_other_vote( + origin: OriginFor, + target: T::AccountId, + class: ClassOf, + index: PollIndexOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let scope = if target == who { UnvoteScope::Any } else { UnvoteScope::OnlyExpired }; + Self::try_remove_vote(&target, index, Some(class), scope)?; + Ok(()) + } + } +} + +impl Pallet { + /// Actually enact a vote, if legit. + fn try_vote( + who: &T::AccountId, + poll_index: PollIndexOf, + vote: AccountVote>, + ) -> DispatchResult { + ensure!(vote.balance() <= T::Currency::free_balance(who), Error::::InsufficientFunds); + T::Polls::try_access_poll(poll_index, |poll_status| { + let (tally, class) = poll_status.ensure_ongoing().ok_or(Error::::NotOngoing)?; + VotingFor::::try_mutate(who, &class, |voting| { + if let Voting::Casting(Casting { ref mut votes, delegations, .. }) = voting { + match votes.binary_search_by_key(&poll_index, |i| i.0) { + Ok(i) => { + // Shouldn't be possible to fail, but we handle it gracefully. + tally.remove(votes[i].1).ok_or(ArithmeticError::Underflow)?; + if let Some(approve) = votes[i].1.as_standard() { + tally.reduce(approve, *delegations); + } + votes[i].1 = vote; + }, + Err(i) => { + votes + .try_insert(i, (poll_index, vote)) + .map_err(|()| Error::::MaxVotesReached)?; + }, + } + // Shouldn't be possible to fail, but we handle it gracefully. + tally.add(vote).ok_or(ArithmeticError::Overflow)?; + if let Some(approve) = vote.as_standard() { + tally.increase(approve, *delegations); + } + } else { + return Err(Error::::AlreadyDelegating.into()) + } + // Extend the lock to `balance` (rather than setting it) since we don't know what + // other votes are in place. + Self::extend_lock(who, &class, vote.balance()); + Ok(()) + }) + }) + } + + /// Remove the account's vote for the given poll if possible. This is possible when: + /// - The poll has not finished. + /// - The poll has finished and the voter lost their direction. + /// - The poll has finished and the voter's lock period is up. + /// + /// This will generally be combined with a call to `unlock`. + fn try_remove_vote( + who: &T::AccountId, + poll_index: PollIndexOf, + class_hint: Option>, + scope: UnvoteScope, + ) -> DispatchResult { + let class = class_hint + .or_else(|| Some(T::Polls::as_ongoing(poll_index)?.1)) + .ok_or(Error::::ClassNeeded)?; + VotingFor::::try_mutate(who, class, |voting| { + if let Voting::Casting(Casting { ref mut votes, delegations, ref mut prior }) = voting { + let i = votes + .binary_search_by_key(&poll_index, |i| i.0) + .map_err(|_| Error::::NotVoter)?; + let v = votes.remove(i); + + T::Polls::try_access_poll(poll_index, |poll_status| match poll_status { + PollStatus::Ongoing(tally, _) => { + ensure!(matches!(scope, UnvoteScope::Any), Error::::NoPermission); + // Shouldn't be possible to fail, but we handle it gracefully. + tally.remove(v.1).ok_or(ArithmeticError::Underflow)?; + if let Some(approve) = v.1.as_standard() { + tally.reduce(approve, *delegations); + } + Ok(()) + }, + PollStatus::Completed(end, approved) => { + if let Some((lock_periods, balance)) = v.1.locked_if(approved) { + let unlock_at = end.saturating_add( + T::VoteLockingPeriod::get().saturating_mul(lock_periods.into()), + ); + let now = frame_system::Pallet::::block_number(); + if now < unlock_at { + ensure!( + matches!(scope, UnvoteScope::Any), + Error::::NoPermissionYet + ); + prior.accumulate(unlock_at, balance) + } + } + Ok(()) + }, + PollStatus::None => Ok(()), // Poll was cancelled. + }) + } else { + Ok(()) + } + }) + } + + /// Return the number of votes for `who` + fn increase_upstream_delegation( + who: &T::AccountId, + class: &ClassOf, + amount: Delegations>, + ) -> u32 { + VotingFor::::mutate(who, class, |voting| match voting { + Voting::Delegating(Delegating { delegations, .. }) => { + // We don't support second level delegating, so we don't need to do anything more. + *delegations = delegations.saturating_add(amount); + 1 + }, + Voting::Casting(Casting { votes, delegations, .. }) => { + *delegations = delegations.saturating_add(amount); + for &(poll_index, account_vote) in votes.iter() { + if let AccountVote::Standard { vote, .. } = account_vote { + T::Polls::access_poll(poll_index, |poll_status| { + if let PollStatus::Ongoing(tally, _) = poll_status { + tally.increase(vote.aye, amount); + } + }); + } + } + votes.len() as u32 + }, + }) + } + + /// Return the number of votes for `who` + fn reduce_upstream_delegation( + who: &T::AccountId, + class: &ClassOf, + amount: Delegations>, + ) -> u32 { + VotingFor::::mutate(who, class, |voting| match voting { + Voting::Delegating(Delegating { delegations, .. }) => { + // We don't support second level delegating, so we don't need to do anything more. + *delegations = delegations.saturating_sub(amount); + 1 + }, + Voting::Casting(Casting { votes, delegations, .. }) => { + *delegations = delegations.saturating_sub(amount); + for &(poll_index, account_vote) in votes.iter() { + if let AccountVote::Standard { vote, .. } = account_vote { + T::Polls::access_poll(poll_index, |poll_status| { + if let PollStatus::Ongoing(tally, _) = poll_status { + tally.reduce(vote.aye, amount); + } + }); + } + } + votes.len() as u32 + }, + }) + } + + /// Attempt to delegate `balance` times `conviction` of voting power from `who` to `target`. + /// + /// Return the upstream number of votes. + fn try_delegate( + who: T::AccountId, + class: ClassOf, + target: T::AccountId, + conviction: Conviction, + balance: BalanceOf, + ) -> Result { + ensure!(who != target, Error::::Nonsense); + T::Polls::classes().binary_search(&class).map_err(|_| Error::::BadClass)?; + ensure!(balance <= T::Currency::free_balance(&who), Error::::InsufficientFunds); + let votes = + VotingFor::::try_mutate(&who, &class, |voting| -> Result { + let old = sp_std::mem::replace( + voting, + Voting::Delegating(Delegating { + balance, + target: target.clone(), + conviction, + delegations: Default::default(), + prior: Default::default(), + }), + ); + match old { + Voting::Delegating(Delegating { .. }) => Err(Error::::AlreadyDelegating)?, + Voting::Casting(Casting { votes, delegations, prior }) => { + // here we just ensure that we're currently idling with no votes recorded. + ensure!(votes.is_empty(), Error::::AlreadyVoting); + voting.set_common(delegations, prior); + }, + } + + let votes = + Self::increase_upstream_delegation(&target, &class, conviction.votes(balance)); + // Extend the lock to `balance` (rather than setting it) since we don't know what + // other votes are in place. + Self::extend_lock(&who, &class, balance); + Ok(votes) + })?; + Self::deposit_event(Event::::Delegated(who, target)); + Ok(votes) + } + + /// Attempt to end the current delegation. + /// + /// Return the number of votes of upstream. + fn try_undelegate(who: T::AccountId, class: ClassOf) -> Result { + let votes = + VotingFor::::try_mutate(&who, &class, |voting| -> Result { + match sp_std::mem::replace(voting, Voting::default()) { + Voting::Delegating(Delegating { + balance, + target, + conviction, + delegations, + mut prior, + }) => { + // remove any delegation votes to our current target. + let votes = Self::reduce_upstream_delegation( + &target, + &class, + conviction.votes(balance), + ); + let now = frame_system::Pallet::::block_number(); + let lock_periods = conviction.lock_periods().into(); + prior.accumulate( + now.saturating_add( + T::VoteLockingPeriod::get().saturating_mul(lock_periods), + ), + balance, + ); + voting.set_common(delegations, prior); + + Ok(votes) + }, + Voting::Casting(_) => Err(Error::::NotDelegating.into()), + } + })?; + Self::deposit_event(Event::::Undelegated(who)); + Ok(votes) + } + + fn extend_lock(who: &T::AccountId, class: &ClassOf, amount: BalanceOf) { + ClassLocksFor::::mutate(who, |locks| match locks.iter().position(|x| &x.0 == class) { + Some(i) => locks[i].1 = locks[i].1.max(amount), + None => locks.push((class.clone(), amount)), + }); + T::Currency::extend_lock(CONVICTION_VOTING_ID, who, amount, WithdrawReasons::TRANSFER); + } + + /// Rejig the lock on an account. It will never get more stringent (since that would indicate + /// a security hole) but may be reduced from what they are currently. + fn update_lock(class: &ClassOf, who: &T::AccountId) { + let class_lock_needed = VotingFor::::mutate(who, class, |voting| { + voting.rejig(frame_system::Pallet::::block_number()); + voting.locked_balance() + }); + let lock_needed = ClassLocksFor::::mutate(who, |locks| { + locks.retain(|x| &x.0 != class); + if !class_lock_needed.is_zero() { + locks.push((class.clone(), class_lock_needed)); + } + locks.iter().map(|x| x.1).max().unwrap_or(Zero::zero()) + }); + if lock_needed.is_zero() { + T::Currency::remove_lock(CONVICTION_VOTING_ID, who); + } else { + T::Currency::set_lock( + CONVICTION_VOTING_ID, + who, + lock_needed, + WithdrawReasons::TRANSFER, + ); + } + } +} diff --git a/frame/conviction-voting/src/tests.rs b/frame/conviction-voting/src/tests.rs new file mode 100644 index 0000000000000..cedb23b02a8db --- /dev/null +++ b/frame/conviction-voting/src/tests.rs @@ -0,0 +1,820 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! The crate's tests. + +use std::collections::BTreeMap; + +use super::*; +use crate as pallet_conviction_voting; +use frame_support::{ + assert_noop, assert_ok, parameter_types, + traits::{ConstU32, ConstU64, Contains, Polling}, +}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Voting: pallet_conviction_voting::{Pallet, Call, Storage, Event}, + } +); + +// Test that a fitlered call can be dispatched. +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &Call) -> bool { + !matches!(call, &Call::Balances(pallet_balances::Call::set_balance { .. })) + } +} + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1_000_000); +} +impl frame_system::Config for Test { + type BaseCallFilter = BaseFilter; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type MaxLocks = ConstU32<10>; + type Balance = u64; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type AccountStore = System; + type WeightInfo = (); +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum TestPollState { + Ongoing(TallyOf, u8), + Completed(u64, bool), +} +use TestPollState::*; + +parameter_types! { + pub static Polls: BTreeMap = vec![ + (1, Completed(1, true)), + (2, Completed(2, false)), + (3, Ongoing(Tally::from_parts(0, 0, 0), 0)), + ].into_iter().collect(); +} + +pub struct TestPolls; +impl Polling> for TestPolls { + type Index = u8; + type Votes = u64; + type Moment = u64; + type Class = u8; + fn classes() -> Vec { + vec![0, 1, 2] + } + fn as_ongoing(index: u8) -> Option<(TallyOf, Self::Class)> { + Polls::get().remove(&index).and_then(|x| { + if let TestPollState::Ongoing(t, c) = x { + Some((t, c)) + } else { + None + } + }) + } + fn access_poll( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut TallyOf, u64, u8>) -> R, + ) -> R { + let mut polls = Polls::get(); + let entry = polls.get_mut(&index); + let r = match entry { + Some(Ongoing(ref mut tally_mut_ref, class)) => + f(PollStatus::Ongoing(tally_mut_ref, *class)), + Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), + None => f(PollStatus::None), + }; + Polls::set(polls); + r + } + fn try_access_poll( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut TallyOf, u64, u8>) -> Result, + ) -> Result { + let mut polls = Polls::get(); + let entry = polls.get_mut(&index); + let r = match entry { + Some(Ongoing(ref mut tally_mut_ref, class)) => + f(PollStatus::Ongoing(tally_mut_ref, *class)), + Some(Completed(when, succeeded)) => f(PollStatus::Completed(*when, *succeeded)), + None => f(PollStatus::None), + }?; + Polls::set(polls); + Ok(r) + } + + #[cfg(feature = "runtime-benchmarks")] + fn create_ongoing(class: Self::Class) -> Result { + let mut polls = Polls::get(); + let i = polls.keys().rev().next().map_or(0, |x| x + 1); + polls.insert(i, Ongoing(Tally::default(), class)); + Polls::set(polls); + Ok(i) + } + + #[cfg(feature = "runtime-benchmarks")] + fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> { + let mut polls = Polls::get(); + match polls.get(&index) { + Some(Ongoing(..)) => {}, + _ => return Err(()), + } + let now = frame_system::Pallet::::block_number(); + polls.insert(index, Completed(now, approved)); + Polls::set(polls); + Ok(()) + } +} + +impl Config for Test { + type Event = Event; + type Currency = pallet_balances::Pallet; + type VoteLockingPeriod = ConstU64<3>; + type MaxVotes = ConstU32<3>; + type WeightInfo = (); + type MaxTurnout = frame_support::traits::TotalIssuanceOf; + type Polls = TestPolls; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], + } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[test] +fn params_should_work() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(42), 0); + assert_eq!(Balances::total_issuance(), 210); + }); +} + +fn next_block() { + System::set_block_number(System::block_number() + 1); +} + +#[allow(dead_code)] +fn run_to(n: u64) { + while System::block_number() < n { + next_block(); + } +} + +fn aye(amount: u64, conviction: u8) -> AccountVote { + let vote = Vote { aye: true, conviction: conviction.try_into().unwrap() }; + AccountVote::Standard { vote, balance: amount } +} + +fn nay(amount: u64, conviction: u8) -> AccountVote { + let vote = Vote { aye: false, conviction: conviction.try_into().unwrap() }; + AccountVote::Standard { vote, balance: amount } +} + +fn tally(index: u8) -> TallyOf { + >>::as_ongoing(index).expect("No poll").0 +} + +fn class(index: u8) -> u8 { + >>::as_ongoing(index).expect("No poll").1 +} + +#[test] +#[ignore] +#[should_panic(expected = "No poll")] +fn unknown_poll_should_panic() { + let _ = tally(0); +} + +#[test] +#[ignore] +#[should_panic(expected = "No poll")] +fn completed_poll_should_panic() { + let _ = tally(1); +} + +#[test] +fn basic_stuff() { + new_test_ext().execute_with(|| { + assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); + }); +} + +#[test] +fn basic_voting_works() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(2, 5))); + assert_eq!(tally(3), Tally::from_parts(10, 0, 2)); + assert_ok!(Voting::vote(Origin::signed(1), 3, nay(2, 5))); + assert_eq!(tally(3), Tally::from_parts(0, 10, 2)); + assert_eq!(Balances::usable_balance(1), 8); + + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(5, 1))); + assert_eq!(tally(3), Tally::from_parts(5, 0, 5)); + assert_ok!(Voting::vote(Origin::signed(1), 3, nay(5, 1))); + assert_eq!(tally(3), Tally::from_parts(0, 5, 5)); + assert_eq!(Balances::usable_balance(1), 5); + + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0))); + assert_eq!(tally(3), Tally::from_parts(1, 0, 10)); + assert_ok!(Voting::vote(Origin::signed(1), 3, nay(10, 0))); + assert_eq!(tally(3), Tally::from_parts(0, 1, 10)); + assert_eq!(Balances::usable_balance(1), 0); + + assert_ok!(Voting::remove_vote(Origin::signed(1), None, 3)); + assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); + + assert_ok!(Voting::unlock(Origin::signed(1), class(3), 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn voting_balance_gets_locked() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(2, 5))); + assert_eq!(tally(3), Tally::from_parts(10, 0, 2)); + assert_ok!(Voting::vote(Origin::signed(1), 3, nay(2, 5))); + assert_eq!(tally(3), Tally::from_parts(0, 10, 2)); + assert_eq!(Balances::usable_balance(1), 8); + + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(5, 1))); + assert_eq!(tally(3), Tally::from_parts(5, 0, 5)); + assert_ok!(Voting::vote(Origin::signed(1), 3, nay(5, 1))); + assert_eq!(tally(3), Tally::from_parts(0, 5, 5)); + assert_eq!(Balances::usable_balance(1), 5); + + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0))); + assert_eq!(tally(3), Tally::from_parts(1, 0, 10)); + assert_ok!(Voting::vote(Origin::signed(1), 3, nay(10, 0))); + assert_eq!(tally(3), Tally::from_parts(0, 1, 10)); + assert_eq!(Balances::usable_balance(1), 0); + + assert_ok!(Voting::remove_vote(Origin::signed(1), None, 3)); + assert_eq!(tally(3), Tally::from_parts(0, 0, 0)); + + assert_ok!(Voting::unlock(Origin::signed(1), class(3), 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn successful_but_zero_conviction_vote_balance_can_be_unlocked() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(1, 1))); + assert_ok!(Voting::vote(Origin::signed(2), 3, nay(20, 0))); + let c = class(3); + Polls::set(vec![(3, Completed(3, false))].into_iter().collect()); + assert_ok!(Voting::remove_vote(Origin::signed(2), Some(c), 3)); + assert_ok!(Voting::unlock(Origin::signed(2), c, 2)); + assert_eq!(Balances::usable_balance(2), 20); + }); +} + +#[test] +fn unsuccessful_conviction_vote_balance_can_be_unlocked() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(1, 1))); + assert_ok!(Voting::vote(Origin::signed(2), 3, nay(20, 0))); + let c = class(3); + Polls::set(vec![(3, Completed(3, false))].into_iter().collect()); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(c), 3)); + assert_ok!(Voting::unlock(Origin::signed(1), c, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn successful_conviction_vote_balance_stays_locked_for_correct_time() { + new_test_ext().execute_with(|| { + for i in 1..=5 { + assert_ok!(Voting::vote(Origin::signed(i), 3, aye(10, i as u8))); + } + let c = class(3); + Polls::set(vec![(3, Completed(3, true))].into_iter().collect()); + for i in 1..=5 { + assert_ok!(Voting::remove_vote(Origin::signed(i), Some(c), 3)); + } + for block in 1..=(3 + 5 * 3) { + run_to(block); + for i in 1..=5 { + assert_ok!(Voting::unlock(Origin::signed(i), c, i)); + let expired = block >= (3 << (i - 1)) + 3; + assert_eq!(Balances::usable_balance(i), i * 10 - if expired { 0 } else { 10 }); + } + } + }); +} + +#[test] +fn classwise_delegation_works() { + new_test_ext().execute_with(|| { + Polls::set( + vec![ + (0, Ongoing(Tally::default(), 0)), + (1, Ongoing(Tally::default(), 1)), + (2, Ongoing(Tally::default(), 2)), + (3, Ongoing(Tally::default(), 2)), + ] + .into_iter() + .collect(), + ); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5)); + assert_ok!(Voting::delegate(Origin::signed(1), 1, 3, Conviction::Locked1x, 5)); + assert_ok!(Voting::delegate(Origin::signed(1), 2, 4, Conviction::Locked1x, 5)); + assert_eq!(Balances::usable_balance(1), 5); + + assert_ok!(Voting::vote(Origin::signed(2), 0, aye(10, 0))); + assert_ok!(Voting::vote(Origin::signed(2), 1, nay(10, 0))); + assert_ok!(Voting::vote(Origin::signed(2), 2, nay(10, 0))); + assert_ok!(Voting::vote(Origin::signed(3), 0, nay(10, 0))); + assert_ok!(Voting::vote(Origin::signed(3), 1, aye(10, 0))); + assert_ok!(Voting::vote(Origin::signed(3), 2, nay(10, 0))); + assert_ok!(Voting::vote(Origin::signed(4), 0, nay(10, 0))); + assert_ok!(Voting::vote(Origin::signed(4), 1, nay(10, 0))); + assert_ok!(Voting::vote(Origin::signed(4), 2, aye(10, 0))); + // 4 hasn't voted yet + + assert_eq!( + Polls::get(), + vec![ + (0, Ongoing(Tally::from_parts(6, 2, 35), 0)), + (1, Ongoing(Tally::from_parts(6, 2, 35), 1)), + (2, Ongoing(Tally::from_parts(6, 2, 35), 2)), + (3, Ongoing(Tally::from_parts(0, 0, 0), 2)), + ] + .into_iter() + .collect() + ); + + // 4 votes nay to 3. + assert_ok!(Voting::vote(Origin::signed(4), 3, nay(10, 0))); + assert_eq!( + Polls::get(), + vec![ + (0, Ongoing(Tally::from_parts(6, 2, 35), 0)), + (1, Ongoing(Tally::from_parts(6, 2, 35), 1)), + (2, Ongoing(Tally::from_parts(6, 2, 35), 2)), + (3, Ongoing(Tally::from_parts(0, 6, 15), 2)), + ] + .into_iter() + .collect() + ); + + // Redelegate for class 2 to account 3. + assert_ok!(Voting::undelegate(Origin::signed(1), 2)); + assert_ok!(Voting::delegate(Origin::signed(1), 2, 3, Conviction::Locked1x, 5)); + assert_eq!( + Polls::get(), + vec![ + (0, Ongoing(Tally::from_parts(6, 2, 35), 0)), + (1, Ongoing(Tally::from_parts(6, 2, 35), 1)), + (2, Ongoing(Tally::from_parts(1, 7, 35), 2)), + (3, Ongoing(Tally::from_parts(0, 1, 10), 2)), + ] + .into_iter() + .collect() + ); + + // Redelegating with a lower lock does not forget previous lock and updates correctly. + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::undelegate(Origin::signed(1), 1)); + assert_ok!(Voting::undelegate(Origin::signed(1), 2)); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 3)); + assert_ok!(Voting::delegate(Origin::signed(1), 1, 3, Conviction::Locked1x, 3)); + assert_ok!(Voting::delegate(Origin::signed(1), 2, 4, Conviction::Locked1x, 3)); + assert_eq!( + Polls::get(), + vec![ + (0, Ongoing(Tally::from_parts(4, 2, 33), 0)), + (1, Ongoing(Tally::from_parts(4, 2, 33), 1)), + (2, Ongoing(Tally::from_parts(4, 2, 33), 2)), + (3, Ongoing(Tally::from_parts(0, 4, 13), 2)), + ] + .into_iter() + .collect() + ); + assert_eq!(Balances::usable_balance(1), 5); + + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + // unlock does nothing since the delegation already took place. + assert_eq!(Balances::usable_balance(1), 5); + + // Redelegating with higher amount extends previous lock. + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 6)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 4); + assert_ok!(Voting::undelegate(Origin::signed(1), 1)); + assert_ok!(Voting::delegate(Origin::signed(1), 1, 3, Conviction::Locked1x, 7)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_eq!(Balances::usable_balance(1), 3); + assert_ok!(Voting::undelegate(Origin::signed(1), 2)); + assert_ok!(Voting::delegate(Origin::signed(1), 2, 4, Conviction::Locked1x, 8)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 2); + assert_eq!( + Polls::get(), + vec![ + (0, Ongoing(Tally::from_parts(7, 2, 36), 0)), + (1, Ongoing(Tally::from_parts(8, 2, 37), 1)), + (2, Ongoing(Tally::from_parts(9, 2, 38), 2)), + (3, Ongoing(Tally::from_parts(0, 9, 18), 2)), + ] + .into_iter() + .collect() + ); + }); +} + +#[test] +fn redelegation_after_vote_ending_should_keep_lock() { + new_test_ext().execute_with(|| { + Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect()); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5)); + assert_ok!(Voting::vote(Origin::signed(2), 0, aye(10, 1))); + Polls::set(vec![(0, Completed(1, true))].into_iter().collect()); + assert_eq!(Balances::usable_balance(1), 5); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 3, Conviction::Locked1x, 3)); + assert_eq!(Balances::usable_balance(1), 5); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 5); + }); +} + +#[test] +fn lock_amalgamation_valid_with_multiple_removed_votes() { + new_test_ext().execute_with(|| { + Polls::set( + vec![ + (0, Ongoing(Tally::default(), 0)), + (1, Ongoing(Tally::default(), 0)), + (2, Ongoing(Tally::default(), 0)), + ] + .into_iter() + .collect(), + ); + assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1))); + assert_ok!(Voting::vote(Origin::signed(1), 1, aye(10, 1))); + assert_ok!(Voting::vote(Origin::signed(1), 2, aye(5, 2))); + assert_eq!(Balances::usable_balance(1), 0); + + Polls::set( + vec![(0, Completed(1, true)), (1, Completed(1, true)), (2, Completed(1, true))] + .into_iter() + .collect(), + ); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 2)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(3); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(6); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert!(Balances::usable_balance(1) <= 5); + + run_to(7); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn lock_amalgamation_valid_with_multiple_delegations() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5)); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 10)); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked2x, 5)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + + run_to(3); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(6); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert!(Balances::usable_balance(1) <= 5); + + run_to(7); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn lock_amalgamation_valid_with_move_roundtrip_to_delegation() { + new_test_ext().execute_with(|| { + Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect()); + assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1))); + Polls::set(vec![(0, Completed(1, true))].into_iter().collect()); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 5); + + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 10)); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + Polls::set(vec![(1, Ongoing(Tally::default(), 0))].into_iter().collect()); + assert_ok!(Voting::vote(Origin::signed(1), 1, aye(5, 2))); + Polls::set(vec![(1, Completed(1, true))].into_iter().collect()); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 1)); + + run_to(3); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(6); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert!(Balances::usable_balance(1) <= 5); + + run_to(7); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn lock_amalgamation_valid_with_move_roundtrip_to_casting() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5)); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 5); + + Polls::set(vec![(0, Ongoing(Tally::default(), 0))].into_iter().collect()); + assert_ok!(Voting::vote(Origin::signed(1), 0, aye(10, 1))); + Polls::set(vec![(0, Completed(1, true))].into_iter().collect()); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0)); + + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked2x, 10)); + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + + run_to(3); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(6); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert!(Balances::usable_balance(1) <= 5); + + run_to(7); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn lock_aggregation_over_different_classes_with_delegation_works() { + new_test_ext().execute_with(|| { + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::Locked1x, 5)); + assert_ok!(Voting::delegate(Origin::signed(1), 1, 2, Conviction::Locked2x, 5)); + assert_ok!(Voting::delegate(Origin::signed(1), 2, 2, Conviction::Locked1x, 10)); + + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + assert_ok!(Voting::undelegate(Origin::signed(1), 1)); + assert_ok!(Voting::undelegate(Origin::signed(1), 2)); + + run_to(3); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(6); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 5); + + run_to(7); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn lock_aggregation_over_different_classes_with_casting_works() { + new_test_ext().execute_with(|| { + Polls::set( + vec![ + (0, Ongoing(Tally::default(), 0)), + (1, Ongoing(Tally::default(), 1)), + (2, Ongoing(Tally::default(), 2)), + ] + .into_iter() + .collect(), + ); + assert_ok!(Voting::vote(Origin::signed(1), 0, aye(5, 1))); + assert_ok!(Voting::vote(Origin::signed(1), 1, aye(10, 1))); + assert_ok!(Voting::vote(Origin::signed(1), 2, aye(5, 2))); + Polls::set( + vec![(0, Completed(1, true)), (1, Completed(1, true)), (2, Completed(1, true))] + .into_iter() + .collect(), + ); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(0), 0)); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(1), 1)); + assert_ok!(Voting::remove_vote(Origin::signed(1), Some(2), 2)); + + run_to(3); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 0); + + run_to(6); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 5); + + run_to(7); + assert_ok!(Voting::unlock(Origin::signed(1), 0, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 1, 1)); + assert_ok!(Voting::unlock(Origin::signed(1), 2, 1)); + assert_eq!(Balances::usable_balance(1), 10); + }); +} + +#[test] +fn errors_with_vote_work() { + new_test_ext().execute_with(|| { + assert_noop!(Voting::vote(Origin::signed(1), 0, aye(10, 0)), Error::::NotOngoing); + assert_noop!(Voting::vote(Origin::signed(1), 1, aye(10, 0)), Error::::NotOngoing); + assert_noop!(Voting::vote(Origin::signed(1), 2, aye(10, 0)), Error::::NotOngoing); + assert_noop!( + Voting::vote(Origin::signed(1), 3, aye(11, 0)), + Error::::InsufficientFunds + ); + + assert_ok!(Voting::delegate(Origin::signed(1), 0, 2, Conviction::None, 10)); + assert_noop!( + Voting::vote(Origin::signed(1), 3, aye(10, 0)), + Error::::AlreadyDelegating + ); + + assert_ok!(Voting::undelegate(Origin::signed(1), 0)); + Polls::set( + vec![ + (0, Ongoing(Tally::default(), 0)), + (1, Ongoing(Tally::default(), 0)), + (2, Ongoing(Tally::default(), 0)), + (3, Ongoing(Tally::default(), 0)), + ] + .into_iter() + .collect(), + ); + assert_ok!(Voting::vote(Origin::signed(1), 0, aye(10, 0))); + assert_ok!(Voting::vote(Origin::signed(1), 1, aye(10, 0))); + assert_ok!(Voting::vote(Origin::signed(1), 2, aye(10, 0))); + assert_noop!( + Voting::vote(Origin::signed(1), 3, aye(10, 0)), + Error::::MaxVotesReached + ); + }); +} + +#[test] +fn errors_with_delegating_work() { + new_test_ext().execute_with(|| { + assert_noop!( + Voting::delegate(Origin::signed(1), 0, 2, Conviction::None, 11), + Error::::InsufficientFunds + ); + assert_noop!( + Voting::delegate(Origin::signed(1), 3, 2, Conviction::None, 10), + Error::::BadClass + ); + + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 0))); + assert_noop!( + Voting::delegate(Origin::signed(1), 0, 2, Conviction::None, 10), + Error::::AlreadyVoting + ); + + assert_noop!(Voting::undelegate(Origin::signed(1), 0), Error::::NotDelegating); + }); +} + +#[test] +fn remove_other_vote_works() { + new_test_ext().execute_with(|| { + assert_noop!( + Voting::remove_other_vote(Origin::signed(2), 1, 0, 3), + Error::::NotVoter + ); + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 2))); + assert_noop!( + Voting::remove_other_vote(Origin::signed(2), 1, 0, 3), + Error::::NoPermission + ); + Polls::set(vec![(3, Completed(1, true))].into_iter().collect()); + run_to(6); + assert_noop!( + Voting::remove_other_vote(Origin::signed(2), 1, 0, 3), + Error::::NoPermissionYet + ); + run_to(7); + assert_ok!(Voting::remove_other_vote(Origin::signed(2), 1, 0, 3)); + }); +} + +#[test] +fn errors_with_remove_vote_work() { + new_test_ext().execute_with(|| { + assert_noop!(Voting::remove_vote(Origin::signed(1), Some(0), 3), Error::::NotVoter); + assert_ok!(Voting::vote(Origin::signed(1), 3, aye(10, 2))); + Polls::set(vec![(3, Completed(1, true))].into_iter().collect()); + assert_noop!(Voting::remove_vote(Origin::signed(1), None, 3), Error::::ClassNeeded); + }); +} diff --git a/frame/conviction-voting/src/types.rs b/frame/conviction-voting/src/types.rs new file mode 100644 index 0000000000000..2ad1a164dd143 --- /dev/null +++ b/frame/conviction-voting/src/types.rs @@ -0,0 +1,236 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! Miscellaneous additional datatypes. + +use sp_std::marker::PhantomData; + +use super::*; +use crate::{AccountVote, Conviction, Vote}; +use codec::{Codec, Decode, Encode, MaxEncodedLen}; +use frame_support::{ + traits::VoteTally, CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, + RuntimeDebugNoBound, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Saturating, Zero}, + RuntimeDebug, +}; + +/// Info regarding an ongoing referendum. +#[derive( + CloneNoBound, + DefaultNoBound, + PartialEqNoBound, + EqNoBound, + RuntimeDebugNoBound, + TypeInfo, + Encode, + Decode, + MaxEncodedLen, +)] +#[scale_info(skip_type_params(Total))] +pub struct Tally< + Votes: Clone + Default + PartialEq + Eq + sp_std::fmt::Debug + TypeInfo + Codec, + Total, +> { + /// The number of aye votes, expressed in terms of post-conviction lock-vote. + pub ayes: Votes, + /// The number of nay votes, expressed in terms of post-conviction lock-vote. + pub nays: Votes, + /// The amount of funds currently expressing its opinion. Pre-conviction. + pub turnout: Votes, + /// Dummy. + dummy: PhantomData, +} + +impl< + Votes: Clone + + Default + + PartialEq + + Eq + + sp_std::fmt::Debug + + Copy + + AtLeast32BitUnsigned + + TypeInfo + + Codec, + Total: Get, + > VoteTally for Tally +{ + fn ayes(&self) -> Votes { + self.ayes + } + + fn turnout(&self) -> Perbill { + Perbill::from_rational(self.turnout, Total::get()) + } + + fn approval(&self) -> Perbill { + Perbill::from_rational(self.ayes, self.ayes.saturating_add(self.nays)) + } + + #[cfg(feature = "runtime-benchmarks")] + fn unanimity() -> Self { + Self { ayes: Total::get(), nays: Zero::zero(), turnout: Total::get(), dummy: PhantomData } + } + + #[cfg(feature = "runtime-benchmarks")] + fn from_requirements(turnout: Perbill, approval: Perbill) -> Self { + let turnout = turnout.mul_ceil(Total::get()); + let ayes = approval.mul_ceil(turnout); + Self { ayes, nays: turnout - ayes, turnout, dummy: PhantomData } + } +} + +impl< + Votes: Clone + + Default + + PartialEq + + Eq + + sp_std::fmt::Debug + + Copy + + AtLeast32BitUnsigned + + TypeInfo + + Codec, + Total: Get, + > Tally +{ + /// Create a new tally. + pub fn new(vote: Vote, balance: Votes) -> Self { + let Delegations { votes, capital } = vote.conviction.votes(balance); + Self { + ayes: if vote.aye { votes } else { Zero::zero() }, + nays: if vote.aye { Zero::zero() } else { votes }, + turnout: capital, + dummy: PhantomData, + } + } + + pub fn from_parts(ayes: Votes, nays: Votes, turnout: Votes) -> Self { + Self { ayes, nays, turnout, dummy: PhantomData } + } + + /// Add an account's vote into the tally. + pub fn add(&mut self, vote: AccountVote) -> Option<()> { + match vote { + AccountVote::Standard { vote, balance } => { + let Delegations { votes, capital } = vote.conviction.votes(balance); + self.turnout = self.turnout.checked_add(&capital)?; + match vote.aye { + true => self.ayes = self.ayes.checked_add(&votes)?, + false => self.nays = self.nays.checked_add(&votes)?, + } + }, + AccountVote::Split { aye, nay } => { + let aye = Conviction::None.votes(aye); + let nay = Conviction::None.votes(nay); + self.turnout = self.turnout.checked_add(&aye.capital)?.checked_add(&nay.capital)?; + self.ayes = self.ayes.checked_add(&aye.votes)?; + self.nays = self.nays.checked_add(&nay.votes)?; + }, + } + Some(()) + } + + /// Remove an account's vote from the tally. + pub fn remove(&mut self, vote: AccountVote) -> Option<()> { + match vote { + AccountVote::Standard { vote, balance } => { + let Delegations { votes, capital } = vote.conviction.votes(balance); + self.turnout = self.turnout.checked_sub(&capital)?; + match vote.aye { + true => self.ayes = self.ayes.checked_sub(&votes)?, + false => self.nays = self.nays.checked_sub(&votes)?, + } + }, + AccountVote::Split { aye, nay } => { + let aye = Conviction::None.votes(aye); + let nay = Conviction::None.votes(nay); + self.turnout = self.turnout.checked_sub(&aye.capital)?.checked_sub(&nay.capital)?; + self.ayes = self.ayes.checked_sub(&aye.votes)?; + self.nays = self.nays.checked_sub(&nay.votes)?; + }, + } + Some(()) + } + + /// Increment some amount of votes. + pub fn increase(&mut self, approve: bool, delegations: Delegations) { + self.turnout = self.turnout.saturating_add(delegations.capital); + match approve { + true => self.ayes = self.ayes.saturating_add(delegations.votes), + false => self.nays = self.nays.saturating_add(delegations.votes), + } + } + + /// Decrement some amount of votes. + pub fn reduce(&mut self, approve: bool, delegations: Delegations) { + self.turnout = self.turnout.saturating_sub(delegations.capital); + match approve { + true => self.ayes = self.ayes.saturating_sub(delegations.votes), + false => self.nays = self.nays.saturating_sub(delegations.votes), + } + } +} + +/// Amount of votes and capital placed in delegation for an account. +#[derive( + Encode, Decode, Default, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, +)] +pub struct Delegations { + /// The number of votes (this is post-conviction). + pub votes: Balance, + /// The amount of raw capital, used for the turnout. + pub capital: Balance, +} + +impl Saturating for Delegations { + fn saturating_add(self, o: Self) -> Self { + Self { + votes: self.votes.saturating_add(o.votes), + capital: self.capital.saturating_add(o.capital), + } + } + + fn saturating_sub(self, o: Self) -> Self { + Self { + votes: self.votes.saturating_sub(o.votes), + capital: self.capital.saturating_sub(o.capital), + } + } + + fn saturating_mul(self, o: Self) -> Self { + Self { + votes: self.votes.saturating_mul(o.votes), + capital: self.capital.saturating_mul(o.capital), + } + } + + fn saturating_pow(self, exp: usize) -> Self { + Self { votes: self.votes.saturating_pow(exp), capital: self.capital.saturating_pow(exp) } + } +} + +/// Whether an `unvote` operation is able to make actions that are not strictly always in the +/// interest of an account. +pub enum UnvoteScope { + /// Permitted to do everything. + Any, + /// Permitted to do only the changes that do not need the owner's permission. + OnlyExpired, +} diff --git a/frame/conviction-voting/src/vote.rs b/frame/conviction-voting/src/vote.rs new file mode 100644 index 0000000000000..d7ca931de35a1 --- /dev/null +++ b/frame/conviction-voting/src/vote.rs @@ -0,0 +1,254 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! The vote datatype. + +use crate::{Conviction, Delegations}; +use codec::{Decode, Encode, EncodeLike, Input, MaxEncodedLen, Output}; +use frame_support::{pallet_prelude::Get, BoundedVec}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Saturating, Zero}, + RuntimeDebug, +}; +use sp_std::{convert::TryFrom, prelude::*, result::Result}; + +/// A number of lock periods, plus a vote, one way or the other. +#[derive(Copy, Clone, Eq, PartialEq, Default, RuntimeDebug, MaxEncodedLen)] +pub struct Vote { + pub aye: bool, + pub conviction: Conviction, +} + +impl Encode for Vote { + fn encode_to(&self, output: &mut T) { + output.push_byte(u8::from(self.conviction) | if self.aye { 0b1000_0000 } else { 0 }); + } +} + +impl EncodeLike for Vote {} + +impl Decode for Vote { + fn decode(input: &mut I) -> Result { + let b = input.read_byte()?; + Ok(Vote { + aye: (b & 0b1000_0000) == 0b1000_0000, + conviction: Conviction::try_from(b & 0b0111_1111) + .map_err(|_| codec::Error::from("Invalid conviction"))?, + }) + } +} + +impl TypeInfo for Vote { + type Identity = Self; + + fn type_info() -> scale_info::Type { + scale_info::Type::builder() + .path(scale_info::Path::new("Vote", module_path!())) + .composite( + scale_info::build::Fields::unnamed() + .field(|f| f.ty::().docs(&["Raw vote byte, encodes aye + conviction"])), + ) + } +} + +/// A vote for a referendum of a particular account. +#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum AccountVote { + /// A standard vote, one-way (approve or reject) with a given amount of conviction. + Standard { vote: Vote, balance: Balance }, + /// A split vote with balances given for both ways, and with no conviction, useful for + /// parachains when voting. + Split { aye: Balance, nay: Balance }, +} + +impl AccountVote { + /// Returns `Some` of the lock periods that the account is locked for, assuming that the + /// referendum passed iff `approved` is `true`. + pub fn locked_if(self, approved: bool) -> Option<(u32, Balance)> { + // winning side: can only be removed after the lock period ends. + match self { + AccountVote::Standard { vote: Vote { conviction: Conviction::None, .. }, .. } => None, + AccountVote::Standard { vote, balance } if vote.aye == approved => + Some((vote.conviction.lock_periods(), balance)), + _ => None, + } + } + + /// The total balance involved in this vote. + pub fn balance(self) -> Balance { + match self { + AccountVote::Standard { balance, .. } => balance, + AccountVote::Split { aye, nay } => aye.saturating_add(nay), + } + } + + /// Returns `Some` with whether the vote is an aye vote if it is standard, otherwise `None` if + /// it is split. + pub fn as_standard(self) -> Option { + match self { + AccountVote::Standard { vote, .. } => Some(vote.aye), + _ => None, + } + } +} + +/// A "prior" lock, i.e. a lock for some now-forgotten reason. +#[derive( + Encode, + Decode, + Default, + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, +)] +pub struct PriorLock(BlockNumber, Balance); + +impl PriorLock { + /// Accumulates an additional lock. + pub fn accumulate(&mut self, until: BlockNumber, amount: Balance) { + self.0 = self.0.max(until); + self.1 = self.1.max(amount); + } + + pub fn locked(&self) -> Balance { + self.1 + } + + pub fn rejig(&mut self, now: BlockNumber) { + if now >= self.0 { + self.0 = Zero::zero(); + self.1 = Zero::zero(); + } + } +} + +/// Information concerning the delegation of some voting power. +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Delegating { + /// The amount of balance delegated. + pub balance: Balance, + /// The account to which the voting power is delegated. + pub target: AccountId, + /// The conviction with which the voting power is delegated. When this gets undelegated, the + /// relevant lock begins. + pub conviction: Conviction, + /// The total amount of delegations that this account has received, post-conviction-weighting. + pub delegations: Delegations, + /// Any pre-existing locks from past voting/delegating activity. + pub prior: PriorLock, +} + +/// Information concerning the direct vote-casting of some voting power. +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(MaxVotes))] +pub struct Casting +where + MaxVotes: Get, +{ + /// The current votes of the account. + pub votes: BoundedVec<(PollIndex, AccountVote), MaxVotes>, + /// The total amount of delegations that this account has received, post-conviction-weighting. + pub delegations: Delegations, + /// Any pre-existing locks from past voting/delegating activity. + pub prior: PriorLock, +} + +/// An indicator for what an account is doing; it can either be delegating or voting. +#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(MaxVotes))] +pub enum Voting +where + MaxVotes: Get, +{ + /// The account is voting directly. + Casting(Casting), + /// The account is delegating `balance` of its balance to a `target` account with `conviction`. + Delegating(Delegating), +} + +impl Default + for Voting +where + MaxVotes: Get, +{ + fn default() -> Self { + Voting::Casting(Casting { + votes: Default::default(), + delegations: Default::default(), + prior: PriorLock(Zero::zero(), Default::default()), + }) + } +} + +impl AsMut> + for Voting +where + MaxVotes: Get, +{ + fn as_mut(&mut self) -> &mut PriorLock { + match self { + Voting::Casting(Casting { prior, .. }) => prior, + Voting::Delegating(Delegating { prior, .. }) => prior, + } + } +} + +impl< + Balance: Saturating + Ord + Zero + Copy, + BlockNumber: Ord + Copy + Zero, + AccountId, + PollIndex, + MaxVotes, + > Voting +where + MaxVotes: Get, +{ + pub fn rejig(&mut self, now: BlockNumber) { + AsMut::>::as_mut(self).rejig(now); + } + + /// The amount of this account's balance that much currently be locked due to voting. + pub fn locked_balance(&self) -> Balance { + match self { + Voting::Casting(Casting { votes, prior, .. }) => + votes.iter().map(|i| i.1.balance()).fold(prior.locked(), |a, i| a.max(i)), + Voting::Delegating(Delegating { balance, prior, .. }) => *balance.max(&prior.locked()), + } + } + + pub fn set_common( + &mut self, + delegations: Delegations, + prior: PriorLock, + ) { + let (d, p) = match self { + Voting::Casting(Casting { ref mut delegations, ref mut prior, .. }) => + (delegations, prior), + Voting::Delegating(Delegating { ref mut delegations, ref mut prior, .. }) => + (delegations, prior), + }; + *d = delegations; + *p = prior; + } +} diff --git a/frame/conviction-voting/src/weights.rs b/frame/conviction-voting/src/weights.rs new file mode 100644 index 0000000000000..da15aac0b47ca --- /dev/null +++ b/frame/conviction-voting/src/weights.rs @@ -0,0 +1,201 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 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_conviction_voting +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-01-09, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_conviction_voting +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/conviction-voting/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_conviction_voting. +pub trait WeightInfo { + fn vote_new() -> Weight; + fn vote_existing() -> Weight; + fn remove_vote() -> Weight; + fn remove_other_vote() -> Weight; + fn delegate(r: u32, ) -> Weight; + fn undelegate(r: u32, ) -> Weight; + fn unlock() -> Weight; +} + +/// Weights for pallet_conviction_voting using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn vote_new() -> Weight { + (159_647_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn vote_existing() -> Weight { + (339_851_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) + } + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn remove_vote() -> Weight { + (317_673_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:0) + fn remove_other_vote() -> Weight { + (52_222_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: ConvictionVoting VotingFor (r:2 w:2) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn delegate(r: u32, ) -> Weight { + (61_553_000 as Weight) + // Standard Error: 123_000 + .saturating_add((33_092_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(r as Weight))) + } + // Storage: ConvictionVoting VotingFor (r:2 w:2) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn undelegate(r: u32, ) -> Weight { + (42_037_000 as Weight) + // Standard Error: 582_000 + .saturating_add((32_296_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((3 as Weight).saturating_mul(r as Weight))) + } + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn unlock() -> Weight { + (69_017_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn vote_new() -> Weight { + (159_647_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn vote_existing() -> Weight { + (339_851_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + } + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn remove_vote() -> Weight { + (317_673_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:0) + fn remove_other_vote() -> Weight { + (52_222_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: ConvictionVoting VotingFor (r:2 w:2) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn delegate(r: u32, ) -> Weight { + (61_553_000 as Weight) + // Standard Error: 123_000 + .saturating_add((33_092_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(r as Weight))) + } + // Storage: ConvictionVoting VotingFor (r:2 w:2) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn undelegate(r: u32, ) -> Weight { + (42_037_000 as Weight) + // Standard Error: 582_000 + .saturating_add((32_296_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().reads((3 as Weight).saturating_mul(r as Weight))) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes((3 as Weight).saturating_mul(r as Weight))) + } + // Storage: ConvictionVoting VotingFor (r:1 w:1) + // Storage: ConvictionVoting ClassLocksFor (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn unlock() -> Weight { + (69_017_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } +} diff --git a/frame/democracy/src/lib.rs b/frame/democracy/src/lib.rs index 4580767d875c3..b578df5909306 100644 --- a/frame/democracy/src/lib.rs +++ b/frame/democracy/src/lib.rs @@ -439,14 +439,6 @@ pub mod pallet { ValueQuery, >; - /// Accounts for which there are locks in action which may be removed at some point in the - /// future. The value is the block number at which the lock expires and may be removed. - /// - /// TWOX-NOTE: OK ― `AccountId` is a secure hash. - #[pallet::storage] - #[pallet::getter(fn locks)] - pub type Locks = StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber>; - /// True if the last referendum tabled was submitted externally. False if it was a public /// proposal. // TODO: There should be any number of tabling origins, not just public and "external" diff --git a/frame/referenda/Cargo.toml b/frame/referenda/Cargo.toml new file mode 100644 index 0000000000000..e979f2b0c4a37 --- /dev/null +++ b/frame/referenda/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "pallet-referenda" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for inclusive on-chain decisions" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.126", optional = true, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } +sp-io = { version = "5.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "5.0.0", default-features = false, path = "../../primitives/runtime" } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +assert_matches = { version = "1.5", optional = true } + +[dev-dependencies] +sp-core = { version = "5.0.0", path = "../../primitives/core" } +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-scheduler = { version = "4.0.0-dev", path = "../scheduler" } +pallet-preimage = { version = "4.0.0-dev", path = "../preimage" } +assert_matches = { version = "1.5" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-io/std", + "frame-benchmarking/std", + "frame-support/std", + "sp-runtime/std", + "frame-system/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "assert_matches", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/referenda/README.md b/frame/referenda/README.md new file mode 100644 index 0000000000000..85031a0113033 --- /dev/null +++ b/frame/referenda/README.md @@ -0,0 +1,8 @@ +# Referenda Pallet + +- [`assembly::Config`](https://docs.rs/pallet-assembly/latest/pallet_assembly/trait.Config.html) +- [`Call`](https://docs.rs/pallet-assembly/latest/pallet_assembly/enum.Call.html) + +## Overview + +The Assembly pallet handles the administration of general stakeholder voting. diff --git a/frame/referenda/src/benchmarking.rs b/frame/referenda/src/benchmarking.rs new file mode 100644 index 0000000000000..76a8173f16c9a --- /dev/null +++ b/frame/referenda/src/benchmarking.rs @@ -0,0 +1,520 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 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. + +//! Democracy pallet benchmarking. + +use super::*; +use crate::Pallet as Referenda; +use assert_matches::assert_matches; +use frame_benchmarking::{account, benchmarks, whitelist_account}; +use frame_support::{ + assert_ok, + traits::{Currency, EnsureOrigin}, +}; +use frame_system::RawOrigin; +use sp_runtime::traits::{Bounded, Hash}; + +const SEED: u32 = 0; + +#[allow(dead_code)] +fn assert_last_event(generic_event: ::Event) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn funded_account(name: &'static str, index: u32) -> T::AccountId { + let caller: T::AccountId = account(name, index, SEED); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + caller +} + +fn create_referendum() -> (T::AccountId, ReferendumIndex) { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + assert_ok!(Referenda::::submit( + RawOrigin::Signed(caller.clone()).into(), + RawOrigin::Root.into(), + T::Hashing::hash_of(&0), + AtOrAfter::After(0u32.into()) + )); + let index = ReferendumCount::::get() - 1; + (caller, index) +} + +fn place_deposit(index: ReferendumIndex) { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + assert_ok!(Referenda::::place_decision_deposit( + RawOrigin::Signed(caller.clone()).into(), + index, + )); +} + +fn nudge(index: ReferendumIndex) { + assert_ok!(Referenda::::nudge_referendum(RawOrigin::Root.into(), index)); +} + +fn fill_queue( + index: ReferendumIndex, + spaces: u32, + pass_after: u32, +) -> Vec { + // First, create enough other referendums to fill the track. + let mut others = vec![]; + for _ in 0..info::(index).max_deciding { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + others.push(index); + } + + // We will also need enough referenda which are queued and passing, we want `MaxQueued - 1` + // in order to force the maximum amount of work to insert ours into the queue. + for _ in spaces..T::MaxQueued::get() { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + make_passing_after::(index, Perbill::from_percent(pass_after)); + others.push(index); + } + + // Skip to when they can start being decided. + skip_prepare_period::(index); + + // Manually nudge the other referenda first to ensure that they begin. + others.iter().for_each(|&i| nudge::(i)); + + others +} + +fn info(index: ReferendumIndex) -> &'static TrackInfoOf { + let status = Referenda::::ensure_ongoing(index).unwrap(); + T::Tracks::info(status.track).expect("Id value returned from T::Tracks") +} + +fn make_passing_after(index: ReferendumIndex, period_portion: Perbill) { + let turnout = info::(index).min_turnout.threshold(period_portion); + let approval = info::(index).min_approval.threshold(period_portion); + Referenda::::access_poll(index, |status| { + if let PollStatus::Ongoing(tally, ..) = status { + *tally = T::Tally::from_requirements(turnout, approval); + } + }); +} + +fn make_passing(index: ReferendumIndex) { + Referenda::::access_poll(index, |status| { + if let PollStatus::Ongoing(tally, ..) = status { + *tally = T::Tally::unanimity(); + } + }); +} + +fn make_failing(index: ReferendumIndex) { + Referenda::::access_poll(index, |status| { + if let PollStatus::Ongoing(tally, ..) = status { + *tally = T::Tally::default(); + } + }); +} + +fn skip_prepare_period(index: ReferendumIndex) { + let status = Referenda::::ensure_ongoing(index).unwrap(); + let prepare_period_over = status.submitted + info::(index).prepare_period; + frame_system::Pallet::::set_block_number(prepare_period_over); +} + +fn skip_decision_period(index: ReferendumIndex) { + let status = Referenda::::ensure_ongoing(index).unwrap(); + let decision_period_over = status.deciding.unwrap().since + info::(index).decision_period; + frame_system::Pallet::::set_block_number(decision_period_over); +} + +fn skip_confirm_period(index: ReferendumIndex) { + let status = Referenda::::ensure_ongoing(index).unwrap(); + let confirm_period_over = status.deciding.unwrap().confirming.unwrap(); + frame_system::Pallet::::set_block_number(confirm_period_over); +} + +fn skip_timeout_period(index: ReferendumIndex) { + let status = Referenda::::ensure_ongoing(index).unwrap(); + let timeout_period_over = status.submitted + T::UndecidingTimeout::get(); + frame_system::Pallet::::set_block_number(timeout_period_over); +} + +fn alarm_time(index: ReferendumIndex) -> T::BlockNumber { + let status = Referenda::::ensure_ongoing(index).unwrap(); + status.alarm.unwrap().0 +} + +fn is_confirming(index: ReferendumIndex) -> bool { + let status = Referenda::::ensure_ongoing(index).unwrap(); + matches!( + status, + ReferendumStatus { deciding: Some(DecidingStatus { confirming: Some(_), .. }), .. } + ) +} + +fn is_not_confirming(index: ReferendumIndex) -> bool { + let status = Referenda::::ensure_ongoing(index).unwrap(); + matches!( + status, + ReferendumStatus { deciding: Some(DecidingStatus { confirming: None, .. }), .. } + ) +} + +benchmarks! { + submit { + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + }: _( + RawOrigin::Signed(caller), + RawOrigin::Root.into(), + T::Hashing::hash_of(&0), + AtOrAfter::After(0u32.into()) + ) verify { + let index = ReferendumCount::::get().checked_sub(1).unwrap(); + assert_matches!(ReferendumInfoFor::::get(index), Some(ReferendumInfo::Ongoing(_))); + } + + place_decision_deposit_preparing { + let (caller, index) = create_referendum::(); + }: place_decision_deposit(RawOrigin::Signed(caller), index) + verify { + assert!(Referenda::::ensure_ongoing(index).unwrap().decision_deposit.is_some()); + } + + place_decision_deposit_queued { + let (caller, index) = create_referendum::(); + fill_queue::(index, 1, 90); + }: place_decision_deposit(RawOrigin::Signed(caller), index) + verify { + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + assert_eq!(TrackQueue::::get(&track)[0], (index, 0u32.into())); + } + + place_decision_deposit_not_queued { + let (caller, index) = create_referendum::(); + fill_queue::(index, 0, 90); + }: place_decision_deposit(RawOrigin::Signed(caller), index) + verify { + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + assert!(TrackQueue::::get(&track).into_iter().all(|(i, _)| i != index)); + } + + place_decision_deposit_passing { + let (caller, index) = create_referendum::(); + skip_prepare_period::(index); + make_passing::(index); + }: place_decision_deposit(RawOrigin::Signed(caller), index) + verify { + assert!(is_confirming::(index)); + } + + place_decision_deposit_failing { + let (caller, index) = create_referendum::(); + skip_prepare_period::(index); + }: place_decision_deposit(RawOrigin::Signed(caller), index) + verify { + assert!(is_not_confirming::(index)); + } + + refund_decision_deposit { + let (caller, index) = create_referendum::(); + place_deposit::(index); + assert_ok!(Referenda::::cancel(T::CancelOrigin::successful_origin(), index)); + }: _(RawOrigin::Signed(caller), index) + verify { + assert_matches!(ReferendumInfoFor::::get(index), Some(ReferendumInfo::Cancelled(_, _, None))); + } + + cancel { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + }: _(T::CancelOrigin::successful_origin(), index) + verify { + assert_matches!(ReferendumInfoFor::::get(index), Some(ReferendumInfo::Cancelled(..))); + } + + kill { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + }: _(T::KillOrigin::successful_origin(), index) + verify { + assert_matches!(ReferendumInfoFor::::get(index), Some(ReferendumInfo::Killed(..))); + } + + one_fewer_deciding_queue_empty { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + nudge::(index); + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_ok!(Referenda::::cancel(T::CancelOrigin::successful_origin(), index)); + assert_eq!(DecidingCount::::get(&track), 1); + }: one_fewer_deciding(RawOrigin::Root, track.clone()) + verify { + assert_eq!(DecidingCount::::get(&track), 0); + } + + one_fewer_deciding_failing { + let (_caller, index) = create_referendum::(); + // No spaces free in the queue. + let queued = fill_queue::(index, 0, 90); + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_ok!(Referenda::::cancel(T::CancelOrigin::successful_origin(), queued[0])); + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + let deciding_count = DecidingCount::::get(&track); + }: one_fewer_deciding(RawOrigin::Root, track.clone()) + verify { + assert_eq!(DecidingCount::::get(&track), deciding_count); + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get() - 1); + assert!(queued.into_iter().skip(1).all(|i| Referenda::::ensure_ongoing(i) + .unwrap() + .deciding + .map_or(true, |d| d.confirming.is_none()) + )); + } + + one_fewer_deciding_passing { + let (_caller, index) = create_referendum::(); + // No spaces free in the queue. + let queued = fill_queue::(index, 0, 0); + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_ok!(Referenda::::cancel(T::CancelOrigin::successful_origin(), queued[0])); + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + let deciding_count = DecidingCount::::get(&track); + }: one_fewer_deciding(RawOrigin::Root, track.clone()) + verify { + assert_eq!(DecidingCount::::get(&track), deciding_count); + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get() - 1); + assert!(queued.into_iter().skip(1).all(|i| Referenda::::ensure_ongoing(i) + .unwrap() + .deciding + .map_or(true, |d| d.confirming.is_some()) + )); + } + + nudge_referendum_requeued_insertion { + // First create our referendum and place the deposit. It will be failing. + let (_caller, index) = create_referendum::(); + place_deposit::(index); + fill_queue::(index, 0, 90); + + // Now nudge ours, with the track now full and the queue full of referenda with votes, + // ours will not be in the queue. + nudge::(index); + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert!(TrackQueue::::get(&track).into_iter().all(|(i, _)| i != index)); + + // Now alter the voting, so that ours goes into pole-position and shifts others down. + make_passing::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let t = TrackQueue::::get(&track); + assert_eq!(t.len() as u32, T::MaxQueued::get()); + assert_eq!(t[t.len() - 1].0, index); + } + + nudge_referendum_requeued_slide { + // First create our referendum and place the deposit. It will be failing. + let (_caller, index) = create_referendum::(); + place_deposit::(index); + fill_queue::(index, 1, 90); + + // Now nudge ours, with the track now full, ours will be queued, but with no votes, it + // will have the worst position. + nudge::(index); + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + assert_eq!(TrackQueue::::get(&track)[0], (index, 0u32.into())); + + // Now alter the voting, so that ours leap-frogs all into the best position. + make_passing::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let t = TrackQueue::::get(&track); + assert_eq!(t.len() as u32, T::MaxQueued::get()); + assert_eq!(t[t.len() - 1].0, index); + } + + nudge_referendum_queued { + // NOTE: worst possible queue situation is with a queue full of passing refs with one slot + // free and this failing. It would result in `QUEUE_SIZE - 1` items being shifted for the + // insertion at the beginning. + + // First create our referendum and place the deposit. It will be failing. + let (_caller, index) = create_referendum::(); + place_deposit::(index); + fill_queue::(index, 1, 0); + + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get() - 1); + assert!(TrackQueue::::get(&track).into_iter().all(|(_, v)| v > 0u32.into())); + + // Then nudge ours, with the track now full, ours will be queued. + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + assert_eq!(TrackQueue::::get(&track)[0], (index, 0u32.into())); + } + + nudge_referendum_not_queued { + // First create our referendum and place the deposit. It will be failing. + let (_caller, index) = create_referendum::(); + place_deposit::(index); + fill_queue::(index, 0, 0); + + let track = Referenda::::ensure_ongoing(index).unwrap().track; + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + assert!(TrackQueue::::get(&track).into_iter().all(|(_, v)| v > 0u32.into())); + + // Then nudge ours, with the track now full, ours will be queued. + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert_eq!(TrackQueue::::get(&track).len() as u32, T::MaxQueued::get()); + assert!(TrackQueue::::get(&track).into_iter().all(|(i, _)| i != index)); + } + + nudge_referendum_no_deposit { + let (_caller, index) = create_referendum::(); + skip_prepare_period::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let status = Referenda::::ensure_ongoing(index).unwrap(); + assert_matches!(status, ReferendumStatus { deciding: None, .. }); + } + + nudge_referendum_preparing { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let status = Referenda::::ensure_ongoing(index).unwrap(); + assert_matches!(status, ReferendumStatus { deciding: None, .. }); + } + + nudge_referendum_timed_out { + let (_caller, index) = create_referendum::(); + skip_timeout_period::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let info = ReferendumInfoFor::::get(index).unwrap(); + assert_matches!(info, ReferendumInfo::TimedOut(..)); + } + + nudge_referendum_begin_deciding_failing { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert!(is_not_confirming::(index)); + } + + nudge_referendum_begin_deciding_passing { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + make_passing::(index); + skip_prepare_period::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert!(is_confirming::(index)); + } + + nudge_referendum_begin_confirming { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + nudge::(index); + assert!(!is_confirming::(index)); + make_passing::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert!(is_confirming::(index)); + } + + nudge_referendum_end_confirming { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + make_passing::(index); + nudge::(index); + assert!(is_confirming::(index)); + make_failing::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert!(!is_confirming::(index)); + } + + nudge_referendum_continue_not_confirming { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + nudge::(index); + assert!(!is_confirming::(index)); + let old_alarm = alarm_time::(index); + make_passing_after::(index, Perbill::from_percent(50)); + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert_ne!(old_alarm, alarm_time::(index)); + assert!(!is_confirming::(index)); + } + + nudge_referendum_continue_confirming { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + make_passing::(index); + skip_prepare_period::(index); + nudge::(index); + assert!(is_confirming::(index)); + let old_alarm = alarm_time::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + assert!(is_confirming::(index)); + } + + nudge_referendum_approved { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + make_passing::(index); + nudge::(index); + skip_confirm_period::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let info = ReferendumInfoFor::::get(index).unwrap(); + assert_matches!(info, ReferendumInfo::Approved(..)); + } + + nudge_referendum_rejected { + let (_caller, index) = create_referendum::(); + place_deposit::(index); + skip_prepare_period::(index); + nudge::(index); + skip_decision_period::(index); + }: nudge_referendum(RawOrigin::Root, index) + verify { + let info = ReferendumInfoFor::::get(index).unwrap(); + assert_matches!(info, ReferendumInfo::Rejected(..)); + } + + impl_benchmark_test_suite!( + Referenda, + crate::mock::new_test_ext(), + crate::mock::Test + ); +} diff --git a/frame/referenda/src/branch.rs b/frame/referenda/src/branch.rs new file mode 100644 index 0000000000000..6a4efa31e15e2 --- /dev/null +++ b/frame/referenda/src/branch.rs @@ -0,0 +1,172 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! Helpers for managing the different weights in various algorithmic branches. + +use super::Config; +use crate::weights::WeightInfo; + +/// Branches within the `begin_deciding` function. +pub enum BeginDecidingBranch { + Passing, + Failing, +} + +/// Branches within the `service_referendum` function. +pub enum ServiceBranch { + Fail, + NoDeposit, + Preparing, + Queued, + NotQueued, + RequeuedInsertion, + RequeuedSlide, + BeginDecidingPassing, + BeginDecidingFailing, + BeginConfirming, + ContinueConfirming, + EndConfirming, + ContinueNotConfirming, + Approved, + Rejected, + TimedOut, +} + +impl From for ServiceBranch { + fn from(x: BeginDecidingBranch) -> Self { + use BeginDecidingBranch::*; + use ServiceBranch::*; + match x { + Passing => BeginDecidingPassing, + Failing => BeginDecidingFailing, + } + } +} + +impl ServiceBranch { + /// Return the weight of the `nudge` function when it takes the branch denoted by `self`. + pub fn weight_of_nudge(self) -> frame_support::weights::Weight { + use ServiceBranch::*; + match self { + NoDeposit => T::WeightInfo::nudge_referendum_no_deposit(), + Preparing => T::WeightInfo::nudge_referendum_preparing(), + Queued => T::WeightInfo::nudge_referendum_queued(), + NotQueued => T::WeightInfo::nudge_referendum_not_queued(), + RequeuedInsertion => T::WeightInfo::nudge_referendum_requeued_insertion(), + RequeuedSlide => T::WeightInfo::nudge_referendum_requeued_slide(), + BeginDecidingPassing => T::WeightInfo::nudge_referendum_begin_deciding_passing(), + BeginDecidingFailing => T::WeightInfo::nudge_referendum_begin_deciding_failing(), + BeginConfirming => T::WeightInfo::nudge_referendum_begin_confirming(), + ContinueConfirming => T::WeightInfo::nudge_referendum_continue_confirming(), + EndConfirming => T::WeightInfo::nudge_referendum_end_confirming(), + ContinueNotConfirming => T::WeightInfo::nudge_referendum_continue_not_confirming(), + Approved => T::WeightInfo::nudge_referendum_approved(), + Rejected => T::WeightInfo::nudge_referendum_rejected(), + TimedOut | Fail => T::WeightInfo::nudge_referendum_timed_out(), + } + } + + /// Return the maximum possible weight of the `nudge` function. + pub fn max_weight_of_nudge() -> frame_support::weights::Weight { + 0.max(T::WeightInfo::nudge_referendum_no_deposit()) + .max(T::WeightInfo::nudge_referendum_preparing()) + .max(T::WeightInfo::nudge_referendum_queued()) + .max(T::WeightInfo::nudge_referendum_not_queued()) + .max(T::WeightInfo::nudge_referendum_requeued_insertion()) + .max(T::WeightInfo::nudge_referendum_requeued_slide()) + .max(T::WeightInfo::nudge_referendum_begin_deciding_passing()) + .max(T::WeightInfo::nudge_referendum_begin_deciding_failing()) + .max(T::WeightInfo::nudge_referendum_begin_confirming()) + .max(T::WeightInfo::nudge_referendum_continue_confirming()) + .max(T::WeightInfo::nudge_referendum_end_confirming()) + .max(T::WeightInfo::nudge_referendum_continue_not_confirming()) + .max(T::WeightInfo::nudge_referendum_approved()) + .max(T::WeightInfo::nudge_referendum_rejected()) + .max(T::WeightInfo::nudge_referendum_timed_out()) + } + + /// Return the weight of the `place_decision_deposit` function when it takes the branch denoted + /// by `self`. + pub fn weight_of_deposit(self) -> Option { + use ServiceBranch::*; + Some(match self { + Preparing => T::WeightInfo::place_decision_deposit_preparing(), + Queued => T::WeightInfo::place_decision_deposit_queued(), + NotQueued => T::WeightInfo::place_decision_deposit_not_queued(), + BeginDecidingPassing => T::WeightInfo::place_decision_deposit_passing(), + BeginDecidingFailing => T::WeightInfo::place_decision_deposit_failing(), + BeginConfirming | + ContinueConfirming | + EndConfirming | + ContinueNotConfirming | + Approved | + Rejected | + RequeuedInsertion | + RequeuedSlide | + TimedOut | + Fail | + NoDeposit => return None, + }) + } + + /// Return the maximum possible weight of the `place_decision_deposit` function. + pub fn max_weight_of_deposit() -> frame_support::weights::Weight { + 0.max(T::WeightInfo::place_decision_deposit_preparing()) + .max(T::WeightInfo::place_decision_deposit_queued()) + .max(T::WeightInfo::place_decision_deposit_not_queued()) + .max(T::WeightInfo::place_decision_deposit_passing()) + .max(T::WeightInfo::place_decision_deposit_failing()) + } +} + +/// Branches that the `one_fewer_deciding` function may take. +pub enum OneFewerDecidingBranch { + QueueEmpty, + BeginDecidingPassing, + BeginDecidingFailing, +} + +impl From for OneFewerDecidingBranch { + fn from(x: BeginDecidingBranch) -> Self { + use BeginDecidingBranch::*; + use OneFewerDecidingBranch::*; + match x { + Passing => BeginDecidingPassing, + Failing => BeginDecidingFailing, + } + } +} + +impl OneFewerDecidingBranch { + /// Return the weight of the `one_fewer_deciding` function when it takes the branch denoted + /// by `self`. + pub fn weight(self) -> frame_support::weights::Weight { + use OneFewerDecidingBranch::*; + match self { + QueueEmpty => T::WeightInfo::one_fewer_deciding_queue_empty(), + BeginDecidingPassing => T::WeightInfo::one_fewer_deciding_passing(), + BeginDecidingFailing => T::WeightInfo::one_fewer_deciding_failing(), + } + } + + /// Return the maximum possible weight of the `one_fewer_deciding` function. + pub fn max_weight() -> frame_support::weights::Weight { + 0.max(T::WeightInfo::one_fewer_deciding_queue_empty()) + .max(T::WeightInfo::one_fewer_deciding_passing()) + .max(T::WeightInfo::one_fewer_deciding_failing()) + } +} diff --git a/frame/referenda/src/lib.rs b/frame/referenda/src/lib.rs new file mode 100644 index 0000000000000..fb8e9aa6a6db7 --- /dev/null +++ b/frame/referenda/src/lib.rs @@ -0,0 +1,1065 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! # Referenda Pallet +//! +//! ## Overview +//! +//! A pallet for executing referenda. No voting logic is present here, and the `Polling` and +//! `PollStatus` traits are used to allow the voting logic (likely in a pallet) to be utilized. +//! +//! A referendum is a vote on whether a proposal should be dispatched from a particular origin. The +//! origin is used to determine which one of several _tracks_ that a referendum happens under. +//! Tracks each have their own configuration which governs the voting process and parameters. +//! +//! A referendum's lifecycle has three main stages: Preparation, deciding and conclusion. +//! Referenda are considered "ongoing" immediately after submission until their eventual +//! conclusion, and votes may be cast throughout. +//! +//! In order to progress from preparating to being decided, three things must be in place: +//! - There must have been a *Decision Deposit* placed, an amount determined by the track. Anyone +//! may place this deposit. +//! - A period must have elapsed since submission of the referendum. This period is known as the +//! *Preparation Period* and is determined by the track. +//! - The track must not already be at capacity with referendum being decided. The maximum number of +//! referenda which may be being decided simultaneously is determined by the track. +//! +//! In order to become concluded, one of three things must happen: +//! - The referendum should remain in an unbroken _Passing_ state for a period of time. This +//! is known as the _Confirmation Period_ and is determined by the track. A referendum is considered +//! _Passing_ when there is a sufficiently high turnout and approval, given the amount of time it +//! has been being decided. Generally the threshold for what counts as being "sufficiently high" +//! will reduce over time. The curves setting these thresholds are determined by the track. In this +//! case, the referendum is considered _Approved_ and the proposal is scheduled for dispatch. +//! - The referendum reaches the end of its deciding phase outside not _Passing_. It ends in +//! rejection and the proposal is not dispatched. +//! - The referendum is cancelled. +//! +//! A general time-out is also in place and referenda which exist in preparation for too long may +//! conclude without ever entering into a deciding stage. +//! +//! Once a referendum is concluded, the decision deposit may be refunded. +//! +//! - [`Config`] +//! - [`Call`] + +#![recursion_limit = "256"] +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Codec, Encode}; +use frame_support::{ + ensure, + traits::{ + schedule::{ + v2::{Anon as ScheduleAnon, Named as ScheduleNamed}, + DispatchTime, MaybeHashed, + }, + Currency, Get, LockIdentifier, LockableCurrency, OnUnbalanced, OriginTrait, PollStatus, + Polling, ReservableCurrency, VoteTally, + }, + BoundedVec, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Dispatchable, One, Saturating, Zero}, + DispatchError, Perbill, +}; +use sp_std::{fmt::Debug, prelude::*}; + +mod branch; +mod types; +pub mod weights; +use branch::{BeginDecidingBranch, OneFewerDecidingBranch, ServiceBranch}; +pub use pallet::*; +pub use types::{ + AtOrAfter, BalanceOf, CallOf, Curve, DecidingStatus, DecidingStatusOf, Deposit, InsertSorted, + NegativeImbalanceOf, PalletsOriginOf, ReferendumIndex, ReferendumInfo, ReferendumInfoOf, + ReferendumStatus, ReferendumStatusOf, ScheduleAddressOf, TallyOf, TrackIdOf, TrackInfo, + TrackInfoOf, TracksInfo, VotesOf, +}; +pub use weights::WeightInfo; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +const ASSEMBLY_ID: LockIdentifier = *b"assembly"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{pallet_prelude::*, traits::EnsureOrigin, Parameter}; + use frame_system::pallet_prelude::*; + use sp_runtime::DispatchResult; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + // System level stuff. + type Call: Parameter + Dispatchable + From>; + type Event: From> + IsType<::Event>; + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + /// The Scheduler. + type Scheduler: ScheduleAnon, PalletsOriginOf, Hash = Self::Hash> + + ScheduleNamed, PalletsOriginOf, Hash = Self::Hash>; + /// Currency type for this pallet. + type Currency: ReservableCurrency + + LockableCurrency; + + // Origins and unbalances. + /// Origin from which any vote may be cancelled. + type CancelOrigin: EnsureOrigin; + /// Origin from which any vote may be killed. + type KillOrigin: EnsureOrigin; + /// Handler for the unbalanced reduction when slashing a preimage deposit. + type Slash: OnUnbalanced>; + /// The counting type for votes. Usually just balance. + type Votes: AtLeast32BitUnsigned + Copy + Parameter + Member; + /// The tallying type. + type Tally: VoteTally + Default + Clone + Codec + Eq + Debug + TypeInfo; + + // Constants + /// The minimum amount to be used as a deposit for a public referendum proposal. + #[pallet::constant] + type SubmissionDeposit: Get>; + + /// Maximum size of the referendum queue for a single track. + #[pallet::constant] + type MaxQueued: Get; + + /// The number of blocks after submission that a referendum must begin being decided by. + /// Once this passes, then anyone may cancel the referendum. + #[pallet::constant] + type UndecidingTimeout: Get; + + /// Quantization level for the referendum wakeup scheduler. A higher number will result in + /// fewer storage reads/writes needed for smaller voters, but also result in delays to the + /// automatic referendum status changes. Explicit servicing instructions are unaffected. + #[pallet::constant] + type AlarmInterval: Get; + + // The other stuff. + /// Information concerning the different referendum tracks. + type Tracks: TracksInfo< + BalanceOf, + Self::BlockNumber, + Origin = ::PalletsOrigin, + >; + } + + /// The next free referendum index, aka the number of referenda started so far. + #[pallet::storage] + pub type ReferendumCount = StorageValue<_, ReferendumIndex, ValueQuery>; + + /// Information concerning any given referendum. + /// + /// TWOX-NOTE: SAFE as indexes are not under an attacker’s control. + #[pallet::storage] + pub type ReferendumInfoFor = + StorageMap<_, Blake2_128Concat, ReferendumIndex, ReferendumInfoOf>; + + /// The sorted list of referenda ready to be decided but not yet being decided, ordered by + /// conviction-weighted approvals. + /// + /// This should be empty if `DecidingCount` is less than `TrackInfo::max_deciding`. + #[pallet::storage] + pub type TrackQueue = StorageMap< + _, + Twox64Concat, + TrackIdOf, + BoundedVec<(ReferendumIndex, T::Votes), T::MaxQueued>, + ValueQuery, + >; + + /// The number of referenda being decided currently. + #[pallet::storage] + pub type DecidingCount = StorageMap<_, Twox64Concat, TrackIdOf, u32, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A referendum has being submitted. + Submitted { + /// Index of the referendum. + index: ReferendumIndex, + /// The track (and by extension proposal dispatch origin) of this referendum. + track: TrackIdOf, + /// The hash of the proposal up for referendum. + proposal_hash: T::Hash, + }, + /// The decision deposit has been placed. + DecisionDepositPlaced { + /// Index of the referendum. + index: ReferendumIndex, + /// The account who placed the deposit. + who: T::AccountId, + /// The amount placed by the account. + amount: BalanceOf, + }, + /// The decision deposit has been refunded. + DecisionDepositRefunded { + /// Index of the referendum. + index: ReferendumIndex, + /// The account who placed the deposit. + who: T::AccountId, + /// The amount placed by the account. + amount: BalanceOf, + }, + /// A deposit has been slashaed. + DepositSlashed { + /// The account who placed the deposit. + who: T::AccountId, + /// The amount placed by the account. + amount: BalanceOf, + }, + /// A referendum has moved into the deciding phase. + DecisionStarted { + /// Index of the referendum. + index: ReferendumIndex, + /// The track (and by extension proposal dispatch origin) of this referendum. + track: TrackIdOf, + /// The hash of the proposal up for referendum. + proposal_hash: T::Hash, + /// The current tally of votes in this referendum. + tally: T::Tally, + }, + ConfirmStarted { + /// Index of the referendum. + index: ReferendumIndex, + }, + ConfirmAborted { + /// Index of the referendum. + index: ReferendumIndex, + }, + /// A referendum has ended its confirmation phase and is ready for approval. + Confirmed { + /// Index of the referendum. + index: ReferendumIndex, + /// The final tally of votes in this referendum. + tally: T::Tally, + }, + /// A referendum has been approved and its proposal has been scheduled. + Approved { + /// Index of the referendum. + index: ReferendumIndex, + }, + /// A proposal has been rejected by referendum. + Rejected { + /// Index of the referendum. + index: ReferendumIndex, + /// The final tally of votes in this referendum. + tally: T::Tally, + }, + /// A referendum has been timed out without being decided. + TimedOut { + /// Index of the referendum. + index: ReferendumIndex, + /// The final tally of votes in this referendum. + tally: T::Tally, + }, + /// A referendum has been cancelled. + Cancelled { + /// Index of the referendum. + index: ReferendumIndex, + /// The final tally of votes in this referendum. + tally: T::Tally, + }, + /// A referendum has been killed. + Killed { + /// Index of the referendum. + index: ReferendumIndex, + /// The final tally of votes in this referendum. + tally: T::Tally, + }, + } + + #[pallet::error] + pub enum Error { + /// Referendum is not ongoing. + NotOngoing, + /// Referendum's decision deposit is already paid. + HaveDeposit, + /// The track identifier given was invalid. + BadTrack, + /// There are already a full complement of referendums in progress for this track. + Full, + /// The queue of the track is empty. + QueueEmpty, + /// The referendum index provided is invalid in this context. + BadReferendum, + /// There was nothing to do in the advancement. + NothingToDo, + /// No track exists for the proposal origin. + NoTrack, + /// Any deposit cannot be refunded until after the decision is over. + Unfinished, + /// The deposit refunder is not the depositor. + NoPermission, + /// The deposit cannot be refunded since none was made. + NoDeposit, + } + + #[pallet::call] + impl Pallet { + /// Propose a referendum on a privileged action. + /// + /// - `origin`: must be `Signed` and the account must have `SubmissionDeposit` funds + /// available. + /// - `proposal_origin`: The origin from which the proposal should be executed. + /// - `proposal_hash`: The hash of the proposal preimage. + /// - `enactment_moment`: The moment that the proposal should be enacted. + /// + /// Emits `Submitted`. + #[pallet::weight(T::WeightInfo::submit())] + pub fn submit( + origin: OriginFor, + proposal_origin: PalletsOriginOf, + proposal_hash: T::Hash, + enactment_moment: AtOrAfter, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let track = T::Tracks::track_for(&proposal_origin).map_err(|_| Error::::NoTrack)?; + let submission_deposit = Self::take_deposit(who, T::SubmissionDeposit::get())?; + let index = ReferendumCount::::mutate(|x| { + let r = *x; + *x += 1; + r + }); + let now = frame_system::Pallet::::block_number(); + let nudge_call = Call::nudge_referendum { index }; + let status = ReferendumStatus { + track, + origin: proposal_origin, + proposal_hash: proposal_hash.clone(), + enactment: enactment_moment, + submitted: now, + submission_deposit, + decision_deposit: None, + deciding: None, + tally: Default::default(), + in_queue: false, + alarm: Self::set_alarm(nudge_call, now.saturating_add(T::UndecidingTimeout::get())), + }; + ReferendumInfoFor::::insert(index, ReferendumInfo::Ongoing(status)); + + Self::deposit_event(Event::::Submitted { index, track, proposal_hash }); + Ok(()) + } + + /// Post the Decision Deposit for a referendum. + /// + /// - `origin`: must be `Signed` and the account must have funds available for the + /// referendum's track's Decision Deposit. + /// - `index`: The index of the submitted referendum whose Decision Deposit is yet to be + /// posted. + /// + /// Emits `DecisionDepositPlaced`. + #[pallet::weight(ServiceBranch::max_weight_of_deposit::())] + pub fn place_decision_deposit( + origin: OriginFor, + index: ReferendumIndex, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let mut status = Self::ensure_ongoing(index)?; + ensure!(status.decision_deposit.is_none(), Error::::HaveDeposit); + let track = Self::track(status.track).ok_or(Error::::NoTrack)?; + status.decision_deposit = + Some(Self::take_deposit(who.clone(), track.decision_deposit)?); + let now = frame_system::Pallet::::block_number(); + let (info, _, branch) = Self::service_referendum(now, index, status); + ReferendumInfoFor::::insert(index, info); + let e = + Event::::DecisionDepositPlaced { index, who, amount: track.decision_deposit }; + Self::deposit_event(e); + Ok(branch.weight_of_deposit::().into()) + } + + /// Refund the Decision Deposit for a closed referendum back to the depositor. + /// + /// - `origin`: must be `Signed` or `Root`. + /// - `index`: The index of a closed referendum whose Decision Deposit has not yet been + /// refunded. + /// + /// Emits `DecisionDepositRefunded`. + #[pallet::weight(T::WeightInfo::refund_decision_deposit())] + pub fn refund_decision_deposit( + origin: OriginFor, + index: ReferendumIndex, + ) -> DispatchResult { + ensure_signed_or_root(origin)?; + let mut info = ReferendumInfoFor::::get(index).ok_or(Error::::BadReferendum)?; + let deposit = info + .take_decision_deposit() + .map_err(|_| Error::::Unfinished)? + .ok_or(Error::::NoDeposit)?; + Self::refund_deposit(Some(deposit.clone())); + ReferendumInfoFor::::insert(index, info); + let e = Event::::DecisionDepositRefunded { + index, + who: deposit.who, + amount: deposit.amount, + }; + Self::deposit_event(e); + Ok(()) + } + + /// Cancel an ongoing referendum. + /// + /// - `origin`: must be the `CancelOrigin`. + /// - `index`: The index of the referendum to be cancelled. + /// + /// Emits `Cancelled`. + #[pallet::weight(T::WeightInfo::cancel())] + pub fn cancel(origin: OriginFor, index: ReferendumIndex) -> DispatchResult { + T::CancelOrigin::ensure_origin(origin)?; + let status = Self::ensure_ongoing(index)?; + if let Some((_, last_alarm)) = status.alarm { + let _ = T::Scheduler::cancel(last_alarm); + } + Self::note_one_fewer_deciding(status.track); + Self::deposit_event(Event::::Cancelled { index, tally: status.tally }); + let info = ReferendumInfo::Cancelled( + frame_system::Pallet::::block_number(), + status.submission_deposit, + status.decision_deposit, + ); + ReferendumInfoFor::::insert(index, info); + Ok(()) + } + + /// Cancel an ongoing referendum and slash the deposits. + /// + /// - `origin`: must be the `KillOrigin`. + /// - `index`: The index of the referendum to be cancelled. + /// + /// Emits `Killed` and `DepositSlashed`. + #[pallet::weight(T::WeightInfo::kill())] + pub fn kill(origin: OriginFor, index: ReferendumIndex) -> DispatchResult { + T::KillOrigin::ensure_origin(origin)?; + let status = Self::ensure_ongoing(index)?; + if let Some((_, last_alarm)) = status.alarm { + let _ = T::Scheduler::cancel(last_alarm); + } + Self::note_one_fewer_deciding(status.track); + Self::deposit_event(Event::::Killed { index, tally: status.tally }); + Self::slash_deposit(Some(status.submission_deposit.clone())); + Self::slash_deposit(status.decision_deposit.clone()); + let info = ReferendumInfo::Killed(frame_system::Pallet::::block_number()); + ReferendumInfoFor::::insert(index, info); + Ok(()) + } + + /// Advance a referendum onto its next logical state. Only used internally. + /// + /// - `origin`: must be `Root`. + /// - `index`: the referendum to be advanced. + #[pallet::weight(ServiceBranch::max_weight_of_nudge::())] + pub fn nudge_referendum( + origin: OriginFor, + index: ReferendumIndex, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let now = frame_system::Pallet::::block_number(); + let mut status = Self::ensure_ongoing(index)?; + // This is our wake-up, so we can disregard the alarm. + status.alarm = None; + let (info, dirty, branch) = Self::service_referendum(now, index, status); + if dirty { + ReferendumInfoFor::::insert(index, info); + } + Ok(Some(branch.weight_of_nudge::()).into()) + } + + /// Advance a track onto its next logical state. Only used internally. + /// + /// - `origin`: must be `Root`. + /// - `track`: the track to be advanced. + /// + /// Action item for when there is now one fewer referendum in the deciding phase and the + /// `DecidingCount` is not yet updated. This means that we should either: + /// - begin deciding another referendum (and leave `DecidingCount` alone); or + /// - decrement `DecidingCount`. + #[pallet::weight(OneFewerDecidingBranch::max_weight::())] + pub fn one_fewer_deciding( + origin: OriginFor, + track: TrackIdOf, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let track_info = T::Tracks::info(track).ok_or(Error::::BadTrack)?; + let mut track_queue = TrackQueue::::get(track); + let branch = + if let Some((index, mut status)) = Self::next_for_deciding(&mut track_queue) { + let now = frame_system::Pallet::::block_number(); + let (maybe_alarm, branch) = + Self::begin_deciding(&mut status, index, now, track_info); + if let Some(set_alarm) = maybe_alarm { + Self::ensure_alarm_at(&mut status, index, set_alarm); + } + ReferendumInfoFor::::insert(index, ReferendumInfo::Ongoing(status)); + TrackQueue::::insert(track, track_queue); + branch.into() + } else { + DecidingCount::::mutate(track, |x| x.saturating_dec()); + OneFewerDecidingBranch::QueueEmpty + }; + Ok(Some(branch.weight::()).into()) + } + } +} + +impl Polling for Pallet { + type Index = ReferendumIndex; + type Votes = VotesOf; + type Moment = T::BlockNumber; + type Class = TrackIdOf; + + fn classes() -> Vec { + T::Tracks::tracks().iter().map(|x| x.0).collect() + } + + fn access_poll( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut T::Tally, T::BlockNumber, TrackIdOf>) -> R, + ) -> R { + match ReferendumInfoFor::::get(index) { + Some(ReferendumInfo::Ongoing(mut status)) => { + let result = f(PollStatus::Ongoing(&mut status.tally, status.track)); + let now = frame_system::Pallet::::block_number(); + Self::ensure_alarm_at(&mut status, index, now + One::one()); + ReferendumInfoFor::::insert(index, ReferendumInfo::Ongoing(status)); + result + }, + Some(ReferendumInfo::Approved(end, ..)) => f(PollStatus::Completed(end, true)), + Some(ReferendumInfo::Rejected(end, ..)) => f(PollStatus::Completed(end, false)), + _ => f(PollStatus::None), + } + } + + fn try_access_poll( + index: Self::Index, + f: impl FnOnce( + PollStatus<&mut T::Tally, T::BlockNumber, TrackIdOf>, + ) -> Result, + ) -> Result { + match ReferendumInfoFor::::get(index) { + Some(ReferendumInfo::Ongoing(mut status)) => { + let result = f(PollStatus::Ongoing(&mut status.tally, status.track))?; + let now = frame_system::Pallet::::block_number(); + Self::ensure_alarm_at(&mut status, index, now + One::one()); + ReferendumInfoFor::::insert(index, ReferendumInfo::Ongoing(status)); + Ok(result) + }, + Some(ReferendumInfo::Approved(end, ..)) => f(PollStatus::Completed(end, true)), + Some(ReferendumInfo::Rejected(end, ..)) => f(PollStatus::Completed(end, false)), + _ => f(PollStatus::None), + } + } + + fn as_ongoing(index: Self::Index) -> Option<(T::Tally, TrackIdOf)> { + Self::ensure_ongoing(index).ok().map(|x| (x.tally, x.track)) + } + + #[cfg(feature = "runtime-benchmarks")] + fn create_ongoing(class: Self::Class) -> Result { + let index = ReferendumCount::::mutate(|x| { + let r = *x; + *x += 1; + r + }); + let now = frame_system::Pallet::::block_number(); + let dummy_account_id = + codec::Decode::decode(&mut sp_runtime::traits::TrailingZeroInput::new(&b"dummy"[..])) + .expect("infinite length input; no invalid inputs for type; qed"); + let mut status = ReferendumStatusOf:: { + track: class, + origin: frame_support::dispatch::RawOrigin::Root.into(), + proposal_hash: ::hash_of(&index), + enactment: AtOrAfter::After(Zero::zero()), + submitted: now, + submission_deposit: Deposit { who: dummy_account_id, amount: Zero::zero() }, + decision_deposit: None, + deciding: None, + tally: Default::default(), + in_queue: false, + alarm: None, + }; + Self::ensure_alarm_at(&mut status, index, sp_runtime::traits::Bounded::max_value()); + ReferendumInfoFor::::insert(index, ReferendumInfo::Ongoing(status)); + Ok(index) + } + + #[cfg(feature = "runtime-benchmarks")] + fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()> { + let mut status = Self::ensure_ongoing(index).map_err(|_| ())?; + Self::ensure_no_alarm(&mut status); + Self::note_one_fewer_deciding(status.track); + let now = frame_system::Pallet::::block_number(); + let info = if approved { + ReferendumInfo::Approved(now, status.submission_deposit, status.decision_deposit) + } else { + ReferendumInfo::Rejected(now, status.submission_deposit, status.decision_deposit) + }; + ReferendumInfoFor::::insert(index, info); + Ok(()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn max_ongoing() -> (Self::Class, u32) { + let r = T::Tracks::tracks() + .iter() + .max_by_key(|(_, info)| info.max_deciding) + .expect("Always one class"); + (r.0.clone(), r.1.max_deciding) + } +} + +impl Pallet { + /// Check that referendum `index` is in the `Ongoing` state and return the `ReferendumStatus` + /// value, or `Err` otherwise. + pub fn ensure_ongoing(index: ReferendumIndex) -> Result, DispatchError> { + match ReferendumInfoFor::::get(index) { + Some(ReferendumInfo::Ongoing(status)) => Ok(status), + _ => Err(Error::::NotOngoing.into()), + } + } + + // Enqueue a proposal from a referendum which has presumably passed. + fn schedule_enactment( + index: ReferendumIndex, + track: &TrackInfoOf, + desired: AtOrAfter, + origin: PalletsOriginOf, + call_hash: T::Hash, + ) { + let now = frame_system::Pallet::::block_number(); + let earliest_allowed = now.saturating_add(track.min_enactment_period); + let desired = desired.evaluate(now); + let ok = T::Scheduler::schedule_named( + (ASSEMBLY_ID, "enactment", index).encode(), + DispatchTime::At(desired.max(earliest_allowed)), + None, + 63, + origin, + MaybeHashed::Hash(call_hash), + ) + .is_ok(); + debug_assert!(ok, "LOGIC ERROR: bake_referendum/schedule_named failed"); + } + + /// Set an alarm to dispatch `call` at block number `when`. + fn set_alarm( + call: impl Into>, + when: T::BlockNumber, + ) -> Option<(T::BlockNumber, ScheduleAddressOf)> { + let alarm_interval = T::AlarmInterval::get().max(One::one()); + let when = (when + alarm_interval - One::one()) / alarm_interval * alarm_interval; + let maybe_result = T::Scheduler::schedule( + DispatchTime::At(when), + None, + 128u8, + frame_system::RawOrigin::Root.into(), + MaybeHashed::Value(call.into()), + ) + .ok() + .map(|x| (when, x)); + debug_assert!( + maybe_result.is_some(), + "Unable to schedule a new alarm at #{:?} (now: #{:?})?!", + when, + frame_system::Pallet::::block_number() + ); + maybe_result + } + + /// Mutate a referendum's `status` into the correct deciding state. + /// + /// - `now` is the current block number. + /// - `track` is the track info for the referendum. + /// + /// This will properly set up the `confirming` item. + fn begin_deciding( + status: &mut ReferendumStatusOf, + index: ReferendumIndex, + now: T::BlockNumber, + track: &TrackInfoOf, + ) -> (Option, BeginDecidingBranch) { + let is_passing = Self::is_passing( + &status.tally, + Zero::zero(), + track.decision_period, + &track.min_turnout, + &track.min_approval, + ); + status.in_queue = false; + Self::deposit_event(Event::::DecisionStarted { + index, + tally: status.tally.clone(), + proposal_hash: status.proposal_hash.clone(), + track: status.track.clone(), + }); + let confirming = if is_passing { + Self::deposit_event(Event::::ConfirmStarted { index }); + Some(now.saturating_add(track.confirm_period)) + } else { + None + }; + let deciding_status = DecidingStatus { since: now, confirming }; + let alarm = Self::decision_time(&deciding_status, &status.tally, track); + status.deciding = Some(deciding_status); + let branch = + if is_passing { BeginDecidingBranch::Passing } else { BeginDecidingBranch::Failing }; + (Some(alarm), branch) + } + + /// If it returns `Some`, deciding has begun and it needs waking at the given block number. The + /// second item is the flag for whether it is confirming or not. + /// + /// If `None`, then it is queued and should be nudged automatically as the queue gets drained. + fn ready_for_deciding( + now: T::BlockNumber, + track: &TrackInfoOf, + index: ReferendumIndex, + status: &mut ReferendumStatusOf, + ) -> (Option, ServiceBranch) { + let deciding_count = DecidingCount::::get(status.track); + if deciding_count < track.max_deciding { + // Begin deciding. + DecidingCount::::insert(status.track, deciding_count.saturating_add(1)); + let r = Self::begin_deciding(status, index, now, track); + (r.0, r.1.into()) + } else { + // Add to queue. + let item = (index, status.tally.ayes()); + status.in_queue = true; + TrackQueue::::mutate(status.track, |q| q.insert_sorted_by_key(item, |x| x.1)); + (None, ServiceBranch::Queued) + } + } + + /// Grab the index and status for the referendum which is the highest priority of those for the + /// given track which are ready for being decided. + fn next_for_deciding( + track_queue: &mut BoundedVec<(u32, VotesOf), T::MaxQueued>, + ) -> Option<(ReferendumIndex, ReferendumStatusOf)> { + loop { + let (index, _) = track_queue.pop()?; + match Self::ensure_ongoing(index) { + Ok(s) => return Some((index, s)), + Err(_) => {}, // referendum already timedout or was cancelled. + } + } + } + + /// Schedule a call to `one_fewer_deciding` function via the dispatchable + /// `defer_one_fewer_deciding`. We could theoretically call it immediately (and it would be + /// overall more efficient), however the weights become rather less easy to measure. + fn note_one_fewer_deciding(track: TrackIdOf) { + // Set an alarm call for the next block to nudge the track along. + let now = frame_system::Pallet::::block_number(); + let next_block = now + One::one(); + let alarm_interval = T::AlarmInterval::get().max(One::one()); + let when = (next_block + alarm_interval - One::one()) / alarm_interval * alarm_interval; + + let maybe_result = T::Scheduler::schedule( + DispatchTime::At(when), + None, + 128u8, + frame_system::RawOrigin::Root.into(), + MaybeHashed::Value(Call::one_fewer_deciding { track }.into()), + ); + debug_assert!( + maybe_result.is_ok(), + "Unable to schedule a new alarm at #{:?} (now: #{:?})?!", + when, + now + ); + } + + /// Ensure that a `service_referendum` alarm happens for the referendum `index` at `alarm`. + /// + /// This will do nothing if the alarm is already set. + /// + /// Returns `false` if nothing changed. + fn ensure_alarm_at( + status: &mut ReferendumStatusOf, + index: ReferendumIndex, + alarm: T::BlockNumber, + ) -> bool { + if status.alarm.as_ref().map_or(true, |&(when, _)| when != alarm) { + // Either no alarm or one that was different + Self::ensure_no_alarm(status); + status.alarm = Self::set_alarm(Call::nudge_referendum { index }, alarm); + true + } else { + false + } + } + + /// Advance the state of a referendum, which comes down to: + /// - If it's ready to be decided, start deciding; + /// - If it's not ready to be decided and non-deciding timeout has passed, fail; + /// - If it's ongoing and passing, ensure confirming; if at end of confirmation period, pass. + /// - If it's ongoing and not passing, stop confirning; if it has reached end time, fail. + /// + /// Weight will be a bit different depending on what it does, but it's designed so as not to + /// differ dramatically, especially if `MaxQueue` is kept small. In particular _there are no + /// balance operations in here_. + /// + /// In terms of storage, every call to it is expected to access: + /// - The scheduler, either to insert, remove or alter an entry; + /// - `TrackQueue`, which should be a `BoundedVec` with a low limit (8-16). + /// - `DecidingCount`. + /// + /// Both of the two storage items will only have as many items as there are different tracks, + /// perhaps around 10 and should be whitelisted. + /// + /// The heaviest branch is likely to be when a proposal is placed into, or moved within, the + /// `TrackQueue`. Basically this happens when a referendum is in the deciding queue and receives + /// a vote, or when it moves into the deciding queue. + fn service_referendum( + now: T::BlockNumber, + index: ReferendumIndex, + mut status: ReferendumStatusOf, + ) -> (ReferendumInfoOf, bool, ServiceBranch) { + let mut dirty = false; + // Should it begin being decided? + let track = match Self::track(status.track) { + Some(x) => x, + None => return (ReferendumInfo::Ongoing(status), false, ServiceBranch::Fail), + }; + let timeout = status.submitted + T::UndecidingTimeout::get(); + // Default the alarm to the submission timeout. + let mut alarm = timeout; + let branch; + match &mut status.deciding { + None => { + // Are we already queued for deciding? + if status.in_queue { + // Does our position in the queue need updating? + let ayes = status.tally.ayes(); + let mut queue = TrackQueue::::get(status.track); + let maybe_old_pos = queue.iter().position(|(x, _)| *x == index); + let new_pos = queue.binary_search_by_key(&ayes, |x| x.1).unwrap_or_else(|x| x); + branch = if maybe_old_pos.is_none() && new_pos > 0 { + // Just insert. + queue.force_insert_keep_right(new_pos, (index, ayes)); + ServiceBranch::RequeuedInsertion + } else if let Some(old_pos) = maybe_old_pos { + // We were in the queue - slide into the correct position. + queue[old_pos].1 = ayes; + queue.slide(old_pos, new_pos); + ServiceBranch::RequeuedSlide + } else { + ServiceBranch::NotQueued + }; + TrackQueue::::insert(status.track, queue); + } else { + // Are we ready for deciding? + branch = if status.decision_deposit.is_some() { + let prepare_end = status.submitted.saturating_add(track.prepare_period); + if now >= prepare_end { + let (maybe_alarm, branch) = + Self::ready_for_deciding(now, &track, index, &mut status); + if let Some(set_alarm) = maybe_alarm { + alarm = alarm.min(set_alarm); + } + dirty = true; + branch + } else { + alarm = alarm.min(prepare_end); + ServiceBranch::Preparing + } + } else { + ServiceBranch::NoDeposit + } + } + // If we didn't move into being decided, then check the timeout. + if status.deciding.is_none() && now >= timeout { + // Too long without being decided - end it. + Self::ensure_no_alarm(&mut status); + Self::deposit_event(Event::::TimedOut { index, tally: status.tally }); + return ( + ReferendumInfo::TimedOut( + now, + status.submission_deposit, + status.decision_deposit, + ), + true, + ServiceBranch::TimedOut, + ) + } + }, + Some(deciding) => { + let is_passing = Self::is_passing( + &status.tally, + now.saturating_sub(deciding.since), + track.decision_period, + &track.min_turnout, + &track.min_approval, + ); + branch = if is_passing { + match deciding.confirming.clone() { + Some(t) if now >= t => { + // Passed! + Self::ensure_no_alarm(&mut status); + Self::note_one_fewer_deciding(status.track); + let (desired, call_hash) = (status.enactment, status.proposal_hash); + Self::schedule_enactment( + index, + track, + desired, + status.origin, + call_hash, + ); + Self::deposit_event(Event::::Confirmed { + index, + tally: status.tally, + }); + return ( + ReferendumInfo::Approved( + now, + status.submission_deposit, + status.decision_deposit, + ), + true, + ServiceBranch::Approved, + ) + }, + Some(_) => ServiceBranch::ContinueConfirming, + None => { + // Start confirming + dirty = true; + deciding.confirming = Some(now.saturating_add(track.confirm_period)); + Self::deposit_event(Event::::ConfirmStarted { index }); + ServiceBranch::BeginConfirming + }, + } + } else { + if now >= deciding.since.saturating_add(track.decision_period) { + // Failed! + Self::ensure_no_alarm(&mut status); + Self::note_one_fewer_deciding(status.track); + Self::deposit_event(Event::::Rejected { index, tally: status.tally }); + return ( + ReferendumInfo::Rejected( + now, + status.submission_deposit, + status.decision_deposit, + ), + true, + ServiceBranch::Rejected, + ) + } + if deciding.confirming.is_some() { + // Stop confirming + dirty = true; + deciding.confirming = None; + Self::deposit_event(Event::::ConfirmAborted { index }); + ServiceBranch::EndConfirming + } else { + ServiceBranch::ContinueNotConfirming + } + }; + alarm = Self::decision_time(&deciding, &status.tally, track); + }, + } + + let dirty_alarm = Self::ensure_alarm_at(&mut status, index, alarm); + (ReferendumInfo::Ongoing(status), dirty_alarm || dirty, branch) + } + + /// Determine the point at which a referendum will be accepted, move into confirmation with the + /// given `tally` or end with rejection (whichever happens sooner). + fn decision_time( + deciding: &DecidingStatusOf, + tally: &T::Tally, + track: &TrackInfoOf, + ) -> T::BlockNumber { + deciding.confirming.unwrap_or_else(|| { + // Set alarm to the point where the current voting would make it pass. + let approval = tally.approval(); + let turnout = tally.turnout(); + let until_approval = track.min_approval.delay(approval); + let until_turnout = track.min_turnout.delay(turnout); + let offset = until_turnout.max(until_approval); + deciding.since.saturating_add(offset * track.decision_period) + }) + } + + /// Cancel the alarm in `status`, if one exists. + fn ensure_no_alarm(status: &mut ReferendumStatusOf) { + if let Some((_, last_alarm)) = status.alarm.take() { + // Incorrect alarm - cancel it. + let _ = T::Scheduler::cancel(last_alarm); + } + } + + /// Reserve a deposit and return the `Deposit` instance. + fn take_deposit( + who: T::AccountId, + amount: BalanceOf, + ) -> Result>, DispatchError> { + T::Currency::reserve(&who, amount)?; + Ok(Deposit { who, amount }) + } + + /// Return a deposit, if `Some`. + fn refund_deposit(deposit: Option>>) { + if let Some(Deposit { who, amount }) = deposit { + T::Currency::unreserve(&who, amount); + } + } + + /// Slash a deposit, if `Some`. + fn slash_deposit(deposit: Option>>) { + if let Some(Deposit { who, amount }) = deposit { + T::Slash::on_unbalanced(T::Currency::slash_reserved(&who, amount).0); + Self::deposit_event(Event::::DepositSlashed { who, amount }); + } + } + + /// Get the track info value for the track `id`. + fn track(id: TrackIdOf) -> Option<&'static TrackInfoOf> { + let tracks = T::Tracks::tracks(); + let index = tracks.binary_search_by_key(&id, |x| x.0).unwrap_or_else(|x| x); + Some(&tracks[index].1) + } + + /// Determine whether the given `tally` would result in a referendum passing at `elapsed` blocks + /// into a total decision `period`, given the two curves for `turnout_needed` and + /// `approval_needed`. + fn is_passing( + tally: &T::Tally, + elapsed: T::BlockNumber, + period: T::BlockNumber, + turnout_needed: &Curve, + approval_needed: &Curve, + ) -> bool { + let x = Perbill::from_rational(elapsed.min(period), period); + turnout_needed.passing(x, tally.turnout()) && approval_needed.passing(x, tally.approval()) + } +} diff --git a/frame/referenda/src/mock.rs b/frame/referenda/src/mock.rs new file mode 100644 index 0000000000000..063b124f2b71f --- /dev/null +++ b/frame/referenda/src/mock.rs @@ -0,0 +1,460 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! The crate's tests. + +use super::*; +use crate as pallet_referenda; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + assert_ok, ord_parameter_types, parameter_types, + traits::{ + ConstU32, ConstU64, Contains, EqualPrivilegeOnly, OnInitialize, OriginTrait, Polling, + PreimageRecipient, SortedMembers, + }, + weights::Weight, +}; +use frame_system::{EnsureRoot, EnsureSignedBy}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Hash, IdentityLookup}, + DispatchResult, Perbill, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Preimage: pallet_preimage, + Scheduler: pallet_scheduler, + Referenda: pallet_referenda, + } +); + +// Test that a fitlered call can be dispatched. +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &Call) -> bool { + !matches!(call, &Call::Balances(pallet_balances::Call::set_balance { .. })) + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1_000_000); +} +impl frame_system::Config for Test { + type BaseCallFilter = BaseFilter; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} +impl pallet_preimage::Config for Test { + type Event = Event; + type WeightInfo = (); + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type MaxSize = ConstU32<4096>; + type BaseDeposit = (); + type ByteDeposit = (); +} +parameter_types! { + pub MaximumSchedulerWeight: Weight = 2_000_000_000_000; +} +impl pallet_scheduler::Config for Test { + type Event = Event; + type Origin = Origin; + type PalletsOrigin = OriginCaller; + type Call = Call; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureRoot; + type MaxScheduledPerBlock = ConstU32<100>; + type WeightInfo = (); + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type PreimageProvider = Preimage; + type NoPreimagePostponement = ConstU64<10>; +} +parameter_types! { + pub const ExistentialDeposit: u64 = 1; + pub const MaxLocks: u32 = 10; +} +impl pallet_balances::Config for Test { + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type MaxLocks = MaxLocks; + type Balance = u64; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); +} +parameter_types! { + pub static AlarmInterval: u64 = 1; + pub const SubmissionDeposit: u64 = 2; + pub const MaxQueued: u32 = 3; + pub const UndecidingTimeout: u64 = 20; +} +ord_parameter_types! { + pub const One: u64 = 1; + pub const Two: u64 = 2; + pub const Three: u64 = 3; + pub const Four: u64 = 4; + pub const Five: u64 = 5; + pub const Six: u64 = 6; +} +pub struct OneToFive; +impl SortedMembers for OneToFive { + fn sorted_members() -> Vec { + vec![1, 2, 3, 4, 5] + } + #[cfg(feature = "runtime-benchmarks")] + fn add(_m: &u64) {} +} + +pub struct TestTracksInfo; +impl TracksInfo for TestTracksInfo { + type Id = u8; + type Origin = ::PalletsOrigin; + fn tracks() -> &'static [(Self::Id, TrackInfo)] { + static DATA: [(u8, TrackInfo); 2] = [ + ( + 0u8, + TrackInfo { + name: "root", + max_deciding: 1, + decision_deposit: 10, + prepare_period: 4, + decision_period: 4, + confirm_period: 2, + min_enactment_period: 4, + min_approval: Curve::LinearDecreasing { + begin: Perbill::from_percent(100), + delta: Perbill::from_percent(50), + }, + min_turnout: Curve::LinearDecreasing { + begin: Perbill::from_percent(100), + delta: Perbill::from_percent(100), + }, + }, + ), + ( + 1u8, + TrackInfo { + name: "none", + max_deciding: 3, + decision_deposit: 1, + prepare_period: 2, + decision_period: 2, + confirm_period: 1, + min_enactment_period: 2, + min_approval: Curve::LinearDecreasing { + begin: Perbill::from_percent(55), + delta: Perbill::from_percent(5), + }, + min_turnout: Curve::LinearDecreasing { + begin: Perbill::from_percent(10), + delta: Perbill::from_percent(10), + }, + }, + ), + ]; + &DATA[..] + } + fn track_for(id: &Self::Origin) -> Result { + if let Ok(system_origin) = frame_system::RawOrigin::try_from(id.clone()) { + match system_origin { + frame_system::RawOrigin::Root => Ok(0), + frame_system::RawOrigin::None => Ok(1), + _ => Err(()), + } + } else { + Err(()) + } + } +} + +impl Config for Test { + type WeightInfo = (); + type Call = Call; + type Event = Event; + type Scheduler = Scheduler; + type Currency = pallet_balances::Pallet; + type CancelOrigin = EnsureSignedBy; + type KillOrigin = EnsureRoot; + type Slash = (); + type Votes = u32; + type Tally = Tally; + type SubmissionDeposit = SubmissionDeposit; + type MaxQueued = MaxQueued; + type UndecidingTimeout = UndecidingTimeout; + type AlarmInterval = AlarmInterval; + type Tracks = TestTracksInfo; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100), (6, 100)]; + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +/// Execute the function two times, with `true` and with `false`. +#[allow(dead_code)] +pub fn new_test_ext_execute_with_cond(execute: impl FnOnce(bool) -> () + Clone) { + new_test_ext().execute_with(|| (execute.clone())(false)); + new_test_ext().execute_with(|| execute(true)); +} + +#[derive(Encode, Debug, Decode, TypeInfo, Eq, PartialEq, Clone, Default, MaxEncodedLen)] +pub struct Tally { + pub ayes: u32, + pub nays: u32, +} + +impl VoteTally for Tally { + fn ayes(&self) -> u32 { + self.ayes + } + + fn turnout(&self) -> Perbill { + Perbill::from_percent(self.ayes + self.nays) + } + + fn approval(&self) -> Perbill { + Perbill::from_rational(self.ayes, self.ayes + self.nays) + } + + #[cfg(feature = "runtime-benchmarks")] + fn unanimity() -> Self { + Self { ayes: 100, nays: 0 } + } + + #[cfg(feature = "runtime-benchmarks")] + fn from_requirements(turnout: Perbill, approval: Perbill) -> Self { + let turnout = turnout.mul_ceil(100u32); + let ayes = approval.mul_ceil(turnout); + Self { ayes, nays: turnout - ayes } + } +} + +pub fn set_balance_proposal(value: u64) -> Vec { + Call::Balances(pallet_balances::Call::set_balance { who: 42, new_free: value, new_reserved: 0 }) + .encode() +} + +pub fn set_balance_proposal_hash(value: u64) -> H256 { + let c = Call::Balances(pallet_balances::Call::set_balance { + who: 42, + new_free: value, + new_reserved: 0, + }); + >::note_preimage(c.encode().try_into().unwrap()); + BlakeTwo256::hash_of(&c) +} + +#[allow(dead_code)] +pub fn propose_set_balance(who: u64, value: u64, delay: u64) -> DispatchResult { + Referenda::submit( + Origin::signed(who), + frame_system::RawOrigin::Root.into(), + set_balance_proposal_hash(value), + AtOrAfter::After(delay), + ) +} + +pub fn next_block() { + System::set_block_number(System::block_number() + 1); + Scheduler::on_initialize(System::block_number()); +} + +pub fn run_to(n: u64) { + while System::block_number() < n { + next_block(); + } +} + +#[allow(dead_code)] +pub fn begin_referendum() -> ReferendumIndex { + System::set_block_number(0); + assert_ok!(propose_set_balance(1, 2, 1)); + run_to(2); + 0 +} + +#[allow(dead_code)] +pub fn tally(r: ReferendumIndex) -> Tally { + Referenda::ensure_ongoing(r).unwrap().tally +} + +pub fn set_tally(index: ReferendumIndex, ayes: u32, nays: u32) { + >::access_poll(index, |status| { + let tally = status.ensure_ongoing().unwrap().0; + tally.ayes = ayes; + tally.nays = nays; + }); +} + +pub fn waiting_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Ongoing(ReferendumStatus { submitted, deciding: None, .. }) => submitted, + _ => panic!("Not waiting"), + } +} + +pub fn deciding_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Ongoing(ReferendumStatus { + deciding: Some(DecidingStatus { since, .. }), + .. + }) => since, + _ => panic!("Not deciding"), + } +} + +pub fn deciding_and_failing_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Ongoing(ReferendumStatus { + deciding: Some(DecidingStatus { since, confirming: None, .. }), + .. + }) => since, + _ => panic!("Not deciding"), + } +} + +pub fn confirming_until(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Ongoing(ReferendumStatus { + deciding: Some(DecidingStatus { confirming: Some(until), .. }), + .. + }) => until, + _ => panic!("Not confirming"), + } +} + +pub fn approved_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Approved(since, ..) => since, + _ => panic!("Not approved"), + } +} + +pub fn rejected_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Rejected(since, ..) => since, + _ => panic!("Not rejected"), + } +} + +pub fn cancelled_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Cancelled(since, ..) => since, + _ => panic!("Not cancelled"), + } +} + +pub fn killed_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::Killed(since, ..) => since, + _ => panic!("Not killed"), + } +} + +pub fn timed_out_since(i: ReferendumIndex) -> u64 { + match ReferendumInfoFor::::get(i).unwrap() { + ReferendumInfo::TimedOut(since, ..) => since, + _ => panic!("Not timed out"), + } +} + +fn is_deciding(i: ReferendumIndex) -> bool { + matches!( + ReferendumInfoFor::::get(i), + Some(ReferendumInfo::Ongoing(ReferendumStatus { deciding: Some(_), .. })) + ) +} + +#[derive(Clone, Copy)] +pub enum RefState { + Failing, + Passing, + Confirming { immediate: bool }, +} + +impl RefState { + pub fn create(self) -> ReferendumIndex { + assert_ok!(Referenda::submit( + Origin::signed(1), + frame_support::dispatch::RawOrigin::Root.into(), + set_balance_proposal_hash(1), + AtOrAfter::At(10), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + if matches!(self, RefState::Confirming { immediate: true }) { + set_tally(0, 100, 0); + } + let index = ReferendumCount::::get() - 1; + while !is_deciding(index) { + run_to(System::block_number() + 1); + } + if matches!(self, RefState::Confirming { immediate: false }) { + set_tally(0, 100, 0); + run_to(System::block_number() + 1); + } + if matches!(self, RefState::Confirming { .. }) { + assert_eq!(confirming_until(index), System::block_number() + 2); + } + if matches!(self, RefState::Passing) { + set_tally(0, 100, 99); + run_to(System::block_number() + 1); + } + index + } +} diff --git a/frame/referenda/src/tests.rs b/frame/referenda/src/tests.rs new file mode 100644 index 0000000000000..cea071ced12fe --- /dev/null +++ b/frame/referenda/src/tests.rs @@ -0,0 +1,511 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! The crate's tests. + +use super::*; +use crate::mock::{RefState::*, *}; +use assert_matches::assert_matches; +use codec::Decode; +use frame_support::{ + assert_noop, assert_ok, + dispatch::{DispatchError::BadOrigin, RawOrigin}, + traits::Contains, +}; +use pallet_balances::Error as BalancesError; + +// TODO: Scheduler should re-use `None` items in its `Agenda`. + +#[test] +fn params_should_work() { + new_test_ext().execute_with(|| { + assert_eq!(ReferendumCount::::get(), 0); + assert_eq!(Balances::free_balance(42), 0); + assert_eq!(Balances::total_issuance(), 600); + }); +} + +#[test] +fn basic_happy_path_works() { + new_test_ext().execute_with(|| { + // #1: submit + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + set_balance_proposal_hash(1), + AtOrAfter::At(10), + )); + assert_eq!(Balances::reserved_balance(&1), 2); + assert_eq!(ReferendumCount::::get(), 1); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + run_to(4); + assert_eq!(DecidingCount::::get(0), 0); + run_to(5); + // #5: 4 blocks after submit - vote should now be deciding. + assert_eq!(DecidingCount::::get(0), 1); + run_to(6); + // #6: Lots of ayes. Should now be confirming. + set_tally(0, 100, 0); + run_to(7); + assert_eq!(confirming_until(0), 9); + run_to(9); + // #8: Should be confirmed & ended. + assert_eq!(approved_since(0), 9); + assert_ok!(Referenda::refund_decision_deposit(Origin::signed(2), 0)); + run_to(12); + // #9: Should not yet be enacted. + assert_eq!(Balances::free_balance(&42), 0); + run_to(13); + // #10: Proposal should be executed. + assert_eq!(Balances::free_balance(&42), 1); + }); +} + +#[test] +fn insta_confirm_then_kill_works() { + new_test_ext().execute_with(|| { + let r = Confirming { immediate: true }.create(); + run_to(6); + assert_ok!(Referenda::kill(Origin::root(), r)); + assert_eq!(killed_since(r), 6); + }); +} + +#[test] +fn confirm_then_reconfirm_with_elapsed_trigger_works() { + new_test_ext().execute_with(|| { + let r = Confirming { immediate: false }.create(); + assert_eq!(confirming_until(r), 8); + run_to(7); + set_tally(r, 100, 99); + run_to(8); + assert_eq!(deciding_and_failing_since(r), 5); + run_to(11); + assert_eq!(approved_since(r), 11); + }); +} + +#[test] +fn instaconfirm_then_reconfirm_with_elapsed_trigger_works() { + new_test_ext().execute_with(|| { + let r = Confirming { immediate: true }.create(); + run_to(6); + assert_eq!(confirming_until(r), 7); + set_tally(r, 100, 99); + run_to(7); + assert_eq!(deciding_and_failing_since(r), 5); + run_to(11); + assert_eq!(approved_since(r), 11); + }); +} + +#[test] +fn instaconfirm_then_reconfirm_with_voting_trigger_works() { + new_test_ext().execute_with(|| { + let r = Confirming { immediate: true }.create(); + run_to(6); + assert_eq!(confirming_until(r), 7); + set_tally(r, 100, 99); + run_to(7); + assert_eq!(deciding_and_failing_since(r), 5); + run_to(8); + set_tally(r, 100, 0); + run_to(9); + assert_eq!(confirming_until(r), 11); + run_to(11); + assert_eq!(approved_since(r), 11); + }); +} + +#[test] +fn voting_should_extend_for_late_confirmation() { + new_test_ext().execute_with(|| { + let r = Passing.create(); + run_to(10); + assert_eq!(confirming_until(r), 11); + run_to(11); + assert_eq!(approved_since(r), 11); + }); +} + +#[test] +fn should_instafail_during_extension_confirmation() { + new_test_ext().execute_with(|| { + let r = Passing.create(); + run_to(10); + assert_eq!(confirming_until(r), 11); + // Should insta-fail since it's now past the normal voting time. + set_tally(r, 100, 101); + run_to(11); + assert_eq!(rejected_since(r), 11); + }); +} + +#[test] +fn confirming_then_fail_works() { + new_test_ext().execute_with(|| { + let r = Failing.create(); + // Normally ends at 5 + 4 (voting period) = 9. + assert_eq!(deciding_and_failing_since(r), 5); + set_tally(r, 100, 0); + run_to(6); + assert_eq!(confirming_until(r), 8); + set_tally(r, 100, 101); + run_to(9); + assert_eq!(rejected_since(r), 9); + }); +} + +#[test] +fn queueing_works() { + new_test_ext().execute_with(|| { + // Submit a proposal into a track with a queue len of 1. + assert_ok!(Referenda::submit( + Origin::signed(5), + RawOrigin::Root.into(), + set_balance_proposal_hash(0), + AtOrAfter::After(0), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(5), 0)); + + run_to(2); + + // Submit 3 more proposals into the same queue. + for i in 1..=4 { + assert_ok!(Referenda::submit( + Origin::signed(i), + RawOrigin::Root.into(), + set_balance_proposal_hash(i), + AtOrAfter::After(0), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(i), i as u32)); + // TODO: decision deposit after some initial votes with a non-highest voted coming + // first. + } + assert_eq!(ReferendumCount::::get(), 5); + + run_to(5); + // One should be being decided. + assert_eq!(DecidingCount::::get(0), 1); + assert_eq!(deciding_and_failing_since(0), 5); + for i in 1..=4 { + assert_eq!(waiting_since(i), 2); + } + + // Vote to set order. + set_tally(1, 1, 10); + set_tally(2, 2, 20); + set_tally(3, 3, 30); + set_tally(4, 100, 0); + println!("Agenda #6: {:?}", pallet_scheduler::Agenda::::get(6)); + run_to(6); + println!("{:?}", Vec::<_>::from(TrackQueue::::get(0))); + + // Cancel the first. + assert_ok!(Referenda::cancel(Origin::signed(4), 0)); + assert_eq!(cancelled_since(0), 6); + + // The other with the most approvals (#4) should be being decided. + run_to(7); + assert_eq!(DecidingCount::::get(0), 1); + assert_eq!(deciding_since(4), 7); + assert_eq!(confirming_until(4), 9); + + // Vote on the remaining two to change order. + println!("Set tally #1"); + set_tally(1, 30, 31); + println!("{:?}", Vec::<_>::from(TrackQueue::::get(0))); + println!("Set tally #2"); + set_tally(2, 20, 20); + println!("{:?}", Vec::<_>::from(TrackQueue::::get(0))); + + // Let confirmation period end. + run_to(9); + + // #4 should have been confirmed. + assert_eq!(approved_since(4), 9); + + // On to the next block to select the new referendum + run_to(10); + // #1 (the one with the most approvals) should now be being decided. + assert_eq!(deciding_since(1), 10); + + // Let it end unsuccessfully. + run_to(14); + assert_eq!(rejected_since(1), 14); + + // Service queue. + run_to(15); + // #2 should now be being decided. It will (barely) pass. + assert_eq!(deciding_and_failing_since(2), 15); + + // #2 moves into confirming at the last moment with a 50% approval. + run_to(19); + assert_eq!(confirming_until(2), 21); + + // #2 gets approved. + run_to(21); + assert_eq!(approved_since(2), 21); + + // The final one has since timed out. + run_to(22); + assert_eq!(timed_out_since(3), 22); + }); +} + +#[test] +fn auto_timeout_should_happen_with_nothing_but_submit() { + new_test_ext().execute_with(|| { + // #1: submit + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + set_balance_proposal_hash(1), + AtOrAfter::At(20), + )); + run_to(20); + assert_matches!(ReferendumInfoFor::::get(0), Some(ReferendumInfo::Ongoing(..))); + run_to(21); + // #11: Timed out - ended. + assert_matches!( + ReferendumInfoFor::::get(0), + Some(ReferendumInfo::TimedOut(21, _, None)) + ); + }); +} + +#[test] +fn tracks_are_distinguished() { + new_test_ext().execute_with(|| { + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + set_balance_proposal_hash(1), + AtOrAfter::At(10), + )); + assert_ok!(Referenda::submit( + Origin::signed(2), + RawOrigin::None.into(), + set_balance_proposal_hash(2), + AtOrAfter::At(20), + )); + + assert_ok!(Referenda::place_decision_deposit(Origin::signed(3), 0)); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(4), 1)); + + let mut i = ReferendumInfoFor::::iter().collect::>(); + i.sort_by_key(|x| x.0); + assert_eq!( + i, + vec![ + ( + 0, + ReferendumInfo::Ongoing(ReferendumStatus { + track: 0, + origin: OriginCaller::system(RawOrigin::Root), + proposal_hash: set_balance_proposal_hash(1), + enactment: AtOrAfter::At(10), + submitted: 1, + submission_deposit: Deposit { who: 1, amount: 2 }, + decision_deposit: Some(Deposit { who: 3, amount: 10 }), + deciding: None, + tally: Tally { ayes: 0, nays: 0 }, + in_queue: false, + alarm: Some((5, (5, 0))), + }) + ), + ( + 1, + ReferendumInfo::Ongoing(ReferendumStatus { + track: 1, + origin: OriginCaller::system(RawOrigin::None), + proposal_hash: set_balance_proposal_hash(2), + enactment: AtOrAfter::At(20), + submitted: 1, + submission_deposit: Deposit { who: 2, amount: 2 }, + decision_deposit: Some(Deposit { who: 4, amount: 1 }), + deciding: None, + tally: Tally { ayes: 0, nays: 0 }, + in_queue: false, + alarm: Some((3, (3, 0))), + }) + ), + ] + ); + }); +} + +#[test] +fn submit_errors_work() { + new_test_ext().execute_with(|| { + let h = set_balance_proposal_hash(1); + // No track for Signed origins. + assert_noop!( + Referenda::submit(Origin::signed(1), RawOrigin::Signed(2).into(), h, AtOrAfter::At(10),), + Error::::NoTrack + ); + + // No funds for deposit + assert_noop!( + Referenda::submit(Origin::signed(10), RawOrigin::Root.into(), h, AtOrAfter::At(10),), + BalancesError::::InsufficientBalance + ); + }); +} + +#[test] +fn decision_deposit_errors_work() { + new_test_ext().execute_with(|| { + let e = Error::::NotOngoing; + assert_noop!(Referenda::place_decision_deposit(Origin::signed(2), 0), e); + + let h = set_balance_proposal_hash(1); + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + h, + AtOrAfter::At(10), + )); + let e = BalancesError::::InsufficientBalance; + assert_noop!(Referenda::place_decision_deposit(Origin::signed(10), 0), e); + + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + let e = Error::::HaveDeposit; + assert_noop!(Referenda::place_decision_deposit(Origin::signed(2), 0), e); + }); +} + +#[test] +fn refund_deposit_works() { + new_test_ext().execute_with(|| { + let e = Error::::BadReferendum; + assert_noop!(Referenda::refund_decision_deposit(Origin::signed(1), 0), e); + + let h = set_balance_proposal_hash(1); + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + h, + AtOrAfter::At(10), + )); + let e = Error::::NoDeposit; + assert_noop!(Referenda::refund_decision_deposit(Origin::signed(2), 0), e); + + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + let e = Error::::Unfinished; + assert_noop!(Referenda::refund_decision_deposit(Origin::signed(3), 0), e); + + run_to(11); + assert_ok!(Referenda::refund_decision_deposit(Origin::signed(3), 0)); + }); +} + +#[test] +fn cancel_works() { + new_test_ext().execute_with(|| { + let h = set_balance_proposal_hash(1); + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + h, + AtOrAfter::At(10), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + + run_to(8); + assert_ok!(Referenda::cancel(Origin::signed(4), 0)); + assert_ok!(Referenda::refund_decision_deposit(Origin::signed(3), 0)); + assert_eq!(cancelled_since(0), 8); + }); +} + +#[test] +fn cancel_errors_works() { + new_test_ext().execute_with(|| { + let h = set_balance_proposal_hash(1); + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + h, + AtOrAfter::At(10), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + assert_noop!(Referenda::cancel(Origin::signed(1), 0), BadOrigin); + + run_to(11); + assert_noop!(Referenda::cancel(Origin::signed(4), 0), Error::::NotOngoing); + }); +} + +#[test] +fn kill_works() { + new_test_ext().execute_with(|| { + let h = set_balance_proposal_hash(1); + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + h, + AtOrAfter::At(10), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + + run_to(8); + assert_ok!(Referenda::kill(Origin::root(), 0)); + let e = Error::::NoDeposit; + assert_noop!(Referenda::refund_decision_deposit(Origin::signed(3), 0), e); + assert_eq!(killed_since(0), 8); + }); +} + +#[test] +fn kill_errors_works() { + new_test_ext().execute_with(|| { + let h = set_balance_proposal_hash(1); + assert_ok!(Referenda::submit( + Origin::signed(1), + RawOrigin::Root.into(), + h, + AtOrAfter::At(10), + )); + assert_ok!(Referenda::place_decision_deposit(Origin::signed(2), 0)); + assert_noop!(Referenda::kill(Origin::signed(4), 0), BadOrigin); + + run_to(11); + assert_noop!(Referenda::kill(Origin::root(), 0), Error::::NotOngoing); + }); +} + +#[test] +fn set_balance_proposal_is_correctly_filtered_out() { + for i in 0..10 { + let call = crate::mock::Call::decode(&mut &set_balance_proposal(i)[..]).unwrap(); + assert!(!::BaseCallFilter::contains(&call)); + } +} + +#[test] +fn curve_handles_all_inputs() { + let test_curve = Curve::LinearDecreasing { begin: Perbill::zero(), delta: Perbill::zero() }; + + let delay = test_curve.delay(Perbill::zero()); + assert_eq!(delay, Perbill::zero()); + + let test_curve = Curve::LinearDecreasing { begin: Perbill::zero(), delta: Perbill::one() }; + + let threshold = test_curve.threshold(Perbill::one()); + assert_eq!(threshold, Perbill::zero()); +} diff --git a/frame/referenda/src/types.rs b/frame/referenda/src/types.rs new file mode 100644 index 0000000000000..1b028fdfe9f79 --- /dev/null +++ b/frame/referenda/src/types.rs @@ -0,0 +1,358 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 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. + +//! Miscellaneous additional datatypes. + +use super::*; +use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; +use frame_support::{traits::schedule::Anon, Parameter}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; +use sp_std::fmt::Debug; + +pub type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; +pub type NegativeImbalanceOf = <::Currency as Currency< + ::AccountId, +>>::NegativeImbalance; +pub type CallOf = ::Call; +pub type VotesOf = ::Votes; +pub type TallyOf = ::Tally; +pub type PalletsOriginOf = <::Origin as OriginTrait>::PalletsOrigin; +pub type ReferendumInfoOf = ReferendumInfo< + TrackIdOf, + PalletsOriginOf, + ::BlockNumber, + ::Hash, + BalanceOf, + TallyOf, + ::AccountId, + ScheduleAddressOf, +>; +pub type ReferendumStatusOf = ReferendumStatus< + TrackIdOf, + PalletsOriginOf, + ::BlockNumber, + ::Hash, + BalanceOf, + TallyOf, + ::AccountId, + ScheduleAddressOf, +>; +pub type DecidingStatusOf = DecidingStatus<::BlockNumber>; +pub type TrackInfoOf = TrackInfo, ::BlockNumber>; +pub type TrackIdOf = <::Tracks as TracksInfo< + BalanceOf, + ::BlockNumber, +>>::Id; +pub type ScheduleAddressOf = <::Scheduler as Anon< + ::BlockNumber, + CallOf, + PalletsOriginOf, +>>::Address; + +/// A referendum index. +pub type ReferendumIndex = u32; + +pub trait InsertSorted { + /// Inserts an item into a sorted series. + /// + /// Returns `true` if it was inserted, `false` if it would belong beyond the bound of the + /// series. + fn insert_sorted_by_key K, K: PartialOrd + Ord>( + &mut self, + t: T, + f: F, + ) -> bool; +} +impl> InsertSorted for BoundedVec { + fn insert_sorted_by_key K, K: PartialOrd + Ord>( + &mut self, + t: T, + mut f: F, + ) -> bool { + let index = self.binary_search_by_key::(&f(&t), f).unwrap_or_else(|x| x); + self.force_insert_keep_right(index, t) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::traits::ConstU32; + + #[test] + fn insert_sorted_works() { + let mut b: BoundedVec> = vec![20, 30, 40].try_into().unwrap(); + assert!(b.insert_sorted_by_key(10, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40][..]); + + assert!(b.insert_sorted_by_key(60, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40, 60][..]); + + assert!(b.insert_sorted_by_key(50, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]); + + assert!(!b.insert_sorted_by_key(9, |&x| x)); + assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]); + + assert!(b.insert_sorted_by_key(11, |&x| x)); + assert_eq!(&b[..], &[11, 20, 30, 40, 50, 60][..]); + + assert!(b.insert_sorted_by_key(21, |&x| x)); + assert_eq!(&b[..], &[20, 21, 30, 40, 50, 60][..]); + + assert!(b.insert_sorted_by_key(61, |&x| x)); + assert_eq!(&b[..], &[21, 30, 40, 50, 60, 61][..]); + + assert!(b.insert_sorted_by_key(51, |&x| x)); + assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]); + } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct DecidingStatus { + /// When this referendum began being "decided". If confirming, then the + /// end will actually be delayed until the end of the confirmation period. + pub(crate) since: BlockNumber, + /// If `Some`, then the referendum has entered confirmation stage and will end at + /// the block number as long as it doesn't lose its approval in the meantime. + pub(crate) confirming: Option, +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct Deposit { + pub(crate) who: AccountId, + pub(crate) amount: Balance, +} + +#[derive(Clone, Encode, TypeInfo)] +pub struct TrackInfo { + /// Name of this track. TODO was &'static str + pub name: &'static str, + /// A limit for the number of referenda on this track that can be being decided at once. + /// For Root origin this should generally be just one. + pub max_deciding: u32, + /// Amount that must be placed on deposit before a decision can be made. + pub decision_deposit: Balance, + /// Amount of time this must be submitted for before a decision can be made. + pub prepare_period: Moment, + /// Amount of time that a decision may take to be approved prior to cancellation. + pub decision_period: Moment, + /// Amount of time that the approval criteria must hold before it can be approved. + pub confirm_period: Moment, + /// Minimum amount of time that an approved proposal must be in the dispatch queue. + pub min_enactment_period: Moment, + /// Minimum aye votes as percentage of overall conviction-weighted votes needed for + /// approval as a function of time into decision period. + pub min_approval: Curve, + /// Minimum turnout as percentage of overall population that is needed for + /// approval as a function of time into decision period. + pub min_turnout: Curve, +} + +/// Information on the voting tracks. +pub trait TracksInfo { + /// The identifier for a track. + type Id: Copy + Parameter + Ord + PartialOrd + Send + Sync + 'static; + + /// The origin type from which a track is implied. + type Origin; + + /// Return the array of known tracks and their information. + fn tracks() -> &'static [(Self::Id, TrackInfo)]; + + /// Determine the voting track for the given `origin`. + fn track_for(origin: &Self::Origin) -> Result; + + /// Return the track info for track `id`, by default this just looks it up in `Self::tracks()`. + fn info(id: Self::Id) -> Option<&'static TrackInfo> { + Self::tracks().iter().find(|x| &x.0 == &id).map(|x| &x.1) + } +} + +/// Indication of either a specific moment or a delay from a implicitly defined moment. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum AtOrAfter { + /// Indiciates that the event should occur at the moment given. + At(Moment), + /// Indiciates that the event should occur some period of time (defined by the parameter) after + /// a prior event. The prior event is defined by the context, but for the purposes of + /// referendum proposals, the "prior event" is the passing of the referendum. + After(Moment), +} + +impl AtOrAfter { + pub fn evaluate(&self, since: Moment) -> Moment { + match &self { + Self::At(m) => *m, + Self::After(m) => m.saturating_add(since), + } + } +} + +/// Info regarding an ongoing referendum. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct ReferendumStatus< + TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Origin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Moment: Parameter + Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike, + Hash: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, +> { + /// The track of this referendum. + pub(crate) track: TrackId, + /// The origin for this referendum. + pub(crate) origin: Origin, + /// The hash of the proposal up for referendum. + pub(crate) proposal_hash: Hash, + /// The time the proposal should be scheduled for enactment. + pub(crate) enactment: AtOrAfter, + /// The time of submission. Once `UndecidingTimeout` passes, it may be closed by anyone if it + /// `deciding` is `None`. + pub(crate) submitted: Moment, + /// The deposit reserved for the submission of this referendum. + pub(crate) submission_deposit: Deposit, + /// The deposit reserved for this referendum to be decided. + pub(crate) decision_deposit: Option>, + /// The status of a decision being made. If `None`, it has not entered the deciding period. + pub(crate) deciding: Option>, + /// The current tally of votes in this referendum. + pub(crate) tally: Tally, + /// Whether we have been placed in the queue for being decided or not. + pub(crate) in_queue: bool, + /// The next scheduled wake-up, if `Some`. + pub(crate) alarm: Option<(Moment, ScheduleAddress)>, +} + +/// Info regarding a referendum, present or past. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum ReferendumInfo< + TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Origin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Moment: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike, + Hash: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, +> { + /// Referendum has been submitted and is being voted on. + Ongoing( + ReferendumStatus, + ), + /// Referendum finished with approval. Submission deposit is held. + Approved(Moment, Deposit, Option>), + /// Referendum finished with rejection. Submission deposit is held. + Rejected(Moment, Deposit, Option>), + /// Referendum finished with cancelation. Submission deposit is held. + Cancelled(Moment, Deposit, Option>), + /// Referendum finished and was never decided. Submission deposit is held. + TimedOut(Moment, Deposit, Option>), + /// Referendum finished with a kill. + Killed(Moment), +} + +impl< + TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Origin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Moment: Parameter + Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike, + Hash: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone, + > ReferendumInfo +{ + /// Take the Decision Deposit from `self`, if there is one. Returns an `Err` if `self` is not + /// in a valid state for the Decision Deposit to be refunded. + pub fn take_decision_deposit(&mut self) -> Result>, ()> { + use ReferendumInfo::*; + match self { + Ongoing(x) if x.decision_deposit.is_none() => Ok(None), + // Cannot refund deposit if Ongoing as this breaks assumptions. + Ongoing(_) => Err(()), + Approved(_, _, d) | Rejected(_, _, d) | TimedOut(_, _, d) | Cancelled(_, _, d) => + Ok(d.take()), + Killed(_) => Ok(None), + } + } +} + +/// Type for describing a curve over the 2-dimensional space of axes between 0-1, as represented +/// by `(Perbill, Perbill)`. +#[derive(Clone, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[cfg_attr(not(feature = "std"), derive(RuntimeDebug))] +pub enum Curve { + /// Linear curve starting at `(0, begin)`, ending at `(period, begin - delta)`. + LinearDecreasing { begin: Perbill, delta: Perbill }, +} + +impl Curve { + /// Determine the `y` value for the given `x` value. + pub(crate) fn threshold(&self, x: Perbill) -> Perbill { + match self { + Self::LinearDecreasing { begin, delta } => *begin - (*delta * x).min(*begin), + } + } + + /// Determine the smallest `x` value such that `passing` returns `true` when passed along with + /// the given `y` value. + /// + /// ```nocompile + /// let c = Curve::LinearDecreasing { begin: Perbill::one(), delta: Perbill::one() }; + /// // ^^^ Can be any curve. + /// let y = Perbill::from_percent(50); + /// // ^^^ Can be any value. + /// let x = c.delay(y); + /// assert!(c.passing(x, y)); + /// ``` + pub fn delay(&self, y: Perbill) -> Perbill { + match self { + Self::LinearDecreasing { begin, delta } => + if delta.is_zero() { + return *delta + } else { + return (*begin - y.min(*begin)).min(*delta) / *delta + }, + } + } + + /// Return `true` iff the `y` value is greater than the curve at the `x`. + pub fn passing(&self, x: Perbill, y: Perbill) -> bool { + y >= self.threshold(x) + } +} + +#[cfg(feature = "std")] +impl Debug for Curve { + fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result { + match self { + Self::LinearDecreasing { begin, delta } => { + write!( + f, + "Linear[(0%, {}%) -> (100%, {}%)]", + *begin * 100u32, + (*begin - *delta) * 100u32, + ) + }, + } + } +} diff --git a/frame/referenda/src/weights.rs b/frame/referenda/src/weights.rs new file mode 100644 index 0000000000000..202901bdd10bd --- /dev/null +++ b/frame/referenda/src/weights.rs @@ -0,0 +1,491 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 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_referenda +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-12-19, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_referenda +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/referenda/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_referenda. +pub trait WeightInfo { + fn submit() -> Weight; + fn place_decision_deposit_preparing() -> Weight; + fn place_decision_deposit_queued() -> Weight; + fn place_decision_deposit_not_queued() -> Weight; + fn place_decision_deposit_passing() -> Weight; + fn place_decision_deposit_failing() -> Weight; + fn refund_decision_deposit() -> Weight; + fn cancel() -> Weight; + fn kill() -> Weight; + fn one_fewer_deciding_queue_empty() -> Weight; + fn one_fewer_deciding_failing() -> Weight; + fn one_fewer_deciding_passing() -> Weight; + fn nudge_referendum_requeued_insertion() -> Weight; + fn nudge_referendum_requeued_slide() -> Weight; + fn nudge_referendum_queued() -> Weight; + fn nudge_referendum_not_queued() -> Weight; + fn nudge_referendum_no_deposit() -> Weight; + fn nudge_referendum_preparing() -> Weight; + fn nudge_referendum_timed_out() -> Weight; + fn nudge_referendum_begin_deciding_failing() -> Weight; + fn nudge_referendum_begin_deciding_passing() -> Weight; + fn nudge_referendum_begin_confirming() -> Weight; + fn nudge_referendum_end_confirming() -> Weight; + fn nudge_referendum_continue_not_confirming() -> Weight; + fn nudge_referendum_continue_confirming() -> Weight; + fn nudge_referendum_approved() -> Weight; + fn nudge_referendum_rejected() -> Weight; +} + +/// Weights for pallet_referenda using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Referenda ReferendumCount (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:0 w:1) + fn submit() -> Weight { + (42_395_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn place_decision_deposit_preparing() -> Weight { + (48_113_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + fn place_decision_deposit_queued() -> Weight { + (53_624_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + fn place_decision_deposit_not_queued() -> Weight { + (52_560_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn place_decision_deposit_passing() -> Weight { + (54_067_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn place_decision_deposit_failing() -> Weight { + (52_457_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + fn refund_decision_deposit() -> Weight { + (28_504_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn cancel() -> Weight { + (40_425_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn kill() -> Weight { + (65_974_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda TrackQueue (r:1 w:0) + // Storage: Referenda DecidingCount (r:1 w:1) + fn one_fewer_deciding_queue_empty() -> Weight { + (8_904_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn one_fewer_deciding_failing() -> Weight { + (181_387_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn one_fewer_deciding_passing() -> Weight { + (179_753_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_requeued_insertion() -> Weight { + (53_592_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_requeued_slide() -> Weight { + (53_173_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_queued() -> Weight { + (55_770_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_not_queued() -> Weight { + (53_922_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_no_deposit() -> Weight { + (26_906_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_preparing() -> Weight { + (27_943_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + fn nudge_referendum_timed_out() -> Weight { + (20_256_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_begin_deciding_failing() -> Weight { + (30_964_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_begin_deciding_passing() -> Weight { + (31_763_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_begin_confirming() -> Weight { + (28_892_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_end_confirming() -> Weight { + (29_666_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_continue_not_confirming() -> Weight { + (29_740_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_continue_confirming() -> Weight { + (29_661_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + fn nudge_referendum_approved() -> Weight { + (55_736_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_rejected() -> Weight { + (32_726_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Referenda ReferendumCount (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:0 w:1) + fn submit() -> Weight { + (42_395_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn place_decision_deposit_preparing() -> Weight { + (48_113_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + fn place_decision_deposit_queued() -> Weight { + (53_624_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + fn place_decision_deposit_not_queued() -> Weight { + (52_560_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn place_decision_deposit_passing() -> Weight { + (54_067_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn place_decision_deposit_failing() -> Weight { + (52_457_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + fn refund_decision_deposit() -> Weight { + (28_504_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn cancel() -> Weight { + (40_425_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn kill() -> Weight { + (65_974_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda TrackQueue (r:1 w:0) + // Storage: Referenda DecidingCount (r:1 w:1) + fn one_fewer_deciding_queue_empty() -> Weight { + (8_904_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn one_fewer_deciding_failing() -> Weight { + (181_387_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + fn one_fewer_deciding_passing() -> Weight { + (179_753_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_requeued_insertion() -> Weight { + (53_592_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_requeued_slide() -> Weight { + (53_173_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_queued() -> Weight { + (55_770_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:0) + // Storage: Referenda TrackQueue (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_not_queued() -> Weight { + (53_922_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_no_deposit() -> Weight { + (26_906_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_preparing() -> Weight { + (27_943_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + fn nudge_referendum_timed_out() -> Weight { + (20_256_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_begin_deciding_failing() -> Weight { + (30_964_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Referenda DecidingCount (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_begin_deciding_passing() -> Weight { + (31_763_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_begin_confirming() -> Weight { + (28_892_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_end_confirming() -> Weight { + (29_666_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_continue_not_confirming() -> Weight { + (29_740_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_continue_confirming() -> Weight { + (29_661_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:2 w:2) + // Storage: Scheduler Lookup (r:1 w:1) + // Storage: Preimage StatusFor (r:1 w:1) + fn nudge_referendum_approved() -> Weight { + (55_736_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + } + // Storage: Referenda ReferendumInfoFor (r:1 w:1) + // Storage: Scheduler Agenda (r:1 w:1) + fn nudge_referendum_rejected() -> Weight { + (32_726_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } +} diff --git a/frame/scheduler/src/lib.rs b/frame/scheduler/src/lib.rs index f9077d4c8c8fa..ec60cedc280b6 100644 --- a/frame/scheduler/src/lib.rs +++ b/frame/scheduler/src/lib.rs @@ -694,13 +694,6 @@ impl Pallet { }); Agenda::::append(when, s); let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; - if index > T::MaxScheduledPerBlock::get() { - log::warn!( - target: "runtime::scheduler", - "Warning: There are more items queued in the Scheduler than \ - expected from the runtime configuration. An update might be needed.", - ); - } Self::deposit_event(Event::Scheduled { when, index }); Ok((when, index)) @@ -795,13 +788,6 @@ impl Pallet { }; Agenda::::append(when, Some(s)); let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; - if index > T::MaxScheduledPerBlock::get() { - log::warn!( - target: "runtime::scheduler", - "Warning: There are more items queued in the Scheduler than \ - expected from the runtime configuration. An update might be needed.", - ); - } let address = (when, index); Lookup::::insert(&id, &address); Self::deposit_event(Event::Scheduled { when, index }); diff --git a/frame/support/src/dispatch.rs b/frame/support/src/dispatch.rs index b5f4ab97767c2..ece5173f2f4ca 100644 --- a/frame/support/src/dispatch.rs +++ b/frame/support/src/dispatch.rs @@ -20,6 +20,7 @@ pub use crate::{ codec::{Codec, Decode, Encode, EncodeAsRef, EncodeLike, HasCompact, Input, Output}, + scale_info::TypeInfo, sp_std::{ fmt, marker, prelude::{Clone, Eq, PartialEq, Vec}, @@ -33,7 +34,7 @@ pub use crate::{ TransactionPriority, WeighData, Weight, WithPostDispatchInfo, }, }; -pub use sp_runtime::{traits::Dispatchable, DispatchError}; +pub use sp_runtime::{traits::Dispatchable, DispatchError, RuntimeDebug}; /// The return type of a `Dispatchable` in frame. When returned explicitly from /// a dispatchable function it allows overriding the default `PostDispatchInfo` @@ -60,6 +61,28 @@ pub trait Callable { // https://github.com/rust-lang/rust/issues/51331 pub type CallableCallFor = >::Call; +/// Origin for the System pallet. +#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo)] +pub enum RawOrigin { + /// The system itself ordained this dispatch to happen: this is the highest privilege level. + Root, + /// It is signed by some public key and we provide the `AccountId`. + Signed(AccountId), + /// It is signed by nobody, can be either: + /// * included and agreed upon by the validators anyway, + /// * or unsigned transaction validated by a pallet. + None, +} + +impl From> for RawOrigin { + fn from(s: Option) -> RawOrigin { + match s { + Some(who) => RawOrigin::Signed(who), + None => RawOrigin::None, + } + } +} + /// A type that can be used as a parameter in a dispatchable function. /// /// When using `decl_module` all arguments for call functions must implement this trait. @@ -2582,21 +2605,7 @@ mod tests { type DbWeight: Get; } - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, scale_info::TypeInfo)] - pub enum RawOrigin { - Root, - Signed(AccountId), - None, - } - - impl From> for RawOrigin { - fn from(s: Option) -> RawOrigin { - match s { - Some(who) => RawOrigin::Signed(who), - None => RawOrigin::None, - } - } - } + pub use super::super::RawOrigin; pub type Origin = RawOrigin<::AccountId>; } @@ -2638,7 +2647,7 @@ mod tests { } } - #[derive(scale_info::TypeInfo)] + #[derive(Eq, PartialEq, Clone, crate::RuntimeDebug, scale_info::TypeInfo)] pub struct TraitImpl {} impl Config for TraitImpl {} @@ -2679,8 +2688,15 @@ mod tests { } } + #[derive(TypeInfo, crate::RuntimeDebug, Eq, PartialEq, Clone, Encode, Decode)] pub struct OuterOrigin; + impl From::AccountId>> for OuterOrigin { + fn from(_: RawOrigin<::AccountId>) -> Self { + unimplemented!("Not required in tests!") + } + } + impl crate::traits::OriginTrait for OuterOrigin { type Call = ::Call; type PalletsOrigin = OuterOrigin; diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 1928023cfe66d..5ee2952e445b7 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -23,7 +23,7 @@ pub mod tokens; pub use tokens::{ currency::{ Currency, LockIdentifier, LockableCurrency, NamedReservableCurrency, ReservableCurrency, - VestingSchedule, + TotalIssuanceOf, VestingSchedule, }, fungible, fungibles, imbalance::{Imbalance, OnUnbalanced, SignedImbalance}, @@ -90,4 +90,6 @@ mod dispatch; pub use dispatch::{EnsureOneOf, EnsureOrigin, OriginTrait, UnfilteredDispatchable}; mod voting; -pub use voting::{CurrencyToVote, SaturatingCurrencyToVote, U128CurrencyToVote}; +pub use voting::{ + CurrencyToVote, PollStatus, Polling, SaturatingCurrencyToVote, U128CurrencyToVote, VoteTally, +}; diff --git a/frame/support/src/traits/dispatch.rs b/frame/support/src/traits/dispatch.rs index afac31e662e3f..1a4e9f6f7cc2a 100644 --- a/frame/support/src/traits/dispatch.rs +++ b/frame/support/src/traits/dispatch.rs @@ -17,8 +17,11 @@ //! Traits for dealing with dispatching calls and the origin from which they are dispatched. -use crate::dispatch::DispatchResultWithPostInfo; -use sp_runtime::{traits::BadOrigin, Either}; +use crate::dispatch::{DispatchResultWithPostInfo, Parameter, RawOrigin}; +use sp_runtime::{ + traits::{BadOrigin, Member}, + Either, +}; /// Some sort of check on the origin is performed by this object. pub trait EnsureOrigin { @@ -56,7 +59,7 @@ pub trait OriginTrait: Sized { type Call; /// The caller origin, overarching type of all pallets origins. - type PalletsOrigin; + type PalletsOrigin: Parameter + Member + Into + From>; /// The AccountId used across the system. type AccountId; diff --git a/frame/support/src/traits/schedule.rs b/frame/support/src/traits/schedule.rs index 5ba233c29be1f..3b8e6da3e2ef3 100644 --- a/frame/support/src/traits/schedule.rs +++ b/frame/support/src/traits/schedule.rs @@ -125,7 +125,7 @@ pub mod v1 { /// A type that can be used as a scheduler. pub trait Anon { /// An address which can be used for removing a scheduled task. - type Address: Codec + Clone + Eq + EncodeLike + Debug; + type Address: Codec + Clone + Eq + EncodeLike + Debug + TypeInfo; /// Schedule a dispatch to happen at the beginning of some block in the future. /// @@ -280,7 +280,7 @@ pub mod v2 { /// A type that can be used as a scheduler. pub trait Anon { /// An address which can be used for removing a scheduled task. - type Address: Codec + Clone + Eq + EncodeLike + Debug; + type Address: Codec + Clone + Eq + EncodeLike + Debug + TypeInfo; /// A means of expressing a call by the hash of its encoded data. type Hash; diff --git a/frame/support/src/traits/tokens/currency.rs b/frame/support/src/traits/tokens/currency.rs index d5756ee84c47a..d4b5c0c184f85 100644 --- a/frame/support/src/traits/tokens/currency.rs +++ b/frame/support/src/traits/tokens/currency.rs @@ -21,7 +21,10 @@ use super::{ imbalance::{Imbalance, SignedImbalance}, misc::{Balance, ExistenceRequirement, WithdrawReasons}, }; -use crate::dispatch::{DispatchError, DispatchResult}; +use crate::{ + dispatch::{DispatchError, DispatchResult}, + traits::Get, +}; use codec::MaxEncodedLen; use sp_runtime::traits::MaybeSerializeDeserialize; use sp_std::fmt::Debug; @@ -200,6 +203,15 @@ pub trait Currency { ) -> SignedImbalance; } +/// A non-const `Get` implementation parameterised by a `Currency` impl which provides the result +/// of `total_issuance`. +pub struct TotalIssuanceOf, A>(sp_std::marker::PhantomData<(C, A)>); +impl, A> Get for TotalIssuanceOf { + fn get() -> C::Balance { + C::total_issuance() + } +} + #[cfg(feature = "std")] impl Currency for () { type Balance = u32; diff --git a/frame/support/src/traits/voting.rs b/frame/support/src/traits/voting.rs index 719ad7f0f71d0..978c5ce4f6a01 100644 --- a/frame/support/src/traits/voting.rs +++ b/frame/support/src/traits/voting.rs @@ -18,7 +18,14 @@ //! Traits and associated data structures concerned with voting, and moving between tokens and //! votes. -use sp_arithmetic::traits::{SaturatedConversion, UniqueSaturatedFrom, UniqueSaturatedInto}; +use crate::dispatch::{DispatchError, Parameter}; +use codec::HasCompact; +use sp_arithmetic::{ + traits::{SaturatedConversion, UniqueSaturatedFrom, UniqueSaturatedInto}, + Perbill, +}; +use sp_runtime::traits::Member; +use sp_std::prelude::*; /// A trait similar to `Convert` to convert values from `B` an abstract balance type /// into u64 and back from u128. (This conversion is used in election and other places where complex @@ -87,3 +94,74 @@ impl + UniqueSaturatedFrom> CurrencyToVote B::unique_saturated_from(value) } } + +pub trait VoteTally { + fn ayes(&self) -> Votes; + fn turnout(&self) -> Perbill; + fn approval(&self) -> Perbill; + #[cfg(feature = "runtime-benchmarks")] + fn unanimity() -> Self; + #[cfg(feature = "runtime-benchmarks")] + fn from_requirements(turnout: Perbill, approval: Perbill) -> Self; +} + +pub enum PollStatus { + None, + Ongoing(Tally, Class), + Completed(Moment, bool), +} + +impl PollStatus { + pub fn ensure_ongoing(self) -> Option<(Tally, Class)> { + match self { + Self::Ongoing(t, c) => Some((t, c)), + _ => None, + } + } +} + +pub trait Polling { + type Index: Parameter + Member + Ord + PartialOrd + Copy + HasCompact; + type Votes: Parameter + Member + Ord + PartialOrd + Copy + HasCompact; + type Class: Parameter + Member + Ord + PartialOrd; + type Moment; + + /// Provides a vec of values that `T` may take. + fn classes() -> Vec; + + /// `Some` if the referendum `index` can be voted on, along with the tally and class of + /// referendum. + /// + /// Don't use this if you might mutate - use `try_access_poll` instead. + fn as_ongoing(index: Self::Index) -> Option<(Tally, Self::Class)>; + + fn access_poll( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut Tally, Self::Moment, Self::Class>) -> R, + ) -> R; + + fn try_access_poll( + index: Self::Index, + f: impl FnOnce(PollStatus<&mut Tally, Self::Moment, Self::Class>) -> Result, + ) -> Result; + + /// Create an ongoing majority-carries poll of given class lasting given period for the purpose + /// of benchmarking. + /// + /// May return `Err` if it is impossible. + #[cfg(feature = "runtime-benchmarks")] + fn create_ongoing(class: Self::Class) -> Result; + + /// End the given ongoing poll and return the result. + /// + /// Returns `Err` if `index` is not an ongoing poll. + #[cfg(feature = "runtime-benchmarks")] + fn end_ongoing(index: Self::Index, approved: bool) -> Result<(), ()>; + + /// The maximum amount of ongoing polls within any single class. By default it practically + /// unlimited (`u32::max_value()`). + #[cfg(feature = "runtime-benchmarks")] + fn max_ongoing() -> (Self::Class, u32) { + (Self::classes().into_iter().next().expect("Always one class"), u32::max_value()) + } +} diff --git a/frame/support/test/tests/system.rs b/frame/support/test/tests/system.rs index 0083835640cb7..b30fd8d5ec561 100644 --- a/frame/support/test/tests/system.rs +++ b/frame/support/test/tests/system.rs @@ -69,23 +69,7 @@ frame_support::decl_error! { } } -/// Origin for the system module. -#[derive(PartialEq, Eq, Clone, sp_runtime::RuntimeDebug, Encode, Decode, scale_info::TypeInfo)] -pub enum RawOrigin { - Root, - Signed(AccountId), - None, -} - -impl From> for RawOrigin { - fn from(s: Option) -> RawOrigin { - match s { - Some(who) => RawOrigin::Signed(who), - None => RawOrigin::None, - } - } -} - +pub use frame_support::dispatch::RawOrigin; pub type Origin = RawOrigin<::AccountId>; #[allow(dead_code)] diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index 878edd7840e74..3b4de0c472c47 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -124,6 +124,7 @@ pub use extensions::{ }; // Backward compatible re-export. pub use extensions::check_mortality::CheckMortality as CheckEra; +pub use frame_support::dispatch::RawOrigin; pub use weights::WeightInfo; /// Compute the trie root of a list of extrinsics. @@ -708,28 +709,6 @@ pub struct EventRecord { pub topics: Vec, } -/// Origin for the System pallet. -#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo)] -pub enum RawOrigin { - /// The system itself ordained this dispatch to happen: this is the highest privilege level. - Root, - /// It is signed by some public key and we provide the `AccountId`. - Signed(AccountId), - /// It is signed by nobody, can be either: - /// * included and agreed upon by the validators anyway, - /// * or unsigned transaction validated by a pallet. - None, -} - -impl From> for RawOrigin { - fn from(s: Option) -> RawOrigin { - match s { - Some(who) => RawOrigin::Signed(who), - None => RawOrigin::None, - } - } -} - // Create a Hash with 69 for each byte, // only used to build genesis config. #[cfg(feature = "std")] @@ -1641,7 +1620,7 @@ impl Lookup for ChainContext { /// Prelude to be used alongside pallet macro, for ease of use. pub mod pallet_prelude { - pub use crate::{ensure_none, ensure_root, ensure_signed}; + pub use crate::{ensure_none, ensure_root, ensure_signed, ensure_signed_or_root}; /// Type alias for the `Origin` associated type of system config. pub type OriginFor = ::Origin; diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index 5c9dfeca0a334..861d95efb3087 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -426,7 +426,7 @@ impl GetRuntimeBlockType for Runtime { type RuntimeBlock = Block; } -#[derive(Clone, RuntimeDebug)] +#[derive(Clone, RuntimeDebug, Encode, Decode, PartialEq, Eq, TypeInfo)] pub struct Origin; impl From> for Origin {