diff --git a/Cargo.lock b/Cargo.lock index e8a3d9292..6ec06b6bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,14 +293,10 @@ dependencies = [ "cumulus-primitives-core", "cumulus-primitives-timestamp", "cumulus-primitives-utility", - "frame-benchmarking", "frame-executive", "frame-support", "frame-system", - "frame-system-benchmarking", "frame-system-rpc-runtime-api", - "frame-try-runtime", - "hex-literal 0.4.1", "log", "pallet-asset-tx-payment", "pallet-assets", @@ -319,7 +315,6 @@ dependencies = [ "pallet-uniques", "pallet-utility", "pallet-xcm", - "pallet-xcm-benchmarks", "parachain-info", "parachains-common", "parity-scale-codec", @@ -3934,13 +3929,18 @@ dependencies = [ "orml-oracle", "pallet-assets", "pallet-balances", + "pallet-collective", + "pallet-democracy 4.0.0-dev", + "pallet-elections-phragmen 5.0.0-dev", "pallet-funding", "pallet-im-online", "pallet-linear-release", "pallet-membership", "pallet-message-queue", "pallet-parachain-staking", + "pallet-scheduler", "pallet-staking", + "pallet-treasury", "pallet-vesting", "pallet-xcm", "parachain-info", @@ -4254,10 +4254,10 @@ dependencies = [ "pallet-child-bounties", "pallet-collective", "pallet-conviction-voting", - "pallet-democracy", + "pallet-democracy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", - "pallet-elections-phragmen", + "pallet-elections-phragmen 5.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-fast-unstake", "pallet-grandpa", "pallet-identity", @@ -6015,6 +6015,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-democracy" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-preimage", + "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" @@ -6069,6 +6089,27 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-elections-phragmen" +version = "5.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-npos-elections", + "sp-runtime", + "sp-staking", + "sp-std", + "sp-tracing", + "substrate-test-utils", +] + [[package]] name = "pallet-elections-phragmen" version = "5.0.0-dev" @@ -7428,11 +7469,16 @@ dependencies = [ "pallet-aura", "pallet-authorship", "pallet-balances", + "pallet-collective", + "pallet-democracy 4.0.0-dev", + "pallet-elections-phragmen 5.0.0-dev", "pallet-membership", "pallet-multisig", "pallet-oracle-ocw", "pallet-parachain-staking", + "pallet-preimage", "pallet-proxy", + "pallet-scheduler", "pallet-session", "pallet-sudo", "pallet-timestamp", @@ -7586,7 +7632,8 @@ dependencies = [ "pallet-authorship", "pallet-balances", "pallet-collective", - "pallet-democracy", + "pallet-democracy 4.0.0-dev", + "pallet-elections-phragmen 5.0.0-dev", "pallet-funding", "pallet-insecure-randomness-collective-flip", "pallet-linear-release", @@ -8524,10 +8571,10 @@ dependencies = [ "pallet-child-bounties", "pallet-collective", "pallet-conviction-voting", - "pallet-democracy", + "pallet-democracy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", - "pallet-elections-phragmen", + "pallet-elections-phragmen 5.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-fast-unstake", "pallet-grandpa", "pallet-identity", @@ -9661,8 +9708,8 @@ dependencies = [ "pallet-bounties", "pallet-child-bounties", "pallet-collective", - "pallet-democracy", - "pallet-elections-phragmen", + "pallet-democracy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", + "pallet-elections-phragmen 5.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-grandpa", "pallet-identity", "pallet-im-online", @@ -12496,6 +12543,27 @@ dependencies = [ "sp-state-machine", ] +[[package]] +name = "substrate-test-utils" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0#948fbd2fd1233dc26dbb9f9bbc1d2cca2c03945d" +dependencies = [ + "futures", + "substrate-test-utils-derive", + "tokio", +] + +[[package]] +name = "substrate-test-utils-derive" +version = "0.10.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0#948fbd2fd1233dc26dbb9f9bbc1d2cca2c03945d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "substrate-typenum" version = "1.16.0" @@ -13772,10 +13840,10 @@ dependencies = [ "pallet-bags-list", "pallet-balances", "pallet-collective", - "pallet-democracy", + "pallet-democracy 4.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", - "pallet-elections-phragmen", + "pallet-elections-phragmen 5.0.0-dev (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "pallet-fast-unstake", "pallet-grandpa", "pallet-identity", diff --git a/Cargo.toml b/Cargo.toml index 88efa2c81..cc14a3de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "macros/tests", "polimec-common", ] - +default-members = ["nodes/*", "pallets/*"] resolver = "2" [workspace.package] @@ -56,6 +56,8 @@ substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", bran # Internal pallets (with default disabled) pallet-funding = { path = "pallets/funding", default-features = false } +pallet-democracy = { path = "pallets/democracy", default-features = false } +pallet-elections-phragmen = { path = "pallets/elections-phragmen", default-features = false } pallet-oracle-ocw = { path = "pallets/oracle-ocw", default-features = false } pallet-sandbox = { path = "pallets/sandbox", default-features = false } pallet-parachain-staking = { path = "pallets/parachain-staking", default-features = false } @@ -121,6 +123,8 @@ sp-transaction-pool = { git = "https://github.com/paritytech/substrate", default sp-trie = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } sp-version = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } sp-consensus-grandpa = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } +sp-npos-elections = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } +sp-tracing = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } try-runtime-cli = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } pallet-im-online = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } sp-authority-discovery = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } @@ -138,7 +142,6 @@ pallet-session = { git = "https://github.com/paritytech/substrate", default-feat pallet-timestamp = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } pallet-asset-tx-payment = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } pallet-collective = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } -pallet-democracy = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } pallet-scheduler = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } pallet-sudo = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v1.0.0" } @@ -224,10 +227,11 @@ sc-transaction_pool-api = { git = "https://github.com/paritytech/substrate", bra substrate-frame-rpc-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } substrate-prometheus-endpoint = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } substrate-build-script-utils = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } +substrate-test-utils = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v1.0.0" } # Benchmarking (with default disabled) cumulus-pallet-session-benchmarking = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "release-v1.0.0" } # Runtimes -polimec-parachain-runtime = { path = "runtimes/testnet" } +polimec-parachain-runtime = { path = "runtimes/testnet"} polimec-base-runtime = { path = "runtimes/base" } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 7d08c7c12..0f1463a24 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -40,6 +40,8 @@ pallet-assets.workspace = true polkadot-core-primitives.workspace = true polkadot-runtime-parachains.workspace = true polkadot-parachain.workspace = true +pallet-collective.workspace = true +pallet-elections-phragmen.workspace = true cumulus-primitives-core.workspace = true cumulus-pallet-xcm.workspace = true @@ -61,6 +63,9 @@ orml-oracle.workspace = true pallet-parachain-staking.workspace = true pallet-vesting.workspace = true itertools.workspace = true +pallet-democracy.workspace = true +pallet-scheduler.workspace = true +pallet-treasury.workspace = true # Runtimes polkadot-runtime.workspace = true @@ -70,8 +75,11 @@ polimec-base-runtime.workspace = true penpal-runtime = { path = "./penpal", default-features = false } [features] -default = [ "std" ] - +default = [ "instant-mode", "std" ] +instant-mode = [ + "polimec-base-runtime/instant-mode", + "polimec-parachain-runtime/instant-mode", +] std = [ "asset-hub-polkadot-runtime/std", "cumulus-pallet-xcm/std", @@ -83,13 +91,18 @@ std = [ "orml-oracle/std", "pallet-assets/std", "pallet-balances/std", + "pallet-collective/std", + "pallet-democracy/std", + "pallet-elections-phragmen/std", "pallet-funding/std", "pallet-im-online/std", "pallet-linear-release/std", "pallet-membership/std", "pallet-message-queue/std", "pallet-parachain-staking/std", + "pallet-scheduler/std", "pallet-staking/std", + "pallet-treasury/std", "pallet-vesting/std", "pallet-xcm/std", "parachain-info/std", @@ -118,65 +131,3 @@ std = [ "xcm/std", ] -runtime-benchmarks = [ - "asset-hub-polkadot-runtime/runtime-benchmarks", - "cumulus-pallet-xcmp-queue/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "itertools/use_alloc", - "pallet-assets/runtime-benchmarks", - "pallet-balances/runtime-benchmarks", - "pallet-funding/runtime-benchmarks", - "pallet-im-online/runtime-benchmarks", - "pallet-linear-release/runtime-benchmarks", - "pallet-membership/runtime-benchmarks", - "pallet-message-queue/runtime-benchmarks", - "pallet-parachain-staking/runtime-benchmarks", - "pallet-staking/runtime-benchmarks", - "pallet-vesting/runtime-benchmarks", - "pallet-xcm/runtime-benchmarks", - "penpal-runtime/runtime-benchmarks", - "polimec-base-runtime/runtime-benchmarks", - "polimec-common/runtime-benchmarks", - "polimec-parachain-runtime/runtime-benchmarks", - "polimec-receiver/runtime-benchmarks", - "polkadot-parachain/runtime-benchmarks", - "polkadot-primitives/runtime-benchmarks", - "polkadot-runtime-parachains/runtime-benchmarks", - "polkadot-runtime/runtime-benchmarks", - "polkadot-service/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", - "xcm-builder/runtime-benchmarks", - "xcm-executor/runtime-benchmarks", -] -try-runtime = [ - "asset-hub-polkadot-runtime/try-runtime", - "cumulus-pallet-xcm/try-runtime", - "cumulus-pallet-xcmp-queue/try-runtime", - "frame-support/try-runtime", - "frame-system/try-runtime", - "orml-oracle/try-runtime", - "pallet-assets/try-runtime", - "pallet-balances/try-runtime", - "pallet-funding/try-runtime", - "pallet-im-online/try-runtime", - "pallet-linear-release/try-runtime", - "pallet-membership/try-runtime", - "pallet-message-queue/try-runtime", - "pallet-parachain-staking/try-runtime", - "pallet-staking/try-runtime", - "pallet-vesting/try-runtime", - "pallet-xcm/try-runtime", - "parachain-info/try-runtime", - "penpal-runtime/try-runtime", - "polimec-base-runtime/try-runtime", - "polimec-common/try-runtime", - "polimec-parachain-runtime/try-runtime", - "polimec-receiver/try-runtime", - "polkadot-runtime-parachains/try-runtime", - "polkadot-runtime/try-runtime", - "polkadot-service/try-runtime", - "sp-runtime/try-runtime", -] -fast-gov = [ "polimec-parachain-runtime/fast-gov" ] - diff --git a/integration-tests/README.md b/integration-tests/README.md index a0bd52c84..db25005c7 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -14,5 +14,5 @@ with the latest chain specs to provide a realistic testing environment. ```bash $ cd polimec-node/integration-tests/ -$ cargo test --features std,fast-gov +$ cargo test ``` diff --git a/integration-tests/penpal/Cargo.toml b/integration-tests/penpal/Cargo.toml index b62eda41e..2b9abbe3b 100644 --- a/integration-tests/penpal/Cargo.toml +++ b/integration-tests/penpal/Cargo.toml @@ -191,9 +191,9 @@ try-runtime = [ "pallet-vesting/try-runtime", "pallet-xcm/try-runtime", "parachain-info/try-runtime", + "polimec-common/try-runtime", "polimec-receiver/try-runtime", "polkadot-runtime-common/try-runtime", "polkadot-runtime-parachains/try-runtime", "sp-runtime/try-runtime", - "polimec-common/try-runtime" ] diff --git a/integration-tests/src/constants.rs b/integration-tests/src/constants.rs index 1cf0a6961..65b006d3a 100644 --- a/integration-tests/src/constants.rs +++ b/integration-tests/src/constants.rs @@ -305,12 +305,14 @@ pub mod polimec { let alice_account = Polimec::account_id_of(accounts::ALICE); let bob_account: AccountId = Polimec::account_id_of(accounts::BOB); let charlie_account: AccountId = Polimec::account_id_of(accounts::CHARLIE); + let dave_account: AccountId = Polimec::account_id_of(accounts::DAVE); + let eve_account: AccountId = Polimec::account_id_of(accounts::EVE); funded_accounts.extend(accounts::init_balances().iter().cloned().map(|k| (k, INITIAL_DEPOSIT))); funded_accounts.extend(collators::initial_authorities().iter().cloned().map(|(acc, _)| (acc, 20_005 * PLMC))); funded_accounts.push((get_account_id_from_seed::("TREASURY_STASH"), 20_005 * PLMC)); - let genesis_config = polimec_parachain_runtime::GenesisConfig { + let genesis_config = polimec_parachain_runtime::RuntimeGenesisConfig { system: polimec_parachain_runtime::SystemConfig { code: polimec_parachain_runtime::WASM_BINARY .expect("WASM binary was not build, please build it!") @@ -346,6 +348,27 @@ pub mod polimec { }, council: Default::default(), democracy: Default::default(), + treasury: Default::default(), + technical_committee: polimec_parachain_runtime::TechnicalCommitteeConfig { + members: vec![ + alice_account.clone(), + bob_account.clone(), + charlie_account.clone(), + dave_account.clone(), + eve_account.clone(), + ], + ..Default::default() + }, + elections: polimec_parachain_runtime::ElectionsConfig { + members: vec![ + (alice_account.clone(), 0), + (bob_account.clone(), 0), + (charlie_account.clone(), 0), + (dave_account.clone(), 0), + (eve_account.clone(), 0), + ], + ..Default::default() + }, oracle_providers_membership: polimec_parachain_runtime::OracleProvidersMembershipConfig { members: bounded_vec![alice_account.clone(), bob_account, charlie_account], ..Default::default() @@ -373,8 +396,6 @@ pub mod polimec { ], accounts: vec![], }, - technical_committee: Default::default(), - treasury: Default::default(), polimec_funding: Default::default(), vesting: Default::default(), }; @@ -444,7 +465,6 @@ pub mod penpal { pub mod polimec_base { use super::*; use crate::PolimecBase; - use pallet_funding::AcceptedFundingAsset; use xcm::{prelude::Parachain, v3::Parent}; pub const PARA_ID: u32 = 3344; @@ -456,8 +476,6 @@ pub mod polimec_base { const GENESIS_NUM_SELECTED_CANDIDATES: u32 = 5; pub fn genesis() -> Storage { - let dot_asset_id = AcceptedFundingAsset::DOT.to_statemint_id(); - let usdt_asset_id = AcceptedFundingAsset::USDT.to_statemint_id(); let mut funded_accounts = vec![ (PolimecBase::sovereign_account_id_of((Parent, Parachain(penpal::PARA_ID)).into()), INITIAL_DEPOSIT), (PolimecBase::sovereign_account_id_of((Parent, Parachain(statemint::PARA_ID)).into()), INITIAL_DEPOSIT), @@ -465,12 +483,14 @@ pub mod polimec_base { let alice_account = PolimecBase::account_id_of(accounts::ALICE); let bob_account: AccountId = PolimecBase::account_id_of(accounts::BOB); let charlie_account: AccountId = PolimecBase::account_id_of(accounts::CHARLIE); + let dave_account: AccountId = PolimecBase::account_id_of(accounts::DAVE); + let eve_account: AccountId = PolimecBase::account_id_of(accounts::EVE); funded_accounts.extend(accounts::init_balances().iter().cloned().map(|k| (k, INITIAL_DEPOSIT))); funded_accounts.extend(collators::initial_authorities().iter().cloned().map(|(acc, _)| (acc, 20_005 * PLMC))); funded_accounts.push((get_account_id_from_seed::("TREASURY_STASH"), 20_005 * PLMC)); - let genesis_config = polimec_base_runtime::GenesisConfig { + let genesis_config = polimec_base_runtime::RuntimeGenesisConfig { system: polimec_base_runtime::SystemConfig { code: polimec_base_runtime::WASM_BINARY.expect("WASM binary was not build, please build it!").to_vec(), ..Default::default() @@ -494,6 +514,28 @@ pub mod polimec_base { }, aura: Default::default(), aura_ext: Default::default(), + council: Default::default(), + technical_committee: polimec_base_runtime::TechnicalCommitteeConfig { + members: vec![ + alice_account.clone(), + bob_account.clone(), + charlie_account.clone(), + dave_account.clone(), + eve_account.clone(), + ], + ..Default::default() + }, + elections: polimec_base_runtime::ElectionsConfig { + members: vec![ + (alice_account.clone(), 0), + (bob_account.clone(), 0), + (charlie_account.clone(), 0), + (dave_account.clone(), 0), + (eve_account.clone(), 0), + ], + ..Default::default() + }, + democracy: Default::default(), parachain_system: Default::default(), polkadot_xcm: polimec_base_runtime::PolkadotXcmConfig { safe_xcm_version: Some(SAFE_XCM_VERSION), @@ -518,6 +560,7 @@ pub mod polimec_base { }, vesting: Default::default(), transaction_payment: Default::default(), + treasury: Default::default(), }; genesis_config.build_storage().unwrap() diff --git a/integration-tests/src/tests/build_spec.rs b/integration-tests/src/tests/build_spec.rs index 3189019fe..b7b492054 100644 --- a/integration-tests/src/tests/build_spec.rs +++ b/integration-tests/src/tests/build_spec.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - +#[ignore] #[test] fn build_spec_testing_node() { // run the polimec-parachain-node compiled with "std" with the build-spec command and --raw flag @@ -37,11 +37,10 @@ fn build_spec_testing_node() { .output() .expect("failed to execute process"); + dbg!(output.clone()); assert_eq!( output.status.success(), true, - "Make sure you compile the polimec-parachain-node with \"--release\" and \"--features std,fast-gov\" before running this test." + "Make sure you compile the polimec-parachain-node with \"--release\" and \"--features std,fast-mode\" before running this test." ); - - dbg!(output); } diff --git a/integration-tests/src/tests/defaults.rs b/integration-tests/src/tests/defaults.rs index d4189db79..936deee29 100644 --- a/integration-tests/src/tests/defaults.rs +++ b/integration-tests/src/tests/defaults.rs @@ -14,17 +14,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{Polimec, PolimecRuntime, ALICE, BOB, CHARLIE}; -use frame_support::{assert_ok, bounded_vec, BoundedVec}; +use crate::PolimecRuntime; +use frame_support::BoundedVec; pub use pallet_funding::instantiator::{BidParams, ContributionParams, UserToPLMCBalance, UserToUSDBalance}; use pallet_funding::{ AcceptedFundingAsset, CurrencyMetadata, ParticipantsSize, ProjectMetadata, ProjectMetadataOf, TicketSize, }; -use sp_arithmetic::FixedU128; use sp_core::H256; -use std::collections::HashMap; -use crate::PolimecOrigin; use macros::generate_accounts; use polimec_parachain_runtime::AccountId; use sp_runtime::{traits::ConstU32, Perquintill}; diff --git a/integration-tests/src/tests/governance.rs b/integration-tests/src/tests/governance.rs new file mode 100644 index 000000000..d43319a63 --- /dev/null +++ b/integration-tests/src/tests/governance.rs @@ -0,0 +1,475 @@ +use crate::{polimec_base::ED, *}; +/// Tests for the oracle pallet integration. +/// Alice, Bob, Charlie are members of the OracleProvidersMembers. +/// Only members should be able to feed data into the oracle. +use frame_support::traits::fungible::Inspect; +use frame_support::traits::{ + fungible::{BalancedHold, MutateFreeze, MutateHold, Unbalanced}, + OnInitialize, WithdrawReasons, +}; +use macros::generate_accounts; +use sp_runtime::{traits::Hash, Digest}; + +use frame_support::{ + dispatch::GetDispatchInfo, + traits::{ + fungible::InspectFreeze, tokens::Precision, Imbalance, LockableCurrency, ReservableCurrency, StorePreimage, + }, +}; +use pallet_democracy::{AccountVote, Conviction, ReferendumInfo, Vote}; +use pallet_vesting::VestingInfo; +use polimec_base_runtime::{ + Balances, Council, Democracy, Elections, ParachainStaking, Preimage, RuntimeOrigin, TechnicalCommittee, Treasury, + Vesting, +}; +use tests::defaults::*; +use xcm_emulator::get_account_id_from_seed; +generate_accounts!(PEPE, CARLOS,); + +/// Test that an account with vested tokens (a lock) can use those tokens for a hold. +/// The hold can also be released or slashed while the lock is still in place. +#[test] +fn vested_tokens_and_holds_work_together() { + PolimecBase::execute_with(|| { + let alice = PolimecBase::account_id_of(ALICE); + let new_account = create_vested_account(); + + assert_eq!(Balances::balance(&alice), 220 * PLMC - ED); + assert_eq!(Balances::balance(&new_account), 200 * PLMC + ED); + + assert_ok!(Balances::hold( + &polimec_base_runtime::RuntimeHoldReason::ParachainStaking( + pallet_parachain_staking::HoldReason::StakingCollator + ), + &new_account, + 200 * PLMC + )); + Balances::set_lock(*b"plmc/gov", &new_account, 200 * PLMC + ED, WithdrawReasons::all()); + assert_ok!(Balances::release( + &polimec_base_runtime::RuntimeHoldReason::ParachainStaking( + pallet_parachain_staking::HoldReason::StakingCollator + ), + &new_account, + 200 * PLMC, + Precision::Exact + )); + + assert_eq!(Balances::reserved_balance(&new_account), 0); + + assert_ok!(Balances::hold( + &polimec_base_runtime::RuntimeHoldReason::ParachainStaking( + pallet_parachain_staking::HoldReason::StakingCollator + ), + &new_account, + 200 * PLMC + )); + let slashed = Balances::slash( + &polimec_base_runtime::RuntimeHoldReason::ParachainStaking( + pallet_parachain_staking::HoldReason::StakingCollator, + ), + &new_account, + 200 * PLMC, + ); + assert_eq!(slashed.0.peek(), 200 * PLMC); + + assert_eq!(Balances::reserved_balance(&new_account), 0); + }) +} + +/// Test that an account with vested tokens (a lock) cannot use those tokens for a reserve. +#[test] +fn vested_tokens_and_reserves_dont_work_together() { + PolimecBase::execute_with(|| { + let alice = PolimecBase::account_id_of(ALICE); + let new_account = create_vested_account(); + + assert_eq!(Balances::balance(&alice), 220 * PLMC - ED); + assert_eq!(Balances::balance(&new_account), 200 * PLMC + ED); + + assert_noop!( + Balances::reserve(&new_account, 200 * PLMC), + pallet_balances::Error::::LiquidityRestrictions + ); + }); +} + +/// Test that locks and freezes can be placed on balance that is already reserved. +#[test] +fn lock_and_freeze_after_reserve_does_work() { + PolimecBase::execute_with(|| { + let alice = PolimecBase::account_id_of(ALICE); + + assert_ok!(Balances::reserve(&alice, 400 * PLMC)); + assert_ok!(Balances::set_freeze( + &polimec_base_runtime::RuntimeFreezeReason::Democracy(pallet_democracy::FreezeReason::Vote), + &alice, + 400 * PLMC + )); + Balances::set_lock(*b"py/trsry", &alice, 400 * PLMC, WithdrawReasons::all()); + }); +} + +/// Test that correct members are set with the default genesis config. +#[test] +fn council_and_technical_committee_members_set_correctly() { + let alice = PolimecBase::account_id_of(ALICE); + let bob = PolimecBase::account_id_of(BOB); + let charlie = PolimecBase::account_id_of(CHARLIE); + let dave = PolimecBase::account_id_of(DAVE); + let eve = PolimecBase::account_id_of(EVE); + let accounts = vec![alice, bob, charlie, dave, eve]; + Polimec::execute_with(|| { + assert_same_members(Council::members(), &accounts); + assert_same_members(TechnicalCommittee::members(), &accounts); + }); +} + +/// Test that basic democracy works correctly. +/// 1. Public proposal is created. +/// 2. Public votes on the proposal. +/// 3. Proposal is approved. +/// 4. Proposal is enacted. +#[test] +fn democracy_works() { + let alice = PolimecBase::account_id_of(ALICE); + // 1. Create a proposal to set the the balance of `account` to 1000 PLMC + PolimecBase::execute_with(|| { + let account = create_vested_account(); + let bounded_call = Preimage::bound(::RuntimeCall::Balances( + pallet_balances::Call::force_set_balance { who: account.clone().into(), new_free: 1000u128 * PLMC }, + )) + .unwrap(); + assert_ok!(Democracy::propose(RuntimeOrigin::signed(account.clone()), bounded_call, 100 * PLMC,)); + }); + + run_gov_n_blocks(1); + // 2. Proposal is turned into a referendum + // Alice votes on the proposal with 100 PLMC + PolimecBase::execute_with(|| { + assert!(Democracy::referendum_count() == 1); + assert_eq!( + Balances::balance_frozen( + &polimec_base_runtime::RuntimeFreezeReason::Democracy(pallet_democracy::FreezeReason::Vote), + &alice + ), + 0 + ); + do_vote(alice.clone(), 0, true, 100 * PLMC); + assert_eq!( + Balances::balance_frozen( + &polimec_base_runtime::RuntimeFreezeReason::Democracy(pallet_democracy::FreezeReason::Vote), + &alice + ), + 100 * PLMC + ); + }); + + run_gov_n_blocks(2); + // 3. Referendum is approved + PolimecBase::execute_with(|| { + assert_eq!(Democracy::referendum_info(0).unwrap(), ReferendumInfo::Finished { approved: true, end: 4u32 }); + assert!(pallet_scheduler::Agenda::::get(6u32).len() == 1); + }); + + // 4. Referendum is enacted + run_gov_n_blocks(2); + PolimecBase::execute_with(|| { + assert_eq!(Balances::balance(&get_account_id_from_seed::("NEW_ACCOUNT")), 1000u128 * PLMC); + }); +} + +/// Test that a user with staked balance can vote on a democracy proposal. +#[test] +fn user_can_vote_with_staked_balance() { + // 1. Create a proposal to set the the balance of `account` to 1000 PLMC + // 2. Account stakes 100 PLMC. + PolimecBase::execute_with(|| { + let account = create_vested_account(); + let bounded_call = Preimage::bound(::RuntimeCall::Balances( + pallet_balances::Call::force_set_balance { who: account.clone().into(), new_free: 1000u128 * PLMC }, + )) + .unwrap(); + assert_ok!(Democracy::propose(RuntimeOrigin::signed(account.clone()), bounded_call, 100 * PLMC)); + + assert_ok!(ParachainStaking::delegate( + RuntimeOrigin::signed(account.clone()), + get_account_id_from_seed::("COLL_1"), + 100 * PLMC, + 0, + 0 + )); + + // Total PLMC reserved for staking (100) + creating proposal (100) = 200 + assert_eq!(Balances::reserved_balance(&account), 200 * PLMC) + }); + + run_gov_n_blocks(1); + // 3. User votes on the proposal with 200 PLMC + PolimecBase::execute_with(|| { + let account = get_account_id_from_seed::("NEW_ACCOUNT"); + assert_eq!( + Balances::balance_frozen( + &polimec_base_runtime::RuntimeFreezeReason::Democracy(pallet_democracy::FreezeReason::Vote), + &account + ), + 0 + ); + do_vote(account.clone(), 0, true, 200 * PLMC); + assert_eq!( + Balances::balance_frozen( + &polimec_base_runtime::RuntimeFreezeReason::Democracy(pallet_democracy::FreezeReason::Vote), + &account + ), + 200 * PLMC + ); + }) +} + +/// Test that treasury proposals can be directly accepted by the council without going through governance. +#[test] +fn treasury_proposal_accepted_by_council() { + let alice = PolimecBase::account_id_of(ALICE); + let bob = PolimecBase::account_id_of(BOB); + let charlie = PolimecBase::account_id_of(CHARLIE); + let dave = PolimecBase::account_id_of(DAVE); + let eve = PolimecBase::account_id_of(EVE); + let accounts = vec![(alice.clone(), true), (bob, true), (charlie, true), (dave, true), (eve, true)]; + PolimecBase::execute_with(|| { + // 0. Set the treasury balance to 1000 PLMC + assert_ok!(Balances::write_balance(&Treasury::account_id(), 1000 * PLMC)); + + // 1. Create treasury proposal for 100 PLMC + assert_ok!(Treasury::propose_spend( + RuntimeOrigin::signed(alice.clone()), + 100 * PLMC, + get_account_id_from_seed::("Beneficiary").into() + )); + assert_eq!(Treasury::proposal_count(), 1); + + // 2. Council will vote on the proposal + let proposal = + polimec_base_runtime::RuntimeCall::Treasury(pallet_treasury::Call::approve_proposal { proposal_id: 0 }); + assert_ok!(Council::propose(RuntimeOrigin::signed(alice.clone()), 5, Box::new(proposal.clone()), 100,)); + + // 3. Council votes on the proposal + let proposal_hash = ::Hashing::hash_of(&proposal); + do_council_vote_for(accounts.clone(), 0, proposal_hash); + + // 4. Proposal is approved + assert_ok!(Council::close( + RuntimeOrigin::signed(alice.clone()), + proposal_hash, + 0, + proposal.get_dispatch_info().weight, + 100, + )); + }); + + run_gov_n_blocks(3); + + PolimecBase::execute_with(|| { + // 5. Beneficiary receives the funds + assert_eq!(Balances::balance(&get_account_id_from_seed::("Beneficiary")), 100 * PLMC); + }); +} + +/// Test that treasury proposals can be directly rejected by the council without going through governance. +/// The treasury proposal deposit is slashed and sent to the treasury. +#[test] +fn slashed_treasury_proposal_funds_send_to_treasury() { + let alice = PolimecBase::account_id_of(ALICE); + PolimecBase::execute_with(|| { + // 0. Set the treasury balance to 1000 PLMC + assert_ok!(Balances::write_balance(&Treasury::account_id(), 1000 * PLMC)); + let alice_balance = Balances::balance(&alice); + // 1. Create treasury proposal for 100 PLMC + assert_ok!(Treasury::propose_spend( + RuntimeOrigin::signed(alice.clone()), + 100 * PLMC, + get_account_id_from_seed::("Beneficiary").into() + )); + + // 2. Reject treasury proposal + assert_ok!(Treasury::reject_proposal( + pallet_collective::RawOrigin::::Members(5, 9).into(), + 0u32, + )); + + // 3. See that the funds are slashed and sent to treasury + assert_eq!(Balances::balance(&Treasury::account_id()), 1050 * PLMC); + assert_eq!(Balances::balance(&alice), alice_balance - 50 * PLMC); + }); +} + +/// Test that users can vote in the election-phragmen pallet with their staked balance. +#[test] +fn user_can_vote_in_election_with_staked_balance() { + let alice = PolimecBase::account_id_of(ALICE); + PolimecBase::execute_with(|| { + let account = create_vested_account(); + + assert_ok!(ParachainStaking::delegate( + RuntimeOrigin::signed(account.clone()), + get_account_id_from_seed::("COLL_1"), + 200 * PLMC, + 0, + 0 + )); + + // Total PLMC reserved for staking (100) + creating proposal (100) = 200 + assert_eq!(Balances::reserved_balance(&account), 200 * PLMC); + + assert_ok!(Elections::vote(RuntimeOrigin::signed(account.clone()), vec![alice], 200 * PLMC,)); + + assert_eq!( + Balances::balance_frozen( + &polimec_base_runtime::RuntimeFreezeReason::Elections(pallet_elections_phragmen::FreezeReason::Voting), + &account + ), + 200 * PLMC + ); + + assert_noop!( + Elections::remove_voter(RuntimeOrigin::signed(account.clone())), + pallet_elections_phragmen::Error::::VotingPeriodNotEnded + ); + }); + + run_gov_n_blocks(5); + + PolimecBase::execute_with(|| { + let account = get_account_id_from_seed::("NEW_ACCOUNT"); + + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(account.clone()))); + assert_eq!( + Balances::balance_frozen( + &polimec_base_runtime::RuntimeFreezeReason::Elections(pallet_elections_phragmen::FreezeReason::Voting), + &account + ), + 0 + ); + }); +} + +/// Tests that the election works as expected. +/// 1. Register 32 candidates +/// 2. 8 accounts vote for 8 candidates +/// 3. Run the election +/// 4. Check that the 9 candidates with the most votes are elected +/// 5. Check that the 6 candidates with the next most votes are runners up +/// 6. Check that the remaining candidates have their funds slashed as they did not receive any votes +#[test] +fn election_phragmen_works() { + let candidates = (1..=32) + .into_iter() + .map(|i| get_account_id_from_seed::(format!("CANDIDATE_{}", i).as_str())) + .collect::>(); + // 1. Register candidates for the election. + PolimecBase::execute_with(|| { + assert_eq!(Elections::candidates().len(), 0); + // Alice .. Eve already selected members + assert_eq!(Elections::members().len(), 5); + assert_eq!(Elections::runners_up().len(), 0); + + for (i, candidate) in candidates.iter().enumerate() { + assert_ok!(Balances::write_balance(&candidate, 1000 * PLMC + ED)); + assert_ok!(Elections::submit_candidacy(RuntimeOrigin::signed((*candidate).clone()), i as u32)); + } + + assert_eq!(Elections::candidates().len(), 32); + + for (i, voter) in vec![ALICE, BOB, CHARLIE, DAVE, EVE, FERDIE, ALICE_STASH, BOB_STASH].into_iter().enumerate() { + let voter = PolimecBase::account_id_of(voter); + assert_ok!(Elections::vote( + RuntimeOrigin::signed(voter.clone()), + candidates[i..(i + 8)].to_vec(), + 200 * PLMC, + )); + } + }); + + run_gov_n_blocks(5); + + PolimecBase::execute_with(|| { + assert_eq!(Elections::candidates().len(), 0); + assert_eq!(Elections::members().len(), 9); + assert_eq!(Elections::runners_up().len(), 6); + + let expected_runners_up = candidates[0..3].iter().cloned().chain(candidates[12..15].iter().cloned()).collect(); + assert_same_members(Elections::members().into_iter().map(|m| m.who).collect(), &(candidates[3..12].to_vec())); + assert_same_members(Elections::runners_up().into_iter().map(|m| m.who).collect(), &expected_runners_up); + + // Check that the candidates that were not elected have their funds slashed + for candidate in &candidates[15..32] { + assert_eq!(Balances::total_balance(candidate), ED); + } + assert_eq!(Balances::balance(&Treasury::account_id()), 17 * 1000 * PLMC + ED) + }); +} + +fn assert_same_members(expected: Vec, actual: &Vec) { + assert_eq!(expected.len(), actual.len()); + for member in expected { + assert!(actual.contains(&member)); + } +} + +fn create_vested_account() -> AccountId { + let alice = PolimecBase::account_id_of(ALICE); + let new_account = get_account_id_from_seed::("NEW_ACCOUNT"); + + // Initially the NEW_ACCOUNT has no PLMC + assert_eq!(Balances::balance(&new_account), 0 * PLMC); + + // Create a vesting schedule for 200 PLMC + ED over 60 blocks (~1 PLMC per block) to NEW_ACCOUNT + let vesting_schedule = VestingInfo::new( + 200 * PLMC + ED, + PLMC, // Vesting over 60 blocks + 1, + ); + // The actual vested transfer + assert_ok!(Vesting::vested_transfer( + RuntimeOrigin::signed(alice.clone()), + sp_runtime::MultiAddress::Id(new_account.clone()), + vesting_schedule + )); + new_account +} + +fn run_gov_n_blocks(n: usize) { + for _ in 0..n { + PolimecBase::execute_with(|| { + let block_number = polimec_base_runtime::System::block_number(); + + let header = polimec_base_runtime::System::finalize(); + + let pre_digest = Digest { logs: vec![] }; + polimec_base_runtime::System::reset_events(); + + let next_block_number = block_number + 1u32; + polimec_base_runtime::Vesting::on_initialize(next_block_number); + polimec_base_runtime::Elections::on_initialize(next_block_number); + polimec_base_runtime::Council::on_initialize(next_block_number); + polimec_base_runtime::TechnicalCommittee::on_initialize(next_block_number); + polimec_base_runtime::Treasury::on_initialize(next_block_number); + polimec_base_runtime::Democracy::on_initialize(next_block_number); + polimec_base_runtime::Preimage::on_initialize(next_block_number); + polimec_base_runtime::Scheduler::on_initialize(next_block_number); + polimec_base_runtime::System::initialize(&next_block_number, &header.hash(), &pre_digest); + }); + } +} + +fn do_vote(account: AccountId, index: u32, approve: bool, amount: u128) { + assert_ok!(Democracy::vote( + RuntimeOrigin::signed(account.clone()), + index, + AccountVote::Standard { balance: amount, vote: Vote { aye: approve, conviction: Conviction::Locked1x } }, + )); +} + +fn do_council_vote_for(accounts: Vec<(AccountId, bool)>, index: u32, hash: polimec_base_runtime::Hash) { + for (account, approve) in accounts { + assert_ok!(Council::vote(RuntimeOrigin::signed(account.clone()), hash, index, approve,)); + } +} diff --git a/integration-tests/src/tests/mod.rs b/integration-tests/src/tests/mod.rs index 5b75db614..ae98810ff 100644 --- a/integration-tests/src/tests/mod.rs +++ b/integration-tests/src/tests/mod.rs @@ -19,6 +19,7 @@ mod build_spec; mod ct_migration; mod defaults; mod e2e; +mod governance; mod oracle; mod reserve_backed_transfers; mod vest; diff --git a/integration-tests/src/tests/vest.rs b/integration-tests/src/tests/vest.rs index 7a5122c71..d23a6261e 100644 --- a/integration-tests/src/tests/vest.rs +++ b/integration-tests/src/tests/vest.rs @@ -19,12 +19,12 @@ use crate::{polimec_base::ED, *}; /// Alice, Bob, Charlie are members of the OracleProvidersMembers. /// Only members should be able to feed data into the oracle. use frame_support::traits::fungible::Inspect; -use frame_support::traits::fungible::{InspectHold, Mutate}; +use frame_support::traits::fungible::Mutate; use macros::generate_accounts; use pallet_funding::assert_close_enough; use pallet_vesting::VestingInfo; use polimec_base_runtime::{Balances, ParachainStaking, RuntimeOrigin, Vesting}; -use sp_runtime::{bounded_vec, BoundedVec, FixedU128, Perquintill}; +use sp_runtime::Perquintill; use tests::defaults::*; use xcm_emulator::get_account_id_from_seed; diff --git a/justfile b/justfile index 0a0fe6f86..af2e2c708 100644 --- a/justfile +++ b/justfile @@ -32,7 +32,8 @@ test-runtime-features: # Run the integration tests test-integration: - cargo test -p integration-tests --features std,fast-gov + cargo test -p integration-tests + # Benchmark the "Testnet" Runtime benchmark-runtime-funding: @@ -61,7 +62,7 @@ benchmark-runtime-linear-release: # Benchmark the "Testnet" Runtime benchmark-pallet-funding: - cargo run --features runtime-benchmarks,fast-gov --release -p polimec-parachain-node benchmark pallet \ + cargo run --features runtime-benchmarks,fast-mode --release -p polimec-parachain-node benchmark pallet \ --chain=polimec-rococo-local \ --steps=50 \ --repeat=20 \ @@ -72,7 +73,7 @@ benchmark-pallet-funding: --template=./.maintain/frame-weight-template.hbs benchmark-pallet-linear-release: - cargo run --features runtime-benchmarks,fast-gov --release -p polimec-parachain-node benchmark pallet \ + cargo run --features runtime-benchmarks,fast-mode --release -p polimec-parachain-node benchmark pallet \ --chain=polimec-rococo-local \ --steps=50 \ --repeat=20 \ @@ -85,6 +86,7 @@ benchmark-pallet-linear-release: benchmarks-test: cargo test --features runtime-benchmarks -p pallet-funding benchmarks + # Build the Node Docker Image docker-build tag = "latest" package= "polimec-parachain-node": ./scripts/build_image.sh {{tag}} ./Dockerfile {{package}} diff --git a/nodes/parachain/Cargo.toml b/nodes/parachain/Cargo.toml index 51e1a01d3..adee890de 100644 --- a/nodes/parachain/Cargo.toml +++ b/nodes/parachain/Cargo.toml @@ -88,6 +88,10 @@ substrate-build-script-utils.workspace = true [features] default = [] +fast-mode = [ + "polimec-base-runtime/fast-mode", + "polimec-parachain-runtime/fast-mode", +] runtime-benchmarks = [ "frame-benchmarking-cli/runtime-benchmarks", "frame-benchmarking/runtime-benchmarks", @@ -110,10 +114,6 @@ try-runtime = [ "try-runtime-cli", "try-runtime-cli?/try-runtime", ] -fast-gov = [ - "polimec-base-runtime/fast-gov", - "polimec-parachain-runtime/fast-gov", -] std = [ "cumulus-primitives-core/std", "frame-benchmarking/std", diff --git a/nodes/parachain/src/chain_spec/base.rs b/nodes/parachain/src/chain_spec/base.rs index f9b911d9a..c3f5344e4 100644 --- a/nodes/parachain/src/chain_spec/base.rs +++ b/nodes/parachain/src/chain_spec/base.rs @@ -251,6 +251,11 @@ fn base_testnet_genesis( members: BoundedVec::truncate_from(initial_authorities), ..Default::default() }, + council: Default::default(), + technical_committee: Default::default(), + democracy: Default::default(), + elections: Default::default(), + treasury: Default::default(), vesting: Default::default(), } } diff --git a/nodes/parachain/src/chain_spec/testnet.rs b/nodes/parachain/src/chain_spec/testnet.rs index 3cef8e3b1..658b34a3b 100644 --- a/nodes/parachain/src/chain_spec/testnet.rs +++ b/nodes/parachain/src/chain_spec/testnet.rs @@ -257,8 +257,12 @@ fn testnet_genesis( treasury: Default::default(), sudo: SudoConfig { key: Some(sudo_account) }, council: CouncilConfig { members: accounts.clone(), phantom: Default::default() }, - technical_committee: TechnicalCommitteeConfig { members: accounts, phantom: Default::default() }, + technical_committee: TechnicalCommitteeConfig { + members: accounts.clone().into_iter().take(5).collect(), + phantom: Default::default(), + }, democracy: Default::default(), + elections: Default::default(), vesting: Default::default(), oracle_providers_membership: OracleProvidersMembershipConfig { members: bounded_vec![ @@ -527,8 +531,12 @@ fn testing_genesis( treasury: Default::default(), sudo: SudoConfig { key: Some(sudo_account) }, council: CouncilConfig { members: accounts.clone(), phantom: Default::default() }, - technical_committee: TechnicalCommitteeConfig { members: accounts, phantom: Default::default() }, + technical_committee: TechnicalCommitteeConfig { + members: accounts.clone().into_iter().take(5).collect(), + phantom: Default::default(), + }, democracy: Default::default(), + elections: Default::default(), vesting: Default::default(), } } diff --git a/pallets/democracy/Cargo.toml b/pallets/democracy/Cargo.toml new file mode 100644 index 000000000..02be6e932 --- /dev/null +++ b/pallets/democracy/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "pallet-democracy" +version = "4.0.0-dev" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage = "https://substrate.io" +repository.workspace = true +description = "FRAME pallet for democracy" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +serde = {workspace = true, features = ["derive"], optional = true} +frame-benchmarking = { workspace = true, optional = true } +frame-support.workspace = true +frame-system.workspace = true +sp-io.workspace = true +pallet-balances.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +sp-core.workspace = true +log.workspace = true + +[dev-dependencies] +pallet-balances.workspace = true +pallet-scheduler.workspace = true +pallet-preimage.workspace = true + +[features] +default = [ "std" ] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-preimage/std", + "pallet-scheduler/std", + "parity-scale-codec/std", + "scale-info/std", + "serde", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", + "pallet-scheduler/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-preimage/try-runtime", + "pallet-scheduler/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/democracy/README.md b/pallets/democracy/README.md new file mode 100644 index 000000000..a56232753 --- /dev/null +++ b/pallets/democracy/README.md @@ -0,0 +1,141 @@ +# Democracy Pallet +This repository contains a modified version of Parity's 'democracy' Substrate Pallet. The original version can be found [here](https://github.com/paritytech/polkadot-sdk/tree/68ac55caee5c726e8b7ef66314beb5013a38be73/substrate/frame/democracy). + +## Modifications +The modifications to the original pallet include the following: +1. Currency traits have been replaced by the Fungibles traits. +2. Voters can vote with their full balance (placing a lock over both free and reserved balance) + +- [`democracy::Config`](https://docs.rs/pallet-democracy/latest/pallet_democracy/trait.Config.html) +- [`Call`](https://docs.rs/pallet-democracy/latest/pallet_democracy/enum.Call.html) + +## Overview + +The Democracy pallet handles the administration of general stakeholder voting. + +There are two different queues that a proposal can be added to before it +becomes a referendum, 1) the proposal queue consisting of all public proposals +and 2) the external queue consisting of a single proposal that originates +from one of the _external_ origins (such as a collective group). + +Every launch period - a length defined in the runtime - the Democracy pallet +launches a referendum from a proposal that it takes from either the proposal +queue or the external queue in turn. Any token holder in the system can vote +on referenda. The voting system +uses time-lock voting by allowing the token holder to set their _conviction_ +behind a vote. The conviction will dictate the length of time the tokens +will be locked, as well as the multiplier that scales the vote power. + +### Terminology + +- **Enactment Period:** The minimum period of locking and the period between a proposal being +approved and enacted. +- **Lock Period:** A period of time after proposal enactment that the tokens of _winning_ voters +will be locked. +- **Conviction:** An indication of a voter's strength of belief in their vote. An increase +of one in conviction indicates that a token holder is willing to lock their tokens for twice +as many lock periods after enactment. +- **Vote:** A value that can either be in approval ("Aye") or rejection ("Nay") + of a particular referendum. +- **Proposal:** A submission to the chain that represents an action that a proposer (either an +account or an external origin) suggests that the system adopt. +- **Referendum:** A proposal that is in the process of being voted on for + either acceptance or rejection as a change to the system. +- **Delegation:** The act of granting your voting power to the decisions of another account for + up to a certain conviction. + +### Adaptive Quorum Biasing + +A _referendum_ can be either simple majority-carries in which 50%+1 of the +votes decide the outcome or _adaptive quorum biased_. Adaptive quorum biasing +makes the threshold for passing or rejecting a referendum higher or lower +depending on how the referendum was originally proposed. There are two types of +adaptive quorum biasing: 1) _positive turnout bias_ makes a referendum +require a super-majority to pass that decreases as turnout increases and +2) _negative turnout bias_ makes a referendum require a super-majority to +reject that decreases as turnout increases. Another way to think about the +quorum biasing is that _positive bias_ referendums will be rejected by +default and _negative bias_ referendums get passed by default. + +## Interface + +### Dispatchable Functions + +#### Public + +These calls can be made from any externally held account capable of creating +a signed extrinsic. + +Basic actions: +- `propose` - Submits a sensitive action, represented as a hash. Requires a deposit. +- `second` - Signals agreement with a proposal, moves it higher on the proposal queue, and + requires a matching deposit to the original. +- `vote` - Votes in a referendum, either the vote is "Aye" to enact the proposal or "Nay" to + keep the status quo. +- `unvote` - Cancel a previous vote, this must be done by the voter before the vote ends. +- `delegate` - Delegates the voting power (tokens * conviction) to another account. +- `undelegate` - Stops the delegation of voting power to another account. + +Administration actions that can be done to any account: +- `reap_vote` - Remove some account's expired votes. +- `unlock` - Redetermine the account's balance lock, potentially making tokens available. + +Preimage actions: +- `note_preimage` - Registers the preimage for an upcoming proposal, requires + a deposit that is returned once the proposal is enacted. +- `note_preimage_operational` - same but provided by `T::OperationalPreimageOrigin`. +- `note_imminent_preimage` - Registers the preimage for an upcoming proposal. + Does not require a deposit, but the proposal must be in the dispatch queue. +- `note_imminent_preimage_operational` - same but provided by `T::OperationalPreimageOrigin`. +- `reap_preimage` - Removes the preimage for an expired proposal. Will only + work under the condition that it's the same account that noted it and + after the voting period, OR it's a different account after the enactment period. + +#### Cancellation Origin + +This call can only be made by the `CancellationOrigin`. + +- `emergency_cancel` - Schedules an emergency cancellation of a referendum. + Can only happen once to a specific referendum. + +#### ExternalOrigin + +This call can only be made by the `ExternalOrigin`. + +- `external_propose` - Schedules a proposal to become a referendum once it is is legal + for an externally proposed referendum. + +#### External Majority Origin + +This call can only be made by the `ExternalMajorityOrigin`. + +- `external_propose_majority` - Schedules a proposal to become a majority-carries + referendum once it is legal for an externally proposed referendum. + +#### External Default Origin + +This call can only be made by the `ExternalDefaultOrigin`. + +- `external_propose_default` - Schedules a proposal to become a negative-turnout-bias + referendum once it is legal for an externally proposed referendum. + +#### Fast Track Origin + +This call can only be made by the `FastTrackOrigin`. + +- `fast_track` - Schedules the current externally proposed proposal that + is "majority-carries" to become a referendum immediately. + +#### Veto Origin + +This call can only be made by the `VetoOrigin`. + +- `veto_external` - Vetoes and blacklists the external proposal hash. + +#### Root + +- `cancel_referendum` - Removes a referendum. +- `cancel_queued` - Cancels a proposal that is queued for enactment. +- `clear_public_proposal` - Removes all public proposals. + +License: Apache-2.0 diff --git a/pallets/democracy/src/benchmarking.rs b/pallets/democracy/src/benchmarking.rs new file mode 100644 index 000000000..ea76b349c --- /dev/null +++ b/pallets/democracy/src/benchmarking.rs @@ -0,0 +1,849 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! Democracy pallet benchmarking. + +use super::*; + +use crate::Pallet as Democracy; +use frame_benchmarking::v1::{account, benchmarks, whitelist_account, BenchmarkError}; +use frame_support::{ + assert_noop, assert_ok, + traits::{fungible::Mutate, Currency, EnsureOrigin, Get, OnInitialize, UnfilteredDispatchable}, +}; +use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; +use sp_core::H256; +use sp_runtime::{traits::Bounded, BoundedVec}; + +const REFERENDUM_COUNT_HINT: u32 = 10; +const SEED: u32 = 0; + +fn funded_account(name: &'static str, index: u32) -> T::AccountId { + let caller: T::AccountId = account(name, index, SEED); + // Minting can overflow, so we can't abuse of the funding. This value happens to be big enough, + // but not too big to make the total supply overflow. + T::Fungible::set_balance(&caller, BalanceOf::::max_value() / 10_000u32.into()); + caller +} + +fn make_proposal(n: u32) -> BoundedCallOf { + let call: CallOf = frame_system::Call::remark { remark: n.encode() }.into(); + ::Preimages::bound(call).unwrap() +} + +fn add_proposal(n: u32) -> Result { + let other = funded_account::("proposer", n); + let value = T::MinimumDeposit::get(); + let proposal = make_proposal::(n); + Democracy::::propose(RawOrigin::Signed(other).into(), proposal.clone(), value)?; + Ok(proposal.hash()) +} + +// add a referendum with a metadata. +fn add_referendum(n: u32) -> (ReferendumIndex, H256, PreimageHash) { + let vote_threshold = VoteThreshold::SimpleMajority; + let proposal = make_proposal::(n); + let hash = proposal.hash(); + let index = Democracy::::inject_referendum(T::LaunchPeriod::get(), proposal, vote_threshold, 0u32.into()); + let preimage_hash = note_preimage::(); + MetadataOf::::insert(crate::MetadataOwner::Referendum(index), preimage_hash.clone()); + (index, hash, preimage_hash) +} + +fn account_vote(b: BalanceOf) -> AccountVote> { + let v = Vote { aye: true, conviction: Conviction::Locked1x }; + + AccountVote::Standard { vote: v, balance: b } +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn assert_has_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_has_event(generic_event.into()); +} + +// note a new preimage. +fn note_preimage() -> PreimageHash { + use core::sync::atomic::{AtomicU8, Ordering}; + use sp_std::borrow::Cow; + // note a new preimage on every function invoke. + static COUNTER: AtomicU8 = AtomicU8::new(0); + let data = Cow::from(vec![COUNTER.fetch_add(1, Ordering::Relaxed)]); + let hash = ::Preimages::note(data).unwrap(); + hash +} + +use frame_support::pallet_prelude::IsType; +benchmarks! { + where_clause { where + T: Config + pallet_balances::Config, + as Currency>::Balance :IsType>, + } + + propose { + let p = T::MaxProposals::get(); + + for i in 0 .. (p - 1) { + add_proposal::(i)?; + } + + let caller = funded_account::("caller", 0); + let proposal = make_proposal::(0); + let value = T::MinimumDeposit::get(); + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller), proposal, value) + verify { + assert_eq!(Democracy::::public_props().len(), p as usize, "Proposals not created."); + } + + second { + let caller = funded_account::("caller", 0); + add_proposal::(0)?; + + // Create s existing "seconds" + // we must reserve one deposit for the `proposal` and one for our benchmarked `second` call. + for i in 0 .. T::MaxDeposits::get() - 2 { + let seconder = funded_account::("seconder", i); + Democracy::::second(RawOrigin::Signed(seconder).into(), 0)?; + } + + let deposits = Democracy::::deposit_of(0).ok_or("Proposal not created")?; + assert_eq!(deposits.0.len(), (T::MaxDeposits::get() - 1) as usize, "Seconds not recorded"); + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller), 0) + verify { + let deposits = Democracy::::deposit_of(0).ok_or("Proposal not created")?; + assert_eq!(deposits.0.len(), (T::MaxDeposits::get()) as usize, "`second` benchmark did not work"); + } + + vote_new { + let caller = funded_account::("caller", 0); + let account_vote = account_vote::(100u32.into()); + + // We need to create existing direct votes + for i in 0 .. T::MaxVotes::get() - 1 { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(caller.clone()).into(), ref_index, account_vote)?; + } + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), (T::MaxVotes::get() - 1) as usize, "Votes were not recorded."); + + let ref_index = add_referendum::(T::MaxVotes::get() - 1).0; + whitelist_account!(caller); + }: vote(RawOrigin::Signed(caller.clone()), ref_index, account_vote) + verify { + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), T::MaxVotes::get() as usize, "Vote was not recorded."); + } + + vote_existing { + let caller = funded_account::("caller", 0); + let account_vote = account_vote::(100u32.into()); + + // We need to create existing direct votes + for i in 0..T::MaxVotes::get() { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(caller.clone()).into(), ref_index, account_vote)?; + } + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), T::MaxVotes::get() as usize, "Votes were not recorded."); + + // Change vote from aye to nay + let nay = Vote { aye: false, conviction: Conviction::Locked1x }; + let new_vote = AccountVote::Standard { vote: nay, balance: 1000u32.into() }; + let ref_index = Democracy::::referendum_count() - 1; + + // This tests when a user changes a vote + whitelist_account!(caller); + }: vote(RawOrigin::Signed(caller.clone()), ref_index, new_vote) + verify { + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), T::MaxVotes::get() as usize, "Vote was incorrectly added"); + let referendum_info = Democracy::::referendum_info(ref_index) + .ok_or("referendum doesn't exist")?; + let tally = match referendum_info { + ReferendumInfo::Ongoing(r) => r.tally, + _ => return Err("referendum not ongoing".into()), + }; + assert_eq!(tally.nays, 1000u32.into(), "changed vote was not recorded"); + } + + emergency_cancel { + let origin = + T::CancellationOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let (ref_index, _, preimage_hash) = add_referendum::(0); + assert_ok!(Democracy::::referendum_status(ref_index)); + }: _(origin, ref_index) + verify { + // Referendum has been canceled + assert_noop!( + Democracy::::referendum_status(ref_index), + Error::::ReferendumInvalid, + ); + assert_last_event::(crate::Event::MetadataCleared { + owner: MetadataOwner::Referendum(ref_index), + hash: preimage_hash, + }.into()); + } + + blacklist { + // Place our proposal at the end to make sure it's worst case. + for i in 0 .. T::MaxProposals::get() - 1 { + add_proposal::(i)?; + } + // We should really add a lot of seconds here, but we're not doing it elsewhere. + + // Add a referendum of our proposal. + let (ref_index, hash, preimage_hash) = add_referendum::(0); + assert_ok!(Democracy::::referendum_status(ref_index)); + // Place our proposal in the external queue, too. + assert_ok!(Democracy::::external_propose( + T::ExternalOrigin::try_successful_origin() + .expect("ExternalOrigin has no successful origin required for the benchmark"), + make_proposal::(0) + )); + let origin = + T::BlacklistOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(origin, hash, Some(ref_index)) + verify { + // Referendum has been canceled + assert_noop!( + Democracy::::referendum_status(ref_index), + Error::::ReferendumInvalid + ); + assert_has_event::(crate::Event::MetadataCleared { + owner: MetadataOwner::Referendum(ref_index), + hash: preimage_hash, + }.into()); + } + + // Worst case scenario, we external propose a previously blacklisted proposal + external_propose { + let origin = + T::ExternalOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let proposal = make_proposal::(0); + // Add proposal to blacklist with block number 0 + + let addresses: BoundedVec<_, _> = (0..(T::MaxBlacklisted::get() - 1)) + .into_iter() + .map(|i| account::("blacklist", i, SEED)) + .collect::>() + .try_into() + .unwrap(); + Blacklist::::insert(proposal.hash(), (BlockNumberFor::::zero(), addresses)); + }: _(origin, proposal) + verify { + // External proposal created + ensure!(>::exists(), "External proposal didn't work"); + } + + external_propose_majority { + let origin = T::ExternalMajorityOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + let proposal = make_proposal::(0); + }: _(origin, proposal) + verify { + // External proposal created + ensure!(>::exists(), "External proposal didn't work"); + } + + external_propose_default { + let origin = T::ExternalDefaultOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + let proposal = make_proposal::(0); + }: _(origin, proposal) + verify { + // External proposal created + ensure!(>::exists(), "External proposal didn't work"); + } + + fast_track { + let origin_propose = T::ExternalDefaultOrigin::try_successful_origin() + .expect("ExternalDefaultOrigin has no successful origin required for the benchmark"); + let proposal = make_proposal::(0); + let proposal_hash = proposal.hash(); + Democracy::::external_propose_default(origin_propose.clone(), proposal)?; + // Set metadata to the external proposal. + let preimage_hash = note_preimage::(); + assert_ok!(Democracy::::set_metadata( + origin_propose, + MetadataOwner::External, + Some(preimage_hash))); + // NOTE: Instant origin may invoke a little bit more logic, but may not always succeed. + let origin_fast_track = + T::FastTrackOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let voting_period = T::FastTrackVotingPeriod::get(); + let delay = 0u32; + }: _(origin_fast_track, proposal_hash, voting_period, delay.into()) + verify { + assert_eq!(Democracy::::referendum_count(), 1, "referendum not created"); + assert_last_event::(crate::Event::MetadataTransferred { + prev_owner: MetadataOwner::External, + owner: MetadataOwner::Referendum(0), + hash: preimage_hash, + }.into()); + } + + veto_external { + let proposal = make_proposal::(0); + let proposal_hash = proposal.hash(); + + let origin_propose = T::ExternalDefaultOrigin::try_successful_origin() + .expect("ExternalDefaultOrigin has no successful origin required for the benchmark"); + Democracy::::external_propose_default(origin_propose.clone(), proposal)?; + + let preimage_hash = note_preimage::(); + assert_ok!(Democracy::::set_metadata( + origin_propose, + MetadataOwner::External, + Some(preimage_hash)) + ); + + let mut vetoers: BoundedVec = Default::default(); + for i in 0 .. (T::MaxBlacklisted::get() - 1) { + vetoers.try_push(account::("vetoer", i, SEED)).unwrap(); + } + vetoers.sort(); + Blacklist::::insert(proposal_hash, (BlockNumberFor::::zero(), vetoers)); + + let origin = T::VetoOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + ensure!(NextExternal::::get().is_some(), "no external proposal"); + }: _(origin, proposal_hash) + verify { + assert!(NextExternal::::get().is_none()); + let (_, new_vetoers) = >::get(&proposal_hash).ok_or("no blacklist")?; + assert_eq!(new_vetoers.len(), T::MaxBlacklisted::get() as usize, "vetoers not added"); + } + + cancel_proposal { + // Place our proposal at the end to make sure it's worst case. + for i in 0 .. T::MaxProposals::get() { + add_proposal::(i)?; + } + // Add metadata to the first proposal. + let proposer = funded_account::("proposer", 0); + let preimage_hash = note_preimage::(); + assert_ok!(Democracy::::set_metadata( + RawOrigin::Signed(proposer).into(), + MetadataOwner::Proposal(0), + Some(preimage_hash))); + let cancel_origin = T::CancelProposalOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + }: _(cancel_origin, 0) + verify { + assert_last_event::(crate::Event::MetadataCleared { + owner: MetadataOwner::Proposal(0), + hash: preimage_hash, + }.into()); + } + + cancel_referendum { + let (ref_index, _, preimage_hash) = add_referendum::(0); + }: _(RawOrigin::Root, ref_index) + verify { + assert_last_event::(crate::Event::MetadataCleared { + owner: MetadataOwner::Referendum(0), + hash: preimage_hash, + }.into()); + } + + #[extra] + on_initialize_external { + let r in 0 .. REFERENDUM_COUNT_HINT; + + for i in 0..r { + add_referendum::(i); + } + + assert_eq!(Democracy::::referendum_count(), r, "referenda not created"); + + // Launch external + LastTabledWasExternal::::put(false); + + let origin = T::ExternalMajorityOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + let proposal = make_proposal::(r); + let call = Call::::external_propose_majority { proposal }; + call.dispatch_bypass_filter(origin)?; + // External proposal created + ensure!(>::exists(), "External proposal didn't work"); + + let block_number = T::LaunchPeriod::get(); + + }: { Democracy::::on_initialize(block_number) } + verify { + // One extra because of next external + assert_eq!(Democracy::::referendum_count(), r + 1, "referenda not created"); + ensure!(!>::exists(), "External wasn't taken"); + + // All but the new next external should be finished + for i in 0 .. r { + if let Some(value) = ReferendumInfoOf::::get(i) { + match value { + ReferendumInfo::Finished { .. } => (), + ReferendumInfo::Ongoing(_) => return Err("Referendum was not finished".into()), + } + } + } + } + + #[extra] + on_initialize_public { + let r in 0 .. (T::MaxVotes::get() - 1); + + for i in 0..r { + add_referendum::(i); + } + + assert_eq!(Democracy::::referendum_count(), r, "referenda not created"); + + // Launch public + assert!(add_proposal::(r).is_ok(), "proposal not created"); + LastTabledWasExternal::::put(true); + + let block_number = T::LaunchPeriod::get(); + + }: { Democracy::::on_initialize(block_number) } + verify { + // One extra because of next public + assert_eq!(Democracy::::referendum_count(), r + 1, "proposal not accepted"); + + // All should be finished + for i in 0 .. r { + if let Some(value) = ReferendumInfoOf::::get(i) { + match value { + ReferendumInfo::Finished { .. } => (), + ReferendumInfo::Ongoing(_) => return Err("Referendum was not finished".into()), + } + } + } + } + + // No launch no maturing referenda. + on_initialize_base { + let r in 0 .. (T::MaxVotes::get() - 1); + + for i in 0..r { + add_referendum::(i); + } + + for (key, mut info) in ReferendumInfoOf::::iter() { + if let ReferendumInfo::Ongoing(ref mut status) = info { + status.end += 100u32.into(); + } + ReferendumInfoOf::::insert(key, info); + } + + assert_eq!(Democracy::::referendum_count(), r, "referenda not created"); + assert_eq!(Democracy::::lowest_unbaked(), 0, "invalid referenda init"); + + }: { Democracy::::on_initialize(1u32.into()) } + verify { + // All should be on going + for i in 0 .. r { + if let Some(value) = ReferendumInfoOf::::get(i) { + match value { + ReferendumInfo::Finished { .. } => return Err("Referendum has been finished".into()), + ReferendumInfo::Ongoing(_) => (), + } + } + } + } + + on_initialize_base_with_launch_period { + let r in 0 .. (T::MaxVotes::get() - 1); + + for i in 0..r { + add_referendum::(i); + } + + for (key, mut info) in ReferendumInfoOf::::iter() { + if let ReferendumInfo::Ongoing(ref mut status) = info { + status.end += 100u32.into(); + } + ReferendumInfoOf::::insert(key, info); + } + + assert_eq!(Democracy::::referendum_count(), r, "referenda not created"); + assert_eq!(Democracy::::lowest_unbaked(), 0, "invalid referenda init"); + + let block_number = T::LaunchPeriod::get(); + + }: { Democracy::::on_initialize(block_number) } + verify { + // All should be on going + for i in 0 .. r { + if let Some(value) = ReferendumInfoOf::::get(i) { + match value { + ReferendumInfo::Finished { .. } => return Err("Referendum has been finished".into()), + ReferendumInfo::Ongoing(_) => (), + } + } + } + } + + delegate { + let r in 0 .. (T::MaxVotes::get() - 1); + + let initial_balance: BalanceOf = 100u32.into(); + let delegated_balance: BalanceOf = 1000u32.into(); + + let caller = funded_account::("caller", 0); + // Caller will initially delegate to `old_delegate` + let old_delegate: T::AccountId = funded_account::("old_delegate", r); + let old_delegate_lookup = T::Lookup::unlookup(old_delegate.clone()); + Democracy::::delegate( + RawOrigin::Signed(caller.clone()).into(), + old_delegate_lookup, + Conviction::Locked1x, + delegated_balance, + )?; + let (target, balance) = match VotingOf::::get(&caller) { + Voting::Delegating { target, balance, .. } => (target, balance), + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(target, old_delegate, "delegation target didn't work"); + assert_eq!(balance, delegated_balance, "delegation balance didn't work"); + // Caller will now switch to `new_delegate` + let new_delegate: T::AccountId = funded_account::("new_delegate", r); + let new_delegate_lookup = T::Lookup::unlookup(new_delegate.clone()); + let account_vote = account_vote::(initial_balance); + // We need to create existing direct votes for the `new_delegate` + for i in 0..r { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(new_delegate.clone()).into(), ref_index, account_vote)?; + } + let votes = match VotingOf::::get(&new_delegate) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r as usize, "Votes were not recorded."); + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller.clone()), new_delegate_lookup, Conviction::Locked1x, delegated_balance) + verify { + let (target, balance) = match VotingOf::::get(&caller) { + Voting::Delegating { target, balance, .. } => (target, balance), + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(target, new_delegate, "delegation target didn't work"); + assert_eq!(balance, delegated_balance, "delegation balance didn't work"); + let delegations = match VotingOf::::get(&new_delegate) { + Voting::Direct { delegations, .. } => delegations, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(delegations.capital, delegated_balance, "delegation was not recorded."); + } + + undelegate { + let r in 0 .. (T::MaxVotes::get() - 1); + + let initial_balance: BalanceOf = 100u32.into(); + let delegated_balance: BalanceOf = 1000u32.into(); + + let caller = funded_account::("caller", 0); + // Caller will delegate + let the_delegate: T::AccountId = funded_account::("delegate", r); + let the_delegate_lookup = T::Lookup::unlookup(the_delegate.clone()); + Democracy::::delegate( + RawOrigin::Signed(caller.clone()).into(), + the_delegate_lookup, + Conviction::Locked1x, + delegated_balance, + )?; + let (target, balance) = match VotingOf::::get(&caller) { + Voting::Delegating { target, balance, .. } => (target, balance), + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(target, the_delegate, "delegation target didn't work"); + assert_eq!(balance, delegated_balance, "delegation balance didn't work"); + // We need to create votes direct votes for the `delegate` + let account_vote = account_vote::(initial_balance); + for i in 0..r { + let ref_index = add_referendum::(i).0; + Democracy::::vote( + RawOrigin::Signed(the_delegate.clone()).into(), + ref_index, + account_vote + )?; + } + let votes = match VotingOf::::get(&the_delegate) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r as usize, "Votes were not recorded."); + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller.clone())) + verify { + // Voting should now be direct + match VotingOf::::get(&caller) { + Voting::Direct { .. } => (), + _ => return Err("undelegation failed".into()), + } + } + + clear_public_proposals { + add_proposal::(0)?; + + }: _(RawOrigin::Root) + + // Test when unlock will remove locks + unlock_remove { + let r in 0 .. (T::MaxVotes::get() - 1); + + let locker = funded_account::("locker", 0); + let locker_lookup = T::Lookup::unlookup(locker.clone()); + // Populate votes so things are locked + let base_balance: BalanceOf = 100u32.into(); + let small_vote = account_vote::(base_balance); + // Vote and immediately unvote + for i in 0 .. r { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(locker.clone()).into(), ref_index, small_vote)?; + Democracy::::remove_vote(RawOrigin::Signed(locker.clone()).into(), ref_index)?; + } + + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + }: unlock(RawOrigin::Signed(caller), locker_lookup) + verify { + // Note that we may want to add a `get_lock` api to actually verify + let voting = VotingOf::::get(&locker); + assert_eq!(voting.locked_balance(), BalanceOf::::zero()); + } + + // Test when unlock will set a new value + unlock_set { + let r in 0 .. (T::MaxVotes::get() - 1); + + let locker = funded_account::("locker", 0); + let locker_lookup = T::Lookup::unlookup(locker.clone()); + // Populate votes so things are locked + let base_balance: BalanceOf = 100u32.into(); + let small_vote = account_vote::(base_balance); + for i in 0 .. r { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(locker.clone()).into(), ref_index, small_vote)?; + } + + // Create a big vote so lock increases + let big_vote = account_vote::(base_balance * 10u32.into()); + let ref_index = add_referendum::(r).0; + Democracy::::vote(RawOrigin::Signed(locker.clone()).into(), ref_index, big_vote)?; + + let votes = match VotingOf::::get(&locker) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), (r + 1) as usize, "Votes were not recorded."); + + let voting = VotingOf::::get(&locker); + assert_eq!(voting.locked_balance(), base_balance * 10u32.into()); + + Democracy::::remove_vote(RawOrigin::Signed(locker.clone()).into(), ref_index)?; + + let caller = funded_account::("caller", 0); + whitelist_account!(caller); + }: unlock(RawOrigin::Signed(caller), locker_lookup) + verify { + let votes = match VotingOf::::get(&locker) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r as usize, "Vote was not removed"); + + let voting = VotingOf::::get(&locker); + // Note that we may want to add a `get_lock` api to actually verify + assert_eq!(voting.locked_balance(), if r > 0 { base_balance } else { 0u32.into() }); + } + + remove_vote { + let r in 1 .. T::MaxVotes::get(); + + let caller = funded_account::("caller", 0); + let account_vote = account_vote::(100u32.into()); + + for i in 0 .. r { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(caller.clone()).into(), ref_index, account_vote)?; + } + + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r as usize, "Votes not created"); + + let ref_index = r - 1; + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller.clone()), ref_index) + verify { + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), (r - 1) as usize, "Vote was not removed"); + } + + // Worst case is when target == caller and referendum is ongoing + remove_other_vote { + let r in 1 .. T::MaxVotes::get(); + + let caller = funded_account::("caller", r); + let caller_lookup = T::Lookup::unlookup(caller.clone()); + let account_vote = account_vote::(100u32.into()); + + for i in 0 .. r { + let ref_index = add_referendum::(i).0; + Democracy::::vote(RawOrigin::Signed(caller.clone()).into(), ref_index, account_vote)?; + } + + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), r as usize, "Votes not created"); + + let ref_index = r - 1; + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller.clone()), caller_lookup, ref_index) + verify { + let votes = match VotingOf::::get(&caller) { + Voting::Direct { votes, .. } => votes, + _ => return Err("Votes are not direct".into()), + }; + assert_eq!(votes.len(), (r - 1) as usize, "Vote was not removed"); + } + + set_external_metadata { + let origin = T::ExternalOrigin::try_successful_origin() + .expect("ExternalOrigin has no successful origin required for the benchmark"); + assert_ok!( + Democracy::::external_propose(origin.clone(), make_proposal::(0)) + ); + let owner = MetadataOwner::External; + let hash = note_preimage::(); + }: set_metadata(origin, owner.clone(), Some(hash)) + verify { + assert_last_event::(crate::Event::MetadataSet { + owner, + hash, + }.into()); + } + + clear_external_metadata { + let origin = T::ExternalOrigin::try_successful_origin() + .expect("ExternalOrigin has no successful origin required for the benchmark"); + assert_ok!( + Democracy::::external_propose(origin.clone(), make_proposal::(0)) + ); + let owner = MetadataOwner::External; + let proposer = funded_account::("proposer", 0); + let hash = note_preimage::(); + assert_ok!(Democracy::::set_metadata(origin.clone(), owner.clone(), Some(hash))); + }: set_metadata(origin, owner.clone(), None) + verify { + assert_last_event::(crate::Event::MetadataCleared { + owner, + hash, + }.into()); + } + + set_proposal_metadata { + // Place our proposal at the end to make sure it's worst case. + for i in 0 .. T::MaxProposals::get() { + add_proposal::(i)?; + } + let owner = MetadataOwner::Proposal(0); + let proposer = funded_account::("proposer", 0); + let hash = note_preimage::(); + }: set_metadata(RawOrigin::Signed(proposer).into(), owner.clone(), Some(hash)) + verify { + assert_last_event::(crate::Event::MetadataSet { + owner, + hash, + }.into()); + } + + clear_proposal_metadata { + // Place our proposal at the end to make sure it's worst case. + for i in 0 .. T::MaxProposals::get() { + add_proposal::(i)?; + } + let proposer = funded_account::("proposer", 0); + let owner = MetadataOwner::Proposal(0); + let hash = note_preimage::(); + assert_ok!(Democracy::::set_metadata( + RawOrigin::Signed(proposer.clone()).into(), + owner.clone(), + Some(hash))); + }: set_metadata(RawOrigin::Signed(proposer).into(), owner.clone(), None) + verify { + assert_last_event::(crate::Event::MetadataCleared { + owner, + hash, + }.into()); + } + + set_referendum_metadata { + // create not ongoing referendum. + ReferendumInfoOf::::insert( + 0, + ReferendumInfo::Finished { end: BlockNumberFor::::zero(), approved: true }, + ); + let owner = MetadataOwner::Referendum(0); + let caller = funded_account::("caller", 0); + let hash = note_preimage::(); + }: set_metadata(RawOrigin::Root.into(), owner.clone(), Some(hash)) + verify { + assert_last_event::(crate::Event::MetadataSet { + owner, + hash, + }.into()); + } + + clear_referendum_metadata { + // create not ongoing referendum. + ReferendumInfoOf::::insert( + 0, + ReferendumInfo::Finished { end: BlockNumberFor::::zero(), approved: true }, + ); + let owner = MetadataOwner::Referendum(0); + let hash = note_preimage::(); + MetadataOf::::insert(owner.clone(), hash); + let caller = funded_account::("caller", 0); + }: set_metadata(RawOrigin::Signed(caller).into(), owner.clone(), None) + verify { + assert_last_event::(crate::Event::MetadataCleared { + owner, + hash, + }.into()); + } + + impl_benchmark_test_suite!( + Democracy, + crate::tests::new_test_ext(), + crate::tests::Test + ); +} diff --git a/pallets/democracy/src/conviction.rs b/pallets/democracy/src/conviction.rs new file mode 100644 index 000000000..26bffb002 --- /dev/null +++ b/pallets/democracy/src/conviction.rs @@ -0,0 +1,112 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The conviction datatype. + +use crate::types::Delegations; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Bounded, CheckedDiv, CheckedMul, Zero}, + RuntimeDebug, +}; +use sp_std::{prelude::*, result::Result}; + +/// A value denoting the strength of conviction of a vote. +#[derive(Encode, MaxEncodedLen, Decode, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo)] +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/pallets/democracy/src/lib.rs b/pallets/democracy/src/lib.rs new file mode 100644 index 000000000..6c361d6fe --- /dev/null +++ b/pallets/democracy/src/lib.rs @@ -0,0 +1,1664 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! # Democracy Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! The Democracy pallet handles the administration of general stakeholder voting. +//! +//! There are two different queues that a proposal can be added to before it +//! becomes a referendum, 1) the proposal queue consisting of all public proposals +//! and 2) the external queue consisting of a single proposal that originates +//! from one of the _external_ origins (such as a collective group). +//! +//! Every launch period - a length defined in the runtime - the Democracy pallet +//! launches a referendum from a proposal that it takes from either the proposal +//! queue or the external queue in turn. Any token holder in the system can vote +//! on referenda. The voting system +//! uses time-lock voting by allowing the token holder to set their _conviction_ +//! behind a vote. The conviction will dictate the length of time the tokens +//! will be locked, as well as the multiplier that scales the vote power. +//! +//! ### Terminology +//! +//! - **Enactment Period:** The minimum period of locking and the period between a proposal being +//! approved and enacted. +//! - **Lock Period:** A period of time after proposal enactment that the tokens of _winning_ voters +//! will be locked. +//! - **Conviction:** An indication of a voter's strength of belief in their vote. An increase +//! of one in conviction indicates that a token holder is willing to lock their tokens for twice +//! as many lock periods after enactment. +//! - **Vote:** A value that can either be in approval ("Aye") or rejection ("Nay") of a particular +//! referendum. +//! - **Proposal:** A submission to the chain that represents an action that a proposer (either an +//! account or an external origin) suggests that the system adopt. +//! - **Referendum:** A proposal that is in the process of being voted on for either acceptance or +//! rejection as a change to the system. +//! - **Delegation:** The act of granting your voting power to the decisions of another account for +//! up to a certain conviction. +//! +//! ### Adaptive Quorum Biasing +//! +//! A _referendum_ can be either simple majority-carries in which 50%+1 of the +//! votes decide the outcome or _adaptive quorum biased_. Adaptive quorum biasing +//! makes the threshold for passing or rejecting a referendum higher or lower +//! depending on how the referendum was originally proposed. There are two types of +//! adaptive quorum biasing: 1) _positive turnout bias_ makes a referendum +//! require a super-majority to pass that decreases as turnout increases and +//! 2) _negative turnout bias_ makes a referendum require a super-majority to +//! reject that decreases as turnout increases. Another way to think about the +//! quorum biasing is that _positive bias_ referendums will be rejected by +//! default and _negative bias_ referendums get passed by default. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! #### Public +//! +//! These calls can be made from any externally held account capable of creating +//! a signed extrinsic. +//! +//! Basic actions: +//! - `propose` - Submits a sensitive action, represented as a hash. Requires a deposit. +//! - `second` - Signals agreement with a proposal, moves it higher on the proposal queue, and +//! requires a matching deposit to the original. +//! - `vote` - Votes in a referendum, either the vote is "Aye" to enact the proposal or "Nay" to +//! keep the status quo. +//! - `unvote` - Cancel a previous vote, this must be done by the voter before the vote ends. +//! - `delegate` - Delegates the voting power (tokens * conviction) to another account. +//! - `undelegate` - Stops the delegation of voting power to another account. +//! +//! Administration actions that can be done to any account: +//! - `reap_vote` - Remove some account's expired votes. +//! - `unlock` - Redetermine the account's balance lock, potentially making tokens available. +//! +//! Preimage actions: +//! - `note_preimage` - Registers the preimage for an upcoming proposal, requires a deposit that is +//! returned once the proposal is enacted. +//! - `note_preimage_operational` - same but provided by `T::OperationalPreimageOrigin`. +//! - `note_imminent_preimage` - Registers the preimage for an upcoming proposal. Does not require a +//! deposit, but the proposal must be in the dispatch queue. +//! - `note_imminent_preimage_operational` - same but provided by `T::OperationalPreimageOrigin`. +//! - `reap_preimage` - Removes the preimage for an expired proposal. Will only work under the +//! condition that it's the same account that noted it and after the voting period, OR it's a +//! different account after the enactment period. +//! +//! #### Cancellation Origin +//! +//! This call can only be made by the `CancellationOrigin`. +//! +//! - `emergency_cancel` - Schedules an emergency cancellation of a referendum. Can only happen once +//! to a specific referendum. +//! +//! #### ExternalOrigin +//! +//! This call can only be made by the `ExternalOrigin`. +//! +//! - `external_propose` - Schedules a proposal to become a referendum once it is is legal for an +//! externally proposed referendum. +//! +//! #### External Majority Origin +//! +//! This call can only be made by the `ExternalMajorityOrigin`. +//! +//! - `external_propose_majority` - Schedules a proposal to become a majority-carries referendum +//! once it is legal for an externally proposed referendum. +//! +//! #### External Default Origin +//! +//! This call can only be made by the `ExternalDefaultOrigin`. +//! +//! - `external_propose_default` - Schedules a proposal to become a negative-turnout-bias referendum +//! once it is legal for an externally proposed referendum. +//! +//! #### Fast Track Origin +//! +//! This call can only be made by the `FastTrackOrigin`. +//! +//! - `fast_track` - Schedules the current externally proposed proposal that is "majority-carries" +//! to become a referendum immediately. +//! +//! #### Veto Origin +//! +//! This call can only be made by the `VetoOrigin`. +//! +//! - `veto_external` - Vetoes and blacklists the external proposal hash. +//! +//! #### Root +//! +//! - `cancel_referendum` - Removes a referendum. +//! - `cancel_queued` - Cancels a proposal that is queued for enactment. +//! - `clear_public_proposal` - Removes all public proposals. + +#![recursion_limit = "256"] +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + ensure, + error::BadOrigin, + traits::{ + defensive_prelude::*, + fungible::{Credit, Inspect, MutateFreeze, MutateHold}, + schedule::{v3::Named as ScheduleNamed, DispatchTime}, + tokens::{imbalance::OnUnbalanced, Precision}, + Bounded, EnsureOrigin, Get, Hash as PreimageHash, LockIdentifier, QueryPreimage, StorePreimage, + }, + weights::Weight, +}; +use frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; +use parity_scale_codec::{Decode, Encode}; +use sp_runtime::{ + traits::{Bounded as ArithBounded, One, Saturating, StaticLookup, Zero}, + ArithmeticError, DispatchError, DispatchResult, +}; + +use sp_std::prelude::*; + +mod conviction; +mod types; +mod vote; +mod vote_threshold; +pub mod weights; +pub use conviction::Conviction; +pub use pallet::*; +pub use types::{ + Delegations, MetadataOwner, PropIndex, ReferendumIndex, ReferendumInfo, ReferendumStatus, Tally, UnvoteScope, +}; +pub use vote::{AccountVote, Vote, Voting}; +pub use vote_threshold::{Approved, VoteThreshold}; +pub use weights::WeightInfo; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +pub(crate) const DEMOCRACY_ID: LockIdentifier = *b"democrac"; + +type BalanceOf = <::Fungible as Inspect<::AccountId>>::Balance; +pub type CreditOf = Credit<::AccountId, ::Fungible>; + +pub type CallOf = ::RuntimeCall; +pub type BoundedCallOf = Bounded>; +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +#[frame_support::pallet] +pub mod pallet { + use super::{DispatchResult, *}; + use frame_support::{ + pallet_prelude::*, + traits::{ + fungible, + fungible::{BalancedHold, MutateHold}, + }, + }; + use frame_system::pallet_prelude::*; + use sp_core::H256; + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + type WeightInfo: WeightInfo; + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The Scheduler. + type Scheduler: ScheduleNamed, CallOf, Self::PalletsOrigin>; + + /// The Preimage provider. + type Preimages: QueryPreimage + StorePreimage; + + /// The overarching hold reason. + type RuntimeHoldReason: From; + + /// The overarching freeze reason. + type RuntimeFreezeReason: From; + + /// The fungible trait. + type Fungible: fungible::Inspect + + fungible::Mutate + + fungible::MutateFreeze + + fungible::MutateHold + + fungible::hold::Balanced; + + /// The period between a proposal being approved and enacted. + /// + /// It should generally be a little more than the unstake period to ensure that + /// voting stakers have an opportunity to remove themselves from the system in the case + /// where they are on the losing side of a vote. + #[pallet::constant] + type EnactmentPeriod: Get>; + + /// How often (in blocks) new public referenda are launched. + #[pallet::constant] + type LaunchPeriod: Get>; + + /// How often (in blocks) to check for new votes. + #[pallet::constant] + type VotingPeriod: 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>; + + /// The minimum amount to be used as a deposit for a public referendum proposal. + #[pallet::constant] + type MinimumDeposit: Get>; + + /// Indicator for whether an emergency origin is even allowed to happen. Some chains may + /// want to set this permanently to `false`, others may want to condition it on things such + /// as an upgrade having happened recently. + #[pallet::constant] + type InstantAllowed: Get; + + /// Minimum voting period allowed for a fast-track referendum. + #[pallet::constant] + type FastTrackVotingPeriod: Get>; + + /// Period in blocks where an external proposal may not be re-submitted after being vetoed. + #[pallet::constant] + type CooloffPeriod: Get>; + + /// The maximum number of votes for an account. + /// + /// Also used to compute weight, an overly big value can + /// lead to extrinsic with very big weight: see `delegate` for instance. + #[pallet::constant] + type MaxVotes: Get; + + /// The maximum number of public proposals that can exist at any time. + #[pallet::constant] + type MaxProposals: Get; + + /// The maximum number of deposits a public proposal may have at any time. + #[pallet::constant] + type MaxDeposits: Get; + + /// The maximum number of items which can be blacklisted. + #[pallet::constant] + type MaxBlacklisted: Get; + + /// Origin from which the next tabled referendum may be forced. This is a normal + /// "super-majority-required" referendum. + type ExternalOrigin: EnsureOrigin; + + /// Origin from which the next tabled referendum may be forced; this allows for the tabling + /// of a majority-carries referendum. + type ExternalMajorityOrigin: EnsureOrigin; + + /// Origin from which the next tabled referendum may be forced; this allows for the tabling + /// of a negative-turnout-bias (default-carries) referendum. + type ExternalDefaultOrigin: EnsureOrigin; + + /// Origin from which the new proposal can be made. + /// + /// The success variant is the account id of the depositor. + type SubmitOrigin: EnsureOrigin; + + /// Origin from which the next majority-carries (or more permissive) referendum may be + /// tabled to vote according to the `FastTrackVotingPeriod` asynchronously in a similar + /// manner to the emergency origin. It retains its threshold method. + type FastTrackOrigin: EnsureOrigin; + + /// Origin from which the next majority-carries (or more permissive) referendum may be + /// tabled to vote immediately and asynchronously in a similar manner to the emergency + /// origin. It retains its threshold method. + type InstantOrigin: EnsureOrigin; + + /// Origin from which any referendum may be cancelled in an emergency. + type CancellationOrigin: EnsureOrigin; + + /// Origin from which proposals may be blacklisted. + type BlacklistOrigin: EnsureOrigin; + + /// Origin from which a proposal may be cancelled and its backers slashed. + type CancelProposalOrigin: EnsureOrigin; + + /// Origin for anyone able to veto proposals. + type VetoOrigin: EnsureOrigin; + + /// Overarching type of all pallets origins. + type PalletsOrigin: From>; + + /// Handler for the unbalanced reduction when slashing a preimage deposit. + type Slash: OnUnbalanced>; + } + + /// The number of (public) proposals that have been made so far. + #[pallet::storage] + #[pallet::getter(fn public_prop_count)] + pub type PublicPropCount = StorageValue<_, PropIndex, ValueQuery>; + + /// The public proposals. Unsorted. The second item is the proposal. + #[pallet::storage] + #[pallet::getter(fn public_props)] + pub type PublicProps = + StorageValue<_, BoundedVec<(PropIndex, BoundedCallOf, T::AccountId), T::MaxProposals>, ValueQuery>; + + /// Those who have locked a deposit. + /// + /// TWOX-NOTE: Safe, as increasing integer keys are safe. + #[pallet::storage] + #[pallet::getter(fn deposit_of)] + pub type DepositOf = + StorageMap<_, Twox64Concat, PropIndex, (BoundedVec, BalanceOf)>; + + /// The next free referendum index, aka the number of referenda started so far. + #[pallet::storage] + #[pallet::getter(fn referendum_count)] + pub type ReferendumCount = StorageValue<_, ReferendumIndex, ValueQuery>; + + /// The lowest referendum index representing an unbaked referendum. Equal to + /// `ReferendumCount` if there isn't a unbaked referendum. + #[pallet::storage] + #[pallet::getter(fn lowest_unbaked)] + pub type LowestUnbaked = StorageValue<_, ReferendumIndex, ValueQuery>; + + /// Information concerning any given referendum. + /// + /// TWOX-NOTE: SAFE as indexes are not under an attacker’s control. + #[pallet::storage] + #[pallet::getter(fn referendum_info)] + pub type ReferendumInfoOf = + StorageMap<_, Twox64Concat, ReferendumIndex, ReferendumInfo, BoundedCallOf, BalanceOf>>; + + /// All votes for a particular voter. We store the balance for the number of votes that we + /// have recorded. The second item is the total amount of delegations, that will be added. + /// + /// TWOX-NOTE: SAFE as `AccountId`s are crypto hashes anyway. + #[pallet::storage] + pub type VotingOf = StorageMap< + _, + Twox64Concat, + T::AccountId, + Voting, T::AccountId, BlockNumberFor, T::MaxVotes>, + ValueQuery, + >; + + /// True if the last referendum tabled was submitted externally. False if it was a public + /// proposal. + #[pallet::storage] + pub type LastTabledWasExternal = StorageValue<_, bool, ValueQuery>; + + /// The referendum to be tabled whenever it would be valid to table an external proposal. + /// This happens when a referendum needs to be tabled and one of two conditions are met: + /// - `LastTabledWasExternal` is `false`; or + /// - `PublicProps` is empty. + #[pallet::storage] + pub type NextExternal = StorageValue<_, (BoundedCallOf, VoteThreshold)>; + + /// A record of who vetoed what. Maps proposal hash to a possible existent block number + /// (until when it may not be resubmitted) and who vetoed it. + #[pallet::storage] + pub type Blacklist = + StorageMap<_, Identity, H256, (BlockNumberFor, BoundedVec)>; + + /// Record of all proposals that have been subject to emergency cancellation. + #[pallet::storage] + pub type Cancellations = StorageMap<_, Identity, H256, bool, ValueQuery>; + + /// General information concerning any proposal or referendum. + /// The `PreimageHash` refers to the preimage of the `Preimages` provider which can be a JSON + /// dump or IPFS hash of a JSON file. + /// + /// Consider a garbage collection for a metadata of finished referendums to `unrequest` (remove) + /// large preimages. + #[pallet::storage] + pub type MetadataOf = StorageMap<_, Blake2_128Concat, MetadataOwner, PreimageHash>; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + #[serde(skip)] + _config: sp_std::marker::PhantomData, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + PublicPropCount::::put(0 as PropIndex); + ReferendumCount::::put(0 as ReferendumIndex); + LowestUnbaked::::put(0 as ReferendumIndex); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A motion has been proposed by a public account. + Proposed { proposal_index: PropIndex, deposit: BalanceOf }, + /// A public proposal has been tabled for referendum vote. + Tabled { proposal_index: PropIndex, deposit: BalanceOf }, + /// An external proposal has been tabled. + ExternalTabled, + /// A referendum has begun. + Started { ref_index: ReferendumIndex, threshold: VoteThreshold }, + /// A proposal has been approved by referendum. + Passed { ref_index: ReferendumIndex }, + /// A proposal has been rejected by referendum. + NotPassed { ref_index: ReferendumIndex }, + /// A referendum has been cancelled. + Cancelled { ref_index: ReferendumIndex }, + /// An account has delegated their vote to another account. + Delegated { who: T::AccountId, target: T::AccountId }, + /// An account has cancelled a previous delegation operation. + Undelegated { account: T::AccountId }, + /// An external proposal has been vetoed. + Vetoed { who: T::AccountId, proposal_hash: PreimageHash, until: BlockNumberFor }, + /// A proposal_hash has been blacklisted permanently. + Blacklisted { proposal_hash: PreimageHash }, + /// An account has voted in a referendum + Voted { voter: T::AccountId, ref_index: ReferendumIndex, vote: AccountVote> }, + /// An account has secconded a proposal + Seconded { seconder: T::AccountId, prop_index: PropIndex }, + /// A proposal got canceled. + ProposalCanceled { prop_index: PropIndex }, + /// Metadata for a proposal or a referendum has been set. + MetadataSet { + /// Metadata owner. + owner: MetadataOwner, + /// Preimage hash. + hash: PreimageHash, + }, + /// Metadata for a proposal or a referendum has been cleared. + MetadataCleared { + /// Metadata owner. + owner: MetadataOwner, + /// Preimage hash. + hash: PreimageHash, + }, + /// Metadata has been transferred to new owner. + MetadataTransferred { + /// Previous metadata owner. + prev_owner: MetadataOwner, + /// New metadata owner. + owner: MetadataOwner, + /// Preimage hash. + hash: PreimageHash, + }, + } + + /// A reason for this pallet placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// Funds are held when a proposal is submitted and are released when the proposal is + /// tabled. If the proposal is vetoed and blacklisted, the deposit is slashed. + Proposal, + } + + /// A reason for this pallet placing a freeze on funds. + #[pallet::composite_enum] + pub enum FreezeReason { + /// Funds are frozen upon casting a vote. They are subsequently unfrozen either + /// when the vote is cancelled or after the specified lock period has elapsed (see + /// [`conviction::Conviction`]). + Vote, + } + + #[pallet::error] + pub enum Error { + /// Value too low + ValueLow, + /// Proposal does not exist + ProposalMissing, + /// Cannot cancel the same proposal twice + AlreadyCanceled, + /// Proposal already made + DuplicateProposal, + /// Proposal still blacklisted + ProposalBlacklisted, + /// Next external proposal not simple majority + NotSimpleMajority, + /// Invalid hash + InvalidHash, + /// No external proposal + NoProposal, + /// Identity may not veto a proposal twice + AlreadyVetoed, + /// Vote given for invalid referendum + ReferendumInvalid, + /// No proposals waiting + NoneWaiting, + /// The given account did not vote on the referendum. + NotVoter, + /// The actor has no permission to conduct the action. + NoPermission, + /// The account is already delegating. + AlreadyDelegating, + /// Too high a balance was provided that the account cannot afford. + InsufficientFunds, + /// The account is not currently delegating. + NotDelegating, + /// The account currently has votes attached to it and the operation cannot succeed until + /// these are removed, either through `unvote` or `reap_vote`. + VotesExist, + /// The instant referendum origin is currently disallowed. + InstantNotAllowed, + /// Delegation to oneself makes no sense. + Nonsense, + /// Invalid upper bound. + WrongUpperBound, + /// Maximum number of votes reached. + MaxVotesReached, + /// Maximum number of items reached. + TooMany, + /// Voting period too low + VotingPeriodLow, + /// The preimage does not exist. + PreimageNotExist, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Weight: see `begin_block` + fn on_initialize(n: BlockNumberFor) -> Weight { + Self::begin_block(n) + } + } + + #[pallet::call] + impl Pallet { + /// Propose a sensitive action to be taken. + /// + /// The dispatch origin of this call must be _Signed_ and the sender must + /// have funds to cover the deposit. + /// + /// - `proposal_hash`: The hash of the proposal preimage. + /// - `value`: The amount of deposit (must be at least `MinimumDeposit`). + /// + /// Emits `Proposed`. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::propose())] + pub fn propose( + origin: OriginFor, + proposal: BoundedCallOf, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResult { + let who = T::SubmitOrigin::ensure_origin(origin)?; + ensure!(value >= T::MinimumDeposit::get(), Error::::ValueLow); + + let index = Self::public_prop_count(); + let real_prop_count = PublicProps::::decode_len().unwrap_or(0) as u32; + let max_proposals = T::MaxProposals::get(); + ensure!(real_prop_count < max_proposals, Error::::TooMany); + let proposal_hash = proposal.hash(); + + if let Some((until, _)) = >::get(proposal_hash) { + ensure!(>::block_number() >= until, Error::::ProposalBlacklisted,); + } + + T::Fungible::hold(&HoldReason::Proposal.into(), &who, value)?; + + let depositors = BoundedVec::<_, T::MaxDeposits>::truncate_from(vec![who.clone()]); + DepositOf::::insert(index, (depositors, value)); + + PublicPropCount::::put(index + 1); + + PublicProps::::try_append((index, proposal, who)).map_err(|_| Error::::TooMany)?; + + Self::deposit_event(Event::::Proposed { proposal_index: index, deposit: value }); + Ok(()) + } + + /// Signals agreement with a particular proposal. + /// + /// The dispatch origin of this call must be _Signed_ and the sender + /// must have funds to cover the deposit, equal to the original deposit. + /// + /// - `proposal`: The index of the proposal to second. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::second())] + pub fn second(origin: OriginFor, #[pallet::compact] proposal: PropIndex) -> DispatchResult { + let who = ensure_signed(origin)?; + + let seconds = Self::len_of_deposit_of(proposal).ok_or(Error::::ProposalMissing)?; + ensure!(seconds < T::MaxDeposits::get(), Error::::TooMany); + let mut deposit = Self::deposit_of(proposal).ok_or(Error::::ProposalMissing)?; + T::Fungible::hold(&HoldReason::Proposal.into(), &who, deposit.1)?; + let ok = deposit.0.try_push(who.clone()).is_ok(); + debug_assert!(ok, "`seconds` is below static limit; `try_insert` should succeed; qed"); + >::insert(proposal, deposit); + Self::deposit_event(Event::::Seconded { seconder: who, prop_index: proposal }); + Ok(()) + } + + /// Vote in a referendum. 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_. + /// + /// - `ref_index`: The index of the referendum to vote for. + /// - `vote`: The vote configuration. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::vote_new().max(T::WeightInfo::vote_existing()))] + pub fn vote( + origin: OriginFor, + #[pallet::compact] ref_index: ReferendumIndex, + vote: AccountVote>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::try_vote(&who, ref_index, vote) + } + + /// Schedule an emergency cancellation of a referendum. Cannot happen twice to the same + /// referendum. + /// + /// The dispatch origin of this call must be `CancellationOrigin`. + /// + /// -`ref_index`: The index of the referendum to cancel. + /// + /// Weight: `O(1)`. + #[pallet::call_index(3)] + #[pallet::weight((T::WeightInfo::emergency_cancel(), DispatchClass::Operational))] + pub fn emergency_cancel(origin: OriginFor, ref_index: ReferendumIndex) -> DispatchResult { + T::CancellationOrigin::ensure_origin(origin)?; + + let status = Self::referendum_status(ref_index)?; + let h = status.proposal.hash(); + ensure!(!>::contains_key(h), Error::::AlreadyCanceled); + + >::insert(h, true); + Self::internal_cancel_referendum(ref_index); + Ok(()) + } + + /// Schedule a referendum to be tabled once it is legal to schedule an external + /// referendum. + /// + /// The dispatch origin of this call must be `ExternalOrigin`. + /// + /// - `proposal_hash`: The preimage hash of the proposal. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::external_propose())] + pub fn external_propose(origin: OriginFor, proposal: BoundedCallOf) -> DispatchResult { + T::ExternalOrigin::ensure_origin(origin)?; + ensure!(!>::exists(), Error::::DuplicateProposal); + if let Some((until, _)) = >::get(proposal.hash()) { + ensure!(>::block_number() >= until, Error::::ProposalBlacklisted,); + } + >::put((proposal, VoteThreshold::SuperMajorityApprove)); + Ok(()) + } + + /// Schedule a majority-carries referendum to be tabled next once it is legal to schedule + /// an external referendum. + /// + /// The dispatch of this call must be `ExternalMajorityOrigin`. + /// + /// - `proposal_hash`: The preimage hash of the proposal. + /// + /// Unlike `external_propose`, blacklisting has no effect on this and it may replace a + /// pre-scheduled `external_propose` call. + /// + /// Weight: `O(1)` + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::external_propose_majority())] + pub fn external_propose_majority(origin: OriginFor, proposal: BoundedCallOf) -> DispatchResult { + T::ExternalMajorityOrigin::ensure_origin(origin)?; + >::put((proposal, VoteThreshold::SimpleMajority)); + Ok(()) + } + + /// Schedule a negative-turnout-bias referendum to be tabled next once it is legal to + /// schedule an external referendum. + /// + /// The dispatch of this call must be `ExternalDefaultOrigin`. + /// + /// - `proposal_hash`: The preimage hash of the proposal. + /// + /// Unlike `external_propose`, blacklisting has no effect on this and it may replace a + /// pre-scheduled `external_propose` call. + /// + /// Weight: `O(1)` + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::external_propose_default())] + pub fn external_propose_default(origin: OriginFor, proposal: BoundedCallOf) -> DispatchResult { + T::ExternalDefaultOrigin::ensure_origin(origin)?; + >::put((proposal, VoteThreshold::SuperMajorityAgainst)); + Ok(()) + } + + /// Schedule the currently externally-proposed majority-carries referendum to be tabled + /// immediately. If there is no externally-proposed referendum currently, or if there is one + /// but it is not a majority-carries referendum then it fails. + /// + /// The dispatch of this call must be `FastTrackOrigin`. + /// + /// - `proposal_hash`: The hash of the current external proposal. + /// - `voting_period`: The period that is allowed for voting on this proposal. Increased to + /// Must be always greater than zero. + /// For `FastTrackOrigin` must be equal or greater than `FastTrackVotingPeriod`. + /// - `delay`: The number of block after voting has ended in approval and this should be + /// enacted. This doesn't have a minimum amount. + /// + /// Emits `Started`. + /// + /// Weight: `O(1)` + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::fast_track())] + pub fn fast_track( + origin: OriginFor, + proposal_hash: PreimageHash, + voting_period: BlockNumberFor, + delay: BlockNumberFor, + ) -> DispatchResult { + // Rather complicated bit of code to ensure that either: + // - `voting_period` is at least `FastTrackVotingPeriod` and `origin` is + // `FastTrackOrigin`; or + // - `InstantAllowed` is `true` and `origin` is `InstantOrigin`. + let maybe_ensure_instant = if voting_period < T::FastTrackVotingPeriod::get() { + Some(origin) + } else if let Err(origin) = T::FastTrackOrigin::try_origin(origin) { + Some(origin) + } else { + None + }; + if let Some(ensure_instant) = maybe_ensure_instant { + T::InstantOrigin::ensure_origin(ensure_instant)?; + ensure!(T::InstantAllowed::get(), Error::::InstantNotAllowed); + } + + ensure!(voting_period > Zero::zero(), Error::::VotingPeriodLow); + let (ext_proposal, threshold) = >::get().ok_or(Error::::ProposalMissing)?; + ensure!(threshold != VoteThreshold::SuperMajorityApprove, Error::::NotSimpleMajority,); + ensure!(proposal_hash == ext_proposal.hash(), Error::::InvalidHash); + + >::kill(); + let now = >::block_number(); + let ref_index = Self::inject_referendum(now.saturating_add(voting_period), ext_proposal, threshold, delay); + Self::transfer_metadata(MetadataOwner::External, MetadataOwner::Referendum(ref_index)); + Ok(()) + } + + /// Veto and blacklist the external proposal hash. + /// + /// The dispatch origin of this call must be `VetoOrigin`. + /// + /// - `proposal_hash`: The preimage hash of the proposal to veto and blacklist. + /// + /// Emits `Vetoed`. + /// + /// Weight: `O(V + log(V))` where V is number of `existing vetoers` + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::veto_external())] + pub fn veto_external(origin: OriginFor, proposal_hash: PreimageHash) -> DispatchResult { + let who = T::VetoOrigin::ensure_origin(origin)?; + + if let Some((ext_proposal, _)) = NextExternal::::get() { + ensure!(proposal_hash == ext_proposal.hash(), Error::::ProposalMissing); + } else { + return Err(Error::::NoProposal.into()) + } + + let mut existing_vetoers = >::get(&proposal_hash).map(|pair| pair.1).unwrap_or_default(); + let insert_position = existing_vetoers.binary_search(&who).err().ok_or(Error::::AlreadyVetoed)?; + existing_vetoers.try_insert(insert_position, who.clone()).map_err(|_| Error::::TooMany)?; + + let until = >::block_number().saturating_add(T::CooloffPeriod::get()); + >::insert(&proposal_hash, (until, existing_vetoers)); + + Self::deposit_event(Event::::Vetoed { who, proposal_hash, until }); + >::kill(); + Self::clear_metadata(MetadataOwner::External); + Ok(()) + } + + /// Remove a referendum. + /// + /// The dispatch origin of this call must be _Root_. + /// + /// - `ref_index`: The index of the referendum to cancel. + /// + /// # Weight: `O(1)`. + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::cancel_referendum())] + pub fn cancel_referendum( + origin: OriginFor, + #[pallet::compact] ref_index: ReferendumIndex, + ) -> DispatchResult { + ensure_root(origin)?; + Self::internal_cancel_referendum(ref_index); + Ok(()) + } + + /// Delegate the voting power (with some given conviction) of the sending account. + /// + /// 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. + /// - `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 referendums the voter delegating to has + /// voted on. Weight is charged as if maximum votes. + // 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::call_index(10)] + #[pallet::weight(T::WeightInfo::delegate(T::MaxVotes::get()))] + pub fn delegate( + origin: OriginFor, + to: AccountIdLookupOf, + conviction: Conviction, + balance: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let to = T::Lookup::lookup(to)?; + let votes = Self::try_delegate(who, to, conviction, balance)?; + + Ok(Some(T::WeightInfo::delegate(votes)).into()) + } + + /// Undelegate the voting power of the sending account. + /// + /// 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. + /// + /// Emits `Undelegated`. + /// + /// Weight: `O(R)` where R is the number of referendums the voter delegating to has + /// voted on. Weight is charged as if maximum votes. + // 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::call_index(11)] + #[pallet::weight(T::WeightInfo::undelegate(T::MaxVotes::get()))] + pub fn undelegate(origin: OriginFor) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let votes = Self::try_undelegate(who)?; + Ok(Some(T::WeightInfo::undelegate(votes)).into()) + } + + /// Clears all public proposals. + /// + /// The dispatch origin of this call must be _Root_. + /// + /// Weight: `O(1)`. + #[pallet::call_index(12)] + #[pallet::weight(T::WeightInfo::clear_public_proposals())] + pub fn clear_public_proposals(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + >::kill(); + Ok(()) + } + + /// Unlock tokens that have an expired lock. + /// + /// The dispatch origin of this call must be _Signed_. + /// + /// - `target`: The account to remove the lock on. + /// + /// Weight: `O(R)` with R number of vote of target. + #[pallet::call_index(13)] + #[pallet::weight(T::WeightInfo::unlock_set(T::MaxVotes::get()).max(T::WeightInfo::unlock_remove(T::MaxVotes::get())))] + pub fn unlock(origin: OriginFor, target: AccountIdLookupOf) -> DispatchResult { + ensure_signed(origin)?; + let target = T::Lookup::lookup(target)?; + Self::update_lock(&target) + } + + /// Remove a vote for a referendum. + /// + /// If: + /// - the referendum was cancelled, or + /// - the referendum is ongoing, or + /// - the referendum 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 referendum 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 referendum `index`. + /// + /// - `index`: The index of referendum of the vote to be removed. + /// + /// Weight: `O(R + log R)` where R is the number of referenda that `target` has voted on. + /// Weight is calculated for the maximum number of vote. + #[pallet::call_index(14)] + #[pallet::weight(T::WeightInfo::remove_vote(T::MaxVotes::get()))] + pub fn remove_vote(origin: OriginFor, index: ReferendumIndex) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::try_remove_vote(&who, index, UnvoteScope::Any) + } + + /// Remove a vote for a referendum. + /// + /// 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 referendum was cancelled, because the voter lost the referendum 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 + /// referendum `index`. + /// - `index`: The index of referendum of the vote to be removed. + /// + /// Weight: `O(R + log R)` where R is the number of referenda that `target` has voted on. + /// Weight is calculated for the maximum number of vote. + #[pallet::call_index(15)] + #[pallet::weight(T::WeightInfo::remove_other_vote(T::MaxVotes::get()))] + pub fn remove_other_vote( + origin: OriginFor, + target: AccountIdLookupOf, + index: ReferendumIndex, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let target = T::Lookup::lookup(target)?; + let scope = if target == who { UnvoteScope::Any } else { UnvoteScope::OnlyExpired }; + Self::try_remove_vote(&target, index, scope)?; + Ok(()) + } + + /// Permanently place a proposal into the blacklist. This prevents it from ever being + /// proposed again. + /// + /// If called on a queued public or external proposal, then this will result in it being + /// removed. If the `ref_index` supplied is an active referendum with the proposal hash, + /// then it will be cancelled. + /// + /// The dispatch origin of this call must be `BlacklistOrigin`. + /// + /// - `proposal_hash`: The proposal hash to blacklist permanently. + /// - `ref_index`: An ongoing referendum whose hash is `proposal_hash`, which will be + /// cancelled. + /// + /// Weight: `O(p)` (though as this is an high-privilege dispatch, we assume it has a + /// reasonable value). + #[pallet::call_index(16)] + #[pallet::weight((T::WeightInfo::blacklist(), DispatchClass::Operational))] + pub fn blacklist( + origin: OriginFor, + proposal_hash: H256, + maybe_ref_index: Option, + ) -> DispatchResult { + T::BlacklistOrigin::ensure_origin(origin)?; + + // Insert the proposal into the blacklist. + let permanent = (BlockNumberFor::::max_value(), BoundedVec::::default()); + Blacklist::::insert(&proposal_hash, permanent); + + // Remove the queued proposal, if it's there. + PublicProps::::mutate(|props| { + if let Some(index) = props.iter().position(|p| p.1.hash() == proposal_hash) { + let (prop_index, ..) = props.remove(index); + if let Some((whos, amount)) = DepositOf::::take(prop_index) { + for who in whos.into_iter() { + T::Slash::on_unbalanced(T::Fungible::slash(&HoldReason::Proposal.into(), &who, amount).0); + } + } + Self::clear_metadata(MetadataOwner::Proposal(prop_index)); + } + }); + + // Remove the external queued referendum, if it's there. + if matches!(NextExternal::::get(), Some((p, ..)) if p.hash() == proposal_hash) { + NextExternal::::kill(); + Self::clear_metadata(MetadataOwner::External); + } + + // Remove the referendum, if it's there. + if let Some(ref_index) = maybe_ref_index { + if let Ok(status) = Self::referendum_status(ref_index) { + if status.proposal.hash() == proposal_hash { + Self::internal_cancel_referendum(ref_index); + } + } + } + + Self::deposit_event(Event::::Blacklisted { proposal_hash }); + Ok(()) + } + + /// Remove a proposal. + /// + /// The dispatch origin of this call must be `CancelProposalOrigin`. + /// + /// - `prop_index`: The index of the proposal to cancel. + /// + /// Weight: `O(p)` where `p = PublicProps::::decode_len()` + #[pallet::call_index(17)] + #[pallet::weight(T::WeightInfo::cancel_proposal())] + pub fn cancel_proposal(origin: OriginFor, #[pallet::compact] prop_index: PropIndex) -> DispatchResult { + T::CancelProposalOrigin::ensure_origin(origin)?; + + PublicProps::::mutate(|props| props.retain(|p| p.0 != prop_index)); + if let Some((whos, amount)) = DepositOf::::take(prop_index) { + for who in whos.into_iter() { + T::Slash::on_unbalanced(T::Fungible::slash(&HoldReason::Proposal.into(), &who, amount).0); + } + } + Self::deposit_event(Event::::ProposalCanceled { prop_index }); + Self::clear_metadata(MetadataOwner::Proposal(prop_index)); + Ok(()) + } + + /// Set or clear a metadata of a proposal or a referendum. + /// + /// Parameters: + /// - `origin`: Must correspond to the `MetadataOwner`. + /// - `ExternalOrigin` for an external proposal with the `SuperMajorityApprove` + /// threshold. + /// - `ExternalDefaultOrigin` for an external proposal with the `SuperMajorityAgainst` + /// threshold. + /// - `ExternalMajorityOrigin` for an external proposal with the `SimpleMajority` + /// threshold. + /// - `Signed` by a creator for a public proposal. + /// - `Signed` to clear a metadata for a finished referendum. + /// - `Root` to set a metadata for an ongoing referendum. + /// - `owner`: an identifier of a metadata owner. + /// - `maybe_hash`: The hash of an on-chain stored preimage. `None` to clear a metadata. + #[pallet::call_index(18)] + #[pallet::weight( + match (owner, maybe_hash) { + (MetadataOwner::External, Some(_)) => T::WeightInfo::set_external_metadata(), + (MetadataOwner::External, None) => T::WeightInfo::clear_external_metadata(), + (MetadataOwner::Proposal(_), Some(_)) => T::WeightInfo::set_proposal_metadata(), + (MetadataOwner::Proposal(_), None) => T::WeightInfo::clear_proposal_metadata(), + (MetadataOwner::Referendum(_), Some(_)) => T::WeightInfo::set_referendum_metadata(), + (MetadataOwner::Referendum(_), None) => T::WeightInfo::clear_referendum_metadata(), + } + )] + pub fn set_metadata(origin: OriginFor, owner: MetadataOwner, maybe_hash: Option) -> DispatchResult { + match owner { + MetadataOwner::External => { + let (_, threshold) = >::get().ok_or(Error::::NoProposal)?; + Self::ensure_external_origin(threshold, origin)?; + }, + MetadataOwner::Proposal(index) => { + let who = ensure_signed(origin)?; + let (_, _, proposer) = Self::proposal(index)?; + ensure!(proposer == who, Error::::NoPermission); + }, + MetadataOwner::Referendum(index) => { + let is_root = ensure_signed_or_root(origin)?.is_none(); + ensure!(is_root || maybe_hash.is_none(), Error::::NoPermission); + ensure!(is_root || Self::referendum_status(index).is_err(), Error::::NoPermission); + }, + } + if let Some(hash) = maybe_hash { + ensure!(T::Preimages::len(&hash).is_some(), Error::::PreimageNotExist); + MetadataOf::::insert(owner.clone(), hash); + Self::deposit_event(Event::::MetadataSet { owner, hash }); + } else { + Self::clear_metadata(owner); + } + Ok(()) + } + } +} + +pub trait EncodeInto: Encode { + fn encode_into + Default>(&self) -> T { + let mut t = T::default(); + self.using_encoded(|data| { + if data.len() <= t.as_mut().len() { + t.as_mut()[0..data.len()].copy_from_slice(data); + } else { + // encoded self is too big to fit into a T. hash it and use the first bytes of that + // instead. + let hash = sp_io::hashing::blake2_256(data); + let l = t.as_mut().len().min(hash.len()); + t.as_mut()[0..l].copy_from_slice(&hash[0..l]); + } + }); + t + } +} +impl EncodeInto for T {} + +impl Pallet { + // exposed immutables. + + /// Get the amount locked in support of `proposal`; `None` if proposal isn't a valid proposal + /// index. + pub fn backing_for(proposal: PropIndex) -> Option> { + Self::deposit_of(proposal).map(|(l, d)| d.saturating_mul((l.len() as u32).into())) + } + + /// Get all referenda ready for tally at block `n`. + pub fn maturing_referenda_at( + n: BlockNumberFor, + ) -> Vec<(ReferendumIndex, ReferendumStatus, BoundedCallOf, BalanceOf>)> { + let next = Self::lowest_unbaked(); + let last = Self::referendum_count(); + Self::maturing_referenda_at_inner(n, next..last) + } + + fn maturing_referenda_at_inner( + n: BlockNumberFor, + range: core::ops::Range, + ) -> Vec<(ReferendumIndex, ReferendumStatus, BoundedCallOf, BalanceOf>)> { + range + .into_iter() + .map(|i| (i, Self::referendum_info(i))) + .filter_map(|(i, maybe_info)| match maybe_info { + Some(ReferendumInfo::Ongoing(status)) => Some((i, status)), + _ => None, + }) + .filter(|(_, status)| status.end == n) + .collect() + } + + // Exposed mutables. + + /// Start a referendum. + pub fn internal_start_referendum( + proposal: BoundedCallOf, + threshold: VoteThreshold, + delay: BlockNumberFor, + ) -> ReferendumIndex { + >::inject_referendum( + >::block_number().saturating_add(T::VotingPeriod::get()), + proposal, + threshold, + delay, + ) + } + + /// Remove a referendum. + pub fn internal_cancel_referendum(ref_index: ReferendumIndex) { + Self::deposit_event(Event::::Cancelled { ref_index }); + ReferendumInfoOf::::remove(ref_index); + Self::clear_metadata(MetadataOwner::Referendum(ref_index)); + } + + // private. + + /// Ok if the given referendum is active, Err otherwise + fn ensure_ongoing( + r: ReferendumInfo, BoundedCallOf, BalanceOf>, + ) -> Result, BoundedCallOf, BalanceOf>, DispatchError> { + match r { + ReferendumInfo::Ongoing(s) => Ok(s), + _ => Err(Error::::ReferendumInvalid.into()), + } + } + + fn referendum_status( + ref_index: ReferendumIndex, + ) -> Result, BoundedCallOf, BalanceOf>, DispatchError> { + let info = ReferendumInfoOf::::get(ref_index).ok_or(Error::::ReferendumInvalid)?; + Self::ensure_ongoing(info) + } + + /// Actually enact a vote, if legit. + fn try_vote(who: &T::AccountId, ref_index: ReferendumIndex, vote: AccountVote>) -> DispatchResult { + let mut status = Self::referendum_status(ref_index)?; + ensure!(vote.balance() <= T::Fungible::total_balance(&who), Error::::InsufficientFunds); + VotingOf::::try_mutate(who, |voting| -> DispatchResult { + if let Voting::Direct { ref mut votes, delegations, .. } = voting { + match votes.binary_search_by_key(&ref_index, |i| i.0) { + Ok(i) => { + // Shouldn't be possible to fail, but we handle it gracefully. + status.tally.remove(votes[i].1).ok_or(ArithmeticError::Underflow)?; + if let Some(approve) = votes[i].1.as_standard() { + status.tally.reduce(approve, *delegations); + } + votes[i].1 = vote; + }, + Err(i) => { + votes.try_insert(i, (ref_index, vote)).map_err(|_| Error::::MaxVotesReached)?; + }, + } + Self::deposit_event(Event::::Voted { voter: who.clone(), ref_index, vote }); + // Shouldn't be possible to fail, but we handle it gracefully. + status.tally.add(vote).ok_or(ArithmeticError::Overflow)?; + if let Some(approve) = vote.as_standard() { + status.tally.increase(approve, *delegations); + } + Ok(()) + } else { + Err(Error::::AlreadyDelegating.into()) + } + })?; + // Extend the lock to `balance` (rather than setting it) since we don't know what other + // votes are in place. + T::Fungible::extend_freeze(&FreezeReason::Vote.into(), who, vote.balance())?; + ReferendumInfoOf::::insert(ref_index, ReferendumInfo::Ongoing(status)); + Ok(()) + } + + /// Remove the account's vote for the given referendum if possible. This is possible when: + /// - The referendum has not finished. + /// - The referendum has finished and the voter lost their direction. + /// - The referendum 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, ref_index: ReferendumIndex, scope: UnvoteScope) -> DispatchResult { + let info = ReferendumInfoOf::::get(ref_index); + VotingOf::::try_mutate(who, |voting| -> DispatchResult { + if let Voting::Direct { ref mut votes, delegations, ref mut prior } = voting { + let i = votes.binary_search_by_key(&ref_index, |i| i.0).map_err(|_| Error::::NotVoter)?; + match info { + Some(ReferendumInfo::Ongoing(mut status)) => { + ensure!(matches!(scope, UnvoteScope::Any), Error::::NoPermission); + // Shouldn't be possible to fail, but we handle it gracefully. + status.tally.remove(votes[i].1).ok_or(ArithmeticError::Underflow)?; + if let Some(approve) = votes[i].1.as_standard() { + status.tally.reduce(approve, *delegations); + } + ReferendumInfoOf::::insert(ref_index, ReferendumInfo::Ongoing(status)); + }, + Some(ReferendumInfo::Finished { end, approved }) => { + if let Some((lock_periods, balance)) = votes[i].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::::NoPermission); + prior.accumulate(unlock_at, balance) + } + } + }, + None => {}, // Referendum was cancelled. + } + votes.remove(i); + } + Ok(()) + })?; + Ok(()) + } + + /// Return the number of votes for `who` + fn increase_upstream_delegation(who: &T::AccountId, amount: Delegations>) -> u32 { + VotingOf::::mutate(who, |voting| match voting { + Voting::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::Direct { votes, delegations, .. } => { + *delegations = delegations.saturating_add(amount); + for &(ref_index, account_vote) in votes.iter() { + if let AccountVote::Standard { vote, .. } = account_vote { + ReferendumInfoOf::::mutate(ref_index, |maybe_info| { + if let Some(ReferendumInfo::Ongoing(ref mut status)) = maybe_info { + status.tally.increase(vote.aye, amount); + } + }); + } + } + votes.len() as u32 + }, + }) + } + + /// Return the number of votes for `who` + fn reduce_upstream_delegation(who: &T::AccountId, amount: Delegations>) -> u32 { + VotingOf::::mutate(who, |voting| match voting { + Voting::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::Direct { votes, delegations, .. } => { + *delegations = delegations.saturating_sub(amount); + for &(ref_index, account_vote) in votes.iter() { + if let AccountVote::Standard { vote, .. } = account_vote { + ReferendumInfoOf::::mutate(ref_index, |maybe_info| { + if let Some(ReferendumInfo::Ongoing(ref mut status)) = maybe_info { + 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, + target: T::AccountId, + conviction: Conviction, + balance: BalanceOf, + ) -> Result { + ensure!(who != target, Error::::Nonsense); + ensure!(balance <= T::Fungible::total_balance(&who), Error::::InsufficientFunds); + + let votes = VotingOf::::try_mutate(&who, |voting| -> Result { + let mut old = Voting::Delegating { + balance, + target: target.clone(), + conviction, + delegations: Default::default(), + prior: Default::default(), + }; + sp_std::mem::swap(&mut old, voting); + match old { + Voting::Delegating { balance, target, conviction, delegations, mut prior, .. } => { + // remove any delegation votes to our current target. + Self::reduce_upstream_delegation(&target, conviction.votes(balance)); + let now = frame_system::Pallet::::block_number(); + let lock_periods = conviction.lock_periods().into(); + let unlock_block = now.saturating_add(T::VoteLockingPeriod::get().saturating_mul(lock_periods)); + prior.accumulate(unlock_block, balance); + voting.set_common(delegations, prior); + }, + Voting::Direct { votes, delegations, prior } => { + // here we just ensure that we're currently idling with no votes recorded. + ensure!(votes.is_empty(), Error::::VotesExist); + voting.set_common(delegations, prior); + }, + } + let votes = Self::increase_upstream_delegation(&target, conviction.votes(balance)); + // Extend the lock to `balance` (rather than setting it) since we don't know what other + // votes are in place. + T::Fungible::extend_freeze(&FreezeReason::Vote.into(), &who, 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) -> Result { + let votes = VotingOf::::try_mutate(&who, |voting| -> Result { + let mut old = Voting::default(); + sp_std::mem::swap(&mut old, voting); + match old { + Voting::Delegating { balance, target, conviction, delegations, mut prior } => { + // remove any delegation votes to our current target. + let votes = Self::reduce_upstream_delegation(&target, conviction.votes(balance)); + let now = frame_system::Pallet::::block_number(); + let lock_periods = conviction.lock_periods().into(); + let unlock_block = now.saturating_add(T::VoteLockingPeriod::get().saturating_mul(lock_periods)); + prior.accumulate(unlock_block, balance); + voting.set_common(delegations, prior); + + Ok(votes) + }, + Voting::Direct { .. } => Err(Error::::NotDelegating.into()), + } + })?; + Self::deposit_event(Event::::Undelegated { account: who }); + Ok(votes) + } + + /// 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(who: &T::AccountId) -> DispatchResult { + let lock_needed = VotingOf::::mutate(who, |voting| { + voting.rejig(frame_system::Pallet::::block_number()); + voting.locked_balance() + }); + if lock_needed.is_zero() { + T::Fungible::thaw(&FreezeReason::Vote.into(), who) + } else { + T::Fungible::set_freeze(&FreezeReason::Vote.into(), who, lock_needed) + } + } + + /// Start a referendum + fn inject_referendum( + end: BlockNumberFor, + proposal: BoundedCallOf, + threshold: VoteThreshold, + delay: BlockNumberFor, + ) -> ReferendumIndex { + let ref_index = Self::referendum_count(); + ReferendumCount::::put(ref_index + 1); + let status = ReferendumStatus { end, proposal, threshold, delay, tally: Default::default() }; + let item = ReferendumInfo::Ongoing(status); + >::insert(ref_index, item); + Self::deposit_event(Event::::Started { ref_index, threshold }); + ref_index + } + + /// Table the next waiting proposal for a vote. + fn launch_next(now: BlockNumberFor) -> DispatchResult { + if LastTabledWasExternal::::take() { + Self::launch_public(now).or_else(|_| Self::launch_external(now)) + } else { + Self::launch_external(now).or_else(|_| Self::launch_public(now)) + } + .map_err(|_| Error::::NoneWaiting.into()) + } + + /// Table the waiting external proposal for a vote, if there is one. + fn launch_external(now: BlockNumberFor) -> DispatchResult { + if let Some((proposal, threshold)) = >::take() { + LastTabledWasExternal::::put(true); + Self::deposit_event(Event::::ExternalTabled); + let ref_index = Self::inject_referendum( + now.saturating_add(T::VotingPeriod::get()), + proposal, + threshold, + T::EnactmentPeriod::get(), + ); + Self::transfer_metadata(MetadataOwner::External, MetadataOwner::Referendum(ref_index)); + Ok(()) + } else { + return Err(Error::::NoneWaiting.into()) + } + } + + /// Table the waiting public proposal with the highest backing for a vote. + fn launch_public(now: BlockNumberFor) -> DispatchResult { + let mut public_props = Self::public_props(); + if let Some((winner_index, _)) = public_props.iter().enumerate().max_by_key( + // defensive only: All current public proposals have an amount locked + |x| Self::backing_for((x.1).0).defensive_unwrap_or_else(Zero::zero), + ) { + let (prop_index, proposal, _) = public_props.swap_remove(winner_index); + >::put(public_props); + + if let Some((depositors, deposit)) = >::take(prop_index) { + // refund depositors + for d in depositors.iter() { + T::Fungible::release(&HoldReason::Proposal.into(), d, deposit, Precision::BestEffort)?; + } + Self::deposit_event(Event::::Tabled { proposal_index: prop_index, deposit }); + let ref_index = Self::inject_referendum( + now.saturating_add(T::VotingPeriod::get()), + proposal, + VoteThreshold::SuperMajorityApprove, + T::EnactmentPeriod::get(), + ); + Self::transfer_metadata(MetadataOwner::Proposal(prop_index), MetadataOwner::Referendum(ref_index)) + } + Ok(()) + } else { + return Err(Error::::NoneWaiting.into()) + } + } + + fn bake_referendum( + now: BlockNumberFor, + index: ReferendumIndex, + status: ReferendumStatus, BoundedCallOf, BalanceOf>, + ) -> bool { + let total_issuance = T::Fungible::total_issuance(); + let approved = status.threshold.approved(status.tally, total_issuance); + + if approved { + Self::deposit_event(Event::::Passed { ref_index: index }); + + // Earliest it can be scheduled for is next block. + let when = now.saturating_add(status.delay.max(One::one())); + if T::Scheduler::schedule_named( + (DEMOCRACY_ID, index).encode_into(), + DispatchTime::At(when), + None, + 63, + frame_system::RawOrigin::Root.into(), + status.proposal, + ) + .is_err() + { + frame_support::print("LOGIC ERROR: bake_referendum/schedule_named failed"); + } + } else { + Self::deposit_event(Event::::NotPassed { ref_index: index }); + } + + approved + } + + /// Current era is ending; we should finish up any proposals. + /// + /// + /// ## Complexity: + /// If a referendum is launched or maturing, this will take full block weight if queue is not + /// empty. Otherwise, `O(R)` where `R` is the number of unbaked referenda. + fn begin_block(now: BlockNumberFor) -> Weight { + let max_block_weight = T::BlockWeights::get().max_block; + let mut weight = Weight::zero(); + + let next = Self::lowest_unbaked(); + let last = Self::referendum_count(); + let r = last.saturating_sub(next); + + // pick out another public referendum if it's time. + if (now % T::LaunchPeriod::get()).is_zero() { + // Errors come from the queue being empty. If the queue is not empty, it will take + // full block weight. + if Self::launch_next(now).is_ok() { + weight = max_block_weight; + } else { + weight.saturating_accrue(T::WeightInfo::on_initialize_base_with_launch_period(r)); + } + } else { + weight.saturating_accrue(T::WeightInfo::on_initialize_base(r)); + } + + // tally up votes for any expiring referenda. + for (index, info) in Self::maturing_referenda_at_inner(now, next..last).into_iter() { + let approved = Self::bake_referendum(now, index, info); + ReferendumInfoOf::::insert(index, ReferendumInfo::Finished { end: now, approved }); + weight = max_block_weight; + } + + // Notes: + // * We don't consider the lowest unbaked to be the last maturing in case some referenda + // have a longer voting period than others. + // * The iteration here shouldn't trigger any storage read that are not in cache, due to + // `maturing_referenda_at_inner` having already read them. + // * We shouldn't iterate more than `LaunchPeriod/VotingPeriod + 1` times because the number + // of unbaked referendum is bounded by this number. In case those number have changed in a + // runtime upgrade the formula should be adjusted but the bound should still be sensible. + >::mutate(|ref_index| { + while *ref_index < last && + Self::referendum_info(*ref_index) + .map_or(true, |info| matches!(info, ReferendumInfo::Finished { .. })) + { + *ref_index += 1 + } + }); + + weight + } + + /// Reads the length of account in DepositOf without getting the complete value in the runtime. + /// + /// Return 0 if no deposit for this proposal. + fn len_of_deposit_of(proposal: PropIndex) -> Option { + // DepositOf first tuple element is a vec, decoding its len is equivalent to decode a + // `Compact`. + decode_compact_u32_at(&>::hashed_key_for(proposal)) + } + + /// Return a proposal of an index. + fn proposal(index: PropIndex) -> Result<(PropIndex, BoundedCallOf, T::AccountId), Error> { + PublicProps::::get() + .into_iter() + .find(|(prop_index, _, _)| prop_index == &index) + .ok_or(Error::::ProposalMissing) + } + + /// Clear metadata if exist for a given owner. + fn clear_metadata(owner: MetadataOwner) { + if let Some(hash) = MetadataOf::::take(&owner) { + Self::deposit_event(Event::::MetadataCleared { owner, hash }); + } + } + + /// Transfer the metadata of an `owner` to a `new_owner`. + fn transfer_metadata(owner: MetadataOwner, new_owner: MetadataOwner) { + if let Some(hash) = MetadataOf::::take(&owner) { + MetadataOf::::insert(&new_owner, hash); + Self::deposit_event(Event::::MetadataTransferred { prev_owner: owner, owner: new_owner, hash }); + } + } + + /// Ensure external origin for corresponding vote threshold. + fn ensure_external_origin(threshold: VoteThreshold, origin: OriginFor) -> Result<(), BadOrigin> { + match threshold { + VoteThreshold::SuperMajorityApprove => { + let _ = T::ExternalOrigin::ensure_origin(origin)?; + }, + VoteThreshold::SuperMajorityAgainst => { + let _ = T::ExternalDefaultOrigin::ensure_origin(origin)?; + }, + VoteThreshold::SimpleMajority => { + let _ = T::ExternalMajorityOrigin::ensure_origin(origin)?; + }, + }; + Ok(()) + } +} + +/// Decode `Compact` from the trie at given key. +fn decode_compact_u32_at(key: &[u8]) -> Option { + // `Compact` takes at most 5 bytes. + let mut buf = [0u8; 5]; + let bytes = sp_io::storage::read(key, &mut buf, 0)?; + // The value may be smaller than 5 bytes. + let mut input = &buf[0..buf.len().min(bytes as usize)]; + match parity_scale_codec::Compact::::decode(&mut input) { + Ok(c) => Some(c.0), + Err(_) => { + sp_runtime::print("Failed to decode compact u32 at:"); + sp_runtime::print(key); + None + }, + } +} diff --git a/pallets/democracy/src/tests.rs b/pallets/democracy/src/tests.rs new file mode 100644 index 000000000..dffa54a70 --- /dev/null +++ b/pallets/democracy/src/tests.rs @@ -0,0 +1,290 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The crate's tests. + +use super::*; +use crate as pallet_democracy; +use frame_support::{ + assert_noop, assert_ok, ord_parameter_types, parameter_types, + traits::{ + fungible::InspectFreeze, ConstU32, ConstU64, Contains, EqualPrivilegeOnly, OnInitialize, SortedMembers, + StorePreimage, + }, + weights::Weight, +}; +use frame_system::{EnsureRoot, EnsureSigned, EnsureSignedBy}; +use sp_core::H256; +use sp_runtime::{ + traits::{BadOrigin, BlakeTwo256, Hash, IdentityLookup}, + BuildStorage, Perbill, +}; +mod cancellation; +mod decoders; +mod delegation; +mod external_proposing; +mod fast_tracking; +mod lock_voting; +mod metadata; +mod public_proposals; +mod scheduling; +mod voting; + +const AYE: Vote = Vote { aye: true, conviction: Conviction::None }; +const NAY: Vote = Vote { aye: false, conviction: Conviction::None }; +const BIG_AYE: Vote = Vote { aye: true, conviction: Conviction::Locked1x }; +const BIG_NAY: Vote = Vote { aye: false, conviction: Conviction::Locked1x }; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Preimage: pallet_preimage, + Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event}, + Democracy: pallet_democracy::{Pallet, Call, Storage, Config, Event, HoldReason, FreezeReason }, + } +); + +// Test that a filtered call can be dispatched. +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &RuntimeCall) -> bool { + !matches!(call, &RuntimeCall::Balances(pallet_balances::Call::force_set_balance { .. })) + } +} + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max( + Weight::from_parts(frame_support::weights::constants::WEIGHT_REF_TIME_PER_SECOND, u64::MAX), + ); +} +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = u64; + type BaseCallFilter = BaseFilter; + type Block = Block; + type BlockHashCount = ConstU64<250>; + type BlockLength = (); + type BlockWeights = BlockWeights; + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = frame_support::traits::ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; +} + +impl pallet_preimage::Config for Test { + type BaseDeposit = (); + type ByteDeposit = (); + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +impl pallet_scheduler::Config for Test { + type MaxScheduledPerBlock = ConstU32<100>; + type MaximumWeight = MaximumSchedulerWeight; + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type PalletsOrigin = OriginCaller; + type Preimages = (); + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type ScheduleOrigin = EnsureRoot; + type WeightInfo = (); +} + +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = ConstU32<10>; + type MaxHolds = ConstU32<10>; + type MaxLocks = ConstU32<10>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); +} +parameter_types! { + pub static PreimageByteDeposit: u64 = 0; + pub static InstantAllowed: bool = false; +} +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) {} +} + +impl Config for Test { + type BlacklistOrigin = EnsureRoot; + type CancelProposalOrigin = EnsureRoot; + type CancellationOrigin = EnsureSignedBy; + type CooloffPeriod = ConstU64<2>; + type EnactmentPeriod = ConstU64<2>; + type ExternalDefaultOrigin = EnsureSignedBy; + type ExternalMajorityOrigin = EnsureSignedBy; + type ExternalOrigin = EnsureSignedBy; + type FastTrackOrigin = EnsureSignedBy; + type FastTrackVotingPeriod = ConstU64<2>; + type Fungible = Balances; + type InstantAllowed = InstantAllowed; + type InstantOrigin = EnsureSignedBy; + type LaunchPeriod = ConstU64<2>; + type MaxBlacklisted = ConstU32<5>; + type MaxDeposits = ConstU32<1000>; + type MaxProposals = ConstU32<100>; + type MaxVotes = ConstU32<100>; + type MinimumDeposit = ConstU64<1>; + type PalletsOrigin = OriginCaller; + type Preimages = Preimage; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type Scheduler = Scheduler; + type Slash = (); + type SubmitOrigin = EnsureSigned; + type VetoOrigin = EnsureSignedBy; + type VoteLockingPeriod = ConstU64<3>; + type VotingPeriod = ConstU64<2>; + type WeightInfo = (); +} + +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(); + pallet_democracy::GenesisConfig::::default().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!(Democracy::referendum_count(), 0); + assert_eq!(Balances::free_balance(42), 0); + assert_eq!(Balances::total_issuance(), 210); + }); +} + +fn set_balance_proposal(value: u64) -> BoundedCallOf { + let inner = pallet_balances::Call::force_set_balance { who: 42, new_free: value }; + let outer = RuntimeCall::Balances(inner); + Preimage::bound(outer).unwrap() +} + +#[test] +fn set_balance_proposal_is_correctly_filtered_out() { + for i in 0..10 { + let call = Preimage::realize(&set_balance_proposal(i)).unwrap().0; + assert!(!::BaseCallFilter::contains(&call)); + } +} + +fn propose_set_balance(who: u64, value: u64, delay: u64) -> DispatchResult { + Democracy::propose(RuntimeOrigin::signed(who), set_balance_proposal(value), delay) +} + +fn next_block() { + System::set_block_number(System::block_number() + 1); + Scheduler::on_initialize(System::block_number()); + Democracy::begin_block(System::block_number()); +} + +fn fast_forward_to(n: u64) { + while System::block_number() < n { + next_block(); + } +} + +fn begin_referendum() -> ReferendumIndex { + System::set_block_number(0); + assert_ok!(propose_set_balance(1, 2, 1)); + fast_forward_to(2); + 0 +} + +fn balance_freezable_of(who: u64) -> u64 { + Balances::balance_freezable(&who) +} + +fn balance_frozen_of(who: u64) -> u64 { + Balances::balance_frozen(&FreezeReason::Vote.into(), &who) +} + +fn aye(who: u64) -> AccountVote { + AccountVote::Standard { vote: AYE, balance: balance_freezable_of(who) } +} + +fn nay(who: u64) -> AccountVote { + AccountVote::Standard { vote: NAY, balance: balance_freezable_of(who) } +} + +fn big_aye(who: u64) -> AccountVote { + AccountVote::Standard { vote: BIG_AYE, balance: balance_freezable_of(who) } +} + +fn big_nay(who: u64) -> AccountVote { + AccountVote::Standard { vote: BIG_NAY, balance: balance_freezable_of(who) } +} + +fn tally(r: ReferendumIndex) -> Tally { + Democracy::referendum_status(r).unwrap().tally +} + +/// note a new preimage without registering. +fn note_preimage(who: u64) -> PreimageHash { + use std::sync::atomic::{AtomicU8, Ordering}; + // note a new preimage on every function invoke. + static COUNTER: AtomicU8 = AtomicU8::new(0); + let data = vec![COUNTER.fetch_add(1, Ordering::Relaxed)]; + assert_ok!(Preimage::note_preimage(RuntimeOrigin::signed(who), data.clone())); + let hash = BlakeTwo256::hash(&data); + assert!(!Preimage::is_requested(&hash)); + hash +} diff --git a/pallets/democracy/src/tests/cancellation.rs b/pallets/democracy/src/tests/cancellation.rs new file mode 100644 index 000000000..f60d1c265 --- /dev/null +++ b/pallets/democracy/src/tests/cancellation.rs @@ -0,0 +1,51 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for cancelation functionality. + +use super::*; + +#[test] +fn cancel_referendum_should_work() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_ok!(Democracy::cancel_referendum(RuntimeOrigin::root(), r.into())); + assert_eq!(Democracy::lowest_unbaked(), 0); + + next_block(); + + next_block(); + + assert_eq!(Democracy::lowest_unbaked(), 1); + assert_eq!(Democracy::lowest_unbaked(), Democracy::referendum_count()); + assert_eq!(Balances::free_balance(42), 0); + }); +} + +#[test] +fn emergency_cancel_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 2); + assert!(Democracy::referendum_status(r).is_ok()); + + assert_noop!(Democracy::emergency_cancel(RuntimeOrigin::signed(3), r), BadOrigin); + assert_ok!(Democracy::emergency_cancel(RuntimeOrigin::signed(4), r)); + assert!(Democracy::referendum_info(r).is_none()); + + // some time later... + + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 2); + assert!(Democracy::referendum_status(r).is_ok()); + assert_noop!(Democracy::emergency_cancel(RuntimeOrigin::signed(4), r), Error::::AlreadyCanceled,); + }); +} diff --git a/pallets/democracy/src/tests/decoders.rs b/pallets/democracy/src/tests/decoders.rs new file mode 100644 index 000000000..acafa04a2 --- /dev/null +++ b/pallets/democracy/src/tests/decoders.rs @@ -0,0 +1,51 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The for various partial storage decoders + +use super::*; +use frame_support::{ + storage::{migration, unhashed}, + BoundedVec, +}; + +#[test] +fn test_decode_compact_u32_at() { + new_test_ext().execute_with(|| { + let v = parity_scale_codec::Compact(u64::MAX); + migration::put_storage_value(b"test", b"", &[], v); + assert_eq!(decode_compact_u32_at(b"test"), None); + + for v in vec![0, 10, u32::MAX] { + let compact_v = parity_scale_codec::Compact(v); + unhashed::put(b"test", &compact_v); + assert_eq!(decode_compact_u32_at(b"test"), Some(v)); + } + + unhashed::kill(b"test"); + assert_eq!(decode_compact_u32_at(b"test"), None); + }) +} + +#[test] +fn len_of_deposit_of() { + new_test_ext().execute_with(|| { + for l in vec![0, 1, 200, 1000] { + let value: (BoundedVec, u64) = + ((0..l).map(|_| Default::default()).collect::>().try_into().unwrap(), 3u64); + DepositOf::::insert(2, value); + assert_eq!(Democracy::len_of_deposit_of(2), Some(l)); + } + + DepositOf::::remove(2); + assert_eq!(Democracy::len_of_deposit_of(2), None); + }) +} diff --git a/pallets/democracy/src/tests/delegation.rs b/pallets/democracy/src/tests/delegation.rs new file mode 100644 index 000000000..337f81dec --- /dev/null +++ b/pallets/democracy/src/tests/delegation.rs @@ -0,0 +1,209 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for functionality concerning delegation. + +use super::*; + +#[test] +fn single_proposal_should_work_with_delegation() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + + assert_ok!(propose_set_balance(1, 2, 1)); + + fast_forward_to(2); + + // Delegate first vote. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::None, 20)); + let r = 0; + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_eq!(tally(r), Tally { ayes: 3, nays: 0, turnout: 30 }); + + // Delegate a second vote. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(3), 1, Conviction::None, 30)); + assert_eq!(tally(r), Tally { ayes: 6, nays: 0, turnout: 60 }); + + // Reduce first vote. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::None, 10)); + assert_eq!(tally(r), Tally { ayes: 5, nays: 0, turnout: 50 }); + + // Second vote delegates to first; we don't do tiered delegation, so it doesn't get used. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(3), 2, Conviction::None, 30)); + assert_eq!(tally(r), Tally { ayes: 2, nays: 0, turnout: 20 }); + + // Main voter cancels their vote + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(1), r)); + assert_eq!(tally(r), Tally { ayes: 0, nays: 0, turnout: 0 }); + + // First delegator delegates half funds with conviction; nothing changes yet. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::Locked1x, 10)); + assert_eq!(tally(r), Tally { ayes: 0, nays: 0, turnout: 0 }); + + // Main voter reinstates their vote + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_eq!(tally(r), Tally { ayes: 11, nays: 0, turnout: 20 }); + }); +} + +#[test] +fn self_delegation_not_allowed() { + new_test_ext().execute_with(|| { + assert_noop!(Democracy::delegate(RuntimeOrigin::signed(1), 1, Conviction::None, 10), Error::::Nonsense,); + }); +} + +#[test] +fn cyclic_delegation_should_unwind() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + + assert_ok!(propose_set_balance(1, 2, 1)); + + fast_forward_to(2); + + // Check behavior with cycle. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::None, 20)); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(3), 2, Conviction::None, 30)); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(1), 3, Conviction::None, 10)); + let r = 0; + assert_ok!(Democracy::undelegate(RuntimeOrigin::signed(3))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(3), r, aye(3))); + assert_ok!(Democracy::undelegate(RuntimeOrigin::signed(1))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, nay(1))); + + // Delegated vote is counted. + assert_eq!(tally(r), Tally { ayes: 3, nays: 3, turnout: 60 }); + }); +} + +#[test] +fn single_proposal_should_work_with_vote_and_delegation() { + // If transactor already voted, delegated vote is overwritten. + new_test_ext().execute_with(|| { + System::set_block_number(0); + + assert_ok!(propose_set_balance(1, 2, 1)); + + fast_forward_to(2); + + let r = 0; + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(2), r, nay(2))); + assert_eq!(tally(r), Tally { ayes: 1, nays: 2, turnout: 30 }); + + // Delegate vote. + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(2), r)); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::None, 20)); + // Delegated vote replaces the explicit vote. + assert_eq!(tally(r), Tally { ayes: 3, nays: 0, turnout: 30 }); + }); +} + +#[test] +fn single_proposal_should_work_with_undelegation() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + + assert_ok!(propose_set_balance(1, 2, 1)); + + // Delegate and undelegate vote. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::None, 20)); + assert_ok!(Democracy::undelegate(RuntimeOrigin::signed(2))); + + fast_forward_to(2); + let r = 0; + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + + // Delegated vote is not counted. + assert_eq!(tally(r), Tally { ayes: 1, nays: 0, turnout: 10 }); + }); +} + +#[test] +fn single_proposal_should_work_with_delegation_and_vote() { + // If transactor voted, delegated vote is overwritten. + new_test_ext().execute_with(|| { + let r = begin_referendum(); + // Delegate, undelegate and vote. + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::None, 20)); + assert_eq!(tally(r), Tally { ayes: 3, nays: 0, turnout: 30 }); + assert_ok!(Democracy::undelegate(RuntimeOrigin::signed(2))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(2), r, aye(2))); + // Delegated vote is not counted. + assert_eq!(tally(r), Tally { ayes: 3, nays: 0, turnout: 30 }); + }); +} + +#[test] +fn conviction_should_be_honored_in_delegation() { + // If transactor voted, delegated vote is overwritten. + new_test_ext().execute_with(|| { + let r = begin_referendum(); + // Delegate and vote. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::Locked6x, 20)); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + // Delegated vote is huge. + assert_eq!(tally(r), Tally { ayes: 121, nays: 0, turnout: 30 }); + }); +} + +#[test] +fn split_vote_delegation_should_be_ignored() { + // If transactor voted, delegated vote is overwritten. + new_test_ext().execute_with(|| { + let r = begin_referendum(); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::Locked6x, 20)); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, AccountVote::Split { aye: 10, nay: 0 })); + // Delegated vote is huge. + assert_eq!(tally(r), Tally { ayes: 1, nays: 0, turnout: 10 }); + }); +} + +#[test] +fn redelegation_keeps_lock() { + // If transactor voted, delegated vote is overwritten. + new_test_ext().execute_with(|| { + let r = begin_referendum(); + // Delegate and vote. + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 1, Conviction::Locked6x, 20)); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + // Delegated vote is huge. + assert_eq!(tally(r), Tally { ayes: 121, nays: 0, turnout: 30 }); + + let mut prior_lock = vote::PriorLock::default(); + + // Locked balance of delegator exists + assert_eq!(VotingOf::::get(2).locked_balance(), 20); + assert_eq!(VotingOf::::get(2).prior(), &prior_lock); + + // Delegate someone else at a lower conviction and amount + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(2), 3, Conviction::None, 10)); + + // 6x prior should appear w/ locked balance. + prior_lock.accumulate(98, 20); + assert_eq!(VotingOf::::get(2).prior(), &prior_lock); + assert_eq!(VotingOf::::get(2).locked_balance(), 20); + // Unlock shouldn't work + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(2), 2)); + assert_eq!(VotingOf::::get(2).prior(), &prior_lock); + assert_eq!(VotingOf::::get(2).locked_balance(), 20); + + fast_forward_to(100); + + // Now unlock can remove the prior lock and reduce the locked amount. + assert_eq!(VotingOf::::get(2).prior(), &prior_lock); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(2), 2)); + assert_eq!(VotingOf::::get(2).prior(), &vote::PriorLock::default()); + assert_eq!(VotingOf::::get(2).locked_balance(), 10); + }); +} diff --git a/pallets/democracy/src/tests/external_proposing.rs b/pallets/democracy/src/tests/external_proposing.rs new file mode 100644 index 000000000..6c2ad5121 --- /dev/null +++ b/pallets/democracy/src/tests/external_proposing.rs @@ -0,0 +1,255 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for functionality concerning the "external" origin. + +use super::*; + +#[test] +fn veto_external_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2),)); + assert!(>::exists()); + + let h = set_balance_proposal(2).hash(); + assert_ok!(Democracy::veto_external(RuntimeOrigin::signed(3), h)); + // cancelled. + assert!(!>::exists()); + // fails - same proposal can't be resubmitted. + assert_noop!( + Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2),), + Error::::ProposalBlacklisted + ); + + fast_forward_to(1); + // fails as we're still in cooloff period. + assert_noop!( + Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2),), + Error::::ProposalBlacklisted + ); + + fast_forward_to(2); + // works; as we're out of the cooloff period. + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2),)); + assert!(>::exists()); + + // 3 can't veto the same thing twice. + assert_noop!(Democracy::veto_external(RuntimeOrigin::signed(3), h), Error::::AlreadyVetoed); + + // 4 vetoes. + assert_ok!(Democracy::veto_external(RuntimeOrigin::signed(4), h)); + // cancelled again. + assert!(!>::exists()); + + fast_forward_to(3); + // same proposal fails as we're still in cooloff + assert_noop!( + Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2)), + Error::::ProposalBlacklisted + ); + // different proposal works fine. + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(3),)); + }); +} + +#[test] +fn external_blacklisting_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2),)); + + let hash = set_balance_proposal(2).hash(); + assert_ok!(Democracy::blacklist(RuntimeOrigin::root(), hash, None)); + + fast_forward_to(2); + assert_noop!(Democracy::referendum_status(0), Error::::ReferendumInvalid); + + assert_noop!( + Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2)), + Error::::ProposalBlacklisted, + ); + }); +} + +#[test] +fn external_referendum_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_noop!(Democracy::external_propose(RuntimeOrigin::signed(1), set_balance_proposal(2),), BadOrigin,); + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2),)); + assert_noop!( + Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(1),), + Error::::DuplicateProposal + ); + fast_forward_to(2); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 4, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + }); +} + +#[test] +fn external_majority_referendum_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_noop!( + Democracy::external_propose_majority(RuntimeOrigin::signed(1), set_balance_proposal(2)), + BadOrigin, + ); + assert_ok!(Democracy::external_propose_majority(RuntimeOrigin::signed(3), set_balance_proposal(2))); + fast_forward_to(2); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 4, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SimpleMajority, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + }); +} + +#[test] +fn external_default_referendum_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_noop!(Democracy::external_propose_default(RuntimeOrigin::signed(3), set_balance_proposal(2)), BadOrigin,); + assert_ok!(Democracy::external_propose_default(RuntimeOrigin::signed(1), set_balance_proposal(2))); + fast_forward_to(2); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 4, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SuperMajorityAgainst, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + }); +} + +#[test] +fn external_and_public_interleaving_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(1),)); + assert_ok!(propose_set_balance(6, 2, 2)); + + fast_forward_to(2); + + // both waiting: external goes first. + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 4, + proposal: set_balance_proposal(1), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + // replenish external + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(3),)); + + fast_forward_to(4); + + // both waiting: public goes next. + assert_eq!( + Democracy::referendum_status(1), + Ok(ReferendumStatus { + end: 6, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + // don't replenish public + + fast_forward_to(6); + + // it's external "turn" again, though since public is empty that doesn't really matter + assert_eq!( + Democracy::referendum_status(2), + Ok(ReferendumStatus { + end: 8, + proposal: set_balance_proposal(3), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + // replenish external + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(5),)); + + fast_forward_to(8); + + // external goes again because there's no public waiting. + assert_eq!( + Democracy::referendum_status(3), + Ok(ReferendumStatus { + end: 10, + proposal: set_balance_proposal(5), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + // replenish both + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(7),)); + assert_ok!(propose_set_balance(6, 4, 2)); + + fast_forward_to(10); + + // public goes now since external went last time. + assert_eq!( + Democracy::referendum_status(4), + Ok(ReferendumStatus { + end: 12, + proposal: set_balance_proposal(4), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + // replenish public again + assert_ok!(propose_set_balance(6, 6, 2)); + // cancel external + let h = set_balance_proposal(7).hash(); + assert_ok!(Democracy::veto_external(RuntimeOrigin::signed(3), h)); + + fast_forward_to(12); + + // public goes again now since there's no external waiting. + assert_eq!( + Democracy::referendum_status(5), + Ok(ReferendumStatus { + end: 14, + proposal: set_balance_proposal(6), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + }); +} diff --git a/pallets/democracy/src/tests/fast_tracking.rs b/pallets/democracy/src/tests/fast_tracking.rs new file mode 100644 index 000000000..8c9ac3782 --- /dev/null +++ b/pallets/democracy/src/tests/fast_tracking.rs @@ -0,0 +1,130 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for fast-tracking functionality. + +use super::*; + +#[test] +fn fast_track_referendum_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let h = set_balance_proposal(2).hash(); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(5), h, 3, 2), Error::::ProposalMissing); + assert_ok!(Democracy::external_propose_majority(RuntimeOrigin::signed(3), set_balance_proposal(2))); + let hash = note_preimage(1); + assert!(>::get(MetadataOwner::External).is_none()); + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(3), MetadataOwner::External, Some(hash),),); + assert!(>::get(MetadataOwner::External).is_some()); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(1), h, 3, 2), BadOrigin); + assert_ok!(Democracy::fast_track(RuntimeOrigin::signed(5), h, 2, 0)); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 2, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SimpleMajority, + delay: 0, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + // metadata reset from the external proposal to the referendum. + assert!(>::get(MetadataOwner::External).is_none()); + assert!(>::get(MetadataOwner::Referendum(0)).is_some()); + }); +} + +#[test] +fn instant_referendum_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let h = set_balance_proposal(2).hash(); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(5), h, 3, 2), Error::::ProposalMissing); + assert_ok!(Democracy::external_propose_majority(RuntimeOrigin::signed(3), set_balance_proposal(2))); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(1), h, 3, 2), BadOrigin); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(5), h, 1, 0), BadOrigin); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(6), h, 1, 0), Error::::InstantNotAllowed); + INSTANT_ALLOWED.with(|v| *v.borrow_mut() = true); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(6), h, 0, 0), Error::::VotingPeriodLow); + assert_ok!(Democracy::fast_track(RuntimeOrigin::signed(6), h, 1, 0)); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 1, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SimpleMajority, + delay: 0, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + }); +} + +#[test] +fn instant_next_block_referendum_backed() { + new_test_ext().execute_with(|| { + // arrange + let start_block_number = 10; + let majority_origin_id = 3; + let instant_origin_id = 6; + let voting_period = 1; + let proposal = set_balance_proposal(2); + let delay = 2; // has no effect on test + + // init + System::set_block_number(start_block_number); + InstantAllowed::set(true); + + // propose with majority origin + assert_ok!(Democracy::external_propose_majority(RuntimeOrigin::signed(majority_origin_id), proposal.clone())); + + // fast track with instant origin and voting period pointing to the next block + assert_ok!(Democracy::fast_track( + RuntimeOrigin::signed(instant_origin_id), + proposal.hash(), + voting_period, + delay + )); + + // fetch the status of the only referendum at index 0 + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: start_block_number + voting_period, + proposal, + threshold: VoteThreshold::SimpleMajority, + delay, + tally: Tally { ayes: 0, nays: 0, turnout: 0 }, + }) + ); + + // referendum expected to be baked with the start of the next block + next_block(); + + // assert no active referendums + assert_noop!(Democracy::referendum_status(0), Error::::ReferendumInvalid); + // the only referendum in the storage is finished and not approved + assert_eq!( + ReferendumInfoOf::::get(0).unwrap(), + ReferendumInfo::Finished { approved: false, end: start_block_number + voting_period } + ); + }); +} + +#[test] +fn fast_track_referendum_fails_when_no_simple_majority() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let h = set_balance_proposal(2).hash(); + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2))); + assert_noop!(Democracy::fast_track(RuntimeOrigin::signed(5), h, 3, 2), Error::::NotSimpleMajority); + }); +} diff --git a/pallets/democracy/src/tests/lock_voting.rs b/pallets/democracy/src/tests/lock_voting.rs new file mode 100644 index 000000000..b911cb8d2 --- /dev/null +++ b/pallets/democracy/src/tests/lock_voting.rs @@ -0,0 +1,307 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for functionality concerning locking and lock-voting. + +use super::*; + +fn aye(x: u8, balance: u64) -> AccountVote { + AccountVote::Standard { vote: Vote { aye: true, conviction: Conviction::try_from(x).unwrap() }, balance } +} + +fn nay(x: u8, balance: u64) -> AccountVote { + AccountVote::Standard { vote: Vote { aye: false, conviction: Conviction::try_from(x).unwrap() }, balance } +} + +#[test] +fn lock_voting_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, nay(5, 10))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(2), r, aye(4, 20))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(3), r, aye(3, 30))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(4), r, aye(2, 40))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, nay(1, 50))); + assert_eq!(tally(r), Tally { ayes: 250, nays: 100, turnout: 150 }); + + // All balances are currently locked. + for i in 1..=5 { + assert_eq!(balance_frozen_of(i), i * 10); + } + + fast_forward_to(3); + + // Referendum passed; 1 and 5 didn't get their way and can now reap and unlock. + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(1), r)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 1)); + // Anyone can reap and unlock anyone else's in this context. + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(2), 5, r)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(2), 5)); + + // 2, 3, 4 got their way with the vote, so they cannot be reaped by others. + assert_noop!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 2, r), Error::::NoPermission); + // However, they can be unvoted by the owner, though it will make no difference to the lock. + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(2), r)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(2), 2)); + + assert_eq!(balance_frozen_of(1), 0); + assert_eq!(balance_frozen_of(2), 20); + assert_eq!(balance_frozen_of(3), 30); + assert_eq!(balance_frozen_of(4), 40); + assert_eq!(balance_frozen_of(5), 0); + assert_eq!(Balances::free_balance(42), 2); + + fast_forward_to(7); + // No change yet... + assert_noop!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 4, r), Error::::NoPermission); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 4)); + assert_eq!(balance_frozen_of(4), 40); + fast_forward_to(8); + // 4 should now be able to reap and unlock + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 4, r)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 4)); + assert_eq!(Balances::locks(4), vec![]); + + fast_forward_to(13); + assert_noop!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 3, r), Error::::NoPermission); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 3)); + assert_eq!(balance_frozen_of(3), 30); + fast_forward_to(14); + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 3, r)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 3)); + assert_eq!(Balances::locks(3), vec![]); + + // 2 doesn't need to reap_vote here because it was already done before. + fast_forward_to(25); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 2)); + assert_eq!(balance_frozen_of(2), 20); + fast_forward_to(26); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(1), 2)); + assert_eq!(balance_frozen_of(2), 0); + }); +} + +#[test] +fn no_locks_without_conviction_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(0, 10))); + + fast_forward_to(3); + + assert_eq!(Balances::free_balance(42), 2); + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(2), 1, r)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(2), 1)); + assert_eq!(Balances::locks(1), vec![]); + }); +} + +#[test] +fn lock_voting_should_work_with_delegation() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, nay(5, 10))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(2), r, aye(4, 20))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(3), r, aye(3, 30))); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(4), 2, Conviction::Locked2x, 40)); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, nay(1, 50))); + + assert_eq!(tally(r), Tally { ayes: 250, nays: 100, turnout: 150 }); + + next_block(); + next_block(); + + assert_eq!(Balances::free_balance(42), 2); + }); +} + +fn setup_three_referenda() -> (u32, u32, u32) { + System::set_block_number(0); + let r1 = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SimpleMajority, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r1, aye(4, 10))); + + let r2 = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SimpleMajority, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r2, aye(3, 20))); + + let r3 = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SimpleMajority, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r3, aye(2, 50))); + + fast_forward_to(2); + + (r1, r2, r3) +} + +#[test] +fn prior_lockvotes_should_be_enforced() { + new_test_ext().execute_with(|| { + let r = setup_three_referenda(); + // r.0 locked 10 until 2 + 8 * 3 = #26 + // r.1 locked 20 until 2 + 4 * 3 = #14 + // r.2 locked 50 until 2 + 2 * 3 = #8 + + fast_forward_to(7); + assert_noop!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 5, r.2), Error::::NoPermission); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 50); + fast_forward_to(8); + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 5, r.2)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 20); + fast_forward_to(13); + assert_noop!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 5, r.1), Error::::NoPermission); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 20); + fast_forward_to(14); + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 5, r.1)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 10); + fast_forward_to(25); + assert_noop!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 5, r.0), Error::::NoPermission); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 10); + fast_forward_to(26); + assert_ok!(Democracy::remove_other_vote(RuntimeOrigin::signed(1), 5, r.0)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 0); + }); +} + +#[test] +fn single_consolidation_of_lockvotes_should_work_as_before() { + new_test_ext().execute_with(|| { + let r = setup_three_referenda(); + // r.0 locked 10 until 2 + 8 * 3 = #26 + // r.1 locked 20 until 2 + 4 * 3 = #14 + // r.2 locked 50 until 2 + 2 * 3 = #8 + + fast_forward_to(7); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.2)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 50); + fast_forward_to(8); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 20); + + fast_forward_to(13); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.1)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 20); + fast_forward_to(14); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 10); + + fast_forward_to(25); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.0)); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 10); + fast_forward_to(26); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 0); + }); +} + +#[test] +fn multi_consolidation_of_lockvotes_should_be_conservative() { + new_test_ext().execute_with(|| { + let r = setup_three_referenda(); + // r.0 locked 10 until 2 + 8 * 3 = #26 + // r.1 locked 20 until 2 + 4 * 3 = #14 + // r.2 locked 50 until 2 + 2 * 3 = #8 + + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.2)); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.1)); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.0)); + + fast_forward_to(8); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 20); + + fast_forward_to(14); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 10); + + fast_forward_to(26); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 0); + }); +} + +#[test] +fn locks_should_persist_from_voting_to_delegation() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SimpleMajority, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, aye(4, 10))); + fast_forward_to(2); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r)); + // locked 10 until #26. + + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(5), 1, Conviction::Locked3x, 20)); + // locked 20. + assert!(balance_frozen_of(5) == 20); + + assert_ok!(Democracy::undelegate(RuntimeOrigin::signed(5))); + // locked 20 until #14 + + fast_forward_to(13); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) == 20); + + fast_forward_to(14); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 10); + + fast_forward_to(25); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 10); + + fast_forward_to(26); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 0); + }); +} + +#[test] +fn locks_should_persist_from_delegation_to_voting() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_ok!(Democracy::delegate(RuntimeOrigin::signed(5), 1, Conviction::Locked5x, 5)); + assert_ok!(Democracy::undelegate(RuntimeOrigin::signed(5))); + // locked 5 until 16 * 3 = #48 + + let r = setup_three_referenda(); + // r.0 locked 10 until 2 + 8 * 3 = #26 + // r.1 locked 20 until 2 + 4 * 3 = #14 + // r.2 locked 50 until 2 + 2 * 3 = #8 + + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.2)); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.1)); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r.0)); + + fast_forward_to(8); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 20); + + fast_forward_to(14); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 10); + + fast_forward_to(26); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert!(balance_frozen_of(5) >= 5); + + fast_forward_to(48); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(balance_frozen_of(5), 0); + }); +} diff --git a/pallets/democracy/src/tests/metadata.rs b/pallets/democracy/src/tests/metadata.rs new file mode 100644 index 000000000..768677cc6 --- /dev/null +++ b/pallets/democracy/src/tests/metadata.rs @@ -0,0 +1,161 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for functionality concerning the metadata. + +use super::*; + +#[test] +fn set_external_metadata_works() { + new_test_ext().execute_with(|| { + // invalid preimage hash. + let invalid_hash: ::Hash = [1u8; 32].into(); + // metadata owner is an external proposal. + let owner = MetadataOwner::External; + // fails to set metadata if an external proposal does not exist. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(2), owner.clone(), Some(invalid_hash)), + Error::::NoProposal, + ); + // create an external proposal. + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2))); + assert!(>::exists()); + // fails to set metadata with non external origin. + assert_noop!(Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), Some(invalid_hash)), BadOrigin,); + // fails to set non-existing preimage. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(2), owner.clone(), Some(invalid_hash)), + Error::::PreimageNotExist, + ); + // set metadata successful. + let hash = note_preimage(1); + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(2), owner.clone(), Some(hash))); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataSet { owner, hash })); + }); +} + +#[test] +fn clear_metadata_works() { + new_test_ext().execute_with(|| { + // metadata owner is an external proposal. + let owner = MetadataOwner::External; + // create an external proposal. + assert_ok!(Democracy::external_propose(RuntimeOrigin::signed(2), set_balance_proposal(2))); + assert!(>::exists()); + // set metadata. + let hash = note_preimage(1); + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(2), owner.clone(), Some(hash))); + // fails to clear metadata with a wrong origin. + assert_noop!(Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), None), BadOrigin,); + // clear metadata successful. + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(2), owner.clone(), None)); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataCleared { owner, hash })); + }); +} + +#[test] +fn set_proposal_metadata_works() { + new_test_ext().execute_with(|| { + // invalid preimage hash. + let invalid_hash: ::Hash = [1u8; 32].into(); + // create an external proposal. + assert_ok!(propose_set_balance(1, 2, 5)); + // metadata owner is a public proposal. + let owner = MetadataOwner::Proposal(Democracy::public_prop_count() - 1); + // fails to set non-existing preimage. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), Some(invalid_hash)), + Error::::PreimageNotExist, + ); + // note preimage. + let hash = note_preimage(1); + // fails to set a preimage if an origin is not a proposer. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(3), owner.clone(), Some(hash)), + Error::::NoPermission, + ); + // set metadata successful. + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), Some(hash))); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataSet { owner, hash })); + }); +} + +#[test] +fn clear_proposal_metadata_works() { + new_test_ext().execute_with(|| { + // create an external proposal. + assert_ok!(propose_set_balance(1, 2, 5)); + // metadata owner is a public proposal. + let owner = MetadataOwner::Proposal(Democracy::public_prop_count() - 1); + // set metadata. + let hash = note_preimage(1); + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), Some(hash))); + // fails to clear metadata with a wrong origin. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(3), owner.clone(), None), + Error::::NoPermission, + ); + // clear metadata successful. + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), None)); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataCleared { owner, hash })); + }); +} + +#[test] +fn set_referendum_metadata_by_root() { + new_test_ext().execute_with(|| { + let index = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + // metadata owner is a referendum. + let owner = MetadataOwner::Referendum(index); + // note preimage. + let hash = note_preimage(1); + // fails to set if not a root. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(3), owner.clone(), Some(hash)), + Error::::NoPermission, + ); + // fails to clear if not a root. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(3), owner.clone(), None), + Error::::NoPermission, + ); + // succeed to set metadata by a root for an ongoing referendum. + assert_ok!(Democracy::set_metadata(RuntimeOrigin::root(), owner.clone(), Some(hash))); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataSet { owner: owner.clone(), hash })); + // succeed to clear metadata by a root for an ongoing referendum. + assert_ok!(Democracy::set_metadata(RuntimeOrigin::root(), owner.clone(), None)); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataCleared { owner, hash })); + }); +} + +#[test] +fn clear_referendum_metadata_works() { + new_test_ext().execute_with(|| { + // create a referendum. + let index = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + // metadata owner is a referendum. + let owner = MetadataOwner::Referendum(index); + // set metadata. + let hash = note_preimage(1); + // referendum finished. + MetadataOf::::insert(owner.clone(), hash); + // no permission to clear metadata of an ongoing referendum. + assert_noop!( + Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), None), + Error::::NoPermission, + ); + // referendum finished. + ReferendumInfoOf::::insert(index, ReferendumInfo::Finished { end: 1, approved: true }); + // clear metadata successful. + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(1), owner.clone(), None)); + System::assert_last_event(RuntimeEvent::Democracy(crate::Event::MetadataCleared { owner, hash })); + }); +} diff --git a/pallets/democracy/src/tests/public_proposals.rs b/pallets/democracy/src/tests/public_proposals.rs new file mode 100644 index 000000000..5af0afeb6 --- /dev/null +++ b/pallets/democracy/src/tests/public_proposals.rs @@ -0,0 +1,139 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for the public proposal queue. + +use super::*; +use sp_runtime::TokenError; + +#[test] +fn backing_for_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(propose_set_balance(1, 2, 2)); + assert_ok!(propose_set_balance(1, 4, 4)); + assert_ok!(propose_set_balance(1, 3, 3)); + assert_eq!(Democracy::backing_for(0), Some(2)); + assert_eq!(Democracy::backing_for(1), Some(4)); + assert_eq!(Democracy::backing_for(2), Some(3)); + }); +} + +#[test] +fn deposit_for_proposals_should_be_taken() { + new_test_ext().execute_with(|| { + assert_ok!(propose_set_balance(1, 2, 5)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(2), 0)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(5), 0)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(5), 0)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(5), 0)); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::free_balance(2), 15); + assert_eq!(Balances::free_balance(5), 35); + }); +} + +#[test] +fn deposit_for_proposals_should_be_returned() { + new_test_ext().execute_with(|| { + assert_ok!(propose_set_balance(1, 2, 5)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(2), 0)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(5), 0)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(5), 0)); + assert_ok!(Democracy::second(RuntimeOrigin::signed(5), 0)); + fast_forward_to(3); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::free_balance(2), 20); + assert_eq!(Balances::free_balance(5), 50); + }); +} + +#[test] +fn proposal_with_deposit_below_minimum_should_not_work() { + new_test_ext().execute_with(|| { + assert_noop!(propose_set_balance(1, 2, 0), Error::::ValueLow); + }); +} + +#[test] +fn poor_proposer_should_not_work() { + new_test_ext().execute_with(|| { + assert_noop!(propose_set_balance(1, 2, 11), TokenError::FundsUnavailable); + }); +} + +#[test] +fn poor_seconder_should_not_work() { + new_test_ext().execute_with(|| { + assert_ok!(propose_set_balance(2, 2, 11)); + assert_noop!(Democracy::second(RuntimeOrigin::signed(1), 0), TokenError::FundsUnavailable); + }); +} + +#[test] +fn cancel_proposal_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(propose_set_balance(1, 2, 2)); + assert_ok!(propose_set_balance(1, 4, 4)); + assert_noop!(Democracy::cancel_proposal(RuntimeOrigin::signed(1), 0), BadOrigin); + let hash = note_preimage(1); + assert_ok!(Democracy::set_metadata(RuntimeOrigin::signed(1), MetadataOwner::Proposal(0), Some(hash))); + assert!(>::get(MetadataOwner::Proposal(0)).is_some()); + assert_ok!(Democracy::cancel_proposal(RuntimeOrigin::root(), 0)); + // metadata cleared, preimage unrequested. + assert!(>::get(MetadataOwner::Proposal(0)).is_none()); + System::assert_has_event(crate::Event::ProposalCanceled { prop_index: 0 }.into()); + System::assert_last_event(crate::Event::MetadataCleared { owner: MetadataOwner::Proposal(0), hash }.into()); + assert_eq!(Democracy::backing_for(0), None); + assert_eq!(Democracy::backing_for(1), Some(4)); + }); +} + +#[test] +fn blacklisting_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + let hash = set_balance_proposal(2).hash(); + + assert_ok!(propose_set_balance(1, 2, 2)); + assert_ok!(propose_set_balance(1, 4, 4)); + + assert_noop!(Democracy::blacklist(RuntimeOrigin::signed(1), hash, None), BadOrigin); + assert_ok!(Democracy::blacklist(RuntimeOrigin::root(), hash, None)); + + assert_eq!(Democracy::backing_for(0), None); + assert_eq!(Democracy::backing_for(1), Some(4)); + + assert_noop!(propose_set_balance(1, 2, 2), Error::::ProposalBlacklisted); + + fast_forward_to(2); + + let hash = set_balance_proposal(4).hash(); + assert_ok!(Democracy::referendum_status(0)); + assert_ok!(Democracy::blacklist(RuntimeOrigin::root(), hash, Some(0))); + assert_noop!(Democracy::referendum_status(0), Error::::ReferendumInvalid); + }); +} + +#[test] +fn runners_up_should_come_after() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_ok!(propose_set_balance(1, 2, 2)); + assert_ok!(propose_set_balance(1, 4, 4)); + assert_ok!(propose_set_balance(1, 3, 3)); + fast_forward_to(2); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 0, aye(1))); + fast_forward_to(4); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 1, aye(1))); + fast_forward_to(6); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), 2, aye(1))); + }); +} diff --git a/pallets/democracy/src/tests/scheduling.rs b/pallets/democracy/src/tests/scheduling.rs new file mode 100644 index 000000000..9140c9e06 --- /dev/null +++ b/pallets/democracy/src/tests/scheduling.rs @@ -0,0 +1,115 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for functionality concerning normal starting, ending and enacting of referenda. + +use super::*; + +#[test] +fn simple_passing_should_work() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_eq!(tally(r), Tally { ayes: 1, nays: 0, turnout: 10 }); + assert_eq!(Democracy::lowest_unbaked(), 0); + next_block(); + next_block(); + assert_eq!(Democracy::lowest_unbaked(), 1); + assert_eq!(Balances::free_balance(42), 2); + }); +} + +#[test] +fn simple_failing_should_work() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, nay(1))); + assert_eq!(tally(r), Tally { ayes: 0, nays: 1, turnout: 10 }); + + next_block(); + next_block(); + + assert_eq!(Balances::free_balance(42), 0); + }); +} + +#[test] +fn ooo_inject_referendums_should_work() { + new_test_ext().execute_with(|| { + let r1 = Democracy::inject_referendum(3, set_balance_proposal(3), VoteThreshold::SuperMajorityApprove, 0); + let r2 = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r2, aye(1))); + assert_eq!(tally(r2), Tally { ayes: 1, nays: 0, turnout: 10 }); + + next_block(); + + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r1, aye(1))); + assert_eq!(tally(r1), Tally { ayes: 1, nays: 0, turnout: 10 }); + + next_block(); + assert_eq!(Balances::free_balance(42), 2); + + next_block(); + assert_eq!(Balances::free_balance(42), 3); + }); +} + +#[test] +fn delayed_enactment_should_work() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 1); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(2), r, aye(2))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(3), r, aye(3))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(4), r, aye(4))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, aye(5))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(6), r, aye(6))); + + assert_eq!(tally(r), Tally { ayes: 21, nays: 0, turnout: 210 }); + + next_block(); + assert_eq!(Balances::free_balance(42), 0); + + next_block(); + assert_eq!(Balances::free_balance(42), 2); + }); +} + +#[test] +fn lowest_unbaked_should_be_sensible() { + new_test_ext().execute_with(|| { + let r1 = Democracy::inject_referendum(3, set_balance_proposal(1), VoteThreshold::SuperMajorityApprove, 0); + let r2 = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + let r3 = Democracy::inject_referendum(10, set_balance_proposal(3), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r1, aye(1))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r2, aye(1))); + // r3 is canceled + assert_ok!(Democracy::cancel_referendum(RuntimeOrigin::root(), r3.into())); + assert_eq!(Democracy::lowest_unbaked(), 0); + + next_block(); + // r2 ends with approval + assert_eq!(Democracy::lowest_unbaked(), 0); + + next_block(); + // r1 ends with approval + assert_eq!(Democracy::lowest_unbaked(), 3); + assert_eq!(Democracy::lowest_unbaked(), Democracy::referendum_count()); + + // r2 is executed + assert_eq!(Balances::free_balance(42), 2); + + next_block(); + // r1 is executed + assert_eq!(Balances::free_balance(42), 1); + }); +} diff --git a/pallets/democracy/src/tests/voting.rs b/pallets/democracy/src/tests/voting.rs new file mode 100644 index 000000000..f1483583b --- /dev/null +++ b/pallets/democracy/src/tests/voting.rs @@ -0,0 +1,145 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The tests for normal voting functionality. + +use super::*; + +#[test] +fn overvoting_should_fail() { + new_test_ext().execute_with(|| { + let r = begin_referendum(); + assert_noop!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(2)), Error::::InsufficientFunds); + }); +} + +#[test] +fn split_voting_should_work() { + new_test_ext().execute_with(|| { + let r = begin_referendum(); + let v = AccountVote::Split { aye: 40, nay: 20 }; + assert_noop!(Democracy::vote(RuntimeOrigin::signed(5), r, v), Error::::InsufficientFunds); + let v = AccountVote::Split { aye: 30, nay: 20 }; + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, v)); + + assert_eq!(tally(r), Tally { ayes: 3, nays: 2, turnout: 50 }); + }); +} + +#[test] +fn split_vote_cancellation_should_work() { + new_test_ext().execute_with(|| { + let r = begin_referendum(); + let v = AccountVote::Split { aye: 30, nay: 20 }; + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, v)); + assert_ok!(Democracy::remove_vote(RuntimeOrigin::signed(5), r)); + assert_eq!(tally(r), Tally { ayes: 0, nays: 0, turnout: 0 }); + assert_ok!(Democracy::unlock(RuntimeOrigin::signed(5), 5)); + assert_eq!(Balances::locks(5), vec![]); + }); +} + +#[test] +fn single_proposal_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + assert_ok!(propose_set_balance(1, 2, 1)); + let r = 0; + assert!(Democracy::referendum_info(r).is_none()); + + // start of 2 => next referendum scheduled. + fast_forward_to(2); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, aye(1))); + + assert_eq!(Democracy::referendum_count(), 1); + assert_eq!( + Democracy::referendum_status(0), + Ok(ReferendumStatus { + end: 4, + proposal: set_balance_proposal(2), + threshold: VoteThreshold::SuperMajorityApprove, + delay: 2, + tally: Tally { ayes: 1, nays: 0, turnout: 10 }, + }) + ); + + fast_forward_to(3); + + // referendum still running + assert_ok!(Democracy::referendum_status(0)); + + // referendum runs during 2 and 3, ends @ start of 4. + fast_forward_to(4); + + assert_noop!(Democracy::referendum_status(0), Error::::ReferendumInvalid); + assert!(pallet_scheduler::Agenda::::get(6)[0].is_some()); + + // referendum passes and wait another two blocks for enactment. + fast_forward_to(6); + + assert_eq!(Balances::free_balance(42), 2); + }); +} + +#[test] +fn controversial_voting_should_work() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + + assert_ok!(Democracy::vote(RuntimeOrigin::signed(1), r, big_aye(1))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(2), r, big_nay(2))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(3), r, big_nay(3))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(4), r, big_aye(4))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, big_nay(5))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(6), r, big_aye(6))); + + assert_eq!(tally(r), Tally { ayes: 110, nays: 100, turnout: 210 }); + + next_block(); + next_block(); + + assert_eq!(Balances::free_balance(42), 2); + }); +} + +#[test] +fn controversial_low_turnout_voting_should_work() { + new_test_ext().execute_with(|| { + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, big_nay(5))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(6), r, big_aye(6))); + + assert_eq!(tally(r), Tally { ayes: 60, nays: 50, turnout: 110 }); + + next_block(); + next_block(); + + assert_eq!(Balances::free_balance(42), 0); + }); +} + +#[test] +fn passing_low_turnout_voting_should_work() { + new_test_ext().execute_with(|| { + assert_eq!(Balances::free_balance(42), 0); + assert_eq!(Balances::total_issuance(), 210); + + let r = Democracy::inject_referendum(2, set_balance_proposal(2), VoteThreshold::SuperMajorityApprove, 0); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(4), r, big_aye(4))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(5), r, big_nay(5))); + assert_ok!(Democracy::vote(RuntimeOrigin::signed(6), r, big_aye(6))); + assert_eq!(tally(r), Tally { ayes: 100, nays: 50, turnout: 150 }); + + next_block(); + next_block(); + assert_eq!(Balances::free_balance(42), 2); + }); +} diff --git a/pallets/democracy/src/types.rs b/pallets/democracy/src/types.rs new file mode 100644 index 000000000..09eb14f40 --- /dev/null +++ b/pallets/democracy/src/types.rs @@ -0,0 +1,194 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! Miscellaneous additional datatypes. + +use crate::{AccountVote, Conviction, Vote, VoteThreshold}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Bounded, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Saturating, Zero}, + RuntimeDebug, +}; + +/// A proposal index. +pub type PropIndex = u32; + +/// A referendum index. +pub type ReferendumIndex = u32; + +/// Info regarding an ongoing referendum. +#[derive(Encode, MaxEncodedLen, Decode, Default, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct Tally { + /// The number of aye votes, expressed in terms of post-conviction lock-vote. + pub ayes: Balance, + /// The number of nay votes, expressed in terms of post-conviction lock-vote. + pub nays: Balance, + /// The amount of funds currently expressing its opinion. Pre-conviction. + pub turnout: Balance, +} + +/// Amount of votes and capital placed in delegation for an account. +#[derive(Encode, MaxEncodedLen, Decode, Default, Copy, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +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) } + } +} + +impl + Zero + Copy + CheckedAdd + CheckedSub + CheckedMul + CheckedDiv + Bounded + Saturating> + Tally +{ + /// Create a new tally. + pub fn new(vote: Vote, balance: Balance) -> 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, + } + } + + /// 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) -> Option<()> { + 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), + } + Some(()) + } + + /// Decrement some amount of votes. + pub fn reduce(&mut self, approve: bool, delegations: Delegations) -> Option<()> { + 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), + } + Some(()) + } +} + +/// Info regarding an ongoing referendum. +#[derive(Encode, MaxEncodedLen, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct ReferendumStatus { + /// When voting on this referendum will end. + pub end: BlockNumber, + /// The proposal being voted on. + pub proposal: Proposal, + /// The thresholding mechanism to determine whether it passed. + pub threshold: VoteThreshold, + /// The delay (in blocks) to wait after a successful referendum before deploying. + pub delay: BlockNumber, + /// The current tally of votes in this referendum. + pub tally: Tally, +} + +/// Info regarding a referendum, present or past. +#[derive(Encode, MaxEncodedLen, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum ReferendumInfo { + /// Referendum is happening, the arg is the block number at which it will end. + Ongoing(ReferendumStatus), + /// Referendum finished at `end`, and has been `approved` or rejected. + Finished { approved: bool, end: BlockNumber }, +} + +impl ReferendumInfo { + /// Create a new instance. + pub fn new(end: BlockNumber, proposal: Proposal, threshold: VoteThreshold, delay: BlockNumber) -> Self { + let s = ReferendumStatus { end, proposal, threshold, delay, tally: Tally::default() }; + ReferendumInfo::Ongoing(s) + } +} + +/// 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, +} + +/// Identifies an owner of a metadata. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub enum MetadataOwner { + /// External proposal. + External, + /// Public proposal of the index. + Proposal(PropIndex), + /// Referendum of the index. + Referendum(ReferendumIndex), +} diff --git a/pallets/democracy/src/vote.rs b/pallets/democracy/src/vote.rs new file mode 100644 index 000000000..f7413a2b1 --- /dev/null +++ b/pallets/democracy/src/vote.rs @@ -0,0 +1,206 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! The vote datatype. + +use crate::{Conviction, Delegations, ReferendumIndex}; +use frame_support::traits::Get; +use parity_scale_codec::{Decode, Encode, EncodeLike, Input, MaxEncodedLen, Output}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Saturating, Zero}, + BoundedVec, RuntimeDebug, +}; +use sp_std::prelude::*; + +/// A number of lock periods, plus a vote, one way or the other. +#[derive(Copy, Clone, Eq, PartialEq, Default, RuntimeDebug)] +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 MaxEncodedLen for Vote { + fn max_encoded_len() -> usize { + 1 + } +} + +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(|_| parity_scale_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, MaxEncodedLen, Decode, Copy, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)] +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, 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, MaxEncodedLen, Decode, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, +)] +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(); + } + } +} + +/// An indicator for what an account is doing; it can either be delegating or voting. +#[derive(Clone, Encode, Decode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +#[codec(mel_bound(skip_type_params(MaxVotes)))] +#[scale_info(skip_type_params(MaxVotes))] +pub enum Voting> { + /// The account is voting directly. `delegations` is the total amount of post-conviction voting + /// weight that it controls from those that have delegated to it. + Direct { + /// The current votes of the account. + votes: BoundedVec<(ReferendumIndex, AccountVote), MaxVotes>, + /// The total amount of delegations that this account has received. + delegations: Delegations, + /// Any pre-existing locks from past voting/delegating activity. + prior: PriorLock, + }, + /// The account is delegating `balance` of its balance to a `target` account with `conviction`. + Delegating { + balance: Balance, + target: AccountId, + conviction: Conviction, + /// The total amount of delegations that this account has received. + delegations: Delegations, + /// Any pre-existing locks from past voting/delegating activity. + prior: PriorLock, + }, +} + +impl> Default + for Voting +{ + fn default() -> Self { + Voting::Direct { + votes: Default::default(), + delegations: Default::default(), + prior: PriorLock(Zero::zero(), Default::default()), + } + } +} + +impl> + Voting +{ + pub fn rejig(&mut self, now: BlockNumber) { + match self { + Voting::Direct { prior, .. } => prior, + Voting::Delegating { prior, .. } => prior, + } + .rejig(now); + } + + /// The amount of this account's balance that must currently be locked due to voting. + pub fn locked_balance(&self) -> Balance { + match self { + Voting::Direct { votes, prior, .. } => + votes.iter().map(|i| i.1.balance()).fold(prior.locked(), |a, i| a.max(i)), + Voting::Delegating { balance, prior, .. } => *balance.max(&prior.locked()), + } + } + + pub fn set_common(&mut self, delegations: Delegations, prior: PriorLock) { + let (d, p) = match self { + Voting::Direct { ref mut delegations, ref mut prior, .. } => (delegations, prior), + Voting::Delegating { ref mut delegations, ref mut prior, .. } => (delegations, prior), + }; + *d = delegations; + *p = prior; + } + + pub fn prior(&self) -> &PriorLock { + match self { + Voting::Direct { prior, .. } => prior, + Voting::Delegating { prior, .. } => prior, + } + } +} diff --git a/pallets/democracy/src/vote_threshold.rs b/pallets/democracy/src/vote_threshold.rs new file mode 100644 index 000000000..fccfc35a8 --- /dev/null +++ b/pallets/democracy/src/vote_threshold.rs @@ -0,0 +1,109 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! Voting thresholds. + +use crate::Tally; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_runtime::traits::{IntegerSquareRoot, Zero}; +use sp_std::ops::{Add, Div, Mul, Rem}; + +/// A means of determining if a vote is past pass threshold. +#[derive(Clone, Copy, PartialEq, Eq, Encode, MaxEncodedLen, Decode, sp_runtime::RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum VoteThreshold { + /// A supermajority of approvals is needed to pass this vote. + SuperMajorityApprove, + /// A supermajority of rejects is needed to fail this vote. + SuperMajorityAgainst, + /// A simple majority of approvals is needed to pass this vote. + SimpleMajority, +} + +pub trait Approved { + /// Given a `tally` of votes and a total size of `electorate`, this returns `true` if the + /// overall outcome is in favor of approval according to `self`'s threshold method. + fn approved(&self, tally: Tally, electorate: Balance) -> bool; +} + +/// Return `true` iff `n1 / d1 < n2 / d2`. `d1` and `d2` may not be zero. +fn compare_rationals + Div + Rem + Ord + Copy>( + mut n1: T, + mut d1: T, + mut n2: T, + mut d2: T, +) -> bool { + // Uses a continued fractional representation for a non-overflowing compare. + // Detailed at https://janmr.com/blog/2014/05/comparing-rational-numbers-without-overflow/. + loop { + let q1 = n1 / d1; + let q2 = n2 / d2; + if q1 < q2 { + return true + } + if q2 < q1 { + return false + } + let r1 = n1 % d1; + let r2 = n2 % d2; + if r2.is_zero() { + return false + } + if r1.is_zero() { + return true + } + n1 = d2; + n2 = d1; + d1 = r2; + d2 = r1; + } +} + +impl< + Balance: IntegerSquareRoot + + Zero + + Ord + + Add + + Mul + + Div + + Rem + + Copy, + > Approved for VoteThreshold +{ + fn approved(&self, tally: Tally, electorate: Balance) -> bool { + let sqrt_voters = tally.turnout.integer_sqrt(); + let sqrt_electorate = electorate.integer_sqrt(); + if sqrt_voters.is_zero() { + return false + } + match *self { + VoteThreshold::SuperMajorityApprove => + compare_rationals(tally.nays, sqrt_voters, tally.ayes, sqrt_electorate), + VoteThreshold::SuperMajorityAgainst => + compare_rationals(tally.nays, sqrt_electorate, tally.ayes, sqrt_voters), + VoteThreshold::SimpleMajority => tally.ayes > tally.nays, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_work() { + assert!(!VoteThreshold::SuperMajorityApprove.approved(Tally { ayes: 60, nays: 50, turnout: 110 }, 210)); + assert!(VoteThreshold::SuperMajorityApprove.approved(Tally { ayes: 100, nays: 50, turnout: 150 }, 210)); + } +} diff --git a/pallets/democracy/src/weights.rs b/pallets/democracy/src/weights.rs new file mode 100644 index 000000000..232747aba --- /dev/null +++ b/pallets/democracy/src/weights.rs @@ -0,0 +1,980 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! Autogenerated weights for pallet_democracy +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-06-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-e8ezs4ez-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_democracy +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/democracy/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_democracy. +pub trait WeightInfo { + fn propose() -> Weight; + fn second() -> Weight; + fn vote_new() -> Weight; + fn vote_existing() -> Weight; + fn emergency_cancel() -> Weight; + fn blacklist() -> Weight; + fn external_propose() -> Weight; + fn external_propose_majority() -> Weight; + fn external_propose_default() -> Weight; + fn fast_track() -> Weight; + fn veto_external() -> Weight; + fn cancel_proposal() -> Weight; + fn cancel_referendum() -> Weight; + fn on_initialize_base(r: u32, ) -> Weight; + fn on_initialize_base_with_launch_period(r: u32, ) -> Weight; + fn delegate(r: u32, ) -> Weight; + fn undelegate(r: u32, ) -> Weight; + fn clear_public_proposals() -> Weight; + fn unlock_remove(r: u32, ) -> Weight; + fn unlock_set(r: u32, ) -> Weight; + fn remove_vote(r: u32, ) -> Weight; + fn remove_other_vote(r: u32, ) -> Weight; + fn set_external_metadata() -> Weight; + fn clear_external_metadata() -> Weight; + fn set_proposal_metadata() -> Weight; + fn clear_proposal_metadata() -> Weight; + fn set_referendum_metadata() -> Weight; + fn clear_referendum_metadata() -> Weight; +} + +/// Weights for pallet_democracy using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Democracy PublicPropCount (r:1 w:1) + /// Proof: Democracy PublicPropCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy PublicProps (r:1 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:1 w:0) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + /// Storage: Democracy DepositOf (r:0 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + fn propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `4801` + // Estimated: `18187` + // Minimum execution time: 49_339_000 picoseconds. + Weight::from_parts(50_942_000, 18187) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy DepositOf (r:1 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + fn second() -> Weight { + // Proof Size summary in bytes: + // Measured: `3556` + // Estimated: `6695` + // Minimum execution time: 43_291_000 picoseconds. + Weight::from_parts(44_856_000, 6695) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn vote_new() -> Weight { + // Proof Size summary in bytes: + // Measured: `3470` + // Estimated: `7260` + // Minimum execution time: 61_890_000 picoseconds. + Weight::from_parts(63_626_000, 7260) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn vote_existing() -> Weight { + // Proof Size summary in bytes: + // Measured: `3492` + // Estimated: `7260` + // Minimum execution time: 67_802_000 picoseconds. + Weight::from_parts(69_132_000, 7260) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy Cancellations (r:1 w:1) + /// Proof: Democracy Cancellations (max_values: None, max_size: Some(33), added: 2508, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn emergency_cancel() -> Weight { + // Proof Size summary in bytes: + // Measured: `366` + // Estimated: `3666` + // Minimum execution time: 25_757_000 picoseconds. + Weight::from_parts(27_226_000, 3666) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy DepositOf (r:1 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:3 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:0 w:1) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + fn blacklist() -> Weight { + // Proof Size summary in bytes: + // Measured: `5910` + // Estimated: `18187` + // Minimum execution time: 113_060_000 picoseconds. + Weight::from_parts(114_813_000, 18187) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:1 w:0) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + fn external_propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `3416` + // Estimated: `6703` + // Minimum execution time: 13_413_000 picoseconds. + Weight::from_parts(13_794_000, 6703) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:0 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + fn external_propose_majority() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_213_000 picoseconds. + Weight::from_parts(3_429_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:0 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + fn external_propose_default() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_280_000 picoseconds. + Weight::from_parts(3_389_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumCount (r:1 w:1) + /// Proof: Democracy ReferendumCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:2) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:0 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + fn fast_track() -> Weight { + // Proof Size summary in bytes: + // Measured: `286` + // Estimated: `3518` + // Minimum execution time: 28_142_000 picoseconds. + Weight::from_parts(28_862_000, 3518) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:1 w:1) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn veto_external() -> Weight { + // Proof Size summary in bytes: + // Measured: `3519` + // Estimated: `6703` + // Minimum execution time: 32_395_000 picoseconds. + Weight::from_parts(33_617_000, 6703) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy DepositOf (r:1 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn cancel_proposal() -> Weight { + // Proof Size summary in bytes: + // Measured: `5821` + // Estimated: `18187` + // Minimum execution time: 92_255_000 picoseconds. + Weight::from_parts(93_704_000, 18187) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:0 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + fn cancel_referendum() -> Weight { + // Proof Size summary in bytes: + // Measured: `271` + // Estimated: `3518` + // Minimum execution time: 19_623_000 picoseconds. + Weight::from_parts(20_545_000, 3518) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Democracy LowestUnbaked (r:1 w:1) + /// Proof: Democracy LowestUnbaked (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumCount (r:1 w:0) + /// Proof: Democracy ReferendumCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:0) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn on_initialize_base(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `244 + r * (86 ±0)` + // Estimated: `1489 + r * (2676 ±0)` + // Minimum execution time: 7_032_000 picoseconds. + Weight::from_parts(7_931_421, 1489) + // Standard Error: 7_395 + .saturating_add(Weight::from_parts(3_236_964, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy LowestUnbaked (r:1 w:1) + /// Proof: Democracy LowestUnbaked (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumCount (r:1 w:0) + /// Proof: Democracy ReferendumCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy LastTabledWasExternal (r:1 w:0) + /// Proof: Democracy LastTabledWasExternal (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: Democracy NextExternal (r:1 w:0) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy PublicProps (r:1 w:0) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:0) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn on_initialize_base_with_launch_period(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `244 + r * (86 ±0)` + // Estimated: `18187 + r * (2676 ±0)` + // Minimum execution time: 10_524_000 picoseconds. + Weight::from_parts(10_369_064, 18187) + // Standard Error: 8_385 + .saturating_add(Weight::from_parts(3_242_334, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy VotingOf (r:3 w:3) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:99) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn delegate(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `830 + r * (108 ±0)` + // Estimated: `19800 + r * (2676 ±0)` + // Minimum execution time: 46_106_000 picoseconds. + Weight::from_parts(48_936_654, 19800) + // Standard Error: 8_879 + .saturating_add(Weight::from_parts(4_708_141, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(r.into()))) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy VotingOf (r:2 w:2) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:99) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn undelegate(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `493 + r * (108 ±0)` + // Estimated: `13530 + r * (2676 ±0)` + // Minimum execution time: 21_078_000 picoseconds. + Weight::from_parts(22_732_737, 13530) + // Standard Error: 7_969 + .saturating_add(Weight::from_parts(4_626_458, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(r.into()))) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy PublicProps (r:0 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + fn clear_public_proposals() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_229_000 picoseconds. + Weight::from_parts(3_415_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn unlock_remove(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `563` + // Estimated: `7260` + // Minimum execution time: 25_735_000 picoseconds. + Weight::from_parts(41_341_468, 7260) + // Standard Error: 3_727 + .saturating_add(Weight::from_parts(94_755, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn unlock_set(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `564 + r * (22 ±0)` + // Estimated: `7260` + // Minimum execution time: 36_233_000 picoseconds. + Weight::from_parts(39_836_017, 7260) + // Standard Error: 1_791 + .saturating_add(Weight::from_parts(132_158, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// The range of component `r` is `[1, 100]`. + fn remove_vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `728 + r * (26 ±0)` + // Estimated: `7260` + // Minimum execution time: 16_081_000 picoseconds. + Weight::from_parts(19_624_101, 7260) + // Standard Error: 1_639 + .saturating_add(Weight::from_parts(133_630, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// The range of component `r` is `[1, 100]`. + fn remove_other_vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `728 + r * (26 ±0)` + // Estimated: `7260` + // Minimum execution time: 15_634_000 picoseconds. + Weight::from_parts(19_573_407, 7260) + // Standard Error: 1_790 + .saturating_add(Weight::from_parts(139_707, 0).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:0) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Preimage StatusFor (r:1 w:0) + /// Proof: Preimage StatusFor (max_values: None, max_size: Some(91), added: 2566, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:0 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn set_external_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `356` + // Estimated: `3556` + // Minimum execution time: 18_344_000 picoseconds. + Weight::from_parts(18_727_000, 3556) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:0) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn clear_external_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `286` + // Estimated: `3518` + // Minimum execution time: 16_497_000 picoseconds. + Weight::from_parts(16_892_000, 3518) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:0) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Preimage StatusFor (r:1 w:0) + /// Proof: Preimage StatusFor (max_values: None, max_size: Some(91), added: 2566, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:0 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn set_proposal_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `4888` + // Estimated: `18187` + // Minimum execution time: 39_517_000 picoseconds. + Weight::from_parts(40_632_000, 18187) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:0) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn clear_proposal_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `4822` + // Estimated: `18187` + // Minimum execution time: 37_108_000 picoseconds. + Weight::from_parts(37_599_000, 18187) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Preimage StatusFor (r:1 w:0) + /// Proof: Preimage StatusFor (max_values: None, max_size: Some(91), added: 2566, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:0 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn set_referendum_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `144` + // Estimated: `3556` + // Minimum execution time: 13_997_000 picoseconds. + Weight::from_parts(14_298_000, 3556) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:0) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn clear_referendum_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `302` + // Estimated: `3666` + // Minimum execution time: 18_122_000 picoseconds. + Weight::from_parts(18_655_000, 3666) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Democracy PublicPropCount (r:1 w:1) + /// Proof: Democracy PublicPropCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy PublicProps (r:1 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:1 w:0) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + /// Storage: Democracy DepositOf (r:0 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + fn propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `4801` + // Estimated: `18187` + // Minimum execution time: 49_339_000 picoseconds. + Weight::from_parts(50_942_000, 18187) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy DepositOf (r:1 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + fn second() -> Weight { + // Proof Size summary in bytes: + // Measured: `3556` + // Estimated: `6695` + // Minimum execution time: 43_291_000 picoseconds. + Weight::from_parts(44_856_000, 6695) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn vote_new() -> Weight { + // Proof Size summary in bytes: + // Measured: `3470` + // Estimated: `7260` + // Minimum execution time: 61_890_000 picoseconds. + Weight::from_parts(63_626_000, 7260) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn vote_existing() -> Weight { + // Proof Size summary in bytes: + // Measured: `3492` + // Estimated: `7260` + // Minimum execution time: 67_802_000 picoseconds. + Weight::from_parts(69_132_000, 7260) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy Cancellations (r:1 w:1) + /// Proof: Democracy Cancellations (max_values: None, max_size: Some(33), added: 2508, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn emergency_cancel() -> Weight { + // Proof Size summary in bytes: + // Measured: `366` + // Estimated: `3666` + // Minimum execution time: 25_757_000 picoseconds. + Weight::from_parts(27_226_000, 3666) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy DepositOf (r:1 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:3 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:0 w:1) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + fn blacklist() -> Weight { + // Proof Size summary in bytes: + // Measured: `5910` + // Estimated: `18187` + // Minimum execution time: 113_060_000 picoseconds. + Weight::from_parts(114_813_000, 18187) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:1 w:0) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + fn external_propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `3416` + // Estimated: `6703` + // Minimum execution time: 13_413_000 picoseconds. + Weight::from_parts(13_794_000, 6703) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:0 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + fn external_propose_majority() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_213_000 picoseconds. + Weight::from_parts(3_429_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:0 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + fn external_propose_default() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_280_000 picoseconds. + Weight::from_parts(3_389_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumCount (r:1 w:1) + /// Proof: Democracy ReferendumCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:2) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:0 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + fn fast_track() -> Weight { + // Proof Size summary in bytes: + // Measured: `286` + // Estimated: `3518` + // Minimum execution time: 28_142_000 picoseconds. + Weight::from_parts(28_862_000, 3518) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:1) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy Blacklist (r:1 w:1) + /// Proof: Democracy Blacklist (max_values: None, max_size: Some(3238), added: 5713, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn veto_external() -> Weight { + // Proof Size summary in bytes: + // Measured: `3519` + // Estimated: `6703` + // Minimum execution time: 32_395_000 picoseconds. + Weight::from_parts(33_617_000, 6703) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy DepositOf (r:1 w:1) + /// Proof: Democracy DepositOf (max_values: None, max_size: Some(3230), added: 5705, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn cancel_proposal() -> Weight { + // Proof Size summary in bytes: + // Measured: `5821` + // Estimated: `18187` + // Minimum execution time: 92_255_000 picoseconds. + Weight::from_parts(93_704_000, 18187) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:0 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + fn cancel_referendum() -> Weight { + // Proof Size summary in bytes: + // Measured: `271` + // Estimated: `3518` + // Minimum execution time: 19_623_000 picoseconds. + Weight::from_parts(20_545_000, 3518) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Democracy LowestUnbaked (r:1 w:1) + /// Proof: Democracy LowestUnbaked (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumCount (r:1 w:0) + /// Proof: Democracy ReferendumCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:0) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn on_initialize_base(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `244 + r * (86 ±0)` + // Estimated: `1489 + r * (2676 ±0)` + // Minimum execution time: 7_032_000 picoseconds. + Weight::from_parts(7_931_421, 1489) + // Standard Error: 7_395 + .saturating_add(Weight::from_parts(3_236_964, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy LowestUnbaked (r:1 w:1) + /// Proof: Democracy LowestUnbaked (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumCount (r:1 w:0) + /// Proof: Democracy ReferendumCount (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: Democracy LastTabledWasExternal (r:1 w:0) + /// Proof: Democracy LastTabledWasExternal (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: Democracy NextExternal (r:1 w:0) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy PublicProps (r:1 w:0) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:0) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn on_initialize_base_with_launch_period(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `244 + r * (86 ±0)` + // Estimated: `18187 + r * (2676 ±0)` + // Minimum execution time: 10_524_000 picoseconds. + Weight::from_parts(10_369_064, 18187) + // Standard Error: 8_385 + .saturating_add(Weight::from_parts(3_242_334, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy VotingOf (r:3 w:3) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:99) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn delegate(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `830 + r * (108 ±0)` + // Estimated: `19800 + r * (2676 ±0)` + // Minimum execution time: 46_106_000 picoseconds. + Weight::from_parts(48_936_654, 19800) + // Standard Error: 8_879 + .saturating_add(Weight::from_parts(4_708_141, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(r.into()))) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy VotingOf (r:2 w:2) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Democracy ReferendumInfoOf (r:99 w:99) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn undelegate(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `493 + r * (108 ±0)` + // Estimated: `13530 + r * (2676 ±0)` + // Minimum execution time: 21_078_000 picoseconds. + Weight::from_parts(22_732_737, 13530) + // Standard Error: 7_969 + .saturating_add(Weight::from_parts(4_626_458, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(r.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(r.into()))) + .saturating_add(Weight::from_parts(0, 2676).saturating_mul(r.into())) + } + /// Storage: Democracy PublicProps (r:0 w:1) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + fn clear_public_proposals() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_229_000 picoseconds. + Weight::from_parts(3_415_000, 0) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn unlock_remove(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `563` + // Estimated: `7260` + // Minimum execution time: 25_735_000 picoseconds. + Weight::from_parts(41_341_468, 7260) + // Standard Error: 3_727 + .saturating_add(Weight::from_parts(94_755, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `r` is `[0, 99]`. + fn unlock_set(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `564 + r * (22 ±0)` + // Estimated: `7260` + // Minimum execution time: 36_233_000 picoseconds. + Weight::from_parts(39_836_017, 7260) + // Standard Error: 1_791 + .saturating_add(Weight::from_parts(132_158, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// The range of component `r` is `[1, 100]`. + fn remove_vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `728 + r * (26 ±0)` + // Estimated: `7260` + // Minimum execution time: 16_081_000 picoseconds. + Weight::from_parts(19_624_101, 7260) + // Standard Error: 1_639 + .saturating_add(Weight::from_parts(133_630, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:1) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy VotingOf (r:1 w:1) + /// Proof: Democracy VotingOf (max_values: None, max_size: Some(3795), added: 6270, mode: MaxEncodedLen) + /// The range of component `r` is `[1, 100]`. + fn remove_other_vote(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `728 + r * (26 ±0)` + // Estimated: `7260` + // Minimum execution time: 15_634_000 picoseconds. + Weight::from_parts(19_573_407, 7260) + // Standard Error: 1_790 + .saturating_add(Weight::from_parts(139_707, 0).saturating_mul(r.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:0) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Preimage StatusFor (r:1 w:0) + /// Proof: Preimage StatusFor (max_values: None, max_size: Some(91), added: 2566, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:0 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn set_external_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `356` + // Estimated: `3556` + // Minimum execution time: 18_344_000 picoseconds. + Weight::from_parts(18_727_000, 3556) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy NextExternal (r:1 w:0) + /// Proof: Democracy NextExternal (max_values: Some(1), max_size: Some(132), added: 627, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn clear_external_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `286` + // Estimated: `3518` + // Minimum execution time: 16_497_000 picoseconds. + Weight::from_parts(16_892_000, 3518) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:0) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Preimage StatusFor (r:1 w:0) + /// Proof: Preimage StatusFor (max_values: None, max_size: Some(91), added: 2566, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:0 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn set_proposal_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `4888` + // Estimated: `18187` + // Minimum execution time: 39_517_000 picoseconds. + Weight::from_parts(40_632_000, 18187) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy PublicProps (r:1 w:0) + /// Proof: Democracy PublicProps (max_values: Some(1), max_size: Some(16702), added: 17197, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn clear_proposal_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `4822` + // Estimated: `18187` + // Minimum execution time: 37_108_000 picoseconds. + Weight::from_parts(37_599_000, 18187) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Preimage StatusFor (r:1 w:0) + /// Proof: Preimage StatusFor (max_values: None, max_size: Some(91), added: 2566, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:0 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn set_referendum_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `144` + // Estimated: `3556` + // Minimum execution time: 13_997_000 picoseconds. + Weight::from_parts(14_298_000, 3556) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Democracy ReferendumInfoOf (r:1 w:0) + /// Proof: Democracy ReferendumInfoOf (max_values: None, max_size: Some(201), added: 2676, mode: MaxEncodedLen) + /// Storage: Democracy MetadataOf (r:1 w:1) + /// Proof: Democracy MetadataOf (max_values: None, max_size: Some(53), added: 2528, mode: MaxEncodedLen) + fn clear_referendum_metadata() -> Weight { + // Proof Size summary in bytes: + // Measured: `302` + // Estimated: `3666` + // Minimum execution time: 18_122_000 picoseconds. + Weight::from_parts(18_655_000, 3666) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/pallets/elections-phragmen/CHANGELOG.md b/pallets/elections-phragmen/CHANGELOG.md new file mode 100644 index 000000000..231de1d2e --- /dev/null +++ b/pallets/elections-phragmen/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog +All notable changes to this crate will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this crate adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [4.0.0] - UNRELEASED + +### Added + +### Changed +\[**Needs Migration**\] [migrate pallet-elections-phragmen to attribute macros](https://github.com/paritytech/substrate/pull/8044) + +### Fixed + +### Security + +## [3.0.0] + +### Added +[Add slashing events to elections-phragmen](https://github.com/paritytech/substrate/pull/7543) + +### Changed + +### Fixed +[Don't slash all outgoing members](https://github.com/paritytech/substrate/pull/7394) +[Fix wrong outgoing calculation in election](https://github.com/paritytech/substrate/pull/7384) + +### Security +\[**Needs Migration**\] [Fix elections-phragmen and proxy issue + Record deposits on-chain](https://github.com/paritytech/substrate/pull/7040) + +## [2.0.0] - 2020-09-2020 + +Initial version from which version tracking has begun. + diff --git a/pallets/elections-phragmen/Cargo.toml b/pallets/elections-phragmen/Cargo.toml new file mode 100644 index 000000000..05a690229 --- /dev/null +++ b/pallets/elections-phragmen/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "pallet-elections-phragmen" +version = "5.0.0-dev" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage = "https://substrate.io" +repository.workspace = true +description = "FRAME pallet based on seq-Phragmén election method." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } +log.workspace = true +frame-benchmarking = { workspace = true, optional = true } +frame-support.workspace = true +frame-system.workspace = true +sp-core.workspace = true +sp-io.workspace = true +sp-npos-elections.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +sp-staking.workspace = true + +[dev-dependencies] +pallet-balances.workspace = true +sp-core.workspace = true +sp-tracing.workspace = true +substrate-test-utils.workspace = true + +[features] +default = [ "std" ] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "parity-scale-codec/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-npos-elections/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", + "sp-tracing/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "sp-staking/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/elections-phragmen/README.md b/pallets/elections-phragmen/README.md new file mode 100644 index 000000000..d529b9c23 --- /dev/null +++ b/pallets/elections-phragmen/README.md @@ -0,0 +1,69 @@ +# Phragmén Election Module +This repository contains a modified version of Parity's 'elections_phragmen' Substrate Pallet. The original version can be found [here](https://github.com/paritytech/polkadot-sdk/tree/481165d92297d7dfd5eaf9c7f442441761fc0a12/substrate/frame/elections-phragmen). + +## Modifications +The modifications to the original pallet include the following: +1. Currency traits have been replaced by the Fungibles traits. +2. Deposit requirement for voters has been removed +3. Voters can vote with their full balance (placing a lock over both free and reserved balance) +4. Voters tokens are locked up for the duration of `VotingLockPeriod`. + +An election module based on sequential phragmen. + +## Term and Round + +The election happens in _rounds_: every `N` blocks, all previous members are retired and a new set is elected (which may +or may not have an intersection with the previous set). Each round lasts for some number of blocks defined by +`TermDuration` storage item. The words _term_ and _round_ can be used interchangeably in this context. + +`TermDuration` might change during a round. This can shorten or extend the length of the round. The next election +round's block number is never stored but rather always checked on the fly. Based on the current block number and +`TermDuration`, the condition `BlockNumber % TermDuration == 0` being satisfied will always trigger a new election +round. + +## Voting + +Voters can vote for any set of the candidates by providing a list of account ids. Invalid votes (voting for +non-candidates) are ignored during election. Yet, a voter _might_ vote for a future candidate. Voters reserve a bond as +they vote. Each vote defines a `value`. This amount is locked from the account of the voter and indicates the weight of +the vote. Voters can update their votes at any time by calling `vote()` again. This keeps the bond untouched but can +optionally change the locked `value`. After a round, votes are kept and might still be valid for further rounds. A voter +is responsible for calling `remove_voter` once they are done to have their bond back and remove the lock. + +Voters also report other voters as being defunct to earn their bond. A voter is defunct once all of the candidates that +they have voted for are neither a valid candidate anymore nor a member. Upon reporting, if the target voter is actually +defunct, the reporter will be rewarded by the voting bond of the target. The target will lose their bond and get +removed. If the target is not defunct, the reporter is slashed and removed. To prevent being reported, voters should +manually submit a `remove_voter()` as soon as they are in the defunct state. + +## Candidacy and Members + +Candidates also reserve a bond as they submit candidacy. A candidate cannot take their candidacy back. A candidate can +end up in one of the below situations: + - **Winner**: A winner is kept as a _member_. They must still have a bond in reserve and they are automatically + counted as a candidate for the next election. + - **Runner-up**: Runners-up are the best candidates immediately after the winners. The number of runners_up to keep is + configurable. Runners-up are used, in order that they are elected, as replacements when a candidate is kicked by + `[remove_member]`, or when an active member renounces their candidacy. Runners are automatically counted as a + candidate for the next election. + - **Loser**: Any of the candidate who are not a winner are left as losers. A loser might be an _outgoing member or + runner_, meaning that they are an active member who failed to keep their spot. An outgoing will always lose their + bond. + +### Renouncing candidacy + +All candidates, elected or not, can renounce their candidacy. A call to [`Module::renounce_candidacy`] will always cause +the candidacy bond to be refunded. + +Note that with the members being the default candidates for the next round and votes persisting in storage, the election +system is entirely stable given no further input. This means that if the system has a particular set of candidates `C` +and voters `V` that lead to a set of members `M` being elected, as long as `V` and `C` don't remove their candidacy and +votes, `M` will keep being re-elected at the end of each round. + +## Module Information + +- [`election_sp_phragmen::Config`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/trait.Config.html) +- [`Call`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/enum.Call.html) +- [`Module`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/struct.Module.html) + +License: Apache-2.0 diff --git a/pallets/elections-phragmen/src/benchmarking.rs b/pallets/elections-phragmen/src/benchmarking.rs new file mode 100644 index 000000000..517bff81f --- /dev/null +++ b/pallets/elections-phragmen/src/benchmarking.rs @@ -0,0 +1,421 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! Elections-Phragmen pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::v1::{account, benchmarks, whitelist, BenchmarkError, BenchmarkResult}; +use frame_support::{ + dispatch::DispatchResultWithPostInfo, + traits::{fungible::Balanced, OnInitialize}, +}; +use frame_system::RawOrigin; + +use crate::Pallet as Elections; + +const BALANCE_FACTOR: u32 = 250; + +/// grab new account with infinite balance. +fn endowed_account(name: &'static str, index: u32) -> T::AccountId { + let account: T::AccountId = account(name, index, 0); + // Fund each account with at-least their stake but still a sane amount as to not mess up + // the vote calculation. + let amount = default_stake::(T::MaxVoters::get()) * BalanceOf::::from(BALANCE_FACTOR); + let _ = T::Currency::set_balance(&account, amount); + // important to increase the total issuance since T::CurrencyToVote will need it to be sane for + // phragmen to work. + let _ = T::Currency::issue(amount); + + account +} + +/// Account to lookup type of system trait. +fn as_lookup(account: T::AccountId) -> AccountIdLookupOf { + T::Lookup::unlookup(account) +} + +/// Get a reasonable amount of stake based on the execution trait's configuration +fn default_stake(_num_votes: u32) -> BalanceOf { + let min = T::Currency::minimum_balance(); + min * 100u32.into() +} + +/// Get the current number of candidates. +fn candidate_count() -> u32 { + >::decode_len().unwrap_or(0usize) as u32 +} + +/// Add `c` new candidates. +fn submit_candidates(c: u32, prefix: &'static str) -> Result, &'static str> { + (0..c) + .map(|i| { + let account = endowed_account::(prefix, i); + >::submit_candidacy(RawOrigin::Signed(account.clone()).into(), candidate_count::()) + .map_err(|_| "failed to submit candidacy")?; + Ok(account) + }) + .collect::>() +} + +/// Add `c` new candidates with self vote. +fn submit_candidates_with_self_vote( + c: u32, + prefix: &'static str, +) -> Result, &'static str> { + let candidates = submit_candidates::(c, prefix)?; + let stake = default_stake::(c); + let _ = candidates.iter().try_for_each(|c| submit_voter::(c.clone(), vec![c.clone()], stake).map(|_| ()))?; + Ok(candidates) +} + +/// Submit one voter. +fn submit_voter( + caller: T::AccountId, + votes: Vec, + stake: BalanceOf, +) -> DispatchResultWithPostInfo { + >::vote(RawOrigin::Signed(caller).into(), votes, stake) +} + +/// create `num_voter` voters who randomly vote for at most `votes` of `all_candidates` if +/// available. +fn distribute_voters( + mut all_candidates: Vec, + num_voters: u32, + votes: usize, +) -> Result<(), &'static str> { + let stake = default_stake::(num_voters); + for i in 0..num_voters { + // to ensure that votes are different + all_candidates.rotate_left(1); + let votes = all_candidates.iter().cloned().take(votes).collect::>(); + let voter = endowed_account::("voter", i); + submit_voter::(voter, votes, stake)?; + } + Ok(()) +} + +/// Fill the seats of members and runners-up up until `m`. Note that this might include either only +/// members, or members and runners-up. +fn fill_seats_up_to(m: u32) -> Result, &'static str> { + let _ = submit_candidates_with_self_vote::(m, "fill_seats_up_to")?; + assert_eq!(>::candidates().len() as u32, m, "wrong number of candidates."); + >::do_phragmen(); + assert_eq!(>::candidates().len(), 0, "some candidates remaining."); + assert_eq!( + >::members().len() + >::runners_up().len(), + m as usize, + "wrong number of members and runners-up", + ); + Ok(>::members() + .into_iter() + .map(|m| m.who) + .chain(>::runners_up().into_iter().map(|r| r.who)) + .collect()) +} + +/// removes all the storage items to reverse any genesis state. +fn clean() { + >::kill(); + >::kill(); + >::kill(); + #[allow(deprecated)] + >::remove_all(None); +} + +benchmarks! { + // -- Signed ones + vote_equal { + let v in 1 .. T::MaxVotesPerVoter::get(); + clean::(); + + // create a bunch of candidates. + let all_candidates = submit_candidates::(v, "candidates")?; + + let caller = endowed_account::("caller", 0); + let stake = default_stake::(v); + + // original votes. + let mut votes = all_candidates; + submit_voter::(caller.clone(), votes.clone(), stake)?; + + // new votes. + votes.rotate_left(1); + + whitelist!(caller); + }: vote(RawOrigin::Signed(caller), votes, stake) + + vote_more { + let v in 2 .. T::MaxVotesPerVoter::get(); + clean::(); + + // create a bunch of candidates. + let all_candidates = submit_candidates::(v, "candidates")?; + + let caller = endowed_account::("caller", 0); + // Multiply the stake with 10 since we want to be able to divide it by 10 again. + let stake = default_stake::(v) * BalanceOf::::from(10u32); + + // original votes. + let mut votes = all_candidates.iter().skip(1).cloned().collect::>(); + submit_voter::(caller.clone(), votes.clone(), stake / >::from(10u32))?; + + // new votes. + votes = all_candidates; + assert!(votes.len() > >::get(caller.clone()).votes.len()); + + whitelist!(caller); + }: vote(RawOrigin::Signed(caller), votes, stake / >::from(10u32)) + + vote_less { + let v in 2 .. T::MaxVotesPerVoter::get(); + clean::(); + + // create a bunch of candidates. + let all_candidates = submit_candidates::(v, "candidates")?; + + let caller = endowed_account::("caller", 0); + let stake = default_stake::(v); + + // original votes. + let mut votes = all_candidates; + submit_voter::(caller.clone(), votes.clone(), stake)?; + + // new votes. + votes = votes.into_iter().skip(1).collect::>(); + assert!(votes.len() < >::get(caller.clone()).votes.len()); + + whitelist!(caller); + }: vote(RawOrigin::Signed(caller), votes, stake) + + remove_voter { + // we fix the number of voted candidates to max + let v = T::MaxVotesPerVoter::get(); + clean::(); + + // create a bunch of candidates. + let all_candidates = submit_candidates::(v, "candidates")?; + + let caller = endowed_account::("caller", 0); + + let stake = default_stake::(v); + submit_voter::(caller.clone(), all_candidates, stake)?; + >::set_block_number( + T::VotingLockPeriod::get() + 1u32.into() + ); + whitelist!(caller); + }: _(RawOrigin::Signed(caller)) + + submit_candidacy { + // number of already existing candidates. + let c in 1 .. T::MaxCandidates::get(); + // we fix the number of members to the number of desired members and runners-up. We'll be in + // this state almost always. + let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); + + clean::(); + let stake = default_stake::(c); + + // create m members and runners combined. + let _ = fill_seats_up_to::(m)?; + + // create previous candidates; + let _ = submit_candidates::(c, "candidates")?; + + // we assume worse case that: extrinsic is successful and candidate is not duplicate. + let candidate_account = endowed_account::("caller", 0); + whitelist!(candidate_account); + }: _(RawOrigin::Signed(candidate_account.clone()), candidate_count::()) + verify { + #[cfg(test)] + { + // reset members in between benchmark tests. + use crate::tests::MEMBERS; + MEMBERS.with(|m| *m.borrow_mut() = vec![]); + } + } + + renounce_candidacy_candidate { + // this will check members, runners-up and candidate for removal. Members and runners-up are + // limited by the runtime bound, nonetheless we fill them by `m`. + // number of already existing candidates. + let c in 1 .. T::MaxCandidates::get(); + // we fix the number of members to the number of desired members and runners-up. We'll be in + // this state almost always. + let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); + + clean::(); + + // create m members and runners combined. + let _ = fill_seats_up_to::(m)?; + let all_candidates = submit_candidates::(c, "caller")?; + + let bailing = all_candidates[0].clone(); // Should be ("caller", 0) + let count = candidate_count::(); + whitelist!(bailing); + }: renounce_candidacy(RawOrigin::Signed(bailing), Renouncing::Candidate(count)) + verify { + #[cfg(test)] + { + // reset members in between benchmark tests. + use crate::tests::MEMBERS; + MEMBERS.with(|m| *m.borrow_mut() = vec![]); + } + } + + renounce_candidacy_members { + // removing members and runners will be cheaper than a candidate. + // we fix the number of members to when members and runners-up to the desired. We'll be in + // this state almost always. + let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); + clean::(); + + // create m members and runners combined. + let members_and_runners_up = fill_seats_up_to::(m)?; + + let bailing = members_and_runners_up[0].clone(); + assert!(>::is_member(&bailing)); + + whitelist!(bailing); + }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), Renouncing::Member) + verify { + #[cfg(test)] + { + // reset members in between benchmark tests. + use crate::tests::MEMBERS; + MEMBERS.with(|m| *m.borrow_mut() = vec![]); + } + } + + renounce_candidacy_runners_up { + // removing members and runners will be cheaper than a candidate. + // we fix the number of members to when members and runners-up to the desired. We'll be in + // this state almost always. + let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); + clean::(); + + // create m members and runners combined. + let members_and_runners_up = fill_seats_up_to::(m)?; + + let bailing = members_and_runners_up[T::DesiredMembers::get() as usize + 1].clone(); + assert!(>::is_runner_up(&bailing)); + + whitelist!(bailing); + }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), Renouncing::RunnerUp) + verify { + #[cfg(test)] + { + // reset members in between benchmark tests. + use crate::tests::MEMBERS; + MEMBERS.with(|m| *m.borrow_mut() = vec![]); + } + } + + // We use the max block weight for this extrinsic for now. See below. + remove_member_without_replacement {}: { + Err(BenchmarkError::Override( + BenchmarkResult::from_weight(T::BlockWeights::get().max_block) + ))?; + } + + remove_member_with_replacement { + // easy case. We have a runner up. Nothing will have that much of an impact. m will be + // number of members and runners. There is always at least one runner. + let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); + clean::(); + + let _ = fill_seats_up_to::(m)?; + let removing = as_lookup::(>::members_ids()[0].clone()); + }: remove_member(RawOrigin::Root, removing, true, false) + verify { + // must still have enough members. + assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); + #[cfg(test)] + { + // reset members in between benchmark tests. + use crate::tests::MEMBERS; + MEMBERS.with(|m| *m.borrow_mut() = vec![]); + } + } + + clean_defunct_voters { + // total number of voters. + let v in (T::MaxVoters::get() / 2) .. T::MaxVoters::get(); + // those that are defunct and need removal. + let d in 0 .. (T::MaxVoters::get() / 2); + + // remove any previous stuff. + clean::(); + + let all_candidates = submit_candidates::(T::MaxCandidates::get(), "candidates")?; + distribute_voters::(all_candidates, v, T::MaxVotesPerVoter::get() as usize)?; + + // all candidates leave. + >::kill(); + + // now everyone is defunct + assert!(>::iter().all(|(_, v)| >::is_defunct_voter(&v.votes))); + assert_eq!(>::iter().count() as u32, v); + let root = RawOrigin::Root; + }: _(root, v, d) + verify { + assert_eq!(>::iter().count() as u32, v - d); + } + + election_phragmen { + // This is just to focus on phragmen in the context of this module. We always select 20 + // members, this is hard-coded in the runtime and cannot be trivially changed at this stage. + // Yet, change the number of voters, candidates and edge per voter to see the impact. Note + // that we give all candidates a self vote to make sure they are all considered. + let c in 1 .. T::MaxCandidates::get(); + let v in 1 .. T::MaxVoters::get(); + let e in (T::MaxVoters::get()) .. T::MaxVoters::get() * T::MaxVotesPerVoter::get(); + clean::(); + + // so we have a situation with v and e. we want e to basically always be in the range of `e + // -> e * T::MaxVotesPerVoter::get()`, but we cannot express that now with the benchmarks. + // So what we do is: when c is being iterated, v, and e are max and fine. when v is being + // iterated, e is being set to max and this is a problem. In these cases, we cap e to a + // lower value, namely v * T::MaxVotesPerVoter::get(). when e is being iterated, v is at + // max, and again fine. all in all, votes_per_voter can never be more than + // T::MaxVotesPerVoter::get(). Note that this might cause `v` to be an overestimate. + let votes_per_voter = (e / v).min(T::MaxVotesPerVoter::get()); + + let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; + let _ = distribute_voters::(all_candidates, v.saturating_sub(c), votes_per_voter as usize)?; + }: { + >::on_initialize(T::TermDuration::get()); + } + verify { + assert_eq!(>::members().len() as u32, T::DesiredMembers::get().min(c)); + assert_eq!( + >::runners_up().len() as u32, + T::DesiredRunnersUp::get().min(c.saturating_sub(T::DesiredMembers::get())), + ); + + #[cfg(test)] + { + // reset members in between benchmark tests. + use crate::tests::MEMBERS; + MEMBERS.with(|m| *m.borrow_mut() = vec![]); + } + } + + impl_benchmark_test_suite!( + Elections, + crate::tests::ExtBuilder::default().desired_members(13).desired_runners_up(7), + crate::tests::Test, + exec_name = build_and_execute, + ); +} diff --git a/pallets/elections-phragmen/src/lib.rs b/pallets/elections-phragmen/src/lib.rs new file mode 100644 index 000000000..3a6a38f10 --- /dev/null +++ b/pallets/elections-phragmen/src/lib.rs @@ -0,0 +1,3069 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! # Phragmén Election Module. +//! +//! An election module based on sequential phragmen. +//! +//! ### Term and Round +//! +//! The election happens in _rounds_: every `N` blocks, all previous members are retired and a new +//! set is elected (which may or may not have an intersection with the previous set). Each round +//! lasts for some number of blocks defined by [`Config::TermDuration`]. The words _term_ and +//! _round_ can be used interchangeably in this context. +//! +//! [`Config::TermDuration`] might change during a round. This can shorten or extend the length of +//! the round. The next election round's block number is never stored but rather always checked on +//! the fly. Based on the current block number and [`Config::TermDuration`], the condition +//! `BlockNumber % TermDuration == 0` being satisfied will always trigger a new election round. +//! +//! ### Bonds and Deposits +//! +//! Both voting and being a candidate requires deposits to be taken, in exchange for the data that +//! needs to be kept on-chain. The terms *bond* and *deposit* can be used interchangeably in this +//! context. +//! +//! Bonds will be unreserved only upon adhering to the protocol laws. Failing to do so will cause in +//! the bond to slashed. +//! +//! ### Voting +//! +//! Voters can vote for a limited number of the candidates by providing a list of account ids, +//! bounded by [`Config::MaxVotesPerVoter`]. Invalid votes (voting for non-candidates) and duplicate +//! votes are ignored during election. Yet, a voter _might_ vote for a future candidate. Voters +//! reserve a bond as they vote. Each vote defines a `value`. This amount is locked from the account +//! of the voter and indicates the weight of the vote. Voters can update their votes at any time by +//! calling `vote()` again. This can update the vote targets (which might update the deposit) or +//! update the vote's stake ([`Voter::stake`]). After a round, votes are kept and might still be +//! valid for further rounds. A voter is responsible for calling `remove_voter` once they are done +//! to have their bond back and remove the lock. +//! +//! See [`Call::vote`], [`Call::remove_voter`]. +//! +//! ### Defunct Voter +//! +//! A voter is defunct once all of the candidates that they have voted for are not a valid candidate +//! (as seen further below, members and runners-up are also always candidates). Defunct voters can +//! be removed via a root call ([`Call::clean_defunct_voters`]). Upon being removed, their bond is +//! returned. This is an administrative operation and can be called only by the root origin in the +//! case of state bloat. +//! +//! ### Candidacy and Members +//! +//! Candidates also reserve a bond as they submit candidacy. A candidate can end up in one of the +//! below situations: +//! - **Members**: A winner is kept as a _member_. They must still have a bond in reserve and they +//! are automatically counted as a candidate for the next election. The number of desired +//! members is set by [`Config::DesiredMembers`]. +//! - **Runner-up**: Runners-up are the best candidates immediately after the winners. The number +//! of runners up to keep is set by [`Config::DesiredRunnersUp`]. Runners-up are used, in the +//! same order as they are elected, as replacements when a candidate is kicked by +//! [`Call::remove_member`], or when an active member renounces their candidacy. Runners are +//! automatically counted as a candidate for the next election. +//! - **Loser**: Any of the candidate who are not member/runner-up are left as losers. A loser +//! might be an _outgoing member or runner-up_, meaning that they are an active member who +//! failed to keep their spot. **An outgoing candidate/member/runner-up will always lose their +//! bond**. +//! +//! #### Renouncing candidacy. +//! +//! All candidates, elected or not, can renounce their candidacy. A call to +//! [`Call::renounce_candidacy`] will always cause the candidacy bond to be refunded. +//! +//! Note that with the members being the default candidates for the next round and votes persisting +//! in storage, the election system is entirely stable given no further input. This means that if +//! the system has a particular set of candidates `C` and voters `V` that lead to a set of members +//! `M` being elected, as long as `V` and `C` don't remove their candidacy and votes, `M` will keep +//! being re-elected at the end of each round. +//! +//! ### Module Information +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Module`] + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + pallet_prelude::DispatchResult, + traits::{ + defensive_prelude::*, + fungible::{hold::Balanced, Credit, Inspect, InspectHold, Mutate, MutateFreeze, MutateHold}, + tokens::{imbalance::OnUnbalanced, Balance, Precision}, + ChangeMembers, Contains, ContainsLengthBound, Get, InitializeMembers, SortedMembers, + }, + weights::Weight, +}; +use log; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_npos_elections::{ElectionResult, ExtendedBalance}; +use sp_runtime::{ + traits::{Saturating, StaticLookup, Zero}, + DispatchError, Perbill, RuntimeDebug, +}; +use sp_staking::currency_to_vote::CurrencyToVote; +use sp_std::prelude::*; + +#[cfg(any(feature = "try-runtime", test))] +use sp_runtime::TryRuntimeError; + +mod benchmarking; +pub mod weights; +pub use weights::WeightInfo; + +const LOG_TARGET: &str = "runtime::elections-phragmen"; + +pub type BalanceOf = ::Balance; +pub type AccountIdOf = ::AccountId; +type CreditOf = Credit<::AccountId, ::Currency>; +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +/// An indication that the renouncing account currently has which of the below roles. +#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug, TypeInfo)] +pub enum Renouncing { + /// A member is renouncing. + Member, + /// A runner-up is renouncing. + RunnerUp, + /// A candidate is renouncing, while the given total number of candidates exists. + Candidate(#[codec(compact)] u32), +} + +/// An active voter. +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, TypeInfo)] +pub struct Voter { + /// The members being backed. + pub votes: Vec, + /// The amount of stake placed on this vote. + pub stake: Balance, + /// The block number at which the voter can unlock their stake. + pub lockup_till: BlockNumber, +} + +impl Default for Voter { + fn default() -> Self { + Self { votes: vec![], stake: Default::default(), lockup_till: Default::default() } + } +} + +/// A holder of a seat as either a member or a runner-up. +#[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq, TypeInfo)] +pub struct SeatHolder { + /// The holder. + pub who: AccountId, + /// The total backing stake. + pub stake: Balance, + /// The amount of deposit held on-chain. + /// + /// To be unreserved upon renouncing, or slashed upon being a loser. + pub deposit: Balance, +} + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::composite_enum] + pub enum HoldReason { + /// Funds are hold for announcing Candidacy. + Candidacy, + } + + /// A reason for freezing funds. + #[pallet::composite_enum] + pub enum FreezeReason { + /// Lock up the funds amount for the upcoming candidancy and election + Voting, + } + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type Currency: Inspect, Balance = BalanceOf> + + Balanced> + + Mutate> + + InspectHold> + + MutateHold, Reason = Self::RuntimeHoldReason> + + MutateFreeze, Id = Self::RuntimeFreezeReason>; + + type Balance: Balance + MaybeSerializeDeserialize; + + /// The overarching runtime hold reason. + type RuntimeHoldReason: From; + + /// The overarching freeze reason. + type RuntimeFreezeReason: From; + + /// What to do when the members change. + type ChangeMembers: ChangeMembers; + + /// What to do with genesis members + type InitializeMembers: InitializeMembers; + + /// Convert a balance into a number used for election calculation. + /// This must fit into a `u64` but is allowed to be sensibly lossy. + type CurrencyToVote: CurrencyToVote>; + + /// How much should be locked up in order to submit one's candidacy. + #[pallet::constant] + type CandidacyBond: Get>; + + /// Handler for the unbalanced reduction when a candidate has lost (and is not a runner-up) + type LoserCandidate: OnUnbalanced>; + + /// Number of members to elect. + #[pallet::constant] + type DesiredMembers: Get; + + /// Number of runners_up to keep. + #[pallet::constant] + type DesiredRunnersUp: Get; + + /// How long each seat is kept. This defines the next block number at which an election + /// round will happen. If set to zero, no elections are ever triggered and the module will + /// be in passive mode. + #[pallet::constant] + type TermDuration: Get>; + + /// How long the voting lock is in effect after voting or changing vote. If set to + /// zero, no lock is enforced. + #[pallet::constant] + type VotingLockPeriod: Get>; + + /// The maximum number of candidates in a phragmen election. + /// + /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and + /// consider how it will impact `T::WeightInfo::election_phragmen`. + /// + /// When this limit is reached no more candidates are accepted in the election. + #[pallet::constant] + type MaxCandidates: Get; + + /// The maximum number of voters to allow in a phragmen election. + /// + /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and + /// consider how it will impact `T::WeightInfo::election_phragmen`. + /// + /// When the limit is reached the new voters are ignored. + #[pallet::constant] + type MaxVoters: Get; + + /// Maximum numbers of votes per voter. + /// + /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and + /// consider how it will impact `T::WeightInfo::election_phragmen`. + #[pallet::constant] + type MaxVotesPerVoter: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// What to do at the end of each block. + /// + /// Checks if an election needs to happen or not. + fn on_initialize(n: BlockNumberFor) -> Weight { + let term_duration = T::TermDuration::get(); + if !term_duration.is_zero() && (n % term_duration).is_zero() { + Self::do_phragmen() + } else { + Weight::zero() + } + } + + fn integrity_test() { + let block_weight = T::BlockWeights::get().max_block; + // mind the order. + let election_weight = T::WeightInfo::election_phragmen( + T::MaxCandidates::get(), + T::MaxVoters::get(), + T::MaxVotesPerVoter::get() * T::MaxVoters::get(), + ); + + let to_seconds = + |w: &Weight| w.ref_time() as f32 / frame_support::weights::constants::WEIGHT_REF_TIME_PER_SECOND as f32; + + log::debug!( + target: LOG_TARGET, + "election weight {}s ({:?}) // chain's block weight {}s ({:?})", + to_seconds(&election_weight), + election_weight, + to_seconds(&block_weight), + block_weight, + ); + assert!( + election_weight.all_lt(block_weight), + "election weight {}s ({:?}) will exceed a {}s chain's block weight ({:?}) (MaxCandidates {}, MaxVoters {}, MaxVotesPerVoter {} -- tweak these parameters)", + election_weight, + to_seconds(&election_weight), + to_seconds(&block_weight), + block_weight, + T::MaxCandidates::get(), + T::MaxVoters::get(), + T::MaxVotesPerVoter::get(), + ); + } + + #[cfg(feature = "try-runtime")] + fn try_state(_n: BlockNumberFor) -> Result<(), TryRuntimeError> { + Self::do_try_state() + } + } + + #[pallet::call] + impl Pallet { + /// Vote for a set of candidates for the upcoming round of election. This can be called to + /// set the initial votes, or update already existing votes. + /// + /// Upon initial voting, `value` units of `who`'s balance is locked and a deposit amount is + /// reserved. The deposit is based on the number of votes and can be updated over time. + /// + /// The `votes` should: + /// - not be empty. + /// - be less than the number of possible candidates. Note that all current members and + /// runners-up are also automatically candidates for the next round. + /// + /// If `value` is more than `who`'s free balance, then the maximum of the two is used. + /// + /// The dispatch origin of this call must be signed. + /// + /// ### Warning + /// + /// It is the responsibility of the caller to **NOT** place all of their balance into the + /// lock and keep some for further operations. + #[pallet::call_index(0)] + #[pallet::weight( + T::WeightInfo::vote_more(votes.len() as u32) + .max(T::WeightInfo::vote_less(votes.len() as u32)) + .max(T::WeightInfo::vote_equal(votes.len() as u32)) + )] + pub fn vote( + origin: OriginFor, + votes: Vec, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + ensure!(votes.len() <= T::MaxVotesPerVoter::get() as usize, Error::::MaximumVotesExceeded); + ensure!(!votes.is_empty(), Error::::NoVotes); + + let candidates_count = >::decode_len().unwrap_or(0); + let members_count = >::decode_len().unwrap_or(0); + let runners_up_count = >::decode_len().unwrap_or(0); + + // can never submit a vote of there are no members, and cannot submit more votes than + // all potential vote targets. + // addition is valid: candidates, members and runners-up will never overlap. + let allowed_votes = candidates_count.saturating_add(members_count).saturating_add(runners_up_count); + ensure!(!allowed_votes.is_zero(), Error::::UnableToVote); + ensure!(votes.len() <= allowed_votes, Error::::TooManyVotes); + + ensure!(value > T::Currency::minimum_balance(), Error::::LowBalance); + + // Amount to be locked up. + let locked_stake = value.min(T::Currency::total_balance(&who)); + T::Currency::set_freeze(&FreezeReason::Voting.into(), &who, locked_stake)?; + let current_block = >::block_number(); + Voting::::insert( + &who, + Voter { votes, stake: locked_stake, lockup_till: current_block + T::VotingLockPeriod::get() }, + ); + Ok(None::.into()) + } + + /// Remove `origin` as a voter. + /// + /// This removes the lock and returns the deposit. + /// + /// The dispatch origin of this call must be signed and be a voter. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::remove_voter())] + pub fn remove_voter(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(Self::is_voter(&who), Error::::MustBeVoter); + ensure!(Self::can_unlock(&who), Error::::VotingPeriodNotEnded); + Self::do_remove_voter(&who)?; + Ok(()) + } + + /// Submit oneself for candidacy. A fixed amount of deposit is recorded. + /// + /// All candidates are wiped at the end of the term. They either become a member/runner-up, + /// or leave the system while their deposit is slashed. + /// + /// The dispatch origin of this call must be signed. + /// + /// ### Warning + /// + /// Even if a candidate ends up being a member, they must call [`Call::renounce_candidacy`] + /// to get their deposit back. Losing the spot in an election will always lead to a slash. + /// + /// The number of current candidates must be provided as witness data. + /// ## Complexity + /// O(C + log(C)) where C is candidate_count. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::submit_candidacy(*candidate_count))] + pub fn submit_candidacy(origin: OriginFor, #[pallet::compact] candidate_count: u32) -> DispatchResult { + let who = ensure_signed(origin)?; + + let actual_count = >::decode_len().unwrap_or(0) as u32; + ensure!(actual_count <= candidate_count, Error::::InvalidWitnessData); + ensure!(actual_count <= ::MaxCandidates::get(), Error::::TooManyCandidates); + + let index = Self::is_candidate(&who).err().ok_or(Error::::DuplicatedCandidate)?; + + ensure!(!Self::is_member(&who), Error::::MemberSubmit); + ensure!(!Self::is_runner_up(&who), Error::::RunnerUpSubmit); + + T::Currency::hold(&HoldReason::Candidacy.into(), &who, T::CandidacyBond::get()) + .map_err(|_| Error::::InsufficientCandidateFunds)?; + + >::mutate(|c| c.insert(index, (who, T::CandidacyBond::get()))); + Ok(()) + } + + /// Renounce one's intention to be a candidate for the next election round. 3 potential + /// outcomes exist: + /// + /// - `origin` is a candidate and not elected in any set. In this case, the deposit is + /// unreserved, returned and origin is removed as a candidate. + /// - `origin` is a current runner-up. In this case, the deposit is unreserved, returned and + /// origin is removed as a runner-up. + /// - `origin` is a current member. In this case, the deposit is unreserved and origin is + /// removed as a member, consequently not being a candidate for the next round anymore. + /// Similar to [`remove_member`](Self::remove_member), if replacement runners exists, they + /// are immediately used. If the prime is renouncing, then no prime will exist until the + /// next round. + /// + /// The dispatch origin of this call must be signed, and have one of the above roles. + /// The type of renouncing must be provided as witness data. + /// + /// ## Complexity + /// - Renouncing::Candidate(count): O(count + log(count)) + /// - Renouncing::Member: O(1) + /// - Renouncing::RunnerUp: O(1) + #[pallet::call_index(3)] + #[pallet::weight(match *renouncing { + Renouncing::Candidate(count) => T::WeightInfo::renounce_candidacy_candidate(count), + Renouncing::Member => T::WeightInfo::renounce_candidacy_members(), + Renouncing::RunnerUp => T::WeightInfo::renounce_candidacy_runners_up(), + })] + pub fn renounce_candidacy(origin: OriginFor, renouncing: Renouncing) -> DispatchResult { + let who = ensure_signed(origin)?; + match renouncing { + Renouncing::Member => { + Self::remove_and_replace_member(&who, false).map_err(|_| Error::::InvalidRenouncing)?; + Self::deposit_event(Event::Renounced { candidate: who }); + }, + Renouncing::RunnerUp => { + >::try_mutate::<_, Error, _>(|runners_up| { + let index = runners_up + .iter() + .position(|SeatHolder { who: r, .. }| r == &who) + .ok_or(Error::::InvalidRenouncing)?; + // can't fail anymore. + let SeatHolder { deposit, .. } = runners_up.remove(index); + T::Currency::release(&HoldReason::Candidacy.into(), &who, deposit, Precision::Exact) + .map_err(|_| Error::::UnableToReleaseBond)?; + Self::deposit_event(Event::Renounced { candidate: who }); + Ok(()) + })?; + }, + Renouncing::Candidate(count) => { + >::try_mutate::<_, Error, _>(|candidates| { + ensure!(count >= candidates.len() as u32, Error::::InvalidWitnessData); + let index = candidates + .binary_search_by(|(c, _)| c.cmp(&who)) + .map_err(|_| Error::::InvalidRenouncing)?; + let (_, deposit) = candidates.remove(index); + T::Currency::release(&HoldReason::Candidacy.into(), &who, deposit, Precision::Exact) + .map_err(|_| Error::::UnableToReleaseBond)?; + Self::deposit_event(Event::Renounced { candidate: who }); + Ok(()) + })?; + }, + }; + Ok(()) + } + + /// Remove a particular member from the set. This is effective immediately and the bond of + /// the outgoing member is slashed. + /// + /// If a runner-up is available, then the best runner-up will be removed and replaces the + /// outgoing member. Otherwise, if `rerun_election` is `true`, a new phragmen election is + /// started, else, nothing happens. + /// + /// If `slash_bond` is set to true, the bond of the member being removed is slashed. Else, + /// it is returned. + /// + /// The dispatch origin of this call must be root. + /// + /// Note that this does not affect the designated block number of the next election. + /// + /// ## Complexity + /// - Check details of remove_and_replace_member() and do_phragmen(). + #[pallet::call_index(4)] + #[pallet::weight(if *rerun_election { + T::WeightInfo::remove_member_without_replacement() + } else { + T::WeightInfo::remove_member_with_replacement() + })] + pub fn remove_member( + origin: OriginFor, + who: AccountIdLookupOf, + slash_bond: bool, + rerun_election: bool, + ) -> DispatchResult { + ensure_root(origin)?; + let who = T::Lookup::lookup(who)?; + + let _ = Self::remove_and_replace_member(&who, slash_bond)?; + Self::deposit_event(Event::MemberKicked { member: who }); + + if rerun_election { + Self::do_phragmen(); + } + + // no refund needed. + Ok(()) + } + + /// Clean all voters who are defunct (i.e. they do not serve any purpose at all). The + /// deposit of the removed voters are returned. + /// + /// This is an root function to be used only for cleaning the state. + /// + /// The dispatch origin of this call must be root. + /// + /// ## Complexity + /// - Check is_defunct_voter() details. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::clean_defunct_voters(*num_voters, *num_defunct))] + pub fn clean_defunct_voters(origin: OriginFor, num_voters: u32, num_defunct: u32) -> DispatchResult { + let _ = ensure_root(origin)?; + + >::iter() + .take(num_voters as usize) + .filter(|(_, x)| Self::is_defunct_voter(&x.votes)) + .take(num_defunct as usize) + .try_for_each(|(dv, _)| Self::do_remove_voter(&dv))?; + + Ok(()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new term with new_members. This indicates that enough candidates existed to run + /// the election, not that enough have has been elected. The inner value must be examined + /// for this purpose. A `NewTerm(\[\])` indicates that some candidates got their bond + /// slashed and none were elected, whilst `EmptyTerm` means that no candidates existed to + /// begin with. + NewTerm { new_members: Vec<(::AccountId, BalanceOf)> }, + /// No (or not enough) candidates existed for this round. This is different from + /// `NewTerm(\[\])`. See the description of `NewTerm`. + EmptyTerm, + /// Internal error happened while trying to perform election. + ElectionError, + /// A member has been removed. This should always be followed by either `NewTerm` or + /// `EmptyTerm`. + MemberKicked { member: ::AccountId }, + /// Someone has renounced their candidacy. + Renounced { candidate: ::AccountId }, + /// A candidate was slashed by amount due to failing to obtain a seat as member or + /// runner-up. + /// + /// Note that old members and runners-up are also candidates. + CandidateSlashed { candidate: ::AccountId, amount: BalanceOf }, + /// A seat holder was slashed by amount by being forcefully removed from the set. + SeatHolderSlashed { seat_holder: ::AccountId, amount: BalanceOf }, + } + + #[pallet::error] + pub enum Error { + /// Cannot vote when no candidates or members exist. + UnableToVote, + /// Must vote for at least one candidate. + NoVotes, + /// Cannot vote more than candidates. + TooManyVotes, + /// Cannot vote more than maximum allowed. + MaximumVotesExceeded, + /// Cannot vote with stake less than minimum balance. + LowBalance, + /// Voter can not pay voting bond. + UnableToPayBond, + /// Must be a voter. + MustBeVoter, + // Voting period is not ended. + VotingPeriodNotEnded, + /// Duplicated candidate submission. + DuplicatedCandidate, + /// Too many candidates have been created. + TooManyCandidates, + /// Member cannot re-submit candidacy. + MemberSubmit, + /// Runner cannot re-submit candidacy. + RunnerUpSubmit, + /// Candidate does not have enough funds. + InsufficientCandidateFunds, + /// Not a member. + NotMember, + /// The provided count of number of candidates is incorrect. + InvalidWitnessData, + /// The provided count of number of votes is incorrect. + InvalidVoteCount, + /// The renouncing origin presented a wrong `Renouncing` parameter. + InvalidRenouncing, + /// Prediction regarding replacement after member removal is wrong. + InvalidReplacement, + /// Unable to release held funds. + UnableToReleaseBond, + } + + /// The current elected members. + /// + /// Invariant: Always sorted based on account id. + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members = StorageValue<_, Vec>>, ValueQuery>; + + /// The current reserved runners-up. + /// + /// Invariant: Always sorted based on rank (worse to best). Upon removal of a member, the + /// last (i.e. _best_) runner-up will be replaced. + #[pallet::storage] + #[pallet::getter(fn runners_up)] + pub type RunnersUp = StorageValue<_, Vec>>, ValueQuery>; + + /// The present candidate list. A current member or runner-up can never enter this vector + /// and is always implicitly assumed to be a candidate. + /// + /// Second element is the deposit. + /// + /// Invariant: Always sorted based on account id. + #[pallet::storage] + #[pallet::getter(fn candidates)] + pub type Candidates = StorageValue<_, Vec<(T::AccountId, BalanceOf)>, ValueQuery>; + + /// The total number of vote rounds that have happened, excluding the upcoming one. + #[pallet::storage] + #[pallet::getter(fn election_rounds)] + pub type ElectionRounds = StorageValue<_, u32, ValueQuery>; + + /// Votes and locked stake of a particular voter. + /// + /// TWOX-NOTE: SAFE as `AccountId` is a crypto hash. + #[pallet::storage] + #[pallet::getter(fn voting)] + pub type Voting = + StorageMap<_, Twox64Concat, T::AccountId, Voter, BlockNumberFor>, ValueQuery>; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub members: Vec<(T::AccountId, BalanceOf)>, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + assert!( + self.members.len() as u32 <= T::DesiredMembers::get(), + "Cannot accept more than DesiredMembers genesis member", + ); + let members = self + .members + .iter() + .map(|(ref member, ref stake)| { + // make sure they have enough stake. + assert!(T::Currency::balance(member) >= *stake, "Genesis member does not have enough stake.",); + + // Note: all members will only vote for themselves, hence they must be given + // exactly their own stake as total backing. Any sane election should behave as + // such. Nonetheless, stakes will be updated for term 1 onwards according to the + // election. + Members::::mutate(|members| match members.binary_search_by(|m| m.who.cmp(member)) { + Ok(_) => { + panic!("Duplicate member in elections-phragmen genesis: {:?}", member) + }, + Err(pos) => members + .insert(pos, SeatHolder { who: member.clone(), stake: *stake, deposit: Zero::zero() }), + }); + + // set self-votes to make persistent. Genesis voters don't have any bond, nor do + // they have any lock. NOTE: this means that we will still try to remove a lock + // once this genesis voter is removed, and for now it is okay because + // remove_lock is noop if lock is not there. + >::insert( + &member, + Voter { votes: vec![member.clone()], stake: *stake, lockup_till: T::VotingLockPeriod::get() }, + ); + + member.clone() + }) + .collect::>(); + + // report genesis members to upstream, if any. + T::InitializeMembers::initialize_members(&members); + } + } +} + +impl Pallet { + /// Attempts to remove a member `who`. If a runner-up exists, it is used as the replacement. + /// + /// Returns: + /// + /// - `Ok(true)` if the member was removed and a replacement was found. + /// - `Ok(false)` if the member was removed and but no replacement was found. + /// - `Err(_)` if the member was no found. + /// + /// Both `Members` and `RunnersUp` storage is updated accordingly. `T::ChangeMember` is called + /// if needed. If `slash` is true, the deposit of the potentially removed member is slashed, + /// else, it is unreserved. + /// + /// ### Note: Prime preservation + /// + /// This function attempts to preserve the prime. If the removed members is not the prime, it is + /// set again via [`Config::ChangeMembers`]. + fn remove_and_replace_member(who: &T::AccountId, slash: bool) -> Result { + // closure will return: + // - `Ok(Option(replacement))` if member was removed and replacement was replaced. + // - `Ok(None)` if member was removed but no replacement was found + // - `Err(_)` if who is not a member. + let maybe_replacement = >::try_mutate::<_, Error, _>(|members| { + let remove_index = members.binary_search_by(|m| m.who.cmp(who)).map_err(|_| Error::::NotMember)?; + // we remove the member anyhow, regardless of having a runner-up or not. + let removed = members.remove(remove_index); + + // slash or unreserve + if slash { + let (imbalance, _remainder) = T::Currency::slash(&HoldReason::Candidacy.into(), who, removed.deposit); + debug_assert!(_remainder.is_zero()); + T::LoserCandidate::on_unbalanced(imbalance); + Self::deposit_event(Event::SeatHolderSlashed { seat_holder: who.clone(), amount: removed.deposit }); + } else { + T::Currency::release(&HoldReason::Candidacy.into(), who, removed.deposit, Precision::Exact) + .map_err(|_| Error::::UnableToReleaseBond)?; + } + + let maybe_next_best = >::mutate(|r| r.pop()).map(|next_best| { + // defensive-only: Members and runners-up are disjoint. This will always be err and + // give us an index to insert. + if let Err(index) = members.binary_search_by(|m| m.who.cmp(&next_best.who)) { + members.insert(index, next_best.clone()); + } else { + // overlap. This can never happen. If so, it seems like our intended replacement + // is already a member, so not much more to do. + log::error!(target: LOG_TARGET, "A member seems to also be a runner-up."); + } + next_best + }); + Ok(maybe_next_best) + })?; + + let remaining_member_ids_sorted = Self::members().into_iter().map(|x| x.who).collect::>(); + let outgoing = &[who.clone()]; + let maybe_current_prime = T::ChangeMembers::get_prime(); + let return_value = match maybe_replacement { + // member ids are already sorted, other two elements have one item. + Some(incoming) => { + T::ChangeMembers::change_members_sorted(&[incoming.who], outgoing, &remaining_member_ids_sorted[..]); + true + }, + None => { + T::ChangeMembers::change_members_sorted(&[], outgoing, &remaining_member_ids_sorted[..]); + false + }, + }; + + // if there was a prime before and they are not the one being removed, then set them + // again. + if let Some(current_prime) = maybe_current_prime { + if ¤t_prime != who { + T::ChangeMembers::set_prime(Some(current_prime)); + } + } + + Ok(return_value) + } + + /// Check if `who` is a candidate. It returns the insert index if the element does not exists as + /// an error. + fn is_candidate(who: &T::AccountId) -> Result<(), usize> { + Self::candidates().binary_search_by(|c| c.0.cmp(who)).map(|_| ()) + } + + /// Check if `who` is a voter. It may or may not be a _current_ one. + fn is_voter(who: &T::AccountId) -> bool { + Voting::::contains_key(who) + } + + /// Check if `who`'s voting lock duration has passed. + fn can_unlock(who: &T::AccountId) -> bool { + let current_block = >::block_number(); + Voting::::get(who).lockup_till <= current_block + } + + /// Check if `who` is currently an active member. + fn is_member(who: &T::AccountId) -> bool { + Self::members().binary_search_by(|m| m.who.cmp(who)).is_ok() + } + + /// Check if `who` is currently an active runner-up. + fn is_runner_up(who: &T::AccountId) -> bool { + Self::runners_up().iter().any(|r| &r.who == who) + } + + /// Get the members' account ids. + pub(crate) fn members_ids() -> Vec { + Self::members().into_iter().map(|m| m.who).collect::>() + } + + /// Get a concatenation of previous members and runners-up and their deposits. + /// + /// These accounts are essentially treated as candidates. + fn implicit_candidates_with_deposit() -> Vec<(T::AccountId, BalanceOf)> { + // invariant: these two are always without duplicates. + Self::members() + .into_iter() + .map(|m| (m.who, m.deposit)) + .chain(Self::runners_up().into_iter().map(|r| (r.who, r.deposit))) + .collect::>() + } + + /// Check if `votes` will correspond to a defunct voter. As no origin is part of the inputs, + /// this function does not check the origin at all. + /// + /// O(NLogM) with M candidates and `who` having voted for `N` of them. + /// Reads Members, RunnersUp, Candidates and Voting(who) from database. + fn is_defunct_voter(votes: &[T::AccountId]) -> bool { + votes.iter().all(|v| !Self::is_member(v) && !Self::is_runner_up(v) && Self::is_candidate(v).is_err()) + } + + /// Remove a certain someone as a voter. + fn do_remove_voter(who: &T::AccountId) -> DispatchResult { + // remove storage, lock and unreserve. + T::Currency::thaw(&FreezeReason::Voting.into(), who).map_err(|_| Error::::UnableToReleaseBond)?; + Voting::::remove(who); + Ok(()) + } + + /// Run the phragmen election with all required side processes and state updates, if election + /// succeeds. Else, it will emit an `ElectionError` event. + /// + /// Calls the appropriate [`ChangeMembers`] function variant internally. + fn do_phragmen() -> Weight { + let desired_seats = T::DesiredMembers::get() as usize; + let desired_runners_up = T::DesiredRunnersUp::get() as usize; + let num_to_elect = desired_runners_up + desired_seats; + + let mut candidates_and_deposit = Self::candidates(); + // add all the previous members and runners-up as candidates as well. + candidates_and_deposit.append(&mut Self::implicit_candidates_with_deposit()); + + if candidates_and_deposit.len().is_zero() { + Self::deposit_event(Event::EmptyTerm); + return T::DbWeight::get().reads(3) + } + + // All of the new winners that come out of phragmen will thus have a deposit recorded. + let candidate_ids = candidates_and_deposit.iter().map(|(x, _)| x).cloned().collect::>(); + + // helper closures to deal with balance/stake. + let total_issuance = T::Currency::total_issuance(); + let to_votes = |b: BalanceOf| T::CurrencyToVote::to_vote(b, total_issuance); + let to_balance = |e: ExtendedBalance| T::CurrencyToVote::to_currency(e, total_issuance); + + let mut num_edges: u32 = 0; + + let max_voters = ::MaxVoters::get() as usize; + // used for prime election. + let mut voters_and_stakes = Vec::new(); + match Voting::::iter().try_for_each(|(voter, Voter { stake, votes, .. })| { + if voters_and_stakes.len() < max_voters { + voters_and_stakes.push((voter, stake, votes)); + Ok(()) + } else { + Err(()) + } + }) { + Ok(_) => (), + Err(_) => { + log::error!( + target: LOG_TARGET, + "Failed to run election. Number of voters exceeded", + ); + Self::deposit_event(Event::ElectionError); + return T::DbWeight::get().reads(3 + max_voters as u64) + }, + } + + // used for phragmen. + let voters_and_votes = voters_and_stakes + .iter() + .cloned() + .map(|(voter, stake, votes)| { + num_edges = num_edges.saturating_add(votes.len() as u32); + (voter, to_votes(stake), votes) + }) + .collect::>(); + + let weight_candidates = candidates_and_deposit.len() as u32; + let weight_voters = voters_and_votes.len() as u32; + let weight_edges = num_edges; + let _ = + sp_npos_elections::seq_phragmen(num_to_elect, candidate_ids, voters_and_votes, None) + .map(|ElectionResult:: { winners, assignments: _ }| { + // this is already sorted by id. + let old_members_ids_sorted = + >::take().into_iter().map(|m| m.who).collect::>(); + // this one needs a sort by id. + let mut old_runners_up_ids_sorted = + >::take().into_iter().map(|r| r.who).collect::>(); + old_runners_up_ids_sorted.sort(); + + // filter out those who end up with no backing stake. + let mut new_set_with_stake = winners + .into_iter() + .filter_map(|(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) }) + .collect::)>>(); + + // OPTIMIZATION NOTE: we could bail out here if `new_set.len() == 0`. There + // isn't much left to do. Yet, re-arranging the code would require duplicating + // the slashing of exposed candidates, cleaning any previous members, and so on. + // For now, in favor of readability and veracity, we keep it simple. + + // split new set into winners and runners up. + let split_point = desired_seats.min(new_set_with_stake.len()); + let mut new_members_sorted_by_id = new_set_with_stake.drain(..split_point).collect::>(); + new_members_sorted_by_id.sort_by(|i, j| i.0.cmp(&j.0)); + + // all the rest will be runners-up + new_set_with_stake.reverse(); + let new_runners_up_sorted_by_rank = new_set_with_stake; + let mut new_runners_up_ids_sorted = + new_runners_up_sorted_by_rank.iter().map(|(r, _)| r.clone()).collect::>(); + new_runners_up_ids_sorted.sort(); + + // Now we select a prime member using a [Borda + // count](https://en.wikipedia.org/wiki/Borda_count). We weigh everyone's vote for + // that new member by a multiplier based on the order of the votes. i.e. the + // first person a voter votes for gets a 16x multiplier, the next person gets a + // 15x multiplier, an so on... (assuming `T::MaxVotesPerVoter` = 16) + let mut prime_votes = + new_members_sorted_by_id.iter().map(|c| (&c.0, BalanceOf::::zero())).collect::>(); + for (_, stake, votes) in voters_and_stakes.into_iter() { + for (vote_multiplier, who) in votes.iter().enumerate().map(|(vote_position, who)| { + ((T::MaxVotesPerVoter::get() as usize - vote_position) as u32, who) + }) { + if let Ok(i) = prime_votes.binary_search_by_key(&who, |k| k.0) { + prime_votes[i].1 = + prime_votes[i].1.saturating_add(stake.saturating_mul(vote_multiplier.into())); + } + } + } + // We then select the new member with the highest weighted stake. In the case of + // a tie, the last person in the list with the tied score is selected. This is + // the person with the "highest" account id based on the sort above. + let prime = prime_votes.into_iter().max_by_key(|x| x.1).map(|x| x.0.clone()); + + // new_members_sorted_by_id is sorted by account id. + let new_members_ids_sorted = + new_members_sorted_by_id.iter().map(|(m, _)| m.clone()).collect::>(); + + // report member changes. We compute diff because we need the outgoing list. + let (incoming, outgoing) = + T::ChangeMembers::compute_members_diff_sorted(&new_members_ids_sorted, &old_members_ids_sorted); + T::ChangeMembers::change_members_sorted(&incoming, &outgoing, &new_members_ids_sorted); + T::ChangeMembers::set_prime(prime); + + // All candidates/members/runners-up who are no longer retaining a position as a + // seat holder will lose their bond. + candidates_and_deposit.iter().for_each(|(c, d)| { + if new_members_ids_sorted.binary_search(c).is_err() && + new_runners_up_ids_sorted.binary_search(c).is_err() + { + let (imbalance, _) = T::Currency::slash(&HoldReason::Candidacy.into(), c, *d); + T::LoserCandidate::on_unbalanced(imbalance); + Self::deposit_event(Event::CandidateSlashed { candidate: c.clone(), amount: *d }); + } + }); + + // write final values to storage. + let deposit_of_candidate = |x: &T::AccountId| -> BalanceOf { + // defensive-only. This closure is used against the new members and new + // runners-up, both of which are phragmen winners and thus must have + // deposit. + candidates_and_deposit + .iter() + .find_map(|(c, d)| if c == x { Some(*d) } else { None }) + .defensive_unwrap_or_default() + }; + // fetch deposits from the one recorded one. This will make sure that a + // candidate who submitted candidacy before a change to candidacy deposit will + // have the correct amount recorded. + >::put( + new_members_sorted_by_id + .iter() + .map(|(who, stake)| SeatHolder { + deposit: deposit_of_candidate(who), + who: who.clone(), + stake: *stake, + }) + .collect::>(), + ); + >::put( + new_runners_up_sorted_by_rank + .into_iter() + .map(|(who, stake)| SeatHolder { deposit: deposit_of_candidate(&who), who, stake }) + .collect::>(), + ); + + // clean candidates. + >::kill(); + + Self::deposit_event(Event::NewTerm { new_members: new_members_sorted_by_id }); + >::mutate(|v| *v += 1); + }) + .map_err(|e| { + log::error!(target: LOG_TARGET, "Failed to run election [{:?}].", e,); + Self::deposit_event(Event::ElectionError); + }); + + T::WeightInfo::election_phragmen(weight_candidates, weight_voters, weight_edges) + } +} + +impl Contains for Pallet { + fn contains(who: &T::AccountId) -> bool { + Self::is_member(who) + } +} + +impl SortedMembers for Pallet { + fn contains(who: &T::AccountId) -> bool { + Self::is_member(who) + } + + fn sorted_members() -> Vec { + Self::members_ids() + } + + // A special function to populate members in this pallet for passing Origin + // checks in runtime benchmarking. + #[cfg(feature = "runtime-benchmarks")] + fn add(who: &T::AccountId) { + Members::::mutate(|members| match members.binary_search_by(|m| m.who.cmp(who)) { + Ok(_) => (), + Err(pos) => { + let s = SeatHolder { who: who.clone(), stake: Default::default(), deposit: Default::default() }; + members.insert(pos, s) + }, + }) + } +} + +impl ContainsLengthBound for Pallet { + fn min_len() -> usize { + 0 + } + + /// Implementation uses a parameter type so calling is cost-free. + fn max_len() -> usize { + T::DesiredMembers::get() as usize + } +} + +#[cfg(any(feature = "try-runtime", test))] +impl Pallet { + fn do_try_state() -> Result<(), TryRuntimeError> { + Self::try_state_members()?; + Self::try_state_runners_up()?; + Self::try_state_candidates()?; + Self::try_state_candidates_runners_up_disjoint()?; + Self::try_state_members_disjoint()?; + Self::try_state_members_approval_stake() + } + + /// [`Members`] state checks. Invariants: + /// - Members are always sorted based on account ID. + fn try_state_members() -> Result<(), TryRuntimeError> { + let mut members = Members::::get().clone(); + members.sort_by_key(|m| m.who.clone()); + + if Members::::get() == members { + Ok(()) + } else { + Err("try_state checks: Members must be always sorted by account ID".into()) + } + } + + // [`RunnersUp`] state checks. Invariants: + // - Elements are sorted based on weight (worst to best). + fn try_state_runners_up() -> Result<(), TryRuntimeError> { + let mut sorted = RunnersUp::::get(); + // worst stake first + sorted.sort_by(|a, b| a.stake.cmp(&b.stake)); + + if RunnersUp::::get() == sorted { + Ok(()) + } else { + Err("try_state checks: Runners Up must always be sorted by stake (worst to best)".into()) + } + } + + // [`Candidates`] state checks. Invariants: + // - Always sorted based on account ID. + fn try_state_candidates() -> Result<(), TryRuntimeError> { + let mut candidates = Candidates::::get().clone(); + candidates.sort_by_key(|(c, _)| c.clone()); + + if Candidates::::get() == candidates { + Ok(()) + } else { + Err("try_state checks: Candidates must be always sorted by account ID".into()) + } + } + + // [`Candidates`] and [`RunnersUp`] state checks. Invariants: + // - Candidates and runners-ups sets are disjoint. + fn try_state_candidates_runners_up_disjoint() -> Result<(), TryRuntimeError> { + match Self::intersects(&Self::candidates_ids(), &Self::runners_up_ids()) { + true => Err("Candidates and runners up sets should always be disjoint".into()), + false => Ok(()), + } + } + + // [`Members`], [`Candidates`] and [`RunnersUp`] state checks. Invariants: + // - Members and candidates sets are disjoint; + // - Members and runners-ups sets are disjoint. + fn try_state_members_disjoint() -> Result<(), TryRuntimeError> { + match Self::intersects(&Pallet::::members_ids(), &Self::candidates_ids()) && + Self::intersects(&Pallet::::members_ids(), &Self::runners_up_ids()) + { + true => Err("Members set should be disjoint from candidates and runners-up sets".into()), + false => Ok(()), + } + } + + // [`Members`], [`RunnersUp`] and approval stake state checks. Invariants: + // - Selected members should have approval stake; + // - Selected RunnersUp should have approval stake. + fn try_state_members_approval_stake() -> Result<(), TryRuntimeError> { + match Members::::get().iter().chain(RunnersUp::::get().iter()).all(|s| s.stake != BalanceOf::::zero()) + { + true => Ok(()), + false => Err("Members and RunnersUp must have approval stake".into()), + } + } + + fn intersects(a: &[P], b: &[P]) -> bool { + a.iter().any(|e| b.contains(e)) + } + + fn candidates_ids() -> Vec { + Pallet::::candidates().iter().map(|(x, _)| x).cloned().collect::>() + } + + fn runners_up_ids() -> Vec { + Pallet::::runners_up().into_iter().map(|r| r.who).collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as elections_phragmen; + use frame_support::{ + assert_noop, assert_ok, derive_impl, + dispatch::DispatchResultWithPostInfo, + parameter_types, + traits::{fungible::InspectFreeze, ConstU32, ConstU64, OnInitialize}, + }; + use frame_system::ensure_signed; + use sp_core::H256; + use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, + }; + use substrate_test_utils::assert_eq_uvec; + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] + impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = u64; + type BaseCallFilter = frame_support::traits::Everything; + type Block = Block; + type BlockHashCount = ConstU64<250>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); + } + + impl pallet_balances::Config for Test { + type AccountStore = frame_system::Pallet; + type Balance = u64; + type DustRemoval = (); + type ExistentialDeposit = ConstU64<1>; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = ConstU32<10>; + type MaxHolds = ConstU32<10>; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); + } + + frame_support::parameter_types! { + pub static CandidacyBond: u64 = 3; + pub static DesiredMembers: u32 = 2; + pub static DesiredRunnersUp: u32 = 0; + pub static TermDuration: u64 = 5; + pub static VotingLockPeriod: u64 = 2; + pub static Members: Vec = vec![]; + pub static Prime: Option = None; + } + + pub struct TestChangeMembers; + impl ChangeMembers for TestChangeMembers { + fn change_members_sorted(incoming: &[u64], outgoing: &[u64], new: &[u64]) { + // new, incoming, outgoing must be sorted. + let mut new_sorted = new.to_vec(); + new_sorted.sort(); + assert_eq!(new, &new_sorted[..]); + + let mut incoming_sorted = incoming.to_vec(); + incoming_sorted.sort(); + assert_eq!(incoming, &incoming_sorted[..]); + + let mut outgoing_sorted = outgoing.to_vec(); + outgoing_sorted.sort(); + assert_eq!(outgoing, &outgoing_sorted[..]); + + // incoming and outgoing must be disjoint + for x in incoming.iter() { + assert!(outgoing.binary_search(x).is_err()); + } + + let mut old_plus_incoming = MEMBERS.with(|m| m.borrow().to_vec()); + old_plus_incoming.extend_from_slice(incoming); + old_plus_incoming.sort(); + + let mut new_plus_outgoing = new.to_vec(); + new_plus_outgoing.extend_from_slice(outgoing); + new_plus_outgoing.sort(); + + assert_eq!(old_plus_incoming, new_plus_outgoing, "change members call is incorrect!"); + + MEMBERS.with(|m| *m.borrow_mut() = new.to_vec()); + PRIME.with(|p| *p.borrow_mut() = None); + } + + fn set_prime(who: Option) { + PRIME.with(|p| *p.borrow_mut() = who); + } + + fn get_prime() -> Option { + PRIME.with(|p| *p.borrow()) + } + } + + parameter_types! { + pub const PhragmenMaxVoters: u32 = 1000; + pub const PhragmenMaxCandidates: u32 = 100; + } + + impl Config for Test { + type Balance = u64; + type CandidacyBond = CandidacyBond; + type ChangeMembers = TestChangeMembers; + type Currency = Balances; + type CurrencyToVote = (); + type DesiredMembers = DesiredMembers; + type DesiredRunnersUp = DesiredRunnersUp; + type InitializeMembers = (); + type LoserCandidate = (); + type MaxCandidates = PhragmenMaxCandidates; + type MaxVoters = PhragmenMaxVoters; + type MaxVotesPerVoter = ConstU32<16>; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type TermDuration = TermDuration; + type VotingLockPeriod = VotingLockPeriod; + type WeightInfo = (); + } + + pub type Block = sp_runtime::generic::Block; + pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; + + frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Elections: elections_phragmen, + } + ); + + pub struct ExtBuilder { + balance_factor: u64, + genesis_members: Vec<(u64, u64)>, + } + + impl Default for ExtBuilder { + fn default() -> Self { + Self { balance_factor: 1, genesis_members: vec![] } + } + } + + impl ExtBuilder { + pub fn desired_runners_up(self, count: u32) -> Self { + DESIRED_RUNNERS_UP.with(|v| *v.borrow_mut() = count); + self + } + + pub fn term_duration(self, duration: u64) -> Self { + TERM_DURATION.with(|v| *v.borrow_mut() = duration); + self + } + + pub fn genesis_members(mut self, members: Vec<(u64, u64)>) -> Self { + MEMBERS.with(|m| *m.borrow_mut() = members.iter().map(|(m, _)| *m).collect::>()); + self.genesis_members = members; + self + } + + pub fn desired_members(self, count: u32) -> Self { + DESIRED_MEMBERS.with(|m| *m.borrow_mut() = count); + self + } + + pub fn balance_factor(mut self, factor: u64) -> Self { + self.balance_factor = factor; + self + } + + pub fn build_and_execute(self, test: impl FnOnce() -> ()) { + sp_tracing::try_init_simple(); + MEMBERS.with(|m| *m.borrow_mut() = self.genesis_members.iter().map(|(m, _)| *m).collect::>()); + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { + system: frame_system::GenesisConfig::default(), + balances: pallet_balances::GenesisConfig:: { + balances: vec![ + (1, 10 * self.balance_factor), + (2, 20 * self.balance_factor), + (3, 30 * self.balance_factor), + (4, 40 * self.balance_factor), + (5, 50 * self.balance_factor), + (6, 60 * self.balance_factor), + ], + }, + elections: elections_phragmen::GenesisConfig:: { members: self.genesis_members }, + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(pre_conditions); + ext.execute_with(test); + + #[cfg(feature = "try-runtime")] + ext.execute_with(|| { + assert_ok!(>::try_state(System::block_number())); + }); + } + } + + fn candidate_ids() -> Vec { + Elections::candidates().into_iter().map(|(c, _)| c).collect::>() + } + + fn candidate_deposit(who: &u64) -> u64 { + Elections::candidates() + .into_iter() + .find_map(|(c, d)| if c == *who { Some(d) } else { None }) + .unwrap_or_default() + } + + fn runners_up_ids() -> Vec { + Elections::runners_up().into_iter().map(|r| r.who).collect::>() + } + + fn members_ids() -> Vec { + Elections::members_ids() + } + + fn members_and_stake() -> Vec<(u64, u64)> { + Elections::members().into_iter().map(|m| (m.who, m.stake)).collect::>() + } + + fn runners_up_and_stake() -> Vec<(u64, u64)> { + Elections::runners_up().into_iter().map(|r| (r.who, r.stake)).collect::>() + } + + fn all_voters() -> Vec { + Voting::::iter().map(|(v, _)| v).collect::>() + } + + fn balances(who: &u64) -> (u64, u64) { + (Balances::free_balance(who), Balances::reserved_balance(who)) + } + + fn has_lock(who: &u64) -> u64 { + Balances::balance_frozen(&FreezeReason::Voting.into(), who) + } + + fn locked_stake_of(who: &u64) -> u64 { + Voting::::get(who).stake + } + + fn pre_conditions() { + System::set_block_number(1); + Elections::do_try_state().unwrap(); + } + + fn submit_candidacy(origin: RuntimeOrigin) -> sp_runtime::DispatchResult { + Elections::submit_candidacy(origin, Elections::candidates().len() as u32) + } + + fn vote(origin: RuntimeOrigin, votes: Vec, stake: u64) -> DispatchResultWithPostInfo { + // historical note: helper function was created in a period of time in which the API of vote + // call was changing. Currently it is a wrapper for the original call and does not do much. + // Nonetheless, totally harmless. + ensure_signed(origin.clone()).expect("vote origin must be signed"); + Elections::vote(origin, votes, stake) + } + + fn votes_of(who: &u64) -> Vec { + Voting::::get(who).votes + } + + #[test] + fn params_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(::DesiredMembers::get(), 2); + assert_eq!(::DesiredRunnersUp::get(), 0); + assert_eq!(::CandidacyBond::get(), 3); + assert_eq!(::TermDuration::get(), 5); + assert_eq!(Elections::election_rounds(), 0); + + assert!(Elections::members().is_empty()); + assert!(Elections::runners_up().is_empty()); + + assert!(candidate_ids().is_empty()); + assert_eq!(>::decode_len(), None); + assert!(Elections::is_candidate(&1).is_err()); + + assert!(all_voters().is_empty()); + assert!(votes_of(&1).is_empty()); + }); + } + + #[test] + fn genesis_members_should_work() { + ExtBuilder::default().genesis_members(vec![(1, 10), (2, 20)]).build_and_execute(|| { + System::set_block_number(1); + assert_eq!( + Elections::members(), + vec![SeatHolder { who: 1, stake: 10, deposit: 0 }, SeatHolder { who: 2, stake: 20, deposit: 0 }] + ); + + assert_eq!(Elections::voting(1), Voter { stake: 10u64, votes: vec![1], lockup_till: 2 }); + assert_eq!(Elections::voting(2), Voter { stake: 20u64, votes: vec![2], lockup_till: 2 }); + + // they will persist since they have self vote. + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![1, 2]); + }) + } + + #[test] + fn genesis_voters_can_remove_lock() { + ExtBuilder::default().genesis_members(vec![(1, 10), (2, 20)]).build_and_execute(|| { + System::set_block_number(1); + + assert_eq!(Elections::voting(1), Voter { stake: 10u64, votes: vec![1], lockup_till: 2 }); + assert_eq!(Elections::voting(2), Voter { stake: 20u64, votes: vec![2], lockup_till: 2 }); + System::set_block_number(2); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(1))); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); + + assert_eq!(Elections::voting(1), Default::default()); + assert_eq!(Elections::voting(2), Default::default()); + }) + } + + #[test] + fn genesis_members_unsorted_should_work() { + ExtBuilder::default().genesis_members(vec![(2, 20), (1, 10)]).build_and_execute(|| { + System::set_block_number(1); + assert_eq!( + Elections::members(), + vec![SeatHolder { who: 1, stake: 10, deposit: 0 }, SeatHolder { who: 2, stake: 20, deposit: 0 },] + ); + + assert_eq!(Elections::voting(1), Voter { stake: 10u64, votes: vec![1], lockup_till: 2 }); + assert_eq!(Elections::voting(2), Voter { stake: 20u64, votes: vec![2], lockup_till: 2 }); + + // they will persist since they have self vote. + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![1, 2]); + }) + } + + #[test] + #[should_panic = "Genesis member does not have enough stake"] + fn genesis_members_cannot_over_stake_0() { + // 10 cannot lock 20 as their stake and extra genesis will panic. + ExtBuilder::default().genesis_members(vec![(1, 20), (2, 20)]).build_and_execute(|| {}); + } + + #[test] + #[should_panic = "Duplicate member in elections-phragmen genesis: 2"] + fn genesis_members_cannot_be_duplicate() { + ExtBuilder::default() + .desired_members(3) + .genesis_members(vec![(1, 10), (2, 10), (2, 10)]) + .build_and_execute(|| {}); + } + + #[test] + #[should_panic = "Cannot accept more than DesiredMembers genesis member"] + fn genesis_members_cannot_too_many() { + ExtBuilder::default() + .genesis_members(vec![(1, 10), (2, 10), (3, 30)]) + .desired_members(2) + .build_and_execute(|| {}); + } + + #[test] + fn term_duration_zero_is_passive() { + ExtBuilder::default().term_duration(0).build_and_execute(|| { + assert_eq!(::TermDuration::get(), 0); + assert_eq!(::DesiredMembers::get(), 2); + assert_eq!(Elections::election_rounds(), 0); + + assert!(members_ids().is_empty()); + assert!(Elections::runners_up().is_empty()); + assert!(candidate_ids().is_empty()); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert!(members_ids().is_empty()); + assert!(Elections::runners_up().is_empty()); + assert!(candidate_ids().is_empty()); + }); + } + + #[test] + fn simple_candidate_submission_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(candidate_ids(), Vec::::new()); + assert!(Elections::is_candidate(&1).is_err()); + assert!(Elections::is_candidate(&2).is_err()); + + assert_eq!(balances(&1), (10, 0)); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); + assert_eq!(balances(&1), (7, 3)); + + assert_eq!(candidate_ids(), vec![1]); + + assert!(Elections::is_candidate(&1).is_ok()); + assert!(Elections::is_candidate(&2).is_err()); + + assert_eq!(balances(&2), (20, 0)); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + assert_eq!(balances(&2), (17, 3)); + + assert_eq!(candidate_ids(), vec![1, 2]); + + assert!(Elections::is_candidate(&1).is_ok()); + assert!(Elections::is_candidate(&2).is_ok()); + + assert_eq!(candidate_deposit(&1), 3); + assert_eq!(candidate_deposit(&2), 3); + assert_eq!(candidate_deposit(&3), 0); + }); + } + + #[test] + fn updating_candidacy_bond_works() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_eq!(Elections::candidates(), vec![(5, 3)]); + + // a runtime upgrade changes the bond. + CANDIDACY_BOND.with(|v| *v.borrow_mut() = 4); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_eq!(Elections::candidates(), vec![(4, 4), (5, 3)]); + + // once elected, they each hold their candidacy bond, no more. + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(balances(&4), (36, 4)); + assert_eq!(balances(&5), (47, 3)); + assert_eq!( + Elections::members(), + vec![SeatHolder { who: 4, stake: 40, deposit: 4 }, SeatHolder { who: 5, stake: 50, deposit: 3 },] + ); + }) + } + + #[test] + fn candidates_are_always_sorted() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(candidate_ids(), Vec::::new()); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_eq!(candidate_ids(), vec![3]); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); + assert_eq!(candidate_ids(), vec![1, 3]); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + assert_eq!(candidate_ids(), vec![1, 2, 3]); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_eq!(candidate_ids(), vec![1, 2, 3, 4]); + }); + } + + #[test] + fn dupe_candidate_submission_should_not_work() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(candidate_ids(), Vec::::new()); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); + assert_eq!(candidate_ids(), vec![1]); + assert_noop!(submit_candidacy(RuntimeOrigin::signed(1)), Error::::DuplicatedCandidate); + }); + } + + #[test] + fn member_candidacy_submission_should_not_work() { + // critically important to make sure that outgoing candidates and losers are not mixed up. + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![5]); + assert!(Elections::runners_up().is_empty()); + assert!(candidate_ids().is_empty()); + + assert_noop!(submit_candidacy(RuntimeOrigin::signed(5)), Error::::MemberSubmit); + }); + } + + #[test] + fn runner_candidate_submission_should_not_work() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 4], 20)); + assert_ok!(vote(RuntimeOrigin::signed(1), vec![3], 10)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![3]); + + assert_noop!(submit_candidacy(RuntimeOrigin::signed(3)), Error::::RunnerUpSubmit); + }); + } + + #[test] + fn poor_candidate_submission_should_not_work() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(candidate_ids(), Vec::::new()); + assert_noop!(submit_candidacy(RuntimeOrigin::signed(7)), Error::::InsufficientCandidateFunds,); + }); + } + + #[test] + fn simple_voting_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(candidate_ids(), Vec::::new()); + assert_eq!(balances(&2), (20, 0)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 20); + }); + } + + #[test] + fn can_vote_with_custom_stake() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(candidate_ids(), Vec::::new()); + assert_eq!(balances(&2), (20, 0)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 12)); + + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 12); + }); + } + + #[test] + fn can_update_votes_and_stake() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(balances(&2), (20, 0)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + + // User only locks up to their free balance. + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 20); + assert_eq!(locked_stake_of(&2), 20); + + // can update; different stake; different lock and reserve. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 4], 15)); + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 15); + assert_eq!(locked_stake_of(&2), 15); + }); + } + + #[test] + fn voting_reserves_bond_per_vote() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(balances(&2), (20, 0)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + // initial vote. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 10)); + + // 2 + 1 + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 10); + assert_eq!(locked_stake_of(&2), 10); + + // can update; different stake; different lock and reserve. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 4], 15)); + // 2 + 2 + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 15); + assert_eq!(locked_stake_of(&2), 15); + + // stay at two votes with different stake. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5, 3], 18)); + // 2 + 2 + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 18); + assert_eq!(locked_stake_of(&2), 18); + + // back to 1 vote. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 12)); + // 2 + 1 + assert_eq!(balances(&2), (20, 0)); + assert_eq!(has_lock(&2), 12); + assert_eq!(locked_stake_of(&2), 12); + }); + } + + #[test] + fn cannot_vote_for_no_candidate() { + ExtBuilder::default().build_and_execute(|| { + assert_noop!(vote(RuntimeOrigin::signed(2), vec![], 20), Error::::NoVotes); + }); + } + + #[test] + fn can_vote_for_old_members_even_when_no_new_candidates() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![4, 5], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert!(candidate_ids().is_empty()); + + assert_ok!(vote(RuntimeOrigin::signed(3), vec![4, 5], 10)); + }); + } + + #[test] + fn prime_works() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + assert_ok!(vote(RuntimeOrigin::signed(1), vec![4, 3], 10)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert!(candidate_ids().is_empty()); + + assert_ok!(vote(RuntimeOrigin::signed(3), vec![4, 5], 10)); + assert_eq!(PRIME.with(|p| *p.borrow()), Some(4)); + }); + } + + #[test] + fn prime_votes_for_exiting_members_are_removed() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + assert_ok!(vote(RuntimeOrigin::signed(1), vec![4, 3], 10)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Candidate(3))); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![3, 5]); + assert!(candidate_ids().is_empty()); + + assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); + }); + } + + #[test] + fn prime_is_kept_if_other_members_leave() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Member)); + + assert_eq!(members_ids(), vec![5]); + assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); + }) + } + + #[test] + fn prime_is_gone_if_renouncing() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(PRIME.with(|p| *p.borrow()), Some(5)); + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Member)); + + assert_eq!(members_ids(), vec![4]); + assert_eq!(PRIME.with(|p| *p.borrow()), None); + }) + } + + #[test] + fn cannot_vote_for_more_than_candidates_and_members_and_runners() { + ExtBuilder::default().desired_runners_up(1).balance_factor(10).build_and_execute(|| { + // when we have only candidates + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_noop!( + // content of the vote is irrelevant. + vote(RuntimeOrigin::signed(1), vec![9, 99, 999, 9999], 5), + Error::::TooManyVotes, + ); + + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + // now we have 2 members, 1 runner-up, and 1 new candidate + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(1), vec![9, 99, 999, 9999], 5)); + assert_noop!( + vote(RuntimeOrigin::signed(1), vec![9, 99, 999, 9_999, 99_999], 5), + Error::::TooManyVotes, + ); + }); + } + + #[test] + fn cannot_vote_for_less_than_ed() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + assert_noop!(vote(RuntimeOrigin::signed(2), vec![4], 1), Error::::LowBalance); + }) + } + + #[test] + fn can_vote_for_more_than_free_balance_but_moot() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + // User has 100 free and 50 reserved. + assert_ok!(Balances::force_set_balance(RuntimeOrigin::root(), 2, 150)); + // User tries to vote with 150 tokens. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![4, 5], 150)); + // We truncate to only their free balance, after reserving additional for voting. + assert_eq!(locked_stake_of(&2), 150); + assert_eq!(has_lock(&2), 150); + }); + } + + #[test] + fn remove_voter_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![5], 30)); + + assert_eq_uvec!(all_voters(), vec![2, 3]); + assert_eq!(balances(&2), (20, 0)); + assert_eq!(locked_stake_of(&2), 20); + assert_eq!(balances(&3), (30, 0)); + assert_eq!(locked_stake_of(&3), 30); + assert_eq!(votes_of(&2), vec![5]); + assert_eq!(votes_of(&3), vec![5]); + + assert_noop!(Elections::remove_voter(RuntimeOrigin::signed(2)), Error::::VotingPeriodNotEnded); + System::set_block_number(3); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); + + assert_eq_uvec!(all_voters(), vec![3]); + assert!(votes_of(&2).is_empty()); + assert_eq!(locked_stake_of(&2), 0); + + assert_eq!(balances(&2), (20, 0)); + assert_eq!(Balances::locks(&2).len(), 0); + }); + } + + #[test] + fn non_voter_remove_should_not_work() { + ExtBuilder::default().build_and_execute(|| { + assert_noop!(Elections::remove_voter(RuntimeOrigin::signed(3)), Error::::MustBeVoter); + }); + } + + #[test] + fn dupe_remove_should_fail() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + System::set_block_number(5); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); + assert!(all_voters().is_empty()); + + assert_noop!(Elections::remove_voter(RuntimeOrigin::signed(2)), Error::::MustBeVoter); + }); + } + + #[test] + fn removed_voter_should_not_be_counted() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + System::set_block_number(3); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![3, 5]); + }); + } + + #[test] + fn simple_voting_rounds_should_work() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 15)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + assert_eq_uvec!(all_voters(), vec![2, 3, 4]); + + assert_eq!(votes_of(&2), vec![5]); + assert_eq!(votes_of(&3), vec![3]); + assert_eq!(votes_of(&4), vec![4]); + + assert_eq!(candidate_ids(), vec![3, 4, 5]); + assert_eq!(>::decode_len().unwrap(), 3); + + assert_eq!(Elections::election_rounds(), 0); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(balances(&3), (27, 3)); + // votes for 5 + assert_eq!(balances(&2), (20, 0)); + assert_eq!(members_and_stake(), vec![(3, 30), (5, 20)]); + assert!(Elections::runners_up().is_empty()); + + assert_eq_uvec!(all_voters(), vec![2, 3, 4]); + assert!(candidate_ids().is_empty()); + assert_eq!(>::decode_len(), None); + + assert_eq!(Elections::election_rounds(), 1); + }); + } + + #[test] + fn empty_term() { + ExtBuilder::default().build_and_execute(|| { + // no candidates, no nothing. + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + System::assert_last_event(RuntimeEvent::Elections(super::Event::EmptyTerm)); + }) + } + + #[test] + fn all_outgoing() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + System::assert_last_event(RuntimeEvent::Elections(super::Event::NewTerm { + new_members: vec![(4, 40), (5, 50)], + })); + + assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]); + assert_eq!(runners_up_and_stake(), vec![]); + + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(5))); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + System::assert_last_event(RuntimeEvent::Elections(super::Event::NewTerm { new_members: vec![] })); + + // outgoing have lost their bond. + assert_eq!(balances(&4), (37, 0)); + assert_eq!(balances(&5), (47, 0)); + }); + } + + #[test] + fn defunct_voter_will_be_counted() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + // This guy's vote is pointless for this round. + assert_ok!(vote(RuntimeOrigin::signed(3), vec![4], 30)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_and_stake(), vec![(5, 50)]); + assert_eq!(Elections::election_rounds(), 1); + + // but now it has a valid target. + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + // candidate 4 is affected by an old vote. + assert_eq!(members_and_stake(), vec![(4, 30), (5, 50)]); + assert_eq!(Elections::election_rounds(), 2); + assert_eq_uvec!(all_voters(), vec![3, 5]); + }); + } + + #[test] + fn only_desired_seats_are_chosen() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(Elections::election_rounds(), 1); + assert_eq!(members_ids(), vec![4, 5]); + }); + } + + #[test] + fn phragmen_should_not_self_vote() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert!(candidate_ids().is_empty()); + assert_eq!(Elections::election_rounds(), 1); + assert!(members_ids().is_empty()); + + System::assert_last_event(RuntimeEvent::Elections(super::Event::NewTerm { new_members: vec![] })); + }); + } + + #[test] + fn runners_up_should_be_kept() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![3], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![2], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + // sorted based on account id. + assert_eq!(members_ids(), vec![4, 5]); + // sorted based on merit (least -> most) + assert_eq!(runners_up_ids(), vec![3, 2]); + + // runner ups are still locked. + assert_eq!(balances(&4), (37, 3)); + assert_eq!(balances(&5), (47, 3)); + assert_eq!(balances(&3), (27, 3)); + }); + } + + #[test] + fn runners_up_should_be_next_candidates() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]); + assert_eq!(runners_up_and_stake(), vec![(2, 20), (3, 30)]); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 10)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_and_stake(), vec![(3, 30), (4, 40)]); + assert_eq!(runners_up_and_stake(), vec![(5, 10), (2, 20)]); + }); + } + + #[test] + fn runners_up_lose_bond_once_outgoing() { + ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![2]); + assert_eq!(balances(&2), (17, 3)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + assert_eq!(runners_up_ids(), vec![3]); + assert_eq!(balances(&2), (17, 0)); + }); + } + + #[test] + fn members_lose_bond_once_outgoing() { + ExtBuilder::default().build_and_execute(|| { + assert_eq!(balances(&5), (50, 0)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_eq!(balances(&5), (47, 3)); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_eq!(balances(&5), (47, 3)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + assert_eq!(members_ids(), vec![5]); + + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(5))); + assert_eq!(balances(&5), (47, 3)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + assert!(members_ids().is_empty()); + + assert_eq!(balances(&5), (47, 0)); + }); + } + + #[test] + fn candidates_lose_the_bond_when_outgoing() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); + + assert_eq!(balances(&5), (47, 3)); + assert_eq!(balances(&3), (27, 3)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![5]); + + // winner + assert_eq!(balances(&5), (47, 3)); + // loser + assert_eq!(balances(&3), (27, 0)); + }); + } + + #[test] + fn current_members_are_always_next_candidate() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + println!("{:?}", Elections::voting(4)); + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(Elections::election_rounds(), 1); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); + + // 5 will persist as candidates despite not being in the list. + assert_eq!(candidate_ids(), vec![2, 3]); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + // 4 removed; 5 and 3 are the new best. + assert_eq!(members_ids(), vec![3, 5]); + }); + } + + #[test] + fn election_state_is_uninterrupted() { + // what I mean by uninterrupted: + // given no input or stimulants the same members are re-elected. + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + let check_at_block = |b: u32| { + System::set_block_number(b.into()); + Elections::on_initialize(System::block_number()); + // we keep re-electing the same folks. + assert_eq!(members_and_stake(), vec![(4, 40), (5, 50)]); + assert_eq!(runners_up_and_stake(), vec![(2, 20), (3, 30)]); + // no new candidates but old members and runners-up are always added. + assert!(candidate_ids().is_empty()); + assert_eq!(Elections::election_rounds(), b / 5); + assert_eq_uvec!(all_voters(), vec![2, 3, 4, 5]); + }; + + // this state will always persist when no further input is given. + check_at_block(5); + check_at_block(10); + check_at_block(15); + check_at_block(20); + }); + } + + #[test] + fn remove_members_triggers_election() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(Elections::election_rounds(), 1); + + // a new candidate + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + assert_ok!(Elections::remove_member(RuntimeOrigin::root(), 4, true, true)); + + assert_eq!(balances(&4), (37, 0)); // slashed + assert_eq!(Elections::election_rounds(), 2); // new election round + assert_eq!(members_ids(), vec![3, 5]); // new members + }); + } + + #[test] + fn seats_should_be_released_when_no_vote() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![3], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + assert_eq!(>::decode_len().unwrap(), 3); + + assert_eq!(Elections::election_rounds(), 0); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + assert_eq!(members_ids(), vec![3, 5]); + assert_eq!(Elections::election_rounds(), 1); + + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(2))); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(3))); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(4))); + assert_ok!(Elections::remove_voter(RuntimeOrigin::signed(5))); + + // meanwhile, no one cares to become a candidate again. + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + assert!(members_ids().is_empty()); + assert_eq!(Elections::election_rounds(), 2); + }); + } + + #[test] + fn incoming_outgoing_are_reported() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + assert_eq!(members_ids(), vec![4, 5]); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + // 5 will change their vote and becomes an `outgoing` + assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 8)); + // 4 will stay in the set + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + // 3 will become a winner + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + // these two are losers. + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + assert_ok!(vote(RuntimeOrigin::signed(1), vec![1], 10)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + // 3, 4 are new members, must still be bonded, nothing slashed. + assert_eq!(members_and_stake(), vec![(3, 30), (4, 48)]); + assert_eq!(balances(&3), (27, 3)); + assert_eq!(balances(&4), (37, 3)); + + // 1 is a loser, slashed by 3. + assert_eq!(balances(&1), (7, 0)); + + // 5 is an outgoing loser. will also get slashed. + assert_eq!(balances(&5), (47, 0)); + + System::assert_has_event(RuntimeEvent::Elections(super::Event::NewTerm { + new_members: vec![(4, 40), (5, 50)], + })); + }) + } + + #[test] + fn invalid_votes_are_moot() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![10], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq_uvec!(members_ids(), vec![3, 4]); + assert_eq!(Elections::election_rounds(), 1); + }); + } + + #[test] + fn members_are_sorted_based_on_id_runners_on_merit() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![3], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![2], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + // id: low -> high. + assert_eq!(members_and_stake(), vec![(4, 50), (5, 40)]); + // merit: low -> high. + assert_eq!(runners_up_and_stake(), vec![(3, 20), (2, 30)]); + }); + } + + #[test] + fn runner_up_replacement_maintains_members_order() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![2], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![2, 4]); + assert_ok!(Elections::remove_member(RuntimeOrigin::root(), 2, true, false)); + assert_eq!(members_ids(), vec![4, 5]); + }); + } + + #[test] + fn can_renounce_candidacy_member_with_runners_bond_is_refunded() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![2, 3]); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Member)); + assert_eq!(balances(&4), (40, 0)); // 2 is voting bond. + + assert_eq!(members_ids(), vec![3, 5]); + assert_eq!(runners_up_ids(), vec![2]); + }) + } + + #[test] + fn can_renounce_candidacy_member_without_runners_bond_is_refunded() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert!(runners_up_ids().is_empty()); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Member)); + assert_eq!(balances(&4), (40, 0)); // 2 is voting bond. + + // no replacement + assert_eq!(members_ids(), vec![5]); + assert!(runners_up_ids().is_empty()); + }) + } + + #[test] + fn can_renounce_candidacy_runner_up() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![4], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![5], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![2, 3]); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(3), Renouncing::RunnerUp)); + assert_eq!(balances(&3), (30, 0)); // 2 is voting bond. + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![2]); + }) + } + + #[test] + fn runner_up_replacement_works_when_out_of_order() { + ExtBuilder::default().desired_runners_up(2).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(2), vec![5], 20)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![2], 50)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![2, 4]); + assert_eq!(runners_up_ids(), vec![5, 3]); + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(3), Renouncing::RunnerUp)); + assert_eq!(members_ids(), vec![2, 4]); + assert_eq!(runners_up_ids(), vec![5]); + }); + } + + #[test] + fn can_renounce_candidacy_candidate() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_eq!(balances(&5), (47, 3)); + assert_eq!(candidate_ids(), vec![5]); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Candidate(1))); + assert_eq!(balances(&5), (50, 0)); + assert!(candidate_ids().is_empty()); + }) + } + + #[test] + fn wrong_renounce_candidacy_should_fail() { + ExtBuilder::default().build_and_execute(|| { + assert_noop!( + Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Candidate(0)), + Error::::InvalidRenouncing, + ); + assert_noop!( + Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Member), + Error::::InvalidRenouncing, + ); + assert_noop!( + Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::RunnerUp), + Error::::InvalidRenouncing, + ); + }) + } + + #[test] + fn non_member_renounce_member_should_fail() { + ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![3]); + + assert_noop!( + Elections::renounce_candidacy(RuntimeOrigin::signed(3), Renouncing::Member), + Error::::InvalidRenouncing, + ); + }) + } + + #[test] + fn non_runner_up_renounce_runner_up_should_fail() { + ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![3]); + + assert_noop!( + Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::RunnerUp), + Error::::InvalidRenouncing, + ); + }) + } + + #[test] + fn wrong_candidate_count_renounce_should_fail() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_noop!( + Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Candidate(2)), + Error::::InvalidWitnessData, + ); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Candidate(3))); + }) + } + + #[test] + fn renounce_candidacy_count_can_overestimate() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + // while we have only 3 candidates. + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Candidate(4))); + }) + } + + #[test] + fn unsorted_runners_up_are_detected() { + ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 5)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 15)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![5]); + assert_eq!(runners_up_ids(), vec![4, 3]); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 10)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![5]); + assert_eq!(runners_up_ids(), vec![2, 3]); + + // 4 is outgoing runner-up. Slash candidacy bond. + assert_eq!(balances(&4), (37, 0)); + // 3 stays. + assert_eq!(balances(&3), (27, 3)); + }) + } + + #[test] + fn member_to_runner_up_wont_slash() { + ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4]); + assert_eq!(runners_up_ids(), vec![2, 3]); + + assert_eq!(balances(&4), (37, 3)); + assert_eq!(balances(&3), (27, 3)); + assert_eq!(balances(&2), (17, 3)); + + // this guy will shift everyone down. + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![5]); + assert_eq!(runners_up_ids(), vec![3, 4]); + + // 4 went from member to runner-up -- don't slash. + assert_eq!(balances(&4), (37, 3)); + // 3 stayed runner-up -- don't slash. + assert_eq!(balances(&3), (27, 3)); + // 2 was removed -- slash. + assert_eq!(balances(&2), (17, 0)); + }); + } + + #[test] + fn runner_up_to_member_wont_slash() { + ExtBuilder::default().desired_runners_up(2).desired_members(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4]); + assert_eq!(runners_up_ids(), vec![2, 3]); + + assert_eq!(balances(&4), (37, 3)); + assert_eq!(balances(&3), (27, 3)); + assert_eq!(balances(&2), (17, 3)); + + // swap some votes. + assert_ok!(vote(RuntimeOrigin::signed(4), vec![2], 40)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![4], 20)); + + System::set_block_number(10); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![2]); + assert_eq!(runners_up_ids(), vec![4, 3]); + + // 2 went from runner to member, don't slash + assert_eq!(balances(&2), (17, 3)); + // 4 went from member to runner, don't slash + assert_eq!(balances(&4), (37, 3)); + // 3 stayed the same + assert_eq!(balances(&3), (27, 3)); + }); + } + + #[test] + fn remove_and_replace_member_works() { + let setup = || { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5], 50)); + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![4, 5]); + assert_eq!(runners_up_ids(), vec![3]); + }; + + // member removed, replacement found. + ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { + setup(); + assert_eq!(Elections::remove_and_replace_member(&4, false), Ok(true)); + + assert_eq!(members_ids(), vec![3, 5]); + assert_eq!(runners_up_ids().len(), 0); + }); + + // member removed, no replacement found. + ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { + setup(); + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(3), Renouncing::RunnerUp)); + assert_eq!(Elections::remove_and_replace_member(&4, false), Ok(false)); + + assert_eq!(members_ids(), vec![5]); + assert_eq!(runners_up_ids().len(), 0); + }); + + // wrong member to remove. + ExtBuilder::default().desired_runners_up(1).build_and_execute(|| { + setup(); + assert!(matches!(Elections::remove_and_replace_member(&2, false), Err(_))); + }); + } + + #[test] + fn no_desired_members() { + // not interested in anything + ExtBuilder::default().desired_members(0).desired_runners_up(0).build_and_execute(|| { + assert_eq!(Elections::candidates().len(), 0); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_eq!(Elections::candidates().len(), 3); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids().len(), 0); + assert_eq!(runners_up_ids().len(), 0); + assert_eq!(all_voters().len(), 3); + assert_eq!(Elections::candidates().len(), 0); + }); + + // not interested in members + ExtBuilder::default().desired_members(0).desired_runners_up(2).build_and_execute(|| { + assert_eq!(Elections::candidates().len(), 0); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_eq!(Elections::candidates().len(), 3); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids().len(), 0); + assert_eq!(runners_up_ids(), vec![3, 4]); + assert_eq!(all_voters().len(), 3); + assert_eq!(Elections::candidates().len(), 0); + }); + + // not interested in runners-up + ExtBuilder::default().desired_members(2).desired_runners_up(0).build_and_execute(|| { + assert_eq!(Elections::candidates().len(), 0); + + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + + assert_eq!(Elections::candidates().len(), 3); + + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 40)); + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2], 20)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![3, 4]); + assert_eq!(runners_up_ids().len(), 0); + assert_eq!(all_voters().len(), 3); + assert_eq!(Elections::candidates().len(), 0); + }); + } + + #[test] + fn dupe_vote_is_moot() { + ExtBuilder::default().desired_members(1).build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(2))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(1))); + + // all these duplicate votes will not cause 2 to win. + assert_ok!(vote(RuntimeOrigin::signed(1), vec![2, 2, 2, 2], 5)); + assert_ok!(vote(RuntimeOrigin::signed(2), vec![2, 2, 2, 2], 20)); + + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 30)); + + System::set_block_number(5); + Elections::on_initialize(System::block_number()); + + assert_eq!(members_ids(), vec![3]); + }) + } + + #[test] + fn remove_defunct_voter_works() { + ExtBuilder::default().build_and_execute(|| { + assert_ok!(submit_candidacy(RuntimeOrigin::signed(5))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(4))); + assert_ok!(submit_candidacy(RuntimeOrigin::signed(3))); + + // defunct + assert_ok!(vote(RuntimeOrigin::signed(5), vec![5, 4], 5)); + // defunct + assert_ok!(vote(RuntimeOrigin::signed(4), vec![4], 5)); + // ok + assert_ok!(vote(RuntimeOrigin::signed(3), vec![3], 5)); + // ok + assert_ok!(vote(RuntimeOrigin::signed(2), vec![3, 4], 5)); + + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(5), Renouncing::Candidate(3))); + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(4), Renouncing::Candidate(2))); + assert_ok!(Elections::renounce_candidacy(RuntimeOrigin::signed(3), Renouncing::Candidate(1))); + + assert_ok!(Elections::clean_defunct_voters(RuntimeOrigin::root(), 4, 2)); + }) + } +} diff --git a/pallets/elections-phragmen/src/weights.rs b/pallets/elections-phragmen/src/weights.rs new file mode 100644 index 000000000..96c0c73c3 --- /dev/null +++ b/pallets/elections-phragmen/src/weights.rs @@ -0,0 +1,576 @@ +// Copyright (C) Parity Technologies (UK) Ltd. + +// Polimec Blockchain – https://www.polimec.org/ +// Copyright (C) Polimec 2022. All rights reserved. + +// This library includes code from Substrate, which is licensed +// under both the GNU General Public License version 3 (GPLv3) and the +// Apache License 2.0. You may choose to redistribute and/or modify this +// code under either the terms of the GPLv3 or the Apache 2.0 License, +// whichever suits your needs. + +//! Autogenerated weights for pallet_elections_phragmen +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-06-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-e8ezs4ez-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_elections_phragmen +// --no-storage-info +// --no-median-slopes +// --no-min-squares +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/elections-phragmen/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_elections_phragmen. +pub trait WeightInfo { + fn vote_equal(v: u32, ) -> Weight; + fn vote_more(v: u32, ) -> Weight; + fn vote_less(v: u32, ) -> Weight; + fn remove_voter() -> Weight; + fn submit_candidacy(c: u32, ) -> Weight; + fn renounce_candidacy_candidate(c: u32, ) -> Weight; + fn renounce_candidacy_members() -> Weight; + fn renounce_candidacy_runners_up() -> Weight; + fn remove_member_without_replacement() -> Weight; + fn remove_member_with_replacement() -> Weight; + fn clean_defunct_voters(v: u32, d: u32, ) -> Weight; + fn election_phragmen(c: u32, v: u32, e: u32, ) -> Weight; +} + +/// Weights for pallet_elections_phragmen using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `v` is `[1, 16]`. + fn vote_equal(v: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `403 + v * (80 ±0)` + // Estimated: `4764 + v * (80 ±0)` + // Minimum execution time: 33_028_000 picoseconds. + Weight::from_parts(34_073_914, 4764) + // Standard Error: 3_474 + .saturating_add(Weight::from_parts(205_252, 0).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 80).saturating_mul(v.into())) + } + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `v` is `[2, 16]`. + fn vote_more(v: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `371 + v * (80 ±0)` + // Estimated: `4764 + v * (80 ±0)` + // Minimum execution time: 45_725_000 picoseconds. + Weight::from_parts(47_169_586, 4764) + // Standard Error: 5_148 + .saturating_add(Weight::from_parts(213_742, 0).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 80).saturating_mul(v.into())) + } + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `v` is `[2, 16]`. + fn vote_less(v: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `403 + v * (80 ±0)` + // Estimated: `4764 + v * (80 ±0)` + // Minimum execution time: 45_519_000 picoseconds. + Weight::from_parts(47_339_108, 4764) + // Standard Error: 5_501 + .saturating_add(Weight::from_parts(195_247, 0).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 80).saturating_mul(v.into())) + } + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn remove_voter() -> Weight { + // Proof Size summary in bytes: + // Measured: `925` + // Estimated: `4764` + // Minimum execution time: 50_386_000 picoseconds. + Weight::from_parts(51_378_000, 4764) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: Elections Candidates (r:1 w:1) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `c` is `[1, 64]`. + fn submit_candidacy(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1570 + c * (48 ±0)` + // Estimated: `3055 + c * (48 ±0)` + // Minimum execution time: 38_987_000 picoseconds. + Weight::from_parts(41_302_276, 3055) + // Standard Error: 2_047 + .saturating_add(Weight::from_parts(125_200, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(c.into())) + } + /// Storage: Elections Candidates (r:1 w:1) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `c` is `[1, 64]`. + fn renounce_candidacy_candidate(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `285 + c * (48 ±0)` + // Estimated: `1770 + c * (48 ±0)` + // Minimum execution time: 33_510_000 picoseconds. + Weight::from_parts(34_947_760, 1770) + // Standard Error: 1_781 + .saturating_add(Weight::from_parts(78_851, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(c.into())) + } + /// Storage: Elections Members (r:1 w:1) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Members (r:0 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + fn renounce_candidacy_members() -> Weight { + // Proof Size summary in bytes: + // Measured: `1900` + // Estimated: `3385` + // Minimum execution time: 50_603_000 picoseconds. + Weight::from_parts(51_715_000, 3385) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + fn renounce_candidacy_runners_up() -> Weight { + // Proof Size summary in bytes: + // Measured: `880` + // Estimated: `2365` + // Minimum execution time: 33_441_000 picoseconds. + Weight::from_parts(34_812_000, 2365) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: Benchmark Override (r:0 w:0) + /// Proof Skipped: Benchmark Override (max_values: None, max_size: None, mode: Measured) + fn remove_member_without_replacement() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_000_000_000_000 picoseconds. + Weight::from_parts(2_000_000_000_000, 0) + } + /// Storage: Elections Members (r:1 w:1) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Members (r:0 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + fn remove_member_with_replacement() -> Weight { + // Proof Size summary in bytes: + // Measured: `1900` + // Estimated: `3593` + // Minimum execution time: 57_289_000 picoseconds. + Weight::from_parts(58_328_000, 3593) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: Elections Voting (r:513 w:512) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Balances Locks (r:512 w:512) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:512 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:512 w:512) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `v` is `[256, 512]`. + /// The range of component `d` is `[0, 256]`. + fn clean_defunct_voters(v: u32, _d: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1149 + v * (811 ±0)` + // Estimated: `4621 + v * (3774 ±0)` + // Minimum execution time: 18_774_231_000 picoseconds. + Weight::from_parts(18_933_040_000, 4621) + // Standard Error: 301_534 + .saturating_add(Weight::from_parts(44_306_903, 0).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((4_u64).saturating_mul(v.into()))) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(v.into()))) + .saturating_add(Weight::from_parts(0, 3774).saturating_mul(v.into())) + } + /// Storage: Elections Candidates (r:1 w:1) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:1) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:513 w:0) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: System Account (r:44 w:44) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Elections ElectionRounds (r:1 w:1) + /// Proof Skipped: Elections ElectionRounds (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Members (r:0 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:0 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `c` is `[1, 64]`. + /// The range of component `v` is `[1, 512]`. + /// The range of component `e` is `[512, 8192]`. + fn election_phragmen(c: u32, v: u32, e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + e * (28 ±0) + v * (606 ±0)` + // Estimated: `178887 + c * (2135 ±7) + e * (12 ±0) + v * (2653 ±6)` + // Minimum execution time: 1_281_877_000 picoseconds. + Weight::from_parts(1_288_147_000, 178887) + // Standard Error: 528_851 + .saturating_add(Weight::from_parts(17_761_407, 0).saturating_mul(v.into())) + // Standard Error: 33_932 + .saturating_add(Weight::from_parts(698_277, 0).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(21_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(v.into()))) + .saturating_add(T::DbWeight::get().writes(6_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2135).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 12).saturating_mul(e.into())) + .saturating_add(Weight::from_parts(0, 2653).saturating_mul(v.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `v` is `[1, 16]`. + fn vote_equal(v: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `403 + v * (80 ±0)` + // Estimated: `4764 + v * (80 ±0)` + // Minimum execution time: 33_028_000 picoseconds. + Weight::from_parts(34_073_914, 4764) + // Standard Error: 3_474 + .saturating_add(Weight::from_parts(205_252, 0).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 80).saturating_mul(v.into())) + } + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `v` is `[2, 16]`. + fn vote_more(v: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `371 + v * (80 ±0)` + // Estimated: `4764 + v * (80 ±0)` + // Minimum execution time: 45_725_000 picoseconds. + Weight::from_parts(47_169_586, 4764) + // Standard Error: 5_148 + .saturating_add(Weight::from_parts(213_742, 0).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 80).saturating_mul(v.into())) + } + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `v` is `[2, 16]`. + fn vote_less(v: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `403 + v * (80 ±0)` + // Estimated: `4764 + v * (80 ±0)` + // Minimum execution time: 45_519_000 picoseconds. + Weight::from_parts(47_339_108, 4764) + // Standard Error: 5_501 + .saturating_add(Weight::from_parts(195_247, 0).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(Weight::from_parts(0, 80).saturating_mul(v.into())) + } + /// Storage: Elections Voting (r:1 w:1) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn remove_voter() -> Weight { + // Proof Size summary in bytes: + // Measured: `925` + // Estimated: `4764` + // Minimum execution time: 50_386_000 picoseconds. + Weight::from_parts(51_378_000, 4764) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: Elections Candidates (r:1 w:1) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `c` is `[1, 64]`. + fn submit_candidacy(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1570 + c * (48 ±0)` + // Estimated: `3055 + c * (48 ±0)` + // Minimum execution time: 38_987_000 picoseconds. + Weight::from_parts(41_302_276, 3055) + // Standard Error: 2_047 + .saturating_add(Weight::from_parts(125_200, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(c.into())) + } + /// Storage: Elections Candidates (r:1 w:1) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `c` is `[1, 64]`. + fn renounce_candidacy_candidate(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `285 + c * (48 ±0)` + // Estimated: `1770 + c * (48 ±0)` + // Minimum execution time: 33_510_000 picoseconds. + Weight::from_parts(34_947_760, 1770) + // Standard Error: 1_781 + .saturating_add(Weight::from_parts(78_851, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_parts(0, 48).saturating_mul(c.into())) + } + /// Storage: Elections Members (r:1 w:1) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Members (r:0 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + fn renounce_candidacy_members() -> Weight { + // Proof Size summary in bytes: + // Measured: `1900` + // Estimated: `3385` + // Minimum execution time: 50_603_000 picoseconds. + Weight::from_parts(51_715_000, 3385) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + fn renounce_candidacy_runners_up() -> Weight { + // Proof Size summary in bytes: + // Measured: `880` + // Estimated: `2365` + // Minimum execution time: 33_441_000 picoseconds. + Weight::from_parts(34_812_000, 2365) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: Benchmark Override (r:0 w:0) + /// Proof Skipped: Benchmark Override (max_values: None, max_size: None, mode: Measured) + fn remove_member_without_replacement() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 2_000_000_000_000 picoseconds. + Weight::from_parts(2_000_000_000_000, 0) + } + /// Storage: Elections Members (r:1 w:1) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:1 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Members (r:0 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + fn remove_member_with_replacement() -> Weight { + // Proof Size summary in bytes: + // Measured: `1900` + // Estimated: `3593` + // Minimum execution time: 57_289_000 picoseconds. + Weight::from_parts(58_328_000, 3593) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: Elections Voting (r:513 w:512) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:0) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:0) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Candidates (r:1 w:0) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Balances Locks (r:512 w:512) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:512 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: System Account (r:512 w:512) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// The range of component `v` is `[256, 512]`. + /// The range of component `d` is `[0, 256]`. + fn clean_defunct_voters(v: u32, _d: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1149 + v * (811 ±0)` + // Estimated: `4621 + v * (3774 ±0)` + // Minimum execution time: 18_774_231_000 picoseconds. + Weight::from_parts(18_933_040_000, 4621) + // Standard Error: 301_534 + .saturating_add(Weight::from_parts(44_306_903, 0).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().reads((4_u64).saturating_mul(v.into()))) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(v.into()))) + .saturating_add(Weight::from_parts(0, 3774).saturating_mul(v.into())) + } + /// Storage: Elections Candidates (r:1 w:1) + /// Proof Skipped: Elections Candidates (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Members (r:1 w:1) + /// Proof Skipped: Elections Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections RunnersUp (r:1 w:1) + /// Proof Skipped: Elections RunnersUp (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Elections Voting (r:513 w:0) + /// Proof Skipped: Elections Voting (max_values: None, max_size: None, mode: Measured) + /// Storage: Council Proposals (r:1 w:0) + /// Proof Skipped: Council Proposals (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: System Account (r:44 w:44) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + /// Storage: Elections ElectionRounds (r:1 w:1) + /// Proof Skipped: Elections ElectionRounds (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Members (r:0 w:1) + /// Proof Skipped: Council Members (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Council Prime (r:0 w:1) + /// Proof Skipped: Council Prime (max_values: Some(1), max_size: None, mode: Measured) + /// The range of component `c` is `[1, 64]`. + /// The range of component `v` is `[1, 512]`. + /// The range of component `e` is `[512, 8192]`. + fn election_phragmen(c: u32, v: u32, e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + e * (28 ±0) + v * (606 ±0)` + // Estimated: `178887 + c * (2135 ±7) + e * (12 ±0) + v * (2653 ±6)` + // Minimum execution time: 1_281_877_000 picoseconds. + Weight::from_parts(1_288_147_000, 178887) + // Standard Error: 528_851 + .saturating_add(Weight::from_parts(17_761_407, 0).saturating_mul(v.into())) + // Standard Error: 33_932 + .saturating_add(Weight::from_parts(698_277, 0).saturating_mul(e.into())) + .saturating_add(RocksDbWeight::get().reads(21_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(v.into()))) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2135).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 12).saturating_mul(e.into())) + .saturating_add(Weight::from_parts(0, 2653).saturating_mul(v.into())) + } +} diff --git a/pallets/funding/Cargo.toml b/pallets/funding/Cargo.toml index 2e8e76e61..9feba58c9 100644 --- a/pallets/funding/Cargo.toml +++ b/pallets/funding/Cargo.toml @@ -127,9 +127,9 @@ try-runtime = [ "pallet-xcm/try-runtime", "polimec-common/try-runtime", "polimec-receiver/try-runtime", + "polimec-xcm-executor/try-runtime", "polkadot-runtime-parachains/try-runtime", "polkadot-runtime/try-runtime", "sp-runtime/try-runtime", - "polimec-xcm-executor/try-runtime" ] diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index bd6cc013c..09755424e 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -287,9 +287,13 @@ parameter_types! { pub PolimecReceiverInfo: xcm::v3::PalletInfo = xcm::v3::PalletInfo::new( 51, "PolimecReceiver".into(), "polimec_receiver".into(), 0, 1, 0 ).unwrap(); - #[cfg(feature = "runtime-benchmarks")] +} + +#[cfg(feature = "runtime-benchmarks")] +parameter_types! { pub BenchmarkReason: RuntimeHoldReason = RuntimeHoldReason::PolimecFunding(crate::HoldReason::Participation(0)); } + impl pallet_linear_release::Config for TestRuntime { type Balance = Balance; #[cfg(feature = "runtime-benchmarks")] diff --git a/pallets/oracle-ocw/Cargo.toml b/pallets/oracle-ocw/Cargo.toml index 0a7850c0c..38bc970de 100644 --- a/pallets/oracle-ocw/Cargo.toml +++ b/pallets/oracle-ocw/Cargo.toml @@ -45,6 +45,7 @@ std = [ "pallet-balances/std", "parity-scale-codec/std", "scale-info/std", + "serde-json-core/std", "serde/std", "sp-consensus-aura/std", "sp-core/std", @@ -53,7 +54,6 @@ std = [ "sp-runtime/std", "sp-std/std", "substrate-fixed/std", - "serde-json-core/std" ] try-runtime = [ @@ -67,5 +67,5 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-balances/runtime-benchmarks", - "sp-runtime/runtime-benchmarks" + "sp-runtime/runtime-benchmarks", ] diff --git a/pallets/parachain-staking/Cargo.toml b/pallets/parachain-staking/Cargo.toml index e583f6c29..4b838a786 100644 --- a/pallets/parachain-staking/Cargo.toml +++ b/pallets/parachain-staking/Cargo.toml @@ -85,6 +85,6 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-session/try-runtime", "pallet-timestamp/try-runtime", + "polimec-common/try-runtime", "sp-runtime/try-runtime", - "polimec-common/try-runtime" ] diff --git a/pallets/parachain-staking/src/auto_compound.rs b/pallets/parachain-staking/src/auto_compound.rs index ab382aac9..8bb04ca8c 100644 --- a/pallets/parachain-staking/src/auto_compound.rs +++ b/pallets/parachain-staking/src/auto_compound.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! Auto-compounding functionality for staking rewards use crate::{ diff --git a/pallets/parachain-staking/src/benchmarks.rs b/pallets/parachain-staking/src/benchmarks.rs index fbea85afd..5da392aca 100644 --- a/pallets/parachain-staking/src/benchmarks.rs +++ b/pallets/parachain-staking/src/benchmarks.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - #![cfg(feature = "runtime-benchmarks")] //! Benchmarking diff --git a/pallets/parachain-staking/src/delegation_requests.rs b/pallets/parachain-staking/src/delegation_requests.rs index cbeb358ae..971d5bdc6 100644 --- a/pallets/parachain-staking/src/delegation_requests.rs +++ b/pallets/parachain-staking/src/delegation_requests.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! Scheduled requests functionality for delegators use crate::{ diff --git a/pallets/parachain-staking/src/inflation.rs b/pallets/parachain-staking/src/inflation.rs index 1e33d2c2a..62c5fabc5 100644 --- a/pallets/parachain-staking/src/inflation.rs +++ b/pallets/parachain-staking/src/inflation.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! Helper methods for computing issuance based on inflation use crate::pallet::{BalanceOf, Config, Pallet}; use frame_support::traits::fungible::Inspect; diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index f55ca75ef..2eec4fe0c 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! # Parachain Staking //! Minimal staking pallet that implements collator selection by total backed stake. //! The main difference between this pallet and `frame/pallet-staking` is that this pallet diff --git a/pallets/parachain-staking/src/migrations.rs b/pallets/parachain-staking/src/migrations.rs index 996043fbf..f6726523b 100644 --- a/pallets/parachain-staking/src/migrations.rs +++ b/pallets/parachain-staking/src/migrations.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! # Migrations //! #[allow(unused_imports)] diff --git a/pallets/parachain-staking/src/mock.rs b/pallets/parachain-staking/src/mock.rs index c20aef51f..b8866b904 100644 --- a/pallets/parachain-staking/src/mock.rs +++ b/pallets/parachain-staking/src/mock.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! Test utilities use crate as pallet_parachain_staking; use crate::{pallet, AwardedPts, Config, Event as ParachainStakingEvent, InflationInfo, Points, Range}; diff --git a/pallets/parachain-staking/src/set.rs b/pallets/parachain-staking/src/set.rs index fe3ad11b7..89bb7522c 100644 --- a/pallets/parachain-staking/src/set.rs +++ b/pallets/parachain-staking/src/set.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; #[cfg(feature = "std")] diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index 76614cbe6..f3302b60e 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! # Staking Pallet Unit Tests //! The unit tests are organized by the call they test. The order matches the order //! of the calls in the `lib.rs`. diff --git a/pallets/parachain-staking/src/traits.rs b/pallets/parachain-staking/src/traits.rs index 26144de00..979a3a776 100644 --- a/pallets/parachain-staking/src/traits.rs +++ b/pallets/parachain-staking/src/traits.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! traits for parachain-staking use frame_support::pallet_prelude::Weight; diff --git a/pallets/parachain-staking/src/types.rs b/pallets/parachain-staking/src/types.rs index 5d2fca665..0bde3605a 100644 --- a/pallets/parachain-staking/src/types.rs +++ b/pallets/parachain-staking/src/types.rs @@ -16,8 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - - //! Types for parachain-staking use crate::{ diff --git a/runtimes/base/Cargo.toml b/runtimes/base/Cargo.toml index c46d4af34..9e9d47c29 100644 --- a/runtimes/base/Cargo.toml +++ b/runtimes/base/Cargo.toml @@ -41,17 +41,22 @@ frame-try-runtime = { workspace = true, optional = true } pallet-aura.workspace = true pallet-authorship.workspace = true pallet-balances.workspace = true +pallet-collective.workspace = true +pallet-democracy.workspace = true +pallet-elections-phragmen.workspace = true pallet-membership.workspace = true pallet-multisig.workspace = true +pallet-preimage.workspace = true pallet-proxy.workspace = true +pallet-scheduler.workspace = true pallet-session.workspace = true pallet-sudo.workspace = true pallet-timestamp.workspace = true pallet-transaction-payment.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-treasury.workspace = true -pallet-vesting.workspace = true pallet-utility.workspace = true +pallet-vesting.workspace = true sp-arithmetic.workspace = true sp-api.workspace = true sp-block-builder.workspace = true @@ -92,7 +97,8 @@ orml-oracle.workspace = true [features] default = [ "std" ] -fast-gov = [ "shared-configuration/fast-gov" ] +fast-mode = [ "shared-configuration/fast-mode" ] +instant-mode = [ "shared-configuration/instant-mode" ] std = [ "cumulus-pallet-aura-ext/std", "cumulus-pallet-dmp-queue/std", @@ -115,11 +121,16 @@ std = [ "pallet-aura/std", "pallet-authorship/std", "pallet-balances/std", + "pallet-collective/std", + "pallet-democracy/std", + "pallet-elections-phragmen/std", "pallet-membership/std", "pallet-multisig/std", "pallet-oracle-ocw/std", "pallet-parachain-staking/std", + "pallet-preimage/std", "pallet-proxy/std", + "pallet-scheduler/std", "pallet-session/std", "pallet-sudo/std", "pallet-timestamp/std", @@ -165,10 +176,16 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "hex-literal", "pallet-balances/runtime-benchmarks", + "pallet-collective/runtime-benchmarks", + "pallet-democracy/runtime-benchmarks", + "pallet-elections-phragmen/runtime-benchmarks", "pallet-membership/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", + "pallet-oracle-ocw/runtime-benchmarks", "pallet-parachain-staking/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", + "pallet-scheduler/runtime-benchmarks", "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", @@ -183,7 +200,6 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", - "pallet-oracle-ocw/runtime-benchmarks" ] try-runtime = [ @@ -200,11 +216,16 @@ try-runtime = [ "pallet-aura/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", + "pallet-collective/try-runtime", + "pallet-democracy/try-runtime", + "pallet-elections-phragmen/try-runtime", "pallet-membership/try-runtime", "pallet-multisig/try-runtime", "pallet-oracle-ocw/try-runtime", "pallet-parachain-staking/try-runtime", + "pallet-preimage/try-runtime", "pallet-proxy/try-runtime", + "pallet-scheduler/try-runtime", "pallet-session/try-runtime", "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", diff --git a/runtimes/base/src/custom_migrations.rs b/runtimes/base/src/custom_migrations.rs index 073b8c67d..ccd2e69fc 100644 --- a/runtimes/base/src/custom_migrations.rs +++ b/runtimes/base/src/custom_migrations.rs @@ -19,49 +19,35 @@ use crate::*; // Substrate #[allow(unused_imports)] -use frame_support::{dispatch::DispatchError, log, migration, storage::unhashed}; - -pub struct CustomOnRuntimeUpgrade; -impl frame_support::traits::OnRuntimeUpgrade for CustomOnRuntimeUpgrade { +use frame_support::{ + dispatch::DispatchError, + log, migration, + storage::unhashed, + traits::{GetStorageVersion, PalletInfoAccess, StorageVersion}, +}; + +pub struct InitializePallet + PalletInfoAccess>( + sp_std::marker::PhantomData, +); +impl + PalletInfoAccess> + frame_support::traits::OnRuntimeUpgrade for InitializePallet +{ #[cfg(feature = "try-runtime")] fn pre_upgrade() -> Result, DispatchError> { - use frame_support::pallet_prelude::StorageVersion; - - let membership_pallet_version = StorageVersion::get::(); - log::info!("OracleProvidersMembership migrating from {:#?}", membership_pallet_version); + log::info!("{} migrating from {:#?}", Pallet::name(), Pallet::on_chain_storage_version()); Ok(Vec::new()) } #[cfg(feature = "try-runtime")] fn post_upgrade(_state: Vec) -> Result<(), DispatchError> { - use frame_support::pallet_prelude::StorageVersion; - - let membership_pallet_version = StorageVersion::get::(); - log::info!("OracleProvidersMembership migrated to {:#?}", membership_pallet_version); + log::info!("{} migrated to {:#?}", Pallet::name(), Pallet::on_chain_storage_version()); Ok(()) } fn on_runtime_upgrade() -> frame_support::weights::Weight { - migrate() - } -} - -fn migrate() -> frame_support::weights::Weight { - // Substrate - use frame_support::traits::StorageVersion; - - // Some pallets are added on chain after the migration. - // Thus, they never required the migration and we just missed to set the correct - // `StorageVersion`. - let membership_pallet_version = StorageVersion::get::(); - log::info!("OracleProvidersMembership migrating from {:#?}", membership_pallet_version); - - if membership_pallet_version < 4 { - StorageVersion::new(4).put::(); + if Pallet::on_chain_storage_version() == StorageVersion::new(0) { + Pallet::current_storage_version().put::(); + } + ::DbWeight::get().reads_writes(1, 1) } - - let new_version = StorageVersion::get::(); - log::info!("OracleProvidersMembership migrated to {:#?}", new_version); - - ::DbWeight::get().reads_writes(0, 1) } diff --git a/runtimes/base/src/lib.rs b/runtimes/base/src/lib.rs index e174ffb6c..9f30492fb 100644 --- a/runtimes/base/src/lib.rs +++ b/runtimes/base/src/lib.rs @@ -24,14 +24,14 @@ extern crate frame_benchmarking; use cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases; use frame_support::{ construct_runtime, parameter_types, - traits::{fungible::Credit, ConstU32, Contains, InstanceFilter}, + traits::{fungible::Credit, tokens, ConstU32, Contains, EitherOfDiverse, InstanceFilter, PrivilegeCmp}, weights::{ConstantMultiplier, Weight}, }; -use frame_system::EnsureRoot; +use frame_system::{EnsureRoot, EnsureSigned}; use pallet_oracle_ocw::types::AssetName; use parachains_common::AssetIdForTrustBackedAssets as AssetId; use parity_scale_codec::Encode; -use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; +use polkadot_runtime_common::{BlockHashCount, CurrencyToVote, SlowAdjustingFeeUpdate}; use sp_api::impl_runtime_apis; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; @@ -46,7 +46,8 @@ use sp_runtime::{ ApplyExtrinsicResult, FixedU128, MultiSignature, SaturatedConversion, }; pub use sp_runtime::{MultiAddress, Perbill, Permill}; -use sp_std::prelude::*; +use sp_std::{cmp::Ordering, prelude::*}; + #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; @@ -130,7 +131,15 @@ pub mod migrations { #![allow(unused_imports)] use super::*; /// Unreleased migrations. Add new ones here: - pub type Unreleased = custom_migrations::CustomOnRuntimeUpgrade; + pub type Unreleased = ( + custom_migrations::InitializePallet, + custom_migrations::InitializePallet, + custom_migrations::InitializePallet, + custom_migrations::InitializePallet, + custom_migrations::InitializePallet, + custom_migrations::InitializePallet, + custom_migrations::InitializePallet, + ); } /// Executive: handles dispatch to the various modules. @@ -386,6 +395,182 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = (); } +pub struct ToTreasury; + +impl tokens::imbalance::OnUnbalanced> for ToTreasury { + fn on_nonzero_unbalanced(amount: CreditOf) { + let treasury_account = Treasury::account_id(); + let _ = >::resolve(&treasury_account, amount); + } +} + +impl pallet_treasury::Config for Runtime { + type ApproveOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; + type Burn = Burn; + type BurnDestination = (); + type Currency = Balances; + type MaxApprovals = MaxApprovals; + type OnSlash = Treasury; + type PalletId = TreasuryId; + type ProposalBond = ProposalBond; + type ProposalBondMaximum = (); + type ProposalBondMinimum = ProposalBondMinimum; + type RejectOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; + type RuntimeEvent = RuntimeEvent; + type SpendFunds = (); + type SpendOrigin = frame_support::traits::NeverEnsureOrigin; + type SpendPeriod = SpendPeriod; + type WeightInfo = pallet_treasury::weights::SubstrateWeight; +} + +type CouncilCollective = pallet_collective::Instance1; +impl pallet_collective::Config for Runtime { + type DefaultVote = pallet_collective::MoreThanMajorityThenPrimeDefaultVote; + type MaxMembers = CouncilMaxMembers; + type MaxProposalWeight = MaxCollectivesProposalWeight; + type MaxProposals = CouncilMaxProposals; + type MotionDuration = CouncilMotionDuration; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SetMembersOrigin = EnsureRoot; + type WeightInfo = pallet_collective::weights::SubstrateWeight; +} + +type TechnicalCollective = pallet_collective::Instance2; +impl pallet_collective::Config for Runtime { + type DefaultVote = pallet_collective::MoreThanMajorityThenPrimeDefaultVote; + type MaxMembers = TechnicalMaxMembers; + type MaxProposalWeight = MaxCollectivesProposalWeight; + type MaxProposals = TechnicalMaxProposals; + type MotionDuration = TechnicalMotionDuration; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SetMembersOrigin = EnsureRoot; + type WeightInfo = pallet_collective::weights::SubstrateWeight; +} + +impl pallet_elections_phragmen::Config for Runtime { + type Balance = Balance; + /// How much should be locked up in order to submit one's candidacy. + type CandidacyBond = CandidacyBond; + type ChangeMembers = Council; + type Currency = Balances; + type CurrencyToVote = CurrencyToVote; + /// Number of members to elect. + type DesiredMembers = DesiredMembers; + /// Number of runners_up to keep. + type DesiredRunnersUp = DesiredRunnersUp; + type InitializeMembers = Council; + type LoserCandidate = ToTreasury; + type MaxCandidates = MaxCandidates; + type MaxVoters = MaxVoters; + type MaxVotesPerVoter = MaxVotesPerVoter; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + /// How long each seat is kept. This defines the next block number at which + /// an election round will happen. If set to zero, no elections are ever + /// triggered and the module will be in passive mode. + type TermDuration = TermDuration; + type VotingLockPeriod = VotingLockPeriod; + type WeightInfo = pallet_elections_phragmen::weights::SubstrateWeight; +} + +impl pallet_democracy::Config for Runtime { + type BlacklistOrigin = EnsureRoot; + // To cancel a proposal before it has been passed, the technical committee must be unanimous or + // Root must agree. + type CancelProposalOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; + // To cancel a proposal which has been passed, 2/3 of the council must agree to it. + type CancellationOrigin = pallet_collective::EnsureProportionAtLeast; + type CooloffPeriod = CooloffPeriod; + type EnactmentPeriod = EnactmentPeriod; + /// A unanimous council can have the next scheduled referendum be a straight default-carries + /// (NTB) vote. + type ExternalDefaultOrigin = pallet_collective::EnsureProportionAtLeast; + /// A super-majority can have the next scheduled referendum be a straight majority-carries vote. + type ExternalMajorityOrigin = pallet_collective::EnsureProportionAtLeast; + /// A straight majority of the council can decide what their next motion is. + type ExternalOrigin = pallet_collective::EnsureProportionAtLeast; + /// Two thirds of the technical committee can have an ExternalMajority/ExternalDefault vote + /// be tabled immediately and with a shorter voting/enactment period. + type FastTrackOrigin = pallet_collective::EnsureProportionAtLeast; + type FastTrackVotingPeriod = FastTrackVotingPeriod; + type Fungible = Balances; + type InstantAllowed = frame_support::traits::ConstBool; + type InstantOrigin = pallet_collective::EnsureProportionAtLeast; + type LaunchPeriod = LaunchPeriod; + type MaxBlacklisted = MaxBlacklisted; + type MaxDeposits = MaxDeposits; + type MaxProposals = MaxProposals; + type MaxVotes = MaxVotes; + // Same as EnactmentPeriod + type MinimumDeposit = MinimumDeposit; + type PalletsOrigin = OriginCaller; + type Preimages = Preimage; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type Scheduler = Scheduler; + type Slash = ToTreasury; + type SubmitOrigin = EnsureSigned; + // Any single technical committee member may veto a coming council proposal, however they can + // only do it once and it lasts only for the cool-off period. + type VetoOrigin = pallet_collective::EnsureMember; + type VoteLockingPeriod = EnactmentPeriod; + type VotingPeriod = VotingPeriod; + type WeightInfo = pallet_democracy::weights::SubstrateWeight; +} + +pub struct EqualOrGreatestRootCmp; + +impl PrivilegeCmp for EqualOrGreatestRootCmp { + fn cmp_privilege(left: &OriginCaller, right: &OriginCaller) -> Option { + if left == right { + return Some(Ordering::Equal) + } + match (left, right) { + // Root is greater than anything. + (OriginCaller::system(frame_system::RawOrigin::Root), _) => Some(Ordering::Greater), + _ => None, + } + } +} + +impl pallet_scheduler::Config for Runtime { + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type MaximumWeight = MaximumSchedulerWeight; + type OriginPrivilegeCmp = EqualOrGreatestRootCmp; + type PalletsOrigin = OriginCaller; + type Preimages = Preimage; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type ScheduleOrigin = EnsureRoot; + type WeightInfo = (); +} + +impl pallet_preimage::Config for Runtime { + // TODO: Check this base deposit value. + type BaseDeposit = PreimageBaseDeposit; + type ByteDeposit = (); + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + impl pallet_parachain_staking::Config for Runtime { type Balance = Balance; type CandidateBondLessDelay = CandidateBondLessDelay; @@ -605,6 +790,15 @@ construct_runtime!( CumulusXcm: cumulus_pallet_xcm = 32, DmpQueue: cumulus_pallet_dmp_queue = 33, + // Governance + Treasury: pallet_treasury = 40, + Democracy: pallet_democracy::{Pallet, Call, Storage, Event, Config, HoldReason, FreezeReason} = 41, + Council: pallet_collective:: = 42, + TechnicalCommittee: pallet_collective:: = 43, + Elections: pallet_elections_phragmen::{Pallet, Call, Storage, Event, Config, HoldReason, FreezeReason} = 44, + Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 45, + Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 46, + // Oracle Oracle: orml_oracle::{Pallet, Call, Storage, Event} = 70, OracleProvidersMembership: pallet_membership:: = 71, diff --git a/runtimes/shared-configuration/Cargo.toml b/runtimes/shared-configuration/Cargo.toml index b98ef0ffa..bc391924a 100644 --- a/runtimes/shared-configuration/Cargo.toml +++ b/runtimes/shared-configuration/Cargo.toml @@ -36,7 +36,8 @@ pallet-parachain-staking.workspace = true [features] default = [ "std" ] -fast-gov = [] +fast-mode = [] +instant-mode = [] std = [ "frame-support/std", "frame-system/std", diff --git a/runtimes/shared-configuration/src/funding.rs b/runtimes/shared-configuration/src/funding.rs index 9aa6a5944..bc4d2d6a7 100644 --- a/runtimes/shared-configuration/src/funding.rs +++ b/runtimes/shared-configuration/src/funding.rs @@ -21,49 +21,67 @@ use parachains_common::AssetIdForTrustBackedAssets; use sp_arithmetic::{FixedU128, Percent}; use sp_std::{collections::btree_map::BTreeMap, vec, vec::Vec}; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const EVALUATION_DURATION: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const EVALUATION_DURATION: BlockNumber = 28; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const EVALUATION_DURATION: BlockNumber = 28 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const AUCTION_INITIALIZE_PERIOD_DURATION: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const AUCTION_INITIALIZE_PERIOD_DURATION: BlockNumber = 7; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const AUCTION_INITIALIZE_PERIOD_DURATION: BlockNumber = 7 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const ENGLISH_AUCTION_DURATION: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const ENGLISH_AUCTION_DURATION: BlockNumber = 10; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const ENGLISH_AUCTION_DURATION: BlockNumber = 2 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const CANDLE_AUCTION_DURATION: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const CANDLE_AUCTION_DURATION: BlockNumber = 5; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const CANDLE_AUCTION_DURATION: BlockNumber = 3 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const COMMUNITY_FUNDING_DURATION: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const COMMUNITY_FUNDING_DURATION: BlockNumber = 10; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const COMMUNITY_FUNDING_DURATION: BlockNumber = 5 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const REMAINDER_FUNDING_DURATION: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const REMAINDER_FUNDING_DURATION: BlockNumber = 10; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const REMAINDER_FUNDING_DURATION: BlockNumber = crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const CONTRIBUTION_VESTING_DURATION: BlockNumber = 5; +#[cfg(feature = "fast-mode")] pub const CONTRIBUTION_VESTING_DURATION: BlockNumber = 365; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const CONTRIBUTION_VESTING_DURATION: BlockNumber = 365 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 3; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const MANUAL_ACCEPTANCE_DURATION: BlockNumber = 3 * crate::DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const SUCCESS_TO_SETTLEMENT_TIME: BlockNumber = 1; +#[cfg(feature = "fast-mode")] pub const SUCCESS_TO_SETTLEMENT_TIME: BlockNumber = 4; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const SUCCESS_TO_SETTLEMENT_TIME: BlockNumber = 4 * crate::DAYS; pub type ProjectIdentifier = u32; diff --git a/runtimes/shared-configuration/src/governance.rs b/runtimes/shared-configuration/src/governance.rs index 91587236f..70ca8c924 100644 --- a/runtimes/shared-configuration/src/governance.rs +++ b/runtimes/shared-configuration/src/governance.rs @@ -20,90 +20,119 @@ use crate::{ }; use frame_support::{parameter_types, PalletId}; use parachains_common::BlockNumber; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "fast-mode")] use parachains_common::MINUTES; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] use parachains_common::{DAYS, HOURS}; use sp_arithmetic::Permill; -pub const MIN_DEPOSIT: Balance = PLMC; - -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const LAUNCH_PERIOD: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const LAUNCH_PERIOD: BlockNumber = 7 * MINUTES; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const LAUNCH_PERIOD: BlockNumber = 7 * DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const VOTING_PERIOD: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const VOTING_PERIOD: BlockNumber = 7 * MINUTES; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const VOTING_PERIOD: BlockNumber = 7 * DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const FAST_TRACK_VOTING_PERIOD: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const FAST_TRACK_VOTING_PERIOD: BlockNumber = 3 * MINUTES; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const FAST_TRACK_VOTING_PERIOD: BlockNumber = 3 * HOURS; -#[cfg(feature = "fast-gov")] -pub const ENACTMENT_PERIOD: BlockNumber = 8 * MINUTES; -#[cfg(not(feature = "fast-gov"))] -pub const ENACTMENT_PERIOD: BlockNumber = DAYS; +#[cfg(feature = "instant-mode")] +pub const ENACTMENT_PERIOD: BlockNumber = 2; +#[cfg(feature = "fast-mode")] +pub const ENACTMENT_PERIOD: BlockNumber = 7 * MINUTES; +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] +pub const ENACTMENT_PERIOD: BlockNumber = 7 * DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const COOLOFF_PERIOD: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const COOLOFF_PERIOD: BlockNumber = 7 * MINUTES; -#[cfg(not(feature = "fast-gov"))] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] pub const COOLOFF_PERIOD: BlockNumber = 7 * DAYS; -#[cfg(feature = "fast-gov")] -pub const SPEND_PERIOD: BlockNumber = 6 * MINUTES; -#[cfg(not(feature = "fast-gov"))] -pub const SPEND_PERIOD: BlockNumber = 6 * DAYS; - -#[cfg(feature = "fast-gov")] -pub const ROTATION_PERIOD: BlockNumber = 80 * MINUTES; -#[cfg(not(feature = "fast-gov"))] -pub const ROTATION_PERIOD: BlockNumber = 80 * HOURS; +#[cfg(feature = "instant-mode")] +pub const SPEND_PERIOD: BlockNumber = 2; +#[cfg(feature = "fast-mode")] +pub const SPEND_PERIOD: BlockNumber = 7 * MINUTES; +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] +pub const SPEND_PERIOD: BlockNumber = 7 * DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const TERM_DURATION: BlockNumber = 5; +#[cfg(feature = "fast-mode")] pub const TERM_DURATION: BlockNumber = 15 * MINUTES; -#[cfg(not(feature = "fast-gov"))] -pub const TERM_DURATION: BlockNumber = DAYS; - -#[cfg(feature = "fast-gov")] +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] +pub const TERM_DURATION: BlockNumber = 28 * DAYS; + +#[cfg(feature = "instant-mode")] +pub const ELECTION_VOTING_LOCK_DURATION: BlockNumber = 5; +#[cfg(feature = "fast-mode")] +pub const ELECTION_VOTING_LOCK_DURATION: BlockNumber = 15 * MINUTES; +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] +pub const ELECTION_VOTING_LOCK_DURATION: BlockNumber = 28 * DAYS; + +#[cfg(feature = "instant-mode")] +pub const COUNCIL_MOTION_DURATION: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const COUNCIL_MOTION_DURATION: BlockNumber = 4 * MINUTES; -#[cfg(not(feature = "fast-gov"))] -pub const COUNCIL_MOTION_DURATION: BlockNumber = 3 * DAYS; +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] +pub const COUNCIL_MOTION_DURATION: BlockNumber = 7 * DAYS; -#[cfg(feature = "fast-gov")] +#[cfg(feature = "instant-mode")] +pub const TECHNICAL_MOTION_DURATION: BlockNumber = 2; +#[cfg(feature = "fast-mode")] pub const TECHNICAL_MOTION_DURATION: BlockNumber = 4 * MINUTES; -#[cfg(not(feature = "fast-gov"))] -pub const TECHNICAL_MOTION_DURATION: BlockNumber = 3 * DAYS; +#[cfg(not(any(feature = "fast-mode", feature = "instant-mode")))] +pub const TECHNICAL_MOTION_DURATION: BlockNumber = 7 * DAYS; parameter_types! { // Democracy Pallet pub const LaunchPeriod: BlockNumber = LAUNCH_PERIOD; pub const VotingPeriod: BlockNumber = VOTING_PERIOD; pub const FastTrackVotingPeriod: BlockNumber = FAST_TRACK_VOTING_PERIOD; - pub const MinimumDeposit: Balance = MIN_DEPOSIT; + pub const MinimumDeposit: Balance = 100 * PLMC; pub const EnactmentPeriod: BlockNumber = ENACTMENT_PERIOD; pub const CooloffPeriod: BlockNumber = COOLOFF_PERIOD; // Council Pallet pub const CouncilMotionDuration: BlockNumber = COUNCIL_MOTION_DURATION; - pub const CouncilMaxProposals: u32 = 100; - pub const CouncilMaxMembers: u32 = 100; + pub const CouncilMaxProposals: u32 = 7; + pub const CouncilMaxMembers: u32 = 20; // Technical Committee pub const TechnicalMotionDuration: BlockNumber = TECHNICAL_MOTION_DURATION; - pub const TechnicalMaxProposals: u32 = 100; - pub const TechnicalMaxMembers: u32 = 100; - // Tipper Group - pub const TipperMaxMembers: u32 = 21; + pub const TechnicalMaxProposals: u32 = 7; + pub const TechnicalMaxMembers: u32 = 5; // Extras pub const PreimageBaseDeposit: Balance = deposit(2, 64); - pub const MaxProposals: u32 = 100; + pub const MaxProposals: u32 = 10; + pub const MaxVotes: u32 = 100; + pub const MaxBlacklisted: u32 = 100; + pub const MaxDeposits: u32 = 100; //Treasury pub const ProposalBond: Permill = Permill::from_percent(5); - pub const ProposalBondMinimum: Balance = 20 * PLMC; + pub const ProposalBondMinimum: Balance = 50 * PLMC; pub const SpendPeriod: BlockNumber = SPEND_PERIOD; pub const Burn: Permill = Permill::zero(); pub const MaxApprovals: u32 = 100; pub const TreasuryId: PalletId = PalletId(*b"plmc/tsy"); + + // Elections phragmen + pub const CandidacyBond: Balance = 1000 * PLMC; + pub TermDuration: BlockNumber = TERM_DURATION; + pub VotingLockPeriod: BlockNumber = ELECTION_VOTING_LOCK_DURATION; + pub const DesiredMembers: u32 = 9; + pub const DesiredRunnersUp: u32 = 20; + pub const MaxCandidates: u32 = 1000; + pub const MaxVoters: u32 = 10000; + pub const MaxVotesPerVoter: u32 = 8; } diff --git a/runtimes/testnet/Cargo.toml b/runtimes/testnet/Cargo.toml index 007ecab86..9e1284c3e 100644 --- a/runtimes/testnet/Cargo.toml +++ b/runtimes/testnet/Cargo.toml @@ -43,6 +43,7 @@ pallet-authorship.workspace = true pallet-balances.workspace = true pallet-collective.workspace = true pallet-democracy.workspace = true +pallet-elections-phragmen.workspace = true pallet-membership.workspace = true pallet-scheduler.workspace = true pallet-session.workspace = true @@ -100,7 +101,8 @@ orml-oracle.workspace = true [features] default = [ "std" ] -fast-gov = [ "shared-configuration/fast-gov" ] +fast-mode = [ "shared-configuration/fast-mode" ] +instant-mode = [ "shared-configuration/instant-mode" ] std = [ "cumulus-pallet-aura-ext/std", "cumulus-pallet-dmp-queue/std", @@ -127,6 +129,7 @@ std = [ "pallet-balances/std", "pallet-collective/std", "pallet-democracy/std", + "pallet-elections-phragmen/std", "pallet-funding/std", "pallet-insecure-randomness-collective-flip/std", "pallet-linear-release/std", @@ -187,10 +190,12 @@ runtime-benchmarks = [ "pallet-balances/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-democracy/runtime-benchmarks", + "pallet-elections-phragmen/runtime-benchmarks", "pallet-funding/runtime-benchmarks", "pallet-linear-release/runtime-benchmarks", "pallet-membership/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", + "pallet-oracle-ocw/runtime-benchmarks", "pallet-parachain-staking/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", @@ -209,7 +214,6 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", - "pallet-oracle-ocw/runtime-benchmarks" ] try-runtime = [ @@ -231,6 +235,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collective/try-runtime", "pallet-democracy/try-runtime", + "pallet-elections-phragmen/try-runtime", "pallet-funding/try-runtime", "pallet-insecure-randomness-collective-flip/try-runtime", "pallet-linear-release/try-runtime", diff --git a/runtimes/testnet/src/lib.rs b/runtimes/testnet/src/lib.rs index 8ec53fa01..c87103a9d 100644 --- a/runtimes/testnet/src/lib.rs +++ b/runtimes/testnet/src/lib.rs @@ -25,7 +25,8 @@ use cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases; use frame_support::{ construct_runtime, ord_parameter_types, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU32, Currency, EitherOfDiverse, EqualPrivilegeOnly, Everything, WithdrawReasons, + fungible::Credit, tokens, AsEnsureOriginWithArg, ConstU32, Currency, EitherOfDiverse, Everything, PrivilegeCmp, + WithdrawReasons, }, weights::{ConstantMultiplier, Weight}, }; @@ -38,7 +39,7 @@ pub use parachains_common::{ use parity_scale_codec::Encode; // Polkadot imports -use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate}; +use polkadot_runtime_common::{BlockHashCount, CurrencyToVote, SlowAdjustingFeeUpdate}; use sp_api::impl_runtime_apis; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; #[cfg(any(feature = "std", test))] @@ -54,7 +55,7 @@ use sp_runtime::{ }; use pallet_oracle_ocw::types::AssetName; -use sp_std::prelude::*; +use sp_std::{cmp::Ordering, prelude::*}; #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; @@ -86,6 +87,7 @@ use pallet_funding::traits::SetPrices; pub type NegativeImbalanceOf = as Currency<::AccountId>>::NegativeImbalance; +pub type CreditOf = Credit<::AccountId, pallet_balances::Pallet>; /// The address format for describing accounts. pub type Address = MultiAddress; @@ -255,7 +257,7 @@ impl pallet_balances::Config for Runtime { type Balance = Balance; type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; - type FreezeIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; type MaxFreezes = MaxReserves; type MaxHolds = MaxLocks; type MaxLocks = MaxLocks; @@ -345,9 +347,20 @@ impl pallet_aura::Config for Runtime { impl pallet_insecure_randomness_collective_flip::Config for Runtime {} +pub struct ToTreasury; + +impl tokens::imbalance::OnUnbalanced> for ToTreasury { + fn on_nonzero_unbalanced(amount: CreditOf) { + let treasury_account = Treasury::account_id(); + let _ = >::resolve(&treasury_account, amount); + } +} + impl pallet_treasury::Config for Runtime { - // TODO: Use the Council instead of Root! - type ApproveOrigin = EnsureRoot; + type ApproveOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionAtLeast, + >; type Burn = Burn; type BurnDestination = (); type Currency = Balances; @@ -362,13 +375,12 @@ impl pallet_treasury::Config for Runtime { type SpendFunds = (); type SpendOrigin = frame_support::traits::NeverEnsureOrigin; type SpendPeriod = SpendPeriod; - type WeightInfo = (); + type WeightInfo = pallet_treasury::weights::SubstrateWeight; } -// TODO: VERY BASIC implementation, more work needed type CouncilCollective = pallet_collective::Instance1; impl pallet_collective::Config for Runtime { - type DefaultVote = pallet_collective::PrimeDefaultVote; + type DefaultVote = pallet_collective::MoreThanMajorityThenPrimeDefaultVote; type MaxMembers = CouncilMaxMembers; type MaxProposalWeight = MaxCollectivesProposalWeight; type MaxProposals = CouncilMaxProposals; @@ -382,7 +394,7 @@ impl pallet_collective::Config for Runtime { type TechnicalCollective = pallet_collective::Instance2; impl pallet_collective::Config for Runtime { - type DefaultVote = pallet_collective::PrimeDefaultVote; + type DefaultVote = pallet_collective::MoreThanMajorityThenPrimeDefaultVote; type MaxMembers = TechnicalMaxMembers; type MaxProposalWeight = MaxCollectivesProposalWeight; type MaxProposals = TechnicalMaxProposals; @@ -394,6 +406,33 @@ impl pallet_collective::Config for Runtime { type WeightInfo = pallet_collective::weights::SubstrateWeight; } +impl pallet_elections_phragmen::Config for Runtime { + type Balance = Balance; + /// How much should be locked up in order to submit one's candidacy. + type CandidacyBond = CandidacyBond; + type ChangeMembers = Council; + type Currency = Balances; + type CurrencyToVote = CurrencyToVote; + /// Number of members to elect. + type DesiredMembers = DesiredMembers; + /// Number of runners_up to keep. + type DesiredRunnersUp = DesiredRunnersUp; + type InitializeMembers = Council; + type LoserCandidate = ToTreasury; + type MaxCandidates = MaxCandidates; + type MaxVoters = MaxVoters; + type MaxVotesPerVoter = MaxVotesPerVoter; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + /// How long each seat is kept. This defines the next block number at which + /// an election round will happen. If set to zero, no elections are ever + /// triggered and the module will be in passive mode. + type TermDuration = TermDuration; + type VotingLockPeriod = VotingPeriod; + type WeightInfo = pallet_elections_phragmen::weights::SubstrateWeight; +} + impl pallet_democracy::Config for Runtime { type BlacklistOrigin = EnsureRoot; // To cancel a proposal before it has been passed, the technical committee must be unanimous or @@ -405,33 +444,35 @@ impl pallet_democracy::Config for Runtime { // To cancel a proposal which has been passed, 2/3 of the council must agree to it. type CancellationOrigin = pallet_collective::EnsureProportionAtLeast; type CooloffPeriod = CooloffPeriod; - type Currency = Balances; type EnactmentPeriod = EnactmentPeriod; /// A unanimous council can have the next scheduled referendum be a straight default-carries /// (NTB) vote. type ExternalDefaultOrigin = pallet_collective::EnsureProportionAtLeast; /// A super-majority can have the next scheduled referendum be a straight majority-carries vote. - type ExternalMajorityOrigin = pallet_collective::EnsureProportionAtLeast; + type ExternalMajorityOrigin = pallet_collective::EnsureProportionAtLeast; /// A straight majority of the council can decide what their next motion is. type ExternalOrigin = pallet_collective::EnsureProportionAtLeast; /// Two thirds of the technical committee can have an ExternalMajority/ExternalDefault vote /// be tabled immediately and with a shorter voting/enactment period. - type FastTrackOrigin = pallet_collective::EnsureProportionAtLeast; + type FastTrackOrigin = pallet_collective::EnsureProportionAtLeast; type FastTrackVotingPeriod = FastTrackVotingPeriod; + type Fungible = Balances; type InstantAllowed = frame_support::traits::ConstBool; type InstantOrigin = pallet_collective::EnsureProportionAtLeast; type LaunchPeriod = LaunchPeriod; - type MaxBlacklisted = (); - type MaxDeposits = (); + type MaxBlacklisted = MaxBlacklisted; + type MaxDeposits = MaxDeposits; type MaxProposals = MaxProposals; - type MaxVotes = ConstU32<128>; + type MaxVotes = MaxVotes; // Same as EnactmentPeriod type MinimumDeposit = MinimumDeposit; type PalletsOrigin = OriginCaller; type Preimages = Preimage; type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; type Scheduler = Scheduler; - type Slash = (); + type Slash = ToTreasury; type SubmitOrigin = EnsureSigned; // Any single technical committee member may veto a coming council proposal, however they can // only do it once and it lasts only for the cool-off period. @@ -441,10 +482,25 @@ impl pallet_democracy::Config for Runtime { type WeightInfo = pallet_democracy::weights::SubstrateWeight; } +pub struct EqualOrGreatestRootCmp; + +impl PrivilegeCmp for EqualOrGreatestRootCmp { + fn cmp_privilege(left: &OriginCaller, right: &OriginCaller) -> Option { + if left == right { + return Some(Ordering::Equal) + } + match (left, right) { + // Root is greater than anything. + (OriginCaller::system(frame_system::RawOrigin::Root), _) => Some(Ordering::Greater), + _ => None, + } + } +} + impl pallet_scheduler::Config for Runtime { type MaxScheduledPerBlock = MaxScheduledPerBlock; type MaximumWeight = MaximumSchedulerWeight; - type OriginPrivilegeCmp = EqualPrivilegeOnly; + type OriginPrivilegeCmp = EqualOrGreatestRootCmp; type PalletsOrigin = OriginCaller; type Preimages = Preimage; type RuntimeCall = RuntimeCall; @@ -454,6 +510,16 @@ impl pallet_scheduler::Config for Runtime { type WeightInfo = (); } +impl pallet_preimage::Config for Runtime { + // TODO: Check this base deposit value. + type BaseDeposit = PreimageBaseDeposit; + type ByteDeposit = (); + type Currency = Balances; + type ManagerOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + impl pallet_utility::Config for Runtime { type PalletsOrigin = OriginCaller; type RuntimeCall = RuntimeCall; @@ -471,15 +537,6 @@ impl pallet_multisig::Config for Runtime { type WeightInfo = (); } -impl pallet_preimage::Config for Runtime { - type BaseDeposit = PreimageBaseDeposit; - type ByteDeposit = (); - type Currency = Balances; - type ManagerOrigin = EnsureRoot; - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); -} - pub type LocalAssetsInstance = pallet_assets::Instance1; pub type StatemintAssetsInstance = pallet_assets::Instance2; @@ -818,10 +875,12 @@ construct_runtime!( // Governance Treasury: pallet_treasury = 40, - Democracy: pallet_democracy = 41, + Democracy: pallet_democracy::{Pallet, Call, Storage, Event, Config, HoldReason, FreezeReason} = 41, Council: pallet_collective:: = 42, TechnicalCommittee: pallet_collective:: = 43, - Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 44, + Elections: pallet_elections_phragmen::{Pallet, Call, Storage, Event, Config, HoldReason, FreezeReason} = 44, + Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 45, + Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 46, // Polimec Core PolimecFunding: pallet_funding::{Pallet, Call, Storage, Event, Config, HoldReason} = 52, @@ -829,8 +888,7 @@ construct_runtime!( Vesting: pallet_vesting::{Pallet, Call, Storage, Event, Config} = 54, // Utilities - Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event} = 61, - Random: pallet_insecure_randomness_collective_flip = 62, + Random: pallet_insecure_randomness_collective_flip = 60,