From d450c2a59935d0c11e333d53bfd4c412595c22e8 Mon Sep 17 00:00:00 2001 From: iStrike7 Date: Mon, 14 Nov 2022 12:25:32 +0530 Subject: [PATCH 1/6] Council improvements - Added logs and ProposalCloseDelay --- Cargo.lock | 22 +++++++++++++++++++--- Cargo.toml | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1991a2e113828..a0fccbc06c4ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4840,7 +4840,7 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "orml-tokens" version = "0.4.1-dev" -source = "git+https://github.com/mangata-finance//open-runtime-module-library?branch=mangata-dev-v0.9.29#979ca6957cdf288f4f13105586ed7450d49f1e4f" +source = "git+https://github.com/mangata-finance//open-runtime-module-library?branch=mangata-dev#ebbad3d3b80fb1829bd01958da091a14d1ec76f1" dependencies = [ "frame-benchmarking", "frame-support", @@ -4857,7 +4857,7 @@ dependencies = [ [[package]] name = "orml-traits" version = "0.4.1-dev" -source = "git+https://github.com/mangata-finance//open-runtime-module-library?branch=mangata-dev-v0.9.29#979ca6957cdf288f4f13105586ed7450d49f1e4f" +source = "git+https://github.com/mangata-finance//open-runtime-module-library?branch=mangata-dev#ebbad3d3b80fb1829bd01958da091a14d1ec76f1" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -4875,7 +4875,7 @@ dependencies = [ [[package]] name = "orml-utilities" version = "0.4.1-dev" -source = "git+https://github.com/mangata-finance//open-runtime-module-library?branch=mangata-dev-v0.9.29#979ca6957cdf288f4f13105586ed7450d49f1e4f" +source = "git+https://github.com/mangata-finance//open-runtime-module-library?branch=mangata-dev#ebbad3d3b80fb1829bd01958da091a14d1ec76f1" dependencies = [ "frame-support", "parity-scale-codec", @@ -5243,6 +5243,22 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-collective-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-contracts" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 1e562d3ef2b6e..9208bde2dbf3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ members = [ "frame/bounties", "frame/child-bounties", "frame/collective", + "frame/collective-mangata", "frame/contracts", "frame/contracts/rpc", "frame/contracts/rpc/runtime-api", From 75f85871d0bb9e24511069b766db4b84cd1682dd Mon Sep 17 00:00:00 2001 From: iStrike7 Date: Mon, 14 Nov 2022 13:33:44 +0530 Subject: [PATCH 2/6] Council improvements - Added logs and ProposalCloseDelay --- frame/collective-mangata/Cargo.toml | 50 + frame/collective-mangata/README.md | 25 + frame/collective-mangata/src/benchmarking.rs | 644 ++++++++ frame/collective-mangata/src/lib.rs | 1248 +++++++++++++++ .../collective-mangata/src/migrations/mod.rs | 19 + frame/collective-mangata/src/migrations/v4.rs | 147 ++ frame/collective-mangata/src/tests.rs | 1418 +++++++++++++++++ frame/collective-mangata/src/weights.rs | 324 ++++ 8 files changed, 3875 insertions(+) create mode 100644 frame/collective-mangata/Cargo.toml create mode 100644 frame/collective-mangata/README.md create mode 100644 frame/collective-mangata/src/benchmarking.rs create mode 100644 frame/collective-mangata/src/lib.rs create mode 100644 frame/collective-mangata/src/migrations/mod.rs create mode 100644 frame/collective-mangata/src/migrations/v4.rs create mode 100644 frame/collective-mangata/src/tests.rs create mode 100644 frame/collective-mangata/src/weights.rs diff --git a/frame/collective-mangata/Cargo.toml b/frame/collective-mangata/Cargo.toml new file mode 100644 index 0000000000000..5b725264745e3 --- /dev/null +++ b/frame/collective-mangata/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "pallet-collective-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +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] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" } +sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime" +] diff --git a/frame/collective-mangata/README.md b/frame/collective-mangata/README.md new file mode 100644 index 0000000000000..444927e51da22 --- /dev/null +++ b/frame/collective-mangata/README.md @@ -0,0 +1,25 @@ +Collective system: Members of a set of account IDs can make their collective feelings known +through dispatched calls from one of two specialized origins. + +The membership can be provided in one of two ways: either directly, using the Root-dispatchable +function `set_members`, or indirectly, through implementing the `ChangeMembers`. +The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. + +A "prime" member may be set to help determine the default vote behavior based on chain +config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +abstentations will first follow the majority of the collective voting, and then the prime +member. + +Voting happens through motions comprising a proposal (i.e. a dispatchable) plus a +number of approvals required for it to pass and be called. Motions are open for members to +vote on for a minimum period given by `MotionDuration`. As soon as the required number of +approvals is given, the motion is closed and executed. If the number of approvals is not reached +during the voting period, then `close` may be called by any account in order to force the end +the motion explicitly. If a prime member is defined, then their vote is used instead of any +abstentions and the proposal is executed if there are enough approvals counting the new votes. + +If there are not, or if no prime member is set, then the motion is dropped without being executed. + +License: Apache-2.0 diff --git a/frame/collective-mangata/src/benchmarking.rs b/frame/collective-mangata/src/benchmarking.rs new file mode 100644 index 0000000000000..fcfe1e3e601c0 --- /dev/null +++ b/frame/collective-mangata/src/benchmarking.rs @@ -0,0 +1,644 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Staking pallet benchmarking. + +use super::*; +use crate::Pallet as Collective; + +use sp_runtime::traits::Bounded; +use sp_std::mem::size_of; + +use frame_benchmarking::{account, benchmarks_instance_pallet, whitelisted_caller}; +use frame_system::{Call as SystemCall, Pallet as System, RawOrigin as SystemOrigin}; + +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()); +} + +benchmarks_instance_pallet! { + set_members { + let m in 1 .. T::MaxMembers::get(); + let n in 1 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + // Set old members. + // We compute the difference of old and new members, so it should influence timing. + let mut old_members = vec![]; + let mut last_old_member = account::("old member", 0, SEED); + for i in 0 .. m { + last_old_member = account::("old member", i, SEED); + old_members.push(last_old_member.clone()); + } + let old_members_count = old_members.len() as u32; + + Collective::::set_members( + SystemOrigin::Root.into(), + old_members.clone(), + Some(last_old_member.clone()), + T::MaxMembers::get(), + )?; + + // Set a high threshold for proposals passing so that they stay around. + let threshold = m.max(2); + // Length of the proposals should be irrelevant to `set_members`. + let length = 100; + for i in 0 .. p { + // Proposals should be different so that different proposal hashes are generated + let proposal: T::Proposal = SystemCall::::remark { + remark: vec![i as u8; length] + }.into(); + Collective::::propose( + SystemOrigin::Signed(last_old_member.clone()).into(), + threshold, + Box::new(proposal.clone()), + MAX_BYTES, + )?; + let hash = T::Hashing::hash_of(&proposal); + // Vote on the proposal to increase state relevant for `set_members`. + // Not voting for `last_old_member` because they proposed and not voting for the first member + // to keep the proposal from passing. + for j in 2 .. m - 1 { + let voter = &old_members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + hash, + i, + approve, + )?; + } + } + + // Construct `new_members`. + // It should influence timing since it will sort this vector. + let mut new_members = vec![]; + let mut last_member = account::("member", 0, SEED); + for i in 0 .. n { + last_member = account::("member", i, SEED); + new_members.push(last_member.clone()); + } + + }: _(SystemOrigin::Root, new_members.clone(), Some(last_member), T::MaxMembers::get()) + verify { + new_members.sort(); + assert_eq!(Collective::::members(), new_members); + } + + execute { + let b in 1 .. MAX_BYTES; + let m in 1 .. T::MaxMembers::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let proposal: T::Proposal = SystemCall::::remark { remark: vec![1; b as usize] }.into(); + + }: _(SystemOrigin::Signed(caller), Box::new(proposal.clone()), bytes_in_storage) + verify { + let proposal_hash = T::Hashing::hash_of(&proposal); + // Note that execution fails due to mis-matched origin + assert_last_event::( + Event::MemberExecuted { proposal_hash, result: Err(DispatchError::BadOrigin) }.into() + ); + } + + // This tests when execution would happen immediately after proposal + propose_execute { + let b in 1 .. MAX_BYTES; + let m in 1 .. T::MaxMembers::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + let proposal: T::Proposal = SystemCall::::remark { remark: vec![1; b as usize] }.into(); + let threshold = 1; + + }: propose(SystemOrigin::Signed(caller), threshold, Box::new(proposal.clone()), bytes_in_storage) + verify { + let proposal_hash = T::Hashing::hash_of(&proposal); + // Note that execution fails due to mis-matched origin + assert_last_event::( + Event::Executed { proposal_hash, result: Err(DispatchError::BadOrigin) }.into() + ); + } + + // This tests when proposal is created and queued as "proposed" + propose_proposed { + let b in 1 .. MAX_BYTES; + let m in 2 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members, None, T::MaxMembers::get())?; + + 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 = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal), + bytes_in_storage, + )?; + } + + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + + let proposal: T::Proposal = SystemCall::::remark { remark: vec![p as u8; b as usize] }.into(); + + }: propose(SystemOrigin::Signed(caller.clone()), threshold, Box::new(proposal.clone()), bytes_in_storage) + verify { + // New proposal is recorded + assert_eq!(Collective::::proposals().len(), p as usize); + let proposal_hash = T::Hashing::hash_of(&proposal); + assert_last_event::(Event::Proposed { account: caller, proposal_index: p - 1, proposal_hash, threshold }.into()); + } + + vote { + // We choose 5 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 5 .. T::MaxMembers::get(); + + let p = T::MaxProposals::get(); + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + let proposer: T::AccountId = account::("proposer", 0, SEED); + members.push(proposer.clone()); + for i in 1 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let voter: T::AccountId = account::("voter", 0, SEED); + members.push(voter.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // 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 = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + 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]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + // Voter votes aye without resolving the vote. + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // 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, index, approve) + verify { + // All proposals exist and the last proposal has just been updated. + assert_eq!(Collective::::proposals().len(), p as usize); + let voting = Collective::::voting(&last_hash).ok_or("Proposal Missing")?; + assert_eq!(voting.ayes.len(), (m - 3) as usize); + assert_eq!(voting.nays.len(), 1); + } + + close_early_disapproved { + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + let proposer = account::("proposer", 0, SEED); + members.push(proposer.clone()); + for i in 1 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let voter = account::("voter", 0, SEED); + members.push(voter.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // 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 = SystemCall::::remark { + remark: vec![i as u8; bytes as usize] + }.into(); + Collective::::propose( + SystemOrigin::Signed(proposer.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + let index = p - 1; + // Have most everyone vote aye on last proposal, while keeping it from passing. + for j in 0 .. m - 2 { + let voter = &members[j as usize]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + // Voter votes aye without resolving the vote. + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Voter switches vote to nay, which kills the vote + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + + // 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()); + frame_system::Pallet::::set_block_number(Collective::::proposal_proposed_time(&last_hash).unwrap() + T::ProposalCloseDelay::get()); + }: close(SystemOrigin::Signed(voter), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + close_early_approved { + let b in 1 .. MAX_BYTES; + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members(SystemOrigin::Root.into(), members.clone(), None, T::MaxMembers::get())?; + + // 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 = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + // Caller switches vote to nay on their own proposal, allowing them to be the deciding approval vote + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + p - 1, + 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]; + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + p - 1, + approve, + )?; + } + + // Member zero is the first aye + Collective::::vote( + SystemOrigin::Signed(members[0].clone()).into(), + last_hash, + p - 1, + true, + )?; + + assert_eq!(Collective::::proposals().len(), p as usize); + + // Caller switches vote to aye, which passes the vote + let index = p - 1; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + index, approve, + )?; + + frame_system::Pallet::::set_block_number(Collective::::proposal_proposed_time(&last_hash).unwrap() + T::ProposalCloseDelay::get()); + + }: close(SystemOrigin::Signed(caller), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + // The last proposal is removed. + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Executed { proposal_hash: last_hash, result: Err(DispatchError::BadOrigin) }.into()); + } + + close_disapproved { + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes = 100; + let bytes_in_storage = bytes + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // 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 = SystemCall::::remark { + remark: vec![i as u8; bytes as usize] + }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&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]; + let approve = true; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + index, + approve, + )?; + } + + // caller is prime, prime votes nay + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + index, + false, + )?; + + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + // Prime nay will close it as disapproved + }: close(SystemOrigin::Signed(caller), last_hash, index, Weight::MAX, bytes_in_storage) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + close_approved { + let b in 1 .. MAX_BYTES; + // We choose 4 as a minimum so we always trigger a vote in the voting loop (`for j in ...`) + let m in 4 .. T::MaxMembers::get(); + let p in 1 .. T::MaxProposals::get(); + + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller: T::AccountId = whitelisted_caller(); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // 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 = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + // The prime member votes aye, so abstentions default to aye. + Collective::::vote( + SystemOrigin::Signed(caller.clone()).into(), + last_hash, + p - 1, + true // Vote aye. + )?; + + // 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]; + let approve = false; + Collective::::vote( + SystemOrigin::Signed(voter.clone()).into(), + last_hash, + p - 1, + approve + )?; + } + + // caller is prime, prime already votes aye by creating the proposal + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + // Prime aye will close it as approved + }: close(SystemOrigin::Signed(caller), last_hash, p - 1, Weight::MAX, bytes_in_storage) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Executed { proposal_hash: last_hash, result: Err(DispatchError::BadOrigin) }.into()); + } + + disapprove_proposal { + let p in 1 .. T::MaxProposals::get(); + + let m = 3; + let b = MAX_BYTES; + let bytes_in_storage = b + size_of::() as u32; + + // Construct `members`. + let mut members = vec![]; + for i in 0 .. m - 1 { + let member = account::("member", i, SEED); + members.push(member); + } + let caller = account::("caller", 0, SEED); + members.push(caller.clone()); + Collective::::set_members( + SystemOrigin::Root.into(), + members.clone(), + Some(caller.clone()), + T::MaxMembers::get(), + )?; + + // 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 = SystemCall::::remark { remark: vec![i as u8; b as usize] }.into(); + Collective::::propose( + SystemOrigin::Signed(caller.clone()).into(), + threshold, + Box::new(proposal.clone()), + bytes_in_storage, + )?; + last_hash = T::Hashing::hash_of(&proposal); + } + + System::::set_block_number(T::BlockNumber::max_value()); + assert_eq!(Collective::::proposals().len(), p as usize); + + }: _(SystemOrigin::Root, last_hash) + verify { + assert_eq!(Collective::::proposals().len(), (p - 1) as usize); + assert_last_event::(Event::Disapproved { proposal_hash: last_hash }.into()); + } + + impl_benchmark_test_suite!(Collective, crate::tests::new_test_ext(), crate::tests::Test); +} diff --git a/frame/collective-mangata/src/lib.rs b/frame/collective-mangata/src/lib.rs new file mode 100644 index 0000000000000..4554d8d2e4582 --- /dev/null +++ b/frame/collective-mangata/src/lib.rs @@ -0,0 +1,1248 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Collective system: Members of a set of account IDs can make their collective feelings known +//! through dispatched calls from one of two specialized origins. +//! +//! The membership can be provided in one of two ways: either directly, using the Root-dispatchable +//! function `set_members`, or indirectly, through implementing the `ChangeMembers`. +//! The pallet assumes that the amount of members stays at or below `MaxMembers` for its weight +//! calculations, but enforces this neither in `set_members` nor in `change_members_sorted`. +//! +//! A "prime" member may be set to help determine the default vote behavior based on chain +//! config. If `PrimeDefaultVote` is used, the prime vote acts as the default vote in case of any +//! abstentions after the voting period. If `MoreThanMajorityThenPrimeDefaultVote` is used, then +//! abstentions will first follow the majority of the collective voting, and then the prime +//! member. +//! +//! Voting happens through motions comprising a proposal (i.e. a curried dispatchable) plus a +//! number of approvals required for it to pass and be called. Motions are open for members to +//! vote on for a minimum period given by `MotionDuration`. As soon as the needed number of +//! approvals is given, the motion is closed and executed. If the number of approvals is not reached +//! during the voting period, then `close` may be called by any account in order to force the end +//! the motion explicitly. If a prime member is defined then their vote is used in place of any +//! abstentions and the proposal is executed if there are enough approvals counting the new votes. +//! +//! If there are not, or if no prime is set, then the motion is dropped without being executed. + +#![cfg_attr(not(feature = "std"), no_std)] +#![recursion_limit = "128"] + +use scale_info::TypeInfo; +use sp_io::storage; +use sp_runtime::{traits::{Hash, Saturating}, RuntimeDebug}; +use sp_std::{marker::PhantomData, prelude::*, result, vec}; + +use frame_support::{ + codec::{Decode, Encode, MaxEncodedLen}, + dispatch::{DispatchError, DispatchResultWithPostInfo, Dispatchable, PostDispatchInfo}, + ensure, + traits::{ + Backing, ChangeMembers, EnsureOrigin, Get, GetBacking, InitializeMembers, StorageVersion, + }, + weights::{GetDispatchInfo, Pays, Weight}, +}; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +pub mod migrations; +pub mod weights; + +pub use pallet::*; +pub use weights::WeightInfo; + +/// Simple index type for proposal counting. +pub type ProposalIndex = u32; + +/// A number of members. +/// +/// This also serves as a number of voting members, and since for motions, each member may +/// vote exactly once, therefore also the number of votes for any given motion. +pub type MemberCount = u32; + +/// Default voting strategy when a member is inactive. +pub trait DefaultVote { + /// Get the default voting strategy, given: + /// + /// - Whether the prime member voted Aye. + /// - Raw number of yes votes. + /// - Raw number of no votes. + /// - Total number of member count. + fn default_vote( + prime_vote: Option, + yes_votes: MemberCount, + no_votes: MemberCount, + len: MemberCount, + ) -> bool; +} + +/// Set the prime member's vote as the default vote. +pub struct PrimeDefaultVote; + +impl DefaultVote for PrimeDefaultVote { + fn default_vote( + prime_vote: Option, + _yes_votes: MemberCount, + _no_votes: MemberCount, + _len: MemberCount, + ) -> bool { + prime_vote.unwrap_or(false) + } +} + +/// First see if yes vote are over majority of the whole collective. If so, set the default vote +/// as yes. Otherwise, use the prime member's vote as the default vote. +pub struct MoreThanMajorityThenPrimeDefaultVote; + +impl DefaultVote for MoreThanMajorityThenPrimeDefaultVote { + fn default_vote( + prime_vote: Option, + yes_votes: MemberCount, + _no_votes: MemberCount, + len: MemberCount, + ) -> bool { + let more_than_majority = yes_votes * 2 > len; + more_than_majority || prime_vote.unwrap_or(false) + } +} + +/// Origin for the collective module. +#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +#[scale_info(skip_type_params(I))] +#[codec(mel_bound(AccountId: MaxEncodedLen))] +pub enum RawOrigin { + /// It has been condoned by a given number of members of the collective from a given total. + Members(MemberCount, MemberCount), + /// It has been condoned by a single member of the collective. + Member(AccountId), + /// Dummy to manage the fact we have instancing. + _Phantom(PhantomData), +} + +impl GetBacking for RawOrigin { + fn get_backing(&self) -> Option { + match self { + RawOrigin::Members(n, d) => Some(Backing { approvals: *n, eligible: *d }), + _ => None, + } + } +} + +/// Info for keeping track of a motion being voted on. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Votes { + /// The proposal's unique index. + index: ProposalIndex, + /// The number of approval votes that are needed to pass the motion. + threshold: MemberCount, + /// The current set of voters that approved it. + ayes: Vec, + /// The current set of voters that rejected it. + nays: Vec, + /// The hard end time of this vote. + end: BlockNumber, +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(4); + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The outer origin type. + type Origin: From>; + + /// The outer call dispatch type. + type Proposal: Parameter + + Dispatchable>::Origin, PostInfo = PostDispatchInfo> + + From> + + GetDispatchInfo; + + /// The outer event type. + type Event: From> + IsType<::Event>; + + /// The minimum amount of time after proposal creation before it can be closed + type ProposalCloseDelay: Get; + + /// The time-out for council motions. + type MotionDuration: Get; + + /// Maximum number of proposals allowed to be active in parallel. + type MaxProposals: Get; + + /// The maximum number of members 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 MaxMembers: Get; + + /// Default vote strategy of this collective. + type DefaultVote: DefaultVote; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub phantom: PhantomData, + pub members: Vec, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + Self { phantom: Default::default(), members: Default::default() } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + use sp_std::collections::btree_set::BTreeSet; + let members_set: BTreeSet<_> = self.members.iter().collect(); + assert_eq!( + members_set.len(), + self.members.len(), + "Members cannot contain duplicate accounts." + ); + + Pallet::::initialize_members(&self.members) + } + } + + /// Origin for the collective pallet. + #[pallet::origin] + pub type Origin = RawOrigin<::AccountId, I>; + + /// The hashes of the active proposals. + #[pallet::storage] + #[pallet::getter(fn proposals)] + pub type Proposals, I: 'static = ()> = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Actual proposal for a given hash, if it's current. + #[pallet::storage] + #[pallet::getter(fn proposal_of)] + pub type ProposalOf, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, >::Proposal, OptionQuery>; + + /// Block when the proposal was proposed. + #[pallet::storage] + #[pallet::getter(fn proposal_proposed_time)] + pub type ProposalProposedTime, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, T::BlockNumber, OptionQuery>; + + /// Votes on a given proposal, if it is ongoing. + #[pallet::storage] + #[pallet::getter(fn voting)] + pub type Voting, I: 'static = ()> = + StorageMap<_, Identity, T::Hash, Votes, OptionQuery>; + + /// Proposals so far. + #[pallet::storage] + #[pallet::getter(fn proposal_count)] + pub type ProposalCount, I: 'static = ()> = StorageValue<_, u32, ValueQuery>; + + /// The current members of the collective. This is stored sorted (just by value). + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members, I: 'static = ()> = + StorageValue<_, Vec, ValueQuery>; + + /// The prime member that helps determine the default vote behavior in case of absentations. + #[pallet::storage] + #[pallet::getter(fn prime)] + pub type Prime, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// A motion (given hash) has been proposed (by given account) with a threshold (given + /// `MemberCount`). + Proposed { + account: T::AccountId, + proposal_index: ProposalIndex, + proposal_hash: T::Hash, + threshold: MemberCount, + }, + /// A motion (given hash) has been voted on by given account, leaving + /// a tally (yes votes and no votes given respectively as `MemberCount`). + Voted { + account: T::AccountId, + proposal_hash: T::Hash, + voted: bool, + yes: MemberCount, + no: MemberCount, + }, + /// A motion was approved by the required threshold. + Approved { proposal_hash: T::Hash }, + /// A motion was not approved by the required threshold. + Disapproved { proposal_hash: T::Hash }, + /// A motion was executed; result will be `Ok` if it returned without error. + Executed { proposal_hash: T::Hash, result: DispatchResult }, + /// A single member did some action; result will be `Ok` if it returned without error. + MemberExecuted { proposal_hash: T::Hash, result: DispatchResult }, + /// A proposal was closed because its threshold was reached or after its duration was up. + Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount }, + /// The members have been changed + MembersChanged {new_members: Vec}, + /// The Prime member has been set + PrimeSet {new_prime: Option}, + } + + #[pallet::error] + pub enum Error { + /// Account is not a member + NotMember, + /// Duplicate proposals not allowed + DuplicateProposal, + /// Proposal must exist + ProposalMissing, + /// Mismatched index + WrongIndex, + /// Duplicate vote ignored + DuplicateVote, + /// Members are already initialized! + AlreadyInitialized, + /// The close call was made too early, before the end of the voting. + TooEarly, + /// To early to close the proposal, can only close ProposalCloseDelay blocks after proposal was proposed + TooEarlyToClose, + /// There can only be a maximum of `MaxProposals` active proposals. + TooManyProposals, + /// The given weight bound for the proposal was too low. + WrongProposalWeight, + /// The given length bound for the proposal was too low. + WrongProposalLength, + } + + // Note that councillor operations are assigned to the operational class. + #[pallet::call] + impl, I: 'static> Pallet { + /// Set the collective's membership. + /// + /// - `new_members`: The new member list. Be nice to the chain and provide it sorted. + /// - `prime`: The prime member whose vote sets the default. + /// - `old_count`: The upper bound for the previous number of members in storage. Used for + /// weight estimation. + /// + /// Requires root origin. + /// + /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but + /// the weight estimations rely on it to estimate dispatchable weight. + /// + /// # WARNING: + /// + /// The `pallet-collective` can also be managed by logic outside of the pallet through the + /// implementation of the trait [`ChangeMembers`]. + /// Any call to `set_members` must be careful that the member set doesn't get out of sync + /// with other logic managing the member set. + /// + /// # + /// ## Weight + /// - `O(MP + N)` where: + /// - `M` old-members-count (code- and governance-bounded) + /// - `N` new-members-count (code- and governance-bounded) + /// - `P` proposals-count (code-bounded) + /// - DB: + /// - 1 storage mutation (codec `O(M)` read, `O(N)` write) for reading and writing the + /// members + /// - 1 storage read (codec `O(P)`) for reading the proposals + /// - `P` storage mutations (codec `O(M)`) for updating the votes for each proposal + /// - 1 storage write (codec `O(1)`) for deleting the old `prime` and setting the new one + /// # + #[pallet::weight(( + T::WeightInfo::set_members( + *old_count, // M + new_members.len() as u32, // N + T::MaxProposals::get() // P + ), + DispatchClass::Operational + ))] + pub fn set_members( + origin: OriginFor, + new_members: Vec, + prime: Option, + old_count: MemberCount, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + if new_members.len() > T::MaxMembers::get() as usize { + log::error!( + target: "runtime::collective", + "New members count ({}) exceeds maximum amount of members expected ({}).", + new_members.len(), + T::MaxMembers::get(), + ); + } + + let old = Members::::get(); + if old.len() > old_count as usize { + log::warn!( + target: "runtime::collective", + "Wrong count used to estimate set_members weight. expected ({}) vs actual ({})", + old_count, + old.len(), + ); + } + let mut new_members = new_members; + new_members.sort(); + >::set_members_sorted(&new_members, &old); + >::set_prime(prime); + + Ok(Some(T::WeightInfo::set_members( + old.len() as u32, // M + new_members.len() as u32, // N + T::MaxProposals::get(), // P + )) + .into()) + } + + /// Dispatch a proposal from a member using the `Member` origin. + /// + /// Origin must be a member of the collective. + /// + /// # + /// ## Weight + /// - `O(M + P)` where `M` members-count (code-bounded) and `P` complexity of dispatching + /// `proposal` + /// - DB: 1 read (codec `O(M)`) + DB access of `proposal` + /// - 1 event + /// # + #[pallet::weight(( + T::WeightInfo::execute( + *length_bound, // B + T::MaxMembers::get(), // M + ).saturating_add(proposal.get_dispatch_info().weight), // P + DispatchClass::Operational + ))] + pub fn execute( + origin: OriginFor, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + let result = proposal.dispatch(RawOrigin::Member(who).into()); + Self::deposit_event(Event::MemberExecuted { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + log::info!( + target: "runtime::collective", + "A member has executed a proposal!" + ); + + Ok(get_result_weight(result) + .map(|w| { + T::WeightInfo::execute( + proposal_len as u32, // B + members.len() as u32, // M + ) + .saturating_add(w) // P + }) + .into()) + } + + /// Add a new proposal to either be voted on or executed directly. + /// + /// Requires the sender to be member. + /// + /// `threshold` determines whether `proposal` is executed directly (`threshold < 2`) + /// or put up for voting. + /// + /// # + /// ## Weight + /// - `O(B + M + P1)` or `O(B + M + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - branching is influenced by `threshold` where: + /// - `P1` is proposal execution complexity (`threshold < 2`) + /// - `P2` is proposals-count (code-bounded) (`threshold >= 2`) + /// - DB: + /// - 1 storage read `is_member` (codec `O(M)`) + /// - 1 storage read `ProposalOf::contains_key` (codec `O(1)`) + /// - DB accesses influenced by `threshold`: + /// - EITHER storage accesses done by `proposal` (`threshold < 2`) + /// - OR proposal insertion (`threshold <= 2`) + /// - 1 storage mutation `Proposals` (codec `O(P2)`) + /// - 1 storage mutation `ProposalCount` (codec `O(1)`) + /// - 1 storage write `ProposalOf` (codec `O(B)`) + /// - 1 storage write `Voting` (codec `O(M)`) + /// - 1 event + /// # + #[pallet::weight(( + if *threshold < 2 { + T::WeightInfo::propose_execute( + *length_bound, // B + T::MaxMembers::get(), // M + ).saturating_add(proposal.get_dispatch_info().weight) // P1 + } else { + T::WeightInfo::propose_proposed( + *length_bound, // B + T::MaxMembers::get(), // M + T::MaxProposals::get(), // P2 + ) + }, + DispatchClass::Operational + ))] + pub fn propose( + origin: OriginFor, + #[pallet::compact] threshold: MemberCount, + proposal: Box<>::Proposal>, + #[pallet::compact] length_bound: u32, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + + if threshold < 2 { + let (proposal_len, result) = Self::do_propose_execute(proposal, length_bound)?; + + Ok(get_result_weight(result) + .map(|w| { + T::WeightInfo::propose_execute( + proposal_len as u32, // B + members.len() as u32, // M + ) + .saturating_add(w) // P1 + }) + .into()) + } else { + 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, // P2 + )) + .into()) + } + } + + /// Add an aye or nay vote for the sender to the given proposal. + /// + /// Requires the sender to be a member. + /// + /// Transaction fees will be waived if the member is voting on any particular proposal + /// for the first time and the call is successful. Subsequent vote changes will charge a + /// fee. + /// # + /// ## Weight + /// - `O(M)` where `M` is members-count (code- and governance-bounded) + /// - DB: + /// - 1 storage read `Members` (codec `O(M)`) + /// - 1 storage mutation `Voting` (codec `O(M)`) + /// - 1 event + /// # + #[pallet::weight((T::WeightInfo::vote(T::MaxMembers::get()), DispatchClass::Operational))] + pub fn vote( + origin: OriginFor, + proposal: T::Hash, + #[pallet::compact] index: ProposalIndex, + approve: bool, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let members = Self::members(); + ensure!(members.contains(&who), Error::::NotMember); + + // Detects first vote of the member in the motion + 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()) + } else { + Ok((Some(T::WeightInfo::vote(members.len() as u32)), Pays::Yes).into()) + } + } + + /// Close a vote that is either approved, disapproved or whose voting period has ended. + /// + /// May be called by any signed account in order to finish voting and close the proposal. + /// + /// If called before the end of the voting period it will only close the vote if it is + /// has enough votes to be approved or disapproved. + /// + /// If called after the end of the voting period abstentions are counted as rejections + /// unless there is a prime member set and the prime member cast an approval. + /// + /// If the close operation completes successfully with disapproval, the transaction fee will + /// be waived. Otherwise execution of the approved operation will be charged to the caller. + /// + /// + `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed + /// proposal. + /// + `length_bound`: The upper bound for the length of the proposal in storage. Checked via + /// `storage::read` so it is `size_of::() == 4` larger than the pure length. + /// + /// # + /// ## Weight + /// - `O(B + M + P1 + P2)` where: + /// - `B` is `proposal` size in bytes (length-fee-bounded) + /// - `M` is members-count (code- and governance-bounded) + /// - `P1` is the complexity of `proposal` preimage. + /// - `P2` is proposal-count (code-bounded) + /// - DB: + /// - 2 storage reads (`Members`: codec `O(M)`, `Prime`: codec `O(1)`) + /// - 3 mutations (`Voting`: codec `O(M)`, `ProposalOf`: codec `O(B)`, `Proposals`: codec + /// `O(P2)`) + /// - any mutations done while executing `proposal` (`P1`) + /// - up to 3 events + /// # + #[pallet::weight(( + { + let b = *length_bound; + let m = T::MaxMembers::get(); + let p1 = *proposal_weight_bound; + let p2 = T::MaxProposals::get(); + T::WeightInfo::close_early_approved(b, m, p2) + .max(T::WeightInfo::close_early_disapproved(m, p2)) + .max(T::WeightInfo::close_approved(b, m, p2)) + .max(T::WeightInfo::close_disapproved(m, 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 _ = ensure_signed(origin)?; + + 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 + /// state. + /// + /// Must be called by the Root origin. + /// + /// Parameters: + /// * `proposal_hash`: The hash of the proposal that should be disapproved. + /// + /// # + /// Complexity: O(P) where P is the number of max proposals + /// DB Weight: + /// * Reads: Proposals + /// * Writes: Voting, Proposals, ProposalOf + /// # + #[pallet::weight(T::WeightInfo::disapprove_proposal(T::MaxProposals::get()))] + pub fn disapprove_proposal( + origin: OriginFor, + proposal_hash: T::Hash, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + Ok(Some(T::WeightInfo::disapprove_proposal(proposal_count)).into()) + } + } +} + +/// Return the weight of a dispatch call result as an `Option`. +/// +/// Will return the weight regardless of what the state of the result is. +fn get_result_weight(result: DispatchResultWithPostInfo) -> Option { + match result { + Ok(post_info) => post_info.actual_weight, + Err(err) => err.post_info.actual_weight, + } +} + +impl, I: 'static> Pallet { + /// Check whether `who` is a member of the collective. + pub fn is_member(who: &T::AccountId) -> bool { + // Note: The dispatchables *do not* use this to check membership so make sure + // to update those if this is changed. + Self::members().contains(who) + } + + /// Execute immediately when adding a new proposal. + pub fn do_propose_execute( + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, DispatchResultWithPostInfo), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let seats = Self::members().len() as MemberCount; + let result = proposal.dispatch(RawOrigin::Members(1, seats).into()); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + log::info!( + target: "runtime::collective", + "A member has executed a proposal!" + ); + + Ok((proposal_len as u32, result)) + } + + /// Add a new proposal to be voted. + pub fn do_propose_proposed( + who: T::AccountId, + threshold: MemberCount, + proposal: Box<>::Proposal>, + length_bound: MemberCount, + ) -> Result<(u32, u32), DispatchError> { + let proposal_len = proposal.encoded_size(); + ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); + + let proposal_hash = T::Hashing::hash_of(&proposal); + ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); + + let active_proposals = + >::try_mutate(|proposals| -> Result { + proposals.try_push(proposal_hash).map_err(|_| Error::::TooManyProposals)?; + Ok(proposals.len()) + })?; + + let index = Self::proposal_count(); + >::mutate(|i| *i += 1); + >::insert(proposal_hash, proposal); + >::insert(proposal_hash, frame_system::Pallet::::block_number()); + + let votes = { + let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); + Votes { index, threshold, ayes: vec![], nays: vec![], end } + }; + >::insert(proposal_hash, votes); + + Self::deposit_event(Event::Proposed { + account: who, + proposal_index: index, + proposal_hash, + threshold, + }); + + log::info!( + target: "runtime::collective", + "A proposal has been proposed!" + ); + + Ok((proposal_len as u32, active_proposals as u32)) + } + + /// Add an aye or nay vote for the member to the given proposal, returns true if it's the first + /// vote of the member in the motion + pub fn do_vote( + who: T::AccountId, + proposal: T::Hash, + index: ProposalIndex, + approve: bool, + ) -> Result { + let mut voting = Self::voting(&proposal).ok_or(Error::::ProposalMissing)?; + ensure!(voting.index == index, Error::::WrongIndex); + + let position_yes = voting.ayes.iter().position(|a| a == &who); + let position_no = voting.nays.iter().position(|a| a == &who); + + // Detects first vote of the member in the motion + let is_account_voting_first_time = position_yes.is_none() && position_no.is_none(); + + if approve { + if position_yes.is_none() { + voting.ayes.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_no { + voting.nays.swap_remove(pos); + } + } else { + if position_no.is_none() { + voting.nays.push(who.clone()); + } else { + return Err(Error::::DuplicateVote.into()) + } + if let Some(pos) = position_yes { + voting.ayes.swap_remove(pos); + } + } + + let yes_votes = voting.ayes.len() as MemberCount; + let no_votes = voting.nays.len() as MemberCount; + Self::deposit_event(Event::Voted { + account: who, + proposal_hash: proposal, + voted: approve, + yes: yes_votes, + no: no_votes, + }); + + log::info!( + target: "runtime::collective", + "A member has voted on a proposal!" + ); + + 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); + + // To allow previously existing proposals to be executed we use unwrap_or_default + // This can be removed later on when no proposals are without a proposed time in storage. + let proposal_proposed_time = Self::proposal_proposed_time(&proposal_hash).unwrap_or_default(); + // Only allow actual closing of the proposal after the voting period has ended. + ensure!(frame_system::Pallet::::block_number() >= proposal_proposed_time.saturating_add(T::ProposalCloseDelay::get()), Error::::TooEarlyToClose); + + let mut no_votes = voting.nays.len() as MemberCount; + let mut yes_votes = voting.ayes.len() as MemberCount; + let seats = Self::members().len() as MemberCount; + let approved = yes_votes >= voting.threshold; + let disapproved = seats.saturating_sub(no_votes) < voting.threshold; + // Allow (dis-)approving the proposal as soon as there are enough votes. + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + log::info!( + target: "runtime::collective", + "A proposal has been closed!" + ); + + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + return Ok(( + Some( + T::WeightInfo::close_early_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else if disapproved { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + log::info!( + target: "runtime::collective", + "A proposal has been closed!" + ); + + let proposal_count = Self::do_disapprove_proposal(proposal_hash); + return Ok(( + Some(T::WeightInfo::close_early_disapproved(seats, proposal_count)), + Pays::No, + ) + .into()) + } + + // Only allow actual closing of the proposal after the voting period has ended. + ensure!(frame_system::Pallet::::block_number() >= voting.end, Error::::TooEarly); + + let prime_vote = Self::prime().map(|who| voting.ayes.iter().any(|a| a == &who)); + + // default voting strategy. + let default = T::DefaultVote::default_vote(prime_vote, yes_votes, no_votes, seats); + + let abstentions = seats - (yes_votes + no_votes); + match default { + true => yes_votes += abstentions, + false => no_votes += abstentions, + } + let approved = yes_votes >= voting.threshold; + + if approved { + let (proposal, len) = Self::validate_and_get_proposal( + &proposal_hash, + length_bound, + proposal_weight_bound, + )?; + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + log::info!( + target: "runtime::collective", + "A proposal has been closed!" + ); + + let (proposal_weight, proposal_count) = + Self::do_approve_proposal(seats, yes_votes, proposal_hash, proposal); + Ok(( + Some( + T::WeightInfo::close_approved(len as u32, seats, proposal_count) + .saturating_add(proposal_weight), + ), + Pays::Yes, + ) + .into()) + } else { + Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); + + log::info!( + target: "runtime::collective", + "A proposal has been closed!" + ); + + 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` + /// to the length. + fn validate_and_get_proposal( + hash: &T::Hash, + length_bound: u32, + weight_bound: Weight, + ) -> Result<(>::Proposal, usize), DispatchError> { + let key = ProposalOf::::hashed_key_for(hash); + // read the length of the proposal storage entry directly + let proposal_len = + storage::read(&key, &mut [0; 0], 0).ok_or(Error::::ProposalMissing)?; + ensure!(proposal_len <= length_bound, Error::::WrongProposalLength); + let proposal = ProposalOf::::get(hash).ok_or(Error::::ProposalMissing)?; + let proposal_weight = proposal.get_dispatch_info().weight; + ensure!(proposal_weight <= weight_bound, Error::::WrongProposalWeight); + Ok((proposal, proposal_len as usize)) + } + + /// Weight: + /// If `approved`: + /// - the weight of `proposal` preimage. + /// - two events deposited. + /// - two removals, one mutation. + /// - computation and i/o `O(P + L)` where: + /// - `P` is number of active proposals, + /// - `L` is the encoded length of `proposal` preimage. + /// + /// If not `approved`: + /// - one event deposited. + /// Two removals, one mutation. + /// Computation and i/o `O(P)` where: + /// - `P` is number of active proposals + fn do_approve_proposal( + seats: MemberCount, + yes_votes: MemberCount, + proposal_hash: T::Hash, + proposal: >::Proposal, + ) -> (Weight, u32) { + Self::deposit_event(Event::Approved { proposal_hash }); + + log::info!( + target: "runtime::collective", + "A proposal has been approved!" + ); + + let dispatch_weight = proposal.get_dispatch_info().weight; + let origin = RawOrigin::Members(yes_votes, seats).into(); + let result = proposal.dispatch(origin); + Self::deposit_event(Event::Executed { + proposal_hash, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + log::info!( + target: "runtime::collective", + "A proposal has been executed!" + ); + + // default to the dispatch info weight for safety + let proposal_weight = get_result_weight(result).unwrap_or(dispatch_weight); // P1 + + let proposal_count = Self::remove_proposal(proposal_hash); + (proposal_weight, proposal_count) + } + + /// 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 }); + + log::info!( + target: "runtime::collective", + "A proposal has been disapproved!" + ); + + Self::remove_proposal(proposal_hash) + } + + // Removes a proposal from the pallet, cleaning up votes and the vector of proposals. + fn remove_proposal(proposal_hash: T::Hash) -> u32 { + // remove proposal and vote + ProposalOf::::remove(&proposal_hash); + ProposalProposedTime::::remove(&proposal_hash); + Voting::::remove(&proposal_hash); + let num_proposals = Proposals::::mutate(|proposals| { + proposals.retain(|h| h != &proposal_hash); + proposals.len() + 1 // calculate weight based on original length + }); + num_proposals as u32 + } +} + +impl, I: 'static> ChangeMembers for Pallet { + /// Update the members of the collective. Votes are updated and the prime is reset. + /// + /// NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but + /// the weight estimations rely on it to estimate dispatchable weight. + /// + /// # + /// ## Weight + /// - `O(MP + N)` + /// - where `M` old-members-count (governance-bounded) + /// - where `N` new-members-count (governance-bounded) + /// - where `P` proposals-count + /// - DB: + /// - 1 storage read (codec `O(P)`) for reading the proposals + /// - `P` storage mutations for updating the votes (codec `O(M)`) + /// - 1 storage write (codec `O(N)`) for storing the new members + /// - 1 storage write (codec `O(1)`) for deleting the old prime + /// # + fn change_members_sorted( + _incoming: &[T::AccountId], + outgoing: &[T::AccountId], + new: &[T::AccountId], + ) { + if new.len() > T::MaxMembers::get() as usize { + log::error!( + target: "runtime::collective", + "New members count ({}) exceeds maximum amount of members expected ({}).", + new.len(), + T::MaxMembers::get(), + ); + } + // remove accounts from all current voting in motions. + let mut outgoing = outgoing.to_vec(); + outgoing.sort(); + for h in Self::proposals().into_iter() { + >::mutate(h, |v| { + if let Some(mut votes) = v.take() { + votes.ayes = votes + .ayes + .into_iter() + .filter(|i| outgoing.binary_search(i).is_err()) + .collect(); + votes.nays = votes + .nays + .into_iter() + .filter(|i| outgoing.binary_search(i).is_err()) + .collect(); + *v = Some(votes); + } + }); + } + Members::::put(new); + Prime::::kill(); + + Pallet::::deposit_event(Event::MembersChanged { + new_members: new.to_vec() + }); + + log::info!( + target: "runtime::collective", + "Collective members have changed!!!" + ); + + } + + fn set_prime(prime: Option) { + Prime::::set(prime.clone()); + Pallet::::deposit_event(Event::PrimeSet { + new_prime: prime + }); + + log::info!( + target: "runtime::collective", + "Prime member has changed!" + ); + + } + + fn get_prime() -> Option { + Prime::::get() + } +} + +impl, I: 'static> InitializeMembers for Pallet { + fn initialize_members(members: &[T::AccountId]) { + if !members.is_empty() { + assert!(>::get().is_empty(), "Members are already initialized!"); + >::put(members); + } + } +} + +/// Ensure that the origin `o` represents at least `n` members. Returns `Ok` or an `Err` +/// otherwise. +pub fn ensure_members( + o: OuterOrigin, + n: MemberCount, +) -> result::Result +where + OuterOrigin: Into, OuterOrigin>>, +{ + match o.into() { + Ok(RawOrigin::Members(x, _)) if x >= n => Ok(n), + _ => Err("bad origin: expected to be a threshold number of members"), + } +} + +pub struct EnsureMember(PhantomData<(AccountId, I)>); +impl< + O: Into, O>> + From>, + I, + AccountId: Decode, + > EnsureOrigin for EnsureMember +{ + type Success = AccountId; + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Member(id) => Ok(id), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + let zero_account_id = + AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) + .expect("infinite length input; no invalid inputs for type; qed"); + Ok(O::from(RawOrigin::Member(zero_account_id))) + } +} + +pub struct EnsureMembers(PhantomData<(AccountId, I)>); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + > EnsureOrigin for EnsureMembers +{ + type Success = (MemberCount, MemberCount); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n >= N => Ok((n, m)), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(N, N))) + } +} + +pub struct EnsureProportionMoreThan( + PhantomData<(AccountId, I)>, +); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + const D: u32, + > EnsureOrigin for EnsureProportionMoreThan +{ + type Success = (); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n * D > N * m => Ok(()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(1u32, 0u32))) + } +} + +pub struct EnsureProportionAtLeast( + PhantomData<(AccountId, I)>, +); +impl< + O: Into, O>> + From>, + AccountId, + I, + const N: u32, + const D: u32, + > EnsureOrigin for EnsureProportionAtLeast +{ + type Success = (); + fn try_origin(o: O) -> Result { + o.into().and_then(|o| match o { + RawOrigin::Members(n, m) if n * D >= N * m => Ok(()), + r => Err(O::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(RawOrigin::Members(0u32, 0u32))) + } +} + +pub trait GetMembers { + fn get_members() -> Vec; +} + +impl, I: 'static> GetMembers for Pallet { + fn get_members() -> Vec { + Pallet::::members() + } +} + +impl GetMembers for () { + fn get_members() -> Vec { + Vec::::default() + } +} diff --git a/frame/collective-mangata/src/migrations/mod.rs b/frame/collective-mangata/src/migrations/mod.rs new file mode 100644 index 0000000000000..235d0f1c7cf15 --- /dev/null +++ b/frame/collective-mangata/src/migrations/mod.rs @@ -0,0 +1,19 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Version 4. +pub mod v4; diff --git a/frame/collective-mangata/src/migrations/v4.rs b/frame/collective-mangata/src/migrations/v4.rs new file mode 100644 index 0000000000000..483c3f9fa9e69 --- /dev/null +++ b/frame/collective-mangata/src/migrations/v4.rs @@ -0,0 +1,147 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sp_io::hashing::twox_128; + +use frame_support::{ + traits::{ + Get, GetStorageVersion, PalletInfoAccess, StorageVersion, + STORAGE_VERSION_STORAGE_KEY_POSTFIX, + }, + weights::Weight, +}; + +/// Migrate the entire storage of this pallet to a new prefix. +/// +/// This new prefix must be the same as the one set in construct_runtime. For safety, use +/// `PalletInfo` to get it, as: +/// `::PalletInfo::name::`. +/// +/// The migration will look into the storage version in order not to trigger a migration on an up +/// to date storage. Thus the on chain storage version must be less than 4 in order to trigger the +/// migration. +pub fn migrate>( + old_pallet_name: N, +) -> Weight { + let old_pallet_name = old_pallet_name.as_ref(); + let new_pallet_name =

::name(); + + if new_pallet_name == old_pallet_name { + log::info!( + target: "runtime::collective", + "New pallet name is equal to the old pallet name. No migration needs to be done.", + ); + return Weight::zero() + } + + let on_chain_storage_version =

::on_chain_storage_version(); + log::info!( + target: "runtime::collective", + "Running migration to v4 for collective with storage version {:?}", + on_chain_storage_version, + ); + + if on_chain_storage_version < 4 { + frame_support::storage::migration::move_pallet( + old_pallet_name.as_bytes(), + new_pallet_name.as_bytes(), + ); + log_migration("migration", old_pallet_name, new_pallet_name); + + StorageVersion::new(4).put::

(); + ::BlockWeights::get().max_block + } else { + log::warn!( + target: "runtime::collective", + "Attempted to apply migration to v4 but failed because storage version is {:?}", + on_chain_storage_version, + ); + Weight::zero() + } +} + +/// Some checks prior to migration. This can be linked to +/// [`frame_support::traits::OnRuntimeUpgrade::pre_upgrade`] for further testing. +/// +/// Panics if anything goes wrong. +pub fn pre_migrate>(old_pallet_name: N) { + let old_pallet_name = old_pallet_name.as_ref(); + let new_pallet_name =

::name(); + log_migration("pre-migration", old_pallet_name, new_pallet_name); + + if new_pallet_name == old_pallet_name { + return + } + + let new_pallet_prefix = twox_128(new_pallet_name.as_bytes()); + let storage_version_key = twox_128(STORAGE_VERSION_STORAGE_KEY_POSTFIX); + + let mut new_pallet_prefix_iter = frame_support::storage::KeyPrefixIterator::new( + new_pallet_prefix.to_vec(), + new_pallet_prefix.to_vec(), + |key| Ok(key.to_vec()), + ); + + // Ensure nothing except the storage_version_key is stored in the new prefix. + assert!(new_pallet_prefix_iter.all(|key| key == storage_version_key)); + + assert!(

::on_chain_storage_version() < 4); +} + +/// Some checks for after migration. This can be linked to +/// [`frame_support::traits::OnRuntimeUpgrade::post_upgrade`] for further testing. +/// +/// Panics if anything goes wrong. +pub fn post_migrate>(old_pallet_name: N) { + let old_pallet_name = old_pallet_name.as_ref(); + let new_pallet_name =

