1#![doc = docify::embed!("src/tests.rs", can_activate)]
55#![doc = docify::embed!("src/tests.rs", can_force_activate_with_config_origin)]
57#![doc = docify::embed!("src/tests.rs", can_force_deactivate_with_config_origin)]
59#![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 #[allow(deprecated)]
100 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
101
102 type Currency: Inspect<Self::AccountId>
104 + Mutate<Self::AccountId, Reason = Self::RuntimeHoldReason>;
105
106 type RuntimeHoldReason: From<HoldReason>;
108
109 type WhitelistedCalls: Contains<Self::RuntimeCall>;
115
116 #[pallet::constant]
118 type EnterDuration: Get<BlockNumberFor<Self>>;
119
120 #[pallet::constant]
124 type ExtendDuration: Get<BlockNumberFor<Self>>;
125
126 #[pallet::constant]
130 type EnterDepositAmount: Get<Option<BalanceOf<Self>>>;
131
132 #[pallet::constant]
136 type ExtendDepositAmount: Get<Option<BalanceOf<Self>>>;
137
138 type ForceEnterOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;
142
143 type ForceExtendOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = BlockNumberFor<Self>>;
147
148 type ForceExitOrigin: EnsureOrigin<Self::RuntimeOrigin>;
150
151 type ForceDepositOrigin: EnsureOrigin<Self::RuntimeOrigin>;
153
154 type Notify: SafeModeNotify;
156
157 #[pallet::constant]
166 type ReleaseDelay: Get<Option<BlockNumberFor<Self>>>;
167
168 type WeightInfo: WeightInfo;
170 }
171
172 #[pallet::error]
173 pub enum Error<T> {
174 Entered,
176
177 Exited,
179
180 NotConfigured,
182
183 NoDeposit,
185
186 AlreadyDeposited,
188
189 CannotReleaseYet,
191
192 CurrencyError,
194 }
195
196 #[pallet::event]
197 #[pallet::generate_deposit(pub(super) fn deposit_event)]
198 pub enum Event<T: Config> {
199 Entered { until: BlockNumberFor<T> },
201
202 Extended { until: BlockNumberFor<T> },
204
205 Exited { reason: ExitReason },
207
208 DepositPlaced { account: T::AccountId, amount: BalanceOf<T> },
210
211 DepositReleased { account: T::AccountId, amount: BalanceOf<T> },
213
214 DepositSlashed { account: T::AccountId, amount: BalanceOf<T> },
216
217 CannotDeposit,
221
222 CannotRelease,
226 }
227
228 #[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 Timeout,
244
245 Force,
247 }
248
249 #[pallet::storage]
255 pub type EnteredUntil<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
256
257 #[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 #[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 #[pallet::composite_enum]
290 pub enum HoldReason {
291 #[codec(index = 0)]
293 EnterOrExtend,
294 }
295
296 #[pallet::call]
297 impl<T: Config> Pallet<T> {
298 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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 pub fn is_entered() -> bool {
579 EnteredUntil::<T>::exists()
580 }
581
582 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 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 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}