diff --git a/Cargo.lock b/Cargo.lock index e53b05d1cc55a..2acf7ea6c7c02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3488,6 +3488,7 @@ dependencies = [ "pallet-im-online", "pallet-indices", "pallet-membership", + "pallet-multisig", "pallet-offences", "pallet-offences-benchmarking", "pallet-proxy", @@ -4225,6 +4226,22 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-multisig" +version = "2.0.0-rc2" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-nicks" version = "2.0.0-rc2" @@ -4289,6 +4306,7 @@ dependencies = [ "frame-support", "frame-system", "pallet-balances", + "pallet-utility", "parity-scale-codec", "serde", "sp-core", diff --git a/Cargo.toml b/Cargo.toml index 650124877cc87..782cdcd23a4e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ members = [ "frame/indices", "frame/membership", "frame/metadata", + "frame/multisig", "frame/nicks", "frame/offences", "frame/proxy", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index b451ac109e983..4f304e6ace000 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -59,6 +59,7 @@ pallet-im-online = { version = "2.0.0-rc2", default-features = false, path = ".. pallet-indices = { version = "2.0.0-rc2", default-features = false, path = "../../../frame/indices" } pallet-identity = { version = "2.0.0-rc2", default-features = false, path = "../../../frame/identity" } pallet-membership = { version = "2.0.0-rc2", default-features = false, path = "../../../frame/membership" } +pallet-multisig = { version = "2.0.0-rc2", default-features = false, path = "../../../frame/multisig" } pallet-offences = { version = "2.0.0-rc2", default-features = false, path = "../../../frame/offences" } pallet-offences-benchmarking = { version = "2.0.0-rc2", path = "../../../frame/offences/benchmarking", default-features = false, optional = true } pallet-proxy = { version = "2.0.0-rc2", default-features = false, path = "../../../frame/proxy" } @@ -108,6 +109,7 @@ std = [ "pallet-indices/std", "sp-inherents/std", "pallet-membership/std", + "pallet-multisig/std", "pallet-identity/std", "node-primitives/std", "sp-offchain/std", @@ -151,6 +153,7 @@ runtime-benchmarks = [ "pallet-elections-phragmen/runtime-benchmarks", "pallet-identity/runtime-benchmarks", "pallet-im-online/runtime-benchmarks", + "pallet-multisig/runtime-benchmarks", "pallet-proxy/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-society/runtime-benchmarks", diff --git a/bin/node/runtime/src/constants.rs b/bin/node/runtime/src/constants.rs index 45f1ab19a450e..8e87d61c1e6b5 100644 --- a/bin/node/runtime/src/constants.rs +++ b/bin/node/runtime/src/constants.rs @@ -24,6 +24,10 @@ pub mod currency { pub const MILLICENTS: Balance = 1_000_000_000; pub const CENTS: Balance = 1_000 * MILLICENTS; // assume this is worth about a cent. pub const DOLLARS: Balance = 100 * CENTS; + + pub const fn deposit(items: u32, bytes: u32) -> Balance { + items as Balance * 15 * CENTS + (bytes as Balance) * 6 * CENTS + } } /// Time. diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index cac93b63e08b4..a1cff7df91df7 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -23,6 +23,7 @@ #![recursion_limit="256"] use sp_std::prelude::*; + use frame_support::{ construct_runtime, parameter_types, debug, RuntimeDebug, weights::{ @@ -31,6 +32,7 @@ use frame_support::{ }, traits::{Currency, Imbalance, KeyOwnerProofSystem, OnUnbalanced, Randomness, LockIdentifier}, }; +use frame_support::traits::{Filter, InstanceFilter}; use codec::{Encode, Decode}; use sp_core::{ crypto::KeyTypeId, @@ -79,7 +81,6 @@ use impls::{CurrencyToVoteHandler, Author, TargetedFeeAdjustment}; /// Constant values used within the runtime. pub mod constants; use constants::{time::*, currency::*}; -use frame_support::traits::InstanceFilter; // Make the WASM binary available. #[cfg(feature = "std")] @@ -111,6 +112,15 @@ pub fn native_version() -> NativeVersion { type NegativeImbalance = >::NegativeImbalance; +pub struct BaseFilter; +impl Filter for BaseFilter { + fn filter(_call: &Call) -> bool { + true + } +} +pub struct IsCallable; +frame_support::impl_filter_stack!(IsCallable, BaseFilter, Call, is_callable); + pub struct DealWithFees; impl OnUnbalanced for DealWithFees { fn on_unbalanceds(mut fees_then_tips: impl Iterator) { @@ -169,24 +179,28 @@ impl frame_system::Trait for Runtime { type OnKilledAccount = (); } -const fn deposit(items: u32, bytes: u32) -> Balance { items as Balance * 15 * CENTS + (bytes as Balance) * 6 * CENTS } +impl pallet_utility::Trait for Runtime { + type Event = Event; + type Call = Call; + type IsCallable = IsCallable; +} parameter_types! { // One storage item; key size is 32; value is size 4+4+16+32 bytes = 56 bytes. - pub const MultisigDepositBase: Balance = deposit(1, 88); + pub const DepositBase: Balance = deposit(1, 88); // Additional storage item size of 32 bytes. - pub const MultisigDepositFactor: Balance = deposit(0, 32); + pub const DepositFactor: Balance = deposit(0, 32); pub const MaxSignatories: u16 = 100; } -impl pallet_utility::Trait for Runtime { +impl pallet_multisig::Trait for Runtime { type Event = Event; type Call = Call; type Currency = Balances; - type MultisigDepositBase = MultisigDepositBase; - type MultisigDepositFactor = MultisigDepositFactor; + type DepositBase = DepositBase; + type DepositFactor = DepositFactor; type MaxSignatories = MaxSignatories; - type IsCallable = (); + type IsCallable = IsCallable; } parameter_types! { @@ -211,8 +225,7 @@ impl InstanceFilter for ProxyType { match self { ProxyType::Any => true, ProxyType::NonTransfer => !matches!(c, - Call::Balances(..) | Call::Utility(..) - | Call::Vesting(pallet_vesting::Call::vested_transfer(..)) + Call::Balances(..) | Call::Vesting(pallet_vesting::Call::vested_transfer(..)) | Call::Indices(pallet_indices::Call::transfer(..)) ), ProxyType::Governance => matches!(c, @@ -222,13 +235,22 @@ impl InstanceFilter for ProxyType { ProxyType::Staking => matches!(c, Call::Staking(..)), } } + fn is_superset(&self, o: &Self) -> bool { + match (self, o) { + (x, y) if x == y => true, + (ProxyType::Any, _) => true, + (_, ProxyType::Any) => false, + (ProxyType::NonTransfer, _) => true, + _ => false, + } + } } impl pallet_proxy::Trait for Runtime { type Event = Event; type Call = Call; type Currency = Balances; - type IsCallable = (); + type IsCallable = IsCallable; type ProxyType = ProxyType; type ProxyDepositBase = ProxyDepositBase; type ProxyDepositFactor = ProxyDepositFactor; @@ -263,9 +285,9 @@ parameter_types! { impl pallet_indices::Trait for Runtime { type AccountIndex = AccountIndex; - type Event = Event; type Currency = Balances; type Deposit = IndexDeposit; + type Event = Event; } parameter_types! { @@ -341,11 +363,11 @@ impl pallet_session::Trait for Runtime { type ValidatorId = ::AccountId; type ValidatorIdOf = pallet_staking::StashOf; type ShouldEndSession = Babe; + type NextSessionRotation = Babe; type SessionManager = pallet_session::historical::NoteHistoricalRoot; type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; - type NextSessionRotation = Babe; } impl pallet_session::historical::Trait for Runtime { @@ -474,8 +496,8 @@ parameter_types! { const_assert!(DesiredMembers::get() <= pallet_collective::MAX_MEMBERS); impl pallet_elections_phragmen::Trait for Runtime { - type ModuleId = ElectionsPhragmenModuleId; type Event = Event; + type ModuleId = ElectionsPhragmenModuleId; type Currency = Balances; type ChangeMembers = Council; // NOTE: this implies that council's genesis members cannot be set directly and must come from @@ -530,6 +552,7 @@ parameter_types! { } impl pallet_treasury::Trait for Runtime { + type ModuleId = TreasuryModuleId; type Currency = Balances; type ApproveOrigin = pallet_collective::EnsureMembers<_4, AccountId, CouncilCollective>; type RejectOrigin = pallet_collective::EnsureMembers<_2, AccountId, CouncilCollective>; @@ -544,7 +567,6 @@ impl pallet_treasury::Trait for Runtime { type ProposalBondMinimum = ProposalBondMinimum; type SpendPeriod = SpendPeriod; type Burn = Burn; - type ModuleId = TreasuryModuleId; } parameter_types! { @@ -635,8 +657,8 @@ impl frame_system::offchain::SigningTypes for Runtime { impl frame_system::offchain::SendTransactionTypes for Runtime where Call: From, { - type OverarchingCall = Call; type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = Call; } impl pallet_im_online::Trait for Runtime { @@ -746,6 +768,7 @@ parameter_types! { impl pallet_society::Trait for Runtime { type Event = Event; + type ModuleId = SocietyModuleId; type Currency = Balances; type Randomness = RandomnessCollectiveFlip; type CandidateDeposit = CandidateDeposit; @@ -758,7 +781,6 @@ impl pallet_society::Trait for Runtime { type FounderSetOrigin = pallet_collective::EnsureProportionMoreThan<_1, _2, AccountId, CouncilCollective>; type SuspensionJudgementOrigin = pallet_society::EnsureFounder; type ChallengePeriod = ChallengePeriod; - type ModuleId = SocietyModuleId; } parameter_types! { @@ -779,7 +801,7 @@ construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsic { System: frame_system::{Module, Call, Config, Storage, Event}, - Utility: pallet_utility::{Module, Call, Storage, Event}, + Utility: pallet_utility::{Module, Call, Event}, Babe: pallet_babe::{Module, Call, Storage, Config, Inherent(Timestamp)}, Timestamp: pallet_timestamp::{Module, Call, Storage, Inherent}, Authorship: pallet_authorship::{Module, Call, Storage, Inherent}, @@ -809,6 +831,7 @@ construct_runtime!( Vesting: pallet_vesting::{Module, Call, Storage, Event, Config}, Scheduler: pallet_scheduler::{Module, Call, Storage, Event}, Proxy: pallet_proxy::{Module, Call, Storage, Event}, + Multisig: pallet_multisig::{Module, Call, Storage, Event}, } ); @@ -1058,6 +1081,7 @@ impl_runtime_apis! { add_benchmark!(params, batches, b"elections", Elections); add_benchmark!(params, batches, b"identity", Identity); add_benchmark!(params, batches, b"im-online", ImOnline); + add_benchmark!(params, batches, b"multisig", Multisig); add_benchmark!(params, batches, b"offences", OffencesBench::); add_benchmark!(params, batches, b"proxy", Proxy); add_benchmark!(params, batches, b"scheduler", Scheduler); diff --git a/frame/balances/src/lib.rs b/frame/balances/src/lib.rs index ea7ec92147e54..b6dc4a11f0391 100644 --- a/frame/balances/src/lib.rs +++ b/frame/balances/src/lib.rs @@ -598,7 +598,7 @@ impl, I: Instance> Module { /// /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that /// the caller will do this. - fn mutate_account( + pub fn mutate_account( who: &T::AccountId, f: impl FnOnce(&mut AccountData) -> R ) -> R { diff --git a/frame/multisig/Cargo.toml b/frame/multisig/Cargo.toml new file mode 100644 index 0000000000000..17e75c817e4e1 --- /dev/null +++ b/frame/multisig/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "pallet-multisig" +version = "2.0.0-rc2" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME multi-signature dispatch pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.101", optional = true } +codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false } +frame-support = { version = "2.0.0-rc2", default-features = false, path = "../support" } +frame-system = { version = "2.0.0-rc2", default-features = false, path = "../system" } +sp-core = { version = "2.0.0-rc2", default-features = false, path = "../../primitives/core" } +sp-runtime = { version = "2.0.0-rc2", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "2.0.0-rc2", default-features = false, path = "../../primitives/std" } +sp-io = { version = "2.0.0-rc2", default-features = false, path = "../../primitives/io" } + +frame-benchmarking = { version = "2.0.0-rc2", default-features = false, path = "../benchmarking", optional = true } + +[dev-dependencies] +sp-core = { version = "2.0.0-rc2", path = "../../primitives/core" } +pallet-balances = { version = "2.0.0-rc2", path = "../balances" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "sp-io/std", + "sp-std/std" +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", +] diff --git a/frame/multisig/src/benchmarking.rs b/frame/multisig/src/benchmarking.rs new file mode 100644 index 0000000000000..0c603be916805 --- /dev/null +++ b/frame/multisig/src/benchmarking.rs @@ -0,0 +1,156 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2020 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. + +// Benchmarks for Multisig Pallet + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_system::RawOrigin; +use frame_benchmarking::{benchmarks, account}; +use sp_runtime::traits::Saturating; + +use crate::Module as Utility; + +const SEED: u32 = 0; + +fn setup_multi(s: u32, z: u32) + -> Result<(Vec, Box<::Call>), &'static str> +{ + let mut signatories: Vec = Vec::new(); + for i in 0 .. s { + let signatory = account("signatory", i, SEED); + // Give them some balance for a possible deposit + let deposit = T::DepositBase::get() + T::DepositFactor::get() * s.into(); + let balance = T::Currency::minimum_balance().saturating_mul(100.into()) + deposit; + T::Currency::make_free_balance_be(&signatory, balance); + signatories.push(signatory); + } + signatories.sort(); + let call: Box<::Call> = Box::new(frame_system::Call::remark(vec![0; z as usize]).into()); + return Ok((signatories, call)) +} + +benchmarks! { + _ { } + + as_multi_create { + // Signatories, need at least 2 total people + let s in 2 .. T::MaxSignatories::get() as u32; + // Transaction Length + let z in 0 .. 10_000; + let (mut signatories, call) = setup_multi::(s, z)?; + let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; + }: as_multi(RawOrigin::Signed(caller), s as u16, signatories, None, call) + + as_multi_approve { + // Signatories, need at least 2 people + let s in 2 .. T::MaxSignatories::get() as u32; + // Transaction Length + let z in 0 .. 10_000; + let (mut signatories, call) = setup_multi::(s, z)?; + let mut signatories2 = signatories.clone(); + let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; + // before the call, get the timepoint + let timepoint = Utility::::timepoint(); + // Create the multi + Utility::::as_multi(RawOrigin::Signed(caller).into(), s as u16, signatories, None, call.clone())?; + let caller2 = signatories2.remove(0); + }: as_multi(RawOrigin::Signed(caller2), s as u16, signatories2, Some(timepoint), call) + + as_multi_complete { + // Signatories, need at least 2 people + let s in 2 .. T::MaxSignatories::get() as u32; + // Transaction Length + let z in 0 .. 10_000; + let (mut signatories, call) = setup_multi::(s, z)?; + let mut signatories2 = signatories.clone(); + let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; + // before the call, get the timepoint + let timepoint = Utility::::timepoint(); + // Create the multi + Utility::::as_multi(RawOrigin::Signed(caller).into(), s as u16, signatories, None, call.clone())?; + // Everyone except the first person approves + for i in 1 .. s - 1 { + let mut signatories_loop = signatories2.clone(); + let caller_loop = signatories_loop.remove(i as usize); + let o = RawOrigin::Signed(caller_loop).into(); + Utility::::as_multi(o, s as u16, signatories_loop, Some(timepoint), call.clone())?; + } + let caller2 = signatories2.remove(0); + }: as_multi(RawOrigin::Signed(caller2), s as u16, signatories2, Some(timepoint), call) + + approve_as_multi_create { + // Signatories, need at least 2 people + let s in 2 .. T::MaxSignatories::get() as u32; + // Transaction Length + let z in 0 .. 10_000; + let (mut signatories, call) = setup_multi::(s, z)?; + let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; + let call_hash = call.using_encoded(blake2_256); + // Create the multi + }: approve_as_multi(RawOrigin::Signed(caller), s as u16, signatories, None, call_hash) + + approve_as_multi_approve { + // Signatories, need at least 2 people + let s in 2 .. T::MaxSignatories::get() as u32; + // Transaction Length + let z in 0 .. 10_000; + let (mut signatories, call) = setup_multi::(s, z)?; + let mut signatories2 = signatories.clone(); + let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; + let call_hash = call.using_encoded(blake2_256); + // before the call, get the timepoint + let timepoint = Utility::::timepoint(); + // Create the multi + Utility::::as_multi(RawOrigin::Signed(caller).into(), s as u16, signatories, None, call.clone())?; + let caller2 = signatories2.remove(0); + }: approve_as_multi(RawOrigin::Signed(caller2), s as u16, signatories2, Some(timepoint), call_hash) + + cancel_as_multi { + // Signatories, need at least 2 people + let s in 2 .. T::MaxSignatories::get() as u32; + // Transaction Length + let z in 0 .. 10_000; + let (mut signatories, call) = setup_multi::(s, z)?; + let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; + let call_hash = call.using_encoded(blake2_256); + let timepoint = Utility::::timepoint(); + // Create the multi + let o = RawOrigin::Signed(caller.clone()).into(); + Utility::::as_multi(o, s as u16, signatories.clone(), None, call.clone())?; + }: _(RawOrigin::Signed(caller), s as u16, signatories, timepoint, call_hash) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{new_test_ext, Test}; + use frame_support::assert_ok; + + #[test] + fn test_benchmarks() { + new_test_ext().execute_with(|| { + assert_ok!(test_benchmark_as_multi_create::()); + assert_ok!(test_benchmark_as_multi_approve::()); + assert_ok!(test_benchmark_as_multi_complete::()); + assert_ok!(test_benchmark_approve_as_multi_create::()); + assert_ok!(test_benchmark_approve_as_multi_approve::()); + assert_ok!(test_benchmark_cancel_as_multi::()); + }); + } +} diff --git a/frame/multisig/src/lib.rs b/frame/multisig/src/lib.rs new file mode 100644 index 0000000000000..bde0a06de60fb --- /dev/null +++ b/frame/multisig/src/lib.rs @@ -0,0 +1,555 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2020 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. + +//! # Multisig Module +//! A module for doing multisig dispatch. +//! +//! - [`multisig::Trait`](./trait.Trait.html) +//! - [`Call`](./enum.Call.html) +//! +//! ## Overview +//! +//! This module contains functionality for multi-signature dispatch, a (potentially) stateful +//! operation, allowing multiple signed +//! origins (accounts) to coordinate and dispatch a call from a well-known origin, derivable +//! deterministically from the set of account IDs and the threshold number of accounts from the +//! set that must approve it. In the case that the threshold is just one then this is a stateless +//! operation. This is useful for multisig wallets where cryptographic threshold signatures are +//! not available or desired. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! * `as_multi` - Approve and if possible dispatch a call from a composite origin formed from a +//! number of signed origins. +//! * `approve_as_multi` - Approve a call from a composite origin. +//! * `cancel_as_multi` - Cancel a call from a composite origin. +//! +//! [`Call`]: ./enum.Call.html +//! [`Trait`]: ./trait.Trait.html + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_std::prelude::*; +use codec::{Encode, Decode}; +use sp_io::hashing::blake2_256; +use frame_support::{decl_module, decl_event, decl_error, decl_storage, Parameter, ensure, RuntimeDebug}; +use frame_support::{traits::{Get, ReservableCurrency, Currency, Filter, FilterStack, ClearFilterGuard}, + weights::{Weight, GetDispatchInfo, DispatchClass, Pays}, + dispatch::{DispatchResultWithPostInfo, DispatchErrorWithPostInfo, PostDispatchInfo}, +}; +use frame_system::{self as system, ensure_signed}; +use sp_runtime::{DispatchError, DispatchResult, traits::Dispatchable}; + +mod tests; +mod benchmarking; + +type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; + +/// Configuration trait. +pub trait Trait: frame_system::Trait { + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// The overarching call type. + type Call: Parameter + Dispatchable + + GetDispatchInfo + From>; + + /// The currency mechanism. + type Currency: ReservableCurrency; + + /// The base amount of currency needed to reserve for creating a multisig execution. + /// + /// This is held for an additional storage item whose value size is + /// `4 + sizeof((BlockNumber, Balance, AccountId))` bytes. + type DepositBase: Get>; + + /// The amount of currency needed per unit threshold when creating a multisig execution. + /// + /// This is held for adding 32 bytes more into a pre-existing storage value. + type DepositFactor: Get>; + + /// The maximum amount of signatories allowed in the multisig. + type MaxSignatories: Get; + + /// Is a given call compatible with the proxying subsystem? + type IsCallable: FilterStack<::Call>; +} + +/// A global extrinsic index, formed as the extrinsic index within a block, together with that +/// block's height. This allows a transaction in which a multisig operation of a particular +/// composite was created to be uniquely identified. +#[derive(Copy, Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)] +pub struct Timepoint { + /// The height of the chain at the point in time. + height: BlockNumber, + /// The index of the extrinsic at the point in time. + index: u32, +} + +/// An open multisig operation. +#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)] +pub struct Multisig { + /// The extrinsic when the multisig operation was opened. + when: Timepoint, + /// The amount held in reserve of the `depositor`, to be returned once the operation ends. + deposit: Balance, + /// The account who opened it (i.e. the first to approve it). + depositor: AccountId, + /// The approvals achieved so far, including the depositor. Always sorted. + approvals: Vec, +} + +decl_storage! { + trait Store for Module as Multisig { + /// The set of open multisig operations. + pub Multisigs: double_map + hasher(twox_64_concat) T::AccountId, hasher(blake2_128_concat) [u8; 32] + => Option, T::AccountId>>; + } +} + +decl_error! { + pub enum Error for Module { + /// Threshold is too low (zero). + ZeroThreshold, + /// Call is already approved by this signatory. + AlreadyApproved, + /// Call doesn't need any (more) approvals. + NoApprovalsNeeded, + /// There are too few signatories in the list. + TooFewSignatories, + /// There are too many signatories in the list. + TooManySignatories, + /// The signatories were provided out of order; they should be ordered. + SignatoriesOutOfOrder, + /// The sender was contained in the other signatories; it shouldn't be. + SenderInSignatories, + /// Multisig operation not found when attempting to cancel. + NotFound, + /// Only the account that originally created the multisig is able to cancel it. + NotOwner, + /// No timepoint was given, yet the multisig operation is already underway. + NoTimepoint, + /// A different timepoint was given to the multisig operation that is underway. + WrongTimepoint, + /// A timepoint was given, yet no multisig operation is underway. + UnexpectedTimepoint, + /// A call with a `false` `IsCallable` filter was attempted. + Uncallable, + } +} + +decl_event! { + /// Events type. + pub enum Event where + AccountId = ::AccountId, + BlockNumber = ::BlockNumber, + CallHash = [u8; 32] + { + /// A new multisig operation has begun. First param is the account that is approving, + /// second is the multisig account, third is hash of the call. + NewMultisig(AccountId, AccountId, CallHash), + /// A multisig operation has been approved by someone. First param is the account that is + /// approving, third is the multisig account, fourth is hash of the call. + MultisigApproval(AccountId, Timepoint, AccountId, CallHash), + /// A multisig operation has been executed. First param is the account that is + /// approving, third is the multisig account, fourth is hash of the call to be executed. + MultisigExecuted(AccountId, Timepoint, AccountId, CallHash, DispatchResult), + /// A multisig operation has been cancelled. First param is the account that is + /// cancelling, third is the multisig account, fourth is hash of the call. + MultisigCancelled(AccountId, Timepoint, AccountId, CallHash), + /// A call with a `false` IsCallable filter was attempted. + Uncallable(u32), + } +} + +mod weight_of { + use super::*; + + /// - Base Weight: + /// - Create: 46.55 + 0.089 * S µs + /// - Approve: 34.03 + .112 * S µs + /// - Complete: 40.36 + .225 * S µs + /// - DB Weight: + /// - Reads: Multisig Storage, [Caller Account] + /// - Writes: Multisig Storage, [Caller Account] + /// - Plus Call Weight + pub fn as_multi(other_sig_len: usize, call_weight: Weight) -> Weight { + call_weight + .saturating_add(45_000_000) + .saturating_add((other_sig_len as Weight).saturating_mul(250_000)) + .saturating_add(T::DbWeight::get().reads_writes(1, 1)) + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + type Error = Error; + + /// Deposit one of this module's events by using the default implementation. + fn deposit_event() = default; + + fn on_runtime_upgrade() -> Weight { + // Utility.Multisigs -> Multisig.Multisigs + use frame_support::migration::{StorageIterator, put_storage_value}; + for (key, value) in StorageIterator::< + Multisig, T::AccountId> + >::new(b"Utility", b"Multisigs").drain() { + put_storage_value(b"Multisig", b"Multisigs", &key, value); + } + 1_000_000_000 + } + + /// Register approval for a dispatch to be made from a deterministic composite account if + /// approved by a total of `threshold - 1` of `other_signatories`. + /// + /// If there are enough, then dispatch the call. Calls must each fulfil the `IsCallable` + /// filter. + /// + /// Payment: `DepositBase` will be reserved if this is the first approval, plus + /// `threshold` times `DepositFactor`. It is returned once this dispatch happens or + /// is cancelled. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `threshold`: The total number of approvals for this dispatch before it is executed. + /// - `other_signatories`: The accounts (other than the sender) who can approve this + /// dispatch. May not be empty. + /// - `maybe_timepoint`: If this is the first approval, then this must be `None`. If it is + /// not the first approval, then it must be `Some`, with the timepoint (block number and + /// transaction index) of the first approval transaction. + /// - `call`: The call to be executed. + /// + /// NOTE: Unless this is the final approval, you will generally want to use + /// `approve_as_multi` instead, since it only requires a hash of the call. + /// + /// Result is equivalent to the dispatched result if `threshold` is exactly `1`. Otherwise + /// on success, result is `Ok` and the result from the interior call, if it was executed, + /// may be found in the deposited `MultisigExecuted` event. + /// + /// # + /// - `O(S + Z + Call)`. + /// - Up to one balance-reserve or unreserve operation. + /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of + /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. + /// - One call encode & hash, both of complexity `O(Z)` where `Z` is tx-len. + /// - One encode & hash, both of complexity `O(S)`. + /// - Up to one binary search and insert (`O(logS + S)`). + /// - I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. + /// - One event. + /// - The weight of the `call`. + /// - Storage: inserts one item, value size bounded by `MaxSignatories`, with a + /// deposit taken for its lifetime of + /// `DepositBase + threshold * DepositFactor`. + /// ------------------------------- + /// - Base Weight: + /// - Create: 46.55 + 0.089 * S µs + /// - Approve: 34.03 + .112 * S µs + /// - Complete: 40.36 + .225 * S µs + /// - DB Weight: + /// - Reads: Multisig Storage, [Caller Account] + /// - Writes: Multisig Storage, [Caller Account] + /// - Plus Call Weight + /// # + #[weight = ( + weight_of::as_multi::(other_signatories.len(), call.get_dispatch_info().weight), + call.get_dispatch_info().class, + Pays::Yes, + )] + fn as_multi(origin, + threshold: u16, + other_signatories: Vec, + maybe_timepoint: Option>, + call: Box<::Call>, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // We're now executing as a freshly authenticated new account, so the previous call + // restrictions no longer apply. + let _guard = ClearFilterGuard::::Call>::new(); + ensure!(T::IsCallable::filter(call.as_ref()), Error::::Uncallable); + ensure!(threshold >= 1, Error::::ZeroThreshold); + let max_sigs = T::MaxSignatories::get() as usize; + ensure!(!other_signatories.is_empty(), Error::::TooFewSignatories); + let other_signatories_len = other_signatories.len(); + ensure!(other_signatories_len < max_sigs, Error::::TooManySignatories); + let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?; + + let id = Self::multi_account_id(&signatories, threshold); + let call_hash = call.using_encoded(blake2_256); + + if let Some(mut m) = >::get(&id, call_hash) { + let timepoint = maybe_timepoint.ok_or(Error::::NoTimepoint)?; + ensure!(m.when == timepoint, Error::::WrongTimepoint); + if let Err(pos) = m.approvals.binary_search(&who) { + // we know threshold is greater than zero from the above ensure. + if (m.approvals.len() as u16) < threshold - 1 { + m.approvals.insert(pos, who.clone()); + >::insert(&id, call_hash, m); + Self::deposit_event(RawEvent::MultisigApproval(who, timepoint, id, call_hash)); + // Call is not made, so the actual weight does not include call + return Ok(Some(weight_of::as_multi::(other_signatories_len, 0)).into()) + } + } else { + if (m.approvals.len() as u16) < threshold { + Err(Error::::AlreadyApproved)? + } + } + + let result = call.dispatch(frame_system::RawOrigin::Signed(id.clone()).into()); + let _ = T::Currency::unreserve(&m.depositor, m.deposit); + >::remove(&id, call_hash); + Self::deposit_event(RawEvent::MultisigExecuted( + who, timepoint, id, call_hash, result.map(|_| ()).map_err(|e| e.error) + )); + return Ok(None.into()) + } else { + ensure!(maybe_timepoint.is_none(), Error::::UnexpectedTimepoint); + if threshold > 1 { + let deposit = T::DepositBase::get() + + T::DepositFactor::get() * threshold.into(); + T::Currency::reserve(&who, deposit)?; + >::insert(&id, call_hash, Multisig { + when: Self::timepoint(), + deposit, + depositor: who.clone(), + approvals: vec![who.clone()], + }); + Self::deposit_event(RawEvent::NewMultisig(who, id, call_hash)); + // Call is not made, so we can return that weight + return Ok(Some(weight_of::as_multi::(other_signatories_len, 0)).into()) + } else { + let result = call.dispatch(frame_system::RawOrigin::Signed(id).into()); + match result { + Ok(post_dispatch_info) => { + match post_dispatch_info.actual_weight { + Some(actual_weight) => return Ok(Some(weight_of::as_multi::(other_signatories_len, actual_weight)).into()), + None => return Ok(None.into()), + } + }, + Err(err) => { + match err.post_info.actual_weight { + Some(actual_weight) => { + let weight_used = weight_of::as_multi::(other_signatories_len, actual_weight); + return Err(DispatchErrorWithPostInfo { post_info: Some(weight_used).into(), error: err.error.into() }) + }, + None => { + return Err(err) + } + } + } + } + } + } + } + + /// Register approval for a dispatch to be made from a deterministic composite account if + /// approved by a total of `threshold - 1` of `other_signatories`. + /// + /// Payment: `DepositBase` will be reserved if this is the first approval, plus + /// `threshold` times `DepositFactor`. It is returned once this dispatch happens or + /// is cancelled. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `threshold`: The total number of approvals for this dispatch before it is executed. + /// - `other_signatories`: The accounts (other than the sender) who can approve this + /// dispatch. May not be empty. + /// - `maybe_timepoint`: If this is the first approval, then this must be `None`. If it is + /// not the first approval, then it must be `Some`, with the timepoint (block number and + /// transaction index) of the first approval transaction. + /// - `call_hash`: The hash of the call to be executed. + /// + /// NOTE: If this is the final approval, you will want to use `as_multi` instead. + /// + /// # + /// - `O(S)`. + /// - Up to one balance-reserve or unreserve operation. + /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of + /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. + /// - One encode & hash, both of complexity `O(S)`. + /// - Up to one binary search and insert (`O(logS + S)`). + /// - I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. + /// - One event. + /// - Storage: inserts one item, value size bounded by `MaxSignatories`, with a + /// deposit taken for its lifetime of + /// `DepositBase + threshold * DepositFactor`. + /// ---------------------------------- + /// - Base Weight: + /// - Create: 44.71 + 0.088 * S + /// - Approve: 31.48 + 0.116 * S + /// - DB Weight: + /// - Read: Multisig Storage, [Caller Account] + /// - Write: Multisig Storage, [Caller Account] + /// # + #[weight = ( + T::DbWeight::get().reads_writes(1, 1) + .saturating_add(45_000_000) + .saturating_add((other_signatories.len() as Weight).saturating_mul(120_000)), + DispatchClass::Normal, + Pays::Yes, + )] + fn approve_as_multi(origin, + threshold: u16, + other_signatories: Vec, + maybe_timepoint: Option>, + call_hash: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(threshold >= 1, Error::::ZeroThreshold); + let max_sigs = T::MaxSignatories::get() as usize; + ensure!(!other_signatories.is_empty(), Error::::TooFewSignatories); + ensure!(other_signatories.len() < max_sigs, Error::::TooManySignatories); + let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?; + + let id = Self::multi_account_id(&signatories, threshold); + + if let Some(mut m) = >::get(&id, call_hash) { + let timepoint = maybe_timepoint.ok_or(Error::::NoTimepoint)?; + ensure!(m.when == timepoint, Error::::WrongTimepoint); + ensure!(m.approvals.len() < threshold as usize, Error::::NoApprovalsNeeded); + if let Err(pos) = m.approvals.binary_search(&who) { + m.approvals.insert(pos, who.clone()); + >::insert(&id, call_hash, m); + Self::deposit_event(RawEvent::MultisigApproval(who, timepoint, id, call_hash)); + } else { + Err(Error::::AlreadyApproved)? + } + } else { + if threshold > 1 { + ensure!(maybe_timepoint.is_none(), Error::::UnexpectedTimepoint); + let deposit = T::DepositBase::get() + + T::DepositFactor::get() * threshold.into(); + T::Currency::reserve(&who, deposit)?; + >::insert(&id, call_hash, Multisig { + when: Self::timepoint(), + deposit, + depositor: who.clone(), + approvals: vec![who.clone()], + }); + Self::deposit_event(RawEvent::NewMultisig(who, id, call_hash)); + } else { + Err(Error::::NoApprovalsNeeded)? + } + } + Ok(()) + } + + /// Cancel a pre-existing, on-going multisig transaction. Any deposit reserved previously + /// for this operation will be unreserved on success. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `threshold`: The total number of approvals for this dispatch before it is executed. + /// - `other_signatories`: The accounts (other than the sender) who can approve this + /// dispatch. May not be empty. + /// - `timepoint`: The timepoint (block number and transaction index) of the first approval + /// transaction for this dispatch. + /// - `call_hash`: The hash of the call to be executed. + /// + /// # + /// - `O(S)`. + /// - Up to one balance-reserve or unreserve operation. + /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of + /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. + /// - One encode & hash, both of complexity `O(S)`. + /// - One event. + /// - I/O: 1 read `O(S)`, one remove. + /// - Storage: removes one item. + /// ---------------------------------- + /// - Base Weight: 37.6 + 0.084 * S + /// - DB Weight: + /// - Read: Multisig Storage, [Caller Account] + /// - Write: Multisig Storage, [Caller Account] + /// # + #[weight = ( + T::DbWeight::get().reads_writes(1, 1) + .saturating_add(40_000_000) + .saturating_add((other_signatories.len() as Weight).saturating_mul(100_000)), + DispatchClass::Normal, + Pays::Yes, + )] + fn cancel_as_multi(origin, + threshold: u16, + other_signatories: Vec, + timepoint: Timepoint, + call_hash: [u8; 32], + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(threshold >= 1, Error::::ZeroThreshold); + let max_sigs = T::MaxSignatories::get() as usize; + ensure!(!other_signatories.is_empty(), Error::::TooFewSignatories); + ensure!(other_signatories.len() < max_sigs, Error::::TooManySignatories); + let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?; + + let id = Self::multi_account_id(&signatories, threshold); + + let m = >::get(&id, call_hash) + .ok_or(Error::::NotFound)?; + ensure!(m.when == timepoint, Error::::WrongTimepoint); + ensure!(m.depositor == who, Error::::NotOwner); + + let _ = T::Currency::unreserve(&m.depositor, m.deposit); + >::remove(&id, call_hash); + + Self::deposit_event(RawEvent::MultisigCancelled(who, timepoint, id, call_hash)); + Ok(()) + } + } +} + +impl Module { + /// Derive a multi-account ID from the sorted list of accounts and the threshold that are + /// required. + /// + /// NOTE: `who` must be sorted. If it is not, then you'll get the wrong answer. + pub fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId { + let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(blake2_256); + T::AccountId::decode(&mut &entropy[..]).unwrap_or_default() + } + + /// The current `Timepoint`. + pub fn timepoint() -> Timepoint { + Timepoint { + height: >::block_number(), + index: >::extrinsic_index().unwrap_or_default(), + } + } + + /// Check that signatories is sorted and doesn't contain sender, then insert sender. + fn ensure_sorted_and_insert(other_signatories: Vec, who: T::AccountId) + -> Result, DispatchError> + { + let mut signatories = other_signatories; + let mut maybe_last = None; + let mut index = 0; + for item in signatories.iter() { + if let Some(last) = maybe_last { + ensure!(last < item, Error::::SignatoriesOutOfOrder); + } + if item <= &who { + ensure!(item != &who, Error::::SenderInSignatories); + index += 1; + } + maybe_last = Some(item); + } + signatories.insert(index, who); + Ok(signatories) + } +} diff --git a/frame/multisig/src/tests.rs b/frame/multisig/src/tests.rs new file mode 100644 index 0000000000000..77855c648217d --- /dev/null +++ b/frame/multisig/src/tests.rs @@ -0,0 +1,408 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2020 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 Multisig Pallet + +#![cfg(test)] + +use super::*; + +use frame_support::{ + assert_ok, assert_noop, impl_outer_origin, parameter_types, impl_outer_dispatch, + weights::Weight, impl_outer_event +}; +use sp_core::H256; +use sp_runtime::{Perbill, traits::{BlakeTwo256, IdentityLookup}, testing::Header}; +use crate as multisig; + +impl_outer_origin! { + pub enum Origin for Test where system = frame_system {} +} + +impl_outer_event! { + pub enum TestEvent for Test { + system, + pallet_balances, + multisig, + } +} +impl_outer_dispatch! { + pub enum Call for Test where origin: Origin { + frame_system::System, + pallet_balances::Balances, + multisig::Multisig, + } +} + +// For testing the pallet, we construct most of a mock runtime. This means +// first constructing a configuration type (`Test`) which `impl`s each of the +// configuration traits of pallets we want to use. +#[derive(Clone, Eq, PartialEq)] +pub struct Test; +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} +impl frame_system::Trait for Test { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Call = Call; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); +} +parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} +impl pallet_balances::Trait for Test { + type Balance = u64; + type Event = TestEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; +} +parameter_types! { + pub const DepositBase: u64 = 1; + pub const DepositFactor: u64 = 1; + pub const MaxSignatories: u16 = 3; +} +pub struct TestIsCallable; +impl Filter for TestIsCallable { + fn filter(c: &Call) -> bool { + match *c { + Call::Balances(_) => true, + _ => false, + } + } +} +impl FilterStack for TestIsCallable { + type Stack = (); + fn push(_: impl Fn(&Call) -> bool + 'static) {} + fn pop() {} + fn take() -> Self::Stack { () } + fn restore(_: Self::Stack) {} +} +impl Trait for Test { + type Event = TestEvent; + type Call = Call; + type Currency = Balances; + type DepositBase = DepositBase; + type DepositFactor = DepositFactor; + type MaxSignatories = MaxSignatories; + type IsCallable = TestIsCallable; +} +type System = frame_system::Module; +type Balances = pallet_balances::Module; +type Multisig = Module; + +use pallet_balances::Call as BalancesCall; +use pallet_balances::Error as BalancesError; + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10), (2, 10), (3, 10), (4, 10), (5, 2)], + }.assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn last_event() -> TestEvent { + system::Module::::events().pop().map(|e| e.event).expect("Event expected") +} + +fn expect_event>(e: E) { + assert_eq!(last_event(), e.into()); +} + +fn now() -> Timepoint { + Multisig::timepoint() +} + +#[test] +fn multisig_deposit_is_taken_and_returned() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_ok!(Multisig::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_eq!(Balances::free_balance(1), 2); + assert_eq!(Balances::reserved_balance(1), 3); + + assert_ok!(Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn cancel_multisig_returns_deposit() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Multisig::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); + assert_ok!(Multisig::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); + assert_eq!(Balances::free_balance(1), 6); + assert_eq!(Balances::reserved_balance(1), 4); + assert_ok!( + Multisig::cancel_as_multi(Origin::signed(1), 3, vec![2, 3], now(), hash.clone()), + ); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::reserved_balance(1), 0); + }); +} + +#[test] +fn timepoint_checking_works() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + + assert_noop!( + Multisig::approve_as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), hash.clone()), + Error::::UnexpectedTimepoint, + ); + + assert_ok!(Multisig::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash)); + + assert_noop!( + Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], None, call.clone()), + Error::::NoTimepoint, + ); + let later = Timepoint { index: 1, .. now() }; + assert_noop!( + Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], Some(later), call.clone()), + Error::::WrongTimepoint, + ); + }); +} + +#[test] +fn multisig_2_of_3_works() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Multisig::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash)); + assert_eq!(Balances::free_balance(6), 0); + + assert_ok!(Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); + assert_eq!(Balances::free_balance(6), 15); + }); +} + +#[test] +fn multisig_3_of_3_works() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 3); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Multisig::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); + assert_ok!(Multisig::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); + assert_eq!(Balances::free_balance(6), 0); + + assert_ok!(Multisig::as_multi(Origin::signed(3), 3, vec![1, 2], Some(now()), call)); + assert_eq!(Balances::free_balance(6), 15); + }); +} + +#[test] +fn cancel_multisig_works() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Multisig::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); + assert_ok!(Multisig::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); + assert_noop!( + Multisig::cancel_as_multi(Origin::signed(2), 3, vec![1, 3], now(), hash.clone()), + Error::::NotOwner, + ); + assert_ok!( + Multisig::cancel_as_multi(Origin::signed(1), 3, vec![2, 3], now(), hash.clone()), + ); + }); +} + +#[test] +fn multisig_2_of_3_as_multi_works() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_ok!(Multisig::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_eq!(Balances::free_balance(6), 0); + + assert_ok!(Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); + assert_eq!(Balances::free_balance(6), 15); + }); +} + +#[test] +fn multisig_2_of_3_as_multi_with_many_calls_works() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call1 = Box::new(Call::Balances(BalancesCall::transfer(6, 10))); + let call2 = Box::new(Call::Balances(BalancesCall::transfer(7, 5))); + + assert_ok!(Multisig::as_multi(Origin::signed(1), 2, vec![2, 3], None, call1.clone())); + assert_ok!(Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], None, call2.clone())); + assert_ok!(Multisig::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call2)); + assert_ok!(Multisig::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call1)); + + assert_eq!(Balances::free_balance(6), 10); + assert_eq!(Balances::free_balance(7), 5); + }); +} + +#[test] +fn multisig_2_of_3_cannot_reissue_same_call() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 2); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 10))); + assert_ok!(Multisig::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_ok!(Multisig::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call.clone())); + assert_eq!(Balances::free_balance(multi), 5); + + assert_ok!(Multisig::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); + assert_ok!(Multisig::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call.clone())); + + let err = DispatchError::from(BalancesError::::InsufficientBalance).stripped(); + expect_event(RawEvent::MultisigExecuted(3, now(), multi, call.using_encoded(blake2_256), Err(err))); + }); +} + +#[test] +fn zero_threshold_fails() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_noop!( + Multisig::as_multi(Origin::signed(1), 0, vec![2], None, call), + Error::::ZeroThreshold, + ); + }); +} + +#[test] +fn too_many_signatories_fails() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + assert_noop!( + Multisig::as_multi(Origin::signed(1), 2, vec![2, 3, 4], None, call.clone()), + Error::::TooManySignatories, + ); + }); +} + +#[test] +fn duplicate_approvals_are_ignored() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_ok!(Multisig::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash.clone())); + assert_noop!( + Multisig::approve_as_multi(Origin::signed(1), 2, vec![2, 3], Some(now()), hash.clone()), + Error::::AlreadyApproved, + ); + assert_ok!(Multisig::approve_as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), hash.clone())); + assert_noop!( + Multisig::approve_as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), hash.clone()), + Error::::NoApprovalsNeeded, + ); + }); +} + +#[test] +fn multisig_1_of_3_works() { + new_test_ext().execute_with(|| { + let multi = Multisig::multi_account_id(&[1, 2, 3][..], 1); + assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); + assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); + let hash = call.using_encoded(blake2_256); + assert_noop!( + Multisig::approve_as_multi(Origin::signed(1), 1, vec![2, 3], None, hash.clone()), + Error::::NoApprovalsNeeded, + ); + assert_noop!( + Multisig::as_multi(Origin::signed(4), 1, vec![2, 3], None, call.clone()), + BalancesError::::InsufficientBalance, + ); + assert_ok!(Multisig::as_multi(Origin::signed(1), 1, vec![2, 3], None, call)); + + assert_eq!(Balances::free_balance(6), 15); + }); +} + +#[test] +fn multisig_filters() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::System(frame_system::Call::remark(vec![]))); + assert_noop!( + Multisig::as_multi(Origin::signed(1), 1, vec![], None, call.clone()), + Error::::Uncallable, + ); + }); +} diff --git a/frame/proxy/Cargo.toml b/frame/proxy/Cargo.toml index ee776951fdfdc..58641aa21c622 100644 --- a/frame/proxy/Cargo.toml +++ b/frame/proxy/Cargo.toml @@ -26,6 +26,7 @@ frame-benchmarking = { version = "2.0.0-rc2", default-features = false, path = " [dev-dependencies] sp-core = { version = "2.0.0-rc2", path = "../../primitives/core" } pallet-balances = { version = "2.0.0-rc2", path = "../balances" } +pallet-utility = { version = "2.0.0-rc2", path = "../utility" } [features] default = ["std"] diff --git a/frame/proxy/src/lib.rs b/frame/proxy/src/lib.rs index 0c5b9c494c9f1..60305dfc74bed 100644 --- a/frame/proxy/src/lib.rs +++ b/frame/proxy/src/lib.rs @@ -40,9 +40,10 @@ use sp_io::hashing::blake2_256; use sp_runtime::{DispatchResult, traits::{Dispatchable, Zero}}; use sp_runtime::traits::Member; use frame_support::{ - decl_module, decl_event, decl_error, decl_storage, Parameter, ensure, - traits::{Get, ReservableCurrency, Currency, Filter, InstanceFilter}, - weights::{GetDispatchInfo, constants::{WEIGHT_PER_MICROS, WEIGHT_PER_NANOS}}, + decl_module, decl_event, decl_error, decl_storage, Parameter, ensure, traits::{ + Get, ReservableCurrency, Currency, Filter, FilterStack, FilterStackGuard, + ClearFilterGuard, InstanceFilter + }, weights::{GetDispatchInfo, constants::{WEIGHT_PER_MICROS, WEIGHT_PER_NANOS}}, dispatch::{PostDispatchInfo, IsSubType}, }; use frame_system::{self as system, ensure_signed}; @@ -65,7 +66,7 @@ pub trait Trait: frame_system::Trait { type Currency: ReservableCurrency; /// Is a given call compatible with the proxying subsystem? - type IsCallable: Filter<::Call>; + type IsCallable: FilterStack<::Call>; /// A kind of proxy; specified with the proxy and passed in to the `IsProxyable` fitler. /// The instance filter determines whether a given call may be proxied under this type. @@ -166,16 +167,22 @@ decl_module! { call: Box<::Call> ) { let who = ensure_signed(origin)?; - ensure!(T::IsCallable::filter(&call), Error::::Uncallable); let (_, proxy_type) = Proxies::::get(&real).0.into_iter() .find(|x| &x.0 == &who && force_proxy_type.as_ref().map_or(true, |y| &x.1 == y)) .ok_or(Error::::NotProxy)?; - match call.is_sub_type() { - Some(Call::add_proxy(_, ref pt)) | Some(Call::remove_proxy(_, ref pt)) => - ensure!(pt.is_no_more_permissive(&proxy_type), Error::::NoPermission), - _ => (), - } - ensure!(proxy_type.filter(&call), Error::::Unproxyable); + + // We're now executing as a freshly authenticated new account, so the previous call + // restrictions no longer apply. + let _clear_guard = ClearFilterGuard::::Call>::new(); + let _filter_guard = FilterStackGuard::::Call>::new( + move |c| match c.is_sub_type() { + Some(Call::add_proxy(_, ref pt)) | Some(Call::remove_proxy(_, ref pt)) + if !proxy_type.is_superset(&pt) => false, + _ => proxy_type.filter(&c) + } + ); + ensure!(T::IsCallable::filter(&call), Error::::Uncallable); + let e = call.dispatch(frame_system::RawOrigin::Signed(real).into()); Self::deposit_event(RawEvent::ProxyExecuted(e.map(|_| ()).map_err(|e| e.error))); } diff --git a/frame/proxy/src/tests.rs b/frame/proxy/src/tests.rs index c331195c26287..93529317f6707 100644 --- a/frame/proxy/src/tests.rs +++ b/frame/proxy/src/tests.rs @@ -23,7 +23,7 @@ use super::*; use frame_support::{ assert_ok, assert_noop, impl_outer_origin, parameter_types, impl_outer_dispatch, - weights::Weight, impl_outer_event, RuntimeDebug, dispatch::DispatchError + impl_filter_stack, weights::Weight, impl_outer_event, RuntimeDebug, dispatch::DispatchError }; use codec::{Encode, Decode}; use sp_core::H256; @@ -38,6 +38,7 @@ impl_outer_event! { system, pallet_balances, proxy, + pallet_utility, } } impl_outer_dispatch! { @@ -45,6 +46,7 @@ impl_outer_dispatch! { frame_system::System, pallet_balances::Balances, proxy::Proxy, + pallet_utility::Utility, } } @@ -94,30 +96,39 @@ impl pallet_balances::Trait for Test { type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; } +impl pallet_utility::Trait for Test { + type Event = TestEvent; + type Call = Call; + type IsCallable = IsCallable; +} parameter_types! { pub const ProxyDepositBase: u64 = 1; pub const ProxyDepositFactor: u64 = 1; - pub const MaxProxies: u16 = 3; + pub const MaxProxies: u16 = 4; } +pub struct IsCallable; +impl_filter_stack!(crate::tests::IsCallable, crate::tests::BaseFilter, crate::tests::Call, is_callable); #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, RuntimeDebug)] pub enum ProxyType { Any, JustTransfer, + JustUtility, } impl Default for ProxyType { fn default() -> Self { Self::Any } } impl InstanceFilter for ProxyType { fn filter(&self, c: &Call) -> bool { match self { ProxyType::Any => true, - ProxyType::JustTransfer => match c { - Call::Balances(pallet_balances::Call::transfer(..)) => true, - _ => false, - } + ProxyType::JustTransfer => matches!(c, Call::Balances(pallet_balances::Call::transfer(..))), + ProxyType::JustUtility => matches!(c, Call::Utility(..)), } } + fn is_superset(&self, o: &Self) -> bool { + self == &ProxyType::Any || self == o + } } -pub struct TestIsCallable; -impl Filter for TestIsCallable { +pub struct BaseFilter; +impl Filter for BaseFilter { fn filter(c: &Call) -> bool { match *c { // Remark is used as a no-op call in the benchmarking @@ -131,7 +142,7 @@ impl Trait for Test { type Event = TestEvent; type Call = Call; type Currency = Balances; - type IsCallable = TestIsCallable; + type IsCallable = IsCallable; type ProxyType = ProxyType; type ProxyDepositBase = ProxyDepositBase; type ProxyDepositFactor = ProxyDepositFactor; @@ -140,11 +151,15 @@ impl Trait for Test { type System = frame_system::Module; type Balances = pallet_balances::Module; +type Utility = pallet_utility::Module; type Proxy = Module; use frame_system::Call as SystemCall; use pallet_balances::Call as BalancesCall; use pallet_balances::Error as BalancesError; +use pallet_utility::Call as UtilityCall; +use pallet_utility::Error as UtilityError; +use pallet_utility::Event as UtilityEvent; use super::Call as ProxyCall; pub fn new_test_ext() -> sp_io::TestExternalities { @@ -165,6 +180,65 @@ fn expect_event>(e: E) { assert_eq!(last_event(), e.into()); } +fn last_events(n: usize) -> Vec { + system::Module::::events().into_iter().rev().take(n).rev().map(|e| e.event).collect() +} + +fn expect_events(e: Vec) { + assert_eq!(last_events(e.len()), e); +} + +#[test] +fn filtering_works() { + new_test_ext().execute_with(|| { + Balances::mutate_account(&1, |a| a.free = 1000); + assert_ok!(Proxy::add_proxy(Origin::signed(1), 2, ProxyType::Any)); + assert_ok!(Proxy::add_proxy(Origin::signed(1), 3, ProxyType::JustTransfer)); + assert_ok!(Proxy::add_proxy(Origin::signed(1), 4, ProxyType::JustUtility)); + + let call = Box::new(Call::Balances(BalancesCall::transfer(6, 1))); + assert_ok!(Proxy::proxy(Origin::signed(2), 1, None, call.clone())); + expect_event(RawEvent::ProxyExecuted(Ok(()))); + assert_ok!(Proxy::proxy(Origin::signed(3), 1, None, call.clone())); + expect_event(RawEvent::ProxyExecuted(Ok(()))); + assert_noop!(Proxy::proxy(Origin::signed(4), 1, None, call.clone()), Error::::Uncallable); + + let sub_id = Utility::sub_account_id(1, 0); + Balances::mutate_account(&sub_id, |a| a.free = 1000); + let inner = Box::new(Call::Balances(BalancesCall::transfer(6, 1))); + + let call = Box::new(Call::Utility(UtilityCall::as_sub(0, inner.clone()))); + assert_ok!(Proxy::proxy(Origin::signed(2), 1, None, call.clone())); + expect_event(RawEvent::ProxyExecuted(Ok(()))); + assert_noop!(Proxy::proxy(Origin::signed(3), 1, None, call.clone()), Error::::Uncallable); + assert_ok!(Proxy::proxy(Origin::signed(4), 1, None, call.clone())); + expect_event(RawEvent::ProxyExecuted(Ok(()))); + + let call = Box::new(Call::Utility(UtilityCall::as_limited_sub(0, inner.clone()))); + assert_ok!(Proxy::proxy(Origin::signed(2), 1, None, call.clone())); + expect_event(RawEvent::ProxyExecuted(Ok(()))); + assert_noop!(Proxy::proxy(Origin::signed(3), 1, None, call.clone()), Error::::Uncallable); + assert_ok!(Proxy::proxy(Origin::signed(4), 1, None, call.clone())); + let de = DispatchError::from(UtilityError::::Uncallable).stripped(); + expect_event(RawEvent::ProxyExecuted(Err(de))); + + let call = Box::new(Call::Utility(UtilityCall::batch(vec![*inner]))); + assert_ok!(Proxy::proxy(Origin::signed(2), 1, None, call.clone())); + expect_events(vec![UtilityEvent::BatchCompleted.into(), RawEvent::ProxyExecuted(Ok(())).into()]); + assert_noop!(Proxy::proxy(Origin::signed(3), 1, None, call.clone()), Error::::Uncallable); + assert_ok!(Proxy::proxy(Origin::signed(4), 1, None, call.clone())); + expect_events(vec![UtilityEvent::Uncallable(0).into(), RawEvent::ProxyExecuted(Ok(())).into()]); + + let inner = Box::new(Call::Proxy(ProxyCall::add_proxy(5, ProxyType::Any))); + let call = Box::new(Call::Utility(UtilityCall::batch(vec![*inner]))); + assert_ok!(Proxy::proxy(Origin::signed(2), 1, None, call.clone())); + expect_events(vec![UtilityEvent::BatchCompleted.into(), RawEvent::ProxyExecuted(Ok(())).into()]); + assert_noop!(Proxy::proxy(Origin::signed(3), 1, None, call.clone()), Error::::Uncallable); + assert_ok!(Proxy::proxy(Origin::signed(4), 1, None, call.clone())); + expect_events(vec![UtilityEvent::Uncallable(0).into(), RawEvent::ProxyExecuted(Ok(())).into()]); + }); +} + #[test] fn add_remove_proxies_works() { new_test_ext().execute_with(|| { @@ -175,8 +249,12 @@ fn add_remove_proxies_works() { assert_eq!(Balances::reserved_balance(1), 3); assert_ok!(Proxy::add_proxy(Origin::signed(1), 3, ProxyType::Any)); assert_eq!(Balances::reserved_balance(1), 4); + assert_ok!(Proxy::add_proxy(Origin::signed(1), 4, ProxyType::JustUtility)); + assert_eq!(Balances::reserved_balance(1), 5); assert_noop!(Proxy::add_proxy(Origin::signed(1), 4, ProxyType::Any), Error::::TooMany); assert_noop!(Proxy::remove_proxy(Origin::signed(1), 3, ProxyType::JustTransfer), Error::::NotFound); + assert_ok!(Proxy::remove_proxy(Origin::signed(1), 4, ProxyType::JustUtility)); + assert_eq!(Balances::reserved_balance(1), 4); assert_ok!(Proxy::remove_proxy(Origin::signed(1), 3, ProxyType::Any)); assert_eq!(Balances::reserved_balance(1), 3); assert_ok!(Proxy::remove_proxy(Origin::signed(1), 2, ProxyType::Any)); @@ -218,7 +296,7 @@ fn proxying_works() { assert_noop!(Proxy::proxy(Origin::signed(3), 1, None, call.clone()), Error::::Uncallable); let call = Box::new(Call::Balances(BalancesCall::transfer_keep_alive(6, 1))); - assert_noop!(Proxy::proxy(Origin::signed(2), 1, None, call.clone()), Error::::Unproxyable); + assert_noop!(Proxy::proxy(Origin::signed(2), 1, None, call.clone()), Error::::Uncallable); assert_ok!(Proxy::proxy(Origin::signed(3), 1, None, call.clone())); expect_event(RawEvent::ProxyExecuted(Ok(()))); assert_eq!(Balances::free_balance(6), 2); diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 979f021e03dba..519164027b721 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -33,6 +33,10 @@ use crate::storage::StorageMap; use crate::weights::Weight; use impl_trait_for_tuples::impl_for_tuples; +/// Re-expected for the macro. +#[doc(hidden)] +pub use sp_std::{mem::{swap, take}, cell::RefCell, vec::Vec, boxed::Box}; + /// Simple trait for providing a filter over a reference to some type. pub trait Filter { /// Determine if a given value should be allowed through the filter (returns `true`) or not. @@ -43,29 +47,241 @@ impl Filter for () { fn filter(_: &T) -> bool { true } } +/// Trait to add a constraint onto the filter. +pub trait FilterStack: Filter { + /// The type used to archive the stack. + type Stack; + + /// Add a new `constraint` onto the filter. + fn push(constraint: impl Fn(&T) -> bool + 'static); + + /// Removes the most recently pushed, and not-yet-popped, constraint from the filter. + fn pop(); + + /// Clear the filter, returning a value that may be used later to `restore` it. + fn take() -> Self::Stack; + + /// Restore the filter from a previous `take` operation. + fn restore(taken: Self::Stack); +} + +/// Guard type for pushing a constraint to a `FilterStack` and popping when dropped. +pub struct FilterStackGuard, T>(PhantomData<(F, T)>); + +/// Guard type for clearing all pushed constraints from a `FilterStack` and reinstating them when +/// dropped. +pub struct ClearFilterGuard, T>(Option, PhantomData); + +impl, T> FilterStackGuard { + /// Create a new instance, adding a new `constraint` onto the filter `T`, and popping it when + /// this instance is dropped. + pub fn new(constraint: impl Fn(&T) -> bool + 'static) -> Self { + F::push(constraint); + Self(PhantomData) + } +} + +impl, T> Drop for FilterStackGuard { + fn drop(&mut self) { + F::pop(); + } +} + +impl, T> ClearFilterGuard { + /// Create a new instance, adding a new `constraint` onto the filter `T`, and popping it when + /// this instance is dropped. + pub fn new() -> Self { + Self(Some(F::take()), PhantomData) + } +} + +impl, T> Drop for ClearFilterGuard { + fn drop(&mut self) { + if let Some(taken) = self.0.take() { + F::restore(taken); + } + } +} + /// Simple trait for providing a filter over a reference to some type, given an instance of itself. -pub trait InstanceFilter { +pub trait InstanceFilter: Sized + Send + Sync { /// Determine if a given value should be allowed through the filter (returns `true`) or not. fn filter(&self, _: &T) -> bool; - /// Determines whether `self` matches at least all items that `o` does. - fn is_no_less_permissive(&self, o: &Self) -> bool { !self.is_less_permissive(o) } + /// Determines whether `self` matches at least everything that `_o` does. + fn is_superset(&self, _o: &Self) -> bool { false } +} - /// Determines whether `self` matches at most only the items that `o` does. - fn is_no_more_permissive(&self, o: &Self) -> bool { !o.is_less_permissive(&self) } +impl InstanceFilter for () { + fn filter(&self, _: &T) -> bool { true } + fn is_superset(&self, _o: &Self) -> bool { true } +} - /// Determines whether `self` matches all the items that `o` does and others. - fn is_more_permissive(&self, o: &Self) -> bool { o.is_less_permissive(self) } +#[macro_export] +macro_rules! impl_filter_stack { + ($target:ty, $base:ty, $call:ty, $module:ident) => { + #[cfg(feature = "std")] + mod $module { + #[allow(unused_imports)] + use super::*; + use $crate::traits::{swap, take, RefCell, Vec, Box, Filter, FilterStack}; - /// Determines whether `self` does not match all the items that `_o` does, nor any others. - /// - /// NOTE: This is the only `*permissive` function that needs to be reimplemented. - fn is_less_permissive(&self, _o: &Self) -> bool { true } + thread_local! { + static FILTER: RefCell bool + 'static>>> = RefCell::new(Vec::new()); + } + + impl Filter<$call> for $target { + fn filter(call: &$call) -> bool { + <$base>::filter(call) && + FILTER.with(|filter| filter.borrow().iter().all(|f| f(call))) + } + } + + impl FilterStack<$call> for $target { + type Stack = Vec bool + 'static>>; + fn push(f: impl Fn(&$call) -> bool + 'static) { + FILTER.with(|filter| filter.borrow_mut().push(Box::new(f))); + } + fn pop() { + FILTER.with(|filter| filter.borrow_mut().pop()); + } + fn take() -> Self::Stack { + FILTER.with(|filter| take(filter.borrow_mut().as_mut())) + } + fn restore(mut s: Self::Stack) { + FILTER.with(|filter| swap(filter.borrow_mut().as_mut(), &mut s)); + } + } + } + + #[cfg(not(feature = "std"))] + mod $module { + #[allow(unused_imports)] + use super::*; + use $crate::traits::{swap, take, RefCell, Vec, Box, Filter, FilterStack}; + + struct ThisFilter(RefCell bool + 'static>>>); + // NOTE: Safe only in wasm (guarded above) because there's only one thread. + unsafe impl Send for ThisFilter {} + unsafe impl Sync for ThisFilter {} + + static FILTER: ThisFilter = ThisFilter(RefCell::new(Vec::new())); + + impl Filter<$call> for $target { + fn filter(call: &$call) -> bool { + <$base>::filter(call) && FILTER.0.borrow().iter().all(|f| f(call)) + } + } + + impl FilterStack<$call> for $target { + type Stack = Vec bool + 'static>>; + fn push(f: impl Fn(&$call) -> bool + 'static) { + FILTER.0.borrow_mut().push(Box::new(f)); + } + fn pop() { + FILTER.0.borrow_mut().pop(); + } + fn take() -> Self::Stack { + take(FILTER.0.borrow_mut().as_mut()) + } + fn restore(mut s: Self::Stack) { + swap(FILTER.0.borrow_mut().as_mut(), &mut s); + } + } + } + } } -impl InstanceFilter for () { - fn filter(&self, _: &T) -> bool { true } - fn is_less_permissive(&self, _o: &Self) -> bool { false } +#[cfg(test)] +mod test_impl_filter_stack { + use super::*; + + pub struct IsCallable; + pub struct BaseFilter; + impl Filter for BaseFilter { + fn filter(x: &u32) -> bool { x % 2 == 0 } + } + impl_filter_stack!( + crate::traits::test_impl_filter_stack::IsCallable, + crate::traits::test_impl_filter_stack::BaseFilter, + u32, + is_callable + ); + + #[test] + fn impl_filter_stack_should_work() { + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(IsCallable::filter(&42)); + assert!(!IsCallable::filter(&43)); + + IsCallable::push(|x| *x < 42); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(!IsCallable::filter(&42)); + + IsCallable::push(|x| *x % 3 == 0); + assert!(IsCallable::filter(&36)); + assert!(!IsCallable::filter(&40)); + + IsCallable::pop(); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(!IsCallable::filter(&42)); + + let saved = IsCallable::take(); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(IsCallable::filter(&42)); + assert!(!IsCallable::filter(&43)); + + IsCallable::restore(saved); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(!IsCallable::filter(&42)); + + IsCallable::pop(); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(IsCallable::filter(&42)); + assert!(!IsCallable::filter(&43)); + } + + #[test] + fn guards_should_work() { + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(IsCallable::filter(&42)); + assert!(!IsCallable::filter(&43)); + { + let _guard_1 = FilterStackGuard::::new(|x| *x < 42); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(!IsCallable::filter(&42)); + { + let _guard_2 = FilterStackGuard::::new(|x| *x % 3 == 0); + assert!(IsCallable::filter(&36)); + assert!(!IsCallable::filter(&40)); + } + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(!IsCallable::filter(&42)); + { + let _guard_2 = ClearFilterGuard::::new(); + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(IsCallable::filter(&42)); + assert!(!IsCallable::filter(&43)); + } + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(!IsCallable::filter(&42)); + } + assert!(IsCallable::filter(&36)); + assert!(IsCallable::filter(&40)); + assert!(IsCallable::filter(&42)); + assert!(!IsCallable::filter(&43)); + } } /// An abstraction of a value stored within storage, but possibly as part of a larger composite diff --git a/frame/utility/src/benchmarking.rs b/frame/utility/src/benchmarking.rs index 4e77c8e44a676..474009d11d607 100644 --- a/frame/utility/src/benchmarking.rs +++ b/frame/utility/src/benchmarking.rs @@ -23,26 +23,10 @@ use super::*; use frame_system::RawOrigin; use frame_benchmarking::{benchmarks, account}; use sp_runtime::traits::Saturating; - use crate::Module as Utility; const SEED: u32 = 0; -fn setup_multi(s: u32, z: u32) -> Result<(Vec, Box<::Call>), &'static str>{ - let mut signatories: Vec = Vec::new(); - for i in 0 .. s { - let signatory = account("signatory", i, SEED); - // Give them some balance for a possible deposit - let deposit = T::MultisigDepositBase::get() + T::MultisigDepositFactor::get() * s.into(); - let balance = T::Currency::minimum_balance().saturating_mul(100.into()) + deposit; - T::Currency::make_free_balance_be(&signatory, balance); - signatories.push(signatory); - } - signatories.sort(); - let call: Box<::Call> = Box::new(frame_system::Call::remark(vec![0; z as usize]).into()); - return Ok((signatories, call)) -} - benchmarks! { _ { } @@ -62,90 +46,11 @@ benchmarks! { let call = Box::new(frame_system::Call::remark(vec![]).into()); }: _(RawOrigin::Signed(caller), u as u16, call) - as_multi_create { - // Signatories, need at least 2 total people - let s in 2 .. T::MaxSignatories::get() as u32; - // Transaction Length - let z in 0 .. 10_000; - let (mut signatories, call) = setup_multi::(s, z)?; - let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; - }: as_multi(RawOrigin::Signed(caller), s as u16, signatories, None, call) - - as_multi_approve { - // Signatories, need at least 2 people - let s in 2 .. T::MaxSignatories::get() as u32; - // Transaction Length - let z in 0 .. 10_000; - let (mut signatories, call) = setup_multi::(s, z)?; - let mut signatories2 = signatories.clone(); - let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; - // before the call, get the timepoint - let timepoint = Utility::::timepoint(); - // Create the multi - Utility::::as_multi(RawOrigin::Signed(caller).into(), s as u16, signatories, None, call.clone())?; - let caller2 = signatories2.remove(0); - }: as_multi(RawOrigin::Signed(caller2), s as u16, signatories2, Some(timepoint), call) - - as_multi_complete { - // Signatories, need at least 2 people - let s in 2 .. T::MaxSignatories::get() as u32; - // Transaction Length - let z in 0 .. 10_000; - let (mut signatories, call) = setup_multi::(s, z)?; - let mut signatories2 = signatories.clone(); - let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; - // before the call, get the timepoint - let timepoint = Utility::::timepoint(); - // Create the multi - Utility::::as_multi(RawOrigin::Signed(caller).into(), s as u16, signatories, None, call.clone())?; - // Everyone except the first person approves - for i in 1 .. s - 1 { - let mut signatories_loop = signatories2.clone(); - let caller_loop = signatories_loop.remove(i as usize); - Utility::::as_multi(RawOrigin::Signed(caller_loop).into(), s as u16, signatories_loop, Some(timepoint), call.clone())?; - } - let caller2 = signatories2.remove(0); - }: as_multi(RawOrigin::Signed(caller2), s as u16, signatories2, Some(timepoint), call) - - approve_as_multi_create { - // Signatories, need at least 2 people - let s in 2 .. T::MaxSignatories::get() as u32; - // Transaction Length - let z in 0 .. 10_000; - let (mut signatories, call) = setup_multi::(s, z)?; - let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; - let call_hash = call.using_encoded(blake2_256); - // Create the multi - }: approve_as_multi(RawOrigin::Signed(caller), s as u16, signatories, None, call_hash) - - approve_as_multi_approve { - // Signatories, need at least 2 people - let s in 2 .. T::MaxSignatories::get() as u32; - // Transaction Length - let z in 0 .. 10_000; - let (mut signatories, call) = setup_multi::(s, z)?; - let mut signatories2 = signatories.clone(); - let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; - let call_hash = call.using_encoded(blake2_256); - // before the call, get the timepoint - let timepoint = Utility::::timepoint(); - // Create the multi - Utility::::as_multi(RawOrigin::Signed(caller).into(), s as u16, signatories, None, call.clone())?; - let caller2 = signatories2.remove(0); - }: approve_as_multi(RawOrigin::Signed(caller2), s as u16, signatories2, Some(timepoint), call_hash) - - cancel_as_multi { - // Signatories, need at least 2 people - let s in 2 .. T::MaxSignatories::get() as u32; - // Transaction Length - let z in 0 .. 10_000; - let (mut signatories, call) = setup_multi::(s, z)?; - let caller = signatories.pop().ok_or("signatories should have len 2 or more")?; - let call_hash = call.using_encoded(blake2_256); - let timepoint = Utility::::timepoint(); - // Create the multi - Utility::::as_multi(RawOrigin::Signed(caller.clone()).into(), s as u16, signatories.clone(), None, call.clone())?; - }: _(RawOrigin::Signed(caller), s as u16, signatories, timepoint, call_hash) + as_limited_sub { + let u in 0 .. 1000; + let caller = account("caller", u, SEED); + let call = Box::new(frame_system::Call::remark(vec![]).into()); + }: _(RawOrigin::Signed(caller), u as u16, call) } #[cfg(test)] @@ -159,12 +64,7 @@ mod tests { new_test_ext().execute_with(|| { assert_ok!(test_benchmark_batch::()); assert_ok!(test_benchmark_as_sub::()); - assert_ok!(test_benchmark_as_multi_create::()); - assert_ok!(test_benchmark_as_multi_approve::()); - assert_ok!(test_benchmark_as_multi_complete::()); - assert_ok!(test_benchmark_approve_as_multi_create::()); - assert_ok!(test_benchmark_approve_as_multi_approve::()); - assert_ok!(test_benchmark_cancel_as_multi::()); + assert_ok!(test_benchmark_as_limited_sub::()); }); } } diff --git a/frame/utility/src/lib.rs b/frame/utility/src/lib.rs index ea56bc45998aa..34385b67864c5 100644 --- a/frame/utility/src/lib.rs +++ b/frame/utility/src/lib.rs @@ -16,14 +16,14 @@ // limitations under the License. //! # Utility Module -//! A module with helpers for dispatch management. +//! A stateless module with helpers for dispatch management. //! //! - [`utility::Trait`](./trait.Trait.html) //! - [`Call`](./enum.Call.html) //! //! ## Overview //! -//! This module contains three basic pieces of functionality, two of which are stateless: +//! This module contains two basic pieces of functionality: //! - Batch dispatch: A stateless operation, allowing any origin to execute multiple calls in a //! single dispatch. This can be useful to amalgamate proposals, combining `set_code` with //! corresponding `set_storage`s, for efficient multiple payouts with just a single signature @@ -33,12 +33,6 @@ //! account IDs) and these can be stacked. This can be useful as a key management tool, where you //! need multiple distinct accounts (e.g. as controllers for many staking accounts), but where //! it's perfectly fine to have each of them controlled by the same underlying keypair. -//! - Multisig dispatch (stateful): A potentially stateful operation, allowing multiple signed -//! origins (accounts) to coordinate and dispatch a call from a well-known origin, derivable -//! deterministically from the set of account IDs and the threshold number of accounts from the -//! set that must approve it. In the case that the threshold is just one then this is a stateless -//! operation. This is useful for multisig wallets where cryptographic threshold signatures are -//! not available or desired. //! //! ## Interface //! @@ -50,12 +44,6 @@ //! #### For pseudonymal dispatch //! * `as_sub` - Dispatch a call from a secondary ("sub") signed origin. //! -//! #### For multisig dispatch -//! * `as_multi` - Approve and if possible dispatch a call from a composite origin formed from a -//! number of signed origins. -//! * `approve_as_multi` - Approve a call from a composite origin. -//! * `cancel_as_multi` - Cancel a call from a composite origin. -//! //! [`Call`]: ./enum.Call.html //! [`Trait`]: ./trait.Trait.html @@ -66,10 +54,9 @@ use sp_std::prelude::*; use codec::{Encode, Decode}; use sp_core::TypeId; use sp_io::hashing::blake2_256; -use frame_support::{decl_module, decl_event, decl_error, decl_storage, Parameter, ensure, RuntimeDebug}; -use frame_support::{traits::{Get, ReservableCurrency, Currency, Filter}, - weights::{Weight, GetDispatchInfo, DispatchClass, FunctionOf, Pays}, - dispatch::{DispatchResultWithPostInfo, DispatchErrorWithPostInfo, PostDispatchInfo}, +use frame_support::{decl_module, decl_event, decl_error, decl_storage, Parameter, ensure}; +use frame_support::{traits::{Filter, FilterStack, ClearFilterGuard}, + weights::{Weight, GetDispatchInfo, DispatchClass, FunctionOf, Pays}, dispatch::PostDispatchInfo, }; use frame_system::{self as system, ensure_signed, ensure_root}; use sp_runtime::{DispatchError, DispatchResult, traits::Dispatchable}; @@ -77,97 +64,25 @@ use sp_runtime::{DispatchError, DispatchResult, traits::Dispatchable}; mod tests; mod benchmarking; -type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; - /// Configuration trait. pub trait Trait: frame_system::Trait { /// The overarching event type. - type Event: From> + Into<::Event>; + type Event: From + Into<::Event>; /// The overarching call type. type Call: Parameter + Dispatchable + GetDispatchInfo + From>; - /// The currency mechanism. - type Currency: ReservableCurrency; - - /// The base amount of currency needed to reserve for creating a multisig execution. - /// - /// This is held for an additional storage item whose value size is - /// `4 + sizeof((BlockNumber, Balance, AccountId))` bytes. - type MultisigDepositBase: Get>; - - /// The amount of currency needed per unit threshold when creating a multisig execution. - /// - /// This is held for adding 32 bytes more into a pre-existing storage value. - type MultisigDepositFactor: Get>; - - /// The maximum amount of signatories allowed in the multisig. - type MaxSignatories: Get; - /// Is a given call compatible with the proxying subsystem? - type IsCallable: Filter<::Call>; -} - -/// A global extrinsic index, formed as the extrinsic index within a block, together with that -/// block's height. This allows a transaction in which a multisig operation of a particular -/// composite was created to be uniquely identified. -#[derive(Copy, Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)] -pub struct Timepoint { - /// The height of the chain at the point in time. - height: BlockNumber, - /// The index of the extrinsic at the point in time. - index: u32, -} - -/// An open multisig operation. -#[derive(Clone, Eq, PartialEq, Encode, Decode, Default, RuntimeDebug)] -pub struct Multisig { - /// The extrinsic when the multisig operation was opened. - when: Timepoint, - /// The amount held in reserve of the `depositor`, to be returned once the operation ends. - deposit: Balance, - /// The account who opened it (i.e. the first to approve it). - depositor: AccountId, - /// The approvals achieved so far, including the depositor. Always sorted. - approvals: Vec, + type IsCallable: FilterStack<::Call>; } decl_storage! { - trait Store for Module as Utility { - /// The set of open multisig operations. - pub Multisigs: double_map - hasher(twox_64_concat) T::AccountId, hasher(blake2_128_concat) [u8; 32] - => Option, T::AccountId>>; - } + trait Store for Module as Utility {} } decl_error! { pub enum Error for Module { - /// Threshold is too low (zero). - ZeroThreshold, - /// Call is already approved by this signatory. - AlreadyApproved, - /// Call doesn't need any (more) approvals. - NoApprovalsNeeded, - /// There are too few signatories in the list. - TooFewSignatories, - /// There are too many signatories in the list. - TooManySignatories, - /// The signatories were provided out of order; they should be ordered. - SignatoriesOutOfOrder, - /// The sender was contained in the other signatories; it shouldn't be. - SenderInSignatories, - /// Multisig operation not found when attempting to cancel. - NotFound, - /// Only the account that originally created the multisig is able to cancel it. - NotOwner, - /// No timepoint was given, yet the multisig operation is already underway. - NoTimepoint, - /// A different timepoint was given to the multisig operation that is underway. - WrongTimepoint, - /// A timepoint was given, yet no multisig operation is underway. - UnexpectedTimepoint, /// A call with a `false` `IsCallable` filter was attempted. Uncallable, } @@ -175,28 +90,12 @@ decl_error! { decl_event! { /// Events type. - pub enum Event where - AccountId = ::AccountId, - BlockNumber = ::BlockNumber, - CallHash = [u8; 32] - { + pub enum Event { /// Batch of dispatches did not complete fully. Index of first failing dispatch given, as /// well as the error. BatchInterrupted(u32, DispatchError), /// Batch of dispatches completed fully with no error. BatchCompleted, - /// A new multisig operation has begun. First param is the account that is approving, - /// second is the multisig account, third is hash of the call. - NewMultisig(AccountId, AccountId, CallHash), - /// A multisig operation has been approved by someone. First param is the account that is - /// approving, third is the multisig account, fourth is hash of the call. - MultisigApproval(AccountId, Timepoint, AccountId, CallHash), - /// A multisig operation has been executed. First param is the account that is - /// approving, third is the multisig account, fourth is hash of the call to be executed. - MultisigExecuted(AccountId, Timepoint, AccountId, CallHash, DispatchResult), - /// A multisig operation has been cancelled. First param is the account that is - /// cancelling, third is the multisig account, fourth is hash of the call. - MultisigCancelled(AccountId, Timepoint, AccountId, CallHash), /// A call with a `false` IsCallable filter was attempted. Uncallable(u32), } @@ -210,25 +109,6 @@ impl TypeId for IndexedUtilityModuleId { const TYPE_ID: [u8; 4] = *b"suba"; } -mod weight_of { - use super::*; - - /// - Base Weight: - /// - Create: 46.55 + 0.089 * S µs - /// - Approve: 34.03 + .112 * S µs - /// - Complete: 40.36 + .225 * S µs - /// - DB Weight: - /// - Reads: Multisig Storage, [Caller Account] - /// - Writes: Multisig Storage, [Caller Account] - /// - Plus Call Weight - pub fn as_multi(other_sig_len: usize, call_weight: Weight) -> Weight { - call_weight - .saturating_add(45_000_000) - .saturating_add((other_sig_len as Weight).saturating_mul(250_000)) - .saturating_add(T::DbWeight::get().reads_writes(1, 1)) - } -} - decl_module! { pub struct Module for enum Call where origin: T::Origin { type Error = Error; @@ -278,21 +158,26 @@ decl_module! { let is_root = ensure_root(origin.clone()).is_ok(); for (index, call) in calls.into_iter().enumerate() { if !is_root && !T::IsCallable::filter(&call) { - Self::deposit_event(Event::::Uncallable(index as u32)); + Self::deposit_event(Event::Uncallable(index as u32)); return Ok(()) } let result = call.dispatch(origin.clone()); if let Err(e) = result { - Self::deposit_event(Event::::BatchInterrupted(index as u32, e.error)); + Self::deposit_event(Event::BatchInterrupted(index as u32, e.error)); return Ok(()); } } - Self::deposit_event(Event::::BatchCompleted); + Self::deposit_event(Event::BatchCompleted); } /// Send a call through an indexed pseudonym of the sender. /// - /// Calls must each fulfil the `IsCallable` filter. + /// The call must fulfil only the pre-cleared `IsCallable` filter (i.e. only the level of + /// filtering that remains after calling `take()`). + /// + /// NOTE: If you need to ensure that any account-based filtering is honored (i.e. because + /// you expect `proxy` to have been used prior in the call stack and you want it to apply to + /// any sub-accounts), then use `as_limited_sub` instead. /// /// The dispatch origin for this call must be _Signed_. /// @@ -309,310 +194,42 @@ decl_module! { )] fn as_sub(origin, index: u16, call: Box<::Call>) -> DispatchResult { let who = ensure_signed(origin)?; + // We're now executing as a freshly authenticated new account, so the previous call + // restrictions no longer apply. + let _guard = ClearFilterGuard::::Call>::new(); ensure!(T::IsCallable::filter(&call), Error::::Uncallable); let pseudonym = Self::sub_account_id(who, index); call.dispatch(frame_system::RawOrigin::Signed(pseudonym).into()) .map(|_| ()).map_err(|e| e.error) } - /// Register approval for a dispatch to be made from a deterministic composite account if - /// approved by a total of `threshold - 1` of `other_signatories`. - /// - /// If there are enough, then dispatch the call. Calls must each fulfil the `IsCallable` - /// filter. - /// - /// Payment: `MultisigDepositBase` will be reserved if this is the first approval, plus - /// `threshold` times `MultisigDepositFactor`. It is returned once this dispatch happens or - /// is cancelled. - /// - /// The dispatch origin for this call must be _Signed_. - /// - /// - `threshold`: The total number of approvals for this dispatch before it is executed. - /// - `other_signatories`: The accounts (other than the sender) who can approve this - /// dispatch. May not be empty. - /// - `maybe_timepoint`: If this is the first approval, then this must be `None`. If it is - /// not the first approval, then it must be `Some`, with the timepoint (block number and - /// transaction index) of the first approval transaction. - /// - `call`: The call to be executed. - /// - /// NOTE: Unless this is the final approval, you will generally want to use - /// `approve_as_multi` instead, since it only requires a hash of the call. - /// - /// Result is equivalent to the dispatched result if `threshold` is exactly `1`. Otherwise - /// on success, result is `Ok` and the result from the interior call, if it was executed, - /// may be found in the deposited `MultisigExecuted` event. - /// - /// # - /// - `O(S + Z + Call)`. - /// - Up to one balance-reserve or unreserve operation. - /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of - /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. - /// - One call encode & hash, both of complexity `O(Z)` where `Z` is tx-len. - /// - One encode & hash, both of complexity `O(S)`. - /// - Up to one binary search and insert (`O(logS + S)`). - /// - I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. - /// - One event. - /// - The weight of the `call`. - /// - Storage: inserts one item, value size bounded by `MaxSignatories`, with a - /// deposit taken for its lifetime of - /// `MultisigDepositBase + threshold * MultisigDepositFactor`. - /// ------------------------------- - /// - Base Weight: - /// - Create: 46.55 + 0.089 * S µs - /// - Approve: 34.03 + .112 * S µs - /// - Complete: 40.36 + .225 * S µs - /// - DB Weight: - /// - Reads: Multisig Storage, [Caller Account] - /// - Writes: Multisig Storage, [Caller Account] - /// - Plus Call Weight - /// # - #[weight = FunctionOf( - |args: (&u16, &Vec, &Option>, &Box<::Call>)| { - weight_of::as_multi::(args.1.len(),args.3.get_dispatch_info().weight) - }, - |args: (&u16, &Vec, &Option>, &Box<::Call>)| { - args.3.get_dispatch_info().class - }, - Pays::Yes, - )] - fn as_multi(origin, - threshold: u16, - other_signatories: Vec, - maybe_timepoint: Option>, - call: Box<::Call>, - ) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - ensure!(T::IsCallable::filter(call.as_ref()), Error::::Uncallable); - ensure!(threshold >= 1, Error::::ZeroThreshold); - let max_sigs = T::MaxSignatories::get() as usize; - ensure!(!other_signatories.is_empty(), Error::::TooFewSignatories); - let other_signatories_len = other_signatories.len(); - ensure!(other_signatories_len < max_sigs, Error::::TooManySignatories); - let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?; - - let id = Self::multi_account_id(&signatories, threshold); - let call_hash = call.using_encoded(blake2_256); - - if let Some(mut m) = >::get(&id, call_hash) { - let timepoint = maybe_timepoint.ok_or(Error::::NoTimepoint)?; - ensure!(m.when == timepoint, Error::::WrongTimepoint); - if let Err(pos) = m.approvals.binary_search(&who) { - // we know threshold is greater than zero from the above ensure. - if (m.approvals.len() as u16) < threshold - 1 { - m.approvals.insert(pos, who.clone()); - >::insert(&id, call_hash, m); - Self::deposit_event(RawEvent::MultisigApproval(who, timepoint, id, call_hash)); - // Call is not made, so the actual weight does not include call - return Ok(Some(weight_of::as_multi::(other_signatories_len, 0)).into()) - } - } else { - if (m.approvals.len() as u16) < threshold { - Err(Error::::AlreadyApproved)? - } - } - - let result = call.dispatch(frame_system::RawOrigin::Signed(id.clone()).into()); - let _ = T::Currency::unreserve(&m.depositor, m.deposit); - >::remove(&id, call_hash); - Self::deposit_event(RawEvent::MultisigExecuted( - who, timepoint, id, call_hash, result.map(|_| ()).map_err(|e| e.error) - )); - return Ok(None.into()) - } else { - ensure!(maybe_timepoint.is_none(), Error::::UnexpectedTimepoint); - if threshold > 1 { - let deposit = T::MultisigDepositBase::get() - + T::MultisigDepositFactor::get() * threshold.into(); - T::Currency::reserve(&who, deposit)?; - >::insert(&id, call_hash, Multisig { - when: Self::timepoint(), - deposit, - depositor: who.clone(), - approvals: vec![who.clone()], - }); - Self::deposit_event(RawEvent::NewMultisig(who, id, call_hash)); - // Call is not made, so we can return that weight - return Ok(Some(weight_of::as_multi::(other_signatories_len, 0)).into()) - } else { - let result = call.dispatch(frame_system::RawOrigin::Signed(id).into()); - match result { - Ok(post_dispatch_info) => { - match post_dispatch_info.actual_weight { - Some(actual_weight) => return Ok(Some(weight_of::as_multi::(other_signatories_len, actual_weight)).into()), - None => return Ok(None.into()), - } - }, - Err(err) => { - match err.post_info.actual_weight { - Some(actual_weight) => { - let weight_used = weight_of::as_multi::(other_signatories_len, actual_weight); - return Err(DispatchErrorWithPostInfo { post_info: Some(weight_used).into(), error: err.error.into() }) - }, - None => { - return Err(err) - } - } - } - } - } - } - } - - /// Register approval for a dispatch to be made from a deterministic composite account if - /// approved by a total of `threshold - 1` of `other_signatories`. - /// - /// Payment: `MultisigDepositBase` will be reserved if this is the first approval, plus - /// `threshold` times `MultisigDepositFactor`. It is returned once this dispatch happens or - /// is cancelled. - /// - /// The dispatch origin for this call must be _Signed_. - /// - /// - `threshold`: The total number of approvals for this dispatch before it is executed. - /// - `other_signatories`: The accounts (other than the sender) who can approve this - /// dispatch. May not be empty. - /// - `maybe_timepoint`: If this is the first approval, then this must be `None`. If it is - /// not the first approval, then it must be `Some`, with the timepoint (block number and - /// transaction index) of the first approval transaction. - /// - `call_hash`: The hash of the call to be executed. + /// Send a call through an indexed pseudonym of the sender. /// - /// NOTE: If this is the final approval, you will want to use `as_multi` instead. + /// Calls must each fulfil the `IsCallable` filter; it is not cleared before. /// - /// # - /// - `O(S)`. - /// - Up to one balance-reserve or unreserve operation. - /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of - /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. - /// - One encode & hash, both of complexity `O(S)`. - /// - Up to one binary search and insert (`O(logS + S)`). - /// - I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. - /// - One event. - /// - Storage: inserts one item, value size bounded by `MaxSignatories`, with a - /// deposit taken for its lifetime of - /// `MultisigDepositBase + threshold * MultisigDepositFactor`. - /// ---------------------------------- - /// - Base Weight: - /// - Create: 44.71 + 0.088 * S - /// - Approve: 31.48 + 0.116 * S - /// - DB Weight: - /// - Read: Multisig Storage, [Caller Account] - /// - Write: Multisig Storage, [Caller Account] - /// # - #[weight = FunctionOf( - |args: (&u16, &Vec, &Option>, &[u8; 32])| { - T::DbWeight::get().reads_writes(1, 1) - .saturating_add(45_000_000) - .saturating_add((args.1.len() as Weight).saturating_mul(120_000)) - }, - DispatchClass::Normal, - Pays::Yes, - )] - fn approve_as_multi(origin, - threshold: u16, - other_signatories: Vec, - maybe_timepoint: Option>, - call_hash: [u8; 32], - ) -> DispatchResult { - let who = ensure_signed(origin)?; - ensure!(threshold >= 1, Error::::ZeroThreshold); - let max_sigs = T::MaxSignatories::get() as usize; - ensure!(!other_signatories.is_empty(), Error::::TooFewSignatories); - ensure!(other_signatories.len() < max_sigs, Error::::TooManySignatories); - let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?; - - let id = Self::multi_account_id(&signatories, threshold); - - if let Some(mut m) = >::get(&id, call_hash) { - let timepoint = maybe_timepoint.ok_or(Error::::NoTimepoint)?; - ensure!(m.when == timepoint, Error::::WrongTimepoint); - ensure!(m.approvals.len() < threshold as usize, Error::::NoApprovalsNeeded); - if let Err(pos) = m.approvals.binary_search(&who) { - m.approvals.insert(pos, who.clone()); - >::insert(&id, call_hash, m); - Self::deposit_event(RawEvent::MultisigApproval(who, timepoint, id, call_hash)); - } else { - Err(Error::::AlreadyApproved)? - } - } else { - if threshold > 1 { - ensure!(maybe_timepoint.is_none(), Error::::UnexpectedTimepoint); - let deposit = T::MultisigDepositBase::get() - + T::MultisigDepositFactor::get() * threshold.into(); - T::Currency::reserve(&who, deposit)?; - >::insert(&id, call_hash, Multisig { - when: Self::timepoint(), - deposit, - depositor: who.clone(), - approvals: vec![who.clone()], - }); - Self::deposit_event(RawEvent::NewMultisig(who, id, call_hash)); - } else { - Err(Error::::NoApprovalsNeeded)? - } - } - Ok(()) - } - - /// Cancel a pre-existing, on-going multisig transaction. Any deposit reserved previously - /// for this operation will be unreserved on success. + /// NOTE: If you need to ensure that any account-based filtering is not honored (i.e. + /// because you expect `proxy` to have been used prior in the call stack and you do not want + /// the call restrictions to apply to any sub-accounts), then use `as_sub` instead. /// /// The dispatch origin for this call must be _Signed_. /// - /// - `threshold`: The total number of approvals for this dispatch before it is executed. - /// - `other_signatories`: The accounts (other than the sender) who can approve this - /// dispatch. May not be empty. - /// - `timepoint`: The timepoint (block number and transaction index) of the first approval - /// transaction for this dispatch. - /// - `call_hash`: The hash of the call to be executed. - /// /// # - /// - `O(S)`. - /// - Up to one balance-reserve or unreserve operation. - /// - One passthrough operation, one insert, both `O(S)` where `S` is the number of - /// signatories. `S` is capped by `MaxSignatories`, with weight being proportional. - /// - One encode & hash, both of complexity `O(S)`. - /// - One event. - /// - I/O: 1 read `O(S)`, one remove. - /// - Storage: removes one item. - /// ---------------------------------- - /// - Base Weight: 37.6 + 0.084 * S - /// - DB Weight: - /// - Read: Multisig Storage, [Caller Account] - /// - Write: Multisig Storage, [Caller Account] + /// - Base weight: 2.861 µs + /// - Plus the weight of the `call` /// # #[weight = FunctionOf( - |args: (&u16, &Vec, &Timepoint, &[u8; 32])| { - T::DbWeight::get().reads_writes(1, 1) - .saturating_add(40_000_000) - .saturating_add((args.1.len() as Weight).saturating_mul(100_000)) + |args: (&u16, &Box<::Call>)| { + args.1.get_dispatch_info().weight.saturating_add(3_000_000) }, - DispatchClass::Normal, + |args: (&u16, &Box<::Call>)| args.1.get_dispatch_info().class, Pays::Yes, )] - fn cancel_as_multi(origin, - threshold: u16, - other_signatories: Vec, - timepoint: Timepoint, - call_hash: [u8; 32], - ) -> DispatchResult { + fn as_limited_sub(origin, index: u16, call: Box<::Call>) -> DispatchResult { let who = ensure_signed(origin)?; - ensure!(threshold >= 1, Error::::ZeroThreshold); - let max_sigs = T::MaxSignatories::get() as usize; - ensure!(!other_signatories.is_empty(), Error::::TooFewSignatories); - ensure!(other_signatories.len() < max_sigs, Error::::TooManySignatories); - let signatories = Self::ensure_sorted_and_insert(other_signatories, who.clone())?; - - let id = Self::multi_account_id(&signatories, threshold); - - let m = >::get(&id, call_hash) - .ok_or(Error::::NotFound)?; - ensure!(m.when == timepoint, Error::::WrongTimepoint); - ensure!(m.depositor == who, Error::::NotOwner); - - let _ = T::Currency::unreserve(&m.depositor, m.deposit); - >::remove(&id, call_hash); - - Self::deposit_event(RawEvent::MultisigCancelled(who, timepoint, id, call_hash)); - Ok(()) + ensure!(T::IsCallable::filter(&call), Error::::Uncallable); + let pseudonym = Self::sub_account_id(who, index); + call.dispatch(frame_system::RawOrigin::Signed(pseudonym).into()) + .map(|_| ()).map_err(|e| e.error) } } } @@ -623,42 +240,4 @@ impl Module { let entropy = (b"modlpy/utilisuba", who, index).using_encoded(blake2_256); T::AccountId::decode(&mut &entropy[..]).unwrap_or_default() } - - /// Derive a multi-account ID from the sorted list of accounts and the threshold that are - /// required. - /// - /// NOTE: `who` must be sorted. If it is not, then you'll get the wrong answer. - pub fn multi_account_id(who: &[T::AccountId], threshold: u16) -> T::AccountId { - let entropy = (b"modlpy/utilisuba", who, threshold).using_encoded(blake2_256); - T::AccountId::decode(&mut &entropy[..]).unwrap_or_default() - } - - /// The current `Timepoint`. - pub fn timepoint() -> Timepoint { - Timepoint { - height: >::block_number(), - index: >::extrinsic_index().unwrap_or_default(), - } - } - - /// Check that signatories is sorted and doesn't contain sender, then insert sender. - fn ensure_sorted_and_insert(other_signatories: Vec, who: T::AccountId) - -> Result, DispatchError> - { - let mut signatories = other_signatories; - let mut maybe_last = None; - let mut index = 0; - for item in signatories.iter() { - if let Some(last) = maybe_last { - ensure!(last < item, Error::::SignatoriesOutOfOrder); - } - if item <= &who { - ensure!(item != &who, Error::::SenderInSignatories); - index += 1; - } - maybe_last = Some(item); - } - signatories.insert(index, who); - Ok(signatories) - } } diff --git a/frame/utility/src/tests.rs b/frame/utility/src/tests.rs index a74d9b3253f64..66a663a38509c 100644 --- a/frame/utility/src/tests.rs +++ b/frame/utility/src/tests.rs @@ -32,12 +32,11 @@ use crate as utility; impl_outer_origin! { pub enum Origin for Test where system = frame_system {} } - impl_outer_event! { pub enum TestEvent for Test { system, pallet_balances, - utility, + utility, } } impl_outer_dispatch! { @@ -89,8 +88,8 @@ parameter_types! { } impl pallet_balances::Trait for Test { type Balance = u64; - type Event = TestEvent; type DustRemoval = (); + type Event = TestEvent; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; } @@ -108,13 +107,16 @@ impl Filter for TestIsCallable { } } } +impl FilterStack for TestIsCallable { + type Stack = (); + fn push(_: impl Fn(&Call) -> bool + 'static) {} + fn pop() {} + fn take() -> Self::Stack { () } + fn restore(_: Self::Stack) {} +} impl Trait for Test { type Event = TestEvent; type Call = Call; - type Currency = Balances; - type MultisigDepositBase = MultisigDepositBase; - type MultisigDepositFactor = MultisigDepositFactor; - type MaxSignatories = MaxSignatories; type IsCallable = TestIsCallable; } type System = frame_system::Module; @@ -142,264 +144,6 @@ fn expect_event>(e: E) { assert_eq!(last_event(), e.into()); } -fn now() -> Timepoint { - Utility::timepoint() -} - -#[test] -fn multisig_deposit_is_taken_and_returned() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); - assert_eq!(Balances::free_balance(1), 2); - assert_eq!(Balances::reserved_balance(1), 3); - - assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); - assert_eq!(Balances::free_balance(1), 5); - assert_eq!(Balances::reserved_balance(1), 0); - }); -} - -#[test] -fn cancel_multisig_returns_deposit() { - new_test_ext().execute_with(|| { - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - assert_ok!(Utility::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); - assert_ok!(Utility::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); - assert_eq!(Balances::free_balance(1), 6); - assert_eq!(Balances::reserved_balance(1), 4); - assert_ok!( - Utility::cancel_as_multi(Origin::signed(1), 3, vec![2, 3], now(), hash.clone()), - ); - assert_eq!(Balances::free_balance(1), 10); - assert_eq!(Balances::reserved_balance(1), 0); - }); -} - -#[test] -fn timepoint_checking_works() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - - assert_noop!( - Utility::approve_as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), hash.clone()), - Error::::UnexpectedTimepoint, - ); - - assert_ok!(Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash)); - - assert_noop!( - Utility::as_multi(Origin::signed(2), 2, vec![1, 3], None, call.clone()), - Error::::NoTimepoint, - ); - let later = Timepoint { index: 1, .. now() }; - assert_noop!( - Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(later), call.clone()), - Error::::WrongTimepoint, - ); - }); -} - -#[test] -fn multisig_2_of_3_works() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - assert_ok!(Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash)); - assert_eq!(Balances::free_balance(6), 0); - - assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); - assert_eq!(Balances::free_balance(6), 15); - }); -} - -#[test] -fn multisig_3_of_3_works() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 3); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - assert_ok!(Utility::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); - assert_ok!(Utility::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); - assert_eq!(Balances::free_balance(6), 0); - - assert_ok!(Utility::as_multi(Origin::signed(3), 3, vec![1, 2], Some(now()), call)); - assert_eq!(Balances::free_balance(6), 15); - }); -} - -#[test] -fn cancel_multisig_works() { - new_test_ext().execute_with(|| { - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - assert_ok!(Utility::approve_as_multi(Origin::signed(1), 3, vec![2, 3], None, hash.clone())); - assert_ok!(Utility::approve_as_multi(Origin::signed(2), 3, vec![1, 3], Some(now()), hash.clone())); - assert_noop!( - Utility::cancel_as_multi(Origin::signed(2), 3, vec![1, 3], now(), hash.clone()), - Error::::NotOwner, - ); - assert_ok!( - Utility::cancel_as_multi(Origin::signed(1), 3, vec![2, 3], now(), hash.clone()), - ); - }); -} - -#[test] -fn multisig_2_of_3_as_multi_works() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); - assert_eq!(Balances::free_balance(6), 0); - - assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call)); - assert_eq!(Balances::free_balance(6), 15); - }); -} - -#[test] -fn multisig_2_of_3_as_multi_with_many_calls_works() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call1 = Box::new(Call::Balances(BalancesCall::transfer(6, 10))); - let call2 = Box::new(Call::Balances(BalancesCall::transfer(7, 5))); - - assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call1.clone())); - assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], None, call2.clone())); - assert_ok!(Utility::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call2)); - assert_ok!(Utility::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call1)); - - assert_eq!(Balances::free_balance(6), 10); - assert_eq!(Balances::free_balance(7), 5); - }); -} - -#[test] -fn multisig_2_of_3_cannot_reissue_same_call() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 2); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 10))); - assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); - assert_ok!(Utility::as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), call.clone())); - assert_eq!(Balances::free_balance(multi), 5); - - assert_ok!(Utility::as_multi(Origin::signed(1), 2, vec![2, 3], None, call.clone())); - assert_ok!(Utility::as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), call.clone())); - - let err = DispatchError::from(BalancesError::::InsufficientBalance).stripped(); - expect_event(RawEvent::MultisigExecuted(3, now(), multi, call.using_encoded(blake2_256), Err(err))); - }); -} - -#[test] -fn zero_threshold_fails() { - new_test_ext().execute_with(|| { - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - assert_noop!( - Utility::as_multi(Origin::signed(1), 0, vec![2], None, call), - Error::::ZeroThreshold, - ); - }); -} - -#[test] -fn too_many_signatories_fails() { - new_test_ext().execute_with(|| { - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - assert_noop!( - Utility::as_multi(Origin::signed(1), 2, vec![2, 3, 4], None, call.clone()), - Error::::TooManySignatories, - ); - }); -} - -#[test] -fn duplicate_approvals_are_ignored() { - new_test_ext().execute_with(|| { - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - assert_ok!(Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], None, hash.clone())); - assert_noop!( - Utility::approve_as_multi(Origin::signed(1), 2, vec![2, 3], Some(now()), hash.clone()), - Error::::AlreadyApproved, - ); - assert_ok!(Utility::approve_as_multi(Origin::signed(2), 2, vec![1, 3], Some(now()), hash.clone())); - assert_noop!( - Utility::approve_as_multi(Origin::signed(3), 2, vec![1, 2], Some(now()), hash.clone()), - Error::::NoApprovalsNeeded, - ); - }); -} - -#[test] -fn multisig_1_of_3_works() { - new_test_ext().execute_with(|| { - let multi = Utility::multi_account_id(&[1, 2, 3][..], 1); - assert_ok!(Balances::transfer(Origin::signed(1), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(2), multi, 5)); - assert_ok!(Balances::transfer(Origin::signed(3), multi, 5)); - - let call = Box::new(Call::Balances(BalancesCall::transfer(6, 15))); - let hash = call.using_encoded(blake2_256); - assert_noop!( - Utility::approve_as_multi(Origin::signed(1), 1, vec![2, 3], None, hash.clone()), - Error::::NoApprovalsNeeded, - ); - assert_noop!( - Utility::as_multi(Origin::signed(4), 1, vec![2, 3], None, call.clone()), - BalancesError::::InsufficientBalance, - ); - assert_ok!(Utility::as_multi(Origin::signed(1), 1, vec![2, 3], None, call)); - - assert_eq!(Balances::free_balance(6), 15); - }); -} - -#[test] -fn multisig_filters() { - new_test_ext().execute_with(|| { - let call = Box::new(Call::System(frame_system::Call::remark(vec![]))); - assert_noop!( - Utility::as_multi(Origin::signed(1), 1, vec![], None, call.clone()), - Error::::Uncallable, - ); - }); -} - #[test] fn as_sub_works() { new_test_ext().execute_with(|| { @@ -469,7 +213,7 @@ fn batch_with_signed_filters() { Call::System(frame_system::Call::remark(vec![])) ]), ); - expect_event(RawEvent::Uncallable(0)); + expect_event(Event::Uncallable(0)); }); } diff --git a/primitives/std/with_std.rs b/primitives/std/with_std.rs index f495fa8fea281..e1994e764d23e 100644 --- a/primitives/std/with_std.rs +++ b/primitives/std/with_std.rs @@ -33,6 +33,7 @@ pub use std::num; pub use std::ops; pub use std::ptr; pub use std::rc; +pub use std::sync; pub use std::result; pub use std::slice; pub use std::str; diff --git a/primitives/std/without_std.rs b/primitives/std/without_std.rs index 452994ca48e99..09f7a1976cc02 100755 --- a/primitives/std/without_std.rs +++ b/primitives/std/without_std.rs @@ -19,6 +19,7 @@ pub extern crate alloc; pub use alloc::boxed; pub use alloc::rc; +pub use alloc::sync; pub use alloc::vec; pub use core::any; pub use core::cell;