::name(); + log_migration("post-migration", old_pallet_name, new_pallet_name); + + if new_pallet_name == old_pallet_name { + return + } + + // Assert that nothing remains at the old prefix. + let old_pallet_prefix = twox_128(old_pallet_name.as_bytes()); + let old_pallet_prefix_iter = frame_support::storage::KeyPrefixIterator::new( + old_pallet_prefix.to_vec(), + old_pallet_prefix.to_vec(), + |_| Ok(()), + ); + assert_eq!(old_pallet_prefix_iter.count(), 0); + + // NOTE: storage_version_key is already in the new prefix. + let new_pallet_prefix = twox_128(new_pallet_name.as_bytes()); + let new_pallet_prefix_iter = frame_support::storage::KeyPrefixIterator::new( + new_pallet_prefix.to_vec(), + new_pallet_prefix.to_vec(), + |_| Ok(()), + ); + assert!(new_pallet_prefix_iter.count() >= 1); + + assert_eq!(

::on_chain_storage_version(), 4); +} + +fn log_migration(stage: &str, old_pallet_name: &str, new_pallet_name: &str) { + log::info!( + target: "runtime::collective", + "{}, prefix: '{}' ==> '{}'", + stage, + old_pallet_name, + new_pallet_name, + ); +} diff --git a/frame/collective-mangata/src/tests.rs b/frame/collective-mangata/src/tests.rs new file mode 100644 index 0000000000000..e81c5dd9c6ff9 --- /dev/null +++ b/frame/collective-mangata/src/tests.rs @@ -0,0 +1,1418 @@ +// This file is part of Substrate. + +// Copyright (C) 2021-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{Event as CollectiveEvent, *}; +use crate as pallet_collective_mangata; +use frame_support::{ + assert_noop, assert_ok, parameter_types, + traits::{ConstU32, ConstU64, GenesisBuild, StorageVersion}, + weights::Pays, + Hashable, +}; +use frame_system::{EventRecord, Phase}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub type Block = sp_runtime::generic::Block; +pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + System: frame_system::{Pallet, Call, Event}, + Collective: pallet_collective_mangata::::{Pallet, Call, Event, Origin, Config}, + CollectiveMajority: pallet_collective_mangata::::{Pallet, Call, Event, Origin, Config}, + DefaultCollective: pallet_collective_mangata::{Pallet, Call, Event, Origin, Config}, + Democracy: mock_democracy::{Pallet, Call, Event}, + } +); + +mod mock_democracy { + pub use pallet::*; + #[frame_support::pallet] + pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + type Event: From> + IsType<::Event>; + type ExternalMajorityOrigin: EnsureOrigin; + } + + #[pallet::call] + impl Pallet { + #[pallet::weight(0)] + pub fn external_propose_majority(origin: OriginFor) -> DispatchResult { + T::ExternalMajorityOrigin::ensure_origin(origin)?; + Self::deposit_event(Event::::ExternalProposed); + Ok(()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ExternalProposed, + } + } +} + +pub type MaxMembers = ConstU32<100>; + +parameter_types! { + pub const MotionDuration: u64 = 3; + pub const ProposalCloseDelay: u64 = 2; + pub const MaxProposals: u32 = 100; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(frame_support::weights::Weight::from_ref_time(1024)); +} +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} +impl Config for Test { + type Origin = Origin; + type Proposal = Call; + type Event = Event; + type MotionDuration = ConstU64<3>; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = PrimeDefaultVote; + type WeightInfo = (); +} +impl Config for Test { + type Origin = Origin; + type Proposal = Call; + type Event = Event; + type MotionDuration = ConstU64<3>; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = MoreThanMajorityThenPrimeDefaultVote; + type WeightInfo = (); +} +impl mock_democracy::Config for Test { + type Event = Event; + type ExternalMajorityOrigin = EnsureProportionAtLeast; +} +impl Config for Test { + type Origin = Origin; + type Proposal = Call; + type Event = Event; + type MotionDuration = ConstU64<3>; + type ProposalCloseDelay = ConstU64<2>; + type MaxProposals = MaxProposals; + type MaxMembers = MaxMembers; + type DefaultVote = PrimeDefaultVote; + type WeightInfo = (); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = GenesisConfig { + collective: pallet_collective_mangata::GenesisConfig { + members: vec![1, 2, 3], + phantom: Default::default(), + }, + collective_majority: pallet_collective_mangata::GenesisConfig { + members: vec![1, 2, 3, 4, 5], + phantom: Default::default(), + }, + default_collective: Default::default(), + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn make_proposal(value: u64) -> Call { + Call::System(frame_system::Call::remark_with_event { remark: value.to_be_bytes().to_vec() }) +} + +fn record(event: Event) -> EventRecord { + EventRecord { phase: Phase::Initialization, event, topics: vec![] } +} + +#[test] +fn motions_basic_environment_works() { + new_test_ext().execute_with(|| { + assert_eq!(Collective::members(), vec![1, 2, 3]); + assert_eq!(*Collective::proposals(), Vec::::new()); + }); +} + +#[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!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_noop!( + Collective::close(Origin::signed(4), hash, 0, proposal_weight, proposal_len), + Error::::TooEarly + ); + + System::set_block_number(4); + assert_ok!(Collective::close(Origin::signed(4), hash, 0, proposal_weight, proposal_len)); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 1 + })), + record(Event::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })) + ] + ); + }); +} + +#[test] +fn proposal_close_delay_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!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_ok!(Collective::close(Origin::signed(4), hash, 0, proposal_weight, proposal_len)); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(Event::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn proposal_weight_limit_works_on_approve() { + new_test_ext().execute_with(|| { + let proposal = Call::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + 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); + // Set 1 as prime voter + Prime::::set(Some(1)); + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + // With 1's prime vote, this should pass + System::set_block_number(4); + assert_noop!( + Collective::close( + Origin::signed(4), + hash, + 0, + proposal_weight - Weight::from_ref_time(100), + proposal_len + ), + Error::::WrongProposalWeight + ); + assert_ok!(Collective::close(Origin::signed(4), hash, 0, proposal_weight, proposal_len)); + }) +} + +#[test] +fn proposal_weight_limit_ignored_on_disapprove() { + new_test_ext().execute_with(|| { + let proposal = Call::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + 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!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + // No votes, this proposal wont pass + System::set_block_number(4); + assert_ok!(Collective::close( + Origin::signed(4), + hash, + 0, + proposal_weight - Weight::from_ref_time(100), + proposal_len + )); + }) +} + +#[test] +fn close_with_prime_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!(Collective::set_members( + Origin::root(), + vec![1, 2, 3], + Some(3), + MaxMembers::get() + )); + + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close(Origin::signed(4), hash, 0, proposal_weight, proposal_len)); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::MembersChanged { + new_members: vec![1, 2, 3] + })), + record(Event::Collective(CollectiveEvent::PrimeSet { + new_prime: Some(3), + })), + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 1 + })), + record(Event::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })) + ] + ); + }); +} + +#[test] +fn close_with_voting_prime_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!(Collective::set_members( + Origin::root(), + vec![1, 2, 3], + Some(1), + MaxMembers::get() + )); + + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(Collective::close(Origin::signed(4), hash, 0, proposal_weight, proposal_len)); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::MembersChanged { + new_members: vec![1, 2, 3] + })), + record(Event::Collective(CollectiveEvent::PrimeSet { + new_prime: Some(1), + })), + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(Event::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn close_with_no_prime_but_majority_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!(CollectiveMajority::set_members( + Origin::root(), + vec![1, 2, 3, 4, 5], + Some(5), + MaxMembers::get() + )); + + assert_ok!(CollectiveMajority::propose( + Origin::signed(1), + 5, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(CollectiveMajority::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(CollectiveMajority::vote(Origin::signed(2), hash, 0, true)); + assert_ok!(CollectiveMajority::vote(Origin::signed(3), hash, 0, true)); + + System::set_block_number(4); + assert_ok!(CollectiveMajority::close( + Origin::signed(4), + hash, + 0, + proposal_weight, + proposal_len + )); + + assert_eq!( + System::events(), + vec![ + record(Event::CollectiveMajority(CollectiveEvent::MembersChanged { + new_members: vec![1, 2, 3, 4, 5] + })), + record(Event::CollectiveMajority(CollectiveEvent::PrimeSet { + new_prime: Some(5), + })), + record(Event::CollectiveMajority(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 5 + })), + record(Event::CollectiveMajority(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::CollectiveMajority(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::CollectiveMajority(CollectiveEvent::Voted { + account: 3, + proposal_hash: hash, + voted: true, + yes: 3, + no: 0 + })), + record(Event::CollectiveMajority(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 5, + no: 0 + })), + record(Event::CollectiveMajority(CollectiveEvent::Approved { + proposal_hash: hash + })), + record(Event::CollectiveMajority(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })) + ] + ); + }); +} + +#[test] +fn removal_of_old_voters_votes_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 = BlakeTwo256::hash_of(&proposal); + let end = 4; + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end }) + ); + Collective::change_members_sorted(&[4], &[1], &[2, 3, 4]); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end }) + ); + + let proposal = make_proposal(69); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::propose( + Origin::signed(2), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(Origin::signed(3), hash, 1, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) + ); + Collective::change_members_sorted(&[], &[3], &[2, 4]); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + }); +} + +#[test] +fn removal_of_old_voters_votes_works_with_set_members() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + let end = 4; + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![1, 2], nays: vec![], end }) + ); + assert_ok!(Collective::set_members(Origin::root(), vec![2, 3, 4], None, MaxMembers::get())); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![2], nays: vec![], end }) + ); + + let proposal = make_proposal(69); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let hash = BlakeTwo256::hash_of(&proposal); + assert_ok!(Collective::propose( + Origin::signed(2), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(Origin::signed(3), hash, 1, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![3], end }) + ); + assert_ok!(Collective::set_members(Origin::root(), vec![2, 4], None, MaxMembers::get())); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 1, threshold: 2, ayes: vec![2], nays: vec![], end }) + ); + }); +} + +#[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 = proposal.blake2_256().into(); + let end = 4; + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![hash]); + assert_eq!(Collective::proposal_of(&hash), Some(proposal)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 3, ayes: vec![], nays: vec![], end }) + ); + + assert_eq!( + System::events(), + vec![record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + }))] + ); + }); +} + +#[test] +fn limit_active_proposals() { + new_test_ext().execute_with(|| { + for i in 0..MaxProposals::get() { + let proposal = make_proposal(i as u64); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + } + let proposal = make_proposal(MaxProposals::get() as u64 + 1); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_noop!( + Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()), proposal_len), + Error::::TooManyProposals + ); + }) +} + +#[test] +fn correct_validate_and_get_proposal() { + new_test_ext().execute_with(|| { + let proposal = Call::Collective(crate::Call::set_members { + new_members: vec![1, 2, 3], + prime: None, + old_count: MaxMembers::get(), + }); + let length = proposal.encode().len() as u32; + assert_ok!(Collective::propose(Origin::signed(1), 3, Box::new(proposal.clone()), length)); + + let hash = BlakeTwo256::hash_of(&proposal); + let weight = proposal.get_dispatch_info().weight; + assert_noop!( + Collective::validate_and_get_proposal( + &BlakeTwo256::hash_of(&vec![3; 4]), + length, + weight + ), + Error::::ProposalMissing + ); + assert_noop!( + Collective::validate_and_get_proposal(&hash, length - 2, weight), + Error::::WrongProposalLength + ); + assert_noop!( + Collective::validate_and_get_proposal( + &hash, + length, + weight - Weight::from_ref_time(10) + ), + Error::::WrongProposalWeight + ); + let res = Collective::validate_and_get_proposal(&hash, length, weight); + assert_ok!(res.clone()); + let (retrieved_proposal, len) = res.unwrap(); + assert_eq!(length as usize, len); + assert_eq!(proposal, retrieved_proposal); + }) +} + +#[test] +fn motions_ignoring_non_collective_proposals_works() { + new_test_ext().execute_with(|| { + let proposal = make_proposal(42); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + assert_noop!( + Collective::propose(Origin::signed(42), 3, Box::new(proposal.clone()), proposal_len), + Error::::NotMember + ); + }); +} + +#[test] +fn motions_ignoring_non_collective_votes_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!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!( + Collective::vote(Origin::signed(42), hash, 0, true), + Error::::NotMember, + ); + }); +} + +#[test] +fn motions_ignoring_bad_index_collective_vote_works() { + new_test_ext().execute_with(|| { + System::set_block_number(3); + 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!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_noop!( + Collective::vote(Origin::signed(2), hash, 1, true), + Error::::WrongIndex, + ); + }); +} + +#[test] +fn motions_vote_after_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(); + let end = 4; + assert_ok!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // Initially there a no votes when the motion is proposed. + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + // Cast first aye vote. + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![1], nays: vec![], end }) + ); + // Try to cast a duplicate aye vote. + assert_noop!( + Collective::vote(Origin::signed(1), hash, 0, true), + Error::::DuplicateVote, + ); + // Cast a nay vote. + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, false)); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![1], end }) + ); + // Try to cast a duplicate nay vote. + assert_noop!( + Collective::vote(Origin::signed(1), hash, 0, false), + Error::::DuplicateVote, + ); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: false, + yes: 0, + no: 1 + })), + ] + ); + }); +} + +#[test] +fn motions_all_first_vote_free_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(); + let end = 4; + assert_ok!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len, + )); + assert_eq!( + Collective::voting(&hash), + Some(Votes { index: 0, threshold: 2, ayes: vec![], nays: vec![], end }) + ); + + // For the motion, acc 2's first vote, expecting Ok with Pays::No. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(Origin::signed(2), hash, 0, true); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::No); + + // Duplicate vote, expecting error with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(Origin::signed(2), hash, 0, true); + assert_eq!(vote_rval.unwrap_err().post_info.pays_fee, Pays::Yes); + + // Modifying vote, expecting ok with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(Origin::signed(2), hash, 0, false); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); + + // For the motion, acc 3's first vote, expecting Ok with Pays::No. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(Origin::signed(3), hash, 0, true); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::No); + + // acc 3 modify the vote, expecting Ok with Pays::Yes. + let vote_rval: DispatchResultWithPostInfo = + Collective::vote(Origin::signed(3), hash, 0, false); + assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); + + // Test close() Extrincis | Check DispatchResultWithPostInfo with Pay Info + + System::set_block_number(3); + + let proposal_weight = proposal.get_dispatch_info().weight; + let close_rval: DispatchResultWithPostInfo = + Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len); + assert_eq!(close_rval.unwrap().pays_fee, Pays::No); + + // trying to close the proposal, which is already closed. + // Expecting error "ProposalMissing" with Pays::Yes + let close_rval: DispatchResultWithPostInfo = + Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len); + assert_eq!(close_rval.unwrap_err().post_info.pays_fee, Pays::Yes); + }); +} + +#[test] +fn motions_reproposing_disapproved_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: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, false)); + + System::set_block_number(3); + assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); + assert_eq!(*Collective::proposals(), vec![]); + assert_ok!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!(*Collective::proposals(), vec![hash]); + }); +} + +#[test] +fn motions_approval_with_enough_votes_and_lower_voting_threshold_works() { + new_test_ext().execute_with(|| { + let proposal = Call::Democracy(mock_democracy::Call::external_propose_majority {}); + let proposal_len: u32 = proposal.using_encoded(|p| p.len() as u32); + let proposal_weight = proposal.get_dispatch_info().weight; + let hash: H256 = proposal.blake2_256().into(); + // The voting threshold is 2, but the required votes for `ExternalMajorityOrigin` is 3. + // The proposal will be executed regardless of the voting threshold + // as long as we have enough yes votes. + // + // Failed to execute with only 2 yes votes. + assert_ok!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(Event::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })), + ] + ); + + System::reset_events(); + + // Executed with 3 yes votes. + assert_ok!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 1, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 1, true)); + assert_ok!(Collective::vote(Origin::signed(3), hash, 1, true)); + + System::set_block_number(5); + assert_ok!(Collective::close(Origin::signed(2), hash, 1, proposal_weight, proposal_len)); + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 1, + proposal_hash: hash, + threshold: 2 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 3, + proposal_hash: hash, + voted: true, + yes: 3, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 3, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(Event::Democracy(mock_democracy::pallet::Event::::ExternalProposed)), + record(Event::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Ok(()) + })), + ] + ); + }); +} + +#[test] +fn motions_disapproval_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: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, false)); + + System::set_block_number(3); + assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: false, + yes: 1, + no: 1 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 1, + no: 1 + })), + record(Event::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })), + ] + ); + }); +} + +#[test] +fn motions_approval_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: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(3); + assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); + + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Approved { proposal_hash: hash })), + record(Event::Collective(CollectiveEvent::Executed { + proposal_hash: hash, + result: Err(DispatchError::BadOrigin) + })), + ] + ); + }); +} + +#[test] +fn motion_with_no_votes_closes_with_disapproval() { + 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: H256 = proposal.blake2_256().into(); + assert_ok!(Collective::propose( + Origin::signed(1), + 3, + Box::new(proposal.clone()), + proposal_len + )); + assert_eq!( + System::events()[0], + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 3 + })) + ); + + System::set_block_number(3); + + // Closing the motion too early is not possible because it has neither + // an approving or disapproving simple majority due to the lack of votes. + assert_noop!( + Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len), + Error::::TooEarly + ); + + // Once the motion duration passes, + let closing_block = System::block_number() + MotionDuration::get(); + System::set_block_number(closing_block); + // we can successfully close the motion. + assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); + + // Events show that the close ended in a disapproval. + assert_eq!( + System::events()[1], + record(Event::Collective(CollectiveEvent::Closed { + proposal_hash: hash, + yes: 0, + no: 3 + })) + ); + assert_eq!( + System::events()[2], + record(Event::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })) + ); + }) +} + +#[test] +fn close_disapprove_does_not_care_about_weight_or_len() { + // This test confirms that if you close a proposal that would be disapproved, + // we do not care about the proposal length or proposal weight since it will + // not be read from storage or executed. + 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!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // First we make the proposal succeed + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + + System::set_block_number(3); + // It will not close with bad weight/len information + assert_noop!( + Collective::close(Origin::signed(2), hash, 0, Weight::zero(), 0), + Error::::WrongProposalLength, + ); + assert_noop!( + Collective::close(Origin::signed(2), hash, 0, Weight::zero(), proposal_len), + Error::::WrongProposalWeight, + ); + // Now we make the proposal fail + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, false)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, false)); + // It can close even if the weight/len information is bad + assert_ok!(Collective::close(Origin::signed(2), hash, 0, Weight::zero(), 0)); + }) +} + +#[test] +fn disapprove_proposal_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!(Collective::propose( + Origin::signed(1), + 2, + Box::new(proposal.clone()), + proposal_len + )); + // Proposal would normally succeed + assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); + assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); + // But Root can disapprove and remove it anyway + assert_ok!(Collective::disapprove_proposal(Origin::root(), hash)); + assert_eq!( + System::events(), + vec![ + record(Event::Collective(CollectiveEvent::Proposed { + account: 1, + proposal_index: 0, + proposal_hash: hash, + threshold: 2 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 1, + proposal_hash: hash, + voted: true, + yes: 1, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Voted { + account: 2, + proposal_hash: hash, + voted: true, + yes: 2, + no: 0 + })), + record(Event::Collective(CollectiveEvent::Disapproved { proposal_hash: hash })), + ] + ); + }) +} + +#[test] +#[should_panic(expected = "Members cannot contain duplicate accounts.")] +fn genesis_build_panics_with_duplicate_members() { + pallet_collective_mangata::GenesisConfig:: { + members: vec![1, 2, 3, 1], + phantom: Default::default(), + } + .build_storage() + .unwrap(); +} + +#[test] +fn migration_v4() { + new_test_ext().execute_with(|| { + use frame_support::traits::PalletInfoAccess; + + let old_pallet = "OldCollective"; + let new_pallet = ::name(); + frame_support::storage::migration::move_pallet( + new_pallet.as_bytes(), + old_pallet.as_bytes(), + ); + StorageVersion::new(0).put::(); + + crate::migrations::v4::pre_migrate::(old_pallet); + crate::migrations::v4::migrate::(old_pallet); + crate::migrations::v4::post_migrate::(old_pallet); + + let old_pallet = "OldCollectiveMajority"; + let new_pallet = ::name(); + frame_support::storage::migration::move_pallet( + new_pallet.as_bytes(), + old_pallet.as_bytes(), + ); + StorageVersion::new(0).put::(); + + crate::migrations::v4::pre_migrate::(old_pallet); + crate::migrations::v4::migrate::(old_pallet); + crate::migrations::v4::post_migrate::(old_pallet); + + let old_pallet = "OldDefaultCollective"; + let new_pallet = ::name(); + frame_support::storage::migration::move_pallet( + new_pallet.as_bytes(), + old_pallet.as_bytes(), + ); + StorageVersion::new(0).put::(); + + crate::migrations::v4::pre_migrate::(old_pallet); + crate::migrations::v4::migrate::(old_pallet); + crate::migrations::v4::post_migrate::(old_pallet); + }); +} diff --git a/frame/collective-mangata/src/weights.rs b/frame/collective-mangata/src/weights.rs new file mode 100644 index 0000000000000..50029046230af --- /dev/null +++ b/frame/collective-mangata/src/weights.rs @@ -0,0 +1,324 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for pallet_collective +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-05-23, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_collective +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --template=./.maintain/frame-weight-template.hbs +// --output=./frame/collective/src/weights.rs + +#![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_collective. +pub trait WeightInfo { + fn set_members(m: u32, n: u32, p: u32, ) -> Weight; + fn execute(b: u32, m: u32, ) -> Weight; + fn propose_execute(b: u32, m: u32, ) -> Weight; + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight; + fn vote(m: u32, ) -> Weight; + fn close_early_disapproved(m: u32, p: u32, ) -> Weight; + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn close_disapproved(m: u32, p: u32, ) -> Weight; + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight; + fn disapprove_proposal(p: u32, ) -> Weight; +} + +/// Weights for pallet_collective using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Council Members (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Voting (r:100 w:100) + // Storage: Council Prime (r:0 w:1) + fn set_members(m: u32, n: u32, p: u32, ) -> Weight { + Weight::from_ref_time(0 as u64) + // Standard Error: 12_000 + .saturating_add(Weight::from_ref_time(10_280_000 as u64).saturating_mul(m as u64)) + // Standard Error: 12_000 + .saturating_add(Weight::from_ref_time(126_000 as u64).saturating_mul(n as u64)) + // Standard Error: 12_000 + .saturating_add(Weight::from_ref_time(13_310_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().reads((1 as u64).saturating_mul(p as u64))) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + .saturating_add(T::DbWeight::get().writes((1 as u64).saturating_mul(p as u64))) + } + // Storage: Council Members (r:1 w:0) + fn execute(b: u32, m: u32, ) -> Weight { + Weight::from_ref_time(16_819_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(2_000 as u64).saturating_mul(b as u64)) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(33_000 as u64).saturating_mul(m as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:0) + fn propose_execute(b: u32, m: u32, ) -> Weight { + Weight::from_ref_time(18_849_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(2_000 as u64).saturating_mul(b as u64)) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(56_000 as u64).saturating_mul(m as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalCount (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(22_204_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(8_000 as u64).saturating_mul(b as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(49_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(180_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(4 as u64)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council Voting (r:1 w:1) + fn vote(m: u32, ) -> Weight { + Weight::from_ref_time(30_941_000 as u64) + // Standard Error: 2_000 + .saturating_add(Weight::from_ref_time(77_000 as u64).saturating_mul(m as u64)) + .saturating_add(T::DbWeight::get().reads(2 as u64)) + .saturating_add(T::DbWeight::get().writes(1 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(32_485_000 as u64) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(39_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(124_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(3 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(33_487_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(5_000 as u64).saturating_mul(b as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(66_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(157_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_disapproved(m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(33_494_000 as u64) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(58_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(124_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(36_566_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(5_000 as u64).saturating_mul(b as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(63_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(158_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(5 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } + // Storage: Council Proposals (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn disapprove_proposal(p: u32, ) -> Weight { + Weight::from_ref_time(20_159_000 as u64) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(173_000 as u64).saturating_mul(p as u64)) + .saturating_add(T::DbWeight::get().reads(1 as u64)) + .saturating_add(T::DbWeight::get().writes(3 as u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Council Members (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Voting (r:100 w:100) + // Storage: Council Prime (r:0 w:1) + fn set_members(m: u32, n: u32, p: u32, ) -> Weight { + Weight::from_ref_time(0 as u64) + // Standard Error: 12_000 + .saturating_add(Weight::from_ref_time(10_280_000 as u64).saturating_mul(m as u64)) + // Standard Error: 12_000 + .saturating_add(Weight::from_ref_time(126_000 as u64).saturating_mul(n as u64)) + // Standard Error: 12_000 + .saturating_add(Weight::from_ref_time(13_310_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().reads((1 as u64).saturating_mul(p as u64))) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + .saturating_add(RocksDbWeight::get().writes((1 as u64).saturating_mul(p as u64))) + } + // Storage: Council Members (r:1 w:0) + fn execute(b: u32, m: u32, ) -> Weight { + Weight::from_ref_time(16_819_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(2_000 as u64).saturating_mul(b as u64)) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(33_000 as u64).saturating_mul(m as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:0) + fn propose_execute(b: u32, m: u32, ) -> Weight { + Weight::from_ref_time(18_849_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(2_000 as u64).saturating_mul(b as u64)) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(56_000 as u64).saturating_mul(m as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalCount (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + fn propose_proposed(b: u32, m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(22_204_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(8_000 as u64).saturating_mul(b as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(49_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(180_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(4 as u64)) + } + // Storage: Council Members (r:1 w:0) + // Storage: Council Voting (r:1 w:1) + fn vote(m: u32, ) -> Weight { + Weight::from_ref_time(30_941_000 as u64) + // Standard Error: 2_000 + .saturating_add(Weight::from_ref_time(77_000 as u64).saturating_mul(m as u64)) + .saturating_add(RocksDbWeight::get().reads(2 as u64)) + .saturating_add(RocksDbWeight::get().writes(1 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_early_disapproved(m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(32_485_000 as u64) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(39_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(124_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(3 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_early_approved(b: u32, m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(33_487_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(5_000 as u64).saturating_mul(b as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(66_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(157_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council Proposals (r:1 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn close_disapproved(m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(33_494_000 as u64) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(58_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(124_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Council Voting (r:1 w:1) + // Storage: Council Members (r:1 w:0) + // Storage: Council Prime (r:1 w:0) + // Storage: Council ProposalOf (r:1 w:1) + // Storage: Council Proposals (r:1 w:1) + fn close_approved(b: u32, m: u32, p: u32, ) -> Weight { + Weight::from_ref_time(36_566_000 as u64) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(5_000 as u64).saturating_mul(b as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(63_000 as u64).saturating_mul(m as u64)) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(158_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(5 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } + // Storage: Council Proposals (r:1 w:1) + // Storage: Council Voting (r:0 w:1) + // Storage: Council ProposalOf (r:0 w:1) + fn disapprove_proposal(p: u32, ) -> Weight { + Weight::from_ref_time(20_159_000 as u64) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(173_000 as u64).saturating_mul(p as u64)) + .saturating_add(RocksDbWeight::get().reads(1 as u64)) + .saturating_add(RocksDbWeight::get().writes(3 as u64)) + } +} From 144c750823b357e36462868ca7ba5247b2f76d81 Mon Sep 17 00:00:00 2001 From: iStrike7 Date: Tue, 6 Dec 2022 13:07:20 +0530 Subject: [PATCH 3/6] Alert logs - Added logs to sudo --- frame/sudo-mangata/Cargo.toml | 41 ++++ frame/sudo-mangata/README.md | 77 ++++++++ frame/sudo-mangata/src/lib.rs | 319 ++++++++++++++++++++++++++++++++ frame/sudo-mangata/src/mock.rs | 172 +++++++++++++++++ frame/sudo-mangata/src/tests.rs | 208 +++++++++++++++++++++ 5 files changed, 817 insertions(+) create mode 100644 frame/sudo-mangata/Cargo.toml create mode 100644 frame/sudo-mangata/README.md create mode 100644 frame/sudo-mangata/src/lib.rs create mode 100644 frame/sudo-mangata/src/mock.rs create mode 100644 frame/sudo-mangata/src/tests.rs diff --git a/frame/sudo-mangata/Cargo.toml b/frame/sudo-mangata/Cargo.toml new file mode 100644 index 0000000000000..6a73021295db7 --- /dev/null +++ b/frame/sudo-mangata/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pallet-sudo-mangata" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME pallet for sudo" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +sp-io = { version = "6.0.0", default-features = false, path = "../../primitives/io" } +sp-runtime = { version = "6.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } + +[dev-dependencies] +sp-core = { version = "6.0.0", path = "../../primitives/core" } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime", +] diff --git a/frame/sudo-mangata/README.md b/frame/sudo-mangata/README.md new file mode 100644 index 0000000000000..7342832d2d7a7 --- /dev/null +++ b/frame/sudo-mangata/README.md @@ -0,0 +1,77 @@ +# Sudo Module + +- [`sudo::Config`](https://docs.rs/pallet-sudo/latest/pallet_sudo/trait.Config.html) +- [`Call`](https://docs.rs/pallet-sudo/latest/pallet_sudo/enum.Call.html) + +## Overview + +The Sudo module allows for a single account (called the "sudo key") +to execute dispatchable functions that require a `Root` call +or designate a new account to replace them as the sudo key. +Only one account can be the sudo key at a time. + +## Interface + +### Dispatchable Functions + +Only the sudo key can call the dispatchable functions from the Sudo module. + +* `sudo` - Make a `Root` call to a dispatchable function. +* `set_key` - Assign a new account to be the sudo key. + +## Usage + +### Executing Privileged Functions + +The Sudo module itself is not intended to be used within other modules. +Instead, you can build "privileged functions" (i.e. functions that require `Root` origin) in other modules. +You can execute these privileged functions by calling `sudo` with the sudo key account. +Privileged functions cannot be directly executed via an extrinsic. + +Learn more about privileged functions and `Root` origin in the [`Origin`] type documentation. + +### Simple Code Snippet + +This is an example of a module that exposes a privileged function: + +```rust +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config {} + + #[pallet::call] + impl Pallet { + #[pallet::weight(0)] + pub fn privileged_function(origin: OriginFor) -> DispatchResult { + ensure_root(origin)?; + + // do something... + + Ok(()) + } + } +} +``` + +## Genesis Config + +The Sudo module depends on the [`GenesisConfig`](https://docs.rs/pallet-sudo/latest/pallet_sudo/struct.GenesisConfig.html). +You need to set an initial superuser account as the sudo `key`. + +## Related Modules + +* [Democracy](https://docs.rs/pallet-democracy/latest/pallet_democracy/) + +[`Call`]: ./enum.Call.html +[`Config`]: ./trait.Config.html +[`Origin`]: https://docs.substrate.io/main-docs/build/origins/ + +License: Apache-2.0 diff --git a/frame/sudo-mangata/src/lib.rs b/frame/sudo-mangata/src/lib.rs new file mode 100644 index 0000000000000..5a5bb74968f7e --- /dev/null +++ b/frame/sudo-mangata/src/lib.rs @@ -0,0 +1,319 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Sudo Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! The Sudo pallet allows for a single account (called the "sudo key") +//! to execute dispatchable functions that require a `Root` call +//! or designate a new account to replace them as the sudo key. +//! Only one account can be the sudo key at a time. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! Only the sudo key can call the dispatchable functions from the Sudo pallet. +//! +//! * `sudo` - Make a `Root` call to a dispatchable function. +//! * `set_key` - Assign a new account to be the sudo key. +//! +//! ## Usage +//! +//! ### Executing Privileged Functions +//! +//! The Sudo pallet itself is not intended to be used within other pallets. +//! Instead, you can build "privileged functions" (i.e. functions that require `Root` origin) in +//! other pallets. You can execute these privileged functions by calling `sudo` with the sudo key +//! account. Privileged functions cannot be directly executed via an extrinsic. +//! +//! Learn more about privileged functions and `Root` origin in the [`Origin`] type documentation. +//! +//! ### Simple Code Snippet +//! +//! This is an example of a pallet that exposes a privileged function: +//! +//! ``` +//! #[frame_support::pallet] +//! pub mod pallet { +//! use super::*; +//! use frame_support::pallet_prelude::*; +//! use frame_system::pallet_prelude::*; +//! +//! #[pallet::pallet] +//! pub struct Pallet(_); +//! +//! #[pallet::config] +//! pub trait Config: frame_system::Config {} +//! +//! #[pallet::call] +//! impl Pallet { +//! #[pallet::weight(0)] +//! pub fn privileged_function(origin: OriginFor) -> DispatchResult { +//! ensure_root(origin)?; +//! +//! // do something... +//! +//! Ok(()) +//! } +//! } +//! } +//! # fn main() {} +//! ``` +//! +//! ## Genesis Config +//! +//! The Sudo pallet depends on the [`GenesisConfig`]. +//! You need to set an initial superuser account as the sudo `key`. +//! +//! ## Related Pallets +//! +//! * [Democracy](../pallet_democracy/index.html) +//! +//! [`Origin`]: https://docs.substrate.io/main-docs/build/origins/ + +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_runtime::{traits::StaticLookup, DispatchResult}; +use sp_std::prelude::*; + +use frame_support::{traits::UnfilteredDispatchable, weights::GetDispatchInfo}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use pallet::*; + +type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + +#[frame_support::pallet] +pub mod pallet { + use super::{DispatchResult, *}; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// A sudo-able call. + type Call: Parameter + UnfilteredDispatchable + GetDispatchInfo; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData); + + #[pallet::call] + impl Pallet { + /// Authenticates the sudo key and dispatches a function call with `Root` origin. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB write (event). + /// - Weight of derivative `call` execution + 10,000. + /// # + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + (dispatch_info.weight, dispatch_info.class) + })] + pub fn sudo( + origin: OriginFor, + call: Box<::Call>, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + + let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + Self::deposit_event(Event::Sudid { sudo_result: res.map(|_| ()).map_err(|e| e.error) }); + log::info!( + target: "runtime::sudo", + "A sudo action was performed: Call - {:?}, Result - {:?}!", + call.clone(), + res.clone() + ); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + + /// Authenticates the sudo key and dispatches a function call with `Root` origin. + /// This function does not check the weight of the call, and instead allows the + /// Sudo user to specify the weight of the call. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// # + /// - O(1). + /// - The weight of this call is defined by the caller. + /// # + #[pallet::weight((*_weight, call.get_dispatch_info().class))] + pub fn sudo_unchecked_weight( + origin: OriginFor, + call: Box<::Call>, + _weight: Weight, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + + let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + Self::deposit_event(Event::Sudid { sudo_result: res.map(|_| ()).map_err(|e| e.error) }); + log::info!( + target: "runtime::sudo", + "A sudo action was performed with unchecked weight: Call - {:?}, Result - {:?}!", + call.clone(), + res.clone() + ); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + + /// Authenticates the current sudo key and sets the given AccountId (`new`) as the new sudo + /// key. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB change. + /// # + #[pallet::weight(0)] + pub fn set_key( + origin: OriginFor, + new: AccountIdLookupOf, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + let new = T::Lookup::lookup(new)?; + + Self::deposit_event(Event::KeyChanged { old_sudoer: Key::::get() }); + log::info!( + target: "runtime::sudo", + "sudo key was changed: New Key - {:?}!", + new.clone(), + ); + Key::::put(&new); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + + /// Authenticates the sudo key and dispatches a function call with `Signed` origin from + /// a given account. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// # + /// - O(1). + /// - Limited storage reads. + /// - One DB write (event). + /// - Weight of derivative `call` execution + 10,000. + /// # + #[pallet::weight({ + let dispatch_info = call.get_dispatch_info(); + ( + dispatch_info.weight + // AccountData for inner call origin accountdata. + .saturating_add(T::DbWeight::get().reads_writes(1, 1)), + dispatch_info.class, + ) + })] + pub fn sudo_as( + origin: OriginFor, + who: AccountIdLookupOf, + call: Box<::Call>, + ) -> DispatchResultWithPostInfo { + // This is a public call, so we ensure that the origin is some signed account. + let sender = ensure_signed(origin)?; + ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); + + let who = T::Lookup::lookup(who)?; + + let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Signed(who).into()); + + Self::deposit_event(Event::SudoAsDone { + sudo_result: res.map(|_| ()).map_err(|e| e.error), + }); + log::info!( + target: "runtime::sudo", + "A sudo_as action was performed: Who - {:?}, Call - {:?}, Result - {:?}!", + who.clone(), + call.clone(), + res.clone() + ); + // Sudo user does not pay a fee. + Ok(Pays::No.into()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A sudo just took place. \[result\] + Sudid { sudo_result: DispatchResult }, + /// The \[sudoer\] just switched identity; the old key is supplied if one existed. + KeyChanged { old_sudoer: Option }, + /// A sudo just took place. \[result\] + SudoAsDone { sudo_result: DispatchResult }, + } + + #[pallet::error] + /// Error for the Sudo pallet + pub enum Error { + /// Sender must be the Sudo account + RequireSudo, + } + + /// The `AccountId` of the sudo key. + #[pallet::storage] + #[pallet::getter(fn key)] + pub(super) type Key = StorageValue<_, T::AccountId, OptionQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + /// The `AccountId` of the sudo key. + pub key: Option, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { key: None } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + if let Some(ref key) = self.key { + Key::::put(key); + } + } + } +} diff --git a/frame/sudo-mangata/src/mock.rs b/frame/sudo-mangata/src/mock.rs new file mode 100644 index 0000000000000..c895eaf830136 --- /dev/null +++ b/frame/sudo-mangata/src/mock.rs @@ -0,0 +1,172 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities + +use super::*; +use crate as sudo; +use frame_support::{ + parameter_types, + traits::{ConstU32, ConstU64, Contains, GenesisBuild}, + weights::Weight, +}; +use frame_system::limits; +use sp_core::H256; +use sp_io; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, +}; + +// Logger module to track execution. +#[frame_support::pallet] +pub mod logger { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData); + + #[pallet::call] + impl Pallet { + #[pallet::weight(*weight)] + pub fn privileged_i32_log( + origin: OriginFor, + i: i32, + weight: Weight, + ) -> DispatchResultWithPostInfo { + // Ensure that the `origin` is `Root`. + ensure_root(origin)?; + >::try_append(i).map_err(|_| "could not append")?; + Self::deposit_event(Event::AppendI32 { value: i, weight }); + Ok(().into()) + } + + #[pallet::weight(*weight)] + pub fn non_privileged_log( + origin: OriginFor, + i: i32, + weight: Weight, + ) -> DispatchResultWithPostInfo { + // Ensure that the `origin` is some signed account. + let sender = ensure_signed(origin)?; + >::try_append(i).map_err(|_| "could not append")?; + >::try_append(sender.clone()).map_err(|_| "could not append")?; + Self::deposit_event(Event::AppendI32AndAccount { sender, value: i, weight }); + Ok(().into()) + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + AppendI32 { value: i32, weight: Weight }, + AppendI32AndAccount { sender: T::AccountId, value: i32, weight: Weight }, + } + + #[pallet::storage] + #[pallet::getter(fn account_log)] + pub(super) type AccountLog = + StorageValue<_, BoundedVec>, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn i32_log)] + pub(super) type I32Log = StorageValue<_, BoundedVec>, ValueQuery>; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Sudo: sudo::{Pallet, Call, Config, Storage, Event}, + Logger: logger::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub BlockWeights: limits::BlockWeights = limits::BlockWeights::simple_max(Weight::from_ref_time(1024)); +} + +pub struct BlockEverything; +impl Contains for BlockEverything { + fn contains(_: &Call) -> bool { + false + } +} + +impl frame_system::Config for Test { + type BaseCallFilter = BlockEverything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + 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 = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +// Implement the logger module's `Config` on the Test runtime. +impl logger::Config for Test { + type Event = Event; +} + +// Implement the sudo module's `Config` on the Test runtime. +impl Config for Test { + type Event = Event; + type Call = Call; +} + +// New types for dispatchable functions. +pub type SudoCall = sudo::Call; +pub type LoggerCall = logger::Call; + +// Build test environment by setting the root `key` for the Genesis. +pub fn new_test_ext(root_key: u64) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + sudo::GenesisConfig:: { key: Some(root_key) } + .assimilate_storage(&mut t) + .unwrap(); + t.into() +} diff --git a/frame/sudo-mangata/src/tests.rs b/frame/sudo-mangata/src/tests.rs new file mode 100644 index 0000000000000..0508772cc88ec --- /dev/null +++ b/frame/sudo-mangata/src/tests.rs @@ -0,0 +1,208 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for the module. + +use super::*; +use frame_support::{assert_noop, assert_ok, weights::Weight}; +use mock::{ + new_test_ext, Call, Event as TestEvent, Logger, LoggerCall, Origin, Sudo, SudoCall, System, + Test, +}; + +#[test] +fn test_setup_works() { + // Environment setup, logger storage, and sudo `key` retrieval should work as expected. + new_test_ext(1).execute_with(|| { + assert_eq!(Sudo::key(), Some(1u64)); + assert!(Logger::i32_log().is_empty()); + assert!(Logger::account_log().is_empty()); + }); +} + +#[test] +fn sudo_basics() { + // Configure a default test environment and set the root `key` to 1. + new_test_ext(1).execute_with(|| { + // A privileged function should work when `sudo` is passed the root `key` as `origin`. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1_000), + })); + assert_ok!(Sudo::sudo(Origin::signed(1), call)); + assert_eq!(Logger::i32_log(), vec![42i32]); + + // A privileged function should not work when `sudo` is passed a non-root `key` as `origin`. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1_000), + })); + assert_noop!(Sudo::sudo(Origin::signed(2), call), Error::::RequireSudo); + }); +} + +#[test] +fn sudo_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // Should emit event to indicate success when called with the root `key` and `call` is `Ok`. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1), + })); + assert_ok!(Sudo::sudo(Origin::signed(1), call)); + System::assert_has_event(TestEvent::Sudo(Event::Sudid { sudo_result: Ok(()) })); + }) +} + +#[test] +fn sudo_unchecked_weight_basics() { + new_test_ext(1).execute_with(|| { + // A privileged function should work when `sudo` is passed the root `key` as origin. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1_000), + })); + assert_ok!(Sudo::sudo_unchecked_weight( + Origin::signed(1), + call, + Weight::from_ref_time(1_000) + )); + assert_eq!(Logger::i32_log(), vec![42i32]); + + // A privileged function should not work when called with a non-root `key`. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1_000), + })); + assert_noop!( + Sudo::sudo_unchecked_weight(Origin::signed(2), call, Weight::from_ref_time(1_000)), + Error::::RequireSudo, + ); + // `I32Log` is unchanged after unsuccessful call. + assert_eq!(Logger::i32_log(), vec![42i32]); + + // Controls the dispatched weight. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1), + })); + let sudo_unchecked_weight_call = + SudoCall::sudo_unchecked_weight { call, weight: Weight::from_ref_time(1_000) }; + let info = sudo_unchecked_weight_call.get_dispatch_info(); + assert_eq!(info.weight, Weight::from_ref_time(1_000)); + }); +} + +#[test] +fn sudo_unchecked_weight_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // Should emit event to indicate success when called with the root `key` and `call` is `Ok`. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1), + })); + assert_ok!(Sudo::sudo_unchecked_weight( + Origin::signed(1), + call, + Weight::from_ref_time(1_000) + )); + System::assert_has_event(TestEvent::Sudo(Event::Sudid { sudo_result: Ok(()) })); + }) +} + +#[test] +fn set_key_basics() { + new_test_ext(1).execute_with(|| { + // A root `key` can change the root `key` + assert_ok!(Sudo::set_key(Origin::signed(1), 2)); + assert_eq!(Sudo::key(), Some(2u64)); + }); + + new_test_ext(1).execute_with(|| { + // A non-root `key` will trigger a `RequireSudo` error and a non-root `key` cannot change + // the root `key`. + assert_noop!(Sudo::set_key(Origin::signed(2), 3), Error::::RequireSudo); + }); +} + +#[test] +fn set_key_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // A root `key` can change the root `key`. + assert_ok!(Sudo::set_key(Origin::signed(1), 2)); + System::assert_has_event(TestEvent::Sudo(Event::KeyChanged { old_sudoer: Some(1) })); + // Double check. + assert_ok!(Sudo::set_key(Origin::signed(2), 4)); + System::assert_has_event(TestEvent::Sudo(Event::KeyChanged { old_sudoer: Some(2) })); + }); +} + +#[test] +fn sudo_as_basics() { + new_test_ext(1).execute_with(|| { + // A privileged function will not work when passed to `sudo_as`. + let call = Box::new(Call::Logger(LoggerCall::privileged_i32_log { + i: 42, + weight: Weight::from_ref_time(1_000), + })); + assert_ok!(Sudo::sudo_as(Origin::signed(1), 2, call)); + assert!(Logger::i32_log().is_empty()); + assert!(Logger::account_log().is_empty()); + + // A non-privileged function should not work when called with a non-root `key`. + let call = Box::new(Call::Logger(LoggerCall::non_privileged_log { + i: 42, + weight: Weight::from_ref_time(1), + })); + assert_noop!(Sudo::sudo_as(Origin::signed(3), 2, call), Error::::RequireSudo); + + // A non-privileged function will work when passed to `sudo_as` with the root `key`. + let call = Box::new(Call::Logger(LoggerCall::non_privileged_log { + i: 42, + weight: Weight::from_ref_time(1), + })); + assert_ok!(Sudo::sudo_as(Origin::signed(1), 2, call)); + assert_eq!(Logger::i32_log(), vec![42i32]); + // The correct user makes the call within `sudo_as`. + assert_eq!(Logger::account_log(), vec![2]); + }); +} + +#[test] +fn sudo_as_emits_events_correctly() { + new_test_ext(1).execute_with(|| { + // Set block number to 1 because events are not emitted on block 0. + System::set_block_number(1); + + // A non-privileged function will work when passed to `sudo_as` with the root `key`. + let call = Box::new(Call::Logger(LoggerCall::non_privileged_log { + i: 42, + weight: Weight::from_ref_time(1), + })); + assert_ok!(Sudo::sudo_as(Origin::signed(1), 2, call)); + System::assert_has_event(TestEvent::Sudo(Event::SudoAsDone { sudo_result: Ok(()) })); + }); +} From 5a4168dbc755e12dfb852eb6ae6f3fbf59b6707c Mon Sep 17 00:00:00 2001 From: iStrike7 Date: Tue, 6 Dec 2022 15:28:57 +0530 Subject: [PATCH 4/6] Alert logs - Added log dep and minor edits --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 1 + frame/sudo-mangata/Cargo.toml | 1 + frame/sudo-mangata/src/lib.rs | 26 +++++++++++++------------- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0fccbc06c4ce..29aa9507ad1a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6124,6 +6124,21 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-sudo-mangata" +version = "4.0.0-dev" +dependencies = [ + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-template" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 9208bde2dbf3a..5abc8eeea8a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,6 +137,7 @@ members = [ "frame/staking/reward-fn", "frame/state-trie-migration", "frame/sudo", + "frame/sudo-mangata", "frame/support", "frame/support/procedural", "frame/support/procedural/tools", diff --git a/frame/sudo-mangata/Cargo.toml b/frame/sudo-mangata/Cargo.toml index 6a73021295db7..7075a735efdcd 100644 --- a/frame/sudo-mangata/Cargo.toml +++ b/frame/sudo-mangata/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +log = { version = "0.4.17", default-features = false } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } diff --git a/frame/sudo-mangata/src/lib.rs b/frame/sudo-mangata/src/lib.rs index 5a5bb74968f7e..e3e9e6394dd9f 100644 --- a/frame/sudo-mangata/src/lib.rs +++ b/frame/sudo-mangata/src/lib.rs @@ -149,13 +149,13 @@ pub mod pallet { let sender = ensure_signed(origin)?; ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); - let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); - Self::deposit_event(Event::Sudid { sudo_result: res.map(|_| ()).map_err(|e| e.error) }); + let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + Self::deposit_event(Event::Sudid { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error) }); log::info!( target: "runtime::sudo", "A sudo action was performed: Call - {:?}, Result - {:?}!", - call.clone(), - res.clone() + call, + res ); // Sudo user does not pay a fee. Ok(Pays::No.into()) @@ -181,13 +181,13 @@ pub mod pallet { let sender = ensure_signed(origin)?; ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); - let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); - Self::deposit_event(Event::Sudid { sudo_result: res.map(|_| ()).map_err(|e| e.error) }); + let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); + Self::deposit_event(Event::Sudid { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error) }); log::info!( target: "runtime::sudo", "A sudo action was performed with unchecked weight: Call - {:?}, Result - {:?}!", - call.clone(), - res.clone() + call, + res ); // Sudo user does not pay a fee. Ok(Pays::No.into()) @@ -255,17 +255,17 @@ pub mod pallet { let who = T::Lookup::lookup(who)?; - let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Signed(who).into()); + let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Signed(who.clone()).into()); Self::deposit_event(Event::SudoAsDone { - sudo_result: res.map(|_| ()).map_err(|e| e.error), + sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), }); log::info!( target: "runtime::sudo", "A sudo_as action was performed: Who - {:?}, Call - {:?}, Result - {:?}!", - who.clone(), - call.clone(), - res.clone() + who, + call, + res ); // Sudo user does not pay a fee. Ok(Pays::No.into()) From 17c93848e1b400eb33d18a5894920e37ca6061ef Mon Sep 17 00:00:00 2001 From: iStrike7 Date: Thu, 8 Dec 2022 15:07:47 +0530 Subject: [PATCH 5/6] Alert Logs - Added Alert string and minor edits --- frame/collective-mangata/src/lib.rs | 124 +++++++++++++++++----------- frame/sudo-mangata/src/lib.rs | 30 +++++-- 2 files changed, 100 insertions(+), 54 deletions(-) diff --git a/frame/collective-mangata/src/lib.rs b/frame/collective-mangata/src/lib.rs index 4554d8d2e4582..d0816ffa0513f 100644 --- a/frame/collective-mangata/src/lib.rs +++ b/frame/collective-mangata/src/lib.rs @@ -65,6 +65,20 @@ mod benchmarking; pub mod migrations; pub mod weights; +pub(crate) const LOG_TARGET: &'static str = "collective-mangata"; +pub(crate) const ALERT_STRING: &'static str = "ALERT!ALERT!ALERT!"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! alert_log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] {:?} ", $patter), >::block_number(), crate::ALERT_STRING $(, $values)* + ) + }; +} + pub use pallet::*; pub use weights::WeightInfo; @@ -457,15 +471,17 @@ pub mod pallet { ensure!(proposal_len <= length_bound as usize, Error::::WrongProposalLength); let proposal_hash = T::Hashing::hash_of(&proposal); - let result = proposal.dispatch(RawOrigin::Member(who).into()); + let result = proposal.clone().dispatch(RawOrigin::Member(who.clone()).into()); Self::deposit_event(Event::MemberExecuted { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); - log::info!( - target: "runtime::collective", - "A member has executed a proposal!" + alert_log!( + info, + "A member has executed a proposal! Member: {:?}, Proposal: {:?}", + who, + proposal ); Ok(get_result_weight(result) @@ -706,15 +722,16 @@ impl, I: 'static> Pallet { ensure!(!>::contains_key(proposal_hash), Error::::DuplicateProposal); let seats = Self::members().len() as MemberCount; - let result = proposal.dispatch(RawOrigin::Members(1, seats).into()); + let result = proposal.clone().dispatch(RawOrigin::Members(1, seats).into()); Self::deposit_event(Event::Executed { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); - log::info!( - target: "runtime::collective", - "A member has executed a proposal!" + alert_log!( + info, + "A member has executed a proposal! Proposal: {:?}", + proposal ); Ok((proposal_len as u32, result)) @@ -741,7 +758,7 @@ impl, I: 'static> Pallet { let index = Self::proposal_count(); >::mutate(|i| *i += 1); - >::insert(proposal_hash, proposal); + >::insert(proposal_hash, proposal.clone()); >::insert(proposal_hash, frame_system::Pallet::::block_number()); let votes = { @@ -757,9 +774,10 @@ impl, I: 'static> Pallet { threshold, }); - log::info!( - target: "runtime::collective", - "A proposal has been proposed!" + alert_log!( + info, + "A proposal has been proposed! Proposal: {:?}", + proposal ); Ok((proposal_len as u32, active_proposals as u32)) @@ -805,16 +823,19 @@ impl, I: 'static> Pallet { let yes_votes = voting.ayes.len() as MemberCount; let no_votes = voting.nays.len() as MemberCount; Self::deposit_event(Event::Voted { - account: who, - proposal_hash: proposal, + account: who.clone(), + proposal_hash: proposal.clone(), voted: approve, yes: yes_votes, no: no_votes, }); - log::info!( - target: "runtime::collective", - "A member has voted on a proposal!" + alert_log!( + info, + "A member has voted on a proposal! Member: {:?}, Proposal: {:?}, Voted: {:?}", + who, + proposal, + approve ); Voting::::insert(&proposal, voting); @@ -852,9 +873,10 @@ impl, I: 'static> Pallet { )?; Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - log::info!( - target: "runtime::collective", - "A proposal has been closed!" + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() ); let (proposal_weight, proposal_count) = @@ -870,9 +892,10 @@ impl, I: 'static> Pallet { } else if disapproved { Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - log::info!( - target: "runtime::collective", - "A proposal has been closed!" + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() ); let proposal_count = Self::do_disapprove_proposal(proposal_hash); @@ -906,9 +929,10 @@ impl, I: 'static> Pallet { )?; Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - log::info!( - target: "runtime::collective", - "A proposal has been closed!" + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() ); let (proposal_weight, proposal_count) = @@ -924,9 +948,10 @@ impl, I: 'static> Pallet { } else { Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - log::info!( - target: "runtime::collective", - "A proposal has been closed!" + alert_log!( + info, + "A proposal has been closed! Proposal Hash: {:?}", + proposal_hash.clone() ); let proposal_count = Self::do_disapprove_proposal(proposal_hash); @@ -976,22 +1001,26 @@ impl, I: 'static> Pallet { ) -> (Weight, u32) { Self::deposit_event(Event::Approved { proposal_hash }); - log::info!( - target: "runtime::collective", - "A proposal has been approved!" + alert_log!( + info, + "A proposal has been approved! Proposal Hash: {:?}, Proposal: {:?}", + proposal_hash.clone(), + proposal.clone() ); let dispatch_weight = proposal.get_dispatch_info().weight; let origin = RawOrigin::Members(yes_votes, seats).into(); - let result = proposal.dispatch(origin); + let result = proposal.clone().dispatch(origin); Self::deposit_event(Event::Executed { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); - log::info!( - target: "runtime::collective", - "A proposal has been executed!" + alert_log!( + info, + "A proposal has been executed! Proposal Hash: {:?}, Proposal: {:?}", + proposal_hash.clone(), + proposal.clone() ); // default to the dispatch info weight for safety @@ -1006,9 +1035,10 @@ impl, I: 'static> Pallet { // disapproved Self::deposit_event(Event::Disapproved { proposal_hash }); - log::info!( - target: "runtime::collective", - "A proposal has been disapproved!" + alert_log!( + info, + "A proposal has been disapproved! Proposal Hash: {:?}", + proposal_hash.clone() ); Self::remove_proposal(proposal_hash) @@ -1086,9 +1116,10 @@ impl, I: 'static> ChangeMembers for Pallet { new_members: new.to_vec() }); - log::info!( - target: "runtime::collective", - "Collective members have changed!!!" + alert_log!( + info, + "Collective members have changed!!! New Members: {:?}", + new.to_vec(), ); } @@ -1096,12 +1127,13 @@ impl, I: 'static> ChangeMembers for Pallet { fn set_prime(prime: Option) { Prime::::set(prime.clone()); Pallet::::deposit_event(Event::PrimeSet { - new_prime: prime + new_prime: prime.clone() }); - log::info!( - target: "runtime::collective", - "Prime member has changed!" + alert_log!( + info, + "Prime member has changed! New Prime: {:?}", + prime, ); } diff --git a/frame/sudo-mangata/src/lib.rs b/frame/sudo-mangata/src/lib.rs index e3e9e6394dd9f..a640488ab9923 100644 --- a/frame/sudo-mangata/src/lib.rs +++ b/frame/sudo-mangata/src/lib.rs @@ -102,6 +102,20 @@ mod mock; #[cfg(test)] mod tests; +pub(crate) const LOG_TARGET: &'static str = "sudo-mangata"; +pub(crate) const ALERT_STRING: &'static str = "ALERT!ALERT!ALERT!"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! alert_log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] {:?} ", $patter), >::block_number(), crate::ALERT_STRING $(, $values)* + ) + }; +} + pub use pallet::*; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; @@ -151,8 +165,8 @@ pub mod pallet { let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); Self::deposit_event(Event::Sudid { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error) }); - log::info!( - target: "runtime::sudo", + alert_log!( + info, "A sudo action was performed: Call - {:?}, Result - {:?}!", call, res @@ -183,8 +197,8 @@ pub mod pallet { let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); Self::deposit_event(Event::Sudid { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error) }); - log::info!( - target: "runtime::sudo", + alert_log!( + info, "A sudo action was performed with unchecked weight: Call - {:?}, Result - {:?}!", call, res @@ -214,8 +228,8 @@ pub mod pallet { let new = T::Lookup::lookup(new)?; Self::deposit_event(Event::KeyChanged { old_sudoer: Key::::get() }); - log::info!( - target: "runtime::sudo", + alert_log!( + info, "sudo key was changed: New Key - {:?}!", new.clone(), ); @@ -260,8 +274,8 @@ pub mod pallet { Self::deposit_event(Event::SudoAsDone { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), }); - log::info!( - target: "runtime::sudo", + alert_log!( + info, "A sudo_as action was performed: Who - {:?}, Call - {:?}, Result - {:?}!", who, call, From b340f09861afd1139320749725079742d2f57231 Mon Sep 17 00:00:00 2001 From: iStrike7 Date: Thu, 8 Dec 2022 15:11:19 +0530 Subject: [PATCH 6/6] fmt --- frame/collective-mangata/src/lib.rs | 76 ++++++++++++--------------- frame/collective-mangata/src/tests.rs | 20 +++---- frame/sudo-mangata/src/lib.rs | 25 ++++----- 3 files changed, 51 insertions(+), 70 deletions(-) diff --git a/frame/collective-mangata/src/lib.rs b/frame/collective-mangata/src/lib.rs index d0816ffa0513f..c0b87ffc05cce 100644 --- a/frame/collective-mangata/src/lib.rs +++ b/frame/collective-mangata/src/lib.rs @@ -44,7 +44,10 @@ use scale_info::TypeInfo; use sp_io::storage; -use sp_runtime::{traits::{Hash, Saturating}, RuntimeDebug}; +use sp_runtime::{ + traits::{Hash, Saturating}, + RuntimeDebug, +}; use sp_std::{marker::PhantomData, prelude::*, result, vec}; use frame_support::{ @@ -329,9 +332,9 @@ pub mod pallet { /// A proposal was closed because its threshold was reached or after its duration was up. Closed { proposal_hash: T::Hash, yes: MemberCount, no: MemberCount }, /// The members have been changed - MembersChanged {new_members: Vec}, + MembersChanged { new_members: Vec }, /// The Prime member has been set - PrimeSet {new_prime: Option}, + PrimeSet { new_prime: Option }, } #[pallet::error] @@ -350,7 +353,8 @@ pub mod pallet { AlreadyInitialized, /// The close call was made too early, before the end of the voting. TooEarly, - /// To early to close the proposal, can only close ProposalCloseDelay blocks after proposal was proposed + /// To early to close the proposal, can only close ProposalCloseDelay blocks after proposal + /// was proposed TooEarlyToClose, /// There can only be a maximum of `MaxProposals` active proposals. TooManyProposals, @@ -727,12 +731,8 @@ impl, I: 'static> Pallet { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); - - alert_log!( - info, - "A member has executed a proposal! Proposal: {:?}", - proposal - ); + + alert_log!(info, "A member has executed a proposal! Proposal: {:?}", proposal); Ok((proposal_len as u32, result)) } @@ -759,7 +759,10 @@ impl, I: 'static> Pallet { let index = Self::proposal_count(); >::mutate(|i| *i += 1); >::insert(proposal_hash, proposal.clone()); - >::insert(proposal_hash, frame_system::Pallet::::block_number()); + >::insert( + proposal_hash, + frame_system::Pallet::::block_number(), + ); let votes = { let end = frame_system::Pallet::::block_number() + T::MotionDuration::get(); @@ -773,12 +776,8 @@ impl, I: 'static> Pallet { proposal_hash, threshold, }); - - alert_log!( - info, - "A proposal has been proposed! Proposal: {:?}", - proposal - ); + + alert_log!(info, "A proposal has been proposed! Proposal: {:?}", proposal); Ok((proposal_len as u32, active_proposals as u32)) } @@ -855,9 +854,14 @@ impl, I: 'static> Pallet { // To allow previously existing proposals to be executed we use unwrap_or_default // This can be removed later on when no proposals are without a proposed time in storage. - let proposal_proposed_time = Self::proposal_proposed_time(&proposal_hash).unwrap_or_default(); + let proposal_proposed_time = + Self::proposal_proposed_time(&proposal_hash).unwrap_or_default(); // Only allow actual closing of the proposal after the voting period has ended. - ensure!(frame_system::Pallet::::block_number() >= proposal_proposed_time.saturating_add(T::ProposalCloseDelay::get()), Error::::TooEarlyToClose); + ensure!( + frame_system::Pallet::::block_number() >= + proposal_proposed_time.saturating_add(T::ProposalCloseDelay::get()), + Error::::TooEarlyToClose + ); let mut no_votes = voting.nays.len() as MemberCount; let mut yes_votes = voting.ayes.len() as MemberCount; @@ -872,7 +876,7 @@ impl, I: 'static> Pallet { proposal_weight_bound, )?; Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - + alert_log!( info, "A proposal has been closed! Proposal Hash: {:?}", @@ -891,7 +895,7 @@ impl, I: 'static> Pallet { .into()) } else if disapproved { Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - + alert_log!( info, "A proposal has been closed! Proposal Hash: {:?}", @@ -928,7 +932,7 @@ impl, I: 'static> Pallet { proposal_weight_bound, )?; Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - + alert_log!( info, "A proposal has been closed! Proposal Hash: {:?}", @@ -947,7 +951,7 @@ impl, I: 'static> Pallet { .into()) } else { Self::deposit_event(Event::Closed { proposal_hash, yes: yes_votes, no: no_votes }); - + alert_log!( info, "A proposal has been closed! Proposal Hash: {:?}", @@ -1000,7 +1004,7 @@ impl, I: 'static> Pallet { proposal: >::Proposal, ) -> (Weight, u32) { Self::deposit_event(Event::Approved { proposal_hash }); - + alert_log!( info, "A proposal has been approved! Proposal Hash: {:?}, Proposal: {:?}", @@ -1015,7 +1019,7 @@ impl, I: 'static> Pallet { proposal_hash, result: result.map(|_| ()).map_err(|e| e.error), }); - + alert_log!( info, "A proposal has been executed! Proposal Hash: {:?}, Proposal: {:?}", @@ -1112,30 +1116,16 @@ impl, I: 'static> ChangeMembers for Pallet { Members::::put(new); Prime::::kill(); - Pallet::::deposit_event(Event::MembersChanged { - new_members: new.to_vec() - }); - - alert_log!( - info, - "Collective members have changed!!! New Members: {:?}", - new.to_vec(), - ); + Pallet::::deposit_event(Event::MembersChanged { new_members: new.to_vec() }); + alert_log!(info, "Collective members have changed!!! New Members: {:?}", new.to_vec(),); } fn set_prime(prime: Option) { Prime::::set(prime.clone()); - Pallet::::deposit_event(Event::PrimeSet { - new_prime: prime.clone() - }); - - alert_log!( - info, - "Prime member has changed! New Prime: {:?}", - prime, - ); + Pallet::::deposit_event(Event::PrimeSet { new_prime: prime.clone() }); + alert_log!(info, "Prime member has changed! New Prime: {:?}", prime,); } fn get_prime() -> Option { diff --git a/frame/collective-mangata/src/tests.rs b/frame/collective-mangata/src/tests.rs index e81c5dd9c6ff9..be6eca86c9bc8 100644 --- a/frame/collective-mangata/src/tests.rs +++ b/frame/collective-mangata/src/tests.rs @@ -407,9 +407,7 @@ fn close_with_prime_works() { record(Event::Collective(CollectiveEvent::MembersChanged { new_members: vec![1, 2, 3] })), - record(Event::Collective(CollectiveEvent::PrimeSet { - new_prime: Some(3), - })), + record(Event::Collective(CollectiveEvent::PrimeSet { new_prime: Some(3) })), record(Event::Collective(CollectiveEvent::Proposed { account: 1, proposal_index: 0, @@ -473,9 +471,7 @@ fn close_with_voting_prime_works() { record(Event::Collective(CollectiveEvent::MembersChanged { new_members: vec![1, 2, 3] })), - record(Event::Collective(CollectiveEvent::PrimeSet { - new_prime: Some(1), - })), + record(Event::Collective(CollectiveEvent::PrimeSet { new_prime: Some(1) })), record(Event::Collective(CollectiveEvent::Proposed { account: 1, proposal_index: 0, @@ -550,9 +546,7 @@ fn close_with_no_prime_but_majority_works() { record(Event::CollectiveMajority(CollectiveEvent::MembersChanged { new_members: vec![1, 2, 3, 4, 5] })), - record(Event::CollectiveMajority(CollectiveEvent::PrimeSet { - new_prime: Some(5), - })), + record(Event::CollectiveMajority(CollectiveEvent::PrimeSet { new_prime: Some(5) })), record(Event::CollectiveMajority(CollectiveEvent::Proposed { account: 1, proposal_index: 0, @@ -952,7 +946,7 @@ fn motions_all_first_vote_free_works() { assert_eq!(vote_rval.unwrap().pays_fee, Pays::Yes); // Test close() Extrincis | Check DispatchResultWithPostInfo with Pay Info - + System::set_block_number(3); let proposal_weight = proposal.get_dispatch_info().weight; @@ -982,7 +976,7 @@ fn motions_reproposing_disapproved_works() { proposal_len )); assert_ok!(Collective::vote(Origin::signed(2), hash, 0, false)); - + System::set_block_number(3); assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); assert_eq!(*Collective::proposals(), vec![]); @@ -1016,7 +1010,7 @@ fn motions_approval_with_enough_votes_and_lower_voting_threshold_works() { )); assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); assert_ok!(Collective::vote(Origin::signed(2), hash, 0, true)); - + System::set_block_number(3); assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); assert_eq!( @@ -1131,7 +1125,7 @@ fn motions_disapproval_works() { )); assert_ok!(Collective::vote(Origin::signed(1), hash, 0, true)); assert_ok!(Collective::vote(Origin::signed(2), hash, 0, false)); - + System::set_block_number(3); assert_ok!(Collective::close(Origin::signed(2), hash, 0, proposal_weight, proposal_len)); diff --git a/frame/sudo-mangata/src/lib.rs b/frame/sudo-mangata/src/lib.rs index a640488ab9923..0995dbb3e0a1a 100644 --- a/frame/sudo-mangata/src/lib.rs +++ b/frame/sudo-mangata/src/lib.rs @@ -164,13 +164,10 @@ pub mod pallet { ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); - Self::deposit_event(Event::Sudid { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error) }); - alert_log!( - info, - "A sudo action was performed: Call - {:?}, Result - {:?}!", - call, - res - ); + Self::deposit_event(Event::Sudid { + sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), + }); + alert_log!(info, "A sudo action was performed: Call - {:?}, Result - {:?}!", call, res); // Sudo user does not pay a fee. Ok(Pays::No.into()) } @@ -196,7 +193,9 @@ pub mod pallet { ensure!(Self::key().map_or(false, |k| sender == k), Error::::RequireSudo); let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Root.into()); - Self::deposit_event(Event::Sudid { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error) }); + Self::deposit_event(Event::Sudid { + sudo_result: res.clone().map(|_| ()).map_err(|e| e.error), + }); alert_log!( info, "A sudo action was performed with unchecked weight: Call - {:?}, Result - {:?}!", @@ -228,11 +227,7 @@ pub mod pallet { let new = T::Lookup::lookup(new)?; Self::deposit_event(Event::KeyChanged { old_sudoer: Key::::get() }); - alert_log!( - info, - "sudo key was changed: New Key - {:?}!", - new.clone(), - ); + alert_log!(info, "sudo key was changed: New Key - {:?}!", new.clone(),); Key::::put(&new); // Sudo user does not pay a fee. Ok(Pays::No.into()) @@ -269,7 +264,9 @@ pub mod pallet { let who = T::Lookup::lookup(who)?; - let res = call.clone().dispatch_bypass_filter(frame_system::RawOrigin::Signed(who.clone()).into()); + let res = call + .clone() + .dispatch_bypass_filter(frame_system::RawOrigin::Signed(who.clone()).into()); Self::deposit_event(Event::SudoAsDone { sudo_result: res.clone().map(|_| ()).map_err(|e| e.error),