pallet_time_release/
lib.rs

1//! Transfer Funds as Frozen with a Time Delayed Release
2//!
3//! ## Quick Links
4//! - [Configuration: `Config`](Config)
5//! - [Extrinsics: `Call`](Call)
6//! - [Event Enum: `Event`](Event)
7//! - [Error Enum: `Error`](Error)
8#![doc = include_str!("../README.md")]
9// Substrate macros are tripping the clippy::expect_used lint.
10#![allow(clippy::expect_used)]
11#![cfg_attr(not(feature = "std"), no_std)]
12// Strong Documentation Lints
13#![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	/// Scheduled item used for configuring genesis.
74	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	/// A reason for freezing funds.
83	/// Creates a freeze reason for this pallet that is aggregated by `construct_runtime`.
84	#[pallet::composite_enum]
85	pub enum FreezeReason {
86		/// Funds are currently frozen and are not yet liquid.
87		TimeReleaseVesting,
88	}
89
90	/// A reason for holding funds
91	/// Create a hold reason for this pallet that is aggregated by 'contract_runtime'.
92	#[pallet::composite_enum]
93	pub enum HoldReason {
94		/// Funds reserved/held for scheduled transfers
95		TimeReleaseScheduledVesting,
96	}
97
98	/// the storage version for this pallet
99	pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2);
100
101	/// Custom origin to be used when scheduling a transfer
102	#[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		/// A scheduled release triggered by the TimeRelease pallet.
116		TimeRelease(T::AccountId),
117	}
118
119	#[pallet::config]
120	pub trait Config: frame_system::Config {
121		/// The overarching event type.
122		#[allow(deprecated)]
123		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
124
125		/// The overarching freeze reason.
126		type RuntimeFreezeReason: From<FreezeReason>;
127
128		/// Overarching hold reason.
129		type RuntimeHoldReason: From<HoldReason>;
130
131		/// runtime origin
132		type RuntimeOrigin: From<Origin<Self>> + From<<Self as frame_system::Config>::RuntimeOrigin>;
133
134		/// We need MaybeSerializeDeserialize because of the genesis config.
135		type Balance: Balance + MaybeSerializeDeserialize;
136
137		/// The currency trait used to set a freeze on a balance.
138		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		/// The minimum amount transferred to call `transfer`.
146		type MinReleaseTransfer: Get<BalanceOf<Self>>;
147
148		/// Required origin for time-release transfer.
149		type TransferOrigin: EnsureOrigin<
150			<Self as frame_system::Config>::RuntimeOrigin,
151			Success = Self::AccountId,
152		>;
153
154		/// Timerelase origin
155		type TimeReleaseOrigin: EnsureOrigin<
156			<Self as frame_system::Config>::RuntimeOrigin,
157			Success = Self::AccountId,
158		>;
159
160		/// Weight information for extrinsics in this module.
161		type WeightInfo: WeightInfo;
162
163		/// The maximum release schedules.
164		type MaxReleaseSchedules: Get<u32>;
165
166		/// The block-number provider.
167		type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
168
169		/// The aggregate call type
170		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		/// The scheduler provider
178		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		/// Release period is zero
188		ZeroReleasePeriod,
189		/// Period-count is zero
190		ZeroReleasePeriodCount,
191		/// Insufficient amount of balance to freeze
192		InsufficientBalanceToFreeze,
193		/// This account have too many release schedules
194		TooManyReleaseSchedules,
195		/// The transfer amount is too low
196		AmountLow,
197		/// Failed because the maximum release schedules was exceeded
198		MaxReleaseSchedulesExceeded,
199		/// A scheduled transfer with the same identifier already exists.
200		DuplicateScheduleName,
201		/// No scheduled transfer found for the given identifier.
202		NotFound,
203	}
204
205	#[pallet::event]
206	#[pallet::generate_deposit(fn deposit_event)]
207	pub enum Event<T: Config> {
208		/// Added new release schedule.
209		ReleaseScheduleAdded {
210			/// The account-id of the sender.
211			from: T::AccountId,
212			/// The account-id of the receiver.
213			to: T::AccountId,
214			/// The schedule indicating when transfer releases happen.
215			release_schedule: ReleaseScheduleOf<T>,
216		},
217		/// Claimed release.
218		Claimed {
219			/// The account-id of the claimed amount.
220			who: T::AccountId,
221			/// The amount claimed.
222			amount: BalanceOf<T>,
223		},
224		/// Updated release schedules.
225		ReleaseSchedulesUpdated {
226			/// The account-id for which a release schedule updated was made for.
227			who: T::AccountId,
228		},
229	}
230
231	/// Release schedules of an account.
232	///
233	/// ReleaseSchedules: `map AccountId => Vec<ReleaseSchedule>`
234	#[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	/// Tracks the amount of funds reserved for each scheduled transfer, keyed by the transfer's identifier.
244	#[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		/// Phantom data.
252		#[serde(skip)]
253		pub _config: core::marker::PhantomData<T>,
254		/// A list of schedules to include.
255		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		/// Claim thawed balances.
312		///
313		/// # Events
314		/// * [`Event::Claimed`]
315		///
316		#[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		/// Add a new release schedule for an account.
327		///
328		/// # Events
329		/// * [`Event::ReleaseScheduleAdded]
330		///
331		/// # Errors
332		///
333		/// * [`Error::InsufficientBalanceToFreeze] - Insufficient amount of balance to freeze.
334		/// * [`Error::MaxReleaseSchedulesExceeded] - Failed because the maximum release schedules was exceeded-
335		/// * [`ArithmeticError::Overflow] - Failed because of an overflow.
336		///
337		#[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		/// Update all release schedules under an account, `root` origin required.
366		///
367		/// # Events
368		/// * [`Event::ReleaseSchedulesUpdated] - Updated release schedules.
369		///
370		/// # Errors
371		///
372		/// * [`Error::InsufficientBalanceToFreeze] - Insufficient amount of balance to freeze.
373		/// * [`Error::MaxReleaseSchedulesExceeded] - Failed because the maximum release schedules was exceeded-
374		/// * [`ArithmeticError::Overflow] - Failed because of an overflow.
375		///
376		#[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		/// Claim thawed balances on behalf for an account.
393		///
394		/// # Events
395		/// * [`Event::Claimed`]
396		///
397		#[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		/// This function schedules a transfer by calling the `Scheduler` pallet's `schedule` function.
412		/// The transfer will be executed at the specified block number.
413		///
414		/// * [`Error::InsufficientBalanceToFreeze`] - Insufficient amount of balance to freeze.
415		/// * [`Error::MaxReleaseSchedulesExceeded`] - Failed because the maximum release schedules was exceeded-
416		/// * [`ArithmeticError::Overflow] - Failed because of an overflow.
417		///
418		#[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		/// Cancels a scheduled transfer by its unique identifier.
454		/// Releases any funds held for the specified transfer.
455		#[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		/// Execute a scheduled transfer
471		///
472		/// This function is called to execute a transfer that was previously scheduled.
473		/// It ensures that the origin is valid, the destination account exists, and the
474		/// release schedule is valid. It then finalizes the vesting transfer from the
475		/// hold balance and emits an event indicating that the release schedule was added.
476		///
477		/// # Arguments
478		///
479		/// * `origin` - The origin of the call, which must be the TimeRelease origin.
480		/// * `dest` - The destination account to which the funds will be transferred.
481		/// * `schedule` - The release schedule that specifies the amount and timing of the transfer.
482		///
483		/// # Errors
484		///
485		/// * [`ArithmeticError::Overflow`] - If the total amount in the schedule overflows.
486		/// * [`Error::InsufficientBalanceToFreeze`] - If the hold balance is insufficient.
487		/// * [`Error::ZeroReleasePeriod`] - If the release period is zero.
488		/// * [`Error::ZeroReleasePeriodCount`] - If the release period count is zero.
489		/// * [`Error::AmountLow`] - If the total amount is below the minimum release transfer amount.
490		///
491		/// # Events
492		///
493		/// * [`Event::ReleaseScheduleAdded`] - Indicates that a new release schedule was added.
494		#[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	/// Deletes schedules that have released all funds up to a block-number.
541	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	/// Returns frozen balance based on current block number.
558	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		// empty release schedules cleanup the storage and thaw the funds
603		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	/// Transfers the specified amount from the hold balance of the `from` account to the `to` account.
706	///
707	/// # Arguments
708	///
709	/// * `from` - The account from which the funds will be transferred.
710	/// * `to` - The account to which the funds will be transferred.
711	/// * `schedule_amount` - The amount to be transferred.
712	///
713	/// # Errors
714	///
715	/// * `ArithmeticError::Overflow` - If the transfer amount overflows.
716	fn transfer_from_hold_balance(
717		from: &T::AccountId,
718		to: &T::AccountId,
719		schedule_amount: BalanceOf<T>,
720	) -> DispatchResult {
721		// Transfer held funds into a destination account.
722		//
723		// If `mode` is `OnHold`, then the destination account must already exist and the assets
724		// transferred will still be on hold in the destination account. If not, then the destination
725		// account need not already exist, but must be creatable.
726		//
727		// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without
728		// error.
729		//
730		// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be
731		// left as `Polite` in most circumstances, but when you want the same power as a `slash`, it
732		// may be `Force`.
733		//
734		// The actual amount transferred is returned, or `Err` in the case of error and nothing is
735		// changed.
736		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
782/// Returns `Ok(total_total)` if valid schedule, or error.
783fn 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}