// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // 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. //! # Safe Mode //! //! Trigger for stopping all extrinsics outside of a specific whitelist. //! //! ## Pezpallet API //! //! See the [`pezpallet`] module for more information about the interfaces this pezpallet exposes, //! including its configuration trait, dispatchables, storage items, events, and errors. //! //! ## Overview //! //! Safe mode is entered via two paths (deposit or forced) until a set block number. //! The mode is exited when the block number is reached or a call to one of the exit extrinsics is //! made. A `WhitelistedCalls` configuration item contains all calls that can be executed while in //! safe mode. //! //! ### Primary Features //! //! - Entering safe mode can be via privileged origin or anyone who places a deposit. //! - Origin configuration items are separated for privileged entering and exiting safe mode. //! - A configurable duration sets the number of blocks after which the system will exit safe mode. //! - Safe mode may be extended beyond the configured exit by additional calls. //! //! ### Example //! //! Configuration of call filters: //! //! ```ignore //! impl pezframe_system::Config for Runtime { //! // … //! type BaseCallFilter = InsideBoth; //! // … //! } //! ``` //! //! Entering safe mode with deposit: #![doc = docify::embed!("src/tests.rs", can_activate)] //! //! Entering safe mode via privileged origin: #![doc = docify::embed!("src/tests.rs", can_force_activate_with_config_origin)] //! //! Exiting safe mode via privileged origin: #![doc = docify::embed!("src/tests.rs", can_force_deactivate_with_config_origin)] //! //! ## Low Level / Implementation Details //! //! ### Use Cost //! //! A storage value (`EnteredUntil`) is used to store the block safe mode will be exited on. //! Using the call filter will require a db read of that storage on the first extrinsic. //! The storage will be added to the overlay and incur low cost for all additional calls. #![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::{ prelude::{ fungible::hold::{Inspect, Mutate}, *, }, traits::{fungible, CallMetadata, GetCallMetadata, SafeModeNotify}, }; pub use pezpallet::*; pub use weights::*; type BalanceOf = <::Currency as fungible::Inspect< ::AccountId, >>::Balance; #[frame::pezpallet] pub mod pezpallet { use super::*; #[pezpallet::pezpallet] pub struct Pezpallet(PhantomData); #[pezpallet::config] pub trait Config: pezframe_system::Config { /// The overarching event type. #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Currency type for this pezpallet, used for Deposits. type Currency: Inspect + Mutate; /// The hold reason when reserving funds for entering or extending the safe-mode. type RuntimeHoldReason: From; /// Contains all runtime calls in any pezpallet that can be dispatched even while the /// safe-mode is entered. /// /// The safe-mode pezpallet 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 [`Pezpallet::enter`]. #[pezpallet::constant] type EnterDuration: Get>; /// For how many blocks the safe-mode can be extended by each [`Pezpallet::extend`] call. /// /// This does not impose a hard limit as the safe-mode can be extended multiple times. #[pezpallet::constant] type ExtendDuration: Get>; /// The amount that will be reserved upon calling [`Pezpallet::enter`]. /// /// `None` disallows permissionlessly enabling the safe-mode and is a sane default. #[pezpallet::constant] type EnterDepositAmount: Get>>; /// The amount that will be reserved upon calling [`Pezpallet::extend`]. /// /// `None` disallows permissionlessly extending the safe-mode and is a sane default. #[pezpallet::constant] type ExtendDepositAmount: Get>>; /// The origin that may call [`Pezpallet::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 [`Pezpallet::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 [`Pezpallet::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 [`Pezpallet::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. #[pezpallet::constant] type ReleaseDelay: Get>>; // Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; } #[pezpallet::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 pezpallet 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, } #[pezpallet::event] #[pezpallet::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, DecodeWithMemTracking, 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 [`Pezpallet::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. #[pezpallet::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. #[pezpallet::storage] pub type Deposits = StorageDoubleMap< _, Twox64Concat, T::AccountId, Twox64Concat, BlockNumberFor, BalanceOf, OptionQuery, >; /// Configure the initial state of this pezpallet in the genesis block. #[pezpallet::genesis_config] #[derive(DefaultNoBound)] pub struct GenesisConfig { pub entered_until: Option>, } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { if let Some(block) = self.entered_until { EnteredUntil::::put(block); } } } /// A reason for the pezpallet placing a hold on funds. #[pezpallet::composite_enum] pub enum HoldReason { /// Funds are held for entering or extending the safe-mode. #[codec(index = 0)] EnterOrExtend, } #[pezpallet::call] impl Pezpallet { /// 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`. #[pezpallet::call_index(0)] #[pezpallet::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. #[pezpallet::call_index(1)] #[pezpallet::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`. #[pezpallet::call_index(2)] #[pezpallet::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. #[pezpallet::call_index(3)] #[pezpallet::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 [`Pezpallet::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. #[pezpallet::call_index(4)] #[pezpallet::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. #[pezpallet::call_index(5)] #[pezpallet::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. #[pezpallet::call_index(6)] #[pezpallet::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. #[pezpallet::call_index(7)] #[pezpallet::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) } } #[pezpallet::hooks] impl Hooks> for Pezpallet { /// 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 Pezpallet { /// Logic for the [`crate::Pezpallet::enter`] and [`crate::Pezpallet::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::Pezpallet::extend`] and [`crate::Pezpallet::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::Pezpallet::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::Pezpallet::release_deposit`] and /// [`crate::Pezpallet::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::Pezpallet::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 { pezpallet_name, .. } = call.get_call_metadata(); // SAFETY: The `SafeMode` pezpallet is always allowed. if pezpallet_name == as PalletInfoAccess>::name() { return true; } if Self::is_entered() { T::WhitelistedCalls::contains(call) } else { true } } } impl Contains for Pezpallet where T::RuntimeCall: GetCallMetadata, { /// Return whether the given call is allowed to be dispatched. fn contains(call: &T::RuntimeCall) -> bool { Pezpallet::::is_allowed(call) } } impl frame::traits::SafeMode for Pezpallet { 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::traits::SafeModeError> { Self::do_enter(None, duration).map_err(Into::into) } fn extend(duration: BlockNumberFor) -> Result<(), frame::traits::SafeModeError> { Self::do_extend(None, duration).map_err(Into::into) } fn exit() -> Result<(), frame::traits::SafeModeError> { Self::do_exit(ExitReason::Force).map_err(Into::into) } } impl From> for frame::traits::SafeModeError { fn from(err: Error) -> Self { match err { Error::::Entered => Self::AlreadyEntered, Error::::Exited => Self::AlreadyExited, _ => Self::Unknown, } } }