referrerpolicy=no-referrer-when-downgrade

pallet_safe_mode/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Safe Mode
19//!
20//! Trigger for stopping all extrinsics outside of a specific whitelist.
21//!
22//! ## Pallet API
23//!
24//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
25//! including its configuration trait, dispatchables, storage items, events, and errors.
26//!
27//! ## Overview
28//!
29//! Safe mode is entered via two paths (deposit or forced) until a set block number.
30//! The mode is exited when the block number is reached or a call to one of the exit extrinsics is
31//! made. A `WhitelistedCalls` configuration item contains all calls that can be executed while in
32//! safe mode.
33//!
34//! ### Primary Features
35//!
36//! - Entering safe mode can be via privileged origin or anyone who places a deposit.
37//! - Origin configuration items are separated for privileged entering and exiting safe mode.
38//! - A configurable duration sets the number of blocks after which the system will exit safe mode.
39//! - Safe mode may be extended beyond the configured exit by additional calls.
40//!
41//! ### Example
42//!
43//! Configuration of call filters:
44//!
45//! ```ignore
46//! impl frame_system::Config for Runtime {
47//!   // …
48//!   type BaseCallFilter = InsideBoth<DefaultFilter, SafeMode>;
49//!   // …
50//! }
51//! ```
52//!
53//! Entering safe mode with deposit:
54#![doc = docify::embed!("src/tests.rs", can_activate)]
55//! Entering safe mode via privileged origin:
56#![doc = docify::embed!("src/tests.rs", can_force_activate_with_config_origin)]
57//! Exiting safe mode via privileged origin:
58#![doc = docify::embed!("src/tests.rs", can_force_deactivate_with_config_origin)]
59//! ## Low Level / Implementation Details
60//!
61//! ### Use Cost
62//!
63//! A storage value (`EnteredUntil`) is used to store the block safe mode will be exited on.
64//! Using the call filter will require a db read of that storage on the first extrinsic.
65//! The storage will be added to the overlay and incur low cost for all additional calls.
66
67#![cfg_attr(not(feature = "std"), no_std)]
68#![deny(rustdoc::broken_intra_doc_links)]
69
70mod benchmarking;
71pub mod mock;
72mod tests;
73pub mod weights;
74
75use frame::{
76	prelude::{
77		fungible::hold::{Inspect, Mutate},
78		*,
79	},
80	traits::{fungible, CallMetadata, GetCallMetadata, SafeModeNotify},
81};
82
83pub use pallet::*;
84pub use weights::*;
85
86type BalanceOf<T> =
87	<<T as Config>::Currency as fungible::Inspect<<T as frame_system::Config>::AccountId>>::Balance;
88
89#[frame::pallet]
90pub mod pallet {
91	use super::*;
92
93	#[pallet::pallet]
94	pub struct Pallet<T>(PhantomData<T>);
95
96	#[pallet::config]
97	pub trait Config: frame_system::Config {
98		/// The overarching event type.
99		#[allow(deprecated)]
100		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
101
102		/// Currency type for this pallet, used for Deposits.
103		type Currency: Inspect<Self::AccountId>
104			+ Mutate<Self::AccountId, Reason = Self::RuntimeHoldReason>;
105
106		/// The hold reason when reserving funds for entering or extending the safe-mode.
107		type RuntimeHoldReason: From<HoldReason>;
108
109		/// Contains all runtime calls in any pallet that can be dispatched even while the safe-mode
110		/// is entered.
111		///
112		/// The safe-mode pallet cannot disable it's own calls, and does not need to be explicitly
113		/// added here.
114		type WhitelistedCalls: Contains<Self::RuntimeCall>;
115
116		/// For how many blocks the safe-mode will be entered by [`Pallet::enter`].
117		#[pallet::constant]
118		type EnterDuration: Get<BlockNumberFor<Self>>;
119
120		/// For how many blocks the safe-mode can be extended by each [`Pallet::extend`] call.
121		///
122		/// This does not impose a hard limit as the safe-mode can be extended multiple times.
123		#[pallet::constant]
124		type ExtendDuration: Get<BlockNumberFor<Self>>;
125
126		/// The amount that will be reserved upon calling [`Pallet::enter`].
127		///
128		/// `None` disallows permissionlessly enabling the safe-mode and is a sane default.
129		#[pallet::constant]
130		type EnterDepositAmount: Get<Option<BalanceOf<Self>>>;
131
132		/// The amount that will be reserved upon calling [`Pallet::extend`].
133		///
134		/// `None` disallows permissionlessly extending the safe-mode and is a sane default.
135		#[pallet::constant]
136		type ExtendDepositAmount: Get<Option<BalanceOf<Self>>>;
137
138		/// The origin that may call [`Pallet::force_enter`].
139		///
140		/// The `Success` value is the number of blocks that this origin can enter safe-mode for.
141		type ForceEnterOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;
142
143		/// The origin that may call [`Pallet::force_extend`].
144		///
145		/// The `Success` value is the number of blocks that this origin can extend the safe-mode.
146		type ForceExtendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;
147
148		/// The origin that may call [`Pallet::force_enter`].
149		type ForceExitOrigin: EnsureOrigin<Self::RuntimeOrigin>;
150
151		/// The only origin that can force to release or slash a deposit.
152		type ForceDepositOrigin: EnsureOrigin<Self::RuntimeOrigin>;
153
154		/// Notifies external logic when the safe-mode is being entered or exited.
155		type Notify: SafeModeNotify;
156
157		/// The minimal duration a deposit will remain reserved after safe-mode is entered or
158		/// extended, unless [`Pallet::force_release_deposit`] is successfully called sooner.
159		///
160		/// Every deposit is tied to a specific activation or extension, thus each deposit can be
161		/// released independently after the delay for it has passed.
162		///
163		/// `None` disallows permissionlessly releasing the safe-mode deposits and is a sane
164		/// default.
165		#[pallet::constant]
166		type ReleaseDelay: Get<Option<BlockNumberFor<Self>>>;
167
168		// Weight information for extrinsics in this pallet.
169		type WeightInfo: WeightInfo;
170	}
171
172	#[pallet::error]
173	pub enum Error<T> {
174		/// The safe-mode is (already or still) entered.
175		Entered,
176
177		/// The safe-mode is (already or still) exited.
178		Exited,
179
180		/// This functionality of the pallet is disabled by the configuration.
181		NotConfigured,
182
183		/// There is no balance reserved.
184		NoDeposit,
185
186		/// The account already has a deposit reserved and can therefore not enter or extend again.
187		AlreadyDeposited,
188
189		/// This deposit cannot be released yet.
190		CannotReleaseYet,
191
192		/// An error from the underlying `Currency`.
193		CurrencyError,
194	}
195
196	#[pallet::event]
197	#[pallet::generate_deposit(pub(super) fn deposit_event)]
198	pub enum Event<T: Config> {
199		/// The safe-mode was entered until inclusively this block.
200		Entered { until: BlockNumberFor<T> },
201
202		/// The safe-mode was extended until inclusively this block.
203		Extended { until: BlockNumberFor<T> },
204
205		/// Exited the safe-mode for a specific reason.
206		Exited { reason: ExitReason },
207
208		/// An account reserved funds for either entering or extending the safe-mode.
209		DepositPlaced { account: T::AccountId, amount: BalanceOf<T> },
210
211		/// An account had a reserve released that was reserved.
212		DepositReleased { account: T::AccountId, amount: BalanceOf<T> },
213
214		/// An account had reserve slashed that was reserved.
215		DepositSlashed { account: T::AccountId, amount: BalanceOf<T> },
216
217		/// Could not hold funds for entering or extending the safe-mode.
218		///
219		/// This error comes from the underlying `Currency`.
220		CannotDeposit,
221
222		/// Could not release funds for entering or extending the safe-mode.
223		///
224		/// This error comes from the underlying `Currency`.
225		CannotRelease,
226	}
227
228	/// The reason why the safe-mode was deactivated.
229	#[derive(
230		Copy,
231		Clone,
232		PartialEq,
233		Eq,
234		Debug,
235		Encode,
236		Decode,
237		DecodeWithMemTracking,
238		TypeInfo,
239		MaxEncodedLen,
240	)]
241	pub enum ExitReason {
242		/// The safe-mode was automatically deactivated after it's duration ran out.
243		Timeout,
244
245		/// The safe-mode was forcefully deactivated by [`Pallet::force_exit`].
246		Force,
247	}
248
249	/// Contains the last block number that the safe-mode will remain entered in.
250	///
251	///  Set to `None` when safe-mode is exited.
252	///
253	/// Safe-mode is automatically exited when the current block number exceeds this value.
254	#[pallet::storage]
255	pub type EnteredUntil<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
256
257	/// Holds the reserve that was taken from an account at a specific block number.
258	///
259	/// This helps governance to have an overview of outstanding deposits that should be returned or
260	/// slashed.
261	#[pallet::storage]
262	pub type Deposits<T: Config> = StorageDoubleMap<
263		_,
264		Twox64Concat,
265		T::AccountId,
266		Twox64Concat,
267		BlockNumberFor<T>,
268		BalanceOf<T>,
269		OptionQuery,
270	>;
271
272	/// Configure the initial state of this pallet in the genesis block.
273	#[pallet::genesis_config]
274	#[derive(DefaultNoBound)]
275	pub struct GenesisConfig<T: Config> {
276		pub entered_until: Option<BlockNumberFor<T>>,
277	}
278
279	#[pallet::genesis_build]
280	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
281		fn build(&self) {
282			if let Some(block) = self.entered_until {
283				EnteredUntil::<T>::put(block);
284			}
285		}
286	}
287
288	/// A reason for the pallet placing a hold on funds.
289	#[pallet::composite_enum]
290	pub enum HoldReason {
291		/// Funds are held for entering or extending the safe-mode.
292		#[codec(index = 0)]
293		EnterOrExtend,
294	}
295
296	#[pallet::call]
297	impl<T: Config> Pallet<T> {
298		/// Enter safe-mode permissionlessly for [`Config::EnterDuration`] blocks.
299		///
300		/// Reserves [`Config::EnterDepositAmount`] from the caller's account.
301		/// Emits an [`Event::Entered`] event on success.
302		/// Errors with [`Error::Entered`] if the safe-mode is already entered.
303		/// Errors with [`Error::NotConfigured`] if the deposit amount is `None`.
304		#[pallet::call_index(0)]
305		#[pallet::weight(T::WeightInfo::enter())]
306		pub fn enter(origin: OriginFor<T>) -> DispatchResult {
307			let who = ensure_signed(origin)?;
308
309			Self::do_enter(Some(who), T::EnterDuration::get()).map_err(Into::into)
310		}
311
312		/// Enter safe-mode by force for a per-origin configured number of blocks.
313		///
314		/// Emits an [`Event::Entered`] event on success.
315		/// Errors with [`Error::Entered`] if the safe-mode is already entered.
316		///
317		/// Can only be called by the [`Config::ForceEnterOrigin`] origin.
318		#[pallet::call_index(1)]
319		#[pallet::weight(T::WeightInfo::force_enter())]
320		pub fn force_enter(origin: OriginFor<T>) -> DispatchResult {
321			let duration = T::ForceEnterOrigin::ensure_origin(origin)?;
322
323			Self::do_enter(None, duration).map_err(Into::into)
324		}
325
326		/// Extend the safe-mode permissionlessly for [`Config::ExtendDuration`] blocks.
327		///
328		/// This accumulates on top of the current remaining duration.
329		/// Reserves [`Config::ExtendDepositAmount`] from the caller's account.
330		/// Emits an [`Event::Extended`] event on success.
331		/// Errors with [`Error::Exited`] if the safe-mode is entered.
332		/// Errors with [`Error::NotConfigured`] if the deposit amount is `None`.
333		///
334		/// This may be called by any signed origin with [`Config::ExtendDepositAmount`] free
335		/// currency to reserve. This call can be disabled for all origins by configuring
336		/// [`Config::ExtendDepositAmount`] to `None`.
337		#[pallet::call_index(2)]
338		#[pallet::weight(T::WeightInfo::extend())]
339		pub fn extend(origin: OriginFor<T>) -> DispatchResult {
340			let who = ensure_signed(origin)?;
341
342			Self::do_extend(Some(who), T::ExtendDuration::get()).map_err(Into::into)
343		}
344
345		/// Extend the safe-mode by force for a per-origin configured number of blocks.
346		///
347		/// Emits an [`Event::Extended`] event on success.
348		/// Errors with [`Error::Exited`] if the safe-mode is inactive.
349		///
350		/// Can only be called by the [`Config::ForceExtendOrigin`] origin.
351		#[pallet::call_index(3)]
352		#[pallet::weight(T::WeightInfo::force_extend())]
353		pub fn force_extend(origin: OriginFor<T>) -> DispatchResult {
354			let duration = T::ForceExtendOrigin::ensure_origin(origin)?;
355
356			Self::do_extend(None, duration).map_err(Into::into)
357		}
358
359		/// Exit safe-mode by force.
360		///
361		/// Emits an [`Event::Exited`] with [`ExitReason::Force`] event on success.
362		/// Errors with [`Error::Exited`] if the safe-mode is inactive.
363		///
364		/// Note: `safe-mode` will be automatically deactivated by [`Pallet::on_initialize`] hook
365		/// after the block height is greater than the [`EnteredUntil`] storage item.
366		/// Emits an [`Event::Exited`] with [`ExitReason::Timeout`] event when deactivated in the
367		/// hook.
368		#[pallet::call_index(4)]
369		#[pallet::weight(T::WeightInfo::force_exit())]
370		pub fn force_exit(origin: OriginFor<T>) -> DispatchResult {
371			T::ForceExitOrigin::ensure_origin(origin)?;
372
373			Self::do_exit(ExitReason::Force).map_err(Into::into)
374		}
375
376		/// Slash a deposit for an account that entered or extended safe-mode at a given
377		/// historical block.
378		///
379		/// This can only be called while safe-mode is entered.
380		///
381		/// Emits a [`Event::DepositSlashed`] event on success.
382		/// Errors with [`Error::Entered`] if safe-mode is entered.
383		///
384		/// Can only be called by the [`Config::ForceDepositOrigin`] origin.
385		#[pallet::call_index(5)]
386		#[pallet::weight(T::WeightInfo::force_slash_deposit())]
387		pub fn force_slash_deposit(
388			origin: OriginFor<T>,
389			account: T::AccountId,
390			block: BlockNumberFor<T>,
391		) -> DispatchResult {
392			T::ForceDepositOrigin::ensure_origin(origin)?;
393
394			Self::do_force_deposit(account, block).map_err(Into::into)
395		}
396
397		/// Permissionlessly release a deposit for an account that entered safe-mode at a
398		/// given historical block.
399		///
400		/// The call can be completely disabled by setting [`Config::ReleaseDelay`] to `None`.
401		/// This cannot be called while safe-mode is entered and not until
402		/// [`Config::ReleaseDelay`] blocks have passed since safe-mode was entered.
403		///
404		/// Emits a [`Event::DepositReleased`] event on success.
405		/// Errors with [`Error::Entered`] if the safe-mode is entered.
406		/// Errors with [`Error::CannotReleaseYet`] if [`Config::ReleaseDelay`] block have not
407		/// passed since safe-mode was entered. Errors with [`Error::NoDeposit`] if the payee has no
408		/// reserved currency at the block specified.
409		#[pallet::call_index(6)]
410		#[pallet::weight(T::WeightInfo::release_deposit())]
411		pub fn release_deposit(
412			origin: OriginFor<T>,
413			account: T::AccountId,
414			block: BlockNumberFor<T>,
415		) -> DispatchResult {
416			ensure_signed(origin)?;
417
418			Self::do_release(false, account, block).map_err(Into::into)
419		}
420
421		/// Force to release a deposit for an account that entered safe-mode at a given
422		/// historical block.
423		///
424		/// This can be called while safe-mode is still entered.
425		///
426		/// Emits a [`Event::DepositReleased`] event on success.
427		/// Errors with [`Error::Entered`] if safe-mode is entered.
428		/// Errors with [`Error::NoDeposit`] if the payee has no reserved currency at the
429		/// specified block.
430		///
431		/// Can only be called by the [`Config::ForceDepositOrigin`] origin.
432		#[pallet::call_index(7)]
433		#[pallet::weight(T::WeightInfo::force_release_deposit())]
434		pub fn force_release_deposit(
435			origin: OriginFor<T>,
436			account: T::AccountId,
437			block: BlockNumberFor<T>,
438		) -> DispatchResult {
439			T::ForceDepositOrigin::ensure_origin(origin)?;
440
441			Self::do_release(true, account, block).map_err(Into::into)
442		}
443	}
444
445	#[pallet::hooks]
446	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
447		/// Automatically exits safe-mode when the current block number is greater than
448		/// [`EnteredUntil`].
449		fn on_initialize(current: BlockNumberFor<T>) -> Weight {
450			let Some(limit) = EnteredUntil::<T>::get() else {
451				return T::WeightInfo::on_initialize_noop();
452			};
453
454			if current > limit {
455				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");
456				T::WeightInfo::on_initialize_exit()
457			} else {
458				T::WeightInfo::on_initialize_noop()
459			}
460		}
461	}
462}
463
464impl<T: Config> Pallet<T> {
465	/// Logic for the [`crate::Pallet::enter`] and [`crate::Pallet::force_enter`] calls.
466	pub(crate) fn do_enter(
467		who: Option<T::AccountId>,
468		duration: BlockNumberFor<T>,
469	) -> Result<(), Error<T>> {
470		ensure!(!Self::is_entered(), Error::<T>::Entered);
471
472		if let Some(who) = who {
473			let amount = T::EnterDepositAmount::get().ok_or(Error::<T>::NotConfigured)?;
474			Self::hold(who, amount)?;
475		}
476
477		let until = <frame_system::Pallet<T>>::block_number().saturating_add(duration);
478		EnteredUntil::<T>::put(until);
479		Self::deposit_event(Event::Entered { until });
480		T::Notify::entered();
481		Ok(())
482	}
483
484	/// Logic for the [`crate::Pallet::extend`] and [`crate::Pallet::force_extend`] calls.
485	pub(crate) fn do_extend(
486		who: Option<T::AccountId>,
487		duration: BlockNumberFor<T>,
488	) -> Result<(), Error<T>> {
489		let mut until = EnteredUntil::<T>::get().ok_or(Error::<T>::Exited)?;
490
491		if let Some(who) = who {
492			let amount = T::ExtendDepositAmount::get().ok_or(Error::<T>::NotConfigured)?;
493			Self::hold(who, amount)?;
494		}
495
496		until.saturating_accrue(duration);
497		EnteredUntil::<T>::put(until);
498		Self::deposit_event(Event::<T>::Extended { until });
499		Ok(())
500	}
501
502	/// Logic for the [`crate::Pallet::force_exit`] call.
503	///
504	/// Errors if safe-mode is already exited.
505	pub(crate) fn do_exit(reason: ExitReason) -> Result<(), Error<T>> {
506		let _until = EnteredUntil::<T>::take().ok_or(Error::<T>::Exited)?;
507		Self::deposit_event(Event::Exited { reason });
508		T::Notify::exited();
509		Ok(())
510	}
511
512	/// Logic for the [`crate::Pallet::release_deposit`] and
513	/// [`crate::Pallet::force_release_deposit`] calls.
514	pub(crate) fn do_release(
515		force: bool,
516		account: T::AccountId,
517		block: BlockNumberFor<T>,
518	) -> Result<(), Error<T>> {
519		let amount = Deposits::<T>::take(&account, &block).ok_or(Error::<T>::NoDeposit)?;
520
521		if !force {
522			ensure!(!Self::is_entered(), Error::<T>::Entered);
523
524			let delay = T::ReleaseDelay::get().ok_or(Error::<T>::NotConfigured)?;
525			let now = <frame_system::Pallet<T>>::block_number();
526			ensure!(now > block.saturating_add(delay), Error::<T>::CannotReleaseYet);
527		}
528
529		let amount = T::Currency::release(
530			&&HoldReason::EnterOrExtend.into(),
531			&account,
532			amount,
533			Precision::BestEffort,
534		)
535		.map_err(|_| Error::<T>::CurrencyError)?;
536		Self::deposit_event(Event::<T>::DepositReleased { account, amount });
537		Ok(())
538	}
539
540	/// Logic for the [`crate::Pallet::slash_deposit`] call.
541	pub(crate) fn do_force_deposit(
542		account: T::AccountId,
543		block: BlockNumberFor<T>,
544	) -> Result<(), Error<T>> {
545		let amount = Deposits::<T>::take(&account, block).ok_or(Error::<T>::NoDeposit)?;
546
547		let burned = T::Currency::burn_held(
548			&&HoldReason::EnterOrExtend.into(),
549			&account,
550			amount,
551			Precision::BestEffort,
552			Fortitude::Force,
553		)
554		.map_err(|_| Error::<T>::CurrencyError)?;
555		defensive_assert!(burned == amount, "Could not burn the full held amount");
556		Self::deposit_event(Event::<T>::DepositSlashed { account, amount });
557		Ok(())
558	}
559
560	/// Place a hold for exactly `amount` and store it in `Deposits`.
561	///
562	/// Errors if the account already has a hold for the same reason.
563	fn hold(who: T::AccountId, amount: BalanceOf<T>) -> Result<(), Error<T>> {
564		let block = <frame_system::Pallet<T>>::block_number();
565		if !T::Currency::balance_on_hold(&HoldReason::EnterOrExtend.into(), &who).is_zero() {
566			return Err(Error::<T>::AlreadyDeposited.into());
567		}
568
569		T::Currency::hold(&HoldReason::EnterOrExtend.into(), &who, amount)
570			.map_err(|_| Error::<T>::CurrencyError)?;
571		Deposits::<T>::insert(&who, block, amount);
572		Self::deposit_event(Event::<T>::DepositPlaced { account: who, amount });
573
574		Ok(())
575	}
576
577	/// Return whether `safe-mode` is entered.
578	pub fn is_entered() -> bool {
579		EnteredUntil::<T>::exists()
580	}
581
582	/// Return whether the given call is allowed to be dispatched.
583	pub fn is_allowed(call: &T::RuntimeCall) -> bool
584	where
585		T::RuntimeCall: GetCallMetadata,
586	{
587		let CallMetadata { pallet_name, .. } = call.get_call_metadata();
588		// SAFETY: The `SafeMode` pallet is always allowed.
589		if pallet_name == <Pallet<T> as PalletInfoAccess>::name() {
590			return true;
591		}
592
593		if Self::is_entered() {
594			T::WhitelistedCalls::contains(call)
595		} else {
596			true
597		}
598	}
599}
600
601impl<T: Config> Contains<T::RuntimeCall> for Pallet<T>
602where
603	T::RuntimeCall: GetCallMetadata,
604{
605	/// Return whether the given call is allowed to be dispatched.
606	fn contains(call: &T::RuntimeCall) -> bool {
607		Pallet::<T>::is_allowed(call)
608	}
609}
610
611impl<T: Config> frame::traits::SafeMode for Pallet<T> {
612	type BlockNumber = BlockNumberFor<T>;
613
614	fn is_entered() -> bool {
615		Self::is_entered()
616	}
617
618	fn remaining() -> Option<BlockNumberFor<T>> {
619		EnteredUntil::<T>::get().map(|until| {
620			let now = <frame_system::Pallet<T>>::block_number();
621			until.saturating_sub(now)
622		})
623	}
624
625	fn enter(duration: BlockNumberFor<T>) -> Result<(), frame::traits::SafeModeError> {
626		Self::do_enter(None, duration).map_err(Into::into)
627	}
628
629	fn extend(duration: BlockNumberFor<T>) -> Result<(), frame::traits::SafeModeError> {
630		Self::do_extend(None, duration).map_err(Into::into)
631	}
632
633	fn exit() -> Result<(), frame::traits::SafeModeError> {
634		Self::do_exit(ExitReason::Force).map_err(Into::into)
635	}
636}
637
638impl<T: Config> From<Error<T>> for frame::traits::SafeModeError {
639	fn from(err: Error<T>) -> Self {
640		match err {
641			Error::<T>::Entered => Self::AlreadyEntered,
642			Error::<T>::Exited => Self::AlreadyExited,
643			_ => Self::Unknown,
644		}
645	}
646}