referrerpolicy=no-referrer-when-downgrade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
// 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.

//! # Safe Mode
//!
//! Trigger for stopping all extrinsics outside of a specific whitelist.
//!
//! ## Pallet API
//!
//! See the [`pallet`] module for more information about the interfaces this pallet 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 frame_system::Config for Runtime {
//!   // …
//!   type BaseCallFilter = InsideBoth<DefaultFilter, SafeMode>;
//!   // …
//! }
//! ```
//!
//! 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_support::{
	defensive_assert,
	pallet_prelude::*,
	traits::{
		fungible::{
			self,
			hold::{Inspect, Mutate},
		},
		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;

pub use pallet::*;
pub use weights::*;

type BalanceOf<T> =
	<<T as Config>::Currency as fungible::Inspect<<T as frame_system::Config>::AccountId>>::Balance;

#[frame_support::pallet]
pub mod pallet {
	use super::*;

	#[pallet::pallet]
	pub struct Pallet<T>(PhantomData<T>);

	#[pallet::config]
	pub trait Config: frame_system::Config {
		/// The overarching event type.
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

		/// Currency type for this pallet, used for Deposits.
		type Currency: Inspect<Self::AccountId>
			+ Mutate<Self::AccountId, Reason = Self::RuntimeHoldReason>;

		/// The hold reason when reserving funds for entering or extending the safe-mode.
		type RuntimeHoldReason: From<HoldReason>;

		/// 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<Self::RuntimeCall>;

		/// For how many blocks the safe-mode will be entered by [`Pallet::enter`].
		#[pallet::constant]
		type EnterDuration: Get<BlockNumberFor<Self>>;

		/// 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<BlockNumberFor<Self>>;

		/// 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<Option<BalanceOf<Self>>>;

		/// 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<Option<BalanceOf<Self>>>;

		/// 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<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;

		/// 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<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;

		/// The origin that may call [`Pallet::force_enter`].
		type ForceExitOrigin: EnsureOrigin<Self::RuntimeOrigin>;

		/// The only origin that can force to release or slash a deposit.
		type ForceDepositOrigin: EnsureOrigin<Self::RuntimeOrigin>;

		/// 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<Option<BlockNumberFor<Self>>>;

		// Weight information for extrinsics in this pallet.
		type WeightInfo: WeightInfo;
	}

	#[pallet::error]
	pub enum Error<T> {
		/// 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<T: Config> {
		/// The safe-mode was entered until inclusively this block.
		Entered { until: BlockNumberFor<T> },

		/// The safe-mode was extended until inclusively this block.
		Extended { until: BlockNumberFor<T> },

		/// 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<T> },

		/// An account had a reserve released that was reserved.
		DepositReleased { account: T::AccountId, amount: BalanceOf<T> },

		/// An account had reserve slashed that was reserved.
		DepositSlashed { account: T::AccountId, amount: BalanceOf<T> },

		/// 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<T: Config> = StorageValue<_, BlockNumberFor<T>, 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<T: Config> = StorageDoubleMap<
		_,
		Twox64Concat,
		T::AccountId,
		Twox64Concat,
		BlockNumberFor<T>,
		BalanceOf<T>,
		OptionQuery,
	>;

	/// Configure the initial state of this pallet in the genesis block.
	#[pallet::genesis_config]
	#[derive(DefaultNoBound)]
	pub struct GenesisConfig<T: Config> {
		pub entered_until: Option<BlockNumberFor<T>>,
	}

	#[pallet::genesis_build]
	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
		fn build(&self) {
			if let Some(block) = self.entered_until {
				EnteredUntil::<T>::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<T: Config> Pallet<T> {
		/// 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<T>) -> 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<T>) -> 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<T>) -> 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<T>) -> 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<T>) -> 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<T>,
			account: T::AccountId,
			block: BlockNumberFor<T>,
		) -> 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<T>,
			account: T::AccountId,
			block: BlockNumberFor<T>,
		) -> 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<T>,
			account: T::AccountId,
			block: BlockNumberFor<T>,
		) -> DispatchResult {
			T::ForceDepositOrigin::ensure_origin(origin)?;

			Self::do_release(true, account, block).map_err(Into::into)
		}
	}

	#[pallet::hooks]
	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
		/// Automatically exits safe-mode when the current block number is greater than
		/// [`EnteredUntil`].
		fn on_initialize(current: BlockNumberFor<T>) -> Weight {
			let Some(limit) = EnteredUntil::<T>::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<T: Config> Pallet<T> {
	/// Logic for the [`crate::Pallet::enter`] and [`crate::Pallet::force_enter`] calls.
	pub(crate) fn do_enter(
		who: Option<T::AccountId>,
		duration: BlockNumberFor<T>,
	) -> Result<(), Error<T>> {
		ensure!(!Self::is_entered(), Error::<T>::Entered);

		if let Some(who) = who {
			let amount = T::EnterDepositAmount::get().ok_or(Error::<T>::NotConfigured)?;
			Self::hold(who, amount)?;
		}

		let until = <frame_system::Pallet<T>>::block_number().saturating_add(duration);
		EnteredUntil::<T>::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<T::AccountId>,
		duration: BlockNumberFor<T>,
	) -> Result<(), Error<T>> {
		let mut until = EnteredUntil::<T>::get().ok_or(Error::<T>::Exited)?;

		if let Some(who) = who {
			let amount = T::ExtendDepositAmount::get().ok_or(Error::<T>::NotConfigured)?;
			Self::hold(who, amount)?;
		}

		until.saturating_accrue(duration);
		EnteredUntil::<T>::put(until);
		Self::deposit_event(Event::<T>::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<T>> {
		let _until = EnteredUntil::<T>::take().ok_or(Error::<T>::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<T>,
	) -> Result<(), Error<T>> {
		let amount = Deposits::<T>::take(&account, &block).ok_or(Error::<T>::NoDeposit)?;

		if !force {
			ensure!(!Self::is_entered(), Error::<T>::Entered);

			let delay = T::ReleaseDelay::get().ok_or(Error::<T>::NotConfigured)?;
			let now = <frame_system::Pallet<T>>::block_number();
			ensure!(now > block.saturating_add(delay), Error::<T>::CannotReleaseYet);
		}

		let amount = T::Currency::release(
			&&HoldReason::EnterOrExtend.into(),
			&account,
			amount,
			Precision::BestEffort,
		)
		.map_err(|_| Error::<T>::CurrencyError)?;
		Self::deposit_event(Event::<T>::DepositReleased { account, amount });
		Ok(())
	}

	/// Logic for the [`crate::Pallet::slash_deposit`] call.
	pub(crate) fn do_force_deposit(
		account: T::AccountId,
		block: BlockNumberFor<T>,
	) -> Result<(), Error<T>> {
		let amount = Deposits::<T>::take(&account, block).ok_or(Error::<T>::NoDeposit)?;

		let burned = T::Currency::burn_held(
			&&HoldReason::EnterOrExtend.into(),
			&account,
			amount,
			Precision::BestEffort,
			Fortitude::Force,
		)
		.map_err(|_| Error::<T>::CurrencyError)?;
		defensive_assert!(burned == amount, "Could not burn the full held amount");
		Self::deposit_event(Event::<T>::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<T>) -> Result<(), Error<T>> {
		let block = <frame_system::Pallet<T>>::block_number();
		if !T::Currency::balance_on_hold(&HoldReason::EnterOrExtend.into(), &who).is_zero() {
			return Err(Error::<T>::AlreadyDeposited.into())
		}

		T::Currency::hold(&HoldReason::EnterOrExtend.into(), &who, amount)
			.map_err(|_| Error::<T>::CurrencyError)?;
		Deposits::<T>::insert(&who, block, amount);
		Self::deposit_event(Event::<T>::DepositPlaced { account: who, amount });

		Ok(())
	}

	/// Return whether `safe-mode` is entered.
	pub fn is_entered() -> bool {
		EnteredUntil::<T>::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 == <Pallet<T> as PalletInfoAccess>::name() {
			return true
		}

		if Self::is_entered() {
			T::WhitelistedCalls::contains(call)
		} else {
			true
		}
	}
}

impl<T: Config> Contains<T::RuntimeCall> for Pallet<T>
where
	T::RuntimeCall: GetCallMetadata,
{
	/// Return whether the given call is allowed to be dispatched.
	fn contains(call: &T::RuntimeCall) -> bool {
		Pallet::<T>::is_allowed(call)
	}
}

impl<T: Config> frame_support::traits::SafeMode for Pallet<T> {
	type BlockNumber = BlockNumberFor<T>;

	fn is_entered() -> bool {
		Self::is_entered()
	}

	fn remaining() -> Option<BlockNumberFor<T>> {
		EnteredUntil::<T>::get().map(|until| {
			let now = <frame_system::Pallet<T>>::block_number();
			until.saturating_sub(now)
		})
	}

	fn enter(duration: BlockNumberFor<T>) -> Result<(), frame_support::traits::SafeModeError> {
		Self::do_enter(None, duration).map_err(Into::into)
	}

	fn extend(duration: BlockNumberFor<T>) -> 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<T: Config> From<Error<T>> for frame_support::traits::SafeModeError {
	fn from(err: Error<T>) -> Self {
		match err {
			Error::<T>::Entered => Self::AlreadyEntered,
			Error::<T>::Exited => Self::AlreadyExited,
			_ => Self::Unknown,
		}
	}
}