1#![doc = include_str!("../README.md")]
9#![allow(clippy::expect_used)]
11#![cfg_attr(not(feature = "std"), no_std)]
12#![deny(
14 rustdoc::broken_intra_doc_links,
15 rustdoc::missing_crate_level_docs,
16 rustdoc::invalid_codeblock_attributes,
17 missing_docs
18)]
19
20use frame_support::{
21 dispatch::DispatchResult,
22 ensure,
23 pallet_prelude::*,
24 traits::{
25 tokens::{
26 fungible::{Inspect as InspectFungible, InspectHold, Mutate, MutateFreeze, MutateHold},
27 Balance,
28 Fortitude::Polite,
29 Precision::Exact,
30 Preservation,
31 Restriction::Free,
32 },
33 BuildGenesisConfig, EnsureOrigin, Get,
34 },
35 BoundedVec,
36};
37use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
38use sp_runtime::{
39 traits::{BlockNumberProvider, CheckedAdd, StaticLookup, Zero},
40 ArithmeticError,
41};
42extern crate alloc;
43use alloc::{boxed::Box, vec::Vec};
44
45#[cfg(test)]
46mod mock;
47#[cfg(test)]
48mod tests;
49
50pub mod types;
51pub use types::*;
52
53pub mod weights;
54pub use weights::*;
55
56#[cfg(feature = "runtime-benchmarks")]
57mod benchmarking;
58
59pub use module::*;
60
61#[frame_support::pallet]
62pub mod module {
63 use frame_support::{dispatch::PostDispatchInfo, traits::fungible::InspectHold};
64 use sp_runtime::traits::Dispatchable;
65
66 use super::*;
67
68 pub(crate) type BalanceOf<T> = <<T as Config>::Currency as InspectFungible<
69 <T as frame_system::Config>::AccountId,
70 >>::Balance;
71 pub(crate) type ReleaseScheduleOf<T> = ReleaseSchedule<BlockNumberFor<T>, BalanceOf<T>>;
72
73 pub type ScheduledItem<T> = (
75 <T as frame_system::Config>::AccountId,
76 BlockNumberFor<T>,
77 BlockNumberFor<T>,
78 u32,
79 BalanceOf<T>,
80 );
81
82 #[pallet::composite_enum]
85 pub enum FreezeReason {
86 TimeReleaseVesting,
88 }
89
90 #[pallet::composite_enum]
93 pub enum HoldReason {
94 TimeReleaseScheduledVesting,
96 }
97
98 pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
100
101 #[derive(
103 PartialEq,
104 Eq,
105 Clone,
106 MaxEncodedLen,
107 Encode,
108 Decode,
109 DecodeWithMemTracking,
110 TypeInfo,
111 RuntimeDebug,
112 )]
113 #[pallet::origin]
114 pub enum Origin<T: Config> {
115 TimeRelease(T::AccountId),
117 }
118
119 #[pallet::config]
120 pub trait Config: frame_system::Config {
121 #[allow(deprecated)]
123 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
124
125 type RuntimeFreezeReason: From<FreezeReason>;
127
128 type RuntimeHoldReason: From<HoldReason>;
130
131 type RuntimeOrigin: From<Origin<Self>> + From<<Self as frame_system::Config>::RuntimeOrigin>;
133
134 type Balance: Balance + MaybeSerializeDeserialize;
136
137 type Currency: MutateFreeze<Self::AccountId, Id = Self::RuntimeFreezeReason>
139 + InspectFungible<Self::AccountId, Balance = Self::Balance>
140 + Mutate<Self::AccountId>
141 + MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>
142 + InspectHold<Self::AccountId>;
143
144 #[pallet::constant]
145 type MinReleaseTransfer: Get<BalanceOf<Self>>;
147
148 type TransferOrigin: EnsureOrigin<
150 <Self as frame_system::Config>::RuntimeOrigin,
151 Success = Self::AccountId,
152 >;
153
154 type TimeReleaseOrigin: EnsureOrigin<
156 <Self as frame_system::Config>::RuntimeOrigin,
157 Success = Self::AccountId,
158 >;
159
160 type WeightInfo: WeightInfo;
162
163 type MaxReleaseSchedules: Get<u32>;
165
166 type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
168
169 type RuntimeCall: Parameter
171 + Dispatchable<
172 RuntimeOrigin = <Self as frame_system::Config>::RuntimeOrigin,
173 PostInfo = PostDispatchInfo,
174 > + From<Call<Self>>
175 + IsType<<Self as frame_system::Config>::RuntimeCall>;
176
177 type SchedulerProvider: SchedulerProviderTrait<
179 <Self as Config>::RuntimeOrigin,
180 BlockNumberFor<Self>,
181 <Self as Config>::RuntimeCall,
182 >;
183 }
184
185 #[pallet::error]
186 pub enum Error<T> {
187 ZeroReleasePeriod,
189 ZeroReleasePeriodCount,
191 InsufficientBalanceToFreeze,
193 TooManyReleaseSchedules,
195 AmountLow,
197 MaxReleaseSchedulesExceeded,
199 DuplicateScheduleName,
201 NotFound,
203 }
204
205 #[pallet::event]
206 #[pallet::generate_deposit(fn deposit_event)]
207 pub enum Event<T: Config> {
208 ReleaseScheduleAdded {
210 from: T::AccountId,
212 to: T::AccountId,
214 release_schedule: ReleaseScheduleOf<T>,
216 },
217 Claimed {
219 who: T::AccountId,
221 amount: BalanceOf<T>,
223 },
224 ReleaseSchedulesUpdated {
226 who: T::AccountId,
228 },
229 }
230
231 #[pallet::storage]
235 pub type ReleaseSchedules<T: Config> = StorageMap<
236 _,
237 Blake2_128Concat,
238 T::AccountId,
239 BoundedVec<ReleaseScheduleOf<T>, T::MaxReleaseSchedules>,
240 ValueQuery,
241 >;
242
243 #[pallet::storage]
245 pub type ScheduleReservedAmounts<T: Config> =
246 StorageMap<_, Twox64Concat, ScheduleName, BalanceOf<T>>;
247
248 #[pallet::genesis_config]
249 #[derive(frame_support::DefaultNoBound)]
250 pub struct GenesisConfig<T: Config> {
251 #[serde(skip)]
253 pub _config: core::marker::PhantomData<T>,
254 pub schedules: Vec<ScheduledItem<T>>,
256 }
257
258 #[pallet::genesis_build]
259 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
260 fn build(&self) {
261 self.schedules
262 .iter()
263 .for_each(|(who, start, period, period_count, per_period)| {
264 let mut bounded_schedules = ReleaseSchedules::<T>::get(who);
265 bounded_schedules
266 .try_push(ReleaseSchedule {
267 start: *start,
268 period: *period,
269 period_count: *period_count,
270 per_period: *per_period,
271 })
272 .expect("Max release schedules exceeded");
273 let total_amount = bounded_schedules
274 .iter()
275 .try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(
276 Zero::zero(),
277 |acc_amount, schedule| {
278 let amount = ensure_valid_release_schedule::<T>(schedule)?;
279 acc_amount
280 .checked_add(&amount)
281 .ok_or_else(|| ArithmeticError::Overflow.into())
282 },
283 )
284 .expect("Invalid release schedule");
285
286 assert!(
287 T::Currency::balance(who) >= total_amount,
288 "Account does not have enough balance."
289 );
290
291 T::Currency::set_freeze(
292 &FreezeReason::TimeReleaseVesting.into(),
293 who,
294 total_amount,
295 )
296 .expect("Failed to set freeze");
297 ReleaseSchedules::<T>::insert(who, bounded_schedules);
298 });
299 }
300 }
301
302 #[pallet::pallet]
303 #[pallet::storage_version(STORAGE_VERSION)]
304 pub struct Pallet<T>(_);
305
306 #[pallet::hooks]
307 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
308
309 #[pallet::call]
310 impl<T: Config> Pallet<T> {
311 #[pallet::call_index(0)]
317 #[pallet::weight(T::WeightInfo::claim(<T as Config>::MaxReleaseSchedules::get() / 2))]
318 pub fn claim(origin: OriginFor<T>) -> DispatchResult {
319 let who = ensure_signed(origin)?;
320 let frozen_amount = Self::claim_frozen_balance(&who)?;
321
322 Self::deposit_event(Event::Claimed { who, amount: frozen_amount });
323 Ok(())
324 }
325
326 #[pallet::call_index(1)]
338 #[pallet::weight(T::WeightInfo::transfer())]
339 pub fn transfer(
340 origin: OriginFor<T>,
341 dest: <T::Lookup as StaticLookup>::Source,
342 schedule: ReleaseScheduleOf<T>,
343 ) -> DispatchResult {
344 let from = T::TransferOrigin::ensure_origin(origin)?;
345 let to = T::Lookup::lookup(dest)?;
346
347 let total_amount = schedule.total_amount().ok_or(ArithmeticError::Overflow)?;
348 Self::ensure_sufficient_free_balance(&from, &to, total_amount)?;
349
350 Self::finalize_vesting_transfer(
351 &from,
352 &to,
353 schedule.clone(),
354 Self::transfer_from_free_balance,
355 )?;
356
357 Self::deposit_event(Event::ReleaseScheduleAdded {
358 from,
359 to,
360 release_schedule: schedule,
361 });
362 Ok(())
363 }
364
365 #[pallet::call_index(2)]
377 #[pallet::weight(T::WeightInfo::update_release_schedules(release_schedules.len() as u32))]
378 pub fn update_release_schedules(
379 origin: OriginFor<T>,
380 who: <T::Lookup as StaticLookup>::Source,
381 release_schedules: Vec<ReleaseScheduleOf<T>>,
382 ) -> DispatchResult {
383 ensure_root(origin)?;
384
385 let account = T::Lookup::lookup(who)?;
386 Self::do_update_release_schedules(&account, release_schedules)?;
387
388 Self::deposit_event(Event::ReleaseSchedulesUpdated { who: account });
389 Ok(())
390 }
391
392 #[pallet::call_index(3)]
398 #[pallet::weight(T::WeightInfo::claim(<T as Config>::MaxReleaseSchedules::get() / 2))]
399 pub fn claim_for(
400 origin: OriginFor<T>,
401 dest: <T::Lookup as StaticLookup>::Source,
402 ) -> DispatchResult {
403 ensure_signed(origin)?;
404 let who = T::Lookup::lookup(dest)?;
405 let frozen_amount = Self::claim_frozen_balance(&who)?;
406
407 Self::deposit_event(Event::Claimed { who, amount: frozen_amount });
408 Ok(())
409 }
410
411 #[pallet::call_index(4)]
419 #[pallet::weight(T::WeightInfo::schedule_named_transfer().saturating_add(T::WeightInfo::execute_scheduled_named_transfer()))]
420 pub fn schedule_named_transfer(
421 origin: OriginFor<T>,
422 id: ScheduleName,
423 dest: <T::Lookup as StaticLookup>::Source,
424 schedule: ReleaseScheduleOf<T>,
425 when: BlockNumberFor<T>,
426 ) -> DispatchResult {
427 let from = T::TransferOrigin::ensure_origin(origin)?;
428 let to = T::Lookup::lookup(dest.clone())?;
429
430 ensure!(
431 !ScheduleReservedAmounts::<T>::contains_key(id),
432 Error::<T>::DuplicateScheduleName
433 );
434
435 let total_amount = Self::validate_and_get_schedule_amount(&schedule)?;
436 Self::ensure_sufficient_free_balance(&from, &to, total_amount)?;
437
438 Self::hold_funds_for_scheduled_vesting(
439 &from,
440 schedule.total_amount().ok_or(ArithmeticError::Overflow)?,
441 )?;
442
443 Self::insert_reserved_amount(
444 id,
445 schedule.total_amount().ok_or(ArithmeticError::Overflow)?,
446 )?;
447
448 Self::schedule_transfer_for(id, from.clone(), dest, schedule, when)?;
449
450 Ok(())
451 }
452
453 #[pallet::call_index(6)]
456 #[pallet::weight(T::WeightInfo::cancel_scheduled_named_transfer(<T as Config>::MaxReleaseSchedules::get() / 2))]
457 pub fn cancel_scheduled_named_transfer(
458 origin: OriginFor<T>,
459 id: ScheduleName,
460 ) -> DispatchResult {
461 let who = T::TransferOrigin::ensure_origin(origin)?;
462
463 T::SchedulerProvider::cancel(Origin::<T>::TimeRelease(who.clone()).into(), id)?;
464
465 Self::release_reserved_funds_by_id(id, &who)?;
466
467 Ok(())
468 }
469
470 #[pallet::call_index(5)]
495 #[pallet::weight(T::WeightInfo::execute_scheduled_named_transfer())]
496 pub fn execute_scheduled_named_transfer(
497 origin: OriginFor<T>,
498 id: ScheduleName,
499 dest: <T::Lookup as StaticLookup>::Source,
500 schedule: ReleaseScheduleOf<T>,
501 ) -> DispatchResult {
502 let from = T::TimeReleaseOrigin::ensure_origin(origin)?;
503 let to = T::Lookup::lookup(dest)?;
504
505 let total_amount = Self::validate_and_get_schedule_amount(&schedule)?;
506 Self::ensure_sufficient_hold_balance(&from, total_amount)?;
507
508 Self::finalize_vesting_transfer(
509 &from,
510 &to,
511 schedule.clone(),
512 Self::transfer_from_hold_balance,
513 )?;
514
515 ScheduleReservedAmounts::<T>::remove(id);
516
517 Self::deposit_event(Event::ReleaseScheduleAdded {
518 from,
519 to,
520 release_schedule: schedule,
521 });
522
523 Ok(())
524 }
525 }
526}
527
528impl<T: Config> Pallet<T> {
529 fn claim_frozen_balance(who: &T::AccountId) -> Result<BalanceOf<T>, DispatchError> {
530 let frozen = Self::prune_and_get_frozen_balance(who);
531 if frozen.is_zero() {
532 Self::delete_freeze(who)?;
533 } else {
534 Self::update_freeze(who, frozen)?;
535 }
536
537 Ok(frozen)
538 }
539
540 fn prune_schedules_for(
542 who: &T::AccountId,
543 block_number: BlockNumberFor<T>,
544 ) -> BoundedVec<ReleaseScheduleOf<T>, T::MaxReleaseSchedules> {
545 let mut schedules = ReleaseSchedules::<T>::get(who);
546 schedules.retain(|schedule| !schedule.frozen_amount(block_number).is_zero());
547
548 if schedules.is_empty() {
549 ReleaseSchedules::<T>::remove(who);
550 } else {
551 Self::set_schedules_for(who, schedules.clone());
552 }
553
554 schedules
555 }
556
557 fn prune_and_get_frozen_balance(who: &T::AccountId) -> BalanceOf<T> {
559 let now = T::BlockNumberProvider::current_block_number();
560
561 let schedules = Self::prune_schedules_for(who, now);
562
563 let total = schedules
564 .iter()
565 .fold(BalanceOf::<T>::zero(), |acc, schedule| acc + schedule.frozen_amount(now));
566
567 total
568 }
569
570 fn schedule_transfer_for(
571 id: ScheduleName,
572 from: T::AccountId,
573 dest: <T::Lookup as StaticLookup>::Source,
574 schedule: ReleaseScheduleOf<T>,
575 when: BlockNumberFor<T>,
576 ) -> DispatchResult {
577 let schedule_call =
578 <T as self::Config>::RuntimeCall::from(Call::<T>::execute_scheduled_named_transfer {
579 id,
580 dest,
581 schedule,
582 });
583
584 T::SchedulerProvider::schedule(
585 Origin::<T>::TimeRelease(from).into(),
586 id,
587 when,
588 Box::new(schedule_call),
589 )?;
590
591 Ok(())
592 }
593
594 fn do_update_release_schedules(
595 who: &T::AccountId,
596 schedules: Vec<ReleaseScheduleOf<T>>,
597 ) -> DispatchResult {
598 let bounded_schedules =
599 BoundedVec::<ReleaseScheduleOf<T>, T::MaxReleaseSchedules>::try_from(schedules)
600 .map_err(|_| Error::<T>::MaxReleaseSchedulesExceeded)?;
601
602 if bounded_schedules.is_empty() {
604 Self::delete_release_schedules(who)?;
605 return Ok(());
606 }
607
608 let total_amount =
609 bounded_schedules.iter().try_fold::<_, _, Result<BalanceOf<T>, DispatchError>>(
610 Zero::zero(),
611 |acc_amount, schedule| {
612 let amount = ensure_valid_release_schedule::<T>(schedule)?;
613 acc_amount.checked_add(&amount).ok_or_else(|| ArithmeticError::Overflow.into())
614 },
615 )?;
616
617 ensure!(T::Currency::balance(who) >= total_amount, Error::<T>::InsufficientBalanceToFreeze,);
618
619 Self::update_freeze(who, total_amount)?;
620 Self::set_schedules_for(who, bounded_schedules);
621
622 Ok(())
623 }
624
625 fn update_freeze(who: &T::AccountId, frozen: BalanceOf<T>) -> DispatchResult {
626 T::Currency::set_freeze(&FreezeReason::TimeReleaseVesting.into(), who, frozen)?;
627 Ok(())
628 }
629
630 fn hold_funds_for_scheduled_vesting(
631 who: &T::AccountId,
632 hold_amount: BalanceOf<T>,
633 ) -> DispatchResult {
634 T::Currency::hold(&HoldReason::TimeReleaseScheduledVesting.into(), who, hold_amount)?;
635 Ok(())
636 }
637
638 fn release_reserved_funds_by_id(id: ScheduleName, who: &T::AccountId) -> DispatchResult {
639 let amount = ScheduleReservedAmounts::<T>::take(id).ok_or(Error::<T>::NotFound)?;
640
641 T::Currency::release(&HoldReason::TimeReleaseScheduledVesting.into(), who, amount, Exact)?;
642
643 Ok(())
644 }
645
646 fn insert_reserved_amount(id: ScheduleName, amount: BalanceOf<T>) -> DispatchResult {
647 ScheduleReservedAmounts::<T>::insert(id, amount);
648
649 Ok(())
650 }
651
652 fn delete_freeze(who: &T::AccountId) -> DispatchResult {
653 T::Currency::thaw(&FreezeReason::TimeReleaseVesting.into(), who)?;
654 Ok(())
655 }
656
657 fn set_schedules_for(
658 who: &T::AccountId,
659 schedules: BoundedVec<ReleaseScheduleOf<T>, T::MaxReleaseSchedules>,
660 ) {
661 ReleaseSchedules::<T>::insert(who, schedules);
662 }
663
664 fn delete_release_schedules(who: &T::AccountId) -> DispatchResult {
665 <ReleaseSchedules<T>>::remove(who);
666 Self::delete_freeze(who)?;
667 Ok(())
668 }
669
670 fn hold_balance_for(who: &T::AccountId) -> BalanceOf<T> {
671 T::Currency::balance_on_hold(&HoldReason::TimeReleaseScheduledVesting.into(), who)
672 }
673
674 fn ensure_sufficient_hold_balance(from: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {
675 ensure!(Self::hold_balance_for(from) >= amount, Error::<T>::InsufficientBalanceToFreeze);
676
677 Ok(())
678 }
679
680 fn validate_and_get_schedule_amount(
681 schedule: &ReleaseScheduleOf<T>,
682 ) -> Result<BalanceOf<T>, DispatchError> {
683 ensure_valid_release_schedule::<T>(schedule)
684 }
685
686 fn append_release_schedule(
687 recipient: &T::AccountId,
688 schedule: ReleaseScheduleOf<T>,
689 ) -> DispatchResult {
690 <ReleaseSchedules<T>>::try_append(recipient, schedule)
691 .map_err(|_| Error::<T>::MaxReleaseSchedulesExceeded)?;
692 Ok(())
693 }
694
695 fn transfer_from_free_balance(
696 from: &T::AccountId,
697 to: &T::AccountId,
698 schedule_amount: BalanceOf<T>,
699 ) -> DispatchResult {
700 T::Currency::transfer(from, to, schedule_amount, Preservation::Expendable)?;
701
702 Ok(())
703 }
704
705 fn transfer_from_hold_balance(
717 from: &T::AccountId,
718 to: &T::AccountId,
719 schedule_amount: BalanceOf<T>,
720 ) -> DispatchResult {
721 T::Currency::transfer_on_hold(
737 &HoldReason::TimeReleaseScheduledVesting.into(),
738 from,
739 to,
740 schedule_amount,
741 Exact,
742 Free,
743 Polite,
744 )?;
745
746 Ok(())
747 }
748
749 fn ensure_sufficient_free_balance(
750 from: &T::AccountId,
751 to: &T::AccountId,
752 amount: BalanceOf<T>,
753 ) -> DispatchResult {
754 if to == from {
755 ensure!(T::Currency::balance(from) >= amount, Error::<T>::InsufficientBalanceToFreeze,);
756 }
757
758 Ok(())
759 }
760
761 fn finalize_vesting_transfer(
762 from: &T::AccountId,
763 to: &T::AccountId,
764 schedule: ReleaseScheduleOf<T>,
765 transfer_fn: fn(&T::AccountId, &T::AccountId, BalanceOf<T>) -> DispatchResult,
766 ) -> DispatchResult {
767 let schedule_amount = Self::validate_and_get_schedule_amount(&schedule)?;
768
769 let total_amount = Self::prune_and_get_frozen_balance(to)
770 .checked_add(&schedule_amount)
771 .ok_or(ArithmeticError::Overflow)?;
772
773 transfer_fn(from, to, schedule_amount)?;
774 Self::update_freeze(to, total_amount)?;
775
776 Self::append_release_schedule(to, schedule)?;
777
778 Ok(())
779 }
780}
781
782fn ensure_valid_release_schedule<T: Config>(
784 schedule: &ReleaseScheduleOf<T>,
785) -> Result<BalanceOf<T>, DispatchError> {
786 ensure!(!schedule.period.is_zero(), Error::<T>::ZeroReleasePeriod);
787 ensure!(!schedule.period_count.is_zero(), Error::<T>::ZeroReleasePeriodCount);
788 ensure!(schedule.end().is_some(), ArithmeticError::Overflow);
789
790 let total_total = schedule.total_amount().ok_or(ArithmeticError::Overflow)?;
791
792 ensure!(total_total >= T::MinReleaseTransfer::get(), Error::<T>::AmountLow);
793
794 Ok(total_total)
795}