diff --git a/Cargo.lock b/Cargo.lock index 199196bc48c29..72a71ad05ddfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4281,6 +4281,7 @@ dependencies = [ "pallet-referenda", "pallet-remark", "pallet-root-testing", + "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-session", @@ -4298,6 +4299,7 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", + "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-vesting", @@ -7430,6 +7432,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-safe-mode" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-proxy", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-salary" version = "4.0.0-dev" @@ -7798,6 +7819,24 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-tx-pause" +version = "4.0.0-dev" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-proxy", + "pallet-utility", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-uniques" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 1411e2ca796da..3585a5ecd3572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,6 +188,8 @@ members = [ "frame/recovery", "frame/referenda", "frame/remark", + "frame/tx-pause", + "frame/safe-mode", "frame/salary", "frame/scheduler", "frame/scored-pool", diff --git a/bin/node/cli/src/chain_spec.rs b/bin/node/cli/src/chain_spec.rs index 57d367e7c49a8..51beaad03688a 100644 --- a/bin/node/cli/src/chain_spec.rs +++ b/bin/node/cli/src/chain_spec.rs @@ -358,6 +358,8 @@ pub fn testnet_genesis( transaction_storage: Default::default(), transaction_payment: Default::default(), alliance: Default::default(), + safe_mode: Default::default(), + tx_pause: Default::default(), alliance_motion: Default::default(), nomination_pools: NominationPoolsConfig { min_create_bond: 10 * DOLLARS, diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 135621a3be730..4aa048efcf60a 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -129,6 +129,8 @@ pallet-transaction-storage = { version = "4.0.0-dev", default-features = false, pallet-uniques = { version = "4.0.0-dev", default-features = false, path = "../../../frame/uniques" } pallet-vesting = { version = "4.0.0-dev", default-features = false, path = "../../../frame/vesting" } pallet-whitelist = { version = "4.0.0-dev", default-features = false, path = "../../../frame/whitelist" } +pallet-tx-pause = { version = "4.0.0-dev", default-features = false, path = "../../../frame/tx-pause" } +pallet-safe-mode = { version = "4.0.0-dev", default-features = false, path = "../../../frame/safe-mode" } [build-dependencies] substrate-wasm-builder = { version = "5.0.0-dev", path = "../../../utils/wasm-builder", optional = true } @@ -200,6 +202,7 @@ std = [ "pallet-referenda/std", "pallet-remark/std", "pallet-root-testing/std", + "pallet-safe-mode/std", "pallet-salary/std", "pallet-scheduler/std", "pallet-session-benchmarking?/std", @@ -216,6 +219,7 @@ std = [ "pallet-transaction-payment/std", "pallet-transaction-storage/std", "pallet-treasury/std", + "pallet-tx-pause/std", "pallet-uniques/std", "pallet-utility/std", "pallet-vesting/std", @@ -290,6 +294,7 @@ runtime-benchmarks = [ "pallet-recovery/runtime-benchmarks", "pallet-referenda/runtime-benchmarks", "pallet-remark/runtime-benchmarks", + "pallet-safe-mode/runtime-benchmarks", "pallet-salary/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-session-benchmarking/runtime-benchmarks", @@ -301,6 +306,7 @@ runtime-benchmarks = [ "pallet-tips/runtime-benchmarks", "pallet-transaction-storage/runtime-benchmarks", "pallet-treasury/runtime-benchmarks", + "pallet-tx-pause/runtime-benchmarks", "pallet-uniques/runtime-benchmarks", "pallet-utility/runtime-benchmarks", "pallet-vesting/runtime-benchmarks", @@ -360,6 +366,7 @@ try-runtime = [ "pallet-referenda/try-runtime", "pallet-remark/try-runtime", "pallet-root-testing/try-runtime", + "pallet-safe-mode/try-runtime", "pallet-salary/try-runtime", "pallet-scheduler/try-runtime", "pallet-session/try-runtime", @@ -373,6 +380,7 @@ try-runtime = [ "pallet-transaction-payment/try-runtime", "pallet-transaction-storage/try-runtime", "pallet-treasury/try-runtime", + "pallet-tx-pause/try-runtime", "pallet-uniques/try-runtime", "pallet-utility/try-runtime", "pallet-vesting/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index fe2382cce613c..4f34e4ecd8122 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -37,9 +37,9 @@ use frame_support::{ traits::{ fungible::{Balanced, Credit, ItemOf}, tokens::{nonfungibles_v2::Inspect, GetSalary, PayFromAccount}, - AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Currency, EitherOfDiverse, - EqualPrivilegeOnly, Everything, Imbalance, InstanceFilter, KeyOwnerProofSystem, - LockIdentifier, Nothing, OnUnbalanced, WithdrawReasons, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU16, ConstU32, Contains, Currency, + EitherOfDiverse, EqualPrivilegeOnly, Imbalance, InsideBoth, InstanceFilter, + KeyOwnerProofSystem, LockIdentifier, Nothing, OnUnbalanced, WithdrawReasons, }, weights::{ constants::{ @@ -64,6 +64,7 @@ use pallet_nis::WithMaximumOf; use pallet_session::historical as pallet_session_historical; pub use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; +use pallet_tx_pause::RuntimeCallNameOf; use sp_api::impl_runtime_apis; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use sp_consensus_grandpa::AuthorityId as GrandpaId; @@ -220,8 +221,67 @@ parameter_types! { const_assert!(NORMAL_DISPATCH_RATIO.deconstruct() >= AVERAGE_ON_INITIALIZE_RATIO.deconstruct()); +/// Calls that can bypass the safe-mode pallet. +pub struct SafeModeWhitelistedCalls; +impl Contains for SafeModeWhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::System(_) | RuntimeCall::SafeMode(_) | RuntimeCall::TxPause(_) => true, + _ => false, + } + } +} + +/// Calls that cannot be paused by the tx-pause pallet. +pub struct TxPauseWhitelistedCalls; +/// Whitelist `Balances::transfer_keep_alive`, all others are pauseable. +impl Contains> for TxPauseWhitelistedCalls { + fn contains(full_name: &RuntimeCallNameOf) -> bool { + match (full_name.0.as_slice(), full_name.1.as_slice()) { + (b"Balances", b"transfer_keep_alive") => true, + _ => false, + } + } +} + +impl pallet_tx_pause::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PauseOrigin = EnsureRoot; + type UnpauseOrigin = EnsureRoot; + type WhitelistedCalls = TxPauseWhitelistedCalls; + type MaxNameLen = ConstU32<256>; + type WeightInfo = pallet_tx_pause::weights::SubstrateWeight; +} + +parameter_types! { + pub const EnterDuration: BlockNumber = 4 * HOURS; + pub const EnterDepositAmount: Balance = 2_000_000 * DOLLARS; + pub const ExtendDuration: BlockNumber = 2 * HOURS; + pub const ExtendDepositAmount: Balance = 1_000_000 * DOLLARS; + pub const ReleaseDelay: u32 = 2 * DAYS; +} + +impl pallet_safe_mode::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type WhitelistedCalls = SafeModeWhitelistedCalls; + type EnterDuration = EnterDuration; + type EnterDepositAmount = EnterDepositAmount; + type ExtendDuration = ExtendDuration; + type ExtendDepositAmount = ExtendDepositAmount; + type ForceEnterOrigin = EnsureRootWithSuccess>; + type ForceExtendOrigin = EnsureRootWithSuccess>; + type ForceExitOrigin = EnsureRoot; + type ForceDepositOrigin = EnsureRoot; + type ReleaseDelay = ReleaseDelay; + type Notify = (); + type WeightInfo = pallet_safe_mode::weights::SubstrateWeight; +} + impl frame_system::Config for Runtime { - type BaseCallFilter = Everything; + type BaseCallFilter = InsideBoth; type BlockWeights = RuntimeBlockWeights; type BlockLength = RuntimeBlockLength; type DbWeight = RocksDbWeight; @@ -1682,8 +1742,8 @@ impl pallet_core_fellowship::Config for Runtime { type Balance = Balance; type ParamsOrigin = frame_system::EnsureRoot; type InductOrigin = pallet_core_fellowship::EnsureInducted; - type ApproveOrigin = frame_system::EnsureRootWithSuccess>; - type PromoteOrigin = frame_system::EnsureRootWithSuccess>; + type ApproveOrigin = EnsureRootWithSuccess>; + type PromoteOrigin = EnsureRootWithSuccess>; type EvidenceSize = ConstU32<16_384>; } @@ -2021,6 +2081,8 @@ construct_runtime!( FastUnstake: pallet_fast_unstake, MessageQueue: pallet_message_queue, Pov: frame_benchmarking_pallet_pov, + TxPause: pallet_tx_pause, + SafeMode: pallet_safe_mode, Statement: pallet_statement, Broker: pallet_broker, } @@ -2153,6 +2215,8 @@ mod benches { [pallet_utility, Utility] [pallet_vesting, Vesting] [pallet_whitelist, Whitelist] + [pallet_tx_pause, TxPause] + [pallet_safe_mode, SafeMode] ); } diff --git a/bin/node/testing/src/genesis.rs b/bin/node/testing/src/genesis.rs index 8063b8ef45708..6e7bcebfc00d1 100644 --- a/bin/node/testing/src/genesis.rs +++ b/bin/node/testing/src/genesis.rs @@ -101,6 +101,8 @@ pub fn config_endowed(code: Option<&[u8]>, extra_endowed: Vec) -> Run alliance: Default::default(), alliance_motion: Default::default(), nomination_pools: Default::default(), + safe_mode: Default::default(), + tx_pause: Default::default(), glutton: GluttonConfig { compute: Default::default(), storage: Default::default(), diff --git a/frame/safe-mode/Cargo.toml b/frame/safe-mode/Cargo.toml new file mode 100644 index 0000000000000..ca04a3e1b93d3 --- /dev/null +++ b/frame/safe-mode/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "pallet-safe-mode" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME safe-mode pallet" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +sp-arithmetic = { version = "16.0.0", default-features = false, path = "../../primitives/arithmetic" } +sp-runtime = { version = "24.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "8.0.0", default-features = false, path = "../../primitives/std" } +pallet-balances = { version = "4.0.0-dev", path = "../balances", default-features = false, optional = true } +pallet-utility = { version = "4.0.0-dev", path = "../utility", default-features = false, optional = true } +pallet-proxy = { version = "4.0.0-dev", path = "../proxy", default-features = false, optional = true } + +[dev-dependencies] +sp-core = { version = "21.0.0", path = "../../primitives/core" } +sp-io = { version = "23.0.0", path = "../../primitives/io" } +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-utility = { version = "4.0.0-dev", path = "../utility" } +pallet-proxy = { version = "4.0.0-dev", path = "../proxy" } + +[features] +default = [ "std" ] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "pallet-balances?/std", + "pallet-proxy?/std", + "pallet-utility?/std", + "scale-info/std", + "sp-arithmetic/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-proxy/runtime-benchmarks", + "pallet-utility/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances?/try-runtime", + "pallet-proxy?/try-runtime", + "pallet-utility?/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/frame/safe-mode/src/benchmarking.rs b/frame/safe-mode/src/benchmarking.rs new file mode 100644 index 0000000000000..566150e982afc --- /dev/null +++ b/frame/safe-mode/src/benchmarking.rs @@ -0,0 +1,236 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +#![cfg(feature = "runtime-benchmarks")] + +use super::{Pallet as SafeMode, *}; + +use frame_benchmarking::v2::*; +use frame_support::traits::{fungible::Mutate as FunMutate, UnfilteredDispatchable}; +use frame_system::{Pallet as System, RawOrigin}; +use sp_runtime::traits::{Bounded, One, Zero}; + +#[benchmarks(where T::Currency: FunMutate)] +mod benchmarks { + use super::*; + + /// `on_initialize` doing nothing. + #[benchmark] + fn on_initialize_noop() { + #[block] + { + SafeMode::::on_initialize(1u32.into()); + } + } + + /// `on_initialize` exiting since the until block is in the past. + #[benchmark] + fn on_initialize_exit() { + EnteredUntil::::put(&BlockNumberFor::::zero()); + assert!(SafeMode::::is_entered()); + + #[block] + { + SafeMode::::on_initialize(1u32.into()); + } + + assert!(!SafeMode::::is_entered()); + } + + /// Permissionless enter - if configured. + #[benchmark] + fn enter() -> Result<(), BenchmarkError> { + T::EnterDepositAmount::get().ok_or_else(|| BenchmarkError::Weightless)?; + + let caller: T::AccountId = whitelisted_caller(); + let origin = RawOrigin::Signed(caller.clone()); + T::Currency::set_balance(&caller, init_bal::()); + + #[extrinsic_call] + _(origin); + + assert_eq!( + EnteredUntil::::get().unwrap(), + System::::block_number() + T::EnterDuration::get() + ); + Ok(()) + } + + /// Forceful enter - if configured. + #[benchmark] + fn force_enter() -> Result<(), BenchmarkError> { + let force_origin = + T::ForceEnterOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + + let duration = T::ForceEnterOrigin::ensure_origin(force_origin.clone()).unwrap(); + + #[extrinsic_call] + _(force_origin as T::RuntimeOrigin); + + assert_eq!(EnteredUntil::::get().unwrap(), System::::block_number() + duration); + Ok(()) + } + + /// Permissionless extend - if configured. + #[benchmark] + fn extend() -> Result<(), BenchmarkError> { + T::ExtendDepositAmount::get().ok_or_else(|| BenchmarkError::Weightless)?; + + let alice: T::AccountId = whitelisted_caller(); + T::Currency::set_balance(&alice, init_bal::()); + + System::::set_block_number(1u32.into()); + assert!(SafeMode::::do_enter(None, 1u32.into()).is_ok()); + + #[extrinsic_call] + _(RawOrigin::Signed(alice)); + + assert_eq!( + EnteredUntil::::get().unwrap(), + System::::block_number() + 1u32.into() + T::ExtendDuration::get() + ); + Ok(()) + } + + /// Forceful extend - if configured. + #[benchmark] + fn force_extend() -> Result<(), BenchmarkError> { + let force_origin = T::ForceExtendOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + System::::set_block_number(1u32.into()); + assert!(SafeMode::::do_enter(None, 1u32.into()).is_ok()); + + let duration = T::ForceExtendOrigin::ensure_origin(force_origin.clone()).unwrap(); + let call = Call::::force_extend {}; + + #[block] + { + call.dispatch_bypass_filter(force_origin)?; + } + + assert_eq!( + EnteredUntil::::get().unwrap(), + System::::block_number() + 1u32.into() + duration + ); + Ok(()) + } + + /// Forceful exit - if configured. + #[benchmark] + fn force_exit() -> Result<(), BenchmarkError> { + let force_origin = + T::ForceExitOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + assert!(SafeMode::::do_enter(None, 1u32.into()).is_ok()); + + #[extrinsic_call] + _(force_origin as T::RuntimeOrigin); + + assert_eq!(EnteredUntil::::get(), None); + Ok(()) + } + + /// Permissionless release of a deposit - if configured. + #[benchmark] + fn release_deposit() -> Result<(), BenchmarkError> { + let delay = T::ReleaseDelay::get().ok_or_else(|| BenchmarkError::Weightless)?; + + let alice: T::AccountId = whitelisted_caller(); + let origin = RawOrigin::Signed(alice.clone()); + + T::Currency::set_balance(&alice, init_bal::()); + // Mock the storage. This is needed in case the `EnterDepositAmount` is zero. + let block: BlockNumberFor = 1u32.into(); + let bal: BalanceOf = 1u32.into(); + Deposits::::insert(&alice, &block, &bal); + T::Currency::hold(&HoldReason::EnterOrExtend.into(), &alice, bal)?; + EnteredUntil::::put(&block); + assert!(SafeMode::::do_exit(ExitReason::Force).is_ok()); + + System::::set_block_number(delay + One::one() + 2u32.into()); + System::::on_initialize(System::::block_number()); + SafeMode::::on_initialize(System::::block_number()); + + #[extrinsic_call] + _(origin, alice.clone(), 1u32.into()); + + assert!(!Deposits::::contains_key(&alice, &block)); + assert_eq!(T::Currency::balance(&alice), init_bal::()); + Ok(()) + } + + /// Forceful release of a deposit - if configured. + #[benchmark] + fn force_release_deposit() -> Result<(), BenchmarkError> { + let force_origin = T::ForceDepositOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + let alice: T::AccountId = whitelisted_caller(); + T::Currency::set_balance(&alice, init_bal::()); + + // Mock the storage. This is needed in case the `EnterDepositAmount` is zero. + let block: BlockNumberFor = 1u32.into(); + let bal: BalanceOf = 1u32.into(); + Deposits::::insert(&alice, &block, &bal); + T::Currency::hold(&&HoldReason::EnterOrExtend.into(), &alice, bal)?; + EnteredUntil::::put(&block); + + assert_eq!(T::Currency::balance(&alice), init_bal::() - 1u32.into()); + assert!(SafeMode::::do_exit(ExitReason::Force).is_ok()); + + System::::set_block_number(System::::block_number() + One::one()); + System::::on_initialize(System::::block_number()); + SafeMode::::on_initialize(System::::block_number()); + + #[extrinsic_call] + _(force_origin as T::RuntimeOrigin, alice.clone(), block); + + assert!(!Deposits::::contains_key(&alice, block)); + assert_eq!(T::Currency::balance(&alice), init_bal::()); + Ok(()) + } + + #[benchmark] + fn force_slash_deposit() -> Result<(), BenchmarkError> { + let force_origin = T::ForceDepositOrigin::try_successful_origin() + .map_err(|_| BenchmarkError::Weightless)?; + + let alice: T::AccountId = whitelisted_caller(); + T::Currency::set_balance(&alice, init_bal::()); + + // Mock the storage. This is needed in case the `EnterDepositAmount` is zero. + let block: BlockNumberFor = 1u32.into(); + let bal: BalanceOf = 1u32.into(); + Deposits::::insert(&alice, &block, &bal); + T::Currency::hold(&&HoldReason::EnterOrExtend.into(), &alice, bal)?; + EnteredUntil::::put(&block); + assert!(SafeMode::::do_exit(ExitReason::Force).is_ok()); + + #[extrinsic_call] + _(force_origin as T::RuntimeOrigin, alice.clone(), block); + + assert!(!Deposits::::contains_key(&alice, block)); + assert_eq!(T::Currency::balance(&alice), init_bal::() - 1u32.into()); + Ok(()) + } + + fn init_bal() -> BalanceOf { + BalanceOf::::max_value() / 10u32.into() + } + + impl_benchmark_test_suite!(SafeMode, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/frame/safe-mode/src/lib.rs b/frame/safe-mode/src/lib.rs new file mode 100644 index 0000000000000..ff045b964afbb --- /dev/null +++ b/frame/safe-mode/src/lib.rs @@ -0,0 +1,596 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +#![cfg_attr(not(feature = "std"), no_std)] +#![deny(rustdoc::broken_intra_doc_links)] + +mod benchmarking; +pub mod mock; +mod tests; +pub mod weights; + +use frame_support::{ + defensive_assert, + pallet_prelude::*, + traits::{ + fungible::{ + hold::{Inspect as FunHoldInspect, Mutate as FunHoldMutate}, + Inspect as FunInspect, + }, + tokens::{Fortitude, Precision}, + CallMetadata, Contains, Defensive, GetCallMetadata, PalletInfoAccess, SafeModeNotify, + }, + weights::Weight, + DefaultNoBound, +}; +use frame_system::pallet_prelude::*; +use sp_arithmetic::traits::Zero; +use sp_runtime::traits::Saturating; +use sp_std::{convert::TryInto, prelude::*}; + +pub use pallet::*; +pub use weights::*; + +type BalanceOf = + <::Currency as FunInspect<::AccountId>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Currency type for this pallet, used for Deposits. + type Currency: FunHoldInspect + + FunHoldMutate; + + /// The hold reason when reserving funds for entering or extending the safe-mode. + type RuntimeHoldReason: From; + + /// Contains all runtime calls in any pallet that can be dispatched even while the safe-mode + /// is entered. + /// + /// The safe-mode pallet cannot disable it's own calls, and does not need to be explicitly + /// added here. + type WhitelistedCalls: Contains; + + /// For how many blocks the safe-mode will be entered by [`Pallet::enter`]. + #[pallet::constant] + type EnterDuration: Get>; + + /// For how many blocks the safe-mode can be extended by each [`Pallet::extend`] call. + /// + /// This does not impose a hard limit as the safe-mode can be extended multiple times. + #[pallet::constant] + type ExtendDuration: Get>; + + /// The amount that will be reserved upon calling [`Pallet::enter`]. + /// + /// `None` disallows permissionlessly enabling the safe-mode and is a sane default. + #[pallet::constant] + type EnterDepositAmount: Get>>; + + /// The amount that will be reserved upon calling [`Pallet::extend`]. + /// + /// `None` disallows permissionlessly extending the safe-mode and is a sane default. + #[pallet::constant] + type ExtendDepositAmount: Get>>; + + /// The origin that may call [`Pallet::force_enter`]. + /// + /// The `Success` value is the number of blocks that this origin can enter safe-mode for. + type ForceEnterOrigin: EnsureOrigin>; + + /// The origin that may call [`Pallet::force_extend`]. + /// + /// The `Success` value is the number of blocks that this origin can extend the safe-mode. + type ForceExtendOrigin: EnsureOrigin>; + + /// The origin that may call [`Pallet::force_enter`]. + type ForceExitOrigin: EnsureOrigin; + + /// The only origin that can force to release or slash a deposit. + type ForceDepositOrigin: EnsureOrigin; + + /// Notifies external logic when the safe-mode is being entered or exited. + type Notify: SafeModeNotify; + + /// The minimal duration a deposit will remain reserved after safe-mode is entered or + /// extended, unless [`Pallet::force_release_deposit`] is successfully called sooner. + /// + /// Every deposit is tied to a specific activation or extension, thus each deposit can be + /// released independently after the delay for it has passed. + /// + /// `None` disallows permissionlessly releasing the safe-mode deposits and is a sane + /// default. + #[pallet::constant] + type ReleaseDelay: Get>>; + + // Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::error] + pub enum Error { + /// The safe-mode is (already or still) entered. + Entered, + + /// The safe-mode is (already or still) exited. + Exited, + + /// This functionality of the pallet is disabled by the configuration. + NotConfigured, + + /// There is no balance reserved. + NoDeposit, + + /// The account already has a deposit reserved and can therefore not enter or extend again. + AlreadyDeposited, + + /// This deposit cannot be released yet. + CannotReleaseYet, + + /// An error from the underlying `Currency`. + CurrencyError, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The safe-mode was entered until inclusively this block. + Entered { until: BlockNumberFor }, + + /// The safe-mode was extended until inclusively this block. + Extended { until: BlockNumberFor }, + + /// Exited the safe-mode for a specific reason. + Exited { reason: ExitReason }, + + /// An account reserved funds for either entering or extending the safe-mode. + DepositPlaced { account: T::AccountId, amount: BalanceOf }, + + /// An account had a reserve released that was reserved. + DepositReleased { account: T::AccountId, amount: BalanceOf }, + + /// An account had reserve slashed that was reserved. + DepositSlashed { account: T::AccountId, amount: BalanceOf }, + + /// Could not hold funds for entering or extending the safe-mode. + /// + /// This error comes from the underlying `Currency`. + CannotDeposit, + + /// Could not release funds for entering or extending the safe-mode. + /// + /// This error comes from the underlying `Currency`. + CannotRelease, + } + + /// The reason why the safe-mode was deactivated. + #[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] + pub enum ExitReason { + /// The safe-mode was automatically deactivated after it's duration ran out. + Timeout, + + /// The safe-mode was forcefully deactivated by [`Pallet::force_exit`]. + Force, + } + + /// Contains the last block number that the safe-mode will remain entered in. + /// + /// Set to `None` when safe-mode is exited. + /// + /// Safe-mode is automatically exited when the current block number exceeds this value. + #[pallet::storage] + pub type EnteredUntil = StorageValue<_, BlockNumberFor, OptionQuery>; + + /// Holds the reserve that was taken from an account at a specific block number. + /// + /// This helps governance to have an overview of outstanding deposits that should be returned or + /// slashed. + #[pallet::storage] + pub type Deposits = StorageDoubleMap< + _, + Twox64Concat, + T::AccountId, + Twox64Concat, + BlockNumberFor, + BalanceOf, + OptionQuery, + >; + + /// Configure the initial state of this pallet in the genesis block. + #[pallet::genesis_config] + #[derive(DefaultNoBound)] + pub struct GenesisConfig { + pub entered_until: Option>, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + if let Some(block) = self.entered_until { + EnteredUntil::::put(block); + } + } + } + + /// A reason for the pallet placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// Funds are held for entering or extending the safe-mode. + #[codec(index = 0)] + EnterOrExtend, + } + + #[pallet::call] + impl Pallet { + /// Enter safe-mode permissionlessly for [`Config::EnterDuration`] blocks. + /// + /// Reserves [`Config::EnterDepositAmount`] from the caller's account. + /// Emits an [`Event::Entered`] event on success. + /// Errors with [`Error::Entered`] if the safe-mode is already entered. + /// Errors with [`Error::NotConfigured`] if the deposit amount is `None`. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::enter())] + pub fn enter(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_enter(Some(who), T::EnterDuration::get()).map_err(Into::into) + } + + /// Enter safe-mode by force for a per-origin configured number of blocks. + /// + /// Emits an [`Event::Entered`] event on success. + /// Errors with [`Error::Entered`] if the safe-mode is already entered. + /// + /// Can only be called by the [`Config::ForceEnterOrigin`] origin. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::force_enter())] + pub fn force_enter(origin: OriginFor) -> DispatchResult { + let duration = T::ForceEnterOrigin::ensure_origin(origin)?; + + Self::do_enter(None, duration).map_err(Into::into) + } + + /// Extend the safe-mode permissionlessly for [`Config::ExtendDuration`] blocks. + /// + /// This accumulates on top of the current remaining duration. + /// Reserves [`Config::ExtendDepositAmount`] from the caller's account. + /// Emits an [`Event::Extended`] event on success. + /// Errors with [`Error::Exited`] if the safe-mode is entered. + /// Errors with [`Error::NotConfigured`] if the deposit amount is `None`. + /// + /// This may be called by any signed origin with [`Config::ExtendDepositAmount`] free + /// currency to reserve. This call can be disabled for all origins by configuring + /// [`Config::ExtendDepositAmount`] to `None`. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::extend())] + pub fn extend(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::do_extend(Some(who), T::ExtendDuration::get()).map_err(Into::into) + } + + /// Extend the safe-mode by force for a per-origin configured number of blocks. + /// + /// Emits an [`Event::Extended`] event on success. + /// Errors with [`Error::Exited`] if the safe-mode is inactive. + /// + /// Can only be called by the [`Config::ForceExtendOrigin`] origin. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::force_extend())] + pub fn force_extend(origin: OriginFor) -> DispatchResult { + let duration = T::ForceExtendOrigin::ensure_origin(origin)?; + + Self::do_extend(None, duration).map_err(Into::into) + } + + /// Exit safe-mode by force. + /// + /// Emits an [`Event::Exited`] with [`ExitReason::Force`] event on success. + /// Errors with [`Error::Exited`] if the safe-mode is inactive. + /// + /// Note: `safe-mode` will be automatically deactivated by [`Pallet::on_initialize`] hook + /// after the block height is greater than the [`EnteredUntil`] storage item. + /// Emits an [`Event::Exited`] with [`ExitReason::Timeout`] event when deactivated in the + /// hook. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::force_exit())] + pub fn force_exit(origin: OriginFor) -> DispatchResult { + T::ForceExitOrigin::ensure_origin(origin)?; + + Self::do_exit(ExitReason::Force).map_err(Into::into) + } + + /// Slash a deposit for an account that entered or extended safe-mode at a given + /// historical block. + /// + /// This can only be called while safe-mode is entered. + /// + /// Emits a [`Event::DepositSlashed`] event on success. + /// Errors with [`Error::Entered`] if safe-mode is entered. + /// + /// Can only be called by the [`Config::ForceDepositOrigin`] origin. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::force_slash_deposit())] + pub fn force_slash_deposit( + origin: OriginFor, + account: T::AccountId, + block: BlockNumberFor, + ) -> DispatchResult { + T::ForceDepositOrigin::ensure_origin(origin)?; + + Self::do_force_deposit(account, block).map_err(Into::into) + } + + /// Permissionlessly release a deposit for an account that entered safe-mode at a + /// given historical block. + /// + /// The call can be completely disabled by setting [`Config::ReleaseDelay`] to `None`. + /// This cannot be called while safe-mode is entered and not until + /// [`Config::ReleaseDelay`] blocks have passed since safe-mode was entered. + /// + /// Emits a [`Event::DepositReleased`] event on success. + /// Errors with [`Error::Entered`] if the safe-mode is entered. + /// Errors with [`Error::CannotReleaseYet`] if [`Config::ReleaseDelay`] block have not + /// passed since safe-mode was entered. Errors with [`Error::NoDeposit`] if the payee has no + /// reserved currency at the block specified. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::release_deposit())] + pub fn release_deposit( + origin: OriginFor, + account: T::AccountId, + block: BlockNumberFor, + ) -> DispatchResult { + ensure_signed(origin)?; + + Self::do_release(false, account, block).map_err(Into::into) + } + + /// Force to release a deposit for an account that entered safe-mode at a given + /// historical block. + /// + /// This can be called while safe-mode is still entered. + /// + /// Emits a [`Event::DepositReleased`] event on success. + /// Errors with [`Error::Entered`] if safe-mode is entered. + /// Errors with [`Error::NoDeposit`] if the payee has no reserved currency at the + /// specified block. + /// + /// Can only be called by the [`Config::ForceDepositOrigin`] origin. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::force_release_deposit())] + pub fn force_release_deposit( + origin: OriginFor, + account: T::AccountId, + block: BlockNumberFor, + ) -> DispatchResult { + T::ForceDepositOrigin::ensure_origin(origin)?; + + Self::do_release(true, account, block).map_err(Into::into) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + /// Automatically exits safe-mode when the current block number is greater than + /// [`EnteredUntil`]. + fn on_initialize(current: BlockNumberFor) -> Weight { + let Some(limit) = EnteredUntil::::get() else { + return T::WeightInfo::on_initialize_noop(); + }; + + if current > limit { + let _ = Self::do_exit(ExitReason::Timeout).defensive_proof("Only Errors if safe-mode is not entered. Safe-mode has already been checked to be entered; qed"); + T::WeightInfo::on_initialize_exit() + } else { + T::WeightInfo::on_initialize_noop() + } + } + } +} + +impl Pallet { + /// Logic for the [`crate::Pallet::enter`] and [`crate::Pallet::force_enter`] calls. + pub(crate) fn do_enter( + who: Option, + duration: BlockNumberFor, + ) -> Result<(), Error> { + ensure!(!Self::is_entered(), Error::::Entered); + + if let Some(who) = who { + let amount = T::EnterDepositAmount::get().ok_or(Error::::NotConfigured)?; + Self::hold(who, amount)?; + } + + let until = >::block_number().saturating_add(duration); + EnteredUntil::::put(until); + Self::deposit_event(Event::Entered { until }); + T::Notify::entered(); + Ok(()) + } + + /// Logic for the [`crate::Pallet::extend`] and [`crate::Pallet::force_extend`] calls. + pub(crate) fn do_extend( + who: Option, + duration: BlockNumberFor, + ) -> Result<(), Error> { + let mut until = EnteredUntil::::get().ok_or(Error::::Exited)?; + + if let Some(who) = who { + let amount = T::ExtendDepositAmount::get().ok_or(Error::::NotConfigured)?; + Self::hold(who, amount)?; + } + + until.saturating_accrue(duration); + EnteredUntil::::put(until); + Self::deposit_event(Event::::Extended { until }); + Ok(()) + } + + /// Logic for the [`crate::Pallet::force_exit`] call. + /// + /// Errors if safe-mode is already exited. + pub(crate) fn do_exit(reason: ExitReason) -> Result<(), Error> { + let _until = EnteredUntil::::take().ok_or(Error::::Exited)?; + Self::deposit_event(Event::Exited { reason }); + T::Notify::exited(); + Ok(()) + } + + /// Logic for the [`crate::Pallet::release_deposit`] and + /// [`crate::Pallet::force_release_deposit`] calls. + pub(crate) fn do_release( + force: bool, + account: T::AccountId, + block: BlockNumberFor, + ) -> Result<(), Error> { + let amount = Deposits::::take(&account, &block).ok_or(Error::::NoDeposit)?; + + if !force { + ensure!(!Self::is_entered(), Error::::Entered); + + let delay = T::ReleaseDelay::get().ok_or(Error::::NotConfigured)?; + let now = >::block_number(); + ensure!(now > block.saturating_add(delay), Error::::CannotReleaseYet); + } + + let amount = T::Currency::release( + &&HoldReason::EnterOrExtend.into(), + &account, + amount, + Precision::BestEffort, + ) + .map_err(|_| Error::::CurrencyError)?; + Self::deposit_event(Event::::DepositReleased { account, amount }); + Ok(()) + } + + /// Logic for the [`crate::Pallet::slash_deposit`] call. + pub(crate) fn do_force_deposit( + account: T::AccountId, + block: BlockNumberFor, + ) -> Result<(), Error> { + let amount = Deposits::::take(&account, block).ok_or(Error::::NoDeposit)?; + + let burned = T::Currency::burn_held( + &&HoldReason::EnterOrExtend.into(), + &account, + amount, + Precision::BestEffort, + Fortitude::Force, + ) + .map_err(|_| Error::::CurrencyError)?; + defensive_assert!(burned == amount, "Could not burn the full held amount"); + Self::deposit_event(Event::::DepositSlashed { account, amount }); + Ok(()) + } + + /// Place a hold for exactly `amount` and store it in `Deposits`. + /// + /// Errors if the account already has a hold for the same reason. + fn hold(who: T::AccountId, amount: BalanceOf) -> Result<(), Error> { + let block = >::block_number(); + if !T::Currency::balance_on_hold(&HoldReason::EnterOrExtend.into(), &who).is_zero() { + return Err(Error::::AlreadyDeposited.into()) + } + + T::Currency::hold(&HoldReason::EnterOrExtend.into(), &who, amount) + .map_err(|_| Error::::CurrencyError)?; + Deposits::::insert(&who, block, amount); + Self::deposit_event(Event::::DepositPlaced { account: who, amount }); + + Ok(()) + } + + /// Return whether `safe-mode` is entered. + pub fn is_entered() -> bool { + EnteredUntil::::exists() + } + + /// Return whether the given call is allowed to be dispatched. + pub fn is_allowed(call: &T::RuntimeCall) -> bool + where + T::RuntimeCall: GetCallMetadata, + { + let CallMetadata { pallet_name, .. } = call.get_call_metadata(); + // SAFETY: The `SafeMode` pallet is always allowed. + if pallet_name == as PalletInfoAccess>::name() { + return true + } + + if Self::is_entered() { + T::WhitelistedCalls::contains(call) + } else { + true + } + } +} + +impl Contains for Pallet +where + T::RuntimeCall: GetCallMetadata, +{ + /// Return whether the given call is allowed to be dispatched. + fn contains(call: &T::RuntimeCall) -> bool { + Pallet::::is_allowed(call) + } +} + +impl frame_support::traits::SafeMode for Pallet { + type BlockNumber = BlockNumberFor; + + fn is_entered() -> bool { + Self::is_entered() + } + + fn remaining() -> Option> { + EnteredUntil::::get().map(|until| { + let now = >::block_number(); + until.saturating_sub(now) + }) + } + + fn enter(duration: BlockNumberFor) -> Result<(), frame_support::traits::SafeModeError> { + Self::do_enter(None, duration).map_err(Into::into) + } + + fn extend(duration: BlockNumberFor) -> Result<(), frame_support::traits::SafeModeError> { + Self::do_extend(None, duration).map_err(Into::into) + } + + fn exit() -> Result<(), frame_support::traits::SafeModeError> { + Self::do_exit(ExitReason::Force).map_err(Into::into) + } +} + +impl From> for frame_support::traits::SafeModeError { + fn from(err: Error) -> Self { + match err { + Error::::Entered => Self::AlreadyEntered, + Error::::Exited => Self::AlreadyExited, + _ => Self::Unknown, + } + } +} diff --git a/frame/safe-mode/src/mock.rs b/frame/safe-mode/src/mock.rs new file mode 100644 index 0000000000000..337b6076f84b9 --- /dev/null +++ b/frame/safe-mode/src/mock.rs @@ -0,0 +1,270 @@ +// This file is part of Substrate. + +// Copyright (C) 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 and test utilities for safe mode pallet. + +#![cfg(test)] + +use super::*; +use crate as pallet_safe_mode; + +use frame_support::{ + parameter_types, + traits::{ConstU64, Everything, InsideBoth, InstanceFilter, IsInVec, SafeModeNotify}, +}; +use frame_system::EnsureSignedBy; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +impl frame_system::Config for Test { + type BaseCallFilter = InsideBoth; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +/// Identifies a hold on an account's balance. +#[derive( + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, Debug, TypeInfo, +)] +pub enum HoldReason { + /// The safe-mode pallet holds funds since an account either entered or extended the safe-mode. + SafeMode, +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU64<2>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + type MaxReserves = ConstU32<10>; + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = RuntimeHoldReason; + type FreezeIdentifier = (); + type MaxHolds = ConstU32<10>; + type MaxFreezes = ConstU32<0>; +} + +impl pallet_utility::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +/// Mocked proxies to check that the safe-mode also works with the proxy pallet. +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + RuntimeDebug, + MaxEncodedLen, + scale_info::TypeInfo, +)] +pub enum ProxyType { + Any, + JustTransfer, + JustUtility, +} + +impl Default for ProxyType { + fn default() -> Self { + Self::Any + } +} + +impl InstanceFilter for ProxyType { + fn filter(&self, c: &RuntimeCall) -> bool { + match self { + ProxyType::Any => true, + ProxyType::JustTransfer => { + matches!(c, RuntimeCall::Balances(pallet_balances::Call::transfer { .. })) + }, + ProxyType::JustUtility => matches!(c, RuntimeCall::Utility { .. }), + } + } + fn is_superset(&self, o: &Self) -> bool { + self == &ProxyType::Any || self == o + } +} + +impl pallet_proxy::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = ProxyType; + type ProxyDepositBase = ConstU64<1>; + type ProxyDepositFactor = ConstU64<1>; + type MaxProxies = ConstU32<4>; + type WeightInfo = (); + type CallHasher = BlakeTwo256; + type MaxPending = ConstU32<2>; + type AnnouncementDepositBase = ConstU64<1>; + type AnnouncementDepositFactor = ConstU64<1>; +} + +/// The calls that can always bypass safe-mode. +pub struct WhitelistedCalls; +impl Contains for WhitelistedCalls { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::Balances(_) => false, + _ => true, + } + } +} + +parameter_types! { + pub const EnterDuration: u64 = 7; + pub const ExtendDuration: u64 = 10; + pub const EnterDepositAmount: u64 = 100; + pub const ExtendDepositAmount: u64 = 100; + pub const ReleaseDelay: u64 = 20; + pub const SafeModeHoldReason: HoldReason = HoldReason::SafeMode; + + pub const ForceEnterWeak: u64 = 3; + pub const ForceEnterStrong: u64 = 5; + + pub const ForceExtendWeak: u64 = 11; + pub const ForceExtendStrong: u64 = 15; + + // NOTE: The account ID maps to the duration. Easy for testing. + pub ForceEnterOrigins: Vec = vec![ForceEnterWeak::get(), ForceEnterStrong::get()]; + pub ForceExtendOrigins: Vec = vec![ForceExtendWeak::get(), ForceExtendStrong::get()]; + + pub storage Notifications: Vec<(u64, bool)> = vec![]; +} + +pub struct MockedNotify; +impl SafeModeNotify for MockedNotify { + fn entered() { + let mut ns = Notifications::get(); + ns.push((>::block_number(), true)); + Notifications::set(&ns); + } + + fn exited() { + let mut ns = Notifications::get(); + ns.push((>::block_number(), false)); + Notifications::set(&ns); + } +} + +frame_support::ord_parameter_types! { + pub const ForceExitOrigin: u64 = 100; + pub const ForceDepositOrigin: u64 = 200; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type WhitelistedCalls = WhitelistedCalls; + type EnterDuration = EnterDuration; + type EnterDepositAmount = EnterDepositAmount; + type ExtendDuration = ExtendDuration; + type ExtendDepositAmount = ExtendDepositAmount; + type ForceEnterOrigin = EnsureSignedBy, u64>; + type ForceExtendOrigin = EnsureSignedBy, u64>; + type ForceExitOrigin = EnsureSignedBy; + type ForceDepositOrigin = EnsureSignedBy; + type ReleaseDelay = ReleaseDelay; + type Notify = MockedNotify; + type WeightInfo = (); +} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Utility: pallet_utility, + Proxy: pallet_proxy, + SafeMode: pallet_safe_mode, + } +); + +pub const BAL_ACC0: u64 = 1234; +pub const BAL_ACC1: u64 = 5678; + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + // The 0 account is NOT a special origin, the rest may be. + balances: vec![(0, BAL_ACC0), (1, BAL_ACC1), (2, 5678), (3, 5678), (4, 5678)], + } + .assimilate_storage(&mut t) + .unwrap(); + pallet_safe_mode::GenesisConfig:: { entered_until: None } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext +} + +pub fn next_block() { + SafeMode::on_finalize(System::block_number()); + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + SafeMode::on_initialize(System::block_number()); +} + +pub fn run_to(n: u64) { + while System::block_number() < n { + next_block(); + } +} diff --git a/frame/safe-mode/src/tests.rs b/frame/safe-mode/src/tests.rs new file mode 100644 index 0000000000000..4ce9922d3b65c --- /dev/null +++ b/frame/safe-mode/src/tests.rs @@ -0,0 +1,613 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities for the safe mode pallet. + +#![cfg(test)] + +use super::*; +use crate::mock::{RuntimeCall, *}; + +use frame_support::{assert_err, assert_noop, assert_ok, dispatch::Dispatchable, traits::Currency}; +use sp_runtime::TransactionOutcome; + +/// Do something hypothetically by rolling back any changes afterwards. +/// +/// Returns the original result of the closure. +macro_rules! hypothetically { + ( $e:expr ) => { + frame_support::storage::transactional::with_transaction( + || -> TransactionOutcome> { + sp_runtime::TransactionOutcome::Rollback(Ok($e)) + }, + ) + .expect("Always returning Ok; qed") + }; +} + +/// Assert something to be [*hypothetically*] `Ok` without actually committing it. +/// +/// Reverts any storage changes made by the closure. +macro_rules! hypothetically_ok { + ($e:expr $(, $args:expr)* $(,)?) => { + let result = hypothetically!($e); + assert_ok!(result $(, $args)*); + }; +} + +#[test] +fn fails_to_filter_calls_to_safe_mode_pallet() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + let activated_at_block = System::block_number(); + + assert_err!( + call_transfer().dispatch(RuntimeOrigin::signed(0)), + frame_system::Error::::CallFiltered + ); + + next_block(); + assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1))); + assert_ok!(SafeMode::force_extend(signed(ForceExtendStrong::get()))); + assert_err!( + call_transfer().dispatch(RuntimeOrigin::signed(0)), + frame_system::Error::::CallFiltered + ); + assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get()))); + assert_ok!(SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + )); + + next_block(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_err!( + call_transfer().dispatch(RuntimeOrigin::signed(0)), + frame_system::Error::::CallFiltered + ); + assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get()))); + assert_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + 2 + )); + }); +} + +#[test] +fn fails_to_activate_if_activated() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_noop!(SafeMode::enter(RuntimeOrigin::signed(2)), Error::::Entered); + }); +} + +#[test] +fn fails_to_extend_if_not_activated() { + new_test_ext().execute_with(|| { + assert_eq!(EnteredUntil::::get(), None); + assert_noop!(SafeMode::extend(RuntimeOrigin::signed(2)), Error::::Exited); + }); +} + +#[test] +fn fails_to_force_release_deposits_with_wrong_block() { + new_test_ext().execute_with(|| { + let activated_at_block = System::block_number(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + run_to(mock::EnterDuration::get() + activated_at_block + 1); + + assert_err!( + SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + 1 + ), + Error::::NoDeposit + ); + + assert_err!( + SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + 1 + ), + Error::::NoDeposit + ); + }); +} + +#[test] +fn fails_to_release_deposits_too_early() { + new_test_ext().execute_with(|| { + let activated_at_block = System::block_number(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get()))); + assert_err!( + SafeMode::release_deposit(RuntimeOrigin::signed(2), 0, activated_at_block), + Error::::CannotReleaseYet + ); + run_to(activated_at_block + mock::ReleaseDelay::get() + 1); + assert_ok!(SafeMode::release_deposit(RuntimeOrigin::signed(2), 0, activated_at_block)); + }); +} + +// GENERAL SUCCESS/POSITIVE TESTS --------------------- + +#[test] +fn can_automatically_deactivate_after_timeout() { + new_test_ext().execute_with(|| { + let activated_at_block = System::block_number(); + assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get()))); + run_to(1 + activated_at_block + ForceEnterWeak::get()); + + assert_eq!(EnteredUntil::::get(), None); + }); +} + +#[test] +fn can_filter_balance_calls_when_activated() { + new_test_ext().execute_with(|| { + assert_ok!(call_transfer().dispatch(RuntimeOrigin::signed(0))); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_err!( + call_transfer().dispatch(RuntimeOrigin::signed(0)), + frame_system::Error::::CallFiltered + ); + }); +} + +#[test] +fn can_filter_balance_in_batch_when_activated() { + new_test_ext().execute_with(|| { + let batch_call = + RuntimeCall::Utility(pallet_utility::Call::batch { calls: vec![call_transfer()] }); + + assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0))); + + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + + assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0))); + System::assert_last_event( + pallet_utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + }); +} + +#[test] +fn can_filter_balance_in_proxy_when_activated() { + new_test_ext().execute_with(|| { + assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(1), 2, ProxyType::JustTransfer, 0)); + + assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, Box::new(call_transfer()))); + System::assert_last_event(pallet_proxy::Event::ProxyExecuted { result: Ok(()) }.into()); + + assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get()))); + + assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, Box::new(call_transfer()))); + System::assert_last_event( + pallet_proxy::Event::ProxyExecuted { + result: DispatchError::from(frame_system::Error::::CallFiltered).into(), + } + .into(), + ); + }); +} + +#[test] +fn can_activate() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_eq!( + EnteredUntil::::get().unwrap(), + System::block_number() + mock::EnterDuration::get() + ); + assert_eq!(Balances::reserved_balance(0), mock::EnterDepositAmount::get()); + assert_eq!(Notifications::get(), vec![(1, true)]); + assert_noop!(SafeMode::enter(RuntimeOrigin::signed(0)), Error::::Entered); + assert_eq!(Notifications::get(), vec![(1, true)]); + // Assert the deposit. + assert_eq!(Deposits::::get(0, 1), Some(mock::EnterDepositAmount::get())); + }); +} + +#[test] +fn notify_works() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_eq!(Notifications::get(), vec![(1, true)]); + run_to(10); + assert_eq!(Notifications::get(), vec![(1, true), (9, false)]); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(1))); + assert_ok!(SafeMode::extend(RuntimeOrigin::signed(2))); + run_to(30); + assert_eq!(Notifications::get(), vec![(1, true), (9, false), (10, true), (28, false)]); + }); +} + +#[test] +fn cannot_extend() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_err!(SafeMode::extend(RuntimeOrigin::signed(0)), Error::::AlreadyDeposited); + assert_eq!( + EnteredUntil::::get().unwrap(), + System::block_number() + mock::EnterDuration::get() + ); + assert_eq!(Balances::reserved_balance(0), mock::EnterDepositAmount::get()); + assert_eq!(Notifications::get(), vec![(1, true)]); + }); +} + +#[test] +fn fails_signed_origin_when_explicit_origin_required() { + new_test_ext().execute_with(|| { + assert_eq!(EnteredUntil::::get(), None); + let activated_at_block = System::block_number(); + + assert_err!(SafeMode::force_enter(RuntimeOrigin::signed(0)), DispatchError::BadOrigin); + assert_err!(SafeMode::force_extend(RuntimeOrigin::signed(0)), DispatchError::BadOrigin); + assert_err!(SafeMode::force_exit(RuntimeOrigin::signed(0)), DispatchError::BadOrigin); + assert_err!( + SafeMode::force_slash_deposit(RuntimeOrigin::signed(0), 0, activated_at_block), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_release_deposit(RuntimeOrigin::signed(0), 0, activated_at_block), + DispatchError::BadOrigin + ); + }); +} + +// CONFIGURED ORIGIN CALL TESTS --------------------- + +#[test] +fn fails_force_deactivate_if_not_activated() { + new_test_ext().execute_with(|| { + assert_noop!( + SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())), + Error::::Exited + ); + assert_noop!( + SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceExitOrigin::get())), + Error::::Exited + ); + assert!(Notifications::get().is_empty()); + }); +} + +#[test] +fn can_force_activate_with_config_origin() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::force_enter(signed(ForceEnterStrong::get()))); + assert_eq!(Notifications::get(), vec![(1, true)]); + assert_eq!( + EnteredUntil::::get().unwrap(), + System::block_number() + ForceEnterStrong::get() + ); + assert_noop!( + SafeMode::force_enter(signed(ForceEnterStrong::get())), + Error::::Entered + ); + assert_eq!(Notifications::get(), vec![(1, true)]); + }); +} + +#[test] +fn can_force_deactivate_with_config_origin() { + new_test_ext().execute_with(|| { + assert_eq!(EnteredUntil::::get(), None); + assert_err!( + SafeMode::force_exit(RuntimeOrigin::signed(ForceExitOrigin::get())), + Error::::Exited + ); + assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get()))); + assert_ok!(SafeMode::force_exit(RuntimeOrigin::signed(ForceExitOrigin::get()))); + assert_eq!(Notifications::get(), vec![(1, true), (1, false)]); + }); +} + +#[test] +fn can_force_extend_with_config_origin() { + new_test_ext().execute_with(|| { + // Activated by `Weak` and extended by `Medium`. + assert_ok!(SafeMode::force_enter(signed(ForceEnterWeak::get()))); + assert_eq!( + EnteredUntil::::get().unwrap(), + System::block_number() + ForceEnterWeak::get() + ); + assert_ok!(SafeMode::force_extend(signed(ForceExtendWeak::get()))); + assert_eq!( + EnteredUntil::::get().unwrap(), + System::block_number() + ForceEnterWeak::get() + ForceExtendWeak::get() + ); + }); +} + +#[test] +fn can_force_release_deposit_with_config_origin() { + new_test_ext().execute_with(|| { + let activated_at_block = System::block_number(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + hypothetically_ok!(SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + ),); + run_to(mock::EnterDuration::get() + activated_at_block + 1); + + assert_ok!(SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + )); + assert_eq!(Balances::free_balance(&0), BAL_ACC0); // accounts set in mock genesis + + Balances::make_free_balance_be(&0, BAL_ACC0); + let activated_and_extended_at_block = System::block_number(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1))); + run_to( + mock::EnterDuration::get() + + mock::ExtendDuration::get() + + activated_and_extended_at_block + + 1, + ); + + assert_ok!(SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_and_extended_at_block + )); + assert_eq!(Balances::free_balance(&0), BAL_ACC0); // accounts set in mock genesis + }); +} + +#[test] +fn can_release_deposit_while_entered() { + new_test_ext().execute_with(|| { + assert_eq!(System::block_number(), 1); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert!(SafeMode::is_entered()); + + assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get()); + + // We could slash in the same block or any later. + for i in 0..mock::EnterDuration::get() + 10 { + run_to(i); + hypothetically_ok!(SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + )); + } + // Now once we slash once + assert_ok!(SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + ),); + assert_eq!(Balances::free_balance(&0), BAL_ACC0); + // ... it wont work ever again. + assert_err!( + SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + ), + Error::::NoDeposit + ); + }); +} + +#[test] +fn can_slash_deposit_while_entered() { + new_test_ext().execute_with(|| { + assert_eq!(System::block_number(), 1); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert!(SafeMode::is_entered()); + + // We could slash in the same block or any later. + for i in 0..mock::EnterDuration::get() + 10 { + run_to(i); + hypothetically_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + )); + } + // Now once we slash once + assert_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + ),); + // ... it wont work ever again. + assert_err!( + SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + ), + Error::::NoDeposit + ); + }); +} + +#[test] +fn can_slash_deposit_from_extend_block() { + new_test_ext().execute_with(|| { + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1))); + assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get()); + assert_eq!(Balances::free_balance(&1), BAL_ACC1 - mock::ExtendDepositAmount::get()); + + assert_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + ),); + assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get()); + + assert_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 1, + 1 + ),); + assert_eq!(Balances::free_balance(&1), BAL_ACC1 - mock::ExtendDepositAmount::get()); + + // But never again. + assert_err!( + SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + 1 + ), + Error::::NoDeposit + ); + assert_err!( + SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 1, + 1 + ), + Error::::NoDeposit + ); + }); +} + +#[test] +fn can_slash_deposit_with_config_origin() { + new_test_ext().execute_with(|| { + let activated_at_block = System::block_number(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + hypothetically_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + ),); + run_to(mock::EnterDuration::get() + activated_at_block + 1); + + assert_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_at_block + )); + // accounts set in mock genesis + assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get()); + + Balances::make_free_balance_be(&0, BAL_ACC0); + let activated_and_extended_at_block = System::block_number(); + assert_ok!(SafeMode::enter(RuntimeOrigin::signed(0))); + assert_ok!(SafeMode::extend(RuntimeOrigin::signed(1))); + run_to( + mock::EnterDuration::get() + + mock::ExtendDuration::get() + + activated_and_extended_at_block + + 1, + ); + + assert_ok!(SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceDepositOrigin::get()), + 0, + activated_and_extended_at_block + )); + // accounts set in mock genesis + assert_eq!(Balances::free_balance(&0), BAL_ACC0 - mock::EnterDepositAmount::get()); + }); +} + +#[test] +fn fails_when_explicit_origin_required() { + new_test_ext().execute_with(|| { + assert_eq!(EnteredUntil::::get(), None); + let activated_at_block = System::block_number(); + + assert_err!(SafeMode::force_extend(signed(1)), DispatchError::BadOrigin); + assert_err!(SafeMode::force_exit(signed(1)), DispatchError::BadOrigin); + assert_err!( + SafeMode::force_slash_deposit(signed(1), 0, activated_at_block), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_release_deposit(signed(1), 0, activated_at_block), + DispatchError::BadOrigin + ); + + assert_err!(SafeMode::force_enter(signed(1)), DispatchError::BadOrigin); + assert_err!(SafeMode::force_exit(signed(1)), DispatchError::BadOrigin); + assert_err!( + SafeMode::force_slash_deposit(signed(1), 0, activated_at_block), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_release_deposit(signed(1), 0, activated_at_block), + DispatchError::BadOrigin + ); + + assert_err!( + SafeMode::force_enter(RuntimeOrigin::signed(mock::ForceExitOrigin::get())), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_extend(RuntimeOrigin::signed(mock::ForceExitOrigin::get())), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_slash_deposit( + RuntimeOrigin::signed(mock::ForceExitOrigin::get()), + 0, + activated_at_block + ), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_release_deposit( + RuntimeOrigin::signed(mock::ForceExitOrigin::get()), + 0, + activated_at_block + ), + DispatchError::BadOrigin + ); + + assert_err!( + SafeMode::force_enter(RuntimeOrigin::signed(mock::ForceDepositOrigin::get())), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_extend(RuntimeOrigin::signed(mock::ForceDepositOrigin::get())), + DispatchError::BadOrigin + ); + assert_err!( + SafeMode::force_exit(RuntimeOrigin::signed(mock::ForceDepositOrigin::get())), + DispatchError::BadOrigin + ); + }); +} + +fn call_transfer() -> RuntimeCall { + RuntimeCall::Balances(pallet_balances::Call::transfer { dest: 1, value: 1 }) +} + +fn signed(who: u64) -> RuntimeOrigin { + RuntimeOrigin::signed(who) +} diff --git a/frame/safe-mode/src/weights.rs b/frame/safe-mode/src/weights.rs new file mode 100644 index 0000000000000..f72bebcab9a4d --- /dev/null +++ b/frame/safe-mode/src/weights.rs @@ -0,0 +1,321 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_safe_mode` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-08-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-aahe6cbd-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/production/substrate-node +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/substrate/.git/.artifacts/bench.json +// --pallet=pallet_safe_mode +// --chain=dev +// --header=./HEADER-APACHE2 +// --output=./frame/safe-mode/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_safe_mode`. +pub trait WeightInfo { + fn on_initialize_noop() -> Weight; + fn on_initialize_exit() -> Weight; + fn enter() -> Weight; + fn force_enter() -> Weight; + fn extend() -> Weight; + fn force_extend() -> Weight; + fn force_exit() -> Weight; + fn release_deposit() -> Weight; + fn force_release_deposit() -> Weight; + fn force_slash_deposit() -> Weight; +} + +/// Weights for `pallet_safe_mode` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn on_initialize_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `1489` + // Minimum execution time: 2_500_000 picoseconds. + Weight::from_parts(2_594_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn on_initialize_exit() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `1489` + // Minimum execution time: 8_868_000 picoseconds. + Weight::from_parts(9_415_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::Deposits` (r:0 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn enter() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `3550` + // Minimum execution time: 50_541_000 picoseconds. + Weight::from_parts(51_558_000, 3550) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn force_enter() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `1489` + // Minimum execution time: 10_489_000 picoseconds. + Weight::from_parts(10_833_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::Deposits` (r:0 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn extend() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `3550` + // Minimum execution time: 50_818_000 picoseconds. + Weight::from_parts(51_873_000, 3550) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn force_extend() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `1489` + // Minimum execution time: 10_843_000 picoseconds. + Weight::from_parts(11_314_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn force_exit() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `1489` + // Minimum execution time: 10_382_000 picoseconds. + Weight::from_parts(10_814_000, 1489) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::Deposits` (r:1 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn release_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `3550` + // Minimum execution time: 42_828_000 picoseconds. + Weight::from_parts(43_752_000, 3550) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `SafeMode::Deposits` (r:1 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn force_release_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `3550` + // Minimum execution time: 40_196_000 picoseconds. + Weight::from_parts(41_298_000, 3550) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `SafeMode::Deposits` (r:1 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn force_slash_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `3550` + // Minimum execution time: 33_660_000 picoseconds. + Weight::from_parts(34_426_000, 3550) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn on_initialize_noop() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `1489` + // Minimum execution time: 2_500_000 picoseconds. + Weight::from_parts(2_594_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn on_initialize_exit() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `1489` + // Minimum execution time: 8_868_000 picoseconds. + Weight::from_parts(9_415_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::Deposits` (r:0 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn enter() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `3550` + // Minimum execution time: 50_541_000 picoseconds. + Weight::from_parts(51_558_000, 3550) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn force_enter() -> Weight { + // Proof Size summary in bytes: + // Measured: `142` + // Estimated: `1489` + // Minimum execution time: 10_489_000 picoseconds. + Weight::from_parts(10_833_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::Deposits` (r:0 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + fn extend() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `3550` + // Minimum execution time: 50_818_000 picoseconds. + Weight::from_parts(51_873_000, 3550) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn force_extend() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `1489` + // Minimum execution time: 10_843_000 picoseconds. + Weight::from_parts(11_314_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::EnteredUntil` (r:1 w:1) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn force_exit() -> Weight { + // Proof Size summary in bytes: + // Measured: `169` + // Estimated: `1489` + // Minimum execution time: 10_382_000 picoseconds. + Weight::from_parts(10_814_000, 1489) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `SafeMode::Deposits` (r:1 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn release_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `3550` + // Minimum execution time: 42_828_000 picoseconds. + Weight::from_parts(43_752_000, 3550) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `SafeMode::Deposits` (r:1 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn force_release_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `3550` + // Minimum execution time: 40_196_000 picoseconds. + Weight::from_parts(41_298_000, 3550) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `SafeMode::Deposits` (r:1 w:1) + /// Proof: `SafeMode::Deposits` (`max_values`: None, `max_size`: Some(68), added: 2543, mode: `MaxEncodedLen`) + /// Storage: `Balances::Holds` (r:1 w:1) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + fn force_slash_deposit() -> Weight { + // Proof Size summary in bytes: + // Measured: `292` + // Estimated: `3550` + // Minimum execution time: 33_660_000 picoseconds. + Weight::from_parts(34_426_000, 3550) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } +} diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 27d16e133aa02..f669046f858f4 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -116,6 +116,12 @@ pub use messages::{ TransformOrigin, }; +mod safe_mode; +pub use safe_mode::{SafeMode, SafeModeError, SafeModeNotify}; + +mod tx_pause; +pub use tx_pause::{TransactionPause, TransactionPauseError}; + #[cfg(feature = "try-runtime")] mod try_runtime; #[cfg(feature = "try-runtime")] diff --git a/frame/support/src/traits/safe_mode.rs b/frame/support/src/traits/safe_mode.rs new file mode 100644 index 0000000000000..332e28d6e52a9 --- /dev/null +++ b/frame/support/src/traits/safe_mode.rs @@ -0,0 +1,76 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Types to put the runtime into safe mode. + +/// Can put the runtime into a safe mode. +/// +/// When the runtime entered safe mode, transaction processing for most general transactions is +/// paused. +pub trait SafeMode { + /// Block number type. + type BlockNumber; + + /// Whether safe mode is entered. + fn is_entered() -> bool { + Self::remaining().is_some() + } + + /// How many more blocks safe mode will stay entered. + /// + /// If this returns `0`, then safe mode will exit in the next block. + fn remaining() -> Option; + + /// Enter safe mode for `duration` blocks. + /// + /// Should error when already entered with `AlreadyEntered`. + fn enter(duration: Self::BlockNumber) -> Result<(), SafeModeError>; + + /// Extend safe mode for `duration` blocks. + /// + /// Should error when not entered with `AlreadyExited`. + fn extend(duration: Self::BlockNumber) -> Result<(), SafeModeError>; + + /// Exit safe mode immediately. + /// + /// This takes effect already in the same block. + fn exit() -> Result<(), SafeModeError>; +} + +/// The error type for [`SafeMode`]. +pub enum SafeModeError { + /// Safe mode is already entered. + AlreadyEntered, + /// Safe mode is already exited. + AlreadyExited, + /// Unknown error. + Unknown, +} + +/// A trait to notify when the runtime enters or exits safe mode. +pub trait SafeModeNotify { + /// Called when the runtime enters safe mode. + fn entered(); + + /// Called when the runtime exits safe mode. + fn exited(); +} + +impl SafeModeNotify for () { + fn entered() {} + fn exited() {} +} diff --git a/frame/support/src/traits/tx_pause.rs b/frame/support/src/traits/tx_pause.rs new file mode 100644 index 0000000000000..64d2f754f5235 --- /dev/null +++ b/frame/support/src/traits/tx_pause.rs @@ -0,0 +1,66 @@ +// This file is part of Substrate. + +// Copyright (C) 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. + +//! Types to pause calls in the runtime. + +/// Can pause specific transactions from being processed. +/// +/// Note that paused transactions will not be queued for later execution. Instead they will be +/// dropped. +pub trait TransactionPause { + /// How to unambiguously identify a call. + /// + /// For example `(pallet_index, call_index)`. + type CallIdentifier; + + /// Whether this call is paused. + fn is_paused(call: Self::CallIdentifier) -> bool; + + /// Whether this call can be paused. + /// + /// This holds for the current block, but may change in the future. + fn can_pause(call: Self::CallIdentifier) -> bool; + + /// Pause this call immediately. + /// + /// This takes effect in the same block and must succeed if `can_pause` returns `true`. + fn pause(call: Self::CallIdentifier) -> Result<(), TransactionPauseError>; + + /// Unpause this call immediately. + /// + /// This takes effect in the same block and must succeed if `is_paused` returns `true`. This + /// invariant is important to not have un-resumable calls. + fn unpause(call: Self::CallIdentifier) -> Result<(), TransactionPauseError>; +} + +/// The error type for [`TransactionPause`]. +pub enum TransactionPauseError { + /// The call could not be found in the runtime. + /// + /// This is a permanent error but could change after a runtime upgrade. + NotFound, + /// Call cannot be paused. + /// + /// This may or may not resolve in a future block. + Unpausable, + /// Call is already paused. + AlreadyPaused, + /// Call is already unpaused. + AlreadyUnpaused, + /// Unknown error. + Unknown, +} diff --git a/frame/system/src/lib.rs b/frame/system/src/lib.rs index c5a36c5059e95..52956085fe3bb 100644 --- a/frame/system/src/lib.rs +++ b/frame/system/src/lib.rs @@ -248,6 +248,14 @@ pub mod pallet { /// The basic call filter to use in Origin. All origins are built with this filter as base, /// except Root. + /// + /// This works as a filter for each incoming call. The call needs to pass this filter in + /// order to dispatch. Otherwise it will be rejected with `CallFiltered`. This can be + /// bypassed via `dispatch_bypass_filter` which should only be accessible by root. The + /// filter can be composed of sub-filters by nesting for example + /// [`frame_support::traits::InsideBoth`], [`frame_support::traits::TheseExcept`] or + /// [`frame_support::traits::EverythingBut`] et al. The default would be + /// [`frame_support::traits::Everything`]. #[pallet::no_default] type BaseCallFilter: Contains; diff --git a/frame/tx-pause/Cargo.toml b/frame/tx-pause/Cargo.toml new file mode 100644 index 0000000000000..24ac55909ca91 --- /dev/null +++ b/frame/tx-pause/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "pallet-tx-pause" +version = "4.0.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME transaction pause pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../benchmarking" } +frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" } +frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +sp-runtime = { version = "24.0.0", default-features = false, path = "../../primitives/runtime" } +sp-std = { version = "8.0.0", default-features = false, path = "../../primitives/std" } +pallet-balances = { version = "4.0.0-dev", path = "../balances", default-features = false, optional = true } +pallet-utility = { version = "4.0.0-dev", path = "../utility", default-features = false, optional = true } +pallet-proxy = { version = "4.0.0-dev", path = "../proxy", default-features = false, optional = true } + +[dev-dependencies] +sp-core = { version = "21.0.0", path = "../../primitives/core" } +sp-io = { version = "23.0.0", path = "../../primitives/io" } +pallet-balances = { version = "4.0.0-dev", path = "../balances" } +pallet-utility = { version = "4.0.0-dev", path = "../utility" } +pallet-proxy = { version = "4.0.0-dev", path = "../proxy" } + +[features] +default = [ "std" ] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "pallet-balances?/std", + "pallet-proxy?/std", + "pallet-utility?/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-proxy/runtime-benchmarks", + "pallet-utility/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances?/try-runtime", + "pallet-proxy?/try-runtime", + "pallet-utility?/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/frame/tx-pause/src/benchmarking.rs b/frame/tx-pause/src/benchmarking.rs new file mode 100644 index 0000000000000..81595ef9f7280 --- /dev/null +++ b/frame/tx-pause/src/benchmarking.rs @@ -0,0 +1,59 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(feature = "runtime-benchmarks")] + +use super::{Pallet as TxPause, *}; +use frame_benchmarking::v2::*; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn pause() { + let origin = T::PauseOrigin::try_successful_origin() + .expect("Tx-pause pallet is not usable without pause origin"); + let full_name = name::(); + + #[extrinsic_call] + _(origin as T::RuntimeOrigin, full_name.clone()); + + assert!(PausedCalls::::get(full_name).is_some()); + } + + #[benchmark] + fn unpause() { + let unpause_origin = T::UnpauseOrigin::try_successful_origin() + .expect("Tx-pause pallet is not usable without pause origin"); + let full_name = name::(); + TxPause::::do_pause(full_name.clone()).unwrap(); + + #[extrinsic_call] + _(unpause_origin as T::RuntimeOrigin, full_name.clone()); + + assert!(PausedCalls::::get(full_name).is_none()); + } + + impl_benchmark_test_suite!(TxPause, crate::mock::new_test_ext(), crate::mock::Test); +} + +/// Longest possible name. +fn name() -> RuntimeCallNameOf { + let max_len = T::MaxNameLen::get() as usize; + (vec![1; max_len].try_into().unwrap(), vec![1; max_len].try_into().unwrap()) +} diff --git a/frame/tx-pause/src/lib.rs b/frame/tx-pause/src/lib.rs new file mode 100644 index 0000000000000..36147d32a2f0e --- /dev/null +++ b/frame/tx-pause/src/lib.rs @@ -0,0 +1,277 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] +#![deny(rustdoc::broken_intra_doc_links)] + +mod benchmarking; +pub mod mock; +mod tests; +pub mod weights; + +use frame_support::{ + dispatch::GetDispatchInfo, + pallet_prelude::*, + traits::{CallMetadata, Contains, GetCallMetadata, IsSubType, IsType}, + DefaultNoBound, +}; +use frame_system::pallet_prelude::*; +use sp_runtime::{traits::Dispatchable, DispatchResult}; +use sp_std::{convert::TryInto, prelude::*}; + +pub use pallet::*; +pub use weights::*; + +/// The stringy name of a pallet from [`GetCallMetadata`] for [`Config::RuntimeCall`] variants. +pub type PalletNameOf = BoundedVec::MaxNameLen>; + +/// The stringy name of a call (within a pallet) from [`GetCallMetadata`] for +/// [`Config::RuntimeCall`] variants. +pub type PalletCallNameOf = BoundedVec::MaxNameLen>; + +/// A fully specified pallet ([`PalletNameOf`]) and optional call ([`PalletCallNameOf`]) +/// to partially or fully specify an item a variant of a [`Config::RuntimeCall`]. +pub type RuntimeCallNameOf = (PalletNameOf, PalletCallNameOf); + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + GetCallMetadata + + From> + + IsSubType> + + IsType<::RuntimeCall>; + + /// The only origin that can pause calls. + type PauseOrigin: EnsureOrigin; + + /// The only origin that can un-pause calls. + type UnpauseOrigin: EnsureOrigin; + + /// Contains all calls that cannot be paused. + /// + /// The `TxMode` pallet cannot pause its own calls, and does not need to be explicitly + /// added here. + type WhitelistedCalls: Contains>; + + /// Maximum length for pallet name and call name SCALE encoded string names. + /// + /// TOO LONG NAMES WILL BE TREATED AS PAUSED. + #[pallet::constant] + type MaxNameLen: Get; + + // Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + /// The set of calls that are explicitly paused. + #[pallet::storage] + pub type PausedCalls = + StorageMap<_, Blake2_128Concat, RuntimeCallNameOf, (), OptionQuery>; + + #[pallet::error] + pub enum Error { + /// The call is paused. + IsPaused, + + /// The call is unpaused. + IsUnpaused, + + /// The call is whitelisted and cannot be paused. + Unpausable, + + // The pallet or call does not exist in the runtime. + NotFound, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// This pallet, or a specific call is now paused. + CallPaused { full_name: RuntimeCallNameOf }, + /// This pallet, or a specific call is now unpaused. + CallUnpaused { full_name: RuntimeCallNameOf }, + } + + /// Configure the initial state of this pallet in the genesis block. + #[pallet::genesis_config] + #[derive(DefaultNoBound)] + pub struct GenesisConfig { + /// Initially paused calls. + pub paused: Vec>, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for call in &self.paused { + Pallet::::ensure_can_pause(&call).expect("Genesis data is known good; qed"); + PausedCalls::::insert(&call, ()); + } + } + } + + #[pallet::call] + impl Pallet { + /// Pause a call. + /// + /// Can only be called by [`Config::PauseOrigin`]. + /// Emits an [`Event::CallPaused`] event on success. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::pause())] + pub fn pause(origin: OriginFor, full_name: RuntimeCallNameOf) -> DispatchResult { + T::PauseOrigin::ensure_origin(origin)?; + + Self::do_pause(full_name).map_err(Into::into) + } + + /// Un-pause a call. + /// + /// Can only be called by [`Config::UnpauseOrigin`]. + /// Emits an [`Event::CallUnpaused`] event on success. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::unpause())] + pub fn unpause(origin: OriginFor, ident: RuntimeCallNameOf) -> DispatchResult { + T::UnpauseOrigin::ensure_origin(origin)?; + + Self::do_unpause(ident).map_err(Into::into) + } + } +} + +impl Pallet { + pub(crate) fn do_pause(ident: RuntimeCallNameOf) -> Result<(), Error> { + Self::ensure_can_pause(&ident)?; + PausedCalls::::insert(&ident, ()); + Self::deposit_event(Event::CallPaused { full_name: ident }); + + Ok(()) + } + + pub(crate) fn do_unpause(ident: RuntimeCallNameOf) -> Result<(), Error> { + Self::ensure_can_unpause(&ident)?; + PausedCalls::::remove(&ident); + Self::deposit_event(Event::CallUnpaused { full_name: ident }); + + Ok(()) + } + + /// Return whether this call is paused. + pub fn is_paused(full_name: &RuntimeCallNameOf) -> bool { + if T::WhitelistedCalls::contains(full_name) { + return false + } + + >::contains_key(full_name) + } + + /// Same as [`Self::is_paused`] but for inputs unbound by max-encoded-len. + pub fn is_paused_unbound(pallet: Vec, call: Vec) -> bool { + let pallet = PalletNameOf::::try_from(pallet); + let call = PalletCallNameOf::::try_from(call); + + match (pallet, call) { + (Ok(pallet), Ok(call)) => Self::is_paused(&(pallet, call)), + _ => true, + } + } + + /// Ensure that this call can be paused. + pub fn ensure_can_pause(full_name: &RuntimeCallNameOf) -> Result<(), Error> { + // SAFETY: The `TxPause` pallet can never pause itself. + if full_name.0.as_ref() == ::name().as_bytes().to_vec() { + return Err(Error::::Unpausable) + } + + if T::WhitelistedCalls::contains(&full_name) { + return Err(Error::::Unpausable) + } + if Self::is_paused(&full_name) { + return Err(Error::::IsPaused) + } + Ok(()) + } + + /// Ensure that this call can be un-paused. + pub fn ensure_can_unpause(full_name: &RuntimeCallNameOf) -> Result<(), Error> { + if Self::is_paused(&full_name) { + // SAFETY: Everything that is paused, can be un-paused. + Ok(()) + } else { + Err(Error::IsUnpaused) + } + } +} + +impl Contains<::RuntimeCall> for Pallet +where + ::RuntimeCall: GetCallMetadata, +{ + /// Return whether the call is allowed to be dispatched. + fn contains(call: &::RuntimeCall) -> bool { + let CallMetadata { pallet_name, function_name } = call.get_call_metadata(); + !Pallet::::is_paused_unbound(pallet_name.into(), function_name.into()) + } +} + +impl frame_support::traits::TransactionPause for Pallet { + type CallIdentifier = RuntimeCallNameOf; + + fn is_paused(full_name: Self::CallIdentifier) -> bool { + Self::is_paused(&full_name) + } + + fn can_pause(full_name: Self::CallIdentifier) -> bool { + Self::ensure_can_pause(&full_name).is_ok() + } + + fn pause( + full_name: Self::CallIdentifier, + ) -> Result<(), frame_support::traits::TransactionPauseError> { + Self::do_pause(full_name).map_err(Into::into) + } + + fn unpause( + full_name: Self::CallIdentifier, + ) -> Result<(), frame_support::traits::TransactionPauseError> { + Self::do_unpause(full_name).map_err(Into::into) + } +} + +impl From> for frame_support::traits::TransactionPauseError { + fn from(err: Error) -> Self { + match err { + Error::::NotFound => Self::NotFound, + Error::::Unpausable => Self::Unpausable, + Error::::IsPaused => Self::AlreadyPaused, + Error::::IsUnpaused => Self::AlreadyUnpaused, + _ => Self::Unknown, + } + } +} diff --git a/frame/tx-pause/src/mock.rs b/frame/tx-pause/src/mock.rs new file mode 100644 index 0000000000000..70c888f3c38da --- /dev/null +++ b/frame/tx-pause/src/mock.rs @@ -0,0 +1,226 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests and test utilities for transaction pause pallet. + +#![cfg(test)] + +use super::*; +use crate as pallet_tx_pause; + +use frame_support::{ + parameter_types, + traits::{ConstU64, Everything, InsideBoth, InstanceFilter}, +}; +use frame_system::EnsureSignedBy; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} +impl frame_system::Config for Test { + type BaseCallFilter = InsideBoth; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type Block = Block; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; + pub const MaxLocks: u32 = 10; +} +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type RuntimeHoldReason = RuntimeHoldReason; + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +impl pallet_utility::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PalletsOrigin = OriginCaller; + type WeightInfo = (); +} + +/// Mocked proxies to check that tx-pause also works with the proxy pallet. +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Encode, + Decode, + RuntimeDebug, + MaxEncodedLen, + scale_info::TypeInfo, +)] +pub enum ProxyType { + Any, + JustTransfer, + JustUtility, +} + +impl Default for ProxyType { + fn default() -> Self { + Self::Any + } +} + +impl InstanceFilter for ProxyType { + fn filter(&self, c: &RuntimeCall) -> bool { + match self { + ProxyType::Any => true, + ProxyType::JustTransfer => { + matches!(c, RuntimeCall::Balances(pallet_balances::Call::transfer { .. })) + }, + ProxyType::JustUtility => matches!(c, RuntimeCall::Utility { .. }), + } + } + fn is_superset(&self, o: &Self) -> bool { + self == &ProxyType::Any || self == o + } +} + +impl pallet_proxy::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = ProxyType; + type ProxyDepositBase = ConstU64<1>; + type ProxyDepositFactor = ConstU64<1>; + type MaxProxies = ConstU32<4>; + type WeightInfo = (); + type CallHasher = BlakeTwo256; + type MaxPending = ConstU32<2>; + type AnnouncementDepositBase = ConstU64<1>; + type AnnouncementDepositFactor = ConstU64<1>; +} + +parameter_types! { + pub const MaxNameLen: u32 = 50; +} + +frame_support::ord_parameter_types! { + pub const PauseOrigin: u64 = 1; + pub const UnpauseOrigin: u64 = 2; +} + +/// Calls that are never allowed to be paused. +pub struct WhitelistedCalls; +impl Contains> for WhitelistedCalls { + fn contains(full_name: &RuntimeCallNameOf) -> bool { + match (full_name.0.as_slice(), full_name.1.as_slice()) { + (b"Balances", b"transfer_keep_alive") => true, + _ => false, + } + } +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type PauseOrigin = EnsureSignedBy; + type UnpauseOrigin = EnsureSignedBy; + type WhitelistedCalls = WhitelistedCalls; + type MaxNameLen = MaxNameLen; + type WeightInfo = (); +} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Utility: pallet_utility, + Proxy: pallet_proxy, + TxPause: pallet_tx_pause, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + // The 0 account is NOT a special origin. The rest may be: + balances: vec![(0, 1234), (1, 5678), (2, 5678), (3, 5678), (4, 5678)], + } + .assimilate_storage(&mut t) + .unwrap(); + + pallet_tx_pause::GenesisConfig:: { paused: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext +} + +pub fn next_block() { + TxPause::on_finalize(System::block_number()); + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + TxPause::on_initialize(System::block_number()); +} + +pub fn run_to(n: u64) { + while System::block_number() < n { + next_block(); + } +} diff --git a/frame/tx-pause/src/tests.rs b/frame/tx-pause/src/tests.rs new file mode 100644 index 0000000000000..ca259315726b0 --- /dev/null +++ b/frame/tx-pause/src/tests.rs @@ -0,0 +1,222 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(test)] + +use super::*; +use crate::mock::{RuntimeCall, *}; + +use frame_support::{assert_err, assert_noop, assert_ok, dispatch::Dispatchable}; + +// GENERAL SUCCESS/POSITIVE TESTS --------------------- + +#[test] +fn can_pause_specific_call() { + new_test_ext().execute_with(|| { + assert_ok!(call_transfer(1, 1).dispatch(RuntimeOrigin::signed(0))); + + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer") + )); + + assert_err!( + call_transfer(2, 1).dispatch(RuntimeOrigin::signed(2)), + frame_system::Error::::CallFiltered + ); + assert_ok!(call_transfer_keep_alive(3, 1).dispatch(RuntimeOrigin::signed(3))); + }); +} + +#[test] +fn can_pause_all_calls_in_pallet_except_on_whitelist() { + new_test_ext().execute_with(|| { + assert_ok!(call_transfer(1, 1).dispatch(RuntimeOrigin::signed(0))); + + let batch_call = + RuntimeCall::Utility(pallet_utility::Call::batch { calls: vec![call_transfer(1, 1)] }); + assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0))); + + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Utility", b"batch") + ),); + + assert_err!( + batch_call.clone().dispatch(RuntimeOrigin::signed(0)), + frame_system::Error::::CallFiltered + ); + }); +} + +#[test] +fn can_unpause_specific_call() { + new_test_ext().execute_with(|| { + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + )); + assert_err!( + call_transfer(2, 1).dispatch(RuntimeOrigin::signed(2)), + frame_system::Error::::CallFiltered + ); + + assert_ok!(TxPause::unpause( + RuntimeOrigin::signed(mock::UnpauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + )); + assert_ok!(call_transfer(4, 1).dispatch(RuntimeOrigin::signed(0))); + }); +} + +#[test] +fn can_filter_balance_in_batch_when_paused() { + new_test_ext().execute_with(|| { + let batch_call = + RuntimeCall::Utility(pallet_utility::Call::batch { calls: vec![call_transfer(1, 1)] }); + + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + )); + + assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0))); + System::assert_last_event( + pallet_utility::Event::BatchInterrupted { + index: 0, + error: frame_system::Error::::CallFiltered.into(), + } + .into(), + ); + }); +} + +#[test] +fn can_filter_balance_in_proxy_when_paused() { + new_test_ext().execute_with(|| { + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + )); + + assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(1), 2, ProxyType::JustTransfer, 0)); + + assert_ok!(Proxy::proxy(RuntimeOrigin::signed(2), 1, None, Box::new(call_transfer(1, 1)))); + System::assert_last_event( + pallet_proxy::Event::ProxyExecuted { + result: DispatchError::from(frame_system::Error::::CallFiltered).into(), + } + .into(), + ); + }); +} + +// GENERAL FAIL/NEGATIVE TESTS --------------------- + +#[test] +fn fails_to_pause_self() { + new_test_ext().execute_with(|| { + assert_noop!( + TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"TxPause", b"pause"), + ), + Error::::Unpausable + ); + }); +} + +#[test] +fn fails_to_pause_unpausable_call_when_other_call_is_paused() { + new_test_ext().execute_with(|| { + assert_ok!(call_transfer(1, 1).dispatch(RuntimeOrigin::signed(0))); + + let batch_call = + RuntimeCall::Utility(pallet_utility::Call::batch { calls: vec![call_transfer(1, 1)] }); + assert_ok!(batch_call.clone().dispatch(RuntimeOrigin::signed(0))); + + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + )); + + assert_ok!(call_transfer_keep_alive(3, 1).dispatch(RuntimeOrigin::signed(3))); + assert_err!( + call_transfer(2, 1).dispatch(RuntimeOrigin::signed(0)), + frame_system::Error::::CallFiltered + ); + }); +} + +#[test] +fn fails_to_pause_unpausable_call() { + new_test_ext().execute_with(|| { + assert_noop!( + TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer_keep_alive"), + ), + Error::::Unpausable + ); + }); +} + +#[test] +fn fails_to_pause_already_paused_pallet() { + new_test_ext().execute_with(|| { + assert_ok!(TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + )); + + assert_noop!( + TxPause::pause( + RuntimeOrigin::signed(mock::PauseOrigin::get()), + full_name::(b"Balances", b"transfer"), + ), + Error::::IsPaused + ); + }); +} + +#[test] +fn fails_to_unpause_not_paused_pallet() { + new_test_ext().execute_with(|| { + assert_noop!( + TxPause::unpause( + RuntimeOrigin::signed(mock::UnpauseOrigin::get()), + full_name::(b"Balances", b"transfer_keep_alive"), + ), + Error::::IsUnpaused + ); + }); +} + +pub fn call_transfer(dest: u64, value: u64) -> RuntimeCall { + RuntimeCall::Balances(pallet_balances::Call::transfer { dest, value }) +} + +pub fn call_transfer_keep_alive(dest: u64, value: u64) -> RuntimeCall { + RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive { dest, value }) +} + +pub fn full_name(pallet_name: &[u8], call_name: &[u8]) -> RuntimeCallNameOf { + >::from(( + pallet_name.to_vec().try_into().unwrap(), + call_name.to_vec().try_into().unwrap(), + )) +} diff --git a/frame/tx-pause/src/weights.rs b/frame/tx-pause/src/weights.rs new file mode 100644 index 0000000000000..b733e64b159dc --- /dev/null +++ b/frame/tx-pause/src/weights.rs @@ -0,0 +1,107 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_tx_pause` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-08-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `runner-aahe6cbd-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/production/substrate-node +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/builds/parity/mirrors/substrate/.git/.artifacts/bench.json +// --pallet=pallet_tx_pause +// --chain=dev +// --header=./HEADER-APACHE2 +// --output=./frame/tx-pause/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_tx_pause`. +pub trait WeightInfo { + fn pause() -> Weight; + fn unpause() -> Weight; +} + +/// Weights for `pallet_tx_pause` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `TxPause::PausedCalls` (r:1 w:1) + /// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `MaxEncodedLen`) + fn pause() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `3997` + // Minimum execution time: 15_096_000 picoseconds. + Weight::from_parts(15_437_000, 3997) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `TxPause::PausedCalls` (r:1 w:1) + /// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `MaxEncodedLen`) + fn unpause() -> Weight { + // Proof Size summary in bytes: + // Measured: `565` + // Estimated: `3997` + // Minimum execution time: 21_546_000 picoseconds. + Weight::from_parts(22_178_000, 3997) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `TxPause::PausedCalls` (r:1 w:1) + /// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `MaxEncodedLen`) + fn pause() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `3997` + // Minimum execution time: 15_096_000 picoseconds. + Weight::from_parts(15_437_000, 3997) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `TxPause::PausedCalls` (r:1 w:1) + /// Proof: `TxPause::PausedCalls` (`max_values`: None, `max_size`: Some(532), added: 3007, mode: `MaxEncodedLen`) + fn unpause() -> Weight { + // Proof Size summary in bytes: + // Measured: `565` + // Estimated: `3997` + // Minimum execution time: 21_546_000 picoseconds. + Weight::from_parts(22_178_000, 3997) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +}