pezpallet_vesting/
lib.rs

1// This file is part of Bizinikiwi.
2
3// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Vesting Pezpallet
19//!
20//! - [`Config`]
21//! - [`Call`]
22//!
23//! ## Overview
24//!
25//! A simple pezpallet providing a means of placing a linear curve on an account's locked balance.
26//! This pezpallet ensures that there is a lock in place preventing the balance to drop below the
27//! *unvested* amount for any reason other than the ones specified in
28//! `UnvestedFundsAllowedWithdrawReasons` configuration value.
29//!
30//! As the amount vested increases over time, the amount unvested reduces. However, locks remain in
31//! place and explicit action is needed on behalf of the user to ensure that the amount locked is
32//! equivalent to the amount remaining to be vested. This is done through a dispatchable function,
33//! either `vest` (in typical case where the sender is calling on their own behalf) or `vest_other`
34//! in case the sender is calling on another account's behalf.
35//!
36//! ## Interface
37//!
38//! This pezpallet implements the `VestingSchedule` trait.
39//!
40//! ### Dispatchable Functions
41//!
42//! - `vest` - Update the lock, reducing it in line with the amount "vested" so far.
43//! - `vest_other` - Update the lock of another account, reducing it in line with the amount
44//!   "vested" so far.
45
46#![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 pezframe_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 pezframe_system::pezpallet_prelude::BlockNumberFor;
75use pezsp_runtime::{
76	traits::{
77		AtLeast32BitUnsigned, BlockNumberProvider, Bounded, Convert, MaybeSerializeDeserialize,
78		One, Saturating, StaticLookup, Zero,
79	},
80	DispatchError, RuntimeDebug,
81};
82use scale_info::TypeInfo;
83
84pub use pezpallet::*;
85pub use vesting_info::*;
86pub use weights::WeightInfo;
87
88type BalanceOf<T> =
89	<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
90type MaxLocksOf<T> = <<T as Config>::Currency as LockableCurrency<
91	<T as pezframe_system::Config>::AccountId,
92>>::MaxLocks;
93type AccountIdLookupOf<T> = <<T as pezframe_system::Config>::Lookup as StaticLookup>::Source;
94
95const VESTING_ID: LockIdentifier = *b"vesting ";
96
97// A value placed in storage that represents the current version of the Vesting storage.
98// This value is used by `on_runtime_upgrade` to determine whether we run storage migration logic.
99#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
100pub enum Releases {
101	V0,
102	V1,
103}
104
105impl Default for Releases {
106	fn default() -> Self {
107		Releases::V0
108	}
109}
110
111/// Actions to take against a user's `Vesting` storage entry.
112#[derive(Clone, Copy)]
113enum VestingAction {
114	/// Do not actively remove any schedules.
115	Passive,
116	/// Remove the schedule specified by the index.
117	Remove { index: usize },
118	/// Remove the two schedules, specified by index, so they can be merged.
119	Merge { index1: usize, index2: usize },
120}
121
122impl VestingAction {
123	/// Whether or not the filter says the schedule index should be removed.
124	fn should_remove(&self, index: usize) -> bool {
125		match self {
126			Self::Passive => false,
127			Self::Remove { index: index1 } => *index1 == index,
128			Self::Merge { index1, index2 } => *index1 == index || *index2 == index,
129		}
130	}
131
132	/// Pick the schedules that this action dictates should continue vesting undisturbed.
133	fn pick_schedules<T: Config>(
134		&self,
135		schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
136	) -> impl Iterator<Item = VestingInfo<BalanceOf<T>, BlockNumberFor<T>>> + '_ {
137		schedules.into_iter().enumerate().filter_map(move |(index, schedule)| {
138			if self.should_remove(index) {
139				None
140			} else {
141				Some(schedule)
142			}
143		})
144	}
145}
146
147// Wrapper for `T::MAX_VESTING_SCHEDULES` to satisfy `trait Get`.
148pub struct MaxVestingSchedulesGet<T>(PhantomData<T>);
149impl<T: Config> Get<u32> for MaxVestingSchedulesGet<T> {
150	fn get() -> u32 {
151		T::MAX_VESTING_SCHEDULES
152	}
153}
154
155#[pezframe_support::pezpallet]
156pub mod pezpallet {
157	use super::*;
158	use pezframe_support::pezpallet_prelude::*;
159	use pezframe_system::pezpallet_prelude::*;
160
161	#[pezpallet::config]
162	pub trait Config: pezframe_system::Config {
163		/// The overarching event type.
164		#[allow(deprecated)]
165		type RuntimeEvent: From<Event<Self>>
166			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
167
168		/// The currency trait.
169		type Currency: LockableCurrency<Self::AccountId>;
170
171		/// Convert the block number into a balance.
172		type BlockNumberToBalance: Convert<BlockNumberFor<Self>, BalanceOf<Self>>;
173
174		/// The minimum amount transferred to call `vested_transfer`.
175		#[pezpallet::constant]
176		type MinVestedTransfer: Get<BalanceOf<Self>>;
177
178		/// Weight information for extrinsics in this pezpallet.
179		type WeightInfo: WeightInfo;
180
181		/// Reasons that determine under which conditions the balance may drop below
182		/// the unvested amount.
183		type UnvestedFundsAllowedWithdrawReasons: Get<WithdrawReasons>;
184
185		/// Query the current block number.
186		///
187		/// Must return monotonically increasing values when called from consecutive blocks.
188		/// Can be configured to return either:
189		/// - the local block number of the runtime via `pezframe_system::Pezpallet`
190		/// - a remote block number, eg from the relay chain through `RelaychainDataProvider`
191		/// - an arbitrary value through a custom implementation of the trait
192		///
193		/// There is currently no migration provided to "hot-swap" block number providers and it may
194		/// result in undefined behavior when doing so. Teyrchains are therefore best off setting
195		/// this to their local block number provider if they have the pezpallet already deployed.
196		///
197		/// Suggested values:
198		/// - Solo- and Relay-chains: `pezframe_system::Pezpallet`
199		/// - Teyrchains that may produce blocks sparingly or only when needed (on-demand):
200		///   - already have the pezpallet deployed: `pezframe_system::Pezpallet`
201		///   - are freshly deploying this pezpallet: `RelaychainDataProvider`
202		/// - Teyrchains with a reliably block production rate (PLO or bulk-coretime):
203		///   - already have the pezpallet deployed: `pezframe_system::Pezpallet`
204		///   - are freshly deploying this pezpallet: no strong recommendation. Both local and
205		///     remote providers can be used. Relay provider can be a bit better in cases where the
206		///     teyrchain is lagging its block production to avoid clock skew.
207		type BlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;
208
209		/// Maximum number of vesting schedules an account may have at a given moment.
210		const MAX_VESTING_SCHEDULES: u32;
211	}
212
213	#[pezpallet::extra_constants]
214	impl<T: Config> Pezpallet<T> {
215		#[pezpallet::constant_name(MaxVestingSchedules)]
216		fn max_vesting_schedules() -> u32 {
217			T::MAX_VESTING_SCHEDULES
218		}
219	}
220
221	#[pezpallet::hooks]
222	impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {
223		fn integrity_test() {
224			assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must be greater than 0");
225		}
226	}
227
228	/// Information regarding the vesting of a given account.
229	#[pezpallet::storage]
230	pub type Vesting<T: Config> = StorageMap<
231		_,
232		Blake2_128Concat,
233		T::AccountId,
234		BoundedVec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>, MaxVestingSchedulesGet<T>>,
235	>;
236
237	/// Storage version of the pezpallet.
238	///
239	/// New networks start with latest version, as determined by the genesis build.
240	#[pezpallet::storage]
241	pub type StorageVersion<T: Config> = StorageValue<_, Releases, ValueQuery>;
242
243	#[pezpallet::pezpallet]
244	pub struct Pezpallet<T>(_);
245
246	#[pezpallet::genesis_config]
247	#[derive(pezframe_support::DefaultNoBound)]
248	pub struct GenesisConfig<T: Config> {
249		pub vesting: Vec<(T::AccountId, BlockNumberFor<T>, BlockNumberFor<T>, BalanceOf<T>)>,
250	}
251
252	#[pezpallet::genesis_build]
253	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
254		fn build(&self) {
255			use pezsp_runtime::traits::Saturating;
256
257			// Genesis uses the latest storage version.
258			StorageVersion::<T>::put(Releases::V1);
259
260			// Generate initial vesting configuration
261			// * who - Account which we are generating vesting configuration for
262			// * begin - Block when the account will start to vest
263			// * length - Number of blocks from `begin` until fully vested
264			// * liquid - Number of units which can be spent before vesting begins
265			for &(ref who, begin, length, liquid) in self.vesting.iter() {
266				let balance = T::Currency::free_balance(who);
267				assert!(!balance.is_zero(), "Currencies must be init'd before vesting");
268				// Total genesis `balance` minus `liquid` equals funds locked for vesting
269				let locked = balance.saturating_sub(liquid);
270				let length_as_balance = T::BlockNumberToBalance::convert(length);
271				let per_block = locked / length_as_balance.max(pezsp_runtime::traits::One::one());
272				let vesting_info = VestingInfo::new(locked, per_block, begin);
273				if !vesting_info.is_valid() {
274					panic!("Invalid VestingInfo params at genesis")
275				};
276
277				Vesting::<T>::try_append(who, vesting_info)
278					.expect("Too many vesting schedules at genesis.");
279
280				let reasons =
281					WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get());
282
283				T::Currency::set_lock(VESTING_ID, who, locked, reasons);
284			}
285		}
286	}
287
288	#[pezpallet::event]
289	#[pezpallet::generate_deposit(pub(super) fn deposit_event)]
290	pub enum Event<T: Config> {
291		/// A vesting schedule has been created.
292		VestingCreated { account: T::AccountId, schedule_index: u32 },
293		/// The amount vested has been updated. This could indicate a change in funds available.
294		/// The balance given is the amount which is left unvested (and thus locked).
295		VestingUpdated { account: T::AccountId, unvested: BalanceOf<T> },
296		/// An \[account\] has become fully vested.
297		VestingCompleted { account: T::AccountId },
298	}
299
300	/// Error for the vesting pezpallet.
301	#[pezpallet::error]
302	pub enum Error<T> {
303		/// The account given is not vesting.
304		NotVesting,
305		/// The account already has `MaxVestingSchedules` count of schedules and thus
306		/// cannot add another one. Consider merging existing schedules in order to add another.
307		AtMaxVestingSchedules,
308		/// Amount being transferred is too low to create a vesting schedule.
309		AmountLow,
310		/// An index was out of bounds of the vesting schedules.
311		ScheduleIndexOutOfBounds,
312		/// Failed to create a new schedule because some parameter was invalid.
313		InvalidScheduleParams,
314	}
315
316	#[pezpallet::call]
317	impl<T: Config> Pezpallet<T> {
318		/// Unlock any vested funds of the sender account.
319		///
320		/// The dispatch origin for this call must be _Signed_ and the sender must have funds still
321		/// locked under this pezpallet.
322		///
323		/// Emits either `VestingCompleted` or `VestingUpdated`.
324		///
325		/// ## Complexity
326		/// - `O(1)`.
327		#[pezpallet::call_index(0)]
328		#[pezpallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
329			.max(T::WeightInfo::vest_unlocked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
330		)]
331		pub fn vest(origin: OriginFor<T>) -> DispatchResult {
332			let who = ensure_signed(origin)?;
333			Self::do_vest(who)
334		}
335
336		/// Unlock any vested funds of a `target` account.
337		///
338		/// The dispatch origin for this call must be _Signed_.
339		///
340		/// - `target`: The account whose vested funds should be unlocked. Must have funds still
341		/// locked under this pezpallet.
342		///
343		/// Emits either `VestingCompleted` or `VestingUpdated`.
344		///
345		/// ## Complexity
346		/// - `O(1)`.
347		#[pezpallet::call_index(1)]
348		#[pezpallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
349			.max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
350		)]
351		pub fn vest_other(origin: OriginFor<T>, target: AccountIdLookupOf<T>) -> DispatchResult {
352			ensure_signed(origin)?;
353			let who = T::Lookup::lookup(target)?;
354			Self::do_vest(who)
355		}
356
357		/// Create a vested transfer.
358		///
359		/// The dispatch origin for this call must be _Signed_.
360		///
361		/// - `target`: The account receiving the vested funds.
362		/// - `schedule`: The vesting schedule attached to the transfer.
363		///
364		/// Emits `VestingCreated`.
365		///
366		/// NOTE: This will unlock all schedules through the current block.
367		///
368		/// ## Complexity
369		/// - `O(1)`.
370		#[pezpallet::call_index(2)]
371		#[pezpallet::weight(
372			T::WeightInfo::vested_transfer(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
373		)]
374		pub fn vested_transfer(
375			origin: OriginFor<T>,
376			target: AccountIdLookupOf<T>,
377			schedule: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
378		) -> DispatchResult {
379			let transactor = ensure_signed(origin)?;
380			let target = T::Lookup::lookup(target)?;
381			Self::do_vested_transfer(&transactor, &target, schedule)
382		}
383
384		/// Force a vested transfer.
385		///
386		/// The dispatch origin for this call must be _Root_.
387		///
388		/// - `source`: The account whose funds should be transferred.
389		/// - `target`: The account that should be transferred the vested funds.
390		/// - `schedule`: The vesting schedule attached to the transfer.
391		///
392		/// Emits `VestingCreated`.
393		///
394		/// NOTE: This will unlock all schedules through the current block.
395		///
396		/// ## Complexity
397		/// - `O(1)`.
398		#[pezpallet::call_index(3)]
399		#[pezpallet::weight(
400			T::WeightInfo::force_vested_transfer(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
401		)]
402		pub fn force_vested_transfer(
403			origin: OriginFor<T>,
404			source: AccountIdLookupOf<T>,
405			target: AccountIdLookupOf<T>,
406			schedule: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
407		) -> DispatchResult {
408			ensure_root(origin)?;
409			let target = T::Lookup::lookup(target)?;
410			let source = T::Lookup::lookup(source)?;
411			Self::do_vested_transfer(&source, &target, schedule)
412		}
413
414		/// Merge two vesting schedules together, creating a new vesting schedule that unlocks over
415		/// the highest possible start and end blocks. If both schedules have already started the
416		/// current block will be used as the schedule start; with the caveat that if one schedule
417		/// is finished by the current block, the other will be treated as the new merged schedule,
418		/// unmodified.
419		///
420		/// NOTE: If `schedule1_index == schedule2_index` this is a no-op.
421		/// NOTE: This will unlock all schedules through the current block prior to merging.
422		/// NOTE: If both schedules have ended by the current block, no new schedule will be created
423		/// and both will be removed.
424		///
425		/// Merged schedule attributes:
426		/// - `starting_block`: `MAX(schedule1.starting_block, scheduled2.starting_block,
427		///   current_block)`.
428		/// - `ending_block`: `MAX(schedule1.ending_block, schedule2.ending_block)`.
429		/// - `locked`: `schedule1.locked_at(current_block) + schedule2.locked_at(current_block)`.
430		///
431		/// The dispatch origin for this call must be _Signed_.
432		///
433		/// - `schedule1_index`: index of the first schedule to merge.
434		/// - `schedule2_index`: index of the second schedule to merge.
435		#[pezpallet::call_index(4)]
436		#[pezpallet::weight(
437			T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
438			.max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
439		)]
440		pub fn merge_schedules(
441			origin: OriginFor<T>,
442			schedule1_index: u32,
443			schedule2_index: u32,
444		) -> DispatchResult {
445			let who = ensure_signed(origin)?;
446			if schedule1_index == schedule2_index {
447				return Ok(());
448			};
449			let schedule1_index = schedule1_index as usize;
450			let schedule2_index = schedule2_index as usize;
451
452			let schedules = Vesting::<T>::get(&who).ok_or(Error::<T>::NotVesting)?;
453			let merge_action =
454				VestingAction::Merge { index1: schedule1_index, index2: schedule2_index };
455
456			let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?;
457
458			Self::write_vesting(&who, schedules)?;
459			Self::write_lock(&who, locked_now);
460
461			Ok(())
462		}
463
464		/// Force remove a vesting schedule
465		///
466		/// The dispatch origin for this call must be _Root_.
467		///
468		/// - `target`: An account that has a vesting schedule
469		/// - `schedule_index`: The vesting schedule index that should be removed
470		#[pezpallet::call_index(5)]
471		#[pezpallet::weight(
472			T::WeightInfo::force_remove_vesting_schedule(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
473		)]
474		pub fn force_remove_vesting_schedule(
475			origin: OriginFor<T>,
476			target: <T::Lookup as StaticLookup>::Source,
477			schedule_index: u32,
478		) -> DispatchResultWithPostInfo {
479			ensure_root(origin)?;
480			let who = T::Lookup::lookup(target)?;
481
482			let schedules_count = Vesting::<T>::decode_len(&who).unwrap_or_default();
483			ensure!(schedule_index < schedules_count as u32, Error::<T>::InvalidScheduleParams);
484
485			Self::remove_vesting_schedule(&who, schedule_index)?;
486
487			Ok(Some(T::WeightInfo::force_remove_vesting_schedule(
488				MaxLocksOf::<T>::get(),
489				schedules_count as u32,
490			))
491			.into())
492		}
493	}
494}
495
496impl<T: Config> Pezpallet<T> {
497	// Public function for accessing vesting storage
498	pub fn vesting(
499		account: T::AccountId,
500	) -> Option<BoundedVec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>, MaxVestingSchedulesGet<T>>>
501	{
502		Vesting::<T>::get(account)
503	}
504
505	// Create a new `VestingInfo`, based off of two other `VestingInfo`s.
506	// NOTE: We assume both schedules have had funds unlocked up through the current block.
507	fn merge_vesting_info(
508		now: BlockNumberFor<T>,
509		schedule1: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
510		schedule2: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
511	) -> Option<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>> {
512		let schedule1_ending_block = schedule1.ending_block_as_balance::<T::BlockNumberToBalance>();
513		let schedule2_ending_block = schedule2.ending_block_as_balance::<T::BlockNumberToBalance>();
514		let now_as_balance = T::BlockNumberToBalance::convert(now);
515
516		// Check if one or both schedules have ended.
517		match (schedule1_ending_block <= now_as_balance, schedule2_ending_block <= now_as_balance) {
518			// If both schedules have ended, we don't merge and exit early.
519			(true, true) => return None,
520			// If one schedule has ended, we treat the one that has not ended as the new
521			// merged schedule.
522			(true, false) => return Some(schedule2),
523			(false, true) => return Some(schedule1),
524			// If neither schedule has ended don't exit early.
525			_ => {},
526		}
527
528		let locked = schedule1
529			.locked_at::<T::BlockNumberToBalance>(now)
530			.saturating_add(schedule2.locked_at::<T::BlockNumberToBalance>(now));
531		// This shouldn't happen because we know at least one ending block is greater than now,
532		// thus at least a schedule a some locked balance.
533		debug_assert!(
534			!locked.is_zero(),
535			"merge_vesting_info validation checks failed to catch a locked of 0"
536		);
537
538		let ending_block = schedule1_ending_block.max(schedule2_ending_block);
539		let starting_block = now.max(schedule1.starting_block()).max(schedule2.starting_block());
540
541		let per_block = {
542			let duration = ending_block
543				.saturating_sub(T::BlockNumberToBalance::convert(starting_block))
544				.max(One::one());
545			(locked / duration).max(One::one())
546		};
547
548		let schedule = VestingInfo::new(locked, per_block, starting_block);
549		debug_assert!(schedule.is_valid(), "merge_vesting_info schedule validation check failed");
550
551		Some(schedule)
552	}
553
554	// Execute a vested transfer from `source` to `target` with the given `schedule`.
555	fn do_vested_transfer(
556		source: &T::AccountId,
557		target: &T::AccountId,
558		schedule: VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
559	) -> DispatchResult {
560		// Validate user inputs.
561		ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
562		if !schedule.is_valid() {
563			return Err(Error::<T>::InvalidScheduleParams.into());
564		};
565
566		// Check we can add to this account prior to any storage writes.
567		Self::can_add_vesting_schedule(
568			target,
569			schedule.locked(),
570			schedule.per_block(),
571			schedule.starting_block(),
572		)?;
573
574		T::Currency::transfer(source, target, schedule.locked(), ExistenceRequirement::AllowDeath)?;
575
576		// We can't let this fail because the currency transfer has already happened.
577		// Must be successful as it has been checked before.
578		// Better to return error on failure anyway.
579		let res = Self::add_vesting_schedule(
580			target,
581			schedule.locked(),
582			schedule.per_block(),
583			schedule.starting_block(),
584		);
585		debug_assert!(res.is_ok(), "Failed to add a schedule when we had to succeed.");
586
587		Ok(())
588	}
589
590	/// Iterate through the schedules to track the current locked amount and
591	/// filter out completed and specified schedules.
592	///
593	/// Returns a tuple that consists of:
594	/// - Vec of vesting schedules, where completed schedules and those specified
595	/// 	by filter are removed. (Note the vec is not checked for respecting
596	/// 	bounded length.)
597	/// - The amount locked at the current block number based on the given schedules.
598	///
599	/// NOTE: the amount locked does not include any schedules that are filtered out via `action`.
600	fn report_schedule_updates(
601		schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
602		action: VestingAction,
603	) -> (Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>, BalanceOf<T>) {
604		let now = T::BlockNumberProvider::current_block_number();
605
606		let mut total_locked_now: BalanceOf<T> = Zero::zero();
607		let filtered_schedules = action
608			.pick_schedules::<T>(schedules)
609			.filter(|schedule| {
610				let locked_now = schedule.locked_at::<T::BlockNumberToBalance>(now);
611				let keep = !locked_now.is_zero();
612				if keep {
613					total_locked_now = total_locked_now.saturating_add(locked_now);
614				}
615				keep
616			})
617			.collect::<Vec<_>>();
618
619		(filtered_schedules, total_locked_now)
620	}
621
622	/// Write an accounts updated vesting lock to storage.
623	fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf<T>) {
624		if total_locked_now.is_zero() {
625			T::Currency::remove_lock(VESTING_ID, who);
626			Self::deposit_event(Event::<T>::VestingCompleted { account: who.clone() });
627		} else {
628			let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get());
629			T::Currency::set_lock(VESTING_ID, who, total_locked_now, reasons);
630			Self::deposit_event(Event::<T>::VestingUpdated {
631				account: who.clone(),
632				unvested: total_locked_now,
633			});
634		};
635	}
636
637	/// Write an accounts updated vesting schedules to storage.
638	fn write_vesting(
639		who: &T::AccountId,
640		schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
641	) -> Result<(), DispatchError> {
642		let schedules: BoundedVec<
643			VestingInfo<BalanceOf<T>, BlockNumberFor<T>>,
644			MaxVestingSchedulesGet<T>,
645		> = schedules.try_into().map_err(|_| Error::<T>::AtMaxVestingSchedules)?;
646
647		if schedules.len() == 0 {
648			Vesting::<T>::remove(&who);
649		} else {
650			Vesting::<T>::insert(who, schedules)
651		}
652
653		Ok(())
654	}
655
656	/// Unlock any vested funds of `who`.
657	fn do_vest(who: T::AccountId) -> DispatchResult {
658		let schedules = Vesting::<T>::get(&who).ok_or(Error::<T>::NotVesting)?;
659
660		let (schedules, locked_now) =
661			Self::exec_action(schedules.to_vec(), VestingAction::Passive)?;
662
663		Self::write_vesting(&who, schedules)?;
664		Self::write_lock(&who, locked_now);
665
666		Ok(())
667	}
668
669	/// Execute a `VestingAction` against the given `schedules`. Returns the updated schedules
670	/// and locked amount.
671	fn exec_action(
672		schedules: Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>,
673		action: VestingAction,
674	) -> Result<(Vec<VestingInfo<BalanceOf<T>, BlockNumberFor<T>>>, BalanceOf<T>), DispatchError> {
675		let (schedules, locked_now) = match action {
676			VestingAction::Merge { index1: idx1, index2: idx2 } => {
677				// The schedule index is based off of the schedule ordering prior to filtering out
678				// any schedules that may be ending at this block.
679				let schedule1 = *schedules.get(idx1).ok_or(Error::<T>::ScheduleIndexOutOfBounds)?;
680				let schedule2 = *schedules.get(idx2).ok_or(Error::<T>::ScheduleIndexOutOfBounds)?;
681
682				// The length of `schedules` decreases by 2 here since we filter out 2 schedules.
683				// Thus we know below that we can push the new merged schedule without error
684				// (assuming initial state was valid).
685				let (mut schedules, mut locked_now) =
686					Self::report_schedule_updates(schedules.to_vec(), action);
687
688				let now = T::BlockNumberProvider::current_block_number();
689				if let Some(new_schedule) = Self::merge_vesting_info(now, schedule1, schedule2) {
690					// Merging created a new schedule so we:
691					// 1) need to add it to the accounts vesting schedule collection,
692					schedules.push(new_schedule);
693					// (we use `locked_at` in case this is a schedule that started in the past)
694					let new_schedule_locked =
695						new_schedule.locked_at::<T::BlockNumberToBalance>(now);
696					// and 2) update the locked amount to reflect the schedule we just added.
697					locked_now = locked_now.saturating_add(new_schedule_locked);
698				} // In the None case there was no new schedule to account for.
699
700				(schedules, locked_now)
701			},
702			_ => Self::report_schedule_updates(schedules.to_vec(), action),
703		};
704
705		debug_assert!(
706			locked_now > Zero::zero() && schedules.len() > 0
707				|| locked_now == Zero::zero() && schedules.len() == 0
708		);
709
710		Ok((schedules, locked_now))
711	}
712}
713
714impl<T: Config> VestingSchedule<T::AccountId> for Pezpallet<T>
715where
716	BalanceOf<T>: MaybeSerializeDeserialize + Debug,
717{
718	type Currency = T::Currency;
719	type Moment = BlockNumberFor<T>;
720
721	/// Get the amount that is currently being vested and cannot be transferred out of this account.
722	fn vesting_balance(who: &T::AccountId) -> Option<BalanceOf<T>> {
723		if let Some(v) = Vesting::<T>::get(who) {
724			let now = T::BlockNumberProvider::current_block_number();
725			let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| {
726				schedule.locked_at::<T::BlockNumberToBalance>(now).saturating_add(total)
727			});
728			Some(T::Currency::free_balance(who).min(total_locked_now))
729		} else {
730			None
731		}
732	}
733
734	/// Adds a vesting schedule to a given account.
735	///
736	/// If the account has `MaxVestingSchedules`, an Error is returned and nothing
737	/// is updated.
738	///
739	/// On success, a linearly reducing amount of funds will be locked. In order to realise any
740	/// reduction of the lock over time as it diminishes, the account owner must use `vest` or
741	/// `vest_other`.
742	///
743	/// It is a no-op if the amount to be vested is zero.
744	///
745	/// NOTE: This doesn't alter the free balance of the account.
746	fn add_vesting_schedule(
747		who: &T::AccountId,
748		locked: BalanceOf<T>,
749		per_block: BalanceOf<T>,
750		starting_block: BlockNumberFor<T>,
751	) -> DispatchResult {
752		if locked.is_zero() {
753			return Ok(());
754		}
755
756		let vesting_schedule = VestingInfo::new(locked, per_block, starting_block);
757		// Check for `per_block` or `locked` of 0.
758		if !vesting_schedule.is_valid() {
759			return Err(Error::<T>::InvalidScheduleParams.into());
760		};
761
762		let mut schedules = Vesting::<T>::get(who).unwrap_or_default();
763
764		// NOTE: we must push the new schedule so that `exec_action`
765		// will give the correct new locked amount.
766		ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::<T>::AtMaxVestingSchedules);
767
768		debug_assert!(schedules.len() > 0, "schedules cannot be empty after insertion");
769		let schedule_index = schedules.len() - 1;
770		Self::deposit_event(Event::<T>::VestingCreated {
771			account: who.clone(),
772			schedule_index: schedule_index as u32,
773		});
774
775		let (schedules, locked_now) =
776			Self::exec_action(schedules.to_vec(), VestingAction::Passive)?;
777
778		Self::write_vesting(who, schedules)?;
779		Self::write_lock(who, locked_now);
780
781		Ok(())
782	}
783
784	/// Ensure we can call `add_vesting_schedule` without error. This should always
785	/// be called prior to `add_vesting_schedule`.
786	fn can_add_vesting_schedule(
787		who: &T::AccountId,
788		locked: BalanceOf<T>,
789		per_block: BalanceOf<T>,
790		starting_block: BlockNumberFor<T>,
791	) -> DispatchResult {
792		// Check for `per_block` or `locked` of 0.
793		if !VestingInfo::new(locked, per_block, starting_block).is_valid() {
794			return Err(Error::<T>::InvalidScheduleParams.into());
795		}
796
797		ensure!(
798			(Vesting::<T>::decode_len(who).unwrap_or_default() as u32) < T::MAX_VESTING_SCHEDULES,
799			Error::<T>::AtMaxVestingSchedules
800		);
801
802		Ok(())
803	}
804
805	/// Remove a vesting schedule for a given account.
806	fn remove_vesting_schedule(who: &T::AccountId, schedule_index: u32) -> DispatchResult {
807		let schedules = Vesting::<T>::get(who).ok_or(Error::<T>::NotVesting)?;
808		let remove_action = VestingAction::Remove { index: schedule_index as usize };
809
810		let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), remove_action)?;
811
812		Self::write_vesting(who, schedules)?;
813		Self::write_lock(who, locked_now);
814		Ok(())
815	}
816}
817
818/// An implementation that allows the Vesting Pezpallet to handle a vested transfer
819/// on behalf of another Pezpallet.
820impl<T: Config> VestedTransfer<T::AccountId> for Pezpallet<T>
821where
822	BalanceOf<T>: MaybeSerializeDeserialize + Debug,
823{
824	type Currency = T::Currency;
825	type Moment = BlockNumberFor<T>;
826
827	fn vested_transfer(
828		source: &T::AccountId,
829		target: &T::AccountId,
830		locked: BalanceOf<T>,
831		per_block: BalanceOf<T>,
832		starting_block: BlockNumberFor<T>,
833	) -> DispatchResult {
834		use pezframe_support::storage::{with_transaction, TransactionOutcome};
835		let schedule = VestingInfo::new(locked, per_block, starting_block);
836		with_transaction(|| -> TransactionOutcome<DispatchResult> {
837			let result = Self::do_vested_transfer(source, target, schedule);
838
839			match &result {
840				Ok(()) => TransactionOutcome::Commit(result),
841				_ => TransactionOutcome::Rollback(result),
842			}
843		})
844	}
845}