diff --git a/Cargo.lock b/Cargo.lock index a3cc547dfa4e7..073b2d2b55ed7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1690,18 +1690,18 @@ dependencies = [ [[package]] name = "enumflags2" -version = "0.6.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +checksum = "a8672257d642ffdd235f6e9c723c2326ac1253c8f3c022e7cfd2e57da55b1131" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.6.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +checksum = "33526f770a27828ce7c2792fdb7cb240220237e0ff12933ed6c23957fc5dd7cf" dependencies = [ "proc-macro2", "quote", @@ -4649,6 +4649,7 @@ dependencies = [ "hex-literal", "log 0.4.14", "node-primitives", + "pallet-alliance", "pallet-assets", "pallet-authority-discovery", "pallet-authorship", @@ -5042,6 +5043,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 0.4.14", + "pallet-balances", + "pallet-collective", + "pallet-identity", + "parity-scale-codec", + "scale-info", + "sha2 0.9.5", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-assets" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 71473a4bc5689..0ce725f5100e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,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/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index d434be8f3c609..c1a2491bc3af4 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -51,6 +51,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" } @@ -171,7 +172,8 @@ std = [ "log/std", "frame-try-runtime/std", "sp-npos-elections/std", - "sp-io/std" + "sp-io/std", + "pallet-alliance/std", ] runtime-benchmarks = [ "frame-benchmarking", @@ -179,6 +181,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "pallet-election-provider-multi-phase/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "pallet-alliance/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", @@ -217,6 +220,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 e315a45e698ce..2ecfaa897915c 100644 --- a/bin/node/runtime/src/impls.rs +++ b/bin/node/runtime/src/impls.rs @@ -17,8 +17,17 @@ //! Some configurable implementations as associated type for the substrate runtime. -use crate::{Authorship, Balances, NegativeImbalance}; -use frame_support::traits::{Currency, OnUnbalanced}; +use node_primitives::{AccountId, Hash}; +use sp_std::prelude::*; + +use frame_support::{ + dispatch::{DispatchError, DispatchResultWithPostInfo}, + traits::{Currency, OnUnbalanced}, + weights::Weight, +}; +use pallet_alliance::{IdentityVerifier, ProposalIndex, ProposalProvider}; + +use crate::{AllianceMotion, Authorship, Balances, Call, NegativeImbalance}; pub struct Author; impl OnUnbalanced for Author { @@ -27,6 +36,87 @@ impl OnUnbalanced for Author { } } +pub struct AllianceIdentityVerifier; +impl IdentityVerifier for AllianceIdentityVerifier { + #[cfg(not(feature = "runtime-benchmarks"))] + fn has_identity(who: &AccountId, fields: u64) -> bool { + crate::Identity::has_identity(who, fields) + } + + #[cfg(feature = "runtime-benchmarks")] + fn has_identity(_who: &AccountId, _fields: u64) -> bool { + true + } + + #[cfg(not(feature = "runtime-benchmarks"))] + 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() + .filter(|(_, j)| Judgement::KnownGood == *j || Judgement::Reasonable == *j) + .count() > 0 + } else { + false + } + } + + #[cfg(feature = "runtime-benchmarks")] + fn has_good_judgement(_who: &AccountId) -> bool { + true + } + + #[cfg(not(feature = "runtime-benchmarks"))] + fn super_account_id(who: &AccountId) -> Option { + crate::Identity::super_of(who).map(|parent| parent.0) + } + + #[cfg(feature = "runtime-benchmarks")] + fn super_account_id(_who: &AccountId) -> Option { + None + } +} + +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 4f620976c3abc..9a5a0ed9004b7 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -87,7 +87,7 @@ pub use sp_runtime::BuildStorage; /// Implementations of some helper traits passed into runtime modules as associated types. pub mod impls; -use impls::Author; +use impls::{AllianceIdentityVerifier, AllianceProposalProvider, Author}; /// Constant values used within the runtime. pub mod constants; @@ -1241,6 +1241,56 @@ impl pallet_transaction_storage::Config for Runtime { type WeightInfo = pallet_transaction_storage::weights::SubstrateWeight; } +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 MaxBlacklistCount: u32 = 100; + pub const MaxWebsiteUrlLength: u32 = 255; +} + +impl pallet_alliance::Config for Runtime { + type Event = Event; + type Proposal = Call; + type SuperMajorityOrigin = EnsureOneOf< + AccountId, + EnsureRoot, + pallet_collective::EnsureProportionMoreThan<_2, _3, AccountId, AllianceCollective>, + >; + type Currency = Balances; + type Slashed = Treasury; + type InitializeMembers = AllianceMotion; + type MembershipChanged = AllianceMotion; + type IdentityVerifier = AllianceIdentityVerifier; + type ProposalProvider = AllianceProposalProvider; + type MaxProposals = AllianceMaxProposals; + type MaxFounders = MaxFounders; + type MaxFellows = MaxFellows; + type MaxAllies = MaxAllies; + type MaxBlacklistCount = MaxBlacklistCount; + type MaxWebsiteUrlLength = MaxWebsiteUrlLength; + type CandidateDeposit = CandidateDeposit; + type WeightInfo = pallet_alliance::weights::SubstrateWeight; +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -1288,6 +1338,8 @@ construct_runtime!( Uniques: pallet_uniques::{Pallet, Call, Storage, Event}, TransactionStorage: pallet_transaction_storage::{Pallet, Call, Storage, Inherent, Config, Event}, BagsList: pallet_bags_list::{Pallet, Call, Storage, Event}, + AllianceMotion: pallet_collective::::{Pallet, Storage, Origin, Event}, + Alliance: pallet_alliance::::{Pallet, Call, Storage, Event}, } ); @@ -1620,6 +1672,7 @@ impl_runtime_apis! { let mut list = Vec::::new(); + list_benchmark!(list, extra, pallet_alliance, Alliance); list_benchmark!(list, extra, pallet_assets, Assets); list_benchmark!(list, extra, pallet_babe, Babe); list_benchmark!(list, extra, pallet_bags_list, BagsList); @@ -1694,6 +1747,7 @@ impl_runtime_apis! { let mut batches = Vec::::new(); let params = (&config, &whitelist); + add_benchmark!(params, batches, pallet_alliance, Alliance); add_benchmark!(params, batches, pallet_assets, Assets); add_benchmark!(params, batches, pallet_babe, Babe); add_benchmark!(params, batches, pallet_balances, Balances); diff --git a/frame/alliance/Cargo.toml b/frame/alliance/Cargo.toml new file mode 100644 index 0000000000000..b9c4ccdbd18a3 --- /dev/null +++ b/frame/alliance/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "pallet-alliance" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "Collective system: Members of a set of account IDs can make their collective feelings known through dispatched calls from one of two specialized origins." +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.9", default-features = false, optional = true } +log = { version = "0.4.14", default-features = false } + +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + +sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } +sp-core = { version = "4.0.0-dev", default-features = false, path = "../../primitives/core" } +sp-io = { version = "4.0.0-dev", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "4.0.0-dev", 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-collective = { version = "4.0.0-dev", path = "../collective", default-features = false, optional = true } +pallet-identity = { version = "4.0.0-dev", path = "../identity", default-features = false, optional = true } + +[dev-dependencies] +hex-literal = "0.3.1" +sha2 = "0.9" +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-collective = { version = "4.0.0-dev", path = "../collective" } +pallet-identity = { version = "4.0.0-dev", path = "../identity" } + +[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", +] +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..fb74b07201370 --- /dev/null +++ b/frame/alliance/README.md @@ -0,0 +1,74 @@ +# Alliance Pallet + +The Alliance Pallet provides a DAO to form an industry group that does two main things: + +- provide a set of ethics against bad behaviors. +- provide recognition and influence for those teams that contribute something back to the ecosystem. + +## Overview + +The Alliance first needs to initialize the Founders with sudo permissions. +After that, anyone with an approved identity and website can apply to become a Candidate. +Members will initiate a motion to determine whether a Candidate can join the Alliance or not. +The motion requires the approval of over 2/3 majority. +The Alliance can also maintain a blacklist list about accounts and websites. +Members can also vote to update the alliance's rule and make announcements. + +### Terminology + +- Rule: The IPFS Hash of the Alliance Rule for the community to read + and the alliance members to enforce for the management. + +- Announcement: An IPFS hash of some content that the Alliance want to announce. + +- Member: An account which is already in the group of the Alliance, + including three types: Founder, Fellow, Ally. + Member can also be kicked by super majority motion or retire by itself. + +- Founder: An account who is initiated by sudo 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 from Ally. + +- Ally: An account who is approved by Founders and Fellows from Candidate. + An Ally doesn't have voting rights. + +- Candidate: An account who is trying to become a member. + The applicant should already have an approved identity with website. + The application should be submitted by the account itself with some token as deposit, + or be nominated by an existing Founder or Fellow for free. + +- Blacklist: A list of bad websites and addresses, and can be added or removed items by Founders and Fellows. + +## Interface + +### Dispatchable Functions + +#### For General Users +- `submit_candidacy` - Submit the application to become a candidate with deposit. + +#### For Members (All) +- `retire` - Member retire to out of the Alliance and release its deposit. + +#### For Members (Founders/Fellows) + +- `propose` - Propose a motion. +- `vote` - Vote on a motion. +- `close` - Close a motion with enough votes or expired. +- `set_rule` - Initialize or update the alliance's rule by IPFS hash. +- `announce` - Make announcement by IPFS hash. +- `nominate_candidacy` - Nominate a non-member to become a Candidate for free. +- `approve_candidate` - Approve a candidate to become an Ally. +- `reject_candidate` - Reject a candidate and slash its deposit. +- `elevate_ally` - Approve an ally to become a Fellow. +- `kick_member` - Kick a member and slash its deposit. +- `add_blacklist` - Add some items of account and website in the blacklist. +- `remove_blacklist` - Remove some items of account and website from the blacklist. + +#### For Members (Only Founders) +- `veto` - Veto on a motion about `set_rule` and `elevate_ally`. + +#### For Super Users +- `init_founders` - Initialize the founding members. + +License: Apache-2.0 diff --git a/frame/alliance/src/benchmarking.rs b/frame/alliance/src/benchmarking.rs new file mode 100644 index 0000000000000..9a28ad2e591da --- /dev/null +++ b/frame/alliance/src/benchmarking.rs @@ -0,0 +1,811 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Alliance pallet benchmarking. + +use sp_runtime::traits::{Bounded, Hash, StaticLookup}; +use sp_std::{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()); + 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 candidate, I: 'static>(index: u32) -> T::AccountId { + funded_account::("candidate", index) +} + +fn outsider, I: 'static>(index: u32) -> T::AccountId { + funded_account::("outsider", index) +} + +fn blacklist, I: 'static>(index: u32) -> T::AccountId { + funded_account::("blacklist", index) +} + +fn set_members, I: 'static>() { + let founders = vec![founder::(1), founder::(2)]; + Members::::insert(MemberRole::Founder, founders.clone()); + + let fellows = vec![fellow::(1), fellow::(2)]; + fellows.iter().for_each(|who| { + T::Currency::reserve(&who, T::CandidateDeposit::get()).unwrap(); + >::insert(&who, T::CandidateDeposit::get()); + }); + Members::::insert(MemberRole::Fellow, fellows.clone()); + + let allies = vec![ally::(1)]; + allies.iter().for_each(|who| { + T::Currency::reserve(&who, T::CandidateDeposit::get()).unwrap(); + >::insert(&who, T::CandidateDeposit::get()); + }); + Members::::insert(MemberRole::Ally, allies); + + T::InitializeMembers::initialize_members(&[founders.as_slice(), fellows.as_slice()].concat()); +} + +fn set_candidates, I: 'static>(indexes: Vec) { + let candidates = indexes.into_iter().map(|i| candidate::(i)).collect::>(); + candidates.iter().for_each(|who| { + T::Currency::reserve(&who, T::CandidateDeposit::get()).unwrap(); + >::insert(&who, T::CandidateDeposit::get()); + }); + Candidates::::put(candidates); +} + +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.clone(), fellows.clone(), 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::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_eq!(Alliance::::rule(), Some(rule.clone())); + assert_last_event::(Event::NewRule(rule).into()); + } + + announce { + set_members::(); + + let announcement = announcement(b"hello world"); + + let call = Call::::announce { announcement: announcement.clone() }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(Alliance::::announcements().contains(&announcement)); + assert_last_event::(Event::NewAnnouncement(announcement).into()); + } + + remove_announcement { + set_members::(); + + let announcement = announcement(b"hello world"); + Announcements::::put(vec![announcement.clone()]); + + let call = Call::::remove_announcement { announcement: announcement.clone() }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(Alliance::::announcements().is_empty()); + assert_last_event::(Event::AnnouncementRemoved(announcement).into()); + } + + submit_candidacy { + set_members::(); + + let outsider = outsider::(1); + assert!(!Alliance::::is_member(&outsider)); + assert!(!Alliance::::is_candidate(&outsider)); + assert_eq!(DepositOf::::get(&outsider), None); + }: _(SystemOrigin::Signed(outsider.clone())) + verify { + assert!(!Alliance::::is_member(&outsider)); + assert!(Alliance::::is_candidate(&outsider)); + assert_eq!(DepositOf::::get(&outsider), Some(T::CandidateDeposit::get())); + assert_last_event::(Event::CandidateAdded(outsider, None, Some(T::CandidateDeposit::get())).into()); + } + + nominate_candidacy { + set_members::(); + + let founder1 = founder::(1); + assert!(Alliance::::is_member_of(&founder1, MemberRole::Founder)); + + let outsider = outsider::(1); + assert!(!Alliance::::is_member(&outsider)); + assert!(!Alliance::::is_candidate(&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(&outsider)); + assert!(Alliance::::is_candidate(&outsider)); + assert_eq!(DepositOf::::get(&outsider), None); + assert_last_event::(Event::CandidateAdded(outsider, Some(founder1), None).into()); + } + + approve_candidate { + set_members::(); + set_candidates::(vec![1]); + + let candidate1 = candidate::(1); + assert!(Alliance::::is_candidate(&candidate1)); + assert!(!Alliance::::is_member(&candidate1)); + assert_eq!(DepositOf::::get(&candidate1), Some(T::CandidateDeposit::get())); + + let candidate1_lookup: ::Source = T::Lookup::unlookup(candidate1.clone()); + let call = Call::::approve_candidate { candidate: candidate1_lookup }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(!Alliance::::is_candidate(&candidate1)); + assert!(Alliance::::is_ally(&candidate1)); + assert_eq!(DepositOf::::get(&candidate1), Some(T::CandidateDeposit::get())); + assert_last_event::(Event::CandidateApproved(candidate1).into()); + } + + reject_candidate { + set_members::(); + set_candidates::(vec![1]); + + let candidate1 = candidate::(1); + assert!(Alliance::::is_candidate(&candidate1)); + assert!(!Alliance::::is_member(&candidate1)); + assert_eq!(DepositOf::::get(&candidate1), Some(T::CandidateDeposit::get())); + + let candidate1_lookup: ::Source = T::Lookup::unlookup(candidate1.clone()); + let call = Call::::reject_candidate { candidate: candidate1_lookup }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(!Alliance::::is_candidate(&candidate1)); + assert!(!Alliance::::is_member(&candidate1)); + assert_eq!(DepositOf::::get(&candidate1), None); + assert_last_event::(Event::CandidateRejected(candidate1).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::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(!Alliance::::is_ally(&ally1)); + assert!(Alliance::::is_fellow(&ally1)); + assert_last_event::(Event::AllyElevated(ally1).into()); + } + + retire { + set_members::(); + + let fellow2 = fellow::(2); + assert!(Alliance::::is_fellow(&fellow2)); + assert!(!Alliance::::is_kicking(&fellow2)); + + assert_eq!(DepositOf::::get(&fellow2), Some(T::CandidateDeposit::get())); + }: _(SystemOrigin::Signed(fellow2.clone())) + verify { + assert!(!Alliance::::is_member(&fellow2)); + assert_eq!(DepositOf::::get(&fellow2), None); + assert_last_event::(Event::MemberRetired(fellow2, Some(T::CandidateDeposit::get())).into()); + } + + kick_member { + set_members::(); + + let fellow2 = fellow::(2); + KickingMembers::::insert(&fellow2, true); + + assert!(Alliance::::is_member_of(&fellow2, MemberRole::Fellow)); + assert!(Alliance::::is_kicking(&fellow2)); + + assert_eq!(DepositOf::::get(&fellow2), Some(T::CandidateDeposit::get())); + + let fellow2_lookup: ::Source = T::Lookup::unlookup(fellow2.clone()); + let call = Call::::kick_member { who: fellow2_lookup }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert!(!Alliance::::is_member(&fellow2)); + assert_eq!(DepositOf::::get(&fellow2), None); + assert_last_event::(Event::MemberKicked(fellow2, Some(T::CandidateDeposit::get())).into()); + } + + add_blacklist { + let n in 1 .. T::MaxBlacklistCount::get(); + let l in 1 .. T::MaxWebsiteUrlLength::get(); + + set_members::(); + + let accounts = (0 .. n).map(|i| blacklist::(i)).collect::>(); + let websites = (0 .. n).map(|i| vec![i as u8; l as usize]).collect::>(); + + let mut blacklist = Vec::with_capacity(accounts.len() + websites.len()); + blacklist.extend(accounts.into_iter().map(BlacklistItem::AccountId)); + blacklist.extend(websites.into_iter().map(BlacklistItem::Website)); + + let call = Call::::add_blacklist { infos: blacklist.clone() }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::BlacklistAdded(blacklist).into()); + } + + remove_blacklist { + let n in 1 .. T::MaxBlacklistCount::get(); + let l in 1 .. T::MaxWebsiteUrlLength::get(); + + set_members::(); + + let mut accounts = (0 .. n).map(|i| blacklist::(i)).collect::>(); + accounts.sort(); + AccountBlacklist::::put(accounts.clone()); + + let mut websites = (0 .. n).map(|i| vec![i as u8; l as usize]).collect::>(); + websites.sort(); + WebsiteBlacklist::::put(websites.clone()); + + let mut blacklist = Vec::with_capacity(accounts.len() + websites.len()); + blacklist.extend(accounts.into_iter().map(BlacklistItem::AccountId)); + blacklist.extend(websites.into_iter().map(BlacklistItem::Website)); + + let call = Call::::remove_blacklist { infos: blacklist.clone() }; + let origin = T::SuperMajorityOrigin::successful_origin(); + }: { call.dispatch_bypass_filter(origin)? } + verify { + assert_last_event::(Event::BlacklistRemoved(blacklist).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..458e00c097c0b --- /dev/null +++ b/frame/alliance/src/lib.rs @@ -0,0 +1,1062 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-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. + +//! # Alliance Pallet +//! +//! The Alliance Pallet provides a DAO to form an industry group that does two main things: +//! +//! - provide a set of ethics against bad behaviors. +//! - provide recognition and influence for those teams that contribute something back to the +//! ecosystem. +//! +//! ## Overview +//! +//! The Alliance first needs to initialize the Founders with sudo permissions. +//! After that, anyone with an approved identity and website can apply to become a Candidate. +//! Members will initiate a motion to determine whether a Candidate can join the Alliance or not. +//! The motion requires the approval of over 2/3 majority. +//! The Alliance can also maintain a blacklist list about accounts and websites. +//! Members can also vote to update the alliance's rule and make announcements. +//! +//! ### Terminology +//! +//! - Rule: The IPFS Hash of the Alliance Rule for the community to read and the alliance members to +//! enforce for the management. +//! +//! - Announcement: An IPFS hash of some content that the Alliance want to announce. +//! +//! - Member: An account which is already in the group of the Alliance, including three types: +//! Founder, Fellow, Ally. Member can also be kicked by super majority motion or retire by itself. +//! +//! - Founder: An account who is initiated by sudo 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 from Ally. +//! +//! - Ally: An account who is approved by Founders and Fellows from Candidate. An Ally doesn't have +//! voting rights. +//! +//! - Candidate: An account who is trying to become a member. The applicant should already have an +//! approved identity with website. The application should be submitted by the account itself with +//! some token as deposit, or be nominated by an existing Founder or Fellow for free. +//! +//! - Blacklist: A list of bad websites and addresses, and can be added or removed items by Founders +//! and Fellows. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! #### For General Users +//! - `submit_candidacy` - Submit the application to become a candidate with deposit. +//! +//! #### For Members (All) +//! - `retire` - Member retire to out of the Alliance and release its deposit. +//! +//! #### For Members (Founders/Fellows) +//! +//! - `propose` - Propose a motion. +//! - `vote` - Vote on a motion. +//! - `close` - Close a motion with enough votes or expired. +//! - `set_rule` - Initialize or update the alliance's rule by IPFS hash. +//! - `announce` - Make announcement by IPFS hash. +//! - `nominate_candidacy` - Nominate a non-member to become a Candidate for free. +//! - `approve_candidate` - Approve a candidate to become an Ally. +//! - `reject_candidate` - Reject a candidate and slash its deposit. +//! - `elevate_ally` - Approve an ally to become a Fellow. +//! - `kick_member` - Kick a member and slash its deposit. +//! - `add_blacklist` - Add some items of account and website in the blacklist. +//! - `remove_blacklist` - Remove some items of account and website from the blacklist. +//! +//! #### For Members (Only Founders) +//! - `veto` - Veto on a motion about `set_rule` and `elevate_ally`. +//! +//! #### For Super Users +//! - `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 sp_runtime::{ + traits::{StaticLookup, Zero}, + RuntimeDebug, +}; +use sp_std::prelude::*; + +use frame_support::{ + codec::{Decode, Encode}, + dispatch::{ + DispatchError, DispatchResult, DispatchResultWithPostInfo, Dispatchable, GetDispatchInfo, + PostDispatchInfo, + }, + ensure, + scale_info::TypeInfo, + traits::{ + ChangeMembers, Currency, Get, InitializeMembers, IsSubType, OnUnbalanced, + ReservableCurrency, + }, + weights::{Pays, Weight}, +}; + +pub use pallet::*; +pub use types::*; +pub use weights::*; + +/// Simple index type for proposal counting. +pub type ProposalIndex = u32; + +type Url = Vec; + +type BalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; +type NegativeImbalanceOf = <>::Currency as Currency< + ::AccountId, +>>::NegativeImbalance; + +pub trait IdentityVerifier { + fn has_identity(who: &AccountId, fields: u64) -> bool; + + fn has_good_judgement(who: &AccountId) -> bool; + + fn super_account_id(who: &AccountId) -> Option; +} + +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 role of members. +#[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo)] +pub enum MemberRole { + Founder, + Fellow, + Ally, +} + +/// The item type of blacklist. +#[derive(Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo)] +pub enum BlacklistItem { + AccountId(AccountId), + Website(Url), +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[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 from which the next tabled referendum may be forced; this allows for the tabling + /// of a majority-carries referendum. + type SuperMajorityOrigin: EnsureOrigin; + + /// The currency used for deposits. + type Currency: ReservableCurrency; + + /// What to do with slashed funds. + type Slashed: OnUnbalanced>; + + /// What to do with genesis voteable members + type InitializeMembers: InitializeMembers; + + /// The receiver of the signal for when the voteable members have changed. + type MembershipChanged: ChangeMembers; + + /// The identity verifier of 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 dependents 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 dependents 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 dependents keep to the limit without enforcing it. + type MaxAllies: Get; + + /// The maximum number of blacklist supported by the pallet. + #[pallet::constant] + type MaxBlacklistCount: Get; + + /// The maximum length of website url. + #[pallet::constant] + type MaxWebsiteUrlLength: Get; + + /// The amount of a deposit required for submitting candidacy. + #[pallet::constant] + type CandidateDeposit: 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, + /// Already be a candidate. + AlreadyCandidate, + /// Not be a candidate. + NotCandidate, + /// Already be a member. + AlreadyMember, + /// Not be a member. + NotMember, + /// Not be an ally member. + NotAlly, + /// Not be a founder member. + NotFounder, + /// Not be a kicking member. + NotKickingMember, + /// Not be a votable (founder or fellow) member. + NotVotableMember, + /// Already be an elevated (fellow) member. + AlreadyElevated, + /// Already be a blacklist item. + AlreadyInBlacklist, + /// Not be a blacklist item. + NotInBlacklist, + /// Number of blacklist exceed MaxBlacklist. + TooManyBlacklist, + /// Length of website url exceed MaxWebsiteUrlLength. + TooLongWebsiteUrl, + /// The member is kicking. + KickingMember, + /// Balance is insufficient to be a candidate. + InsufficientCandidateFunds, + /// The account's identity has not 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, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A new rule has been set. \[rule\] + NewRule(Cid), + /// A new announcement has been proposed. \[announcement\] + NewAnnouncement(Cid), + /// A on-chain announcement has been removed. \[announcement\] + AnnouncementRemoved(Cid), + /// Some accounts have been initialized to members (founders/fellows/allies). \[founders, + /// fellows, allies\] + MembersInitialized(Vec, Vec, Vec), + /// An account has been added as a candidate and lock its deposit. \[candidate, nominator, + /// reserved\] + CandidateAdded(T::AccountId, Option, Option>), + /// A proposal has been proposed to approve the candidate. \[candidate\] + CandidateApproved(T::AccountId), + /// A proposal has been proposed to reject the candidate. \[candidate\] + CandidateRejected(T::AccountId), + /// As an active member, an ally has been elevated to fellow. \[ally\] + AllyElevated(T::AccountId), + /// A member has retired to an ordinary account with its deposit unreserved. \[member, + /// unreserved\] + MemberRetired(T::AccountId, Option>), + /// A member has been kicked out to an ordinary account with its deposit slashed. \[member, + /// slashed\] + MemberKicked(T::AccountId, Option>), + /// Accounts or websites have been added into blacklist. \[items\] + BlacklistAdded(Vec>), + /// Accounts or websites have been removed from blacklist. \[items\] + BlacklistRemoved(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) { + #[cfg(not(test))] + { + 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!" + ); + Members::::insert(MemberRole::Founder, self.founders.clone()); + } + if !self.fellows.is_empty() { + assert!( + !Pallet::::has_member(MemberRole::Fellow), + "Fellows are already initialized!" + ); + Members::::insert(MemberRole::Fellow, self.fellows.clone()); + } + if !self.allies.is_empty() { + Members::::insert(MemberRole::Ally, self.allies.clone()) + } + + 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, other founders and fellows make a traditional + /// super-majority votes, vote to determine if the rules take effect. + /// + /// 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 the announcements. + #[pallet::storage] + #[pallet::getter(fn announcements)] + pub type Announcements, I: 'static = ()> = StorageValue<_, Vec, ValueQuery>; + + /// Maps member and their candidate deposit. + #[pallet::storage] + #[pallet::getter(fn deposit_of)] + pub type DepositOf, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf, OptionQuery>; + + /// The current set of candidates. + /// If the candidacy is approved by a motion, then it will become an ally member. + #[pallet::storage] + #[pallet::getter(fn candidates)] + pub type Candidates, I: 'static = ()> = + StorageValue<_, Vec, ValueQuery>; + + /// Maps member type to alliance members, including founder, fellow and ally. + /// Founders and fellows can propose and vote on alliance motions, + /// and ally can only wait to be elevated to fellow. + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members, I: 'static = ()> = + StorageMap<_, Twox64Concat, MemberRole, Vec, ValueQuery>; + + /// The members are being kicked out. They can't retire during the motion. + #[pallet::storage] + #[pallet::getter(fn kicking_member)] + pub type KickingMembers, I: 'static = ()> = + StorageMap<_, Blake2_128Concat, T::AccountId, bool, ValueQuery>; + + /// The current blacklist of accounts. The accounts can't submit candidacy. + #[pallet::storage] + #[pallet::getter(fn account_blacklist)] + pub type AccountBlacklist, I: 'static = ()> = + StorageValue<_, Vec, ValueQuery>; + + /// The current blacklist of websites. + #[pallet::storage] + #[pallet::getter(fn website_blacklist)] + pub type WebsiteBlacklist, I: 'static = ()> = + StorageValue<_, Vec, ValueQuery>; + + #[pallet::call] + impl, I: 'static> Pallet { + /// Add a new proposal to be voted on. + /// + /// Requires the sender to be founder or fellow. + #[pallet::weight(( + T::WeightInfo::propose_proposed( + *length_bound, // B + T::MaxFounders::get(), // X + T::MaxFellows::get(), // Y + T::MaxProposals::get(), // P2 + ), + DispatchClass::Operational + ))] + pub fn propose( + origin: OriginFor, + #[pallet::compact] threshold: u32, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let proposor = ensure_signed(origin)?; + ensure!(Self::is_votable_member(&proposor), Error::::NotVotableMember); + + 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(().into()) + } + + /// Add an aye or nay vote for the sender to the given proposal. + /// + /// Requires the sender to be founder or fellow. + #[pallet::weight(( + T::WeightInfo::vote(T::MaxFounders::get(), T::MaxFellows::get()), + DispatchClass::Operational + ))] + pub fn vote( + origin: OriginFor, + proposal: T::Hash, + #[pallet::compact] index: ProposalIndex, + approve: bool, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!(Self::is_votable_member(&who), Error::::NotVotableMember); + + T::ProposalProvider::vote_proposal(who, proposal, index, approve)?; + Ok(().into()) + } + + /// Disapprove 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) -> DispatchResultWithPostInfo { + let proposor = ensure_signed(origin)?; + ensure!(Self::is_founder(&proposor), Error::::NotFounder); + + let proposal = T::ProposalProvider::proposal_of(proposal_hash); + ensure!(proposal.is_some(), Error::::MissingProposalHash); + match proposal.expect("proposal must be exist; qed").is_sub_type() { + Some(Call::set_rule { .. }) | Some(Call::elevate_ally { .. }) => { + T::ProposalProvider::veto_proposal(proposal_hash); + Ok(().into()) + }, + _ => Err(Error::::NotVetoableProposal.into()), + } + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + /// + /// Requires the sender to be 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) + }, + DispatchClass::Operational + ))] + 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::is_votable_member(&who), Error::::NotVotableMember); + + let proposal = T::ProposalProvider::proposal_of(proposal_hash); + ensure!(proposal.is_some(), Error::::MissingProposalHash); + + let info = T::ProposalProvider::close_proposal( + proposal_hash, + index, + proposal_weight_bound, + length_bound, + )?; + if Pays::No == info.pays_fee { + if let Some(Call::kick_member { who }) = + proposal.expect("proposal must be exist; qed").is_sub_type() + { + let strike = T::Lookup::lookup(who.clone())?; + >::remove(strike); + } + } + Ok(info.into()) + } + + /// Initialize the founders/fellows/allies. + /// + /// This should only be called once. + #[pallet::weight(T::WeightInfo::init_members( + T::MaxFounders::get(), + T::MaxFellows::get(), + T::MaxAllies::get() + ))] + pub fn init_members( + origin: OriginFor, + mut founders: Vec, + mut fellows: Vec, + mut allies: Vec, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + 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, fellows, allies)); + Ok(().into()) + } + + /// Set a new IPFS cid to the alliance rule. + #[pallet::weight(T::WeightInfo::set_rule())] + pub fn set_rule(origin: OriginFor, rule: Cid) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + + Rule::::put(&rule); + + Self::deposit_event(Event::NewRule(rule)); + Ok(().into()) + } + + /// Make a new announcement by a new IPFS cid about the alliance issues. + #[pallet::weight(T::WeightInfo::announce())] + pub fn announce(origin: OriginFor, announcement: Cid) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + + let mut announcements = >::get(); + announcements.push(announcement.clone()); + >::put(announcements); + + Self::deposit_event(Event::NewAnnouncement(announcement)); + Ok(().into()) + } + + /// Remove the announcement. + #[pallet::weight(T::WeightInfo::remove_announcement())] + pub fn remove_announcement( + origin: OriginFor, + announcement: Cid, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::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(().into()) + } + + /// Submit oneself for candidacy. A fixed amount of deposit is recorded. + #[pallet::weight(T::WeightInfo::submit_candidacy())] + pub fn submit_candidacy(origin: OriginFor) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!(!Self::is_account_blacklist(&who), Error::::AlreadyInBlacklist); + ensure!(!Self::is_candidate(&who), Error::::AlreadyCandidate); + 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::CandidateDeposit::get(); + T::Currency::reserve(&who, deposit) + .map_err(|_| Error::::InsufficientCandidateFunds)?; + >::insert(&who, deposit); + + let res = Self::add_candidate(&who); + debug_assert!(res.is_ok()); + + Self::deposit_event(Event::CandidateAdded(who, None, Some(deposit))); + Ok(().into()) + } + + /// Founder or fellow can nominate someone to join the alliance and become a candidate. + /// There is no deposit required to the nominator or nominee. + #[pallet::weight(T::WeightInfo::nominate_candidacy())] + pub fn nominate_candidacy( + origin: OriginFor, + who: ::Source, + ) -> DispatchResultWithPostInfo { + let nominator = ensure_signed(origin)?; + ensure!(Self::is_votable_member(&nominator), Error::::NotVotableMember); + let who = T::Lookup::lookup(who)?; + ensure!(!Self::is_account_blacklist(&who), Error::::AlreadyInBlacklist); + ensure!(!Self::is_candidate(&who), Error::::AlreadyCandidate); + 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 res = Self::add_candidate(&who); + debug_assert!(res.is_ok()); + + Self::deposit_event(Event::CandidateAdded(who, Some(nominator), None)); + Ok(().into()) + } + + /// Approve a `Candidate` to become an `Ally`. + #[pallet::weight(T::WeightInfo::approve_candidate())] + pub fn approve_candidate( + origin: OriginFor, + candidate: ::Source, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + let candidate = T::Lookup::lookup(candidate)?; + ensure!(Self::is_candidate(&candidate), Error::::NotCandidate); + ensure!(!Self::is_member(&candidate), Error::::AlreadyMember); + + Self::remove_candidate(&candidate)?; + Self::add_member(&candidate, MemberRole::Ally)?; + + Self::deposit_event(Event::CandidateApproved(candidate)); + Ok(().into()) + } + + /// Reject a `Candidate` back to an ordinary account. + #[pallet::weight(T::WeightInfo::reject_candidate())] + pub fn reject_candidate( + origin: OriginFor, + candidate: ::Source, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + let candidate = T::Lookup::lookup(candidate)?; + ensure!(Self::is_candidate(&candidate), Error::::NotCandidate); + ensure!(!Self::is_member(&candidate), Error::::AlreadyMember); + + Self::remove_candidate(&candidate)?; + if let Some(deposit) = DepositOf::::take(&candidate) { + T::Slashed::on_unbalanced(T::Currency::slash_reserved(&candidate, deposit).0); + } + + Self::deposit_event(Event::CandidateRejected(candidate)); + Ok(().into()) + } + + /// Elevate an ally to fellow. + #[pallet::weight(T::WeightInfo::reject_candidate())] + pub fn elevate_ally( + origin: OriginFor, + ally: ::Source, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + let ally = T::Lookup::lookup(ally)?; + ensure!(Self::is_ally(&ally), Error::::NotAlly); + ensure!(!Self::is_votable_member(&ally), Error::::AlreadyElevated); + + Self::remove_member(&ally, MemberRole::Ally)?; + Self::add_member(&ally, MemberRole::Fellow)?; + + Self::deposit_event(Event::AllyElevated(ally)); + Ok(().into()) + } + + /// As a member, retire and back to an ordinary account and unlock its deposit. + #[pallet::weight(T::WeightInfo::retire())] + pub fn retire(origin: OriginFor) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + ensure!(!Self::is_kicking(&who), Error::::KickingMember); + + 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(who, deposit)); + Ok(().into()) + } + + /// Kick a member to ordinary account with its deposit slashed. + #[pallet::weight(T::WeightInfo::kick_member())] + pub fn kick_member( + origin: OriginFor, + who: ::Source, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + let member = T::Lookup::lookup(who)?; + ensure!(Self::is_kicking(&member), Error::::NotKickingMember); + + 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); + } + Self::deposit_event(Event::MemberKicked(member, deposit)); + Ok(().into()) + } + + /// Add accounts or websites into blacklist. + #[pallet::weight(T::WeightInfo::add_blacklist(infos.len() as u32, T::MaxWebsiteUrlLength::get()))] + pub fn add_blacklist( + origin: OriginFor, + infos: Vec>, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + + let mut accounts = vec![]; + let mut webs = vec![]; + for info in infos.iter() { + ensure!(!Self::is_blacklist(info), Error::::AlreadyInBlacklist); + match info { + BlacklistItem::AccountId(who) => accounts.push(who.clone()), + BlacklistItem::Website(url) => { + ensure!( + url.len() as u32 <= T::MaxWebsiteUrlLength::get(), + Error::::TooLongWebsiteUrl + ); + webs.push(url.clone()); + }, + } + } + + let account_blacklist_len = AccountBlacklist::::decode_len().unwrap_or_default(); + ensure!( + (account_blacklist_len + accounts.len()) as u32 <= T::MaxBlacklistCount::get(), + Error::::TooManyBlacklist + ); + let web_blacklist_len = WebsiteBlacklist::::decode_len().unwrap_or_default(); + ensure!( + (web_blacklist_len + webs.len()) as u32 <= T::MaxBlacklistCount::get(), + Error::::TooManyBlacklist + ); + + Self::do_add_blacklist(&mut accounts, &mut webs)?; + Self::deposit_event(Event::BlacklistAdded(infos)); + Ok(().into()) + } + + /// Remove accounts or websites from blacklist. + #[pallet::weight(>::WeightInfo::remove_blacklist(infos.len() as u32, T::MaxWebsiteUrlLength::get()))] + pub fn remove_blacklist( + origin: OriginFor, + infos: Vec>, + ) -> DispatchResultWithPostInfo { + T::SuperMajorityOrigin::ensure_origin(origin)?; + let mut accounts = vec![]; + let mut webs = vec![]; + for info in infos.iter() { + ensure!(Self::is_blacklist(info), Error::::NotInBlacklist); + match info { + BlacklistItem::AccountId(who) => accounts.push(who.clone()), + BlacklistItem::Website(url) => webs.push(url.clone()), + } + } + Self::do_remove_blacklist(&mut accounts, &mut webs)?; + Self::deposit_event(Event::BlacklistRemoved(infos)); + Ok(().into()) + } + } +} + +impl, I: 'static> Pallet { + /// Check if a user is a candidate. + pub fn is_candidate(who: &T::AccountId) -> bool { + >::get().contains(who) + } + + /// Add a candidate to the sorted candidate list. + fn add_candidate(who: &T::AccountId) -> DispatchResult { + let mut candidates = >::get(); + let pos = candidates.binary_search(who).err().ok_or(Error::::AlreadyCandidate)?; + candidates.insert(pos, who.clone()); + Candidates::::put(candidates); + Ok(()) + } + + /// Remove a candidate from the candidates list. + fn remove_candidate(who: &T::AccountId) -> DispatchResult { + let mut candidates = >::get(); + let pos = candidates.binary_search(who).ok().ok_or(Error::::NotCandidate)?; + candidates.remove(pos); + Candidates::::put(candidates); + Ok(()) + } + + fn has_member(role: MemberRole) -> bool { + !Members::::get(role).is_empty() + } + + 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() + } + + pub fn is_member_of(who: &T::AccountId, role: MemberRole) -> bool { + Members::::get(role).contains(&who) + } + + fn is_founder(who: &T::AccountId) -> bool { + Self::is_member_of(who, MemberRole::Founder) + } + + fn is_fellow(who: &T::AccountId) -> bool { + Self::is_member_of(who, MemberRole::Fellow) + } + + fn is_ally(who: &T::AccountId) -> bool { + Self::is_member_of(who, MemberRole::Ally) + } + + fn is_votable_member(who: &T::AccountId) -> bool { + Self::is_founder(who) || Self::is_fellow(who) + } + + fn votable_member_sorted() -> Vec { + let mut founders = Members::::get(MemberRole::Founder); + let mut fellows = Members::::get(MemberRole::Fellow); + founders.append(&mut fellows); + founders.sort(); + founders + } + + fn is_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 { + let mut members = >::get(role); + let pos = members.binary_search(who).err().ok_or(Error::::AlreadyMember)?; + members.insert(pos, who.clone()); + Members::::insert(role, members); + + if role == MemberRole::Founder || role == MemberRole::Fellow { + let members = Self::votable_member_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 { + let mut members = >::get(role); + let pos = members.binary_search(who).ok().ok_or(Error::::NotMember)?; + members.remove(pos); + Members::::insert(role, members); + + if role == MemberRole::Founder || role == MemberRole::Fellow { + let members = Self::votable_member_sorted(); + T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]); + } + Ok(()) + } + + /// Check if a user is in blacklist. + fn is_blacklist(info: &BlacklistItem) -> bool { + match info { + BlacklistItem::Website(url) => >::get().contains(url), + BlacklistItem::AccountId(who) => >::get().contains(who), + } + } + + /// Check if a user is in account blacklist. + fn is_account_blacklist(who: &T::AccountId) -> bool { + >::get().contains(who) + } + + /// Add a identity info to the blacklist set. + fn do_add_blacklist( + new_accounts: &mut Vec, + new_webs: &mut Vec, + ) -> DispatchResult { + if !new_accounts.is_empty() { + let mut accounts = >::get(); + accounts.append(new_accounts); + accounts.sort(); + AccountBlacklist::::put(accounts); + } + if !new_webs.is_empty() { + let mut webs = >::get(); + webs.append(new_webs); + webs.sort(); + WebsiteBlacklist::::put(webs); + } + Ok(()) + } + + /// Remove a identity info from the blacklist. + fn do_remove_blacklist( + out_accounts: &mut Vec, + out_webs: &mut Vec, + ) -> DispatchResult { + if !out_accounts.is_empty() { + let mut accounts = >::get(); + for who in out_accounts.iter() { + let pos = accounts.binary_search(who).ok().ok_or(Error::::NotInBlacklist)?; + accounts.remove(pos); + } + AccountBlacklist::::put(accounts); + } + if !out_webs.is_empty() { + let mut webs = >::get(); + for web in out_webs.iter() { + let pos = webs.binary_search(web).ok().ok_or(Error::::NotInBlacklist)?; + webs.remove(pos); + } + WebsiteBlacklist::::put(webs); + } + Ok(()) + } + + fn has_identity(who: &T::AccountId) -> DispatchResult { + const IDENTITY_FIELD_DISPLAY: u64 = + 0b0000000000000000000000000000000000000000000000000000000000000001; + const IDENTITY_FIELD_WEB: u64 = + 0b0000000000000000000000000000000000000000000000000000000000000100; + + 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..c71ae4d1a0ef2 --- /dev/null +++ b/frame/alliance/src/mock.rs @@ -0,0 +1,330 @@ +// 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::TryInto; + +pub use frame_support::{ + assert_ok, ord_parameter_types, parameter_types, traits::SortedMembers, BoundedVec, +}; +pub use frame_system::{EnsureOneOf, 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 = (); +} + +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 = EnsureOneOf, EnsureSignedBy>; +type EnsureTwoOrRoot = EnsureOneOf, 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 { + #[cfg(not(feature = "runtime-benchmarks"))] + fn has_identity(who: &u64, fields: u64) -> bool { + Identity::has_identity(who, fields) + } + + #[cfg(feature = "runtime-benchmarks")] + fn has_identity(_who: &u64, _fields: u64) -> bool { + true + } + + #[cfg(not(feature = "runtime-benchmarks"))] + fn has_good_judgement(who: &u64) -> bool { + if let Some(judgements) = + Identity::identity(who).map(|registration| registration.judgements) + { + judgements + .iter() + .filter(|(_, j)| Judgement::KnownGood == *j || Judgement::Reasonable == *j) + .count() > 0 + } else { + false + } + } + + #[cfg(feature = "runtime-benchmarks")] + fn has_good_judgement(_who: &u64) -> bool { + true + } + + #[cfg(not(feature = "runtime-benchmarks"))] + fn super_account_id(who: &u64) -> Option { + Identity::super_of(who).map(|parent| parent.0) + } + + #[cfg(feature = "runtime-benchmarks")] + fn super_account_id(_who: &u64) -> Option { + None + } +} + +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 CandidateDeposit: u64 = 25; + pub const MaxBlacklistCount: u32 = 100; + pub const MaxWebsiteUrlLength: u32 = 255; +} +impl Config for Test { + type Event = Event; + type Proposal = Call; + type SuperMajorityOrigin = EnsureSignedBy; + type Currency = Balances; + type Slashed = (); + type InitializeMembers = AllianceMotion; + type MembershipChanged = AllianceMotion; + type IdentityVerifier = AllianceIdentityVerifier; + type ProposalProvider = AllianceProposalProvider; + type MaxProposals = MaxProposals; + type MaxFounders = MaxFounders; + type MaxFellows = MaxFellows; + type MaxAllies = MaxAllies; + type MaxBlacklistCount = MaxBlacklistCount; + type MaxWebsiteUrlLength = MaxWebsiteUrlLength; + type CandidateDeposit = CandidateDeposit; + 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::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Identity: pallet_identity::{Pallet, Call, Storage, Event}, + AllianceMotion: pallet_collective::::{Pallet, Storage, Origin, Event}, + Alliance: pallet_alliance::{Pallet, Call, Storage, Event, Config}, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = GenesisConfig { + balances: pallet_balances::GenesisConfig { + balances: vec![(1, 50), (2, 50), (3, 50), (4, 50), (5, 30), (6, 50), (7, 50)], + }, + alliance: pallet_alliance::GenesisConfig { + founders: vec![], + fellows: vec![], + allies: vec![], + phantom: Default::default(), + }, + } + .build_storage() + .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..7d5a6f73953eb --- /dev/null +++ b/frame/alliance/src/tests.rs @@ -0,0 +1,503 @@ +// 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::::NotVotableMember + ); + + 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(1, 0, hash, 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( + 1, + 0, + hash.clone(), + 3 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted( + 2, + hash.clone(), + true, + 1, + 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( + 1, + 0, + hash.clone(), + 3 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Proposed( + 1, + 1, + vetoable_hash.clone(), + 3 + ))), + // record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted(2, hash.clone(), + // true, 2, 0))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Disapproved( + 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( + 1, + 0, + hash.clone(), + 3 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted( + 1, + hash.clone(), + true, + 1, + 0 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted( + 2, + hash.clone(), + true, + 2, + 0 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Voted( + 3, + hash.clone(), + true, + 3, + 0 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Closed( + hash.clone(), + 3, + 0 + ))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Approved(hash.clone()))), + record(mock::Event::AllianceMotion(AllianceMotionEvent::Executed( + hash.clone(), + 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::NewRule(cid))); + }); +} + +#[test] +fn announce_works() { + new_test_ext().execute_with(|| { + let cid = test_cid(); + assert_ok!(Alliance::announce(Origin::signed(1), cid.clone())); + assert_eq!(Alliance::announcements(), vec![cid.clone()]); + + System::assert_last_event(mock::Event::Alliance(crate::Event::NewAnnouncement(cid))); + }); +} + +#[test] +fn remove_announcement_works() { + new_test_ext().execute_with(|| { + let cid = test_cid(); + assert_ok!(Alliance::announce(Origin::signed(1), cid.clone())); + assert_eq!(Alliance::announcements(), vec![cid.clone()]); + System::assert_last_event(mock::Event::Alliance(crate::Event::NewAnnouncement( + cid.clone(), + ))); + + System::set_block_number(2); + + assert_ok!(Alliance::remove_announcement(Origin::signed(1), cid.clone())); + assert_eq!(Alliance::announcements(), vec![]); + System::assert_last_event(mock::Event::Alliance(crate::Event::AnnouncementRemoved(cid))); + }); +} + +#[test] +fn submit_candidacy_works() { + new_test_ext().execute_with(|| { + // check already member + assert_noop!( + Alliance::submit_candidacy(Origin::signed(1)), + Error::::AlreadyMember + ); + + // check already in blacklist + assert_ok!(Alliance::add_blacklist(Origin::signed(1), vec![BlacklistItem::AccountId(4)])); + assert_noop!( + Alliance::submit_candidacy(Origin::signed(4)), + Error::::AlreadyInBlacklist + ); + assert_ok!(Alliance::remove_blacklist( + Origin::signed(1), + vec![BlacklistItem::AccountId(4)] + )); + + // check deposit funds + assert_noop!( + Alliance::submit_candidacy(Origin::signed(5)), + Error::::InsufficientCandidateFunds + ); + + // success to submit + assert_ok!(Alliance::submit_candidacy(Origin::signed(4))); + assert_eq!(Alliance::deposit_of(4), Some(25)); + assert_eq!(Alliance::candidates(), vec![4]); + + // check already candidate + assert_noop!( + Alliance::submit_candidacy(Origin::signed(4)), + Error::::AlreadyCandidate + ); + + // check missing identity judgement + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::submit_candidacy(Origin::signed(6)), + Error::::WithoutGoodIdentityJudgement + ); + // check missing identity info + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::submit_candidacy(Origin::signed(7)), + Error::::WithoutIdentityDisplayAndWebsite + ); + }); +} + +#[test] +fn nominate_candidacy_works() { + new_test_ext().execute_with(|| { + // check already member + assert_noop!( + Alliance::nominate_candidacy(Origin::signed(1), 2), + Error::::AlreadyMember + ); + + // only votable member(founder/fellow) have nominate right + assert_noop!( + Alliance::nominate_candidacy(Origin::signed(5), 4), + Error::::NotVotableMember + ); + + // check already in blacklist + assert_ok!(Alliance::add_blacklist(Origin::signed(1), vec![BlacklistItem::AccountId(4)])); + assert_noop!( + Alliance::nominate_candidacy(Origin::signed(1), 4), + Error::::AlreadyInBlacklist + ); + assert_ok!(Alliance::remove_blacklist( + Origin::signed(1), + vec![BlacklistItem::AccountId(4)] + )); + + // success to nominate + assert_ok!(Alliance::nominate_candidacy(Origin::signed(1), 4)); + assert_eq!(Alliance::deposit_of(4), None); + assert_eq!(Alliance::candidates(), vec![4]); + + // check already candidate + assert_noop!( + Alliance::nominate_candidacy(Origin::signed(1), 4), + Error::::AlreadyCandidate + ); + + // check missing identity judgement + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::submit_candidacy(Origin::signed(6)), + Error::::WithoutGoodIdentityJudgement + ); + // check missing identity info + #[cfg(not(feature = "runtime-benchmarks"))] + assert_noop!( + Alliance::submit_candidacy(Origin::signed(7)), + Error::::WithoutIdentityDisplayAndWebsite + ); + }); +} + +#[test] +fn approve_candidate_works() { + new_test_ext().execute_with(|| { + assert_noop!( + Alliance::approve_candidate(Origin::signed(1), 4), + Error::::NotCandidate + ); + + assert_ok!(Alliance::submit_candidacy(Origin::signed(4))); + assert_eq!(Alliance::candidates(), vec![4]); + + assert_ok!(Alliance::approve_candidate(Origin::signed(1), 4)); + assert_eq!(Alliance::candidates(), Vec::::new()); + assert_eq!(Alliance::members(MemberRole::Ally), vec![4]); + }); +} + +#[test] +fn reject_candidate_works() { + new_test_ext().execute_with(|| { + assert_noop!( + Alliance::reject_candidate(Origin::signed(1), 4), + Error::::NotCandidate + ); + + assert_ok!(Alliance::submit_candidacy(Origin::signed(4))); + assert_eq!(Alliance::deposit_of(4), Some(25)); + assert_eq!(Alliance::candidates(), vec![4]); + + assert_ok!(Alliance::reject_candidate(Origin::signed(1), 4)); + assert_eq!(Alliance::deposit_of(4), None); + assert_eq!(Alliance::candidates(), Vec::::new()); + }); +} + +#[test] +fn elevate_ally_works() { + new_test_ext().execute_with(|| { + assert_noop!(Alliance::elevate_ally(Origin::signed(1), 4), Error::::NotAlly); + + assert_ok!(Alliance::submit_candidacy(Origin::signed(4))); + assert_ok!(Alliance::approve_candidate(Origin::signed(1), 4)); + assert_eq!(Alliance::members(MemberRole::Ally), vec![4]); + assert_eq!(Alliance::members(MemberRole::Fellow), vec![3]); + + assert_ok!(Alliance::elevate_ally(Origin::signed(1), 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::::KickingMember); + + 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(|| { + assert_noop!( + Alliance::kick_member(Origin::signed(1), 2), + Error::::NotKickingMember + ); + + 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::kicking_member(2), true); + assert_eq!(Alliance::members(MemberRole::Founder), vec![1, 2]); + + assert_ok!(Alliance::kick_member(Origin::signed(1), 2)); + assert_eq!(Alliance::members(MemberRole::Founder), vec![1]); + }); +} + +#[test] +fn add_blacklist_works() { + new_test_ext().execute_with(|| { + assert_ok!(Alliance::add_blacklist( + Origin::signed(1), + vec![BlacklistItem::AccountId(3), BlacklistItem::Website("abc".as_bytes().to_vec())] + )); + assert_eq!(Alliance::account_blacklist(), vec![3]); + assert_eq!(Alliance::website_blacklist(), vec!["abc".as_bytes().to_vec()]); + + assert_noop!( + Alliance::add_blacklist(Origin::signed(1), vec![BlacklistItem::AccountId(3)]), + Error::::AlreadyInBlacklist + ); + }); +} + +#[test] +fn remove_blacklist_works() { + new_test_ext().execute_with(|| { + assert_noop!( + Alliance::remove_blacklist(Origin::signed(1), vec![BlacklistItem::AccountId(3)]), + Error::::NotInBlacklist + ); + + assert_ok!(Alliance::add_blacklist(Origin::signed(1), vec![BlacklistItem::AccountId(3)])); + assert_eq!(Alliance::account_blacklist(), vec![3]); + assert_ok!(Alliance::remove_blacklist( + Origin::signed(1), + vec![BlacklistItem::AccountId(3)] + )); + assert_eq!(Alliance::account_blacklist(), Vec::::new()); + }); +} diff --git a/frame/alliance/src/types.rs b/frame/alliance/src/types.rs new file mode 100644 index 0000000000000..b245482dc5368 --- /dev/null +++ b/frame/alliance/src/types.rs @@ -0,0 +1,76 @@ +// 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. + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; +use sp_std::prelude::*; + +/// A Multihash instance that only supports the basic functionality and no hashing. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, RuntimeDebug, Encode, Decode, TypeInfo)] +pub struct Multihash { + /// The code of the Multihash. + pub code: u64, + /// The digest. + pub digest: Vec, +} + +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, Hash, RuntimeDebug, Encode, Decode, TypeInfo, +)] +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, Hash, RuntimeDebug, Encode, Decode, TypeInfo)] +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!(digest.len() == 32); + + Self { version: Version::V0, codec: DAG_PB, hash: Multihash { code: SHA2_256, digest } } + } +} diff --git a/frame/alliance/src/weights.rs b/frame/alliance/src/weights.rs new file mode 100644 index 0000000000000..45cca8c980051 --- /dev/null +++ b/frame/alliance/src/weights.rs @@ -0,0 +1,523 @@ +// 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 submit_candidacy() -> Weight; + fn nominate_candidacy() -> Weight; + fn approve_candidate() -> Weight; + fn reject_candidate() -> Weight; + fn elevate_ally() -> Weight; + fn retire() -> Weight; + fn kick_member() -> Weight; + fn add_blacklist(n: u32, l: u32, ) -> Weight; + fn remove_blacklist(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 AccountBlacklist (r:1 w:0) + // Storage: Alliance Candidates (r:1 w:1) + // Storage: Alliance Members (r:4 w:0) + // Storage: System Account (r:1 w:1) + // Storage: Alliance DepositOf (r:0 w:1) + fn submit_candidacy() -> 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 AccountBlacklist (r:1 w:0) + // Storage: Alliance Candidates (r:1 w:1) + fn nominate_candidacy() -> 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 Candidates (r:1 w:1) + // Storage: Alliance Members (r:4 w:1) + fn approve_candidate() -> Weight { + (71_876_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Alliance Candidates (r:1 w:1) + // Storage: Alliance Members (r:4 w:0) + // Storage: Alliance DepositOf (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn reject_candidate() -> Weight { + (69_732_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: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 AccountBlacklist (r:1 w:1) + // Storage: Alliance WebsiteBlacklist (r:1 w:1) + fn add_blacklist(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 AccountBlacklist (r:1 w:1) + // Storage: Alliance WebsiteBlacklist (r:1 w:1) + fn remove_blacklist(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 AccountBlacklist (r:1 w:0) + // Storage: Alliance Candidates (r:1 w:1) + // Storage: Alliance Members (r:4 w:0) + // Storage: System Account (r:1 w:1) + // Storage: Alliance DepositOf (r:0 w:1) + fn submit_candidacy() -> 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 AccountBlacklist (r:1 w:0) + // Storage: Alliance Candidates (r:1 w:1) + fn nominate_candidacy() -> Weight { + (44_764_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(6 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Alliance Candidates (r:1 w:1) + // Storage: Alliance Members (r:4 w:1) + fn approve_candidate() -> Weight { + (71_876_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + // Storage: Alliance Candidates (r:1 w:1) + // Storage: Alliance Members (r:4 w:0) + // Storage: Alliance DepositOf (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn reject_candidate() -> Weight { + (69_732_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(7 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 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 AccountBlacklist (r:1 w:1) + // Storage: Alliance WebsiteBlacklist (r:1 w:1) + fn add_blacklist(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 AccountBlacklist (r:1 w:1) + // Storage: Alliance WebsiteBlacklist (r:1 w:1) + fn remove_blacklist(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 89d4c8a150c36..45ebe1e7c9cd9 100644 --- a/frame/collective/src/lib.rs +++ b/frame/collective/src/lib.rs @@ -55,7 +55,7 @@ use frame_support::{ traits::{ Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers, StorageVersion, }, - weights::{GetDispatchInfo, Weight}, + weights::{GetDispatchInfo, Pays, Weight}, }; #[cfg(test)] @@ -503,21 +503,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.map(|_| ()).map_err(|e| e.error), - )); + let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?; Ok(get_result_weight(result) .map(|w| { @@ -529,28 +516,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(who, 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()) } @@ -582,40 +554,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(who, proposal, approve, yes_votes, 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()) @@ -679,82 +619,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_votes, 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_votes, 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_votes, 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_votes, 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 @@ -801,6 +666,186 @@ 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.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); + + let seats = Self::members().len() as MemberCount; + let result = proposal.dispatch(RawOrigin::Members(1, seats).into()); + Self::deposit_event(Event::Executed( + proposal_hash, + 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.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); + + 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(who, 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(who, proposal, approve, yes_votes, 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_votes, 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_votes, 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_votes, 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_votes, 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` @@ -857,7 +902,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) diff --git a/frame/identity/Cargo.toml b/frame/identity/Cargo.toml index 598be25c5ef38..ad43b46148c9c 100644 --- a/frame/identity/Cargo.toml +++ b/frame/identity/Cargo.toml @@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false, features = ["derive", "max-encoded-len"] } scale-info = { version = "1.0", default-features = false, features = ["derive"] } -enumflags2 = { version = "0.6.2" } +enumflags2 = { version = "0.7.1" } sp-std = { version = "4.0.0-dev", default-features = false, path = "../../primitives/std" } sp-io = { version = "4.0.0-dev", default-features = false, path = "../../primitives/io" } sp-runtime = { version = "4.0.0-dev", default-features = false, path = "../../primitives/runtime" } diff --git a/frame/identity/src/lib.rs b/frame/identity/src/lib.rs index a91381f1edd8b..c06d213db666e 100644 --- a/frame/identity/src/lib.rs +++ b/frame/identity/src/lib.rs @@ -81,13 +81,13 @@ pub mod weights; use frame_support::traits::{BalanceStatus, Currency, OnUnbalanced, ReservableCurrency}; use sp_runtime::traits::{AppendZerosInput, Saturating, StaticLookup, Zero}; use sp_std::{convert::TryInto, prelude::*}; -pub use weights::WeightInfo; pub use pallet::*; pub use types::{ Data, IdentityField, IdentityFields, IdentityInfo, Judgement, RegistrarIndex, RegistrarInfo, Registration, }; +pub use weights::WeightInfo; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -160,7 +160,7 @@ pub mod pallet { /// TWOX-NOTE: OK ― `AccountId` is a secure hash. #[pallet::storage] #[pallet::getter(fn identity)] - pub(super) type IdentityOf = StorageMap< + pub type IdentityOf = StorageMap< _, Twox64Concat, T::AccountId, @@ -172,7 +172,7 @@ pub mod pallet { /// context. If the account is not some other account's sub-identity, then just `None`. #[pallet::storage] #[pallet::getter(fn super_of)] - pub(super) type SuperOf = + pub type SuperOf = StorageMap<_, Blake2_128Concat, T::AccountId, (T::AccountId, Data), OptionQuery>; /// Alternative "sub" identities of this account. @@ -182,7 +182,7 @@ pub mod pallet { /// TWOX-NOTE: OK ― `AccountId` is a secure hash. #[pallet::storage] #[pallet::getter(fn subs_of)] - pub(super) type SubsOf = StorageMap< + pub type SubsOf = StorageMap< _, Twox64Concat, T::AccountId, @@ -196,7 +196,7 @@ pub mod pallet { /// The index into this can be cast to `RegistrarIndex` to get a valid value. #[pallet::storage] #[pallet::getter(fn registrars)] - pub(super) type Registrars = StorageValue< + pub type Registrars = StorageValue< _, BoundedVec, T::AccountId>>, T::MaxRegistrars>, ValueQuery, @@ -970,4 +970,13 @@ impl Pallet { .filter_map(|a| SuperOf::::get(&a).map(|x| (a, x.1))) .collect() } + + /// Check if the account has corresponding identity information by the identity field. + pub fn has_identity(who: &T::AccountId, fields: u64) -> bool { + if let Some(info) = IdentityOf::::get(who).map(|registration| registration.info) { + (info.fields().0.bits() & fields) == fields + } else { + false + } + } } diff --git a/frame/identity/src/tests.rs b/frame/identity/src/tests.rs index c842b0e2f64be..11304449ab2cb 100644 --- a/frame/identity/src/tests.rs +++ b/frame/identity/src/tests.rs @@ -500,3 +500,20 @@ fn setting_account_id_should_work() { assert_ok!(Identity::set_account_id(Origin::signed(4), 0, 3)); }); } + +#[test] +fn test_has_identity() { + new_test_ext().execute_with(|| { + assert_ok!(Identity::set_identity(Origin::signed(10), Box::new(ten()))); + assert!(Identity::has_identity(&10, IdentityField::Display as u64)); + assert!(Identity::has_identity(&10, IdentityField::Legal as u64)); + assert!(Identity::has_identity( + &10, + IdentityField::Display as u64 | IdentityField::Legal as u64 + )); + assert!(!Identity::has_identity( + &10, + IdentityField::Display as u64 | IdentityField::Legal as u64 | IdentityField::Web as u64 + )); + }); +} diff --git a/frame/identity/src/types.rs b/frame/identity/src/types.rs index ed6aeb18e96a1..127c4367862f6 100644 --- a/frame/identity/src/types.rs +++ b/frame/identity/src/types.rs @@ -17,7 +17,7 @@ use super::*; use codec::{Decode, Encode, MaxEncodedLen}; -use enumflags2::BitFlags; +use enumflags2::{bitflags, BitFlags}; use frame_support::{ traits::{ConstU32, Get}, BoundedVec, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, @@ -230,8 +230,9 @@ impl> { pub twitter: Data, } +impl> IdentityInfo { + pub(crate) fn fields(&self) -> IdentityFields { + let mut res = >::empty(); + if self.display != Data::None { + res.insert(IdentityField::Display); + } + if self.legal != Data::None { + res.insert(IdentityField::Legal); + } + if self.web != Data::None { + res.insert(IdentityField::Web); + } + if self.riot != Data::None { + res.insert(IdentityField::Riot); + } + if self.email != Data::None { + res.insert(IdentityField::Email); + } + if self.pgp_fingerprint.is_some() { + res.insert(IdentityField::PgpFingerprint); + } + if self.image != Data::None { + res.insert(IdentityField::Image); + } + if self.twitter != Data::None { + res.insert(IdentityField::Twitter); + } + IdentityFields(res) + } +} + /// Information concerning the identity of the controller of an account. /// /// NOTE: This is stored separately primarily to facilitate the addition of extra fields in a