diff --git a/.maintain/frame-weight-template.hbs b/.maintain/frame-weight-template.hbs index 5e837b2471..f7acff006a 100644 --- a/.maintain/frame-weight-template.hbs +++ b/.maintain/frame-weight-template.hbs @@ -17,7 +17,7 @@ #![allow(unused_imports)] #![allow(missing_docs)] -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use frame_support::{traits::Get, weights::{Weight, constants::ParityDbWeight}}; use core::marker::PhantomData; /// Weight functions needed for `{{pallet}}`. @@ -102,16 +102,16 @@ impl WeightInfo for () { .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) {{/each}} {{#if (ne benchmark.base_reads "0")}} - .saturating_add(RocksDbWeight::get().reads({{benchmark.base_reads}}_u64)) + .saturating_add(ParityDbWeight::get().reads({{benchmark.base_reads}}_u64)) {{/if}} {{#each benchmark.component_reads as |cr|}} - .saturating_add(RocksDbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) + .saturating_add(ParityDbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) {{/each}} {{#if (ne benchmark.base_writes "0")}} - .saturating_add(RocksDbWeight::get().writes({{benchmark.base_writes}}_u64)) + .saturating_add(ParityDbWeight::get().writes({{benchmark.base_writes}}_u64)) {{/if}} {{#each benchmark.component_writes as |cw|}} - .saturating_add(RocksDbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + .saturating_add(ParityDbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) {{/each}} {{#each benchmark.component_calculated_proof_size as |cp|}} .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) diff --git a/Cargo.lock b/Cargo.lock index 3e93e6db44..e7efe3dd86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8334,6 +8334,7 @@ dependencies = [ "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", "pallet-fast-unstake", + "pallet-governance", "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-insecure-randomness-collective-flip", @@ -9838,6 +9839,27 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-governance" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-preimage", + "pallet-scheduler", + "parity-scale-codec", + "polkadot-sdk-frame", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "subtensor-macros", +] + [[package]] name = "pallet-grandpa" version = "41.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4a51bda878..c5738dc5bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ pallet-subtensor = { path = "pallets/subtensor", default-features = false } pallet-subtensor-swap = { path = "pallets/swap", default-features = false } pallet-subtensor-swap-runtime-api = { path = "pallets/swap/runtime-api", default-features = false } pallet-subtensor-swap-rpc = { path = "pallets/swap/rpc", default-features = false } +pallet-governance = { path = "pallets/governance", default-features = false } procedural-fork = { path = "support/procedural-fork", default-features = false } safe-math = { path = "primitives/safe-math", default-features = false } share-pool = { path = "primitives/share-pool", default-features = false } diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 0117dff889..81c67bdee0 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -379,7 +379,6 @@ parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; pub const MaxScheduledPerBlock: u32 = 50; - pub const NoPreimagePostponement: Option = Some(10); } impl pallet_scheduler::Config for Test { diff --git a/pallets/governance/Cargo.toml b/pallets/governance/Cargo.toml new file mode 100644 index 0000000000..82808c180e --- /dev/null +++ b/pallets/governance/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "pallet-governance" +version = "1.0.0" +authors = ["Bittensor Nucleus Team"] +edition.workspace = true +license = "Apache-2.0" +homepage = "https://bittensor.com" +description = "BitTensor governance pallet" +readme = "README.md" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, features = ["max-encoded-len"] } +scale-info = { workspace = true, features = ["derive"] } +frame.workspace = true +subtensor-macros.workspace = true +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true +sp-core.workspace = true +log.workspace = true + +[dev-dependencies] +pallet-balances = { workspace = true, default-features = true } +pallet-preimage = { workspace = true, default-features = true } +pallet-scheduler = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "log/std", + "sp-core/std", + "pallet-balances/std", + "pallet-preimage/std", + "pallet-scheduler/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", + "pallet-scheduler/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", + "pallet-balances/try-runtime", + "pallet-preimage/try-runtime", + "pallet-scheduler/try-runtime", +] diff --git a/pallets/governance/README.md b/pallets/governance/README.md new file mode 100644 index 0000000000..b13ebd100d --- /dev/null +++ b/pallets/governance/README.md @@ -0,0 +1,117 @@ +# On-Chain Governance System + +## Abstract + +This proposes a comprehensive on-chain governance system to replace the current broken governance implementation that relies on a sudo-based triumvirate multisig. The new system introduces a separation of powers model with three key components: (1) multiple proposer accounts (mostly controlled by OTF) to submit proposals (call executed with root privilege), (2) a three-member Triumvirate that votes on proposals, and (3) two collective bodies (Economic Power and Building Power) that can delay, cancel, or fast-track proposals and vote to replace Triumvirate members. The system will be deployed in two phases: first coexisting with the current sudo implementation for validation, then fully replacing it. + +## Motivation + +The current governance system in Subtensor is broken and relies entirely on a triumvirate multisig with sudo privileges. The runtime contains dead code related to the original triumvirate collective and senate that no longer functions properly. This centralized approach creates several critical issues: + +1. **Single Point of Failure**: The sudo key represents a concentration of power with no on-chain checks or balances (i.e., no blockchain-enforced voting, approval, or oversight mechanisms). +2. **Lack of Transparency**: The governance decision-making process (who voted, when, on what proposal) happens off-chain and is not recorded or auditable on-chain. While the multisig signature itself provides cryptographic proof that the threshold was met, the governance process leading to that decision is opaque. +3. **No Stakeholder Representation**: Major stakeholders (validators and subnet owners) have no formal mechanism to influence protocol upgrades. +4. **Technical Debt**: Dead governance code in the runtime creates maintenance burden and confusion. + +This proposal addresses these issues by implementing a proper separation of powers that balances efficiency with stakeholder representation, while maintaining upgrade capability and security. + +## Specification + +### Overview + +The governance system consists of three main actors working together: + +1. **Allowed Proposers**: Accounts authorized to submit proposals (mostly controlled by OTF) +2. **Triumvirate**: Approval body of 3 members that vote on proposals +3. **Economic and Building Collectives**: Oversight bodies representing major stakeholders: top 16 validators by total stake and top 16 subnet owners by moving average price respectively + +### Actors and Roles + +#### Allowed Proposers (mostly OTF-controlled) + +- **Purpose**: Authorized to submit proposals (calls executed with root privilege) +- **Assignment**: Allowed proposer account keys are configured in the runtime via governance +- **Permissions**: + - Can submit proposals to the main governance track (i.e., runtime upgrade proposals or any root extrinsic) + - Can cancel or withdraw their own proposals anytime before execution (i.e., if they find a bug in the proposal code) + - Can eject its own key from the allowed proposers list (i.e., if it is lost or compromised) + - Can propose an update to the allowed proposers list via proposal flow + +#### Triumvirate + +- **Composition**: 3 distinct accounts (must always maintain 3 members) +- **Role**: Vote on proposals submitted by allowed proposers +- **Voting Threshold**: 2-of-3 approval required for proposals to pass +- **Term**: Indefinite, subject to replacement by collective vote every 6 months (configurable) +- **Accountability**: Each member can be replaced through collective vote process (see Replacement Mechanism) +- **Permissions**: + - Can vote on proposals submitted by allowed proposers + +#### Economic and Building Collectives + +- **Economic Collective**: Top 16 validators by total stake (including delegated stake) (configurable) +- **Building Collective**: Top 16 subnet owners by moving average price (with minimum age of 6 months) (configurable) +- **Total Collective Size**: 32 members (16 Economic + 16 Building) +- **Recalculation**: Membership refreshed every 6 months (configurable) +- **Permissions**: + - Can vote aye/nay on proposals submitted by allowed proposers and approved by Triumvirate + - Votes are aggregated across both collectives (total of 32 possible votes) + - More than configured threshold of aye votes (based on total collective size of 32) fast tracks the proposal (next block execution) (threshold configurable) + - More than configured threshold of nay votes (based on total collective size of 32) cancels the proposal (threshold configurable) + - Delay is calculated using net score (nays - ayes) and applies exponential delay until cancellation (see Delay Period section) + +### Governance Process Flow + +#### Proposal Submission + +1. An allowed proposer account submits a proposal containing runtime upgrade or any root extrinsic +2. Proposal enters "Triumvirate Voting" phase +3. Voting period: 7 days (configurable), after this period, the proposal is automatically rejected if not approved by the Triumvirate. + +- There is a queue limit in the number of proposals that can be submitted at the same time (configurable) +- Proposal can be cancelled by the proposer before the final execution for security reasons (e.g., if they find a bug in the proposal code). +- An allowed proposer can eject its own key from the allowed proposers, removing all its submitted proposals waiting for triumvirate approval from the queue. + +#### Triumvirate Approval + +1. Triumvirate members cast votes (aye/nay) on the proposal + +- 2/3 vote aye, proposal is approved: Proposal is scheduled for execution in 1 hour (configurable) and enters "Delay Period" +- 2/3 vote nay, proposal is rejected: Proposal is cleaned up from storage (it was never scheduled for execution). + +- Triumvirate members can change their vote during the voting period (before the proposal is scheduled or cancelled). +- There is a queue limit in the number of scheduled proposals and in the delay period (configurable). +- If a triumvirate member is replaced, all his votes are removed from the active proposals. + +#### Delay Period (Collective Oversight) + +When a proposal has been approved by the Triumvirate, it is scheduled in 1 hour (configurable) and enters the "Delay Period" where the Economic and Building Collectives can vote to delay, cancel or fast-track the proposal. + +1. Both collectives can vote aye/nay on the proposal, with votes aggregated across all 32 collective members +2. Delay is calculated using **net score** (nays - ayes) and applies an exponential function based on a configurable delay factor. + +- Initial delay is 1 hour (configurable). +- Net score = (number of nays) - (number of ayes) +- If net score > 0: additional delay = initial_delay × (delay_factor ^ net_score) +- If net score ≤ 0: no additional delay (proposal can be fast-tracked if net score becomes negative) +- **Example with delay_factor = 2**: + - Net score of 1 (e.g., 1 nay, 0 ayes): delay = 1 hour × 2^1 = 2 hours + - Net score of 2 (e.g., 2 nays, 0 ayes): delay = 1 hour × 2^2 = 4 hours + - Net score of 3 (e.g., 3 nays, 0 ayes): delay = 1 hour × 2^3 = 8 hours + - Net score of 4 (e.g., 4 nays, 0 ayes): delay = 1 hour × 2^4 = 16 hours + - Net score of 5 (e.g., 5 nays, 0 ayes): delay = 1 hour × 2^5 = 32 hours + - Net score of 16 (e.g., 16 nays, 0 ayes): delay = 1 hour × 2^16 = 65,536 hours + - Net score of 17 (e.g., 17 nays, 0 ayes): proposal is cancelled (threshold configurable, typically ≥ 17 nays out of 32 total members) + +3. If the delay period expires without cancellation: Proposal executes automatically + +- The delay is calculated based on the **net score** across both collectives (total of 32 members), not per collective +- More than configured threshold of aye votes (based on total collective size of 32) fast tracks the proposal (next block execution) (threshold configurable) +- More than configured threshold of nay votes (based on total collective size of 32) cancels the proposal (threshold configurable, typically ≥ 17 nays) +- Collective members can change their vote during the delay period. If changing a nay vote to aye (or vice versa) changes the net score such that the delay is reduced below the time already elapsed, the proposal executes immediately. + - **Example**: A proposal has net score of 3 (3 nays, 0 ayes), creating an 8 hour delay. After 5 hours have elapsed, a collective member changes their nay vote to aye, reducing the net score to 2 (2 nays, 1 aye) and the delay to 4 hours. Since 5 hours have already passed (more than the new 4 hours delay), the proposal executes immediately. + +#### Execution + +- Proposals executed automatically after the delay period if not cancelled or when fast-tracked by the collectives. +- If executing fails, the proposal is not retried and is cleaned up from storage. \ No newline at end of file diff --git a/pallets/governance/src/benchmarking.rs b/pallets/governance/src/benchmarking.rs new file mode 100644 index 0000000000..8890410b2c --- /dev/null +++ b/pallets/governance/src/benchmarking.rs @@ -0,0 +1,234 @@ +//! Benchmarks for Governance Pallet +#![cfg(feature = "runtime-benchmarks")] +#![allow( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used +)] +use crate::pallet::*; +use crate::{ProposalIndex, TriumvirateVotes}; +use codec::Encode; +use frame_benchmarking::{account, v2::*}; +use frame_support::{ + assert_ok, + traits::{QueryPreimage, StorePreimage}, +}; +use frame_system::RawOrigin; +use sp_runtime::{ + BoundedVec, Vec, + traits::{Get, Hash}, +}; +use sp_std::vec; + +extern crate alloc; + +const SEED: u32 = 0; + +use alloc::boxed::Box; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn set_allowed_proposers(p: Linear<1, { T::MaxProposals::get() }>) { + let max_proposers = T::MaxAllowedProposers::get(); + + for i in 0..max_proposers { + allowed_proposer::(i); + } + + for i in 0..p { + let proposer = AllowedProposers::::get()[(i % max_proposers) as usize].clone(); + create_dummy_proposal::(proposer, Some(i), vec![], vec![]); + } + + // Generate some allowed proposers all different from the old ones to force worst case clean up. + let mut new_allowed_proposers = (0..max_proposers) + .map(|i| account("allowed_proposer", 1000 + i, SEED)) + .collect::>(); + + #[extrinsic_call] + _( + RawOrigin::Root, + BoundedVec::truncate_from(new_allowed_proposers.clone()), + ); + + new_allowed_proposers.sort(); + assert_eq!(AllowedProposers::::get().to_vec(), new_allowed_proposers); + assert_eq!(Proposals::::get().len(), 0); + assert_eq!(ProposalOf::::iter().count(), 0); + assert_eq!(TriumvirateVoting::::iter().count(), 0); + } + + #[benchmark] + fn set_triumvirate(p: Linear<1, { T::MaxProposals::get() }>) { + let proposer = allowed_proposer::(0); + let triumvirate = triumvirate::(); + + // Set up some proposals with triumvirate votes + let proposals = (0..p) + .map(|i| { + let ayes = vec![triumvirate[0].clone()]; + let nays = vec![triumvirate[2].clone()]; + create_dummy_proposal::(proposer.clone(), Some(i), ayes, nays) + }) + .collect::>(); + + // Setup some triumvirate totally different from the old one to force worst case clean up. + let mut new_triumvirate = vec![ + account("triumvirate", 1000, SEED), + account("triumvirate", 1001, SEED), + account("triumvirate", 1002, SEED), + ]; + + #[extrinsic_call] + _( + RawOrigin::Root, + BoundedVec::truncate_from(new_triumvirate.clone()), + ); + + new_triumvirate.sort(); + assert_eq!(Triumvirate::::get().to_vec(), new_triumvirate); + for (hash, _) in proposals { + let voting = TriumvirateVoting::::get(hash).unwrap(); + assert!(voting.ayes.to_vec().is_empty()); + assert!(voting.nays.to_vec().is_empty()); + } + } + + #[benchmark] + fn propose() { + let proposer = allowed_proposer::(0); + + // Create a large enough proposal to avoid inlining + let key_value = (b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec()); + let proposal: Box<::RuntimeCall> = Box::new( + frame_system::Call::::set_storage { + items: sp_std::iter::repeat_n(key_value, 50).collect::>(), + } + .into(), + ); + let proposal_hash = T::Hashing::hash_of(&proposal); + let length_bound = proposal.encoded_size() as u32; + + #[extrinsic_call] + _( + RawOrigin::Signed(proposer.clone()), + proposal.clone(), + length_bound, + ); + + assert_eq!( + Proposals::::get().to_vec(), + vec![(proposer.clone(), proposal_hash)] + ); + assert!(ProposalOf::::contains_key(proposal_hash)); + let stored_proposals = ProposalOf::::iter().collect::>(); + assert_eq!(stored_proposals.len(), 1); + let (_stored_hash, bounded_proposal) = &stored_proposals[0]; + assert!(::Preimages::have(bounded_proposal)); + } + + #[benchmark] + fn vote_on_proposed() { + let proposer = allowed_proposer::(0); + let triumvirate = triumvirate::(); + + // Set up some proposal with two votes, fast tracking is the worst case. + let ayes = vec![triumvirate[0].clone()]; + let nays = vec![triumvirate[1].clone()]; + let (hash, index) = create_dummy_proposal::(proposer, Some(0), ayes, nays); + + #[extrinsic_call] + _(RawOrigin::Signed(triumvirate[2].clone()), hash, index, true); + + assert!(Proposals::::get().is_empty()); + assert_eq!(ProposalOf::::iter().count(), 0); + assert_eq!(TriumvirateVoting::::iter().count(), 0); + assert_eq!(Scheduled::::get().to_vec(), vec![hash]); + } + + #[benchmark] + fn vote_on_scheduled() { + let proposer = allowed_proposer::(0); + let triumvirate = triumvirate::(); + + let member: T::AccountId = account("collective_member", 4242, SEED); + EconomicCollective::::try_append(member.clone()).unwrap(); + + // Set up some scheduled proposal + let ayes = vec![triumvirate[0].clone()]; + let nays = vec![triumvirate[1].clone()]; + let (hash, index) = create_dummy_proposal::(proposer, Some(0), ayes, nays); + assert_ok!(Pallet::::vote_on_proposed( + RawOrigin::Signed(triumvirate[2].clone()).into(), + hash, + index, + true, + )); + let delay = CollectiveVoting::::get(hash).unwrap().delay; + + #[extrinsic_call] + _(RawOrigin::Signed(member.clone()), hash, index, false); + + assert_eq!(CollectiveVoting::::iter().count(), 1); + let voting = CollectiveVoting::::get(hash).unwrap(); + assert!(voting.ayes.to_vec().is_empty()); + assert_eq!(voting.nays.to_vec(), vec![member]); + assert!(voting.delay > delay); + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} + +fn allowed_proposer(index: u32) -> T::AccountId { + let proposer: T::AccountId = account("allowed_proposer", index, SEED); + AllowedProposers::::try_append(proposer.clone()).unwrap(); + proposer +} + +fn triumvirate() -> Vec { + let triumvirate = vec![ + account("triumvirate", 0, SEED), + account("triumvirate", 1, SEED), + account("triumvirate", 2, SEED), + ]; + Triumvirate::::put(BoundedVec::truncate_from(triumvirate.clone())); + triumvirate +} + +fn dummy_proposal(n: u32) -> Box<::RuntimeCall> { + Box::new( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), n.to_be_bytes().to_vec())], + } + .into(), + ) +} + +fn create_dummy_proposal( + proposer: T::AccountId, + index: Option, + ayes: Vec, + nays: Vec, +) -> (T::Hash, ProposalIndex) { + let proposal_index = index.unwrap_or(0); + let proposal = dummy_proposal::(proposal_index); + let proposal_hash = T::Hashing::hash_of(&proposal); + let bounded_proposal = T::Preimages::bound(*proposal).unwrap(); + + Proposals::::try_append((proposer.clone(), proposal_hash)).unwrap(); + ProposalOf::::insert(proposal_hash, bounded_proposal); + TriumvirateVoting::::insert( + proposal_hash, + TriumvirateVotes { + index: proposal_index, + ayes: BoundedVec::truncate_from(ayes), + nays: BoundedVec::truncate_from(nays), + end: frame_system::Pallet::::block_number() + T::MotionDuration::get(), + }, + ); + + (proposal_hash, proposal_index) +} diff --git a/pallets/governance/src/lib.rs b/pallets/governance/src/lib.rs new file mode 100644 index 0000000000..b9d6b59a40 --- /dev/null +++ b/pallets/governance/src/lib.rs @@ -0,0 +1,1101 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use frame::arithmetic::CheckedRem; +use frame_support::{ + dispatch::{GetDispatchInfo, RawOrigin}, + pallet_prelude::*, + sp_runtime::traits::Dispatchable, + traits::{ + Bounded, ChangeMembers, IsSubType, QueryPreimage, StorePreimage, fungible, + schedule::{ + DispatchTime, Priority, + v3::{Named as ScheduleNamed, TaskName}, + }, + }, +}; +use frame_system::pallet_prelude::*; +pub use pallet::*; +use sp_runtime::{ + FixedU128, Percent, Saturating, + traits::{Hash, SaturatedConversion, UniqueSaturatedInto}, +}; +use sp_std::{boxed::Box, collections::btree_set::BTreeSet, vec::Vec}; +use subtensor_macros::freeze_struct; +use weights::WeightInfo; + +mod benchmarking; +mod mock; +mod tests; +pub mod weights; + +/// WARNING: Any changes to these 3 constants require a migration to update the `BoundedVec` in storage +/// for `Triumvirate`, `EconomicCollective`, or `BuildingCollective`. +pub const TRIUMVIRATE_SIZE: u32 = 3; +pub const ECONOMIC_COLLECTIVE_SIZE: u32 = 16; +pub const BUILDING_COLLECTIVE_SIZE: u32 = 16; + +pub const TOTAL_COLLECTIVES_SIZE: u32 = ECONOMIC_COLLECTIVE_SIZE + BUILDING_COLLECTIVE_SIZE; + +pub type CurrencyOf = ::Currency; + +pub type BalanceOf = + as fungible::Inspect<::AccountId>>::Balance; + +pub type LocalCallOf = ::RuntimeCall; + +pub type BoundedCallOf = Bounded, ::Hashing>; + +pub type PalletsOriginOf = + <::RuntimeOrigin as OriginTrait>::PalletsOrigin; + +pub type ScheduleAddressOf = + , LocalCallOf, PalletsOriginOf>>::Address; + +/// Simple index type for proposal counting. +pub type ProposalIndex = u32; + +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[freeze_struct("7b322ade3ccaaba")] +pub struct TriumvirateVotes { + /// The proposal's unique index. + index: ProposalIndex, + /// The set of triumvirate members that approved it. + ayes: BoundedVec>, + /// The set of triumvirate members that rejected it. + nays: BoundedVec>, + /// The hard end time of this vote. + end: BlockNumber, +} + +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)] +#[freeze_struct("68b000ed325d45c4")] +pub struct CollectiveVotes { + /// The proposal's unique index. + index: ProposalIndex, + /// The set of collective members that approved it. + ayes: BoundedVec>, + /// The set of collective members that rejected it. + nays: BoundedVec>, + /// The initial dispatch time of the proposal. + initial_dispatch_time: BlockNumber, + /// The additional delay applied to the proposal on top of the initial delay. + delay: BlockNumber, +} + +/// The type of collective. +#[derive( + PartialEq, + Eq, + Clone, + Encode, + Decode, + RuntimeDebug, + TypeInfo, + MaxEncodedLen, + Copy, + DecodeWithMemTracking, +)] +pub enum CollectiveType { + Economic, + Building, +} + +pub trait CollectiveMembersProvider { + fn get_economic_collective() -> ( + BoundedVec>, + Weight, + ); + fn get_building_collective() -> ( + BoundedVec>, + Weight, + ); +} + +#[frame_support::pallet] +#[allow(clippy::expect_used)] +pub mod pallet { + use super::*; + + const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From> + + IsSubType> + + IsType<::RuntimeCall>; + + /// The weight info. + type WeightInfo: WeightInfo; + + /// The currency mechanism. + type Currency: fungible::Mutate; + + /// The preimage provider which will be used to store the call to dispatch. + type Preimages: QueryPreimage + StorePreimage; + + /// The scheduler which will be used to schedule the proposal for execution. + type Scheduler: ScheduleNamed< + BlockNumberFor, + LocalCallOf, + PalletsOriginOf, + Hasher = Self::Hashing, + >; + + /// Origin allowed to set allowed proposers. + type SetAllowedProposersOrigin: EnsureOrigin; + + /// Origin allowed to set triumvirate. + type SetTriumvirateOrigin: EnsureOrigin; + + /// The collective members provider. + type CollectiveMembersProvider: CollectiveMembersProvider; + + /// How many accounts allowed to submit proposals. + #[pallet::constant] + type MaxAllowedProposers: Get; + + /// Maximum weight for a proposal. + #[pallet::constant] + type MaxProposalWeight: Get; + + /// Maximum number of proposals allowed to be active in parallel. + #[pallet::constant] + type MaxProposals: Get; + + /// Maximum number of proposals that can be scheduled for execution in parallel. + #[pallet::constant] + type MaxScheduled: Get; + + /// The duration of a motion. + #[pallet::constant] + type MotionDuration: Get>; + + /// Initial scheduling delay for proposal execution. + #[pallet::constant] + type InitialSchedulingDelay: Get>; + + /// The factor to be used to compute the additional delay for a proposal. + #[pallet::constant] + type AdditionalDelayFactor: Get; + + /// Period of time between collective rotations. + #[pallet::constant] + type CollectiveRotationPeriod: Get>; + + /// Period of time between cleanup of proposals and scheduled proposals. + #[pallet::constant] + type CleanupPeriod: Get>; + + /// Percent threshold for a proposal to be cancelled by a collective vote. + #[pallet::constant] + type CancellationThreshold: Get; + + /// Percent threshold for a proposal to be fast-tracked by a collective vote. + #[pallet::constant] + type FastTrackThreshold: Get; + } + + /// Accounts allowed to submit proposals. + #[pallet::storage] + pub type AllowedProposers = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Active members of the triumvirate. + #[pallet::storage] + pub type Triumvirate = + StorageValue<_, BoundedVec>, ValueQuery>; + + #[pallet::storage] + pub type ProposalCount = StorageValue<_, u32, ValueQuery>; + + /// Tuples of account proposer and hash of the active proposals being voted on. + #[pallet::storage] + pub type Proposals = + StorageValue<_, BoundedVec<(T::AccountId, T::Hash), T::MaxProposals>, ValueQuery>; + + /// Actual proposal for a given hash. + #[pallet::storage] + pub type ProposalOf = + StorageMap<_, Identity, T::Hash, BoundedCallOf, OptionQuery>; + + /// Triumvirate votes for a given proposal, if it is ongoing. + #[pallet::storage] + pub type TriumvirateVoting = StorageMap< + _, + Identity, + T::Hash, + TriumvirateVotes>, + OptionQuery, + >; + + /// The hashes of the proposals that have been scheduled for execution. + #[pallet::storage] + pub type Scheduled = + StorageValue<_, BoundedVec, ValueQuery>; + + /// The economic collective members (top 20 validators by total stake). + #[pallet::storage] + pub type EconomicCollective = + StorageValue<_, BoundedVec>, ValueQuery>; + + /// The building collective members (top 20 subnet owners by moving average price). + #[pallet::storage] + pub type BuildingCollective = + StorageValue<_, BoundedVec>, ValueQuery>; + + /// Collectives votes for a given proposal, if it is scheduled. + #[pallet::storage] + pub type CollectiveVoting = StorageMap< + _, + Identity, + T::Hash, + CollectiveVotes>, + OptionQuery, + >; + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub allowed_proposers: Vec, + pub triumvirate: Vec, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + let allowed_proposers_set = Pallet::::check_for_duplicates(&self.allowed_proposers) + .expect("Allowed proposers cannot contain duplicate accounts."); + assert!( + self.allowed_proposers.len() <= T::MaxAllowedProposers::get() as usize, + "Allowed proposers length cannot exceed MaxAllowedProposers." + ); + + let triumvirate_set = Pallet::::check_for_duplicates(&self.triumvirate) + .expect("Triumvirate cannot contain duplicate accounts."); + assert!( + self.triumvirate.len() <= TRIUMVIRATE_SIZE as usize, + "Triumvirate length cannot exceed {TRIUMVIRATE_SIZE}." + ); + + assert!( + allowed_proposers_set.is_disjoint(&triumvirate_set), + "Allowed proposers and triumvirate must be disjoint." + ); + + Pallet::::initialize_allowed_proposers(&self.allowed_proposers); + Pallet::::initialize_triumvirate(&self.triumvirate); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The allowed proposers have been set. + AllowedProposersSet { + incoming: Vec, + outgoing: Vec, + removed_proposals: Vec<(T::AccountId, T::Hash)>, + }, + /// The triumvirate has been set. + TriumvirateSet { + incoming: Vec, + outgoing: Vec, + }, + /// A proposal has been submitted. + ProposalSubmitted { + account: T::AccountId, + proposal_index: u32, + proposal_hash: T::Hash, + voting_end: BlockNumberFor, + }, + /// A triumvirate member has voted on a proposal. + VotedOnProposal { + account: T::AccountId, + proposal_hash: T::Hash, + voted: bool, + yes: u32, + no: u32, + }, + /// A collective member has voted on a scheduled proposal. + VotedOnScheduled { + account: T::AccountId, + proposal_hash: T::Hash, + voted: bool, + yes: u32, + no: u32, + }, + /// A proposal has been scheduled for execution by triumvirate. + ProposalScheduled { proposal_hash: T::Hash }, + /// A proposal has been cancelled by triumvirate. + ProposalCancelled { proposal_hash: T::Hash }, + /// A scheduled proposal has been fast-tracked by collectives. + ScheduledProposalFastTracked { proposal_hash: T::Hash }, + /// A scheduled proposal has been cancelled by collectives. + ScheduledProposalCancelled { proposal_hash: T::Hash }, + /// A scheduled proposal schedule time has been delayed by collectives. + ScheduledProposalDelayAdjusted { + proposal_hash: T::Hash, + dispatch_time: DispatchTime>, + }, + } + + #[pallet::error] + pub enum Error { + /// Duplicate accounts not allowed. + DuplicateAccounts, + /// There can only be a maximum of `MaxAllowedProposers` allowed proposers. + TooManyAllowedProposers, + /// Triumvirate length cannot exceed 3. + InvalidTriumvirateLength, + /// Allowed proposers and triumvirate must be disjoint. + AllowedProposersAndTriumvirateMustBeDisjoint, + /// Origin is not an allowed proposer. + NotAllowedProposer, + /// The given weight bound for the proposal was too low. + WrongProposalLength, + /// The given weight bound for the proposal was too low. + WrongProposalWeight, + /// Duplicate proposals not allowed. + DuplicateProposal, + /// There can only be a maximum of `MaxProposals` active proposals in parallel. + TooManyProposals, + /// Origin is not a triumvirate member. + NotTriumvirateMember, + /// Proposal must exist. + ProposalMissing, + /// Mismatched index. + WrongProposalIndex, + /// Duplicate vote not allowed. + DuplicateVote, + /// Unreachable code path. + Unreachable, + /// There can only be a maximum of `MaxScheduled` proposals scheduled for execution. + TooManyScheduled, + /// Call is not available in the preimage storage. + CallUnavailable, + /// Proposal hash is not 32 bytes. + InvalidProposalHashLength, + /// Proposal is already scheduled. + AlreadyScheduled, + /// Origin is not a collective member. + NotCollectiveMember, + /// Proposal is not scheduled. + ProposalNotScheduled, + /// Proposal voting period has ended. + VotingPeriodEnded, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let mut weight = Weight::zero(); + + let economic_collective = EconomicCollective::::get(); + let building_collective = BuildingCollective::::get(); + let is_first_run = economic_collective.is_empty() || building_collective.is_empty(); + let should_rotate = now + .checked_rem(&T::CollectiveRotationPeriod::get()) + .unwrap_or(now) + .is_zero(); + let should_cleanup = now + .checked_rem(&T::CleanupPeriod::get()) + .unwrap_or(now) + .is_zero(); + + if is_first_run || should_rotate { + weight.saturating_accrue(Self::rotate_collectives()); + } + + if should_cleanup { + weight.saturating_accrue(Self::cleanup_proposals(now)); + weight.saturating_accrue(Self::cleanup_scheduled()); + } + + weight + } + } + + #[pallet::call] + impl Pallet { + #![deny(clippy::expect_used)] + + /// Set the allowed proposers. + /// + /// Updates the list of accounts that are allowed to submit proposals. The new list must + /// not contain duplicate accounts and must be disjoint from the triumvirate members. + /// Any active proposals from accounts being removed will be cancelled. + /// + /// The dispatch origin for this call must satisfy `SetAllowedProposersOrigin`. + /// + /// Parameters: + /// - `new_allowed_proposers`: The new list of allowed proposers. Must not exceed + /// `MaxAllowedProposers` and must not contain duplicates. + /// + /// Emits `AllowedProposersSet` event with the incoming and outgoing accounts, as well as + /// any removed proposals. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::set_allowed_proposers(T::MaxProposals::get()))] + pub fn set_allowed_proposers( + origin: OriginFor, + mut new_allowed_proposers: BoundedVec, + ) -> DispatchResultWithPostInfo { + T::SetAllowedProposersOrigin::ensure_origin(origin)?; + + let new_allowed_proposers_set = + Pallet::::check_for_duplicates(&new_allowed_proposers) + .ok_or(Error::::DuplicateAccounts)?; + + let triumvirate = Triumvirate::::get(); + let triumvirate_set: BTreeSet<_> = triumvirate.iter().collect(); + ensure!( + triumvirate_set.is_disjoint(&new_allowed_proposers_set), + Error::::AllowedProposersAndTriumvirateMustBeDisjoint + ); + + let mut allowed_proposers = AllowedProposers::::get().to_vec(); + allowed_proposers.sort(); + new_allowed_proposers.sort(); + let (incoming, outgoing) = + <() as ChangeMembers>::compute_members_diff_sorted( + new_allowed_proposers.as_ref(), + &allowed_proposers, + ); + + // Remove proposals from the outgoing allowed proposers. + let mut removed_proposals = Vec::new(); + for (proposer, proposal_hash) in Proposals::::get() { + if outgoing.contains(&proposer) { + Self::clear_proposal(proposal_hash); + removed_proposals.push((proposer, proposal_hash)); + } + } + let removed_proposals_count = removed_proposals.len() as u32; + + AllowedProposers::::put(new_allowed_proposers); + + Self::deposit_event(Event::::AllowedProposersSet { + incoming, + outgoing, + removed_proposals, + }); + + Ok(Some(T::WeightInfo::set_allowed_proposers( + removed_proposals_count, + )) + .into()) + } + + /// Set the triumvirate. + /// + /// Updates the triumvirate members who can vote on proposals. The new triumvirate must + /// contain exactly 3 members, must not contain duplicate accounts, and must be disjoint + /// from the allowed proposers. Votes from outgoing triumvirate members will be removed + /// from active proposals. + /// + /// The dispatch origin for this call must satisfy `SetTriumvirateOrigin`. + /// + /// Parameters: + /// - `new_triumvirate`: The new triumvirate members. Must contain exactly 3 accounts + /// with no duplicates. + /// + /// Emits `TriumvirateSet` event with the incoming and outgoing members. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::set_triumvirate(T::MaxProposals::get()))] + pub fn set_triumvirate( + origin: OriginFor, + mut new_triumvirate: BoundedVec>, + ) -> DispatchResultWithPostInfo { + T::SetTriumvirateOrigin::ensure_origin(origin)?; + + let new_triumvirate_set = Pallet::::check_for_duplicates(&new_triumvirate) + .ok_or(Error::::DuplicateAccounts)?; + ensure!( + new_triumvirate.len() == TRIUMVIRATE_SIZE as usize, + Error::::InvalidTriumvirateLength + ); + + let allowed_proposers = AllowedProposers::::get(); + let allowed_proposers_set: BTreeSet<_> = allowed_proposers.iter().collect(); + ensure!( + allowed_proposers_set.is_disjoint(&new_triumvirate_set), + Error::::AllowedProposersAndTriumvirateMustBeDisjoint + ); + + let mut triumvirate = Triumvirate::::get().to_vec(); + triumvirate.sort(); + new_triumvirate.sort(); + let (incoming, outgoing) = + <() as ChangeMembers>::compute_members_diff_sorted( + new_triumvirate.as_ref(), + &triumvirate, + ); + + // Remove votes from the outgoing triumvirate members. + let mut voting_count = 0; + for (_proposer, proposal_hash) in Proposals::::get() { + TriumvirateVoting::::mutate(proposal_hash, |voting| { + if let Some(voting) = voting.as_mut() { + voting.ayes.retain(|a| !outgoing.contains(a)); + voting.nays.retain(|a| !outgoing.contains(a)); + voting_count.saturating_inc(); + } + }); + } + + Triumvirate::::put(new_triumvirate); + + Self::deposit_event(Event::::TriumvirateSet { incoming, outgoing }); + + Ok(Some(T::WeightInfo::set_triumvirate(voting_count)).into()) + } + + /// Propose a new proposal. + /// + /// Submits a proposal for triumvirate voting. The proposal will be stored and a voting + /// period will begin. The proposal must not already exist and must not be scheduled. + /// + /// The dispatch origin for this call must be _Signed_ and the account must be an allowed + /// proposer. + /// + /// Parameters: + /// - `proposal`: The call to be executed if the proposal passes. Must be boxed to reduce + /// stack size. + /// - `length_bound`: The maximum encoded length of the proposal. The actual encoded length + /// must not exceed this bound. + /// + /// The proposal's weight must not exceed `MaxProposalWeight` and the number of active + /// proposals must not exceed `MaxProposals`. + /// + /// Emits `ProposalSubmitted` event with the proposal details and voting end block. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::propose())] + pub fn propose( + origin: OriginFor, + proposal: Box<::RuntimeCall>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResult { + let who = Self::ensure_allowed_proposer(origin)?; + + let proposal_len = proposal.encoded_size(); + ensure!( + proposal_len <= length_bound as usize, + Error::::WrongProposalLength + ); + let proposal_weight = proposal.get_dispatch_info().call_weight; + ensure!( + proposal_weight.all_lte(T::MaxProposalWeight::get()), + Error::::WrongProposalWeight + ); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!( + !ProposalOf::::contains_key(proposal_hash), + Error::::DuplicateProposal + ); + let scheduled = Scheduled::::get(); + ensure!( + !scheduled.contains(&proposal_hash), + Error::::AlreadyScheduled + ); + + Proposals::::try_append((who.clone(), proposal_hash)) + .map_err(|_| Error::::TooManyProposals)?; + + let proposal_index = ProposalCount::::get(); + ProposalCount::::mutate(|i| i.saturating_inc()); + + let bounded_proposal = T::Preimages::bound(*proposal)?; + ProposalOf::::insert(proposal_hash, bounded_proposal); + + let now = frame_system::Pallet::::block_number(); + let end = now.saturating_add(T::MotionDuration::get()); + TriumvirateVoting::::insert( + proposal_hash, + TriumvirateVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::new(), + end, + }, + ); + + Self::deposit_event(Event::::ProposalSubmitted { + account: who, + proposal_index, + proposal_hash, + voting_end: end, + }); + Ok(()) + } + + /// Vote on a proposal as a triumvirate member. + /// + /// Allows a triumvirate member to vote on an active proposal. If 2 or more members vote + /// yes, the proposal is scheduled for execution. If 2 or more members vote no, the proposal + /// is cancelled. + /// + /// The dispatch origin for this call must be _Signed_ and the account must be a triumvirate + /// member. + /// + /// Parameters: + /// - `proposal_hash`: The hash of the proposal to vote on. + /// - `proposal_index`: The index of the proposal. Must match the stored proposal index. + /// - `approve`: `true` to vote yes, `false` to vote no. + /// + /// The proposal must exist and the voting period must not have ended. Each member can only + /// vote once per proposal. + /// + /// Emits `VotedOnProposal` event. If the vote results in scheduling or cancellation, + /// `ProposalScheduled` or `ProposalCancelled` events are also emitted. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::vote_on_proposed())] + pub fn vote_on_proposed( + origin: OriginFor, + proposal_hash: T::Hash, + #[pallet::compact] proposal_index: ProposalIndex, + approve: bool, + ) -> DispatchResult { + let who = Self::ensure_triumvirate_member(origin)?; + + let proposals = Proposals::::get(); + ensure!( + proposals.iter().any(|(_, h)| h == &proposal_hash), + Error::::ProposalMissing + ); + + let voting = Self::do_vote_on_proposed(&who, proposal_hash, proposal_index, approve)?; + + let yes_votes = voting.ayes.len() as u32; + let no_votes = voting.nays.len() as u32; + + Self::deposit_event(Event::::VotedOnProposal { + account: who, + proposal_hash, + voted: approve, + yes: yes_votes, + no: no_votes, + }); + + if yes_votes >= 2 { + Self::schedule(proposal_hash, proposal_index)?; + } else if no_votes >= 2 { + Self::cancel(proposal_hash)?; + } + + Ok(()) + } + + /// Vote on a proposal as a collective member. + /// + /// Allows a member of the economic or building collective to vote on a scheduled proposal. + /// Based on the vote results, the proposal may be fast-tracked, cancelled, or have its + /// delay adjusted. + /// + /// The dispatch origin for this call must be _Signed_ and the account must be a member of + /// either the economic or building collective. + /// + /// Parameters: + /// - `proposal_hash`: The hash of the scheduled proposal to vote on. + /// - `proposal_index`: The index of the proposal. Must match the stored proposal index. + /// - `approve`: `true` to vote yes, `false` to vote no. + /// + /// The proposal must be scheduled. If the yes votes reach the fast-track threshold, the + /// proposal is executed immediately. If the no votes reach the cancellation threshold, the + /// proposal is cancelled. Otherwise, the delay is adjusted based on the net vote score. + /// + /// Emits `VotedOnScheduled` event. If the vote results in fast-tracking or cancellation, + /// `ScheduledProposalFastTracked` or `ScheduledProposalCancelled` events are also emitted. + /// If the delay is adjusted, `ScheduledProposalDelayAdjusted` event is emitted. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::vote_on_scheduled())] + pub fn vote_on_scheduled( + origin: OriginFor, + proposal_hash: T::Hash, + #[pallet::compact] proposal_index: ProposalIndex, + approve: bool, + ) -> DispatchResult { + let (who, _) = Self::ensure_collective_member(origin)?; + + let scheduled = Scheduled::::get(); + ensure!( + scheduled.contains(&proposal_hash), + Error::::ProposalNotScheduled + ); + + let voting = Self::do_vote_on_scheduled(&who, proposal_hash, proposal_index, approve)?; + + let yes_votes = voting.ayes.len() as u32; + let no_votes = voting.nays.len() as u32; + + Self::deposit_event(Event::::VotedOnScheduled { + account: who, + proposal_hash, + voted: approve, + yes: yes_votes, + no: no_votes, + }); + + let should_fast_track = + yes_votes >= T::FastTrackThreshold::get().mul_ceil(TOTAL_COLLECTIVES_SIZE); + let should_cancel = + no_votes >= T::CancellationThreshold::get().mul_ceil(TOTAL_COLLECTIVES_SIZE); + + if should_fast_track { + Self::fast_track(proposal_hash)?; + } else if should_cancel { + Self::cancel_scheduled(proposal_hash)?; + } else { + Self::adjust_delay(proposal_hash, voting)?; + } + + Ok(()) + } + } +} + +impl Pallet { + fn initialize_allowed_proposers(allowed_proposers: &[T::AccountId]) { + if !allowed_proposers.is_empty() { + assert!( + AllowedProposers::::get().is_empty(), + "Allowed proposers are already initialized!" + ); + let mut allowed_proposers = BoundedVec::truncate_from(allowed_proposers.to_vec()); + allowed_proposers.sort(); + AllowedProposers::::put(allowed_proposers); + } + } + + fn initialize_triumvirate(triumvirate: &[T::AccountId]) { + assert!( + Triumvirate::::get().is_empty(), + "Triumvirate is already initialized!" + ); + let mut triumvirate = BoundedVec::truncate_from(triumvirate.to_vec()); + triumvirate.sort(); + Triumvirate::::put(triumvirate); + } + + fn check_for_duplicates(accounts: &[T::AccountId]) -> Option> { + let accounts_set: BTreeSet<_> = accounts.iter().collect(); + if accounts_set.len() == accounts.len() { + Some(accounts_set) + } else { + None + } + } + + fn do_vote_on_proposed( + who: &T::AccountId, + proposal_hash: T::Hash, + index: ProposalIndex, + approve: bool, + ) -> Result>, DispatchError> { + TriumvirateVoting::::try_mutate(proposal_hash, |voting| { + let voting = voting.as_mut().ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongProposalIndex); + let now = frame_system::Pallet::::block_number(); + ensure!(voting.end > now, Error::::VotingPeriodEnded); + Self::vote_inner(who, approve, &mut voting.ayes, &mut voting.nays)?; + Ok(voting.clone()) + }) + } + + fn do_vote_on_scheduled( + who: &T::AccountId, + proposal_hash: T::Hash, + index: ProposalIndex, + approve: bool, + ) -> Result>, DispatchError> { + CollectiveVoting::::try_mutate(proposal_hash, |voting| { + // No voting here but we have proposal in scheduled, proposal + // has been fast-tracked. + let voting = voting.as_mut().ok_or(Error::::VotingPeriodEnded)?; + ensure!(voting.index == index, Error::::WrongProposalIndex); + Self::vote_inner(who, approve, &mut voting.ayes, &mut voting.nays)?; + Ok(voting.clone()) + }) + } + + fn vote_inner>( + who: &T::AccountId, + approve: bool, + ayes: &mut BoundedVec, + nays: &mut BoundedVec, + ) -> DispatchResult { + let has_yes_vote = ayes.iter().any(|a| a == who); + let has_no_vote = nays.iter().any(|a| a == who); + + if approve { + if !has_yes_vote { + ayes.try_push(who.clone()) + // Unreachable because nobody can double vote. + .map_err(|_| Error::::Unreachable)?; + } else { + return Err(Error::::DuplicateVote.into()); + } + if has_no_vote { + nays.retain(|a| a != who); + } + } else { + if !has_no_vote { + nays.try_push(who.clone()) + // Unreachable because nobody can double vote. + .map_err(|_| Error::::Unreachable)?; + } else { + return Err(Error::::DuplicateVote.into()); + } + if has_yes_vote { + ayes.retain(|a| a != who); + } + } + + Ok(()) + } + + fn schedule(proposal_hash: T::Hash, proposal_index: ProposalIndex) -> DispatchResult { + Scheduled::::try_append(proposal_hash).map_err(|_| Error::::TooManyScheduled)?; + + let bounded = ProposalOf::::get(proposal_hash).ok_or(Error::::ProposalMissing)?; + ensure!(T::Preimages::have(&bounded), Error::::CallUnavailable); + + let now = frame_system::Pallet::::block_number(); + let name = Self::task_name_from_hash(proposal_hash)?; + let dispatch_time = now.saturating_add(T::InitialSchedulingDelay::get()); + T::Scheduler::schedule_named( + name, + DispatchTime::At(dispatch_time), + None, + Priority::default(), + RawOrigin::Root.into(), + bounded, + )?; + Self::clear_proposal(proposal_hash); + + CollectiveVoting::::insert( + proposal_hash, + CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::new(), + initial_dispatch_time: dispatch_time, + delay: Zero::zero(), + }, + ); + + Self::deposit_event(Event::::ProposalScheduled { proposal_hash }); + Ok(()) + } + + fn cancel(proposal_hash: T::Hash) -> DispatchResult { + Self::clear_proposal(proposal_hash); + Self::deposit_event(Event::::ProposalCancelled { proposal_hash }); + Ok(()) + } + + fn fast_track(proposal_hash: T::Hash) -> DispatchResult { + let name = Self::task_name_from_hash(proposal_hash)?; + T::Scheduler::reschedule_named( + name, + // It will be scheduled on the next block because scheduler already ran for this block. + DispatchTime::After(Zero::zero()), + )?; + CollectiveVoting::::remove(proposal_hash); + Self::deposit_event(Event::::ScheduledProposalFastTracked { proposal_hash }); + Ok(()) + } + + fn cancel_scheduled(proposal_hash: T::Hash) -> DispatchResult { + let name = Self::task_name_from_hash(proposal_hash)?; + T::Scheduler::cancel_named(name)?; + Scheduled::::mutate(|scheduled| scheduled.retain(|h| h != &proposal_hash)); + CollectiveVoting::::remove(proposal_hash); + Self::deposit_event(Event::::ScheduledProposalCancelled { proposal_hash }); + Ok(()) + } + + fn adjust_delay( + proposal_hash: T::Hash, + mut voting: CollectiveVotes>, + ) -> DispatchResult { + let net_score = (voting.nays.len() as i32).saturating_sub(voting.ayes.len() as i32); + let additional_delay = Self::compute_additional_delay(net_score); + + // No change, no need to reschedule + if voting.delay == additional_delay { + return Ok(()); + } + + let now = frame_system::Pallet::::block_number(); + let elapsed_time = now.saturating_sub(voting.initial_dispatch_time); + + // We are past new delay, fast track + if elapsed_time > additional_delay { + return Self::fast_track(proposal_hash); + } + + let name = Self::task_name_from_hash(proposal_hash)?; + let dispatch_time = DispatchTime::At( + voting + .initial_dispatch_time + .saturating_add(additional_delay), + ); + T::Scheduler::reschedule_named(name, dispatch_time)?; + + voting.delay = additional_delay; + CollectiveVoting::::insert(proposal_hash, voting); + + Self::deposit_event(Event::::ScheduledProposalDelayAdjusted { + proposal_hash, + dispatch_time, + }); + Ok(()) + } + + fn clear_proposal(proposal_hash: T::Hash) { + Proposals::::mutate(|proposals| { + proposals.retain(|(_, h)| h != &proposal_hash); + }); + ProposalOf::::remove(proposal_hash); + TriumvirateVoting::::remove(proposal_hash); + } + + fn rotate_collectives() -> Weight { + let mut weight = Weight::zero(); + + let (economic_members, economic_weight) = + T::CollectiveMembersProvider::get_economic_collective(); + let (building_members, building_weight) = + T::CollectiveMembersProvider::get_building_collective(); + + EconomicCollective::::put(economic_members); + BuildingCollective::::put(building_members); + weight.saturating_accrue( + T::DbWeight::get() + .writes(2) + .saturating_add(economic_weight) + .saturating_add(building_weight), + ); + + weight + } + + fn cleanup_proposals(now: BlockNumberFor) -> Weight { + let mut weight = Weight::zero(); + + let mut proposals = Proposals::::get(); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + proposals.retain(|(_, proposal_hash)| { + let voting = TriumvirateVoting::::get(proposal_hash); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + match voting { + Some(voting) if voting.end > now => true, + _ => { + ProposalOf::::remove(proposal_hash); + TriumvirateVoting::::remove(proposal_hash); + weight.saturating_accrue(T::DbWeight::get().writes(2)); + false + } + } + }); + + Proposals::::put(proposals); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + weight + } + + fn cleanup_scheduled() -> Weight { + let mut weight = Weight::zero(); + + let mut scheduled = Scheduled::::get(); + weight.saturating_accrue(T::DbWeight::get().reads(1)); + + scheduled.retain( + |proposal_hash| match Self::task_name_from_hash(*proposal_hash) { + Ok(name) => { + let dispatch_time = T::Scheduler::next_dispatch_time(name); + CollectiveVoting::::remove(proposal_hash); + weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1)); + dispatch_time.is_ok() + } + // Unreachable because proposal hash is always 32 bytes. + Err(_) => false, + }, + ); + + Scheduled::::put(scheduled); + weight.saturating_accrue(T::DbWeight::get().writes(1)); + + weight + } + + fn ensure_allowed_proposer(origin: OriginFor) -> Result { + let who = ensure_signed(origin)?; + let allowed_proposers = AllowedProposers::::get(); + ensure!( + allowed_proposers.contains(&who), + Error::::NotAllowedProposer + ); + Ok(who) + } + + fn ensure_triumvirate_member(origin: OriginFor) -> Result { + let who = ensure_signed(origin)?; + let triumvirate = Triumvirate::::get(); + ensure!(triumvirate.contains(&who), Error::::NotTriumvirateMember); + Ok(who) + } + + fn ensure_collective_member( + origin: OriginFor, + ) -> Result<(T::AccountId, CollectiveType), DispatchError> { + let who = ensure_signed(origin)?; + + let economic_collective = EconomicCollective::::get(); + if economic_collective.contains(&who) { + return Ok((who, CollectiveType::Economic)); + } + + let building_collective = BuildingCollective::::get(); + if building_collective.contains(&who) { + return Ok((who, CollectiveType::Building)); + } + + Err(Error::::NotCollectiveMember.into()) + } + + fn task_name_from_hash(proposal_hash: T::Hash) -> Result { + Ok(proposal_hash + .as_ref() + .try_into() + .map_err(|_| Error::::InvalidProposalHashLength)?) + } + + fn compute_additional_delay(net_score: i32) -> BlockNumberFor { + if net_score > 0 { + let initial_delay = + FixedU128::from_inner(T::InitialSchedulingDelay::get().unique_saturated_into()); + let multiplier = + T::AdditionalDelayFactor::get().saturating_pow(net_score.unsigned_abs() as usize); + multiplier + .saturating_mul(initial_delay) + .into_inner() + .saturated_into() + } else { + Zero::zero() + } + } +} diff --git a/pallets/governance/src/mock.rs b/pallets/governance/src/mock.rs new file mode 100644 index 0000000000..73ed51a7f6 --- /dev/null +++ b/pallets/governance/src/mock.rs @@ -0,0 +1,301 @@ +#![cfg(test)] +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::unwrap_used +)] +use frame_support::{derive_impl, pallet_prelude::*, parameter_types, traits::EqualPrivilegeOnly}; +use frame_system::{EnsureRoot, limits, pallet_prelude::*}; +use sp_core::U256; +use sp_runtime::{BuildStorage, FixedU128, Perbill, Percent, traits::IdentityLookup}; +use sp_std::cell::RefCell; +use std::marker::PhantomData; + +use crate::{ + BUILDING_COLLECTIVE_SIZE, BalanceOf, CollectiveMembersProvider, ECONOMIC_COLLECTIVE_SIZE, + pallet as pallet_governance, +}; + +type Block = frame_system::mocking::MockBlock; +pub(crate) type AccountOf = ::AccountId; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system = 1, + Balances: pallet_balances = 2, + Preimage: pallet_preimage = 3, + Scheduler: pallet_scheduler = 4, + Governance: pallet_governance = 5, + TestPallet: pallet_test = 6, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = U256; + type AccountData = pallet_balances::AccountData; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +impl pallet_preimage::Config for Test { + type WeightInfo = pallet_preimage::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type ManagerOrigin = EnsureRoot>; + type Consideration = (); +} + +parameter_types! { + pub BlockWeights: limits::BlockWeights = limits::BlockWeights::with_sensible_defaults( + Weight::from_parts(2_000_000_000_000, u64::MAX), + Perbill::from_percent(75), + ); + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; +} + +impl pallet_scheduler::Config for Test { + type RuntimeOrigin = RuntimeOrigin; + type RuntimeEvent = RuntimeEvent; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureRoot>; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = pallet_scheduler::weights::SubstrateWeight; + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type Preimages = Preimage; + type BlockNumberProvider = System; +} + +pub struct FakeCollectiveMembersProvider(PhantomData); +impl CollectiveMembersProvider for FakeCollectiveMembersProvider +where + T::AccountId: From>, +{ + fn get_economic_collective() -> ( + BoundedVec>, + Weight, + ) { + ( + BoundedVec::truncate_from( + ECONOMIC_COLLECTIVE + .with(|c| c.borrow().iter().map(|a| T::AccountId::from(*a)).collect()), + ), + Weight::zero(), + ) + } + fn get_building_collective() -> ( + BoundedVec>, + Weight, + ) { + ( + BoundedVec::truncate_from( + BUILDING_COLLECTIVE + .with(|c| c.borrow().iter().map(|a| T::AccountId::from(*a)).collect()), + ), + Weight::zero(), + ) + } +} + +thread_local! { + pub static ECONOMIC_COLLECTIVE: RefCell>> = const { RefCell::new(vec![]) }; + pub static BUILDING_COLLECTIVE: RefCell>> = const { RefCell::new(vec![]) }; +} + +#[macro_export] +macro_rules! set_next_economic_collective { + ($members:expr) => {{ + assert_eq!($members.len(), ECONOMIC_COLLECTIVE_SIZE as usize); + ECONOMIC_COLLECTIVE.with_borrow_mut(|c| *c = $members.clone()); + }}; +} + +#[macro_export] +macro_rules! set_next_building_collective { + ($members:expr) => {{ + assert_eq!($members.len(), BUILDING_COLLECTIVE_SIZE as usize); + BUILDING_COLLECTIVE.with_borrow_mut(|c| *c = $members.clone()); + }}; +} + +parameter_types! { + pub const MaxAllowedProposers: u32 = 5; + pub const MaxProposalWeight: Weight = Weight::from_parts(1_000_000_000_000, 0); + pub const MaxProposals: u32 = 5; + pub const MaxScheduled: u32 = 10; + pub const MotionDuration: BlockNumberFor = 20; + pub const InitialSchedulingDelay: BlockNumberFor = 20; + pub const AdditionalDelayFactor: FixedU128 = FixedU128::from_rational(3, 2); // 1.5 + pub const CollectiveRotationPeriod: BlockNumberFor = 100; + pub const CleanupPeriod: BlockNumberFor = 500; + pub const FastTrackThreshold: Percent = Percent::from_percent(67); // ~2/3 + pub const CancellationThreshold: Percent = Percent::from_percent(51); +} + +impl pallet_governance::Config for Test { + type RuntimeCall = RuntimeCall; + type WeightInfo = crate::weights::SubstrateWeight; + type Currency = Balances; + type Preimages = Preimage; + type Scheduler = Scheduler; + type SetAllowedProposersOrigin = EnsureRoot>; + type SetTriumvirateOrigin = EnsureRoot>; + type CollectiveMembersProvider = FakeCollectiveMembersProvider; + type MaxAllowedProposers = MaxAllowedProposers; + type MaxProposalWeight = MaxProposalWeight; + type MaxProposals = MaxProposals; + type MaxScheduled = MaxScheduled; + type MotionDuration = MotionDuration; + type InitialSchedulingDelay = InitialSchedulingDelay; + type AdditionalDelayFactor = AdditionalDelayFactor; + type CollectiveRotationPeriod = CollectiveRotationPeriod; + type CleanupPeriod = CleanupPeriod; + type CancellationThreshold = CancellationThreshold; + type FastTrackThreshold = FastTrackThreshold; +} + +#[frame_support::pallet] +pub(crate) mod pallet_test { + use super::MaxProposalWeight; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized {} + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(MaxProposalWeight::get() * 2)] + pub fn expensive_call(_origin: OriginFor) -> DispatchResult { + Ok(()) + } + } +} + +impl pallet_test::Config for Test {} + +pub(crate) struct TestState { + block_number: BlockNumberFor, + balances: Vec<(AccountOf, BalanceOf)>, + allowed_proposers: Vec>, + triumvirate: Vec>, + economic_collective: BoundedVec, ConstU32>, + building_collective: BoundedVec, ConstU32>, +} + +impl Default for TestState { + fn default() -> Self { + Self { + block_number: 1, + balances: vec![], + allowed_proposers: vec![U256::from(1), U256::from(2), U256::from(3)], + triumvirate: vec![U256::from(1001), U256::from(1002), U256::from(1003)], + economic_collective: BoundedVec::truncate_from( + (1..=ECONOMIC_COLLECTIVE_SIZE) + .map(|i| U256::from(2000 + i)) + .collect::>(), + ), + building_collective: BoundedVec::truncate_from( + (1..=BUILDING_COLLECTIVE_SIZE) + .map(|i| U256::from(3000 + i)) + .collect::>(), + ), + } + } +} + +impl TestState { + pub(crate) fn with_allowed_proposers( + mut self, + allowed_proposers: Vec>, + ) -> Self { + self.allowed_proposers = allowed_proposers; + self + } + + pub(crate) fn with_triumvirate(mut self, triumvirate: Vec>) -> Self { + self.triumvirate = triumvirate; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { + system: frame_system::GenesisConfig::default(), + balances: pallet_balances::GenesisConfig { + balances: self.balances, + ..Default::default() + }, + governance: pallet_governance::GenesisConfig { + allowed_proposers: self.allowed_proposers, + triumvirate: self.triumvirate, + }, + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| { + set_next_economic_collective!(self.economic_collective.to_vec()); + set_next_building_collective!(self.building_collective.to_vec()); + run_to_block(self.block_number); + }); + ext + } + + pub(crate) fn build_and_execute(self, test: impl FnOnce()) { + self.build().execute_with(|| { + test(); + }); + } +} + +pub(crate) fn nth_last_event(n: usize) -> RuntimeEvent { + System::events() + .into_iter() + .rev() + .nth(n) + .expect("RuntimeEvent expected") + .event +} + +pub(crate) fn last_event() -> RuntimeEvent { + nth_last_event(0) +} + +pub(crate) fn run_to_block(n: BlockNumberFor) { + System::run_to_block::(n); +} + +#[allow(unused)] +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Expected to not panic"); + pallet_balances::GenesisConfig:: { + balances: vec![ + (U256::from(1), 10), + (U256::from(2), 10), + (U256::from(3), 10), + (U256::from(4), 10), + (U256::from(5), 3), + ], + dev_accounts: None, + } + .assimilate_storage(&mut t) + .expect("Expected to not panic"); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/governance/src/tests.rs b/pallets/governance/src/tests.rs new file mode 100644 index 0000000000..fdf6c1e439 --- /dev/null +++ b/pallets/governance/src/tests.rs @@ -0,0 +1,1690 @@ +#![cfg(test)] +#![allow(clippy::iter_skip_next, clippy::unwrap_used, clippy::indexing_slicing)] +use super::*; +use crate::mock::*; +use frame_support::{assert_noop, assert_ok}; +use sp_core::U256; + +#[test] +fn environment_works() { + TestState::default().build_and_execute(|| { + assert_eq!( + AllowedProposers::::get(), + vec![U256::from(1), U256::from(2), U256::from(3)] + ); + assert_eq!( + Triumvirate::::get(), + vec![U256::from(1001), U256::from(1002), U256::from(1003)] + ); + }); +} + +#[test] +fn environment_members_are_sorted() { + TestState::default() + .with_allowed_proposers(vec![U256::from(2), U256::from(3), U256::from(1)]) + .with_triumvirate(vec![U256::from(1002), U256::from(1001), U256::from(1003)]) + .build_and_execute(|| { + assert_eq!( + AllowedProposers::::get(), + vec![U256::from(1), U256::from(2), U256::from(3)] + ); + assert_eq!( + Triumvirate::::get(), + vec![U256::from(1001), U256::from(1002), U256::from(1003)] + ); + }); +} + +#[test] +#[should_panic(expected = "Allowed proposers cannot contain duplicate accounts.")] +fn environment_with_duplicate_allowed_proposers_panics() { + TestState::default() + .with_allowed_proposers(vec![U256::from(1), U256::from(2), U256::from(2)]) + .build_and_execute(|| {}); +} + +#[test] +#[should_panic(expected = "Allowed proposers length cannot exceed MaxAllowedProposers.")] +fn environment_with_too_many_allowed_proposers_panics() { + let max_allowed_proposers = ::MaxAllowedProposers::get() as usize; + let allowed_proposers = (0..=max_allowed_proposers).map(U256::from).collect(); + TestState::default() + .with_allowed_proposers(allowed_proposers) + .build_and_execute(|| {}); +} + +#[test] +#[should_panic(expected = "Triumvirate cannot contain duplicate accounts.")] +fn environment_with_duplicate_triumvirate_panics() { + TestState::default() + .with_triumvirate(vec![U256::from(1001), U256::from(1002), U256::from(1002)]) + .build_and_execute(|| {}); +} + +#[test] +#[should_panic(expected = "Triumvirate length cannot exceed 3.")] +fn environment_with_too_many_triumvirate_panics() { + let triumvirate = (1..=4).map(U256::from).collect(); + TestState::default() + .with_triumvirate(triumvirate) + .build_and_execute(|| {}); +} + +#[test] +#[should_panic(expected = "Allowed proposers and triumvirate must be disjoint.")] +fn environment_with_overlapping_allowed_proposers_and_triumvirate_panics() { + TestState::default() + .with_allowed_proposers(vec![U256::from(1), U256::from(2), U256::from(3)]) + .with_triumvirate(vec![U256::from(1001), U256::from(1002), U256::from(1)]) + .build_and_execute(|| {}); +} + +#[test] +fn set_allowed_proposers_works() { + TestState::default() + .with_allowed_proposers(vec![]) + .build_and_execute(|| { + let allowed_proposers = BoundedVec::truncate_from(vec![ + U256::from(5), + U256::from(1), + U256::from(4), + U256::from(3), + U256::from(2), + ]); + assert!(AllowedProposers::::get().is_empty()); + + assert_ok!(Pallet::::set_allowed_proposers( + // SetAllowedProposersOrigin is EnsureRoot + RuntimeOrigin::root(), + allowed_proposers.clone() + )); + + assert_eq!( + AllowedProposers::::get().to_vec(), + // Sorted allowed proposers + vec![ + U256::from(1), + U256::from(2), + U256::from(3), + U256::from(4), + U256::from(5) + ] + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::AllowedProposersSet { + incoming: vec![ + U256::from(1), + U256::from(2), + U256::from(3), + U256::from(4), + U256::from(5) + ], + outgoing: vec![], + removed_proposals: vec![], + }) + ); + }); +} + +#[test] +fn set_allowed_proposers_removes_proposals_of_outgoing_proposers() { + TestState::default().build_and_execute(|| { + let (proposal_hash1, _proposal_index1) = create_custom_proposal!( + U256::from(1), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 1i32.to_be_bytes().to_vec())], + } + ); + let (proposal_hash2, _proposal_index2) = create_custom_proposal!( + U256::from(1), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 2i32.to_be_bytes().to_vec())], + } + ); + let (proposal_hash3, _proposal_index3) = create_custom_proposal!( + U256::from(3), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 3i32.to_be_bytes().to_vec())], + } + ); + assert_eq!( + AllowedProposers::::get(), + vec![U256::from(1), U256::from(2), U256::from(3)] + ); + + let allowed_proposers = + BoundedVec::truncate_from(vec![U256::from(2), U256::from(3), U256::from(4)]); + assert_ok!(Pallet::::set_allowed_proposers( + RuntimeOrigin::root(), + allowed_proposers.clone() + )); + + assert_eq!(AllowedProposers::::get(), allowed_proposers); + assert_eq!( + Proposals::::get(), + vec![(U256::from(3), proposal_hash3)] + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::AllowedProposersSet { + incoming: vec![U256::from(4)], + outgoing: vec![U256::from(1)], + removed_proposals: vec![ + (U256::from(1), proposal_hash1), + (U256::from(1), proposal_hash2) + ], + }) + ); + }); +} + +#[test] +fn set_allowed_proposers_with_bad_origin_fails() { + TestState::default() + .with_allowed_proposers(vec![]) + .build_and_execute(|| { + let allowed_proposers = + BoundedVec::truncate_from((1..=5).map(U256::from).collect::>()); + + assert_noop!( + Pallet::::set_allowed_proposers( + RuntimeOrigin::signed(U256::from(42)), + allowed_proposers.clone() + ), + DispatchError::BadOrigin + ); + + assert_noop!( + Pallet::::set_allowed_proposers(RuntimeOrigin::none(), allowed_proposers), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_allowed_proposers_with_duplicate_accounts_fails() { + TestState::default() + .with_allowed_proposers(vec![]) + .build_and_execute(|| { + let allowed_proposers = BoundedVec::truncate_from( + std::iter::repeat_n(U256::from(1), 2).collect::>(), + ); + + assert_noop!( + Pallet::::set_allowed_proposers(RuntimeOrigin::root(), allowed_proposers), + Error::::DuplicateAccounts + ); + }); +} + +#[test] +fn set_allowed_proposers_with_triumvirate_intersection_fails() { + TestState::default() + .with_allowed_proposers(vec![]) + .with_triumvirate(vec![U256::from(1), U256::from(2), U256::from(3)]) + .build_and_execute(|| { + let allowed_proposers = + BoundedVec::truncate_from((3..=8).map(U256::from).collect::>()); + + assert_noop!( + Pallet::::set_allowed_proposers(RuntimeOrigin::root(), allowed_proposers), + Error::::AllowedProposersAndTriumvirateMustBeDisjoint + ); + }); +} + +#[test] +fn set_triumvirate_works() { + TestState::default() + .with_triumvirate(vec![]) + .build_and_execute(|| { + let triumvirate = BoundedVec::truncate_from(vec![ + U256::from(1003), + U256::from(1001), + U256::from(1002), + ]); + assert!(Triumvirate::::get().is_empty()); + + assert_ok!(Pallet::::set_triumvirate( + // SetTriumvirateOrigin is EnsureRoot + RuntimeOrigin::root(), + triumvirate.clone() + )); + + assert_eq!( + Triumvirate::::get(), + // Sorted triumvirate + vec![U256::from(1001), U256::from(1002), U256::from(1003)] + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::TriumvirateSet { + incoming: vec![U256::from(1001), U256::from(1002), U256::from(1003)], + outgoing: vec![], + }) + ); + }); +} + +#[test] +fn set_triumvirate_removes_votes_of_outgoing_triumvirate_members() { + TestState::default().build_and_execute(|| { + let (proposal_hash1, proposal_index1) = create_custom_proposal!( + U256::from(1), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 1i32.to_be_bytes().to_vec())], + } + ); + let (proposal_hash2, proposal_index2) = create_custom_proposal!( + U256::from(2), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 2i32.to_be_bytes().to_vec())], + } + ); + let (proposal_hash3, proposal_index3) = create_custom_proposal!( + U256::from(3), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 3i32.to_be_bytes().to_vec())], + } + ); + assert_eq!( + Triumvirate::::get(), + vec![U256::from(1001), U256::from(1002), U256::from(1003)] + ); + + vote_aye_on_proposed!(U256::from(1001), proposal_hash1, proposal_index1); + + vote_nay_on_proposed!(U256::from(1002), proposal_hash2, proposal_index2); + vote_aye_on_proposed!(U256::from(1003), proposal_hash2, proposal_index2); + + vote_nay_on_proposed!(U256::from(1001), proposal_hash3, proposal_index3); + vote_aye_on_proposed!(U256::from(1002), proposal_hash3, proposal_index3); + + let triumvirate = + BoundedVec::truncate_from(vec![U256::from(1001), U256::from(1003), U256::from(1004)]); + assert_ok!(Pallet::::set_triumvirate( + RuntimeOrigin::root(), + triumvirate.clone() + )); + assert_eq!(Triumvirate::::get(), triumvirate); + let voting1 = TriumvirateVoting::::get(proposal_hash1).unwrap(); + assert_eq!(voting1.ayes.to_vec(), vec![U256::from(1001)]); + assert!(voting1.nays.to_vec().is_empty()); + let voting2 = TriumvirateVoting::::get(proposal_hash2).unwrap(); + assert_eq!(voting2.ayes.to_vec(), vec![U256::from(1003)]); + assert!(voting2.nays.to_vec().is_empty()); + let voting3 = TriumvirateVoting::::get(proposal_hash3).unwrap(); + assert!(voting3.ayes.to_vec().is_empty()); + assert_eq!(voting3.nays.to_vec(), vec![U256::from(1001)]); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::TriumvirateSet { + incoming: vec![U256::from(1004)], + outgoing: vec![U256::from(1002)], + }) + ); + }); +} + +#[test] +fn set_triumvirate_with_bad_origin_fails() { + TestState::default() + .with_triumvirate(vec![]) + .build_and_execute(|| { + let triumvirate = BoundedVec::truncate_from( + (1..=3).map(|i| U256::from(1000 + i)).collect::>(), + ); + + assert_noop!( + Pallet::::set_triumvirate( + RuntimeOrigin::signed(U256::from(42)), + triumvirate.clone() + ), + DispatchError::BadOrigin + ); + + assert_noop!( + Pallet::::set_triumvirate(RuntimeOrigin::none(), triumvirate), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn set_triumvirate_with_duplicate_accounts_fails() { + TestState::default() + .with_triumvirate(vec![]) + .build_and_execute(|| { + let triumvirate = BoundedVec::truncate_from( + std::iter::repeat_n(U256::from(1001), 2).collect::>(), + ); + + assert_noop!( + Pallet::::set_triumvirate(RuntimeOrigin::root(), triumvirate), + Error::::DuplicateAccounts + ); + }); +} + +#[test] +fn set_triumvirate_with_allowed_proposers_intersection_fails() { + TestState::default() + .with_allowed_proposers(vec![U256::from(1), U256::from(2), U256::from(3)]) + .build_and_execute(|| { + let triumvirate = + BoundedVec::truncate_from((3..=8).map(U256::from).collect::>()); + + assert_noop!( + Pallet::::set_triumvirate(RuntimeOrigin::root(), triumvirate), + Error::::AllowedProposersAndTriumvirateMustBeDisjoint + ); + }); +} + +#[test] +fn propose_works_with_inline_preimage() { + TestState::default().build_and_execute(|| { + let key_value = (b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec()); + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![key_value], + }, + )); + let length_bound = proposal.encoded_size() as u32; + + let proposal_index = ProposalCount::::get(); + assert_eq!(proposal_index, 0); + assert_ok!(Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + )); + + let proposal_hash = ::Hashing::hash_of(&proposal); + let bounded_proposal = ::Preimages::bound(*proposal).unwrap(); + assert_eq!( + Proposals::::get(), + vec![(U256::from(1), proposal_hash)] + ); + assert_eq!(ProposalCount::::get(), 1); + assert_eq!( + ProposalOf::::get(proposal_hash), + Some(bounded_proposal) + ); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + TriumvirateVoting::::get(proposal_hash), + Some(TriumvirateVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::new(), + end: now + MotionDuration::get(), + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ProposalSubmitted { + account: U256::from(1), + proposal_index: 0, + proposal_hash, + voting_end: now + MotionDuration::get(), + }) + ); + }); +} + +#[test] +fn propose_works_with_lookup_preimage() { + TestState::default().build_and_execute(|| { + let key_value = (b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec()); + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + // We deliberately create a large proposal to avoid inlining. + items: std::iter::repeat_n(key_value, 50).collect::>(), + }, + )); + let length_bound = proposal.encoded_size() as u32; + + let proposal_index = ProposalCount::::get(); + assert_eq!(proposal_index, 0); + assert_ok!(Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + )); + + let proposal_hash = ::Hashing::hash_of(&proposal); + assert_eq!( + Proposals::::get(), + vec![(U256::from(1), proposal_hash)] + ); + assert_eq!(ProposalCount::::get(), 1); + let stored_proposals = ProposalOf::::iter().collect::>(); + assert_eq!(stored_proposals.len(), 1); + let (stored_hash, bounded_proposal) = &stored_proposals[0]; + assert_eq!(stored_hash, &proposal_hash); + assert!(::Preimages::have(bounded_proposal)); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + TriumvirateVoting::::get(proposal_hash), + Some(TriumvirateVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::new(), + end: now + MotionDuration::get(), + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ProposalSubmitted { + account: U256::from(1), + proposal_index: 0, + proposal_hash, + voting_end: now + MotionDuration::get(), + }) + ); + }); +} + +#[test] +fn propose_with_bad_origin_fails() { + TestState::default().build_and_execute(|| { + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + }, + )); + let length_bound = proposal.encoded_size() as u32; + + assert_noop!( + Pallet::::propose(RuntimeOrigin::root(), proposal.clone(), length_bound), + DispatchError::BadOrigin + ); + + assert_noop!( + Pallet::::propose(RuntimeOrigin::none(), proposal.clone(), length_bound), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn propose_with_non_allowed_proposer_fails() { + TestState::default().build_and_execute(|| { + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + }, + )); + let length_bound = proposal.encoded_size() as u32; + + assert_noop!( + Pallet::::propose( + RuntimeOrigin::signed(U256::from(42)), + proposal.clone(), + length_bound + ), + Error::::NotAllowedProposer + ); + }); +} + +#[test] +fn propose_with_incorrect_length_bound_fails() { + TestState::default().build_and_execute(|| { + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + }, + )); + // We deliberately set the length bound to be one less than the proposal length. + let length_bound = proposal.encoded_size() as u32 - 1; + + assert_noop!( + Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + ), + Error::::WrongProposalLength + ); + }); +} + +#[test] +fn propose_with_incorrect_weight_bound_fails() { + TestState::default().build_and_execute(|| { + let proposal = Box::new(RuntimeCall::TestPallet( + pallet_test::Call::::expensive_call {}, + )); + let length_bound = proposal.encoded_size() as u32; + + assert_noop!( + Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + ), + Error::::WrongProposalWeight + ); + }); +} + +#[test] +fn propose_with_duplicate_proposal_fails() { + TestState::default().build_and_execute(|| { + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + }, + )); + let length_bound = proposal.encoded_size() as u32; + + assert_ok!(Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + )); + + assert_noop!( + Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + ), + Error::::DuplicateProposal + ); + }); +} + +#[test] +fn propose_with_already_scheduled_proposal_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + vote_aye_on_proposed!(U256::from(1002), proposal_hash, proposal_index); + + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + }, + )); + let length_bound = proposal.encoded_size() as u32; + assert_noop!( + Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal.clone(), + length_bound + ), + Error::::AlreadyScheduled + ); + }); +} + +#[test] +fn propose_with_too_many_proposals_fails() { + TestState::default().build_and_execute(|| { + // Create the maximum number of proposals. + let proposals = (1..=MaxProposals::get()) + .map(|i| { + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![( + format!("Foobar{i}").as_bytes().to_vec(), + 42u32.to_be_bytes().to_vec(), + )], + }, + )); + let length_bound = proposal.encoded_size() as u32; + (proposal, length_bound) + }) + .collect::>(); + + for (proposal, length_bound) in proposals { + assert_ok!(Pallet::::propose( + RuntimeOrigin::signed(U256::from(1)), + proposal, + length_bound + )); + } + + let proposal = Box::new(RuntimeCall::System( + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + }, + )); + let length_bound = proposal.encoded_size() as u32; + assert_noop!( + Pallet::::propose(RuntimeOrigin::signed(U256::from(1)), proposal, length_bound), + Error::::TooManyProposals + ); + }); +} + +#[test] +fn triumirate_vote_aye_as_first_voter_works() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + let approve = true; + assert_ok!(Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1001)), + proposal_hash, + proposal_index, + approve + )); + + let votes = TriumvirateVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.ayes.to_vec(), vec![U256::from(1001)]); + assert!(votes.nays.to_vec().is_empty()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1001), + proposal_hash, + voted: true, + yes: 1, + no: 0, + }) + ); + }); +} + +#[test] +fn triumvirate_vote_nay_as_first_voter_works() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + let approve = false; + assert_ok!(Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1001)), + proposal_hash, + proposal_index, + approve + )); + + let votes = TriumvirateVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.nays.to_vec(), vec![U256::from(1001)]); + assert!(votes.ayes.to_vec().is_empty()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1001), + proposal_hash, + voted: false, + yes: 0, + no: 1, + }) + ); + }); +} + +#[test] +fn triumvirate_vote_can_be_updated() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + // Vote aye initially + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + let votes = TriumvirateVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.ayes.to_vec(), vec![U256::from(1001)]); + assert!(votes.nays.to_vec().is_empty()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1001), + proposal_hash, + voted: true, + yes: 1, + no: 0, + }) + ); + + // Then vote nay, replacing the aye vote + vote_nay_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + let votes = TriumvirateVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.nays.to_vec(), vec![U256::from(1001)]); + assert!(votes.ayes.to_vec().is_empty()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1001), + proposal_hash, + voted: false, + yes: 0, + no: 1, + }) + ); + + // Then vote aye again, replacing the nay vote + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + let votes = TriumvirateVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.ayes.to_vec(), vec![U256::from(1001)]); + assert!(votes.nays.to_vec().is_empty()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1001), + proposal_hash, + voted: true, + yes: 1, + no: 0, + }) + ); + }); +} + +#[test] +fn two_triumvirate_aye_votes_schedule_proposal() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + vote_nay_on_proposed!(U256::from(1002), proposal_hash, proposal_index); + vote_aye_on_proposed!(U256::from(1003), proposal_hash, proposal_index); + + assert!(Proposals::::get().is_empty()); + assert!(!TriumvirateVoting::::contains_key(proposal_hash)); + assert_eq!(Scheduled::::get(), vec![proposal_hash]); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::new(), + initial_dispatch_time: now + MotionDuration::get(), + delay: Zero::zero(), + }) + ); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + now + MotionDuration::get() + ); + assert_eq!( + nth_last_event(2), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1003), + proposal_hash, + voted: true, + yes: 2, + no: 1, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ProposalScheduled { proposal_hash }) + ); + }); +} + +#[test] +fn two_triumvirate_nay_votes_cancel_proposal() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + vote_nay_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + vote_aye_on_proposed!(U256::from(1002), proposal_hash, proposal_index); + vote_nay_on_proposed!(U256::from(1003), proposal_hash, proposal_index); + + assert!(Proposals::::get().is_empty()); + assert!(!TriumvirateVoting::::contains_key(proposal_hash)); + assert!(Scheduled::::get().is_empty()); + assert!(ProposalOf::::get(proposal_hash).is_none()); + assert_eq!( + nth_last_event(1), + RuntimeEvent::Governance(Event::::VotedOnProposal { + account: U256::from(1003), + proposal_hash, + voted: false, + yes: 1, + no: 2, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ProposalCancelled { proposal_hash }) + ); + }); +} + +#[test] +fn triumvirate_vote_as_bad_origin_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::root(), + proposal_hash, + proposal_index, + true + ), + DispatchError::BadOrigin + ); + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::none(), + proposal_hash, + proposal_index, + true + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn triumvirate_vote_as_non_triumvirate_member_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(42)), + proposal_hash, + proposal_index, + true + ), + Error::::NotTriumvirateMember + ); + }); +} + +#[test] +fn triumvirate_vote_on_missing_proposal_fails() { + TestState::default().build_and_execute(|| { + let invalid_proposal_hash = + ::Hashing::hash(b"Invalid proposal"); + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1001)), + invalid_proposal_hash, + 0, + true + ), + Error::::ProposalMissing + ); + }); +} + +#[test] +fn triumvirate_vote_on_scheduled_proposal_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + vote_aye_on_proposed!(U256::from(1002), proposal_hash, proposal_index); + + assert!(Proposals::::get().is_empty()); + assert_eq!(Scheduled::::get(), vec![proposal_hash]); + + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1003)), + proposal_hash, + proposal_index, + true + ), + Error::::ProposalMissing + ); + }) +} + +#[test] +fn triumvirate_vote_on_proposal_with_wrong_index_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1001)), + proposal_hash, + proposal_index + 1, + true + ), + Error::::WrongProposalIndex + ); + }); +} + +#[test] +fn triumvirate_vote_after_voting_period_ended_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + let now = frame_system::Pallet::::block_number(); + run_to_block(now + MotionDuration::get() + 1); + + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1001)), + proposal_hash, + proposal_index, + true + ), + Error::::VotingPeriodEnded + ); + }); +} + +#[test] +fn duplicate_triumvirate_vote_on_proposal_already_voted_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + let aye_voter = RuntimeOrigin::signed(U256::from(1001)); + let approve = true; + assert_ok!(Pallet::::vote_on_proposed( + aye_voter.clone(), + proposal_hash, + proposal_index, + approve + )); + assert_noop!( + Pallet::::vote_on_proposed(aye_voter, proposal_hash, proposal_index, approve), + Error::::DuplicateVote + ); + + let nay_voter = RuntimeOrigin::signed(U256::from(1002)); + let approve = false; + assert_ok!(Pallet::::vote_on_proposed( + nay_voter.clone(), + proposal_hash, + proposal_index, + approve + )); + assert_noop!( + Pallet::::vote_on_proposed(nay_voter, proposal_hash, proposal_index, approve), + Error::::DuplicateVote + ); + }); +} + +#[test] +fn triumvirate_aye_vote_on_proposal_with_too_many_scheduled_fails() { + TestState::default().build_and_execute(|| { + // We fill the scheduled proposals up to the maximum. + for i in 0..MaxScheduled::get() { + let (proposal_hash, proposal_index) = create_custom_proposal!( + U256::from(1), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), i.to_be_bytes().to_vec())], + } + ); + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + vote_aye_on_proposed!(U256::from(1002), proposal_hash, proposal_index); + } + + let (proposal_hash, proposal_index) = create_proposal!(); + + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + assert_noop!( + Pallet::::vote_on_proposed( + RuntimeOrigin::signed(U256::from(1002)), + proposal_hash, + proposal_index, + true + ), + Error::::TooManyScheduled + ); + }); +} + +#[test] +fn collective_member_aye_vote_on_scheduled_proposal_works() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + + // Add an aye vote from an economic collective member. + let economic_member = U256::from(2001); + assert_ok!(Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(economic_member), + proposal_hash, + proposal_index, + true + )); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::truncate_from(vec![economic_member]), + nays: BoundedVec::new(), + initial_dispatch_time: now + MotionDuration::get(), + delay: Zero::zero(), + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: economic_member, + proposal_hash, + voted: true, + yes: 1, + no: 0, + }) + ); + + // Add a second aye vote from a building collective member. + let building_member = U256::from(3001); + assert_ok!(Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(building_member), + proposal_hash, + proposal_index, + true + )); + + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::truncate_from(vec![economic_member, building_member]), + nays: BoundedVec::new(), + initial_dispatch_time: now + MotionDuration::get(), + delay: Zero::zero(), + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: building_member, + proposal_hash, + voted: true, + yes: 2, + no: 0, + }) + ); + }); +} + +#[test] +fn collective_member_votes_succession_on_scheduled_proposal_adjust_delay_and_can_fast_track() { + TestState::default().build_and_execute(|| { + let now = frame_system::Pallet::::block_number(); + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + let voting = CollectiveVoting::::get(proposal_hash).unwrap(); + assert_eq!(voting.delay, 0); + + // Adding a nay vote increases the delay + vote_nay_on_scheduled!(U256::from(2001), proposal_hash, proposal_index); + let initial_delay = InitialSchedulingDelay::get() as f64; + let initial_dispatch_time = now + MotionDuration::get(); + let delay = (initial_delay * 1.5_f64.powi(1)).ceil() as u64; + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::truncate_from(vec![U256::from(2001)]), + initial_dispatch_time, + delay, + }) + ); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + initial_dispatch_time + delay + ); + assert_eq!( + nth_last_event(3), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: U256::from(2001), + proposal_hash, + voted: false, + yes: 0, + no: 1, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalDelayAdjusted { + proposal_hash, + dispatch_time: DispatchTime::At(initial_dispatch_time + delay), + }) + ); + + // Adding a second nay vote increases the delay + vote_nay_on_scheduled!(U256::from(2002), proposal_hash, proposal_index); + let delay = (initial_delay * 1.5_f64.powi(2)).ceil() as u64; + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::truncate_from(vec![U256::from(2001), U256::from(2002)]), + initial_dispatch_time, + delay, + }) + ); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + initial_dispatch_time + delay + ); + assert_eq!( + nth_last_event(3), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: U256::from(2002), + proposal_hash, + voted: false, + yes: 0, + no: 2, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalDelayAdjusted { + proposal_hash, + dispatch_time: DispatchTime::At(initial_dispatch_time + delay), + }) + ); + + // Adding a third nay vote increases the delay + vote_nay_on_scheduled!(U256::from(2003), proposal_hash, proposal_index); + let delay = (initial_delay * 1.5_f64.powi(3)) as u64; + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::new(), + nays: BoundedVec::truncate_from(vec![ + U256::from(2001), + U256::from(2002), + U256::from(2003) + ]), + initial_dispatch_time, + delay, + }) + ); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + initial_dispatch_time + delay + ); + assert_eq!( + nth_last_event(3), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: U256::from(2003), + proposal_hash, + voted: false, + yes: 0, + no: 3, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalDelayAdjusted { + proposal_hash, + dispatch_time: DispatchTime::At(initial_dispatch_time + delay), + }) + ); + + // Adding a aye vote decreases the delay because net score become lower + vote_aye_on_scheduled!(U256::from(2004), proposal_hash, proposal_index); + let delay = (initial_delay * 1.5_f64.powi(2)).ceil() as u64; + assert_eq!( + CollectiveVoting::::get(proposal_hash), + Some(CollectiveVotes { + index: proposal_index, + ayes: BoundedVec::truncate_from(vec![U256::from(2004)]), + nays: BoundedVec::truncate_from(vec![ + U256::from(2001), + U256::from(2002), + U256::from(2003) + ]), + initial_dispatch_time, + delay, + }) + ); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + initial_dispatch_time + delay + ); + assert_eq!( + nth_last_event(3), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: U256::from(2004), + proposal_hash, + voted: true, + yes: 1, + no: 3, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalDelayAdjusted { + proposal_hash, + dispatch_time: DispatchTime::At(initial_dispatch_time + delay), + }) + ); + + // Now let's run some blocks until before the sheduled time + run_to_block(initial_dispatch_time + delay - 5); + // Task hasn't been executed yet + assert!(get_scheduler_proposal_task(proposal_hash).is_some()); + + // Adding a new aye vote should fast track the proposal because the delay will + // fall below the elapsed time + vote_aye_on_scheduled!(U256::from(2005), proposal_hash, proposal_index); + assert!(CollectiveVoting::::get(proposal_hash).is_none()); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + // Fast track here means next block scheduling + now + 1 + ); + // The proposal is still scheduled, even if next block, we keep track of it + assert_eq!(Scheduled::::get(), vec![proposal_hash]); + assert_eq!( + nth_last_event(3), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: U256::from(2005), + proposal_hash, + voted: true, + yes: 2, + no: 3, + }) + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalFastTracked { proposal_hash }) + ); + + // Now let run one block to see the proposal executed + assert_eq!(sp_io::storage::get(b"Foobar"), None); // Not executed yet + run_to_block(now + delay + 1); + assert!(get_scheduler_proposal_task(proposal_hash).is_none()); + let stored_value = 42u32.to_be_bytes().to_vec().into(); + assert_eq!(sp_io::storage::get(b"Foobar"), Some(stored_value)); // Executed + }); +} + +#[test] +fn collective_member_vote_on_scheduled_proposal_can_be_updated() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + let economic_member = U256::from(2001); + + // Vote aye initially as an economic collective member + vote_aye_on_scheduled!(economic_member, proposal_hash, proposal_index); + let votes = CollectiveVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.ayes.to_vec(), vec![economic_member]); + assert!(votes.nays.to_vec().is_empty()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: economic_member, + proposal_hash, + voted: true, + yes: 1, + no: 0, + }) + ); + + // Then vote nay, replacing the aye vote + vote_nay_on_scheduled!(economic_member, proposal_hash, proposal_index); + let votes = CollectiveVoting::::get(proposal_hash).unwrap(); + assert!(votes.ayes.to_vec().is_empty()); + assert_eq!(votes.nays.to_vec(), vec![economic_member]); + assert_eq!( + System::events().into_iter().rev().nth(3).unwrap().event, + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: economic_member, + proposal_hash, + voted: false, + yes: 0, + no: 1, + }) + ); + + // Then vote aye again, replacing the nay vote + vote_aye_on_scheduled!(economic_member, proposal_hash, proposal_index); + let votes = CollectiveVoting::::get(proposal_hash).unwrap(); + assert_eq!(votes.ayes.to_vec(), vec![economic_member]); + assert!(votes.nays.to_vec().is_empty()); + assert_eq!( + System::events().into_iter().rev().nth(3).unwrap().event, + RuntimeEvent::Governance(Event::::VotedOnScheduled { + account: economic_member, + proposal_hash, + voted: true, + yes: 1, + no: 0, + }) + ); + }); +} + +#[test] +fn collective_member_aye_votes_above_threshold_on_scheduled_proposal_fast_tracks() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + let threshold = FastTrackThreshold::get().mul_ceil(TOTAL_COLLECTIVES_SIZE); + let combined_collective = EconomicCollective::::get() + .into_iter() + .chain(BuildingCollective::::get().into_iter()); + + for member in combined_collective.into_iter().take(threshold as usize) { + vote_aye_on_scheduled!(member, proposal_hash, proposal_index); + } + + assert!(CollectiveVoting::::get(proposal_hash).is_none()); + let now = frame_system::Pallet::::block_number(); + assert_eq!( + get_scheduler_proposal_task(proposal_hash).unwrap().0, + now + 1 + ); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalFastTracked { proposal_hash }) + ); + + // Now let run one block to see the proposal executed + assert_eq!(sp_io::storage::get(b"Foobar"), None); // Not executed yet + run_to_block(now + 1); + assert!(get_scheduler_proposal_task(proposal_hash).is_none()); + let stored_value = 42u32.to_be_bytes().to_vec().into(); + assert_eq!(sp_io::storage::get(b"Foobar"), Some(stored_value)); // Executed + }); +} + +#[test] +fn collective_member_nay_votes_above_threshold_on_scheduled_proposal_cancels() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + let threshold = CancellationThreshold::get().mul_ceil(TOTAL_COLLECTIVES_SIZE); + let combined_collective = EconomicCollective::::get() + .into_iter() + .chain(BuildingCollective::::get().into_iter()); + + for member in combined_collective.into_iter().take(threshold as usize) { + vote_nay_on_scheduled!(member, proposal_hash, proposal_index); + } + + assert!(Scheduled::::get().is_empty()); + assert!(CollectiveVoting::::get(proposal_hash).is_none()); + assert!(get_scheduler_proposal_task(proposal_hash).is_none()); + assert_eq!( + last_event(), + RuntimeEvent::Governance(Event::::ScheduledProposalCancelled { proposal_hash }) + ); + }); +} + +#[test] +fn collective_member_aye_vote_triggering_fast_track_on_next_block_scheduled_proposal_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + let threshold = FastTrackThreshold::get().mul_ceil(TOTAL_COLLECTIVES_SIZE); + let combined_collective = EconomicCollective::::get() + .into_iter() + .chain(BuildingCollective::::get().into_iter()); + + let below_threshold = (threshold - 1) as usize; + for member in combined_collective.clone().take(below_threshold) { + vote_aye_on_scheduled!(member, proposal_hash, proposal_index); + } + + let voting = CollectiveVoting::::get(proposal_hash).unwrap(); + run_to_block(voting.initial_dispatch_time - 1); + + let voter = combined_collective.skip(below_threshold).next().unwrap(); + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(voter), + proposal_hash, + proposal_index, + true + ), + pallet_scheduler::Error::::RescheduleNoChange + ); + }); +} + +#[test] +fn collective_member_vote_on_scheduled_proposal_from_non_collective_member_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(U256::from(42)), + proposal_hash, + proposal_index, + true + ), + Error::::NotCollectiveMember + ); + }); +} + +#[test] +fn collective_member_vote_on_non_scheduled_proposal_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_proposal!(); + + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(U256::from(2001)), + proposal_hash, + proposal_index, + true + ), + Error::::ProposalNotScheduled + ); + }); +} + +#[test] +fn collective_member_vote_on_fast_tracked_scheduled_proposal_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + let threshold = FastTrackThreshold::get().mul_ceil(TOTAL_COLLECTIVES_SIZE); + let combined_collective = EconomicCollective::::get() + .into_iter() + .chain(BuildingCollective::::get().into_iter()); + + for member in combined_collective.clone().take(threshold as usize) { + vote_aye_on_scheduled!(member, proposal_hash, proposal_index); + } + + let voter = combined_collective.skip(threshold as usize).next().unwrap(); + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(voter), + proposal_hash, + proposal_index, + true + ), + Error::::VotingPeriodEnded + ); + }); +} + +#[test] +fn collective_member_vote_on_scheduled_proposal_with_wrong_index_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, _proposal_index) = create_scheduled_proposal!(); + + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(U256::from(2001)), + proposal_hash, + 42, + true + ), + Error::::WrongProposalIndex + ); + }); +} + +#[test] +fn duplicate_collective_member_vote_on_scheduled_proposal_already_voted_fails() { + TestState::default().build_and_execute(|| { + let (proposal_hash, proposal_index) = create_scheduled_proposal!(); + + let aye_voter = U256::from(2001); + vote_aye_on_scheduled!(aye_voter, proposal_hash, proposal_index); + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(aye_voter), + proposal_hash, + proposal_index, + true + ), + Error::::DuplicateVote + ); + + let nay_voter = U256::from(2002); + vote_nay_on_scheduled!(nay_voter, proposal_hash, proposal_index); + assert_noop!( + Pallet::::vote_on_scheduled( + RuntimeOrigin::signed(nay_voter), + proposal_hash, + proposal_index, + false + ), + Error::::DuplicateVote + ); + }); +} + +#[test] +fn collective_rotation_run_correctly_at_rotation_period() { + TestState::default().build_and_execute(|| { + let next_economic_collective = (1..=ECONOMIC_COLLECTIVE_SIZE) + .map(|i| U256::from(4000 + i)) + .collect::>(); + let next_building_collective = (1..=BUILDING_COLLECTIVE_SIZE) + .map(|i| U256::from(5000 + i)) + .collect::>(); + + assert_eq!( + EconomicCollective::::get().len(), + ECONOMIC_COLLECTIVE_SIZE as usize, + ); + assert_ne!( + EconomicCollective::::get().to_vec(), + next_economic_collective + ); + assert_eq!( + BuildingCollective::::get().len(), + BUILDING_COLLECTIVE_SIZE as usize, + ); + assert_ne!( + BuildingCollective::::get().to_vec(), + next_building_collective + ); + + set_next_economic_collective!(next_economic_collective.clone()); + set_next_building_collective!(next_building_collective.clone()); + + run_to_block(CollectiveRotationPeriod::get()); + + assert_eq!( + EconomicCollective::::get().to_vec(), + next_economic_collective + ); + assert_eq!( + BuildingCollective::::get().to_vec(), + next_building_collective + ); + }); +} + +#[macro_export] +macro_rules! create_custom_proposal { + ($proposer:expr, $call:expr) => {{ + let proposal: Box<::RuntimeCall> = Box::new($call.into()); + let length_bound = proposal.encoded_size() as u32; + let proposal_hash = ::Hashing::hash_of(&proposal); + let proposal_index = ProposalCount::::get(); + + assert_ok!(Pallet::::propose( + RuntimeOrigin::signed($proposer), + proposal.clone(), + length_bound + )); + + (proposal_hash, proposal_index) + }}; +} + +#[macro_export] +macro_rules! create_proposal { + () => {{ + create_custom_proposal!( + U256::from(1), + frame_system::Call::::set_storage { + items: vec![(b"Foobar".to_vec(), 42u32.to_be_bytes().to_vec())], + } + ) + }}; +} + +#[macro_export] +macro_rules! create_scheduled_proposal { + () => {{ + let (proposal_hash, proposal_index) = create_proposal!(); + vote_aye_on_proposed!(U256::from(1001), proposal_hash, proposal_index); + vote_aye_on_proposed!(U256::from(1002), proposal_hash, proposal_index); + (proposal_hash, proposal_index) + }}; +} + +#[macro_export] +macro_rules! vote_aye_on_proposed { + ($voter:expr, $proposal_hash:expr, $proposal_index:expr) => {{ + assert_ok!(Pallet::::vote_on_proposed( + RuntimeOrigin::signed($voter), + $proposal_hash, + $proposal_index, + true + )); + }}; +} + +#[macro_export] +macro_rules! vote_nay_on_proposed { + ($voter:expr, $proposal_hash:expr, $proposal_index:expr) => {{ + assert_ok!(Pallet::::vote_on_proposed( + RuntimeOrigin::signed($voter), + $proposal_hash, + $proposal_index, + false + )); + }}; +} + +#[macro_export] +macro_rules! vote_aye_on_scheduled { + ($voter:expr, $proposal_hash:expr, $proposal_index:expr) => {{ + assert_ok!(Pallet::::vote_on_scheduled( + RuntimeOrigin::signed($voter), + $proposal_hash, + $proposal_index, + true + )); + }}; +} + +#[macro_export] +macro_rules! vote_nay_on_scheduled { + ($voter:expr, $proposal_hash:expr, $proposal_index:expr) => {{ + assert_ok!(Pallet::::vote_on_scheduled( + RuntimeOrigin::signed($voter), + $proposal_hash, + $proposal_index, + false + )); + }}; +} + +pub(crate) fn get_scheduler_proposal_task( + proposal_hash: ::Hash, +) -> Option>> { + let task_name: [u8; 32] = proposal_hash.as_ref().try_into().unwrap(); + pallet_scheduler::Lookup::::get(task_name) +} diff --git a/pallets/governance/src/weights.rs b/pallets/governance/src/weights.rs new file mode 100644 index 0000000000..746b71c4a1 --- /dev/null +++ b/pallets/governance/src/weights.rs @@ -0,0 +1,287 @@ + +//! Autogenerated weights for `pallet_governance` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 52.0.0 +//! DATE: 2025-12-17, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `MacBook-Air.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// ./target/production/wbuild/node-subtensor-runtime/node_subtensor_runtime.compact.compressed.wasm +// --pallet +// pallet_governance +// --extrinsic +// * +// --template +// ./.maintain/frame-weight-template.hbs +// --output +// ./pallets/governance/src/weights.rs +// --genesis-builder-preset=benchmark +// --genesis-builder=runtime +// --allow-missing-host-functions + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::ParityDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_governance`. +pub trait WeightInfo { + fn set_allowed_proposers(p: u32, ) -> Weight; + fn set_triumvirate(p: u32, ) -> Weight; + fn propose() -> Weight; + fn vote_on_proposed() -> Weight; + fn vote_on_scheduled() -> Weight; +} + +/// Weights for `pallet_governance` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Governance::Triumvirate` (r:1 w:0) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::AllowedProposers` (r:1 w:1) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:20) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:0 w:20) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 20]`. + fn set_allowed_proposers(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `827 + p * (64 ±0)` + // Estimated: `2766` + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(8_386_353, 2766) + // Standard Error: 10_807 + .saturating_add(Weight::from_parts(2_865_833, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(p.into()))) + } + /// Storage: `Governance::AllowedProposers` (r:1 w:0) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::Triumvirate` (r:1 w:1) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:0) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:20 w:20) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 20]`. + fn set_triumvirate(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `303 + p * (178 ±0)` + // Estimated: `2766 + p * (2709 ±0)` + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(9_300_991, 2766) + // Standard Error: 6_483 + .saturating_add(Weight::from_parts(2_726_847, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2709).saturating_mul(p.into())) + } + /// Storage: `Governance::AllowedProposers` (r:1 w:0) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:1 w:1) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:0) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalCount` (r:1 w:1) + /// Proof: `Governance::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Preimage::StatusFor` (r:1 w:0) + /// Proof: `Preimage::StatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Preimage::RequestStatusFor` (r:1 w:1) + /// Proof: `Preimage::RequestStatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:1) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Preimage::PreimageFor` (r:0 w:1) + /// Proof: `Preimage::PreimageFor` (`max_values`: None, `max_size`: Some(4194344), added: 4196819, mode: `MaxEncodedLen`) + fn propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `166` + // Estimated: `3628` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(28_000_000, 3628) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: `Governance::Triumvirate` (r:1 w:0) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:1 w:1) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:1) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:1 w:1) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Lookup` (r:1 w:1) + /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Agenda` (r:1 w:1) + /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10463), added: 12938, mode: `MaxEncodedLen`) + /// Storage: `Governance::CollectiveVoting` (r:0 w:1) + /// Proof: `Governance::CollectiveVoting` (`max_values`: None, `max_size`: Some(2094), added: 4569, mode: `MaxEncodedLen`) + fn vote_on_proposed() -> Weight { + // Proof Size summary in bytes: + // Measured: `512` + // Estimated: `13928` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 13928) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } + /// Storage: `Governance::EconomicCollective` (r:1 w:0) + /// Proof: `Governance::EconomicCollective` (`max_values`: Some(1), `max_size`: Some(513), added: 1008, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:0) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::CollectiveVoting` (r:1 w:1) + /// Proof: `Governance::CollectiveVoting` (`max_values`: None, `max_size`: Some(2094), added: 4569, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Lookup` (r:1 w:1) + /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Agenda` (r:2 w:2) + /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10463), added: 12938, mode: `MaxEncodedLen`) + fn vote_on_scheduled() -> Weight { + // Proof Size summary in bytes: + // Measured: `476` + // Estimated: `26866` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 26866) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Governance::Triumvirate` (r:1 w:0) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::AllowedProposers` (r:1 w:1) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:20) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:0 w:20) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 20]`. + fn set_allowed_proposers(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `827 + p * (64 ±0)` + // Estimated: `2766` + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(8_386_353, 2766) + // Standard Error: 10_807 + .saturating_add(Weight::from_parts(2_865_833, 0).saturating_mul(p.into())) + .saturating_add(ParityDbWeight::get().reads(3_u64)) + .saturating_add(ParityDbWeight::get().writes(2_u64)) + .saturating_add(ParityDbWeight::get().writes((2_u64).saturating_mul(p.into()))) + } + /// Storage: `Governance::AllowedProposers` (r:1 w:0) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::Triumvirate` (r:1 w:1) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:0) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:20 w:20) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 20]`. + fn set_triumvirate(p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `303 + p * (178 ±0)` + // Estimated: `2766 + p * (2709 ±0)` + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(9_300_991, 2766) + // Standard Error: 6_483 + .saturating_add(Weight::from_parts(2_726_847, 0).saturating_mul(p.into())) + .saturating_add(ParityDbWeight::get().reads(3_u64)) + .saturating_add(ParityDbWeight::get().reads((1_u64).saturating_mul(p.into()))) + .saturating_add(ParityDbWeight::get().writes(1_u64)) + .saturating_add(ParityDbWeight::get().writes((1_u64).saturating_mul(p.into()))) + .saturating_add(Weight::from_parts(0, 2709).saturating_mul(p.into())) + } + /// Storage: `Governance::AllowedProposers` (r:1 w:0) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:1 w:1) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:0) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalCount` (r:1 w:1) + /// Proof: `Governance::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Preimage::StatusFor` (r:1 w:0) + /// Proof: `Preimage::StatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Preimage::RequestStatusFor` (r:1 w:1) + /// Proof: `Preimage::RequestStatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:1) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Preimage::PreimageFor` (r:0 w:1) + /// Proof: `Preimage::PreimageFor` (`max_values`: None, `max_size`: Some(4194344), added: 4196819, mode: `MaxEncodedLen`) + fn propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `166` + // Estimated: `3628` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(28_000_000, 3628) + .saturating_add(ParityDbWeight::get().reads(7_u64)) + .saturating_add(ParityDbWeight::get().writes(6_u64)) + } + /// Storage: `Governance::Triumvirate` (r:1 w:0) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(1281), added: 1776, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:1 w:1) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:1) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:1 w:1) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Lookup` (r:1 w:1) + /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Agenda` (r:1 w:1) + /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10463), added: 12938, mode: `MaxEncodedLen`) + /// Storage: `Governance::CollectiveVoting` (r:0 w:1) + /// Proof: `Governance::CollectiveVoting` (`max_values`: None, `max_size`: Some(2094), added: 4569, mode: `MaxEncodedLen`) + fn vote_on_proposed() -> Weight { + // Proof Size summary in bytes: + // Measured: `512` + // Estimated: `13928` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 13928) + .saturating_add(ParityDbWeight::get().reads(7_u64)) + .saturating_add(ParityDbWeight::get().writes(7_u64)) + } + /// Storage: `Governance::EconomicCollective` (r:1 w:0) + /// Proof: `Governance::EconomicCollective` (`max_values`: Some(1), `max_size`: Some(513), added: 1008, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:0) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(641), added: 1136, mode: `MaxEncodedLen`) + /// Storage: `Governance::CollectiveVoting` (r:1 w:1) + /// Proof: `Governance::CollectiveVoting` (`max_values`: None, `max_size`: Some(2094), added: 4569, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Lookup` (r:1 w:1) + /// Proof: `Scheduler::Lookup` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Scheduler::Agenda` (r:2 w:2) + /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(10463), added: 12938, mode: `MaxEncodedLen`) + fn vote_on_scheduled() -> Weight { + // Proof Size summary in bytes: + // Measured: `476` + // Estimated: `26866` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 26866) + .saturating_add(ParityDbWeight::get().reads(6_u64)) + .saturating_add(ParityDbWeight::get().writes(4_u64)) + } +} \ No newline at end of file diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index b744c9b771..fd4e436885 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -340,7 +340,6 @@ parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; pub const MaxScheduledPerBlock: u32 = 50; - pub const NoPreimagePostponement: Option = Some(10); } impl pallet_scheduler::Config for Test { diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 3bad4a275f..54762cede3 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -425,7 +425,6 @@ parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; pub const MaxScheduledPerBlock: u32 = 50; - pub const NoPreimagePostponement: Option = Some(10); } impl pallet_scheduler::Config for Test { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b3aced2160..d3b490657b 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -154,6 +154,8 @@ pallet-shield.workspace = true ethereum.workspace = true +pallet-governance.workspace = true + [dev-dependencies] frame-metadata.workspace = true sp-io.workspace = true @@ -197,6 +199,7 @@ std = [ "pallet-scheduler/std", "pallet-preimage/std", "pallet-commitments/std", + "pallet-governance/std", "precompile-utils/std", "sp-api/std", "sp-block-builder/std", @@ -308,6 +311,7 @@ runtime-benchmarks = [ "pallet-offences/runtime-benchmarks", "sp-staking/runtime-benchmarks", "pallet-contracts/runtime-benchmarks", + "pallet-governance/runtime-benchmarks", # EVM + Frontier "pallet-ethereum/runtime-benchmarks", @@ -357,6 +361,7 @@ try-runtime = [ "pallet-fast-unstake/try-runtime", "pallet-nomination-pools/try-runtime", "pallet-offences/try-runtime", + "pallet-governance/try-runtime", # EVM + Frontier "fp-self-contained/try-runtime", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 225c2e2706..3d82dfbbdb 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -28,6 +28,7 @@ use frame_support::{ }; use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned}; use pallet_commitments::{CanCommit, OnMetadataCommitment}; +use pallet_governance::{BUILDING_COLLECTIVE_SIZE, ECONOMIC_COLLECTIVE_SIZE}; use pallet_grandpa::{AuthorityId as GrandpaId, fg_primitives}; use pallet_registry::CanRegisterIdentity; pub use pallet_shield; @@ -56,7 +57,8 @@ use sp_core::{ use sp_runtime::Cow; use sp_runtime::generic::Era; use sp_runtime::{ - AccountId32, ApplyExtrinsicResult, ConsensusEngineId, Percent, generic, impl_opaque_keys, + AccountId32, ApplyExtrinsicResult, ConsensusEngineId, FixedU128, Percent, generic, + impl_opaque_keys, traits::{ AccountIdLookup, BlakeTwo256, Block as BlockT, DispatchInfoOf, Dispatchable, One, PostDispatchInfoOf, UniqueSaturatedInto, Verify, @@ -818,7 +820,6 @@ parameter_types! { pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; pub const MaxScheduledPerBlock: u32 = 50; - pub const NoPreimagePostponement: Option = Some(10); } /// Used the compare the privilege of an origin inside the scheduler. @@ -1043,7 +1044,6 @@ parameter_types! { pub const SubtensorInitialMinAllowedUids: u16 = 64; pub const SubtensorInitialMinLockCost: u64 = 1_000_000_000_000; // 1000 TAO pub const SubtensorInitialSubnetOwnerCut: u16 = 11_796; // 18 percent - // pub const SubtensorInitialSubnetLimit: u16 = 12; // (DEPRECATED) pub const SubtensorInitialNetworkLockReductionInterval: u64 = 14 * 7200; pub const SubtensorInitialNetworkRateLimit: u64 = 7200; pub const SubtensorInitialKeySwapCost: u64 = 100_000_000; // 0.1 TAO @@ -1051,13 +1051,11 @@ parameter_types! { pub const InitialAlphaLow: u16 = 45875; // Represents 0.7 as per the production default pub const InitialLiquidAlphaOn: bool = false; // Default value for LiquidAlphaOn pub const InitialYuma3On: bool = false; // Default value for Yuma3On - // pub const SubtensorInitialNetworkMaxStake: u64 = u64::MAX; // (DEPRECATED) pub const InitialColdkeySwapScheduleDuration: BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days pub const InitialColdkeySwapRescheduleDuration: BlockNumber = 24 * 60 * 60 / 12; // 1 day pub const InitialDissolveNetworkScheduleDuration: BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days pub const SubtensorInitialTaoWeight: u64 = 971_718_665_099_567_868; // 0.05267697438728329% tao weight. pub const InitialEmaPriceHalvingPeriod: u64 = 201_600_u64; // 4 weeks - // 0 days pub const InitialStartCallDelay: u64 = 0; pub const SubtensorInitialKeySwapOnSubnetCost: u64 = 1_000_000; // 0.001 TAO pub const HotkeySwapOnSubnetInterval : BlockNumber = 5 * 24 * 60 * 60 / 12; // 5 days @@ -1604,6 +1602,60 @@ impl pallet_contracts::Config for Runtime { type ApiVersion = (); } +parameter_types! { + pub const MaxAllowedProposers: u32 = 20; + pub MaxProposalWeight: Weight = Perbill::from_percent(20) * BlockWeights::get().max_block; + pub const MaxProposals: u32 = 20; + pub const MaxScheduled: u32 = 20; + pub const MotionDuration: BlockNumber = prod_or_fast!(50_400, 50); // 7 days + pub const InitialSchedulingDelay: BlockNumber = prod_or_fast!(300, 30); // 1 hour + pub const AdditionalDelayFactor: FixedU128 = FixedU128::from_rational(3, 2); // 1.5 + pub const CollectiveRotationPeriod: BlockNumber = prod_or_fast!(432_000, 100); // 60 days + pub const CleanupPeriod: BlockNumber = prod_or_fast!(21_600, 50); // 3 days + pub const FastTrackThreshold: Percent = Percent::from_percent(67); + pub const CancellationThreshold: Percent = Percent::from_percent(51); +} + +impl pallet_governance::Config for Runtime { + type RuntimeCall = RuntimeCall; + type WeightInfo = pallet_governance::weights::SubstrateWeight; + type Currency = Balances; + type Preimages = Preimage; + type Scheduler = Scheduler; + type SetAllowedProposersOrigin = EnsureRoot; + type SetTriumvirateOrigin = EnsureRoot; + type CollectiveMembersProvider = CollectiveMembersProvider; + type MaxAllowedProposers = MaxAllowedProposers; + type MaxProposalWeight = MaxProposalWeight; + type MaxProposals = MaxProposals; + type MaxScheduled = MaxScheduled; + type MotionDuration = MotionDuration; + type InitialSchedulingDelay = InitialSchedulingDelay; + type AdditionalDelayFactor = AdditionalDelayFactor; + type CollectiveRotationPeriod = CollectiveRotationPeriod; + type CleanupPeriod = CleanupPeriod; + type CancellationThreshold = CancellationThreshold; + type FastTrackThreshold = FastTrackThreshold; +} + +pub struct CollectiveMembersProvider; + +impl pallet_governance::CollectiveMembersProvider for CollectiveMembersProvider { + fn get_economic_collective() -> ( + BoundedVec>, + Weight, + ) { + (BoundedVec::new(), Weight::zero()) + } + + fn get_building_collective() -> ( + BoundedVec>, + Weight, + ) { + (BoundedVec::new(), Weight::zero()) + } +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub struct Runtime @@ -1642,6 +1694,7 @@ construct_runtime!( Swap: pallet_subtensor_swap = 28, Contracts: pallet_contracts = 29, MevShield: pallet_shield = 30, + Governance: pallet_governance = 31, } ); @@ -1715,6 +1768,7 @@ mod benches { [pallet_crowdloan, Crowdloan] [pallet_subtensor_swap, Swap] [pallet_shield, MevShield] + [pallet_governance, Governance] ); } diff --git a/weights.rs b/weights.rs new file mode 100644 index 0000000000..ec947ed563 --- /dev/null +++ b/weights.rs @@ -0,0 +1,156 @@ + +//! Autogenerated weights for `pallet_governance` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 52.0.0 +//! DATE: 2025-12-17, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Loriss-MacBook-Air.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// ./target/debug/wbuild/node-subtensor-runtime/node_subtensor_runtime.wasm +// --pallet +// pallet_governance +// --extrinsic +// * +// --template +// ./.maintain/frame-weight-template.hbs +// --output +// weights.rs +// --genesis-builder-preset=benchmark +// --genesis-builder=runtime +// --allow-missing-host-functions + +#![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_governance`. +pub trait WeightInfo { + fn set_allowed_proposers(k: u32, p: u32, ) -> Weight; + fn propose() -> Weight; +} + +/// Weights for `pallet_governance` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Governance::Triumvirate` (r:1 w:0) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::AllowedProposers` (r:1 w:1) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(161), added: 656, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(321), added: 816, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:5) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:0 w:5) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// The range of component `k` is `[1, 5]`. + /// The range of component `p` is `[1, 5]`. + fn set_allowed_proposers(k: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `187 + k * (32 ±0) + p * (64 ±0)` + // Estimated: `1806` + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(8_376_985, 1806) + // Standard Error: 71_457 + .saturating_add(Weight::from_parts(376_464, 0).saturating_mul(k.into())) + // Standard Error: 71_457 + .saturating_add(Weight::from_parts(2_818_219, 0).saturating_mul(p.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(p.into()))) + } + /// Storage: `Governance::AllowedProposers` (r:1 w:0) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(161), added: 656, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:1 w:1) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:0) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(321), added: 816, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(321), added: 816, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalCount` (r:1 w:1) + /// Proof: `Governance::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Preimage::StatusFor` (r:1 w:0) + /// Proof: `Preimage::StatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Preimage::RequestStatusFor` (r:1 w:1) + /// Proof: `Preimage::RequestStatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:1) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Preimage::PreimageFor` (r:0 w:1) + /// Proof: `Preimage::PreimageFor` (`max_values`: None, `max_size`: Some(4194344), added: 4196819, mode: `MaxEncodedLen`) + fn propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `166` + // Estimated: `3628` + // Minimum execution time: 35_000_000 picoseconds. + Weight::from_parts(38_000_000, 3628) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Governance::Triumvirate` (r:1 w:0) + /// Proof: `Governance::Triumvirate` (`max_values`: Some(1), `max_size`: Some(97), added: 592, mode: `MaxEncodedLen`) + /// Storage: `Governance::AllowedProposers` (r:1 w:1) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(161), added: 656, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(321), added: 816, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:5) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:0 w:5) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// The range of component `k` is `[1, 5]`. + /// The range of component `p` is `[1, 5]`. + fn set_allowed_proposers(k: u32, p: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `187 + k * (32 ±0) + p * (64 ±0)` + // Estimated: `1806` + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(8_376_985, 1806) + // Standard Error: 71_457 + .saturating_add(Weight::from_parts(376_464, 0).saturating_mul(k.into())) + // Standard Error: 71_457 + .saturating_add(Weight::from_parts(2_818_219, 0).saturating_mul(p.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(p.into()))) + } + /// Storage: `Governance::AllowedProposers` (r:1 w:0) + /// Proof: `Governance::AllowedProposers` (`max_values`: Some(1), `max_size`: Some(161), added: 656, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalOf` (r:1 w:1) + /// Proof: `Governance::ProposalOf` (`max_values`: None, `max_size`: Some(163), added: 2638, mode: `MaxEncodedLen`) + /// Storage: `Governance::Scheduled` (r:1 w:0) + /// Proof: `Governance::Scheduled` (`max_values`: Some(1), `max_size`: Some(321), added: 816, mode: `MaxEncodedLen`) + /// Storage: `Governance::Proposals` (r:1 w:1) + /// Proof: `Governance::Proposals` (`max_values`: Some(1), `max_size`: Some(321), added: 816, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalCount` (r:1 w:1) + /// Proof: `Governance::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Preimage::StatusFor` (r:1 w:0) + /// Proof: `Preimage::StatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Preimage::RequestStatusFor` (r:1 w:1) + /// Proof: `Preimage::RequestStatusFor` (`max_values`: None, `max_size`: Some(83), added: 2558, mode: `MaxEncodedLen`) + /// Storage: `Governance::TriumvirateVoting` (r:0 w:1) + /// Proof: `Governance::TriumvirateVoting` (`max_values`: None, `max_size`: Some(234), added: 2709, mode: `MaxEncodedLen`) + /// Storage: `Preimage::PreimageFor` (r:0 w:1) + /// Proof: `Preimage::PreimageFor` (`max_values`: None, `max_size`: Some(4194344), added: 4196819, mode: `MaxEncodedLen`) + fn propose() -> Weight { + // Proof Size summary in bytes: + // Measured: `166` + // Estimated: `3628` + // Minimum execution time: 35_000_000 picoseconds. + Weight::from_parts(38_000_000, 3628) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } +} \ No newline at end of file