diff --git a/Cargo.lock b/Cargo.lock index 42accdb455cef..3b581bf9d0ff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4741,6 +4741,7 @@ dependencies = [ "hex-literal", "log", "node-primitives", + "pallet-alliance", "pallet-asset-tx-payment", "pallet-assets", "pallet-authority-discovery", @@ -5132,6 +5133,28 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "pallet-alliance" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "hex", + "hex-literal", + "log", + "pallet-balances", + "pallet-collective", + "pallet-identity", + "parity-scale-codec", + "scale-info", + "sha2 0.10.2", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-asset-tx-payment" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 74e7aae7949c5..7f013c08a9144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ members = [ "client/transaction-pool", "client/transaction-pool/api", "client/utils", + "frame/alliance", "frame/assets", "frame/atomic-swap", "frame/aura", diff --git a/bin/node/cli/src/chain_spec.rs b/bin/node/cli/src/chain_spec.rs index 221c229876275..1c7794fd0cde6 100644 --- a/bin/node/cli/src/chain_spec.rs +++ b/bin/node/cli/src/chain_spec.rs @@ -363,6 +363,8 @@ pub fn testnet_genesis( gilt: Default::default(), transaction_storage: Default::default(), transaction_payment: Default::default(), + alliance: Default::default(), + alliance_motion: Default::default(), nomination_pools: Default::default(), } } diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 5ecd6ccedaf01..ca971f29e93c9 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -50,6 +50,7 @@ frame-system-benchmarking = { version = "4.0.0-dev", default-features = false, p frame-election-provider-support = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-support" } frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../../frame/system/rpc/runtime-api/" } frame-try-runtime = { version = "0.10.0-dev", default-features = false, path = "../../../frame/try-runtime", optional = true } +pallet-alliance = { version = "4.0.0-dev", default-features = false, path = "../../../frame/alliance" } pallet-assets = { version = "4.0.0-dev", default-features = false, path = "../../../frame/assets" } pallet-authority-discovery = { version = "4.0.0-dev", default-features = false, path = "../../../frame/authority-discovery" } pallet-authorship = { version = "4.0.0-dev", default-features = false, path = "../../../frame/authorship" } @@ -187,12 +188,14 @@ std = [ "frame-try-runtime/std", "sp-io/std", "pallet-child-bounties/std", + "pallet-alliance/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "pallet-alliance/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -243,6 +246,7 @@ try-runtime = [ "frame-executive/try-runtime", "frame-try-runtime", "frame-system/try-runtime", + "pallet-alliance/try-runtime", "pallet-assets/try-runtime", "pallet-authority-discovery/try-runtime", "pallet-authorship/try-runtime", diff --git a/bin/node/runtime/src/impls.rs b/bin/node/runtime/src/impls.rs index 4973aed4bd4fc..68c780094208f 100644 --- a/bin/node/runtime/src/impls.rs +++ b/bin/node/runtime/src/impls.rs @@ -17,12 +17,19 @@ //! Some configurable implementations as associated type for the substrate runtime. -use crate::{AccountId, Assets, Authorship, Balances, NegativeImbalance, Runtime}; -use frame_support::traits::{ - fungibles::{Balanced, CreditOf}, - Currency, OnUnbalanced, +use crate::{ + AccountId, AllianceMotion, Assets, Authorship, Balances, Call, Hash, NegativeImbalance, Runtime, }; +use frame_support::{ + pallet_prelude::*, + traits::{ + fungibles::{Balanced, CreditOf}, + Currency, OnUnbalanced, + }, +}; +use pallet_alliance::{IdentityVerifier, ProposalIndex, ProposalProvider}; use pallet_asset_tx_payment::HandleCredit; +use sp_std::prelude::*; pub struct Author; impl OnUnbalanced for Author { @@ -45,6 +52,68 @@ impl HandleCredit for CreditToBlockAuthor { } } +pub struct AllianceIdentityVerifier; +impl IdentityVerifier for AllianceIdentityVerifier { + fn has_identity(who: &AccountId, fields: u64) -> bool { + crate::Identity::has_identity(who, fields) + } + + fn has_good_judgement(who: &AccountId) -> bool { + use pallet_identity::Judgement; + if let Some(judgements) = + crate::Identity::identity(who).map(|registration| registration.judgements) + { + judgements + .iter() + .any(|(_, j)| matches!(j, Judgement::KnownGood | Judgement::Reasonable)) + } else { + false + } + } + + fn super_account_id(who: &AccountId) -> Option { + crate::Identity::super_of(who).map(|parent| parent.0) + } +} + +pub struct AllianceProposalProvider; +impl ProposalProvider for AllianceProposalProvider { + fn propose_proposal( + who: AccountId, + threshold: u32, + proposal: Box, + length_bound: u32, + ) -> Result<(u32, u32), DispatchError> { + AllianceMotion::do_propose_proposed(who, threshold, proposal, length_bound) + } + + fn vote_proposal( + who: AccountId, + proposal: Hash, + index: ProposalIndex, + approve: bool, + ) -> Result { + AllianceMotion::do_vote(who, proposal, index, approve) + } + + fn veto_proposal(proposal_hash: Hash) -> u32 { + AllianceMotion::do_disapprove_proposal(proposal_hash) + } + + fn close_proposal( + proposal_hash: Hash, + proposal_index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, + ) -> DispatchResultWithPostInfo { + AllianceMotion::do_close(proposal_hash, proposal_index, proposal_weight_bound, length_bound) + } + + fn proposal_of(proposal_hash: Hash) -> Option { + AllianceMotion::proposal_of(proposal_hash) + } +} + #[cfg(test)] mod multiplier_tests { use pallet_transaction_payment::{Multiplier, TargetedFeeAdjustment}; diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index b421a4f05ed1a..8e854b770a4ff 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -19,7 +19,7 @@ //! The Substrate runtime. This can be compiled with `#[no_std]`, ready for Wasm. #![cfg_attr(not(feature = "std"), no_std)] -// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. +// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 512. #![recursion_limit = "512"] use codec::{Decode, Encode, MaxEncodedLen}; @@ -90,7 +90,9 @@ pub use sp_runtime::BuildStorage; /// Implementations of some helper traits passed into runtime modules as associated types. pub mod impls; -use impls::{Author, CreditToBlockAuthor}; +#[cfg(not(feature = "runtime-benchmarks"))] +use impls::AllianceIdentityVerifier; +use impls::{AllianceProposalProvider, Author, CreditToBlockAuthor}; /// Constant values used within the runtime. pub mod constants; @@ -1506,6 +1508,67 @@ impl pallet_state_trie_migration::Config for Runtime { type WeightInfo = (); } +parameter_types! { + pub const AllianceMotionDuration: BlockNumber = 5 * DAYS; + pub const AllianceMaxProposals: u32 = 100; + pub const AllianceMaxMembers: u32 = 100; +} + +type AllianceCollective = pallet_collective::Instance3; +impl pallet_collective::Config for Runtime { + type Origin = Origin; + type Proposal = Call; + type Event = Event; + type MotionDuration = AllianceMotionDuration; + type MaxProposals = AllianceMaxProposals; + type MaxMembers = AllianceMaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; +} + +parameter_types! { + pub const MaxFounders: u32 = 10; + pub const MaxFellows: u32 = AllianceMaxMembers::get() - MaxFounders::get(); + pub const MaxAllies: u32 = 100; + pub const AllyDeposit: Balance = 10 * DOLLARS; +} + +impl pallet_alliance::Config for Runtime { + type Event = Event; + type Proposal = Call; + type AdminOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionMoreThan, + >; + type MembershipManager = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionMoreThan, + >; + type AnnouncementOrigin = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionMoreThan, + >; + type Currency = Balances; + type Slashed = Treasury; + type InitializeMembers = AllianceMotion; + type MembershipChanged = AllianceMotion; + #[cfg(not(feature = "runtime-benchmarks"))] + type IdentityVerifier = AllianceIdentityVerifier; + #[cfg(feature = "runtime-benchmarks")] + type IdentityVerifier = (); + type ProposalProvider = AllianceProposalProvider; + type MaxProposals = AllianceMaxProposals; + type MaxFounders = MaxFounders; + type MaxFellows = MaxFellows; + type MaxAllies = MaxAllies; + type MaxUnscrupulousItems = ConstU32<100>; + type MaxWebsiteUrlLength = ConstU32<255>; + type MaxAnnouncementsCount = ConstU32<100>; + type MaxMembersCount = AllianceMaxMembers; + type AllyDeposit = AllyDeposit; + type WeightInfo = pallet_alliance::weights::SubstrateWeight; +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -1563,6 +1626,8 @@ construct_runtime!( Remark: pallet_remark, ConvictionVoting: pallet_conviction_voting, Whitelist: pallet_whitelist, + AllianceMotion: pallet_collective::, + Alliance: pallet_alliance, NominationPools: pallet_nomination_pools, RankedPolls: pallet_referenda::, RankedCollective: pallet_ranked_collective, @@ -2049,7 +2114,6 @@ impl_runtime_apis! { let mut batches = Vec::::new(); let params = (&config, &whitelist); add_benchmarks!(params, batches); - Ok(batches) } } diff --git a/bin/node/testing/src/genesis.rs b/bin/node/testing/src/genesis.rs index 73e7ea50261d3..fbd28c5af0298 100644 --- a/bin/node/testing/src/genesis.rs +++ b/bin/node/testing/src/genesis.rs @@ -92,6 +92,8 @@ pub fn config_endowed(code: Option<&[u8]>, extra_endowed: Vec) -> Gen gilt: Default::default(), transaction_storage: Default::default(), transaction_payment: Default::default(), + alliance: Default::default(), + alliance_motion: Default::default(), nomination_pools: Default::default(), } } diff --git a/frame/alliance/Cargo.toml b/frame/alliance/Cargo.toml new file mode 100644 index 0000000000000..706827708ce88 --- /dev/null +++ b/frame/alliance/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "pallet-alliance" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://docs.substrate.io/" +repository = "https://github.com/paritytech/substrate/" +description = "The Alliance pallet provides a collective for standard-setting industry collaboration." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +hex = { version = "0.4", default-features = false, features = ["alloc"], optional = true } +sha2 = { version = "0.10.1", default-features = false, optional = true } +log = { version = "0.4.14", default-features = false } + +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } + +sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } +sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" } +sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" } + +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } + +pallet-identity = { version = "4.0.0-dev", path = "../identity", default-features = false } +pallet-collective = { version = "4.0.0-dev", path = "../collective", default-features = false, optional = true } + +[dev-dependencies] +hex-literal = "0.3.1" +sha2 = "0.10.1" +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-collective = { version = "4.0.0-dev", path = "../collective" } + +[features] +default = ["std"] +std = [ + "log/std", + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "pallet-identity/std", +] +runtime-benchmarks = [ + "hex", + "sha2", + "frame-benchmarking", + "sp-runtime/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-collective/runtime-benchmarks", + "pallet-identity/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/frame/alliance/README.md b/frame/alliance/README.md new file mode 100644 index 0000000000000..f0900c84cbd85 --- /dev/null +++ b/frame/alliance/README.md @@ -0,0 +1,68 @@ +# Alliance Pallet + +The Alliance Pallet provides a collective that curates a list of accounts and URLs, deemed by +the voting members to be unscrupulous actors. The alliance + +- provides a set of ethics against bad behavior, and +- provides recognition and influence for those teams that contribute something back to the + ecosystem. + +## Overview + +The network initializes the Alliance via a Root call. After that, anyone with an approved +identity and website can join as an Ally. The `MembershipManager` origin can elevate Allies to +Fellows, giving them voting rights within the Alliance. + +Voting members of the Alliance maintain a list of accounts and websites. Members can also vote +to update the Alliance's rule and make announcements. + +### Terminology + +- Rule: The IPFS CID (hash) of the Alliance rules for the community to read and the Alliance + members to enforce. Similar to a Code of Conduct. +- Announcement: An IPFS CID of some content that the Alliance want to announce. +- Member: An account that is already in the group of the Alliance, including three types: + Founder, Fellow, or Ally. A member can also be kicked by the `MembershipManager` origin or + retire by itself. +- Founder: An account who is initiated by Root with normal voting rights for basic motions and + special veto rights for rule change and Ally elevation motions. +- Fellow: An account who is elevated from Ally by Founders and other Fellows. +- Ally: An account who would like to join the alliance. To become a voting member, Fellow or + Founder, it will need approval from the `MembershipManager` origin. Any account can join as an + Ally either by placing a deposit or by nomination from a voting member. +- Unscrupulous List: A list of bad websites and addresses, items can be added or removed by + Founders and Fellows. + +## Interface + +### Dispatchable Functions + +#### For General Users + +- `join_alliance` - Join the Alliance as an Ally. This requires a slashable deposit. + +#### For Members (All) + +- `retire` - Retire from the Alliance and release the caller's deposit. + +#### For Members (Founders/Fellows) + +- `propose` - Propose a motion. +- `vote` - Vote on a motion. +- `close` - Close a motion with enough votes or that has expired. +- `set_rule` - Initialize or update the Alliance's rule by IPFS CID. +- `announce` - Make announcement by IPFS CID. +- `nominate_ally` - Nominate a non-member to become an Ally, without deposit. +- `elevate_ally` - Approve an ally to become a Fellow. +- `kick_member` - Kick a member and slash its deposit. +- `add_unscrupulous_items` - Add some items, either accounts or websites, to the list of + unscrupulous items. +- `remove_unscrupulous_items` - Remove some items from the list of unscrupulous items. + +#### For Members (Only Founders) + +- `veto` - Veto on a motion about `set_rule` and `elevate_ally`. + +#### Root Calls + +- `init_founders` - Initialize the founding members. diff --git a/frame/alliance/src/benchmarking.rs b/frame/alliance/src/benchmarking.rs new file mode 100644 index 0000000000000..527c35b58a5d8 --- /dev/null +++ b/frame/alliance/src/benchmarking.rs @@ -0,0 +1,791 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Alliance pallet benchmarking. + +use sp_runtime::traits::{Bounded, Hash, StaticLookup}; +use sp_std::{ + convert::{TryFrom, TryInto}, + mem::size_of, + prelude::*, +}; + +use frame_benchmarking::{account, benchmarks_instance_pallet}; +use frame_support::traits::{EnsureOrigin, Get, UnfilteredDispatchable}; +use frame_system::{Pallet as System, RawOrigin as SystemOrigin}; + +use super::{Call as AllianceCall, Pallet as Alliance, *}; + +const SEED: u32 = 0; + +const MAX_BYTES: u32 = 1_024; + +fn assert_last_event, I: 'static>(generic_event: >::Event) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn cid(input: impl AsRef<[u8]>) -> Cid { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(input); + let result = hasher.finalize(); + Cid::new_v0(&*result) +} + +fn rule(input: impl AsRef<[u8]>) -> Cid { + cid(input) +} + +fn announcement(input: impl AsRef<[u8]>) -> Cid { + cid(input) +} + +fn funded_account, I: 'static>(name: &'static str, index: u32) -> T::AccountId { + let account: T::AccountId = account(name, index, SEED); + T::Currency::make_free_balance_be(&account, BalanceOf::::max_value() / 100u8.into()); + account +} + +fn founder, I: 'static>(index: u32) -> T::AccountId { + funded_account::("founder", index) +} + +fn fellow, I: 'static>(index: u32) -> T::AccountId { + funded_account::("fellow", index) +} + +fn ally, I: 'static>(index: u32) -> T::AccountId { + funded_account::("ally", index) +} + +fn outsider, I: 'static>(index: u32) -> T::AccountId { + funded_account::("outsider", index) +} + +fn generate_unscrupulous_account, I: 'static>(index: u32) -> T::AccountId { + funded_account::("unscrupulous", index) +} + +fn set_members, I: 'static>() { + let founders: BoundedVec<_, T::MaxMembersCount> = + BoundedVec::try_from(vec![founder::(1), founder::(2)]).unwrap(); + Members::::insert(MemberRole::Founder, founders.clone()); + + let fellows: BoundedVec<_, T::MaxMembersCount> = + BoundedVec::try_from(vec![fellow::(1), fellow::(2)]).unwrap(); + fellows.iter().for_each(|who| { + T::Currency::reserve(&who, T::AllyDeposit::get()).unwrap(); + >::insert(&who, T::AllyDeposit::get()); + }); + Members::::insert(MemberRole::Fellow, fellows.clone()); + + let allies: BoundedVec<_, T::MaxMembersCount> = + BoundedVec::try_from(vec![ally::(1)]).unwrap(); + allies.iter().for_each(|who| { + T::Currency::reserve(&who, T::AllyDeposit::get()).unwrap(); + >::insert(&who, T::AllyDeposit::get()); + }); + Members::::insert(MemberRole::Ally, allies); + + T::InitializeMembers::initialize_members(&[founders.as_slice(), fellows.as_slice()].concat()); +} + +benchmarks_instance_pallet! { + // This tests when proposal is created and queued as "proposed" + propose_proposed { + let b in 1 .. MAX_BYTES; + let x in 2 .. T::MaxFounders::get(); + let y in 0 .. T::MaxFellows::get(); + let p in 1 .. T::MaxProposals::get(); + + let m = x + y; + + let bytes_in_storage = b + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. x).map(founder::).collect::>(); + let proposer = founders[0].clone(); + let fellows = (0 .. y).map(fellow::).collect::>(); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, fellows, vec![])?; + + let threshold = m; + // Add previous proposals. + for i in 0 .. p - 1 { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; b as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal), + bytes_in_storage, + )?; + } + + let proposal: T::Proposal = AllianceCall::::set_rule { rule: rule(vec![p as u8; b as usize]) }.into(); + + }: propose(SystemOrigin::Signed(proposer.clone()), threshold, Box::new(proposal.clone()), bytes_in_storage) + verify { + // New proposal is recorded + let proposal_hash = T::Hashing::hash_of(&proposal); + assert_eq!(T::ProposalProvider::proposal_of(proposal_hash), Some(proposal)); + } + + vote { + // We choose 5 (3 founders + 2 fellows) as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let x in 3 .. T::MaxFounders::get(); + let y in 2 .. T::MaxFellows::get(); + + let m = x + y; + + let p = T::MaxProposals::get(); + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. x).map(founder::).collect::>(); + let proposer = founders[0].clone(); + let fellows = (0 .. y).map(fellow::).collect::>(); + + let mut members = Vec::with_capacity(founders.len() + fellows.len()); + members.extend(founders.clone()); + members.extend(fellows.clone()); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, fellows, vec![])?; + + // Threshold is 1 less than the number of members so that one person can vote nay + let threshold = m - 1; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; b as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + b, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have almost everyone vote aye on last proposal, while keeping it from passing. + for j in 0 .. m - 3 { + let voter = &members[j as usize]; + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + true, + )?; + } + + let voter = members[m as usize - 3].clone(); + // Voter votes aye without resolving the vote. + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + true, + )?; + + // Voter switches vote to nay, but does not kill the vote, just updates + inserts + let approve = false; + + // Whitelist voter account from further DB operations. + let voter_key = frame_system::Account::::hashed_key_for(&voter); + frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); + }: _(SystemOrigin::Signed(voter), last_hash.clone(), index, approve) + verify { + } + + veto { + let p in 1 .. T::MaxProposals::get(); + + let m = 3; + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. m).map(founder::).collect::>(); + let vetor = founders[0].clone(); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, vec![], vec![])?; + + // Threshold is one less than total members so that two nays will disapprove the vote + let threshold = m - 1; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; b as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(vetor.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + }: _(SystemOrigin::Signed(vetor), last_hash.clone()) + verify { + // The proposal is removed + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + } + + close_early_disapproved { + // We choose 4 (2 founders + 2 fellows) as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let x in 2 .. T::MaxFounders::get(); + let y in 2 .. T::MaxFellows::get(); + let p in 1 .. T::MaxProposals::get(); + + let m = x + y; + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. x).map(founder::).collect::>(); + let fellows = (0 .. y).map(fellow::).collect::>(); + + let mut members = Vec::with_capacity(founders.len() + fellows.len()); + members.extend(founders.clone()); + members.extend(fellows.clone()); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, fellows, vec![])?; + + let proposer = members[0].clone(); + let voter = members[1].clone(); + + // Threshold is total members so that one nay will disapprove the vote + let threshold = m; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; bytes as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), Some(proposal)); + } + + let index = p - 1; + // Have most everyone vote aye on last proposal, while keeping it from passing. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + true, + )?; + } + + // Voter votes aye without resolving the vote. + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + true, + )?; + + // Voter switches vote to nay, which kills the vote + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + false, + )?; + + // Whitelist voter account from further DB operations. + let voter_key = frame_system::Account::::hashed_key_for(&voter); + frame_benchmarking::benchmarking::add_to_whitelist(voter_key.into()); + }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::max_value(), bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + } + + close_early_approved { + let b in 1 .. MAX_BYTES; + // We choose 4 (2 founders + 2 fellows) as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let x in 2 .. T::MaxFounders::get(); + let y in 2 .. T::MaxFellows::get(); + let p in 1 .. T::MaxProposals::get(); + + let m = x + y; + let bytes_in_storage = b + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. x).map(founder::).collect::>(); + let fellows = (0 .. y).map(fellow::).collect::>(); + + let mut members = Vec::with_capacity(founders.len() + fellows.len()); + members.extend(founders.clone()); + members.extend(fellows.clone()); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, fellows, vec![])?; + + let proposer = members[0].clone(); + let voter = members[1].clone(); + + // Threshold is 2 so any two ayes will approve the vote + let threshold = 2; + + // Add previous proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; b as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), Some(proposal)); + } + + let index = p - 1; + // Caller switches vote to nay on their own proposal, allowing them to be the deciding approval vote + Alliance::::vote( + SystemOrigin::Signed(proposer.clone()).into(), + last_hash.clone(), + index, + false, + )?; + + // Have almost everyone vote nay on last proposal, while keeping it from failing. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + false, + )?; + } + + // Member zero is the first aye + Alliance::::vote( + SystemOrigin::Signed(members[0].clone()).into(), + last_hash.clone(), + index, + true, + )?; + + let voter = members[1].clone(); + // Caller switches vote to aye, which passes the vote + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + true, + )?; + }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::max_value(), bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + } + + close_disapproved { + // We choose 2 (2 founders / 2 fellows) as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let x in 2 .. T::MaxFounders::get(); + let y in 2 .. T::MaxFellows::get(); + let p in 1 .. T::MaxProposals::get(); + + let m = x + y; + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. x).map(founder::).collect::>(); + let fellows = (0 .. y).map(fellow::).collect::>(); + + let mut members = Vec::with_capacity(founders.len() + fellows.len()); + members.extend(founders.clone()); + members.extend(fellows.clone()); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, fellows, vec![])?; + + let proposer = members[0].clone(); + let voter = members[1].clone(); + + // Threshold is one less than total members so that two nays will disapprove the vote + let threshold = m - 1; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; bytes as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), Some(proposal)); + } + + let index = p - 1; + // Have almost everyone vote aye on last proposal, while keeping it from passing. + // A few abstainers will be the nay votes needed to fail the vote. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + true, + )?; + } + + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + false, + )?; + + System::::set_block_number(T::BlockNumber::max_value()); + + }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::max_value(), bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + } + + close_approved { + let b in 1 .. MAX_BYTES; + // We choose 4 (2 founders + 2 fellows) as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let x in 2 .. T::MaxFounders::get(); + let y in 2 .. T::MaxFellows::get(); + let p in 1 .. T::MaxProposals::get(); + + let m = x + y; + let bytes_in_storage = b + size_of::() as u32 + 32; + + // Construct `members`. + let founders = (0 .. x).map(founder::).collect::>(); + let fellows = (0 .. y).map(fellow::).collect::>(); + + let mut members = Vec::with_capacity(founders.len() + fellows.len()); + members.extend(founders.clone()); + members.extend(fellows.clone()); + + Alliance::::init_members(SystemOrigin::Root.into(), founders, fellows, vec![])?; + + let proposer = members[0].clone(); + let voter = members[1].clone(); + + // Threshold is two, so any two ayes will pass the vote + let threshold = 2; + + // Add proposals + let mut last_hash = T::Hash::default(); + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = AllianceCall::::set_rule { + rule: rule(vec![i as u8; b as usize]) + }.into(); + Alliance::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + assert_eq!(T::ProposalProvider::proposal_of(last_hash), Some(proposal)); + } + + // The prime member votes aye, so abstentions default to aye. + Alliance::::vote( + SystemOrigin::Signed(proposer.clone()).into(), + last_hash.clone(), + p - 1, + true // Vote aye. + )?; + + let index = p - 1; + // Have almost everyone vote nay on last proposal, while keeping it from failing. + // A few abstainers will be the aye votes needed to pass the vote. + for j in 2 .. m - 1 { + let voter = &members[j as usize]; + Alliance::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash.clone(), + index, + false + )?; + } + + // caller is prime, prime already votes aye by creating the proposal + System::::set_block_number(T::BlockNumber::max_value()); + + }: close(SystemOrigin::Signed(voter), last_hash.clone(), index, Weight::max_value(), bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(T::ProposalProvider::proposal_of(last_hash), None); + } + + init_members { + // at least 2 founders + let x in 2 .. T::MaxFounders::get(); + let y in 0 .. T::MaxFellows::get(); + let z in 0 .. T::MaxAllies::get(); + + let mut founders = (2 .. x).map(founder::).collect::>(); + let mut fellows = (0 .. y).map(fellow::).collect::>(); + let mut allies = (0 .. z).map(ally::).collect::>(); + + }: _(SystemOrigin::Root, founders.clone(), fellows.clone(), allies.clone()) + verify { + founders.sort(); + fellows.sort(); + allies.sort(); + assert_last_event::(Event::MembersInitialized { + founders: founders.clone(), + fellows: fellows.clone(), + allies: allies.clone(), + }.into()); + assert_eq!(Alliance::::members(MemberRole::Founder), founders); + assert_eq!(Alliance::::members(MemberRole::Fellow), fellows); + assert_eq!(Alliance::::members(MemberRole::Ally), allies); + } + + set_rule { + set_members::(); + + let rule = rule(b"hello world"); + + let call = Call::::set_rule { rule: rule.clone() }; + let origin = T::AdminOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_eq!(Alliance::::rule(), Some(rule.clone())); + assert_last_event::(Event::NewRuleSet { rule }.into()); + } + + announce { + set_members::(); + + let announcement = announcement(b"hello world"); + + let call = Call::::announce { announcement: announcement.clone() }; + let origin = T::AnnouncementOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(Alliance::::announcements().contains(&announcement)); + assert_last_event::(Event::Announced { announcement }.into()); + } + + remove_announcement { + set_members::(); + + let announcement = announcement(b"hello world"); + let announcements: BoundedVec<_, T::MaxAnnouncementsCount> = BoundedVec::try_from(vec![announcement.clone()]).unwrap(); + Announcements::::put(announcements); + + let call = Call::::remove_announcement { announcement: announcement.clone() }; + let origin = T::AnnouncementOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(Alliance::::announcements().is_empty()); + assert_last_event::(Event::AnnouncementRemoved { announcement }.into()); + } + + join_alliance { + set_members::(); + + let outsider = outsider::(1); + assert!(!Alliance::::is_member(&outsider)); + assert_eq!(DepositOf::::get(&outsider), None); + }: _(SystemOrigin::Signed(outsider.clone())) + verify { + assert!(Alliance::::is_member_of(&outsider, MemberRole::Ally)); // outsider is now an ally + assert_eq!(DepositOf::::get(&outsider), Some(T::AllyDeposit::get())); // with a deposit + assert!(!Alliance::::has_voting_rights(&outsider)); // allies don't have voting rights + assert_last_event::(Event::NewAllyJoined { + ally: outsider, + nominator: None, + reserved: Some(T::AllyDeposit::get()) + }.into()); + } + + nominate_ally { + set_members::(); + + let founder1 = founder::(1); + assert!(Alliance::::is_member_of(&founder1, MemberRole::Founder)); + + let outsider = outsider::(1); + assert!(!Alliance::::is_member(&outsider)); + assert_eq!(DepositOf::::get(&outsider), None); + + let outsider_lookup: ::Source = T::Lookup::unlookup(outsider.clone()); + }: _(SystemOrigin::Signed(founder1.clone()), outsider_lookup) + verify { + assert!(Alliance::::is_member_of(&outsider, MemberRole::Ally)); // outsider is now an ally + assert_eq!(DepositOf::::get(&outsider), None); // without a deposit + assert!(!Alliance::::has_voting_rights(&outsider)); // allies don't have voting rights + assert_last_event::(Event::NewAllyJoined { + ally: outsider, + nominator: Some(founder1), + reserved: None + }.into()); + } + + elevate_ally { + set_members::(); + + let ally1 = ally::(1); + assert!(Alliance::::is_ally(&ally1)); + + let ally1_lookup: ::Source = T::Lookup::unlookup(ally1.clone()); + let call = Call::::elevate_ally { ally: ally1_lookup }; + let origin = T::MembershipManager::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(!Alliance::::is_ally(&ally1)); + assert!(Alliance::::is_fellow(&ally1)); + assert_last_event::(Event::AllyElevated { ally: ally1 }.into()); + } + + retire { + set_members::(); + + let fellow2 = fellow::(2); + assert!(Alliance::::is_fellow(&fellow2)); + assert!(!Alliance::::is_up_for_kicking(&fellow2)); + + assert_eq!(DepositOf::::get(&fellow2), Some(T::AllyDeposit::get())); + }: _(SystemOrigin::Signed(fellow2.clone())) + verify { + assert!(!Alliance::::is_member(&fellow2)); + assert_eq!(DepositOf::::get(&fellow2), None); + assert_last_event::(Event::MemberRetired { + member: fellow2, + unreserved: Some(T::AllyDeposit::get()) + }.into()); + } + + kick_member { + set_members::(); + + let fellow2 = fellow::(2); + UpForKicking::::insert(&fellow2, true); + + assert!(Alliance::::is_member_of(&fellow2, MemberRole::Fellow)); + assert!(Alliance::::is_up_for_kicking(&fellow2)); + + assert_eq!(DepositOf::::get(&fellow2), Some(T::AllyDeposit::get())); + + let fellow2_lookup: ::Source = T::Lookup::unlookup(fellow2.clone()); + let call = Call::::kick_member { who: fellow2_lookup }; + let origin = T::MembershipManager::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(!Alliance::::is_member(&fellow2)); + assert_eq!(DepositOf::::get(&fellow2), None); + assert_last_event::(Event::MemberKicked { + member: fellow2, + slashed: Some(T::AllyDeposit::get()) + }.into()); + } + + add_unscrupulous_items { + let n in 1 .. T::MaxUnscrupulousItems::get(); + let l in 1 .. T::MaxWebsiteUrlLength::get(); + + set_members::(); + + let accounts = (0 .. n) + .map(|i| generate_unscrupulous_account::(i)) + .collect::>(); + let websites = (0 .. n).map(|i| -> BoundedVec { + BoundedVec::try_from(vec![i as u8; l as usize]).unwrap() + }).collect::>(); + + let mut unscrupulous_list = Vec::with_capacity(accounts.len() + websites.len()); + unscrupulous_list.extend(accounts.into_iter().map(UnscrupulousItem::AccountId)); + unscrupulous_list.extend(websites.into_iter().map(UnscrupulousItem::Website)); + + let call = Call::::add_unscrupulous_items { items: unscrupulous_list.clone() }; + let origin = T::AnnouncementOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::UnscrupulousItemAdded { items: unscrupulous_list }.into()); + } + + remove_unscrupulous_items { + let n in 1 .. T::MaxUnscrupulousItems::get(); + let l in 1 .. T::MaxWebsiteUrlLength::get(); + + set_members::(); + + let mut accounts = (0 .. n) + .map(|i| generate_unscrupulous_account::(i)) + .collect::>(); + accounts.sort(); + let accounts: BoundedVec<_, T::MaxUnscrupulousItems> = accounts.try_into().unwrap(); + UnscrupulousAccounts::::put(accounts.clone()); + + let mut websites = (0 .. n).map(|i| -> BoundedVec<_, T::MaxWebsiteUrlLength> + { BoundedVec::try_from(vec![i as u8; l as usize]).unwrap() }).collect::>(); + websites.sort(); + let websites: BoundedVec<_, T::MaxUnscrupulousItems> = websites.try_into().unwrap(); + UnscrupulousWebsites::::put(websites.clone()); + + let mut unscrupulous_list = Vec::with_capacity(accounts.len() + websites.len()); + unscrupulous_list.extend(accounts.into_iter().map(UnscrupulousItem::AccountId)); + unscrupulous_list.extend(websites.into_iter().map(UnscrupulousItem::Website)); + + let call = Call::::remove_unscrupulous_items { items: unscrupulous_list.clone() }; + let origin = T::AnnouncementOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::UnscrupulousItemRemoved { items: unscrupulous_list }.into()); + } + + impl_benchmark_test_suite!(Alliance, crate::mock::new_bench_ext(), crate::mock::Test); +} diff --git a/frame/alliance/src/lib.rs b/frame/alliance/src/lib.rs new file mode 100644 index 0000000000000..f9e85e270af16 --- /dev/null +++ b/frame/alliance/src/lib.rs @@ -0,0 +1,1053 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Alliance Pallet +//! +//! The Alliance Pallet provides a collective that curates a list of accounts and URLs, deemed by +//! the voting members to be unscrupulous actors. The alliance +//! +//! - provides a set of ethics against bad behavior, and +//! - provides recognition and influence for those teams that contribute something back to the +//! ecosystem. +//! +//! ## Overview +//! +//! The network initializes the Alliance via a Root call. After that, anyone with an approved +//! identity and website can join as an Ally. The `MembershipManager` origin can elevate Allies to +//! Fellows, giving them voting rights within the Alliance. +//! +//! Voting members of the Alliance maintain a list of accounts and websites. Members can also vote +//! to update the Alliance's rule and make announcements. +//! +//! ### Terminology +//! +//! - Rule: The IPFS CID (hash) of the Alliance rules for the community to read and the Alliance +//! members to enforce. Similar to a Code of Conduct. +//! - Announcement: An IPFS CID of some content that the Alliance want to announce. +//! - Member: An account that is already in the group of the Alliance, including three types: +//! Founder, Fellow, or Ally. A member can also be kicked by the `MembershipManager` origin or +//! retire by itself. +//! - Founder: An account who is initiated by Root with normal voting rights for basic motions and +//! special veto rights for rule change and Ally elevation motions. +//! - Fellow: An account who is elevated from Ally by Founders and other Fellows. +//! - Ally: An account who would like to join the alliance. To become a voting member, Fellow or +//! Founder, it will need approval from the `MembershipManager` origin. Any account can join as an +//! Ally either by placing a deposit or by nomination from a voting member. +//! - Unscrupulous List: A list of bad websites and addresses, items can be added or removed by +//! Founders and Fellows. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! #### For General Users +//! +//! - `join_alliance` - Join the Alliance as an Ally. This requires a slashable deposit. +//! +//! #### For Members (All) +//! +//! - `retire` - Retire from the Alliance and release the caller's deposit. +//! +//! #### For Members (Founders/Fellows) +//! +//! - `propose` - Propose a motion. +//! - `vote` - Vote on a motion. +//! - `close` - Close a motion with enough votes or that has expired. +//! - `set_rule` - Initialize or update the Alliance's rule by IPFS CID. +//! - `announce` - Make announcement by IPFS CID. +//! - `nominate_ally` - Nominate a non-member to become an Ally, without deposit. +//! - `elevate_ally` - Approve an ally to become a Fellow. +//! - `kick_member` - Kick a member and slash its deposit. +//! - `add_unscrupulous_items` - Add some items, either accounts or websites, to the list of +//! unscrupulous items. +//! - `remove_unscrupulous_items` - Remove some items from the list of unscrupulous items. +//! +//! #### For Members (Only Founders) +//! +//! - `veto` - Veto on a motion about `set_rule` and `elevate_ally`. +//! +//! #### Root Calls +//! +//! - `init_founders` - Initialize the founding members. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +mod types; +pub mod weights; + +use frame_support::pallet_prelude::*; +use frame_system::pallet_prelude::*; +use sp_runtime::{ + traits::{StaticLookup, Zero}, + RuntimeDebug, +}; +use sp_std::{convert::TryInto, prelude::*}; + +use frame_support::{ + codec::{Decode, Encode, MaxEncodedLen}, + dispatch::{ + DispatchError, DispatchResult, DispatchResultWithPostInfo, Dispatchable, GetDispatchInfo, + PostDispatchInfo, + }, + ensure, + scale_info::TypeInfo, + traits::{ + ChangeMembers, Currency, Get, InitializeMembers, IsSubType, OnUnbalanced, + ReservableCurrency, + }, + weights::Weight, +}; +use pallet_identity::IdentityField; + +pub use pallet::*; +pub use types::*; +pub use weights::*; + +/// Simple index type for proposal counting. +pub type ProposalIndex = u32; + +type UrlOf = BoundedVec>::MaxWebsiteUrlLength>; + +type BalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; +type NegativeImbalanceOf = <>::Currency as Currency< + ::AccountId, +>>::NegativeImbalance; + +/// Interface required for identity verification. +pub trait IdentityVerifier { + /// Function that returns whether an account has an identity registered with the identity + /// provider. + fn has_identity(who: &AccountId, fields: u64) -> bool; + + /// Whether an account has been deemed "good" by the provider. + fn has_good_judgement(who: &AccountId) -> bool; + + /// If the identity provider allows sub-accounts, provide the super of an account. Should + /// return `None` if the provider does not allow sub-accounts or if the account is not a sub. + fn super_account_id(who: &AccountId) -> Option; +} + +/// The non-provider. Imposes no restrictions on account identity. +impl IdentityVerifier for () { + fn has_identity(_who: &AccountId, _fields: u64) -> bool { + true + } + + fn has_good_judgement(_who: &AccountId) -> bool { + true + } + + fn super_account_id(_who: &AccountId) -> Option { + None + } +} + +/// The provider of a collective action interface, for example an instance of `pallet-collective`. +pub trait ProposalProvider { + fn propose_proposal( + who: AccountId, + threshold: u32, + proposal: Box, + length_bound: u32, + ) -> Result<(u32, u32), DispatchError>; + + fn vote_proposal( + who: AccountId, + proposal: Hash, + index: ProposalIndex, + approve: bool, + ) -> Result; + + fn veto_proposal(proposal_hash: Hash) -> u32; + + fn close_proposal( + proposal_hash: Hash, + index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, + ) -> DispatchResultWithPostInfo; + + fn proposal_of(proposal_hash: Hash) -> Option; +} + +/// The various roles that a member can hold. +#[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum MemberRole { + Founder, + Fellow, + Ally, +} + +/// The type of item that may be deemed unscrupulous. +#[derive(Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub enum UnscrupulousItem { + AccountId(AccountId), + Website(Url), +} + +type UnscrupulousItemOf = + UnscrupulousItem<::AccountId, UrlOf>; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub (super) trait Store)] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// The outer call dispatch type. + type Proposal: Parameter + + Dispatchable + + From> + + From> + + GetDispatchInfo + + IsSubType> + + IsType<::Call>; + + /// Origin for admin-level operations, like setting the Alliance's rules. + type AdminOrigin: EnsureOrigin; + + /// Origin that manages entry and forcible discharge from the Alliance. + type MembershipManager: EnsureOrigin; + + /// Origin for making announcements and adding/removing unscrupulous items. + type AnnouncementOrigin: EnsureOrigin; + + /// The currency used for deposits. + type Currency: ReservableCurrency; + + /// What to do with slashed funds. + type Slashed: OnUnbalanced>; + + /// What to do with initial voting members of the Alliance. + type InitializeMembers: InitializeMembers; + + /// What to do when a member has been added or removed. + type MembershipChanged: ChangeMembers; + + /// The identity verifier of an Alliance member. + type IdentityVerifier: IdentityVerifier; + + /// The provider of the proposal operation. + type ProposalProvider: ProposalProvider; + + /// Maximum number of proposals allowed to be active in parallel. + type MaxProposals: Get; + + /// The maximum number of founders supported by the pallet. Used for weight estimation. + /// + /// NOTE: + /// + Benchmarks will need to be re-run and weights adjusted if this changes. + /// + This pallet assumes that dependencies keep to the limit without enforcing it. + type MaxFounders: Get; + + /// The maximum number of fellows supported by the pallet. Used for weight estimation. + /// + /// NOTE: + /// + Benchmarks will need to be re-run and weights adjusted if this changes. + /// + This pallet assumes that dependencies keep to the limit without enforcing it. + type MaxFellows: Get; + + /// The maximum number of allies supported by the pallet. Used for weight estimation. + /// + /// NOTE: + /// + Benchmarks will need to be re-run and weights adjusted if this changes. + /// + This pallet assumes that dependencies keep to the limit without enforcing it. + type MaxAllies: Get; + + /// The maximum number of the unscrupulous items supported by the pallet. + #[pallet::constant] + type MaxUnscrupulousItems: Get; + + /// The maximum length of a website URL. + #[pallet::constant] + type MaxWebsiteUrlLength: Get; + + /// The deposit required for submitting candidacy. + #[pallet::constant] + type AllyDeposit: Get>; + + /// The maximum number of announcements. + #[pallet::constant] + type MaxAnnouncementsCount: Get; + + /// The maximum number of members per member role. Should not exceed the sum of + /// `MaxFounders` and `MaxFellows`. + #[pallet::constant] + type MaxMembersCount: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::error] + pub enum Error { + /// The founders/fellows/allies have already been initialized. + MembersAlreadyInitialized, + /// Account is already a member. + AlreadyMember, + /// Account is not a member. + NotMember, + /// Account is not an ally. + NotAlly, + /// Account is not a founder. + NotFounder, + /// This member is up for being kicked from the Alliance and cannot perform this operation. + UpForKicking, + /// Account does not have voting rights. + NoVotingRights, + /// Account is already an elevated (fellow) member. + AlreadyElevated, + /// Item is already listed as unscrupulous. + AlreadyUnscrupulous, + /// Account has been deemed unscrupulous by the Alliance and is not welcome to join or be + /// nominated. + AccountNonGrata, + /// Item has not been deemed unscrupulous. + NotListedAsUnscrupulous, + /// The number of unscrupulous items exceeds `MaxUnscrupulousItems`. + TooManyUnscrupulousItems, + /// Length of website URL exceeds `MaxWebsiteUrlLength`. + TooLongWebsiteUrl, + /// Balance is insufficient for the required deposit. + InsufficientFunds, + /// The account's identity does not have display field and website field. + WithoutIdentityDisplayAndWebsite, + /// The account's identity has no good judgement. + WithoutGoodIdentityJudgement, + /// The proposal hash is not found. + MissingProposalHash, + /// The proposal is not vetoable. + NotVetoableProposal, + /// The announcement is not found. + MissingAnnouncement, + /// Number of members exceeds `MaxMembersCount`. + TooManyMembers, + /// Number of announcements exceeds `MaxAnnouncementsCount`. + TooManyAnnouncements, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A new rule has been set. + NewRuleSet { rule: Cid }, + /// A new announcement has been proposed. + Announced { announcement: Cid }, + /// An on-chain announcement has been removed. + AnnouncementRemoved { announcement: Cid }, + /// Some accounts have been initialized as members (founders/fellows/allies). + MembersInitialized { + founders: Vec, + fellows: Vec, + allies: Vec, + }, + /// An account has been added as an Ally and reserved its deposit. + NewAllyJoined { + ally: T::AccountId, + nominator: Option, + reserved: Option>, + }, + /// An ally has been elevated to Fellow. + AllyElevated { ally: T::AccountId }, + /// A member has retired with its deposit unreserved. + MemberRetired { member: T::AccountId, unreserved: Option> }, + /// A member has been kicked out with its deposit slashed. + MemberKicked { member: T::AccountId, slashed: Option> }, + /// Accounts or websites have been added into the list of unscrupulous items. + UnscrupulousItemAdded { items: Vec> }, + /// Accounts or websites have been removed from the list of unscrupulous items. + UnscrupulousItemRemoved { items: Vec> }, + } + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub founders: Vec, + pub fellows: Vec, + pub allies: Vec, + pub phantom: PhantomData<(T, I)>, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { + founders: Vec::new(), + fellows: Vec::new(), + allies: Vec::new(), + phantom: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + for m in self.founders.iter().chain(self.fellows.iter()).chain(self.allies.iter()) { + assert!(Pallet::::has_identity(m).is_ok(), "Member does not set identity!"); + } + + if !self.founders.is_empty() { + assert!( + !Pallet::::has_member(MemberRole::Founder), + "Founders are already initialized!" + ); + let members: BoundedVec = + self.founders.clone().try_into().expect("Too many genesis founders"); + Members::::insert(MemberRole::Founder, members); + } + if !self.fellows.is_empty() { + assert!( + !Pallet::::has_member(MemberRole::Fellow), + "Fellows are already initialized!" + ); + let members: BoundedVec = + self.fellows.clone().try_into().expect("Too many genesis fellows"); + Members::::insert(MemberRole::Fellow, members); + } + if !self.allies.is_empty() { + let members: BoundedVec = + self.allies.clone().try_into().expect("Too many genesis allies"); + Members::::insert(MemberRole::Ally, members); + } + + T::InitializeMembers::initialize_members( + &[self.founders.as_slice(), self.fellows.as_slice()].concat(), + ) + } + } + + /// The IPFS CID of the alliance rule. + /// Founders and fellows can propose a new rule with a super-majority. + /// + /// Any founder has a special one-vote veto right to the rule setting. + #[pallet::storage] + #[pallet::getter(fn rule)] + pub type Rule, I: 'static = ()> = StorageValue<_, Cid, OptionQuery>; + + /// The current IPFS CIDs of any announcements. + #[pallet::storage] + #[pallet::getter(fn announcements)] + pub type Announcements, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Maps members to their candidacy deposit. + #[pallet::storage] + #[pallet::getter(fn deposit_of)] + pub type DepositOf, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf, OptionQuery>; + + /// Maps member type to members of each type. + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members, I: 'static = ()> = StorageMap< + _, + Twox64Concat, + MemberRole, + BoundedVec, + ValueQuery, + >; + + /// A set of members that are (potentially) being kicked out. They cannot retire until the + /// motion is settled. + #[pallet::storage] + #[pallet::getter(fn up_for_kicking)] + pub type UpForKicking, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::AccountId, bool, ValueQuery>; + + /// The current list of accounts deemed unscrupulous. These accounts non grata cannot submit + /// candidacy. + #[pallet::storage] + #[pallet::getter(fn unscrupulous_accounts)] + pub type UnscrupulousAccounts, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + /// The current list of websites deemed unscrupulous. + #[pallet::storage] + #[pallet::getter(fn unscrupulous_websites)] + pub type UnscrupulousWebsites, I: 'static = ()> = + StorageValue<_, BoundedVec, T::MaxUnscrupulousItems>, ValueQuery>; + + #[pallet::call] + impl, I: 'static> Pallet { + /// Add a new proposal to be voted on. + /// + /// Requires the sender to be a founder or fellow. + #[pallet::weight(T::WeightInfo::propose_proposed( + *length_bound, // B + T::MaxFounders::get(), // X + T::MaxFellows::get(), // Y + T::MaxProposals::get(), // P2 + ))] + pub fn propose( + origin: OriginFor, + #[pallet::compact] threshold: u32, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResult { + let proposor = ensure_signed(origin)?; + ensure!(Self::has_voting_rights(&proposor), Error::::NoVotingRights); + + if let Some(Call::kick_member { who }) = proposal.is_sub_type() { + let strike = T::Lookup::lookup(who.clone())?; + >::insert(strike, true); + } + + T::ProposalProvider::propose_proposal(proposor, threshold, proposal, length_bound)?; + Ok(()) + } + + /// Add an aye or nay vote for the sender to the given proposal. + /// + /// Requires the sender to be a founder or fellow. + #[pallet::weight(T::WeightInfo::vote(T::MaxFounders::get(), T::MaxFellows::get()))] + pub fn vote( + origin: OriginFor, + proposal: T::Hash, + #[pallet::compact] index: ProposalIndex, + approve: bool, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(Self::has_voting_rights(&who), Error::::NoVotingRights); + + T::ProposalProvider::vote_proposal(who, proposal, index, approve)?; + Ok(()) + } + + /// Veto a proposal about `set_rule` and `elevate_ally`, close, and remove it from the + /// system, regardless of its current state. + /// + /// Must be called by a founder. + #[pallet::weight(T::WeightInfo::veto(T::MaxProposals::get()))] + pub fn veto(origin: OriginFor, proposal_hash: T::Hash) -> DispatchResult { + let proposor = ensure_signed(origin)?; + ensure!(Self::is_founder(&proposor), Error::::NotFounder); + + let proposal = T::ProposalProvider::proposal_of(proposal_hash) + .ok_or(Error::::MissingProposalHash)?; + match proposal.is_sub_type() { + Some(Call::set_rule { .. }) | Some(Call::elevate_ally { .. }) => { + T::ProposalProvider::veto_proposal(proposal_hash); + Ok(()) + }, + _ => Err(Error::::NotVetoableProposal.into()), + } + } + + /// Close a vote that is either approved, disapproved, or whose voting period has ended. + /// + /// Requires the sender to be a founder or fellow. + #[pallet::weight({ + let b = *length_bound; + let x = T::MaxFounders::get(); + let y = T::MaxFellows::get(); + let p1 = *proposal_weight_bound; + let p2 = T::MaxProposals::get(); + T::WeightInfo::close_early_approved(b, x, y, p2) + .max(T::WeightInfo::close_early_disapproved(x, y, p2)) + .max(T::WeightInfo::close_approved(b, x, y, p2)) + .max(T::WeightInfo::close_disapproved(x, y, p2)) + .saturating_add(p1) + })] + pub fn close( + origin: OriginFor, + proposal_hash: T::Hash, + #[pallet::compact] index: ProposalIndex, + #[pallet::compact] proposal_weight_bound: Weight, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!(Self::has_voting_rights(&who), Error::::NoVotingRights); + + let info = T::ProposalProvider::close_proposal( + proposal_hash, + index, + proposal_weight_bound, + length_bound, + )?; + Ok(info.into()) + } + + /// Initialize the founders, fellows, and allies. + /// + /// This should only be called once, and must be called by the Root origin. + #[pallet::weight(T::WeightInfo::init_members( + T::MaxFounders::get(), + T::MaxFellows::get(), + T::MaxAllies::get() + ))] + pub fn init_members( + origin: OriginFor, + founders: Vec, + fellows: Vec, + allies: Vec, + ) -> DispatchResult { + ensure_root(origin)?; + + let mut founders: BoundedVec = + founders.try_into().map_err(|_| Error::::TooManyMembers)?; + let mut fellows: BoundedVec = + fellows.try_into().map_err(|_| Error::::TooManyMembers)?; + let mut allies: BoundedVec = + allies.try_into().map_err(|_| Error::::TooManyMembers)?; + + ensure!( + !Self::has_member(MemberRole::Founder) && + !Self::has_member(MemberRole::Fellow) && + !Self::has_member(MemberRole::Ally), + Error::::MembersAlreadyInitialized + ); + for member in founders.iter().chain(fellows.iter()).chain(allies.iter()) { + Self::has_identity(member)?; + } + + founders.sort(); + Members::::insert(&MemberRole::Founder, founders.clone()); + fellows.sort(); + Members::::insert(&MemberRole::Fellow, fellows.clone()); + allies.sort(); + Members::::insert(&MemberRole::Ally, allies.clone()); + + let mut voteable_members = Vec::with_capacity(founders.len() + fellows.len()); + voteable_members.extend(founders.clone()); + voteable_members.extend(fellows.clone()); + voteable_members.sort(); + + T::InitializeMembers::initialize_members(&voteable_members); + + log::debug!( + target: "runtime::alliance", + "Initialize alliance founders: {:?}, fellows: {:?}, allies: {:?}", + founders, fellows, allies + ); + + Self::deposit_event(Event::MembersInitialized { + founders: founders.into(), + fellows: fellows.into(), + allies: allies.into(), + }); + Ok(()) + } + + /// Set a new IPFS CID to the alliance rule. + #[pallet::weight(T::WeightInfo::set_rule())] + pub fn set_rule(origin: OriginFor, rule: Cid) -> DispatchResult { + T::AdminOrigin::ensure_origin(origin)?; + + Rule::::put(&rule); + + Self::deposit_event(Event::NewRuleSet { rule }); + Ok(()) + } + + /// Make an announcement of a new IPFS CID about alliance issues. + #[pallet::weight(T::WeightInfo::announce())] + pub fn announce(origin: OriginFor, announcement: Cid) -> DispatchResult { + T::AnnouncementOrigin::ensure_origin(origin)?; + + let mut announcements = >::get(); + announcements + .try_push(announcement.clone()) + .map_err(|_| Error::::TooManyAnnouncements)?; + >::put(announcements); + + Self::deposit_event(Event::Announced { announcement }); + Ok(()) + } + + /// Remove an announcement. + #[pallet::weight(T::WeightInfo::remove_announcement())] + pub fn remove_announcement(origin: OriginFor, announcement: Cid) -> DispatchResult { + T::AnnouncementOrigin::ensure_origin(origin)?; + + let mut announcements = >::get(); + let pos = announcements + .binary_search(&announcement) + .ok() + .ok_or(Error::::MissingAnnouncement)?; + announcements.remove(pos); + >::put(announcements); + + Self::deposit_event(Event::AnnouncementRemoved { announcement }); + Ok(()) + } + + /// Submit oneself for candidacy. A fixed deposit is reserved. + #[pallet::weight(T::WeightInfo::join_alliance())] + pub fn join_alliance(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + + // Unscrupulous accounts are non grata. + ensure!(!Self::is_unscrupulous_account(&who), Error::::AccountNonGrata); + ensure!(!Self::is_member(&who), Error::::AlreadyMember); + // check user self or parent should has verified identity to reuse display name and + // website. + Self::has_identity(&who)?; + + let deposit = T::AllyDeposit::get(); + T::Currency::reserve(&who, deposit).map_err(|_| Error::::InsufficientFunds)?; + >::insert(&who, deposit); + + Self::add_member(&who, MemberRole::Ally)?; + + Self::deposit_event(Event::NewAllyJoined { + ally: who, + nominator: None, + reserved: Some(deposit), + }); + Ok(()) + } + + /// A founder or fellow can nominate someone to join the alliance as an Ally. + /// There is no deposit required to the nominator or nominee. + #[pallet::weight(T::WeightInfo::nominate_ally())] + pub fn nominate_ally( + origin: OriginFor, + who: ::Source, + ) -> DispatchResult { + let nominator = ensure_signed(origin)?; + ensure!(Self::has_voting_rights(&nominator), Error::::NoVotingRights); + let who = T::Lookup::lookup(who)?; + + // Individual voting members cannot nominate accounts non grata. + ensure!(!Self::is_unscrupulous_account(&who), Error::::AccountNonGrata); + ensure!(!Self::is_member(&who), Error::::AlreadyMember); + // check user self or parent should has verified identity to reuse display name and + // website. + Self::has_identity(&who)?; + + Self::add_member(&who, MemberRole::Ally)?; + + Self::deposit_event(Event::NewAllyJoined { + ally: who, + nominator: Some(nominator), + reserved: None, + }); + Ok(()) + } + + /// Elevate an ally to fellow. + #[pallet::weight(T::WeightInfo::elevate_ally())] + pub fn elevate_ally( + origin: OriginFor, + ally: ::Source, + ) -> DispatchResult { + T::MembershipManager::ensure_origin(origin)?; + let ally = T::Lookup::lookup(ally)?; + ensure!(Self::is_ally(&ally), Error::::NotAlly); + ensure!(!Self::has_voting_rights(&ally), Error::::AlreadyElevated); + + Self::remove_member(&ally, MemberRole::Ally)?; + Self::add_member(&ally, MemberRole::Fellow)?; + + Self::deposit_event(Event::AllyElevated { ally }); + Ok(()) + } + + /// As a member, retire from the alliance and unreserve the deposit. + #[pallet::weight(T::WeightInfo::retire())] + pub fn retire(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + // A member up for kicking cannot retire. + ensure!(!Self::is_up_for_kicking(&who), Error::::UpForKicking); + + let role = Self::member_role_of(&who).ok_or(Error::::NotMember)?; + Self::remove_member(&who, role)?; + let deposit = DepositOf::::take(&who); + if let Some(deposit) = deposit { + let err_amount = T::Currency::unreserve(&who, deposit); + debug_assert!(err_amount.is_zero()); + } + Self::deposit_event(Event::MemberRetired { member: who, unreserved: deposit }); + Ok(()) + } + + /// Kick a member from the alliance and slash its deposit. + #[pallet::weight(T::WeightInfo::kick_member())] + pub fn kick_member( + origin: OriginFor, + who: ::Source, + ) -> DispatchResult { + T::MembershipManager::ensure_origin(origin)?; + let member = T::Lookup::lookup(who)?; + + let role = Self::member_role_of(&member).ok_or(Error::::NotMember)?; + Self::remove_member(&member, role)?; + let deposit = DepositOf::::take(member.clone()); + if let Some(deposit) = deposit { + T::Slashed::on_unbalanced(T::Currency::slash_reserved(&member, deposit).0); + } + + >::remove(&member); + + Self::deposit_event(Event::MemberKicked { member, slashed: deposit }); + Ok(()) + } + + /// Add accounts or websites to the list of unscrupulous items. + #[pallet::weight(T::WeightInfo::add_unscrupulous_items(items.len() as u32, T::MaxWebsiteUrlLength::get()))] + pub fn add_unscrupulous_items( + origin: OriginFor, + items: Vec>, + ) -> DispatchResult { + T::AnnouncementOrigin::ensure_origin(origin)?; + + let mut accounts = vec![]; + let mut webs = vec![]; + for info in items.iter() { + ensure!(!Self::is_unscrupulous(info), Error::::AlreadyUnscrupulous); + match info { + UnscrupulousItem::AccountId(who) => accounts.push(who.clone()), + UnscrupulousItem::Website(url) => { + ensure!( + url.len() as u32 <= T::MaxWebsiteUrlLength::get(), + Error::::TooLongWebsiteUrl + ); + webs.push(url.clone()); + }, + } + } + + Self::do_add_unscrupulous_items(&mut accounts, &mut webs)?; + Self::deposit_event(Event::UnscrupulousItemAdded { items }); + Ok(()) + } + + /// Deem an item no longer unscrupulous. + #[pallet::weight(>::WeightInfo::remove_unscrupulous_items( + items.len() as u32, T::MaxWebsiteUrlLength::get() + ))] + pub fn remove_unscrupulous_items( + origin: OriginFor, + items: Vec>, + ) -> DispatchResult { + T::AnnouncementOrigin::ensure_origin(origin)?; + let mut accounts = vec![]; + let mut webs = vec![]; + for info in items.iter() { + ensure!(Self::is_unscrupulous(info), Error::::NotListedAsUnscrupulous); + match info { + UnscrupulousItem::AccountId(who) => accounts.push(who.clone()), + UnscrupulousItem::Website(url) => webs.push(url.clone()), + } + } + Self::do_remove_unscrupulous_items(&mut accounts, &mut webs)?; + Self::deposit_event(Event::UnscrupulousItemRemoved { items }); + Ok(()) + } + } +} + +impl, I: 'static> Pallet { + /// Check if a given role has any members. + fn has_member(role: MemberRole) -> bool { + Members::::decode_len(role).unwrap_or_default() > 0 + } + + /// Look up the role, if any, of an account. + fn member_role_of(who: &T::AccountId) -> Option { + Members::::iter() + .find_map(|(r, members)| if members.contains(who) { Some(r) } else { None }) + } + + /// Check if a user is a alliance member. + pub fn is_member(who: &T::AccountId) -> bool { + Self::member_role_of(who).is_some() + } + + /// Check if an account has a given role. + pub fn is_member_of(who: &T::AccountId, role: MemberRole) -> bool { + Members::::get(role).contains(&who) + } + + /// Check if an account is a founder. + fn is_founder(who: &T::AccountId) -> bool { + Self::is_member_of(who, MemberRole::Founder) + } + + /// Check if an account is a fellow. + fn is_fellow(who: &T::AccountId) -> bool { + Self::is_member_of(who, MemberRole::Fellow) + } + + /// Check if an account is an ally. + fn is_ally(who: &T::AccountId) -> bool { + Self::is_member_of(who, MemberRole::Ally) + } + + /// Check if a member has voting rights. + fn has_voting_rights(who: &T::AccountId) -> bool { + Self::is_founder(who) || Self::is_fellow(who) + } + + /// Collect all members who have voting rights into one list. + fn votable_members_sorted() -> Vec { + let mut founders = Members::::get(MemberRole::Founder).into_inner(); + let mut fellows = Members::::get(MemberRole::Fellow).into_inner(); + founders.append(&mut fellows); + founders.sort(); + founders.into() + } + + /// Check if an account's forced removal is up for consideration. + fn is_up_for_kicking(who: &T::AccountId) -> bool { + >::contains_key(&who) + } + + /// Add a user to the sorted alliance member set. + fn add_member(who: &T::AccountId, role: MemberRole) -> DispatchResult { + >::try_mutate(role, |members| -> DispatchResult { + let pos = members.binary_search(who).err().ok_or(Error::::AlreadyMember)?; + members + .try_insert(pos, who.clone()) + .map_err(|_| Error::::TooManyMembers)?; + Ok(()) + })?; + + if role == MemberRole::Founder || role == MemberRole::Fellow { + let members = Self::votable_members_sorted(); + T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members[..]); + } + Ok(()) + } + + /// Remove a user from the alliance member set. + fn remove_member(who: &T::AccountId, role: MemberRole) -> DispatchResult { + >::try_mutate(role, |members| -> DispatchResult { + let pos = members.binary_search(who).ok().ok_or(Error::::NotMember)?; + members.remove(pos); + Ok(()) + })?; + + if matches!(role, MemberRole::Founder | MemberRole::Fellow) { + let members = Self::votable_members_sorted(); + T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]); + } + Ok(()) + } + + /// Check if an item is listed as unscrupulous. + fn is_unscrupulous(info: &UnscrupulousItemOf) -> bool { + match info { + UnscrupulousItem::Website(url) => >::get().contains(url), + UnscrupulousItem::AccountId(who) => >::get().contains(who), + } + } + + /// Check if an account is listed as unscrupulous. + fn is_unscrupulous_account(who: &T::AccountId) -> bool { + >::get().contains(who) + } + + /// Add item to the unscrupulous list. + fn do_add_unscrupulous_items( + new_accounts: &mut Vec, + new_webs: &mut Vec>, + ) -> DispatchResult { + if !new_accounts.is_empty() { + >::try_mutate(|accounts| -> DispatchResult { + accounts + .try_append(new_accounts) + .map_err(|_| Error::::TooManyUnscrupulousItems)?; + accounts.sort(); + + Ok(()) + })?; + } + if !new_webs.is_empty() { + >::try_mutate(|webs| -> DispatchResult { + webs.try_append(new_webs).map_err(|_| Error::::TooManyUnscrupulousItems)?; + webs.sort(); + + Ok(()) + })?; + } + + Ok(()) + } + + /// Remove item from the unscrupulous list. + fn do_remove_unscrupulous_items( + out_accounts: &mut Vec, + out_webs: &mut Vec>, + ) -> DispatchResult { + if !out_accounts.is_empty() { + >::try_mutate(|accounts| -> DispatchResult { + for who in out_accounts.iter() { + let pos = accounts + .binary_search(who) + .ok() + .ok_or(Error::::NotListedAsUnscrupulous)?; + accounts.remove(pos); + } + Ok(()) + })?; + } + if !out_webs.is_empty() { + >::try_mutate(|webs| -> DispatchResult { + for web in out_webs.iter() { + let pos = webs + .binary_search(web) + .ok() + .ok_or(Error::::NotListedAsUnscrupulous)?; + webs.remove(pos); + } + Ok(()) + })?; + } + Ok(()) + } + + fn has_identity(who: &T::AccountId) -> DispatchResult { + const IDENTITY_FIELD_DISPLAY: u64 = IdentityField::Display as u64; + const IDENTITY_FIELD_WEB: u64 = IdentityField::Web as u64; + + let judgement = |who: &T::AccountId| -> DispatchResult { + ensure!( + T::IdentityVerifier::has_identity(who, IDENTITY_FIELD_DISPLAY | IDENTITY_FIELD_WEB), + Error::::WithoutIdentityDisplayAndWebsite + ); + ensure!( + T::IdentityVerifier::has_good_judgement(who), + Error::::WithoutGoodIdentityJudgement + ); + Ok(()) + }; + + let res = judgement(who); + if res.is_err() { + if let Some(parent) = T::IdentityVerifier::super_account_id(who) { + return judgement(&parent) + } + } + res + } +} diff --git a/frame/alliance/src/mock.rs b/frame/alliance/src/mock.rs new file mode 100644 index 0000000000000..d6e9a92a10dec --- /dev/null +++ b/frame/alliance/src/mock.rs @@ -0,0 +1,324 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities + +pub use sp_core::H256; +pub use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; +use sp_std::convert::{TryFrom, TryInto}; + +pub use frame_support::{ + assert_ok, ord_parameter_types, parameter_types, + traits::{EitherOfDiverse, GenesisBuild, SortedMembers}, + BoundedVec, +}; +use frame_system::{EnsureRoot, EnsureSignedBy}; +use pallet_identity::{Data, IdentityInfo, Judgement}; + +pub use crate as pallet_alliance; + +use super::*; + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; + pub const MaxLocks: u32 = 10; +} +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; +} + +parameter_types! { + pub const MotionDuration: u64 = 3; + pub const MaxProposals: u32 = 100; + pub const MaxMembers: u32 = 100; +} +type AllianceCollective = pallet_collective::Instance1; +impl pallet_collective::Config for Test { + type Origin = Origin; + type Proposal = Call; + type Event = Event; + type MotionDuration = MotionDuration; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = (); +} + +parameter_types! { + pub const BasicDeposit: u64 = 10; + pub const FieldDeposit: u64 = 10; + pub const SubAccountDeposit: u64 = 10; + pub const MaxSubAccounts: u32 = 2; + pub const MaxAdditionalFields: u32 = 2; + pub const MaxRegistrars: u32 = 20; +} +ord_parameter_types! { + pub const One: u64 = 1; + pub const Two: u64 = 2; + pub const Three: u64 = 3; + pub const Four: u64 = 4; + pub const Five: u64 = 5; +} +type EnsureOneOrRoot = EitherOfDiverse, EnsureSignedBy>; +type EnsureTwoOrRoot = EitherOfDiverse, EnsureSignedBy>; + +impl pallet_identity::Config for Test { + type Event = Event; + type Currency = Balances; + type BasicDeposit = BasicDeposit; + type FieldDeposit = FieldDeposit; + type SubAccountDeposit = SubAccountDeposit; + type MaxSubAccounts = MaxSubAccounts; + type MaxAdditionalFields = MaxAdditionalFields; + type MaxRegistrars = MaxRegistrars; + type Slashed = (); + type RegistrarOrigin = EnsureOneOrRoot; + type ForceOrigin = EnsureTwoOrRoot; + type WeightInfo = (); +} + +pub struct AllianceIdentityVerifier; +impl IdentityVerifier for AllianceIdentityVerifier { + fn has_identity(who: &u64, fields: u64) -> bool { + Identity::has_identity(who, fields) + } + + fn has_good_judgement(who: &u64) -> bool { + if let Some(judgements) = + Identity::identity(who).map(|registration| registration.judgements) + { + judgements + .iter() + .any(|(_, j)| matches!(j, Judgement::KnownGood | Judgement::Reasonable)) + } else { + false + } + } + + fn super_account_id(who: &u64) -> Option { + Identity::super_of(who).map(|parent| parent.0) + } +} + +pub struct AllianceProposalProvider; +impl ProposalProvider for AllianceProposalProvider { + fn propose_proposal( + who: u64, + threshold: u32, + proposal: Box, + length_bound: u32, + ) -> Result<(u32, u32), DispatchError> { + AllianceMotion::do_propose_proposed(who, threshold, proposal, length_bound) + } + + fn vote_proposal( + who: u64, + proposal: H256, + index: ProposalIndex, + approve: bool, + ) -> Result { + AllianceMotion::do_vote(who, proposal, index, approve) + } + + fn veto_proposal(proposal_hash: H256) -> u32 { + AllianceMotion::do_disapprove_proposal(proposal_hash) + } + + fn close_proposal( + proposal_hash: H256, + proposal_index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, + ) -> DispatchResultWithPostInfo { + AllianceMotion::do_close(proposal_hash, proposal_index, proposal_weight_bound, length_bound) + } + + fn proposal_of(proposal_hash: H256) -> Option { + AllianceMotion::proposal_of(proposal_hash) + } +} + +parameter_types! { + pub const MaxFounders: u32 = 10; + pub const MaxFellows: u32 = MaxMembers::get() - MaxFounders::get(); + pub const MaxAllies: u32 = 100; + pub const AllyDeposit: u64 = 25; +} +impl Config for Test { + type Event = Event; + type Proposal = Call; + type AdminOrigin = EnsureSignedBy; + type MembershipManager = EnsureSignedBy; + type AnnouncementOrigin = EnsureSignedBy; + type Currency = Balances; + type Slashed = (); + type InitializeMembers = AllianceMotion; + type MembershipChanged = AllianceMotion; + #[cfg(not(feature = "runtime-benchmarks"))] + type IdentityVerifier = AllianceIdentityVerifier; + #[cfg(feature = "runtime-benchmarks")] + type IdentityVerifier = (); + type ProposalProvider = AllianceProposalProvider; + type MaxProposals = MaxProposals; + type MaxFounders = MaxFounders; + type MaxFellows = MaxFellows; + type MaxAllies = MaxAllies; + type MaxUnscrupulousItems = ConstU32<100>; + type MaxWebsiteUrlLength = ConstU32<255>; + type MaxAnnouncementsCount = ConstU32<100>; + type MaxMembersCount = MaxMembers; + type AllyDeposit = AllyDeposit; + type WeightInfo = (); +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Identity: pallet_identity, + AllianceMotion: pallet_collective::, + Alliance: pallet_alliance, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(1, 50), (2, 50), (3, 50), (4, 50), (5, 30), (6, 50), (7, 50)], + } + .assimilate_storage(&mut t) + .unwrap(); + + GenesisBuild::::assimilate_storage( + &pallet_alliance::GenesisConfig { + founders: vec![], + fellows: vec![], + allies: vec![], + phantom: Default::default(), + }, + &mut t, + ) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + assert_ok!(Identity::add_registrar(Origin::signed(1), 1)); + + let info = IdentityInfo { + additional: BoundedVec::default(), + display: Data::Raw(b"name".to_vec().try_into().unwrap()), + legal: Data::default(), + web: Data::Raw(b"website".to_vec().try_into().unwrap()), + riot: Data::default(), + email: Data::default(), + pgp_fingerprint: None, + image: Data::default(), + twitter: Data::default(), + }; + assert_ok!(Identity::set_identity(Origin::signed(1), Box::new(info.clone()))); + assert_ok!(Identity::provide_judgement(Origin::signed(1), 0, 1, Judgement::KnownGood)); + assert_ok!(Identity::set_identity(Origin::signed(2), Box::new(info.clone()))); + assert_ok!(Identity::provide_judgement(Origin::signed(1), 0, 2, Judgement::KnownGood)); + assert_ok!(Identity::set_identity(Origin::signed(3), Box::new(info.clone()))); + assert_ok!(Identity::provide_judgement(Origin::signed(1), 0, 3, Judgement::KnownGood)); + assert_ok!(Identity::set_identity(Origin::signed(4), Box::new(info.clone()))); + assert_ok!(Identity::provide_judgement(Origin::signed(1), 0, 4, Judgement::KnownGood)); + assert_ok!(Identity::set_identity(Origin::signed(5), Box::new(info.clone()))); + assert_ok!(Identity::provide_judgement(Origin::signed(1), 0, 5, Judgement::KnownGood)); + assert_ok!(Identity::set_identity(Origin::signed(6), Box::new(info.clone()))); + + assert_ok!(Alliance::init_members(Origin::root(), vec![1, 2], vec![3], vec![])); + + System::set_block_number(1); + }); + ext +} + +#[cfg(feature = "runtime-benchmarks")] +pub fn new_bench_ext() -> sp_io::TestExternalities { + GenesisConfig::default().build_storage().unwrap().into() +} + +pub fn test_cid() -> Cid { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(b"hello world"); + let result = hasher.finalize(); + Cid::new_v0(&*result) +} + +pub fn make_proposal(value: u64) -> Call { + Call::System(frame_system::Call::remark { remark: value.encode() }) +} + +pub fn make_set_rule_proposal(rule: Cid) -> Call { + Call::Alliance(pallet_alliance::Call::set_rule { rule }) +} + +pub fn make_kick_member_proposal(who: u64) -> Call { + Call::Alliance(pallet_alliance::Call::kick_member { who }) +} diff --git a/frame/alliance/src/tests.rs b/frame/alliance/src/tests.rs new file mode 100644 index 0000000000000..85c91b451d351 --- /dev/null +++ b/frame/alliance/src/tests.rs @@ -0,0 +1,483 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for the alliance pallet. + +use sp_runtime::traits::Hash; + +use frame_support::{assert_noop, assert_ok, Hashable}; +use frame_system::{EventRecord, Phase}; + +use super::*; +use crate::mock::*; + +type AllianceMotionEvent = pallet_collective::Event; + +#[test] +fn propose_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + + // only votable member can propose proposal, 4 is ally not have vote rights + assert_noop!( + Alliance::propose(Origin::signed(4), 3, Box::new(proposal.clone()), proposal_len), + Error::::NoVotingRights + ); + + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*AllianceMotion::proposals(), vec![hash]); + assert_eq!(AllianceMotion::proposal_of(&hash), Some(proposal)); + assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: mock::Event::AllianceMotion(AllianceMotionEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3, + }), + topics: vec![], + }] + ); + }); +} + +#[test] +fn vote_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Alliance::vote(Origin::signed(2), hash.clone(), 0, true)); + + let record = |event| EventRecord { phase: Phase::Initialization, event, topics: vec![] }; + assert_eq!( + System::events(), + vec![ + record(mock::Event::AllianceMotion(AllianceMotionEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash.clone(), + threshold: 3 + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted { + account: 2, + proposal_hash: hash.clone(), + voted: true, + yes: 1, + no: 0, + })), + ] + ); + }); +} + +#[test] +fn veto_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash: H256 = proposal.blake2_256().into(); + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + // only set_rule/elevate_ally can be veto + assert_noop!( + Alliance::veto(Origin::signed(1), hash.clone()), + Error::::NotVetoableProposal + ); + + let cid = test_cid(); + let vetoable_proposal = make_set_rule_proposal(cid); + let vetoable_proposal_len: u32 = vetoable_proposal.using_encoded(|p| p.len() as u32); + let vetoable_hash: H256 = vetoable_proposal.blake2_256().into(); + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(vetoable_proposal.clone()), + vetoable_proposal_len + )); + + // only founder have veto rights, 3 is fellow + assert_noop!( + Alliance::veto(Origin::signed(3), vetoable_hash.clone()), + Error::::NotFounder + ); + + assert_ok!(Alliance::veto(Origin::signed(2), vetoable_hash.clone())); + let record = |event| EventRecord { phase: Phase::Initialization, event, topics: vec![] }; + assert_eq!( + System::events(), + vec![ + record(mock::Event::AllianceMotion(AllianceMotionEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash.clone(), + threshold: 3 + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Proposed { + account: 1, + proposal_index: 1, + proposal_hash: vetoable_hash.clone(), + threshold: 3 + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Disapproved { + proposal_hash: vetoable_hash.clone() + })), + ] + ); + }) +} + +#[test] +fn close_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Alliance::vote(Origin::signed(1), hash.clone(), 0, true)); + assert_ok!(Alliance::vote(Origin::signed(2), hash.clone(), 0, true)); + assert_ok!(Alliance::vote(Origin::signed(3), hash.clone(), 0, true)); + assert_ok!(Alliance::close( + Origin::signed(1), + hash.clone(), + 0, + proposal_weight, + proposal_len + )); + + let record = |event| EventRecord { phase: Phase::Initialization, event, topics: vec![] }; + assert_eq!( + System::events(), + vec![ + record(mock::Event::AllianceMotion(AllianceMotionEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash.clone(), + threshold: 3 + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted { + account: 1, + proposal_hash: hash.clone(), + voted: true, + yes: 1, + no: 0, + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted { + account: 2, + proposal_hash: hash.clone(), + voted: true, + yes: 2, + no: 0, + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted { + account: 3, + proposal_hash: hash.clone(), + voted: true, + yes: 3, + no: 0, + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Closed { + proposal_hash: hash.clone(), + yes: 3, + no: 0, + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Approved { + proposal_hash: hash.clone() + })), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Executed { + proposal_hash: hash.clone(), + result: Err(DispatchError::BadOrigin), + })) + ] + ); + }); +} + +#[test] +fn set_rule_works() { + new_test_ext().execute_with(|| { + let cid = test_cid(); + assert_ok!(Alliance::set_rule(Origin::signed(1), cid.clone())); + assert_eq!(Alliance::rule(), Some(cid.clone())); + + System::assert_last_event(mock::Event::Alliance(crate::Event::NewRuleSet { rule: cid })); + }); +} + +#[test] +fn announce_works() { + new_test_ext().execute_with(|| { + let cid = test_cid(); + assert_ok!(Alliance::announce(Origin::signed(3), cid.clone())); + assert_eq!(Alliance::announcements(), vec![cid.clone()]); + + System::assert_last_event(mock::Event::Alliance(crate::Event::Announced { + announcement: cid, + })); + }); +} + +#[test] +fn remove_announcement_works() { + new_test_ext().execute_with(|| { + let cid = test_cid(); + assert_ok!(Alliance::announce(Origin::signed(3), cid.clone())); + assert_eq!(Alliance::announcements(), vec![cid.clone()]); + System::assert_last_event(mock::Event::Alliance(crate::Event::Announced { + announcement: cid.clone(), + })); + + System::set_block_number(2); + + assert_ok!(Alliance::remove_announcement(Origin::signed(3), cid.clone())); + assert_eq!(Alliance::announcements(), vec![]); + System::assert_last_event(mock::Event::Alliance(crate::Event::AnnouncementRemoved { + announcement: cid, + })); + }); +} + +#[test] +fn join_alliance_works() { + new_test_ext().execute_with(|| { + // check already member + assert_noop!(Alliance::join_alliance(Origin::signed(1)), Error::::AlreadyMember); + + // check already listed as unscrupulous + assert_ok!(Alliance::add_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(4)] + )); + assert_noop!( + Alliance::join_alliance(Origin::signed(4)), + Error::::AccountNonGrata + ); + assert_ok!(Alliance::remove_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(4)] + )); + + // check deposit funds + assert_noop!( + Alliance::join_alliance(Origin::signed(5)), + Error::::InsufficientFunds + ); + + // success to submit + assert_ok!(Alliance::join_alliance(Origin::signed(4))); + assert_eq!(Alliance::deposit_of(4), Some(25)); + assert_eq!(Alliance::members(MemberRole::Ally), vec![4]); + + // check already member + assert_noop!(Alliance::join_alliance(Origin::signed(4)), Error::::AlreadyMember); + + // check missing identity judgement + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::join_alliance(Origin::signed(6)), + Error::::WithoutGoodIdentityJudgement + ); + // check missing identity info + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::join_alliance(Origin::signed(7)), + Error::::WithoutIdentityDisplayAndWebsite + ); + }); +} + +#[test] +fn nominate_ally_works() { + new_test_ext().execute_with(|| { + // check already member + assert_noop!( + Alliance::nominate_ally(Origin::signed(1), 2), + Error::::AlreadyMember + ); + + // only votable member(founder/fellow) have nominate right + assert_noop!( + Alliance::nominate_ally(Origin::signed(5), 4), + Error::::NoVotingRights + ); + + // check already listed as unscrupulous + assert_ok!(Alliance::add_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(4)] + )); + assert_noop!( + Alliance::nominate_ally(Origin::signed(1), 4), + Error::::AccountNonGrata + ); + assert_ok!(Alliance::remove_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(4)] + )); + + // success to nominate + assert_ok!(Alliance::nominate_ally(Origin::signed(1), 4)); + assert_eq!(Alliance::deposit_of(4), None); + assert_eq!(Alliance::members(MemberRole::Ally), vec![4]); + + // check already member + assert_noop!( + Alliance::nominate_ally(Origin::signed(1), 4), + Error::::AlreadyMember + ); + + // check missing identity judgement + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::join_alliance(Origin::signed(6)), + Error::::WithoutGoodIdentityJudgement + ); + // check missing identity info + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::join_alliance(Origin::signed(7)), + Error::::WithoutIdentityDisplayAndWebsite + ); + }); +} + +#[test] +fn elevate_ally_works() { + new_test_ext().execute_with(|| { + assert_noop!(Alliance::elevate_ally(Origin::signed(2), 4), Error::::NotAlly); + + assert_ok!(Alliance::join_alliance(Origin::signed(4))); + assert_eq!(Alliance::members(MemberRole::Ally), vec![4]); + assert_eq!(Alliance::members(MemberRole::Fellow), vec![3]); + + assert_ok!(Alliance::elevate_ally(Origin::signed(2), 4)); + assert_eq!(Alliance::members(MemberRole::Ally), Vec::::new()); + assert_eq!(Alliance::members(MemberRole::Fellow), vec![3, 4]); + }); +} + +#[test] +fn retire_works() { + new_test_ext().execute_with(|| { + let proposal = make_kick_member_proposal(2); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!(Alliance::retire(Origin::signed(2)), Error::::UpForKicking); + + assert_noop!(Alliance::retire(Origin::signed(4)), Error::::NotMember); + + assert_eq!(Alliance::members(MemberRole::Fellow), vec![3]); + assert_ok!(Alliance::retire(Origin::signed(3))); + assert_eq!(Alliance::members(MemberRole::Fellow), Vec::::new()); + }); +} + +#[test] +fn kick_member_works() { + new_test_ext().execute_with(|| { + let proposal = make_kick_member_proposal(2); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_ok!(Alliance::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(Alliance::up_for_kicking(2), true); + assert_eq!(Alliance::members(MemberRole::Founder), vec![1, 2]); + + assert_ok!(Alliance::kick_member(Origin::signed(2), 2)); + assert_eq!(Alliance::members(MemberRole::Founder), vec![1]); + }); +} + +#[test] +fn add_unscrupulous_items_works() { + new_test_ext().execute_with(|| { + assert_ok!(Alliance::add_unscrupulous_items( + Origin::signed(3), + vec![ + UnscrupulousItem::AccountId(3), + UnscrupulousItem::Website("abc".as_bytes().to_vec().try_into().unwrap()) + ] + )); + assert_eq!(Alliance::unscrupulous_accounts().into_inner(), vec![3]); + assert_eq!(Alliance::unscrupulous_websites().into_inner(), vec!["abc".as_bytes().to_vec()]); + + assert_noop!( + Alliance::add_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(3)] + ), + Error::::AlreadyUnscrupulous + ); + }); +} + +#[test] +fn remove_unscrupulous_items_works() { + new_test_ext().execute_with(|| { + assert_noop!( + Alliance::remove_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(3)] + ), + Error::::NotListedAsUnscrupulous + ); + + assert_ok!(Alliance::add_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(3)] + )); + assert_eq!(Alliance::unscrupulous_accounts(), vec![3]); + assert_ok!(Alliance::remove_unscrupulous_items( + Origin::signed(3), + vec![UnscrupulousItem::AccountId(3)] + )); + assert_eq!(Alliance::unscrupulous_accounts(), Vec::::new()); + }); +} diff --git a/frame/alliance/src/types.rs b/frame/alliance/src/types.rs new file mode 100644 index 0000000000000..8fb0ae96fd02d --- /dev/null +++ b/frame/alliance/src/types.rs @@ -0,0 +1,95 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{traits::ConstU32, BoundedVec}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; +use sp_std::{convert::TryInto, prelude::*}; + +/// A Multihash instance that only supports the basic functionality and no hashing. +#[derive( + Clone, PartialEq, Eq, PartialOrd, Ord, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen, +)] +pub struct Multihash { + /// The code of the Multihash. + pub code: u64, + /// The digest. + pub digest: BoundedVec>, // 4 byte dig size + 64 bytes hash digest +} + +impl Multihash { + /// Returns the size of the digest. + pub fn size(&self) -> usize { + self.digest.len() + } +} + +/// The version of the CID. +#[derive( + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + RuntimeDebug, + Encode, + Decode, + TypeInfo, + MaxEncodedLen, +)] +pub enum Version { + /// CID version 0. + V0, + /// CID version 1. + V1, +} + +/// Representation of a CID. +/// +/// The generic is about the allocated size of the multihash. +#[derive( + Clone, PartialEq, Eq, PartialOrd, Ord, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen, +)] +pub struct Cid { + /// The version of CID. + pub version: Version, + /// The codec of CID. + pub codec: u64, + /// The multihash of CID. + pub hash: Multihash, +} + +impl Cid { + /// Creates a new CIDv0. + pub fn new_v0(sha2_256_digest: impl Into>) -> Self { + /// DAG-PB multicodec code + const DAG_PB: u64 = 0x70; + /// The SHA_256 multicodec code + const SHA2_256: u64 = 0x12; + + let digest = sha2_256_digest.into(); + assert_eq!(digest.len(), 32); + + Self { + version: Version::V0, + codec: DAG_PB, + hash: Multihash { code: SHA2_256, digest: digest.try_into().expect("msg") }, + } + } +} diff --git a/frame/alliance/src/weights.rs b/frame/alliance/src/weights.rs new file mode 100644 index 0000000000000..495dd1b83df93 --- /dev/null +++ b/frame/alliance/src/weights.rs @@ -0,0 +1,485 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_alliance +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2021-10-11, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// ./target/release/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_alliance +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/alliance/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_alliance. +pub trait WeightInfo { + fn propose_proposed(b: u32, x: u32, y: u32, p: u32, ) -> Weight; + fn vote(x: u32, y: u32, ) -> Weight; + fn veto(p: u32, ) -> Weight; + fn close_early_disapproved(x: u32, y: u32, p: u32, ) -> Weight; + fn close_early_approved(b: u32, x: u32, y: u32, p: u32, ) -> Weight; + fn close_disapproved(x: u32, y: u32, p: u32, ) -> Weight; + fn close_approved(b: u32, x: u32, y: u32, p: u32, ) -> Weight; + fn init_members(x: u32, y: u32, z: u32, ) -> Weight; + fn set_rule() -> Weight; + fn announce() -> Weight; + fn remove_announcement() -> Weight; + fn join_alliance() -> Weight; + fn nominate_ally() -> Weight; + fn elevate_ally() -> Weight; + fn retire() -> Weight; + fn kick_member() -> Weight; + fn add_unscrupulous_items(n: u32, l: u32, ) -> Weight; + fn remove_unscrupulous_items(n: u32, l: u32, ) -> Weight; +} + +/// Weights for pallet_alliance using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Proposals (r:1 w:1) + // Storage: AllianceMotion ProposalCount (r:1 w:1) + // Storage: AllianceMotion Voting (r:0 w:1) + fn propose_proposed(_b: u32, _x: u32, y: u32, p: u32, ) -> Weight { + (39_992_000 as Weight) + // Standard Error: 2_000 + .saturating_add((44_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((323_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Alliance Members (r:2 w:0) + // Storage: AllianceMotion Voting (r:1 w:1) + fn vote(x: u32, y: u32, ) -> Weight { + (36_649_000 as Weight) + // Standard Error: 90_000 + .saturating_add((42_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 3_000 + .saturating_add((195_000 as Weight).saturating_mul(y as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Proposals (r:1 w:1) + // Storage: AllianceMotion Voting (r:0 w:1) + fn veto(p: u32, ) -> Weight { + (30_301_000 as Weight) + // Standard Error: 1_000 + .saturating_add((330_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_early_disapproved(x: u32, y: u32, p: u32, ) -> Weight { + (40_472_000 as Weight) + // Standard Error: 69_000 + .saturating_add((485_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 2_000 + .saturating_add((192_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((330_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_early_approved(b: u32, x: u32, y: u32, p: u32, ) -> Weight { + (52_076_000 as Weight) + // Standard Error: 0 + .saturating_add((4_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 77_000 + .saturating_add((194_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 3_000 + .saturating_add((188_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((329_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Prime (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_disapproved(x: u32, y: u32, p: u32, ) -> Weight { + (47_009_000 as Weight) + // Standard Error: 66_000 + .saturating_add((256_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 2_000 + .saturating_add((176_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((327_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Prime (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_approved(b: u32, x: u32, y: u32, p: u32, ) -> Weight { + (43_650_000 as Weight) + // Standard Error: 0 + .saturating_add((3_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 85_000 + .saturating_add((124_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 3_000 + .saturating_add((199_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 3_000 + .saturating_add((326_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:3 w:3) + // Storage: AllianceMotion Members (r:1 w:1) + fn init_members(_x: u32, y: u32, z: u32, ) -> Weight { + (45_100_000 as Weight) + // Standard Error: 4_000 + .saturating_add((162_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 4_000 + .saturating_add((151_000 as Weight).saturating_mul(z as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Alliance Rule (r:0 w:1) + fn set_rule() -> Weight { + (14_517_000 as Weight) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Announcements (r:1 w:1) + fn announce() -> Weight { + (16_801_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Announcements (r:1 w:1) + fn remove_announcement() -> Weight { + (17_133_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance UnscrupulousAccounts (r:1 w:0) + // Storage: Alliance Members (r:4 w:0) + // Storage: System Account (r:1 w:1) + // Storage: Alliance DepositOf (r:0 w:1) + fn join_alliance() -> Weight { + (95_370_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:4 w:0) + // Storage: Alliance UnscrupulousAccounts (r:1 w:0) + fn nominate_ally() -> Weight { + (44_764_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Members (r:3 w:2) + // Storage: AllianceMotion Proposals (r:1 w:0) + // Storage: AllianceMotion Members (r:0 w:1) + // Storage: AllianceMotion Prime (r:0 w:1) + fn elevate_ally() -> Weight { + (44_013_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Alliance KickingMembers (r:1 w:0) + // Storage: Alliance Members (r:3 w:1) + // Storage: AllianceMotion Proposals (r:1 w:0) + // Storage: Alliance DepositOf (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: AllianceMotion Members (r:0 w:1) + // Storage: AllianceMotion Prime (r:0 w:1) + fn retire() -> Weight { + (60_183_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) + } + // Storage: Alliance KickingMembers (r:1 w:0) + // Storage: Alliance Members (r:3 w:1) + // Storage: AllianceMotion Proposals (r:1 w:0) + // Storage: Alliance DepositOf (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: AllianceMotion Members (r:0 w:1) + // Storage: AllianceMotion Prime (r:0 w:1) + fn kick_member() -> Weight { + (67_467_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) + } + // Storage: Alliance UnscrupulousAccounts (r:1 w:1) + // Storage: Alliance UnscrupulousWebsites (r:1 w:1) + fn add_unscrupulous_items(n: u32, l: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 16_000 + .saturating_add((2_673_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 7_000 + .saturating_add((224_000 as Weight).saturating_mul(l as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Alliance UnscrupulousAccounts (r:1 w:1) + // Storage: Alliance UnscrupulousWebsites (r:1 w:1) + fn remove_unscrupulous_items(n: u32, l: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 343_000 + .saturating_add((59_025_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 153_000 + .saturating_add((6_725_000 as Weight).saturating_mul(l as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Proposals (r:1 w:1) + // Storage: AllianceMotion ProposalCount (r:1 w:1) + // Storage: AllianceMotion Voting (r:0 w:1) + fn propose_proposed(_b: u32, _x: u32, y: u32, p: u32, ) -> Weight { + (39_992_000 as Weight) + // Standard Error: 2_000 + .saturating_add((44_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((323_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Alliance Members (r:2 w:0) + // Storage: AllianceMotion Voting (r:1 w:1) + fn vote(x: u32, y: u32, ) -> Weight { + (36_649_000 as Weight) + // Standard Error: 90_000 + .saturating_add((42_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 3_000 + .saturating_add((195_000 as Weight).saturating_mul(y as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Proposals (r:1 w:1) + // Storage: AllianceMotion Voting (r:0 w:1) + fn veto(p: u32, ) -> Weight { + (30_301_000 as Weight) + // Standard Error: 1_000 + .saturating_add((330_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_early_disapproved(x: u32, y: u32, p: u32, ) -> Weight { + (40_472_000 as Weight) + // Standard Error: 69_000 + .saturating_add((485_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 2_000 + .saturating_add((192_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((330_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_early_approved(b: u32, x: u32, y: u32, p: u32, ) -> Weight { + (52_076_000 as Weight) + // Standard Error: 0 + .saturating_add((4_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 77_000 + .saturating_add((194_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 3_000 + .saturating_add((188_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((329_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Prime (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_disapproved(x: u32, y: u32, p: u32, ) -> Weight { + (47_009_000 as Weight) + // Standard Error: 66_000 + .saturating_add((256_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 2_000 + .saturating_add((176_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 2_000 + .saturating_add((327_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:1 w:0) + // Storage: AllianceMotion ProposalOf (r:1 w:1) + // Storage: AllianceMotion Voting (r:1 w:1) + // Storage: AllianceMotion Members (r:1 w:0) + // Storage: AllianceMotion Prime (r:1 w:0) + // Storage: AllianceMotion Proposals (r:1 w:1) + fn close_approved(b: u32, x: u32, y: u32, p: u32, ) -> Weight { + (43_650_000 as Weight) + // Standard Error: 0 + .saturating_add((3_000 as Weight).saturating_mul(b as Weight)) + // Standard Error: 85_000 + .saturating_add((124_000 as Weight).saturating_mul(x as Weight)) + // Standard Error: 3_000 + .saturating_add((199_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 3_000 + .saturating_add((326_000 as Weight).saturating_mul(p as Weight)) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:3 w:3) + // Storage: AllianceMotion Members (r:1 w:1) + fn init_members(_x: u32, y: u32, z: u32, ) -> Weight { + (45_100_000 as Weight) + // Standard Error: 4_000 + .saturating_add((162_000 as Weight).saturating_mul(y as Weight)) + // Standard Error: 4_000 + .saturating_add((151_000 as Weight).saturating_mul(z as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Alliance Rule (r:0 w:1) + fn set_rule() -> Weight { + (14_517_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Announcements (r:1 w:1) + fn announce() -> Weight { + (16_801_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Announcements (r:1 w:1) + fn remove_announcement() -> Weight { + (17_133_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance UnscrupulousAccounts (r:1 w:0) + // Storage: Alliance Members (r:4 w:0) + // Storage: System Account (r:1 w:1) + // Storage: Alliance DepositOf (r:0 w:1) + fn join_alliance() -> Weight { + (95_370_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Alliance Members (r:4 w:0) + // Storage: Alliance UnscrupulousAccounts (r:1 w:0) + fn nominate_ally() -> Weight { + (44_764_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Members (r:3 w:2) + // Storage: AllianceMotion Proposals (r:1 w:0) + // Storage: AllianceMotion Members (r:0 w:1) + // Storage: AllianceMotion Prime (r:0 w:1) + fn elevate_ally() -> Weight { + (44_013_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Alliance KickingMembers (r:1 w:0) + // Storage: Alliance Members (r:3 w:1) + // Storage: AllianceMotion Proposals (r:1 w:0) + // Storage: Alliance DepositOf (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: AllianceMotion Members (r:0 w:1) + // Storage: AllianceMotion Prime (r:0 w:1) + fn retire() -> Weight { + (60_183_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + } + // Storage: Alliance KickingMembers (r:1 w:0) + // Storage: Alliance Members (r:3 w:1) + // Storage: AllianceMotion Proposals (r:1 w:0) + // Storage: Alliance DepositOf (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: AllianceMotion Members (r:0 w:1) + // Storage: AllianceMotion Prime (r:0 w:1) + fn kick_member() -> Weight { + (67_467_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(5 as Weight)) + } + // Storage: Alliance UnscrupulousAccounts (r:1 w:1) + // Storage: Alliance UnscrupulousWebsites (r:1 w:1) + fn add_unscrupulous_items(n: u32, l: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 16_000 + .saturating_add((2_673_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 7_000 + .saturating_add((224_000 as Weight).saturating_mul(l as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Alliance UnscrupulousAccounts (r:1 w:1) + // Storage: Alliance UnscrupulousWebsites (r:1 w:1) + fn remove_unscrupulous_items(n: u32, l: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 343_000 + .saturating_add((59_025_000 as Weight).saturating_mul(n as Weight)) + // Standard Error: 153_000 + .saturating_add((6_725_000 as Weight).saturating_mul(l as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } +} diff --git a/frame/collective/src/lib.rs b/frame/collective/src/lib.rs index 13d03562cce49..bc6d9ab3310e0 100644 --- a/frame/collective/src/lib.rs +++ b/frame/collective/src/lib.rs @@ -51,8 +51,10 @@ use frame_support::{ codec::{Decode, Encode}, dispatch::{DispatchError, DispatchResultWithPostInfo, Dispatchable, PostDispatchInfo}, ensure, - traits::{Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers}, - weights::{GetDispatchInfo, Weight}, + traits::{ + Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers, StorageVersion, + }, + weights::{GetDispatchInfo, Pays, Weight}, }; #[cfg(test)] @@ -435,7 +437,7 @@ pub mod pallet { let who = ensure_signed(origin)?; let members = Self::members(); ensure!(members.contains(&who), Error::::NotMember); - let proposal_len = proposal.using_encoded(|x| x.len()); + let proposal_len = proposal.encoded_size(); ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); let proposal_hash = T::Hashing::hash_of(&proposal); @@ -508,21 +510,8 @@ pub mod pallet { let members = Self::members(); ensure!(members.contains(&who), Error::::NotMember); - let proposal_len = proposal.using_encoded(|x| x.len()); - ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); - let proposal_hash = T::Hashing::hash_of(&proposal); - ensure!( - !>::contains_key(proposal_hash), - Error::::DuplicateProposal - ); - if threshold < 2 { - let seats = Self::members().len() as MemberCount; - let result = proposal.dispatch(RawOrigin::Members(1, seats).into()); - Self::deposit_event(Event::Executed { - proposal_hash, - result: result.map(|_| ()).map_err(|e| e.error), - }); + let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?; Ok(get_result_weight(result) .map(|w| { @@ -534,33 +523,13 @@ pub mod pallet { }) .into()) } else { - let active_proposals = - >::try_mutate(|proposals| -> Result { - proposals - .try_push(proposal_hash) - .map_err(|_| Error::::TooManyProposals)?; - Ok(proposals.len()) - })?; - let index = Self::proposal_count(); - >::mutate(|i| *i += 1); - >::insert(proposal_hash, *proposal); - let votes = { - let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); - Votes { index, threshold, ayes: vec![], nays: vec![], end } - }; - >::insert(proposal_hash, votes); - - Self::deposit_event(Event::Proposed { - account: who, - proposal_index: index, - proposal_hash, - threshold, - }); + let (proposal_len, active_proposals) = + Self::do_propose_proposed(who, threshold, proposal, length_bound)?; Ok(Some(T::WeightInfo::propose_proposed( - proposal_len as u32, // B - members.len() as u32, // M - active_proposals as u32, // P2 + proposal_len as u32, // B + members.len() as u32, // M + active_proposals, // P2 )) .into()) } @@ -592,46 +561,8 @@ pub mod pallet { let members = Self::members(); ensure!(members.contains(&who), Error::::NotMember); - let mut voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; - ensure!(voting.index == index, Error::::WrongIndex); - - let position_yes = voting.ayes.iter().position(|a| a == &who); - let position_no = voting.nays.iter().position(|a| a == &who); - // Detects first vote of the member in the motion - let is_account_voting_first_time = position_yes.is_none() && position_no.is_none(); - - if approve { - if position_yes.is_none() { - voting.ayes.push(who.clone()); - } else { - return Err(Error::::DuplicateVote.into()) - } - if let Some(pos) = position_no { - voting.nays.swap_remove(pos); - } - } else { - if position_no.is_none() { - voting.nays.push(who.clone()); - } else { - return Err(Error::::DuplicateVote.into()) - } - if let Some(pos) = position_yes { - voting.ayes.swap_remove(pos); - } - } - - let yes_votes = voting.ayes.len() as MemberCount; - let no_votes = voting.nays.len() as MemberCount; - Self::deposit_event(Event::Voted { - account: who, - proposal_hash: proposal, - voted: approve, - yes: yes_votes, - no: no_votes, - }); - - Voting::::insert(&proposal, voting); + let is_account_voting_first_time = Self::do_vote(who, proposal, index, approve)?; if is_account_voting_first_time { Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::No).into()) @@ -695,82 +626,7 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; - let voting = Self::voting(&proposal_hash).ok_or(Error::::ProposalMissing)?; - ensure!(voting.index == index, Error::::WrongIndex); - - let mut no_votes = voting.nays.len() as MemberCount; - let mut yes_votes = voting.ayes.len() as MemberCount; - let seats = Self::members().len() as MemberCount; - let approved = yes_votes >= voting.threshold; - let disapproved = seats.saturating_sub(no_votes) < voting.threshold; - // Allow (dis-)approving the proposal as soon as there are enough votes. - if approved { - let (proposal, len) = Self::validate_and_get_proposal( - &proposal_hash, - length_bound, - proposal_weight_bound, - )?; - Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - let (proposal_weight, proposal_count) = - Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); - return Ok(( - Some( - T::WeightInfo::close_early_approved(len as u32, seats, proposal_count) - .saturating_add(proposal_weight), - ), - Pays::Yes, - ) - .into()) - } else if disapproved { - Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - let proposal_count = Self::do_disapprove_proposal(proposal_hash); - return Ok(( - Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)), - Pays::No, - ) - .into()) - } - - // Only allow actual closing of the proposal after the voting period has ended. - ensure!( - frame_system::Pallet::::block_number() >= voting.end, - Error::::TooEarly - ); - - let prime_vote = Self::prime().map(|who| voting.ayes.iter().any(|a| a == &who)); - - // default voting strategy. - let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats); - - let abstentions = seats - (yes_votes + no_votes); - match default { - true => yes_votes += abstentions, - false => no_votes += abstentions, - } - let approved = yes_votes >= voting.threshold; - - if approved { - let (proposal, len) = Self::validate_and_get_proposal( - &proposal_hash, - length_bound, - proposal_weight_bound, - )?; - Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - let (proposal_weight, proposal_count) = - Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); - Ok(( - Some( - T::WeightInfo::close_approved(len as u32, seats, proposal_count) - .saturating_add(proposal_weight), - ), - Pays::Yes, - ) - .into()) - } else { - Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - let proposal_count = Self::do_disapprove_proposal(proposal_hash); - Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into()) - } + Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound) } /// Disapprove a proposal, close, and remove it from the system, regardless of its current @@ -817,6 +673,197 @@ impl, I: 'static> Pallet { Self::members().contains(who) } + /// Execute immediately when adding a new proposal. + pub fn do_propose_execute( + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let seats = Self::members().len() as MemberCount; + let result = proposal.dispatch(RawOrigin::Members(1, seats).into()); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + Ok((proposal_len as u32, result)) + } + + /// Add a new proposal to be voted. + pub fn do_propose_proposed( + who: T::AccountId, + threshold: MemberCount, + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, u32), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let active_proposals = + >::try_mutate(|proposals| -> Result { + proposals.try_push(proposal_hash).map_err(|_| Error::::TooManyProposals)?; + Ok(proposals.len()) + })?; + + let index = Self::proposal_count(); + >::mutate(|i| *i += 1); + >::insert(proposal_hash, proposal); + let votes = { + let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); + Votes { index, threshold, ayes: vec![], nays: vec![], end } + }; + >::insert(proposal_hash, votes); + + Self::deposit_event(Event::Proposed { + account: who, + proposal_index: index, + proposal_hash, + threshold, + }); + Ok((proposal_len as u32, active_proposals as u32)) + } + + /// Add an aye or nay vote for the member to the given proposal, returns true if it's the first + /// vote of the member in the motion + pub fn do_vote( + who: T::AccountId, + proposal: T::Hash, + index: ProposalIndex, + approve: bool, + ) -> Result { + let mut voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let position_yes = voting.ayes.iter().position(|a| a == &who); + let position_no = voting.nays.iter().position(|a| a == &who); + + // Detects first vote of the member in the motion + let is_account_voting_first_time = position_yes.is_none() && position_no.is_none(); + + if approve { + if position_yes.is_none() { + voting.ayes.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_no { + voting.nays.swap_remove(pos); + } + } else { + if position_no.is_none() { + voting.nays.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_yes { + voting.ayes.swap_remove(pos); + } + } + + let yes_votes = voting.ayes.len() as MemberCount; + let no_votes = voting.nays.len() as MemberCount; + Self::deposit_event(Event::Voted { + account: who, + proposal_hash: proposal, + voted: approve, + yes: yes_votes, + no: no_votes, + }); + + Voting::::insert(&proposal, voting); + + Ok(is_account_voting_first_time) + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + pub fn do_close( + proposal_hash: T::Hash, + index: ProposalIndex, + proposal_weight_bound: Weight, + length_bound: u32, + ) -> DispatchResultWithPostInfo { + let voting = Self::voting(&proposal_hash).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let mut no_votes = voting.nays.len() as MemberCount; + let mut yes_votes = voting.ayes.len() as MemberCount; + let seats = Self::members().len() as MemberCount; + let approved = yes_votes >= voting.threshold; + let disapproved = seats.saturating_sub(no_votes) < voting.threshold; + // Allow (dis-)approving the proposal as soon as there are enough votes. + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + return Ok(( + Some( + T::WeightInfo::close_early_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else if disapproved { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + return Ok(( + Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)), + Pays::No, + ) + .into()) + } + + // Only allow actual closing of the proposal after the voting period has ended. + ensure!(frame_system::Pallet::::block_number() >= voting.end, Error::::TooEarly); + + let prime_vote = Self::prime().map(|who| voting.ayes.iter().any(|a| a == &who)); + + // default voting strategy. + let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats); + + let abstentions = seats - (yes_votes + no_votes); + match default { + true => yes_votes += abstentions, + false => no_votes += abstentions, + } + let approved = yes_votes >= voting.threshold; + + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + Ok(( + Some( + T::WeightInfo::close_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + Ok((Some(T::WeightInfo::close_disapproved(seats, proposal_count)), Pays::No).into()) + } + } + /// Ensure that the right proposal bounds were passed and get the proposal from storage. /// /// Checks the length in storage via `storage::read` which adds an extra `size_of::() == 4` @@ -873,7 +920,8 @@ impl, I: 'static> Pallet { (proposal_weight, proposal_count) } - fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 { + /// Removes a proposal from the pallet, and deposit the `Disapproved` event. + pub fn do_disapprove_proposal(proposal_hash: T::Hash) -> u32 { // disapproved Self::deposit_event(Event::Disapproved { proposal_hash }); Self::remove_proposal(proposal_hash)