1#![doc = docify::embed!("src/tests.rs", can_activate)]
55#![doc = docify::embed!("src/tests.rs", can_force_activate_with_config_origin)]
58#![doc = docify::embed!("src/tests.rs", can_force_deactivate_with_config_origin)]
61#![cfg_attr(not(feature = "std"), no_std)]
71#![deny(rustdoc::broken_intra_doc_links)]
72
73mod benchmarking;
74pub mod mock;
75mod tests;
76pub mod weights;
77
78use frame::{
79 prelude::{
80 fungible::hold::{Inspect, Mutate},
81 *,
82 },
83 traits::{fungible, CallMetadata, GetCallMetadata, SafeModeNotify},
84};
85
86pub use pallet::*;
87pub use weights::*;
88
89type BalanceOf<T> =
90 <<T as Config>::Currency as fungible::Inspect<<T as frame_system::Config>::AccountId>>::Balance;
91
92#[frame::pallet]
93pub mod pallet {
94 use super::*;
95
96 #[pallet::pallet]
97 pub struct Pallet<T>(PhantomData<T>);
98
99 #[pallet::config]
100 pub trait Config: frame_system::Config {
101 #[allow(deprecated)]
103 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
104
105 type Currency: Inspect<Self::AccountId>
107 + Mutate<Self::AccountId, Reason = Self::RuntimeHoldReason>;
108
109 type RuntimeHoldReason: From<HoldReason>;
111
112 type WhitelistedCalls: Contains<Self::RuntimeCall>;
118
119 #[pallet::constant]
121 type EnterDuration: Get<BlockNumberFor<Self>>;
122
123 #[pallet::constant]
127 type ExtendDuration: Get<BlockNumberFor<Self>>;
128
129 #[pallet::constant]
133 type EnterDepositAmount: Get<Option<BalanceOf<Self>>>;
134
135 #[pallet::constant]
139 type ExtendDepositAmount: Get<Option<BalanceOf<Self>>>;
140
141 type ForceEnterOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;
145
146 type ForceExtendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;
150
151 type ForceExitOrigin: EnsureOrigin<Self::RuntimeOrigin>;
153
154 type ForceDepositOrigin: EnsureOrigin<Self::RuntimeOrigin>;
156
157 type Notify: SafeModeNotify;
159
160 #[pallet::constant]
169 type ReleaseDelay: Get<Option<BlockNumberFor<Self>>>;
170
171 type WeightInfo: WeightInfo;
173 }
174
175 #[pallet::error]
176 pub enum Error<T> {
177 Entered,
179
180 Exited,
182
183 NotConfigured,
185
186 NoDeposit,
188
189 AlreadyDeposited,
191
192 CannotReleaseYet,
194
195 CurrencyError,
197 }
198
199 #[pallet::event]
200 #[pallet::generate_deposit(pub(super) fn deposit_event)]
201 pub enum Event<T: Config> {
202 Entered { until: BlockNumberFor<T> },
204
205 Extended { until: BlockNumberFor<T> },
207
208 Exited { reason: ExitReason },
210
211 DepositPlaced { account: T::AccountId, amount: BalanceOf<T> },
213
214 DepositReleased { account: T::AccountId, amount: BalanceOf<T> },
216
217 DepositSlashed { account: T::AccountId, amount: BalanceOf<T> },
219
220 CannotDeposit,
224
225 CannotRelease,
229 }
230
231 #[derive(
233 Copy,
234 Clone,
235 PartialEq,
236 Eq,
237 RuntimeDebug,
238 Encode,
239 Decode,
240 DecodeWithMemTracking,
241 TypeInfo,
242 MaxEncodedLen,
243 )]
244 pub enum ExitReason {
245 Timeout,
247
248 Force,
250 }
251
252 #[pallet::storage]
258 pub type EnteredUntil<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
259
260 #[pallet::storage]
265 pub type Deposits<T: Config> = StorageDoubleMap<
266 _,
267 Twox64Concat,
268 T::AccountId,
269 Twox64Concat,
270 BlockNumberFor<T>,
271 BalanceOf<T>,
272 OptionQuery,
273 >;
274
275 #[pallet::genesis_config]
277 #[derive(DefaultNoBound)]
278 pub struct GenesisConfig<T: Config> {
279 pub entered_until: Option<BlockNumberFor<T>>,
280 }
281
282 #[pallet::genesis_build]
283 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
284 fn build(&self) {
285 if let Some(block) = self.entered_until {
286 EnteredUntil::<T>::put(block);
287 }
288 }
289 }
290
291 #[pallet::composite_enum]
293 pub enum HoldReason {
294 #[codec(index = 0)]
296 EnterOrExtend,
297 }
298
299 #[pallet::call]
300 impl<T: Config> Pallet<T> {
301 #[pallet::call_index(0)]
308 #[pallet::weight(T::WeightInfo::enter())]
309 pub fn enter(origin: OriginFor<T>) -> DispatchResult {
310 let who = ensure_signed(origin)?;
311
312 Self::do_enter(Some(who), T::EnterDuration::get()).map_err(Into::into)
313 }
314
315 #[pallet::call_index(1)]
322 #[pallet::weight(T::WeightInfo::force_enter())]
323 pub fn force_enter(origin: OriginFor<T>) -> DispatchResult {
324 let duration = T::ForceEnterOrigin::ensure_origin(origin)?;
325
326 Self::do_enter(None, duration).map_err(Into::into)
327 }
328
329 #[pallet::call_index(2)]
341 #[pallet::weight(T::WeightInfo::extend())]
342 pub fn extend(origin: OriginFor<T>) -> DispatchResult {
343 let who = ensure_signed(origin)?;
344
345 Self::do_extend(Some(who), T::ExtendDuration::get()).map_err(Into::into)
346 }
347
348 #[pallet::call_index(3)]
355 #[pallet::weight(T::WeightInfo::force_extend())]
356 pub fn force_extend(origin: OriginFor<T>) -> DispatchResult {
357 let duration = T::ForceExtendOrigin::ensure_origin(origin)?;
358
359 Self::do_extend(None, duration).map_err(Into::into)
360 }
361
362 #[pallet::call_index(4)]
372 #[pallet::weight(T::WeightInfo::force_exit())]
373 pub fn force_exit(origin: OriginFor<T>) -> DispatchResult {
374 T::ForceExitOrigin::ensure_origin(origin)?;
375
376 Self::do_exit(ExitReason::Force).map_err(Into::into)
377 }
378
379 #[pallet::call_index(5)]
389 #[pallet::weight(T::WeightInfo::force_slash_deposit())]
390 pub fn force_slash_deposit(
391 origin: OriginFor<T>,
392 account: T::AccountId,
393 block: BlockNumberFor<T>,
394 ) -> DispatchResult {
395 T::ForceDepositOrigin::ensure_origin(origin)?;
396
397 Self::do_force_deposit(account, block).map_err(Into::into)
398 }
399
400 #[pallet::call_index(6)]
413 #[pallet::weight(T::WeightInfo::release_deposit())]
414 pub fn release_deposit(
415 origin: OriginFor<T>,
416 account: T::AccountId,
417 block: BlockNumberFor<T>,
418 ) -> DispatchResult {
419 ensure_signed(origin)?;
420
421 Self::do_release(false, account, block).map_err(Into::into)
422 }
423
424 #[pallet::call_index(7)]
436 #[pallet::weight(T::WeightInfo::force_release_deposit())]
437 pub fn force_release_deposit(
438 origin: OriginFor<T>,
439 account: T::AccountId,
440 block: BlockNumberFor<T>,
441 ) -> DispatchResult {
442 T::ForceDepositOrigin::ensure_origin(origin)?;
443
444 Self::do_release(true, account, block).map_err(Into::into)
445 }
446 }
447
448 #[pallet::hooks]
449 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
450 fn on_initialize(current: BlockNumberFor<T>) -> Weight {
453 let Some(limit) = EnteredUntil::<T>::get() else {
454 return T::WeightInfo::on_initialize_noop()
455 };
456
457 if current > limit {
458 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");
459 T::WeightInfo::on_initialize_exit()
460 } else {
461 T::WeightInfo::on_initialize_noop()
462 }
463 }
464 }
465}
466
467impl<T: Config> Pallet<T> {
468 pub(crate) fn do_enter(
470 who: Option<T::AccountId>,
471 duration: BlockNumberFor<T>,
472 ) -> Result<(), Error<T>> {
473 ensure!(!Self::is_entered(), Error::<T>::Entered);
474
475 if let Some(who) = who {
476 let amount = T::EnterDepositAmount::get().ok_or(Error::<T>::NotConfigured)?;
477 Self::hold(who, amount)?;
478 }
479
480 let until = <frame_system::Pallet<T>>::block_number().saturating_add(duration);
481 EnteredUntil::<T>::put(until);
482 Self::deposit_event(Event::Entered { until });
483 T::Notify::entered();
484 Ok(())
485 }
486
487 pub(crate) fn do_extend(
489 who: Option<T::AccountId>,
490 duration: BlockNumberFor<T>,
491 ) -> Result<(), Error<T>> {
492 let mut until = EnteredUntil::<T>::get().ok_or(Error::<T>::Exited)?;
493
494 if let Some(who) = who {
495 let amount = T::ExtendDepositAmount::get().ok_or(Error::<T>::NotConfigured)?;
496 Self::hold(who, amount)?;
497 }
498
499 until.saturating_accrue(duration);
500 EnteredUntil::<T>::put(until);
501 Self::deposit_event(Event::<T>::Extended { until });
502 Ok(())
503 }
504
505 pub(crate) fn do_exit(reason: ExitReason) -> Result<(), Error<T>> {
509 let _until = EnteredUntil::<T>::take().ok_or(Error::<T>::Exited)?;
510 Self::deposit_event(Event::Exited { reason });
511 T::Notify::exited();
512 Ok(())
513 }
514
515 pub(crate) fn do_release(
518 force: bool,
519 account: T::AccountId,
520 block: BlockNumberFor<T>,
521 ) -> Result<(), Error<T>> {
522 let amount = Deposits::<T>::take(&account, &block).ok_or(Error::<T>::NoDeposit)?;
523
524 if !force {
525 ensure!(!Self::is_entered(), Error::<T>::Entered);
526
527 let delay = T::ReleaseDelay::get().ok_or(Error::<T>::NotConfigured)?;
528 let now = <frame_system::Pallet<T>>::block_number();
529 ensure!(now > block.saturating_add(delay), Error::<T>::CannotReleaseYet);
530 }
531
532 let amount = T::Currency::release(
533 &&HoldReason::EnterOrExtend.into(),
534 &account,
535 amount,
536 Precision::BestEffort,
537 )
538 .map_err(|_| Error::<T>::CurrencyError)?;
539 Self::deposit_event(Event::<T>::DepositReleased { account, amount });
540 Ok(())
541 }
542
543 pub(crate) fn do_force_deposit(
545 account: T::AccountId,
546 block: BlockNumberFor<T>,
547 ) -> Result<(), Error<T>> {
548 let amount = Deposits::<T>::take(&account, block).ok_or(Error::<T>::NoDeposit)?;
549
550 let burned = T::Currency::burn_held(
551 &&HoldReason::EnterOrExtend.into(),
552 &account,
553 amount,
554 Precision::BestEffort,
555 Fortitude::Force,
556 )
557 .map_err(|_| Error::<T>::CurrencyError)?;
558 defensive_assert!(burned == amount, "Could not burn the full held amount");
559 Self::deposit_event(Event::<T>::DepositSlashed { account, amount });
560 Ok(())
561 }
562
563 fn hold(who: T::AccountId, amount: BalanceOf<T>) -> Result<(), Error<T>> {
567 let block = <frame_system::Pallet<T>>::block_number();
568 if !T::Currency::balance_on_hold(&HoldReason::EnterOrExtend.into(), &who).is_zero() {
569 return Err(Error::<T>::AlreadyDeposited.into())
570 }
571
572 T::Currency::hold(&HoldReason::EnterOrExtend.into(), &who, amount)
573 .map_err(|_| Error::<T>::CurrencyError)?;
574 Deposits::<T>::insert(&who, block, amount);
575 Self::deposit_event(Event::<T>::DepositPlaced { account: who, amount });
576
577 Ok(())
578 }
579
580 pub fn is_entered() -> bool {
582 EnteredUntil::<T>::exists()
583 }
584
585 pub fn is_allowed(call: &T::RuntimeCall) -> bool
587 where
588 T::RuntimeCall: GetCallMetadata,
589 {
590 let CallMetadata { pallet_name, .. } = call.get_call_metadata();
591 if pallet_name == <Pallet<T> as PalletInfoAccess>::name() {
593 return true
594 }
595
596 if Self::is_entered() {
597 T::WhitelistedCalls::contains(call)
598 } else {
599 true
600 }
601 }
602}
603
604impl<T: Config> Contains<T::RuntimeCall> for Pallet<T>
605where
606 T::RuntimeCall: GetCallMetadata,
607{
608 fn contains(call: &T::RuntimeCall) -> bool {
610 Pallet::<T>::is_allowed(call)
611 }
612}
613
614impl<T: Config> frame::traits::SafeMode for Pallet<T> {
615 type BlockNumber = BlockNumberFor<T>;
616
617 fn is_entered() -> bool {
618 Self::is_entered()
619 }
620
621 fn remaining() -> Option<BlockNumberFor<T>> {
622 EnteredUntil::<T>::get().map(|until| {
623 let now = <frame_system::Pallet<T>>::block_number();
624 until.saturating_sub(now)
625 })
626 }
627
628 fn enter(duration: BlockNumberFor<T>) -> Result<(), frame::traits::SafeModeError> {
629 Self::do_enter(None, duration).map_err(Into::into)
630 }
631
632 fn extend(duration: BlockNumberFor<T>) -> Result<(), frame::traits::SafeModeError> {
633 Self::do_extend(None, duration).map_err(Into::into)
634 }
635
636 fn exit() -> Result<(), frame::traits::SafeModeError> {
637 Self::do_exit(ExitReason::Force).map_err(Into::into)
638 }
639}
640
641impl<T: Config> From<Error<T>> for frame::traits::SafeModeError {
642 fn from(err: Error<T>) -> Self {
643 match err {
644 Error::<T>::Entered => Self::AlreadyEntered,
645 Error::<T>::Exited => Self::AlreadyExited,
646 _ => Self::Unknown,
647 }
648 }
649}