1#![cfg_attr(not(feature = "std"), no_std)]
47
48mod benchmarking;
49
50#[cfg(test)]
51mod mock;
52#[cfg(test)]
53mod tests;
54mod vesting_info;
55
56pub mod migrations;
57pub mod weights;
58
59extern crate alloc;
60
61use alloc::vec::Vec;
62use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
63use core::{fmt::Debug, marker::PhantomData};
64use frame_support::{
65 dispatch::DispatchResult,
66 ensure,
67 storage::bounded_vec::BoundedVec,
68 traits::{
69 Currency, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, VestedTransfer,
70 VestingSchedule, WithdrawReasons,
71 },
72 weights::Weight,
73};
74use frame_system::pallet_prelude::BlockNumberFor;
75use scale_info::TypeInfo;
76use sp_runtime::{
77 traits::{
78 AtLeast32BitUnsigned, BlockNumberProvider, Bounded, Convert, MaybeSerializeDeserialize,
79 One, Saturating, StaticLookup, Zero,
80 },
81 DispatchError,
82};
83
84pub use pallet::*;
85pub use vesting_info::*;
86pub use weights::WeightInfo;
87
88type BalanceOf<T> =
89 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
90type MaxLocksOf<T> =
91 <<T as Config>::Currency as LockableCurrency<<T as frame_system::Config>::AccountId>>::MaxLocks;
92type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
93
94const VESTING_ID: LockIdentifier = *b"vesting ";
95
96#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, Debug, MaxEncodedLen, TypeInfo)]
99pub enum Releases {
100 V0,
101 V1,
102}
103
104impl Default for Releases {
105 fn default() -> Self {
106 Releases::V0
107 }
108}
109
110#[derive(Clone, Copy)]
112enum VestingAction {
113 Passive,
115 Remove { index: usize },
117 Merge { index1: usize, index2: usize },
119}
120
121impl VestingAction {
122 fn should_remove(&self, index: usize) -> bool {
124 match self {
125 Self::Passive => false,
126 Self::Remove { index: index1 } => *index1 == index,
127 Self::Merge { index1, index2 } => *index1 == index || *index2 == index,
128 }
129 }
130
131 fn pick_schedules<T: Config>(
133 &self,
134 schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
135 ) -> impl Iterator<Item = VestingInfo<BalanceOf<T>, BlockNumberFor<T>>> + '_ {
136 schedules.into_iter().enumerate().filter_map(move |(index, schedule)| {
137 if self.should_remove(index) {
138 None
139 } else {
140 Some(schedule)
141 }
142 })
143 }
144}
145
146pub struct MaxVestingSchedulesGet<T>(PhantomData<T>);
148impl<T: Config> Get<u32> for MaxVestingSchedulesGet<T> {
149 fn get() -> u32 {
150 T::MAX_VESTING_SCHEDULES
151 }
152}
153
154#[frame_support::pallet]
155pub mod pallet {
156 use super::*;
157 use frame_support::pallet_prelude::*;
158 use frame_system::pallet_prelude::*;
159
160 #[pallet::config]
161 pub trait Config: frame_system::Config {
162 #[allow(deprecated)]
164 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
165
166 type Currency: LockableCurrency<Self::AccountId>;
168
169 type BlockNumberToBalance: Convert<BlockNumberFor<Self>, BalanceOf<Self>>;
171
172 #[pallet::constant]
174 type MinVestedTransfer: Get<BalanceOf<Self>>;
175
176 type WeightInfo: WeightInfo;
178
179 type UnvestedFundsAllowedWithdrawReasons: Get<WithdrawReasons>;
182
183 type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
206
207 const MAX_VESTING_SCHEDULES: u32;
209 }
210
211 #[pallet::extra_constants]
212 impl<T: Config> Pallet<T> {
213 #[pallet::constant_name(MaxVestingSchedules)]
214 fn max_vesting_schedules() -> u32 {
215 T::MAX_VESTING_SCHEDULES
216 }
217 }
218
219 #[pallet::hooks]
220 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
221 fn integrity_test() {
222 assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must be greater than 0");
223 }
224 }
225
226 #[pallet::storage]
228 pub type Vesting<T: Config> = StorageMap<
229 _,
230 Blake2_128Concat,
231 T::AccountId,
232 BoundedVec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>, MaxVestingSchedulesGet<T>>,
233 >;
234
235 #[pallet::storage]
239 pub type StorageVersion<T: Config> = StorageValue<_, Releases, ValueQuery>;
240
241 #[pallet::pallet]
242 pub struct Pallet<T>(_);
243
244 #[pallet::genesis_config]
245 #[derive(frame_support::DefaultNoBound)]
246 pub struct GenesisConfig<T: Config> {
247 pub vesting: Vec<(T::AccountId, BlockNumberFor<T>, BlockNumberFor<T>, BalanceOf<T>)>,
248 }
249
250 #[pallet::genesis_build]
251 impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
252 fn build(&self) {
253 use sp_runtime::traits::Saturating;
254
255 StorageVersion::<T>::put(Releases::V1);
257
258 for &(ref who, begin, length, liquid) in self.vesting.iter() {
264 let balance = T::Currency::free_balance(who);
265 assert!(!balance.is_zero(), "Currencies must be init'd before vesting");
266 let locked = balance.saturating_sub(liquid);
268 let length_as_balance = T::BlockNumberToBalance::convert(length);
269 let per_block = locked / length_as_balance.max(sp_runtime::traits::One::one());
270 let vesting_info = VestingInfo::new(locked, per_block, begin);
271 if !vesting_info.is_valid() {
272 panic!("Invalid VestingInfo params at genesis")
273 };
274
275 Vesting::<T>::try_append(who, vesting_info)
276 .expect("Too many vesting schedules at genesis.");
277 }
278
279 let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get());
283 for (who, schedules) in Vesting::<T>::iter() {
284 let locked = schedules
285 .iter()
286 .map(|s| s.locked())
287 .fold(BalanceOf::<T>::zero(), |a, b| a.saturating_add(b));
288 T::Currency::set_lock(VESTING_ID, &who, locked, reasons);
289 }
290 }
291 }
292
293 #[pallet::event]
294 #[pallet::generate_deposit(pub(super) fn deposit_event)]
295 pub enum Event<T: Config> {
296 VestingCreated { account: T::AccountId, schedule_index: u32 },
298 VestingUpdated { account: T::AccountId, unvested: BalanceOf<T> },
301 VestingCompleted { account: T::AccountId },
303 }
304
305 #[pallet::error]
307 pub enum Error<T> {
308 NotVesting,
310 AtMaxVestingSchedules,
313 AmountLow,
315 ScheduleIndexOutOfBounds,
317 InvalidScheduleParams,
319 }
320
321 #[pallet::call]
322 impl<T: Config> Pallet<T> {
323 #[pallet::call_index(0)]
333 #[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
334 .max(T::WeightInfo::vest_unlocked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
335 )]
336 pub fn vest(origin: OriginFor<T>) -> DispatchResult {
337 let who = ensure_signed(origin)?;
338 Self::do_vest(who)
339 }
340
341 #[pallet::call_index(1)]
353 #[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
354 .max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
355 )]
356 pub fn vest_other(origin: OriginFor<T>, target: AccountIdLookupOf<T>) -> DispatchResult {
357 ensure_signed(origin)?;
358 let who = T::Lookup::lookup(target)?;
359 Self::do_vest(who)
360 }
361
362 #[pallet::call_index(2)]
376 #[pallet::weight(
377 T::WeightInfo::vested_transfer(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
378 )]
379 pub fn vested_transfer(
380 origin: OriginFor<T>,
381 target: AccountIdLookupOf<T>,
382 schedule: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
383 ) -> DispatchResult {
384 let transactor = ensure_signed(origin)?;
385 let target = T::Lookup::lookup(target)?;
386 Self::do_vested_transfer(&transactor, &target, schedule)
387 }
388
389 #[pallet::call_index(3)]
404 #[pallet::weight(
405 T::WeightInfo::force_vested_transfer(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
406 )]
407 pub fn force_vested_transfer(
408 origin: OriginFor<T>,
409 source: AccountIdLookupOf<T>,
410 target: AccountIdLookupOf<T>,
411 schedule: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
412 ) -> DispatchResult {
413 ensure_root(origin)?;
414 let target = T::Lookup::lookup(target)?;
415 let source = T::Lookup::lookup(source)?;
416 Self::do_vested_transfer(&source, &target, schedule)
417 }
418
419 #[pallet::call_index(4)]
441 #[pallet::weight(
442 T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
443 .max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
444 )]
445 pub fn merge_schedules(
446 origin: OriginFor<T>,
447 schedule1_index: u32,
448 schedule2_index: u32,
449 ) -> DispatchResult {
450 let who = ensure_signed(origin)?;
451 if schedule1_index == schedule2_index {
452 return Ok(());
453 };
454 let schedule1_index = schedule1_index as usize;
455 let schedule2_index = schedule2_index as usize;
456
457 let schedules = Vesting::<T>::get(&who).ok_or(Error::<T>::NotVesting)?;
458 let merge_action =
459 VestingAction::Merge { index1: schedule1_index, index2: schedule2_index };
460
461 let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?;
462
463 Self::write_vesting(&who, schedules)?;
464 Self::write_lock(&who, locked_now);
465
466 Ok(())
467 }
468
469 #[pallet::call_index(5)]
476 #[pallet::weight(
477 T::WeightInfo::force_remove_vesting_schedule(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
478 )]
479 pub fn force_remove_vesting_schedule(
480 origin: OriginFor<T>,
481 target: <T::Lookup as StaticLookup>::Source,
482 schedule_index: u32,
483 ) -> DispatchResultWithPostInfo {
484 ensure_root(origin)?;
485 let who = T::Lookup::lookup(target)?;
486
487 let schedules_count = Vesting::<T>::decode_len(&who).unwrap_or_default();
488 ensure!(schedule_index < schedules_count as u32, Error::<T>::InvalidScheduleParams);
489
490 Self::remove_vesting_schedule(&who, schedule_index)?;
491
492 Ok(Some(T::WeightInfo::force_remove_vesting_schedule(
493 MaxLocksOf::<T>::get(),
494 schedules_count as u32,
495 ))
496 .into())
497 }
498 }
499}
500
501impl<T: Config> Pallet<T> {
502 pub fn vesting(
504 account: T::AccountId,
505 ) -> Option<BoundedVec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>, MaxVestingSchedulesGet<T>>>
506 {
507 Vesting::<T>::get(account)
508 }
509
510 fn merge_vesting_info(
513 now: BlockNumberFor<T>,
514 schedule1: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
515 schedule2: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
516 ) -> Option<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>> {
517 let schedule1_ending_block = schedule1.ending_block_as_balance::<T::BlockNumberToBalance>();
518 let schedule2_ending_block = schedule2.ending_block_as_balance::<T::BlockNumberToBalance>();
519 let now_as_balance = T::BlockNumberToBalance::convert(now);
520
521 match (schedule1_ending_block <= now_as_balance, schedule2_ending_block <= now_as_balance) {
523 (true, true) => return None,
525 (true, false) => return Some(schedule2),
528 (false, true) => return Some(schedule1),
529 _ => {},
531 }
532
533 let locked = schedule1
534 .locked_at::<T::BlockNumberToBalance>(now)
535 .saturating_add(schedule2.locked_at::<T::BlockNumberToBalance>(now));
536 debug_assert!(
539 !locked.is_zero(),
540 "merge_vesting_info validation checks failed to catch a locked of 0"
541 );
542
543 let ending_block = schedule1_ending_block.max(schedule2_ending_block);
544 let starting_block = now.max(schedule1.starting_block()).max(schedule2.starting_block());
545
546 let per_block = {
547 let duration = ending_block
548 .saturating_sub(T::BlockNumberToBalance::convert(starting_block))
549 .max(One::one());
550 (locked / duration).max(One::one())
551 };
552
553 let schedule = VestingInfo::new(locked, per_block, starting_block);
554 debug_assert!(schedule.is_valid(), "merge_vesting_info schedule validation check failed");
555
556 Some(schedule)
557 }
558
559 fn do_vested_transfer(
561 source: &T::AccountId,
562 target: &T::AccountId,
563 schedule: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
564 ) -> DispatchResult {
565 ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
567 if !schedule.is_valid() {
568 return Err(Error::<T>::InvalidScheduleParams.into());
569 };
570
571 Self::can_add_vesting_schedule(
573 target,
574 schedule.locked(),
575 schedule.per_block(),
576 schedule.starting_block(),
577 )?;
578
579 T::Currency::transfer(source, target, schedule.locked(), ExistenceRequirement::AllowDeath)?;
580
581 let res = Self::add_vesting_schedule(
585 target,
586 schedule.locked(),
587 schedule.per_block(),
588 schedule.starting_block(),
589 );
590 debug_assert!(res.is_ok(), "Failed to add a schedule when we had to succeed.");
591
592 Ok(())
593 }
594
595 fn report_schedule_updates(
606 schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
607 action: VestingAction,
608 ) -> (Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>, BalanceOf<T>) {
609 let now = T::BlockNumberProvider::current_block_number();
610
611 let mut total_locked_now: BalanceOf<T> = Zero::zero();
612 let filtered_schedules = action
613 .pick_schedules::<T>(schedules)
614 .filter(|schedule| {
615 let locked_now = schedule.locked_at::<T::BlockNumberToBalance>(now);
616 let keep = !locked_now.is_zero();
617 if keep {
618 total_locked_now = total_locked_now.saturating_add(locked_now);
619 }
620 keep
621 })
622 .collect::<Vec<_>>();
623
624 (filtered_schedules, total_locked_now)
625 }
626
627 fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf<T>) {
629 if total_locked_now.is_zero() {
630 T::Currency::remove_lock(VESTING_ID, who);
631 Self::deposit_event(Event::<T>::VestingCompleted { account: who.clone() });
632 } else {
633 let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get());
634 T::Currency::set_lock(VESTING_ID, who, total_locked_now, reasons);
635 Self::deposit_event(Event::<T>::VestingUpdated {
636 account: who.clone(),
637 unvested: total_locked_now,
638 });
639 };
640 }
641
642 fn write_vesting(
644 who: &T::AccountId,
645 schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
646 ) -> Result<(), DispatchError> {
647 let schedules: BoundedVec<
648 VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
649 MaxVestingSchedulesGet<T>,
650 > = schedules.try_into().map_err(|_| Error::<T>::AtMaxVestingSchedules)?;
651
652 if schedules.len() == 0 {
653 Vesting::<T>::remove(&who);
654 } else {
655 Vesting::<T>::insert(who, schedules)
656 }
657
658 Ok(())
659 }
660
661 fn do_vest(who: T::AccountId) -> DispatchResult {
663 let schedules = Vesting::<T>::get(&who).ok_or(Error::<T>::NotVesting)?;
664
665 let (schedules, locked_now) =
666 Self::exec_action(schedules.to_vec(), VestingAction::Passive)?;
667
668 Self::write_vesting(&who, schedules)?;
669 Self::write_lock(&who, locked_now);
670
671 Ok(())
672 }
673
674 fn exec_action(
677 schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
678 action: VestingAction,
679 ) -> Result<(Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>, BalanceOf<T>), DispatchError> {
680 let (schedules, locked_now) = match action {
681 VestingAction::Merge { index1: idx1, index2: idx2 } => {
682 let schedule1 = *schedules.get(idx1).ok_or(Error::<T>::ScheduleIndexOutOfBounds)?;
685 let schedule2 = *schedules.get(idx2).ok_or(Error::<T>::ScheduleIndexOutOfBounds)?;
686
687 let (mut schedules, mut locked_now) =
691 Self::report_schedule_updates(schedules.to_vec(), action);
692
693 let now = T::BlockNumberProvider::current_block_number();
694 if let Some(new_schedule) = Self::merge_vesting_info(now, schedule1, schedule2) {
695 schedules.push(new_schedule);
698 let new_schedule_locked =
700 new_schedule.locked_at::<T::BlockNumberToBalance>(now);
701 locked_now = locked_now.saturating_add(new_schedule_locked);
703 } (schedules, locked_now)
706 },
707 _ => Self::report_schedule_updates(schedules.to_vec(), action),
708 };
709
710 debug_assert!(
711 locked_now > Zero::zero() && schedules.len() > 0 ||
712 locked_now == Zero::zero() && schedules.len() == 0
713 );
714
715 Ok((schedules, locked_now))
716 }
717}
718
719impl<T: Config> frame_support::traits::tokens::VestedPayout<T::AccountId, BalanceOf<T>>
720 for Pallet<T>
721where
722 BalanceOf<T>: MaybeSerializeDeserialize + Debug,
723{
724 type BlockNumber = BlockNumberFor<T>;
725
726 fn vested_transfer(
727 source: &T::AccountId,
728 dest: &T::AccountId,
729 amount: BalanceOf<T>,
730 duration: BlockNumberFor<T>,
731 start_at: Option<BlockNumberFor<T>>,
732 ) -> DispatchResult {
733 if amount.is_zero() {
734 return Ok(());
735 }
736
737 if duration.is_zero() {
738 T::Currency::transfer(source, dest, amount, ExistenceRequirement::AllowDeath)
740 } else {
741 let starting_block =
742 start_at.unwrap_or_else(|| T::BlockNumberProvider::current_block_number());
743 let duration_as_balance = T::BlockNumberToBalance::convert(duration);
744 let per_block =
746 ((amount.saturating_add(duration_as_balance).saturating_sub(One::one())) /
747 duration_as_balance)
748 .max(One::one());
749 let schedule = VestingInfo::new(amount, per_block, starting_block);
750 Self::do_vested_transfer(source, dest, schedule)
751 }
752 }
753}
754
755impl<T: Config> VestingSchedule<T::AccountId> for Pallet<T>
756where
757 BalanceOf<T>: MaybeSerializeDeserialize + Debug,
758{
759 type Currency = T::Currency;
760 type Moment = BlockNumberFor<T>;
761
762 fn vesting_balance(who: &T::AccountId) -> Option<BalanceOf<T>> {
764 if let Some(v) = Vesting::<T>::get(who) {
765 let now = T::BlockNumberProvider::current_block_number();
766 let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| {
767 schedule.locked_at::<T::BlockNumberToBalance>(now).saturating_add(total)
768 });
769 Some(T::Currency::free_balance(who).min(total_locked_now))
770 } else {
771 None
772 }
773 }
774
775 fn add_vesting_schedule(
788 who: &T::AccountId,
789 locked: BalanceOf<T>,
790 per_block: BalanceOf<T>,
791 starting_block: BlockNumberFor<T>,
792 ) -> DispatchResult {
793 if locked.is_zero() {
794 return Ok(());
795 }
796
797 let vesting_schedule = VestingInfo::new(locked, per_block, starting_block);
798 if !vesting_schedule.is_valid() {
800 return Err(Error::<T>::InvalidScheduleParams.into());
801 };
802
803 let mut schedules = Vesting::<T>::get(who).unwrap_or_default();
804
805 ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::<T>::AtMaxVestingSchedules);
808
809 debug_assert!(schedules.len() > 0, "schedules cannot be empty after insertion");
810 let schedule_index = schedules.len() - 1;
811 Self::deposit_event(Event::<T>::VestingCreated {
812 account: who.clone(),
813 schedule_index: schedule_index as u32,
814 });
815
816 let (schedules, locked_now) =
817 Self::exec_action(schedules.to_vec(), VestingAction::Passive)?;
818
819 Self::write_vesting(who, schedules)?;
820 Self::write_lock(who, locked_now);
821
822 Ok(())
823 }
824
825 fn can_add_vesting_schedule(
828 who: &T::AccountId,
829 locked: BalanceOf<T>,
830 per_block: BalanceOf<T>,
831 starting_block: BlockNumberFor<T>,
832 ) -> DispatchResult {
833 if !VestingInfo::new(locked, per_block, starting_block).is_valid() {
835 return Err(Error::<T>::InvalidScheduleParams.into());
836 }
837
838 ensure!(
839 (Vesting::<T>::decode_len(who).unwrap_or_default() as u32) < T::MAX_VESTING_SCHEDULES,
840 Error::<T>::AtMaxVestingSchedules
841 );
842
843 Ok(())
844 }
845
846 fn remove_vesting_schedule(who: &T::AccountId, schedule_index: u32) -> DispatchResult {
848 let schedules = Vesting::<T>::get(who).ok_or(Error::<T>::NotVesting)?;
849 let remove_action = VestingAction::Remove { index: schedule_index as usize };
850
851 let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), remove_action)?;
852
853 Self::write_vesting(who, schedules)?;
854 Self::write_lock(who, locked_now);
855 Ok(())
856 }
857}
858
859impl<T: Config> VestedTransfer<T::AccountId> for Pallet<T>
862where
863 BalanceOf<T>: MaybeSerializeDeserialize + Debug,
864{
865 type Currency = T::Currency;
866 type Moment = BlockNumberFor<T>;
867
868 fn vested_transfer(
869 source: &T::AccountId,
870 target: &T::AccountId,
871 locked: BalanceOf<T>,
872 per_block: BalanceOf<T>,
873 starting_block: BlockNumberFor<T>,
874 ) -> DispatchResult {
875 use frame_support::storage::{with_transaction, TransactionOutcome};
876 let schedule = VestingInfo::new(locked, per_block, starting_block);
877 with_transaction(|| -> TransactionOutcome<DispatchResult> {
878 let result = Self::do_vested_transfer(source, target, schedule);
879
880 match &result {
881 Ok(()) => TransactionOutcome::Commit(result),
882 _ => TransactionOutcome::Rollback(result),
883 }
884 })
885 }
886}