// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Vesting Pezpallet //! //! - [`Config`] //! - [`Call`] //! //! ## Overview //! //! A simple pezpallet providing a means of placing a linear curve on an account's locked balance. //! This pezpallet ensures that there is a lock in place preventing the balance to drop below the //! *unvested* amount for any reason other than the ones specified in //! `UnvestedFundsAllowedWithdrawReasons` configuration value. //! //! As the amount vested increases over time, the amount unvested reduces. However, locks remain in //! place and explicit action is needed on behalf of the user to ensure that the amount locked is //! equivalent to the amount remaining to be vested. This is done through a dispatchable function, //! either `vest` (in typical case where the sender is calling on their own behalf) or `vest_other` //! in case the sender is calling on another account's behalf. //! //! ## Interface //! //! This pezpallet implements the `VestingSchedule` trait. //! //! ### Dispatchable Functions //! //! - `vest` - Update the lock, reducing it in line with the amount "vested" so far. //! - `vest_other` - Update the lock of another account, reducing it in line with the amount //! "vested" so far. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; #[cfg(test)] mod mock; #[cfg(test)] mod tests; mod vesting_info; pub mod migrations; pub mod weights; extern crate alloc; use alloc::vec::Vec; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::{fmt::Debug, marker::PhantomData}; use pezframe_support::{ dispatch::DispatchResult, ensure, storage::bounded_vec::BoundedVec, traits::{ Currency, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, VestedTransfer, VestingSchedule, WithdrawReasons, }, weights::Weight, }; use pezframe_system::pezpallet_prelude::BlockNumberFor; use pezsp_runtime::{ traits::{ AtLeast32BitUnsigned, BlockNumberProvider, Bounded, Convert, MaybeSerializeDeserialize, One, Saturating, StaticLookup, Zero, }, DispatchError, RuntimeDebug, }; use scale_info::TypeInfo; pub use pezpallet::*; pub use vesting_info::*; pub use weights::WeightInfo; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; type MaxLocksOf = <::Currency as LockableCurrency< ::AccountId, >>::MaxLocks; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; const VESTING_ID: LockIdentifier = *b"vesting "; // A value placed in storage that represents the current version of the Vesting storage. // This value is used by `on_runtime_upgrade` to determine whether we run storage migration logic. #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub enum Releases { V0, V1, } impl Default for Releases { fn default() -> Self { Releases::V0 } } /// Actions to take against a user's `Vesting` storage entry. #[derive(Clone, Copy)] enum VestingAction { /// Do not actively remove any schedules. Passive, /// Remove the schedule specified by the index. Remove { index: usize }, /// Remove the two schedules, specified by index, so they can be merged. Merge { index1: usize, index2: usize }, } impl VestingAction { /// Whether or not the filter says the schedule index should be removed. fn should_remove(&self, index: usize) -> bool { match self { Self::Passive => false, Self::Remove { index: index1 } => *index1 == index, Self::Merge { index1, index2 } => *index1 == index || *index2 == index, } } /// Pick the schedules that this action dictates should continue vesting undisturbed. fn pick_schedules( &self, schedules: Vec, BlockNumberFor>>, ) -> impl Iterator, BlockNumberFor>> + '_ { schedules.into_iter().enumerate().filter_map(move |(index, schedule)| { if self.should_remove(index) { None } else { Some(schedule) } }) } } // Wrapper for `T::MAX_VESTING_SCHEDULES` to satisfy `trait Get`. pub struct MaxVestingSchedulesGet(PhantomData); impl Get for MaxVestingSchedulesGet { fn get() -> u32 { T::MAX_VESTING_SCHEDULES } } #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_support::pezpallet_prelude::*; use pezframe_system::pezpallet_prelude::*; #[pezpallet::config] pub trait Config: pezframe_system::Config { /// The overarching event type. #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The currency trait. type Currency: LockableCurrency; /// Convert the block number into a balance. type BlockNumberToBalance: Convert, BalanceOf>; /// The minimum amount transferred to call `vested_transfer`. #[pezpallet::constant] type MinVestedTransfer: Get>; /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; /// Reasons that determine under which conditions the balance may drop below /// the unvested amount. type UnvestedFundsAllowedWithdrawReasons: Get; /// Query the current block number. /// /// Must return monotonically increasing values when called from consecutive blocks. /// Can be configured to return either: /// - the local block number of the runtime via `pezframe_system::Pezpallet` /// - a remote block number, eg from the relay chain through `RelaychainDataProvider` /// - an arbitrary value through a custom implementation of the trait /// /// There is currently no migration provided to "hot-swap" block number providers and it may /// result in undefined behavior when doing so. Teyrchains are therefore best off setting /// this to their local block number provider if they have the pezpallet already deployed. /// /// Suggested values: /// - Solo- and Relay-chains: `pezframe_system::Pezpallet` /// - Teyrchains that may produce blocks sparingly or only when needed (on-demand): /// - already have the pezpallet deployed: `pezframe_system::Pezpallet` /// - are freshly deploying this pezpallet: `RelaychainDataProvider` /// - Teyrchains with a reliably block production rate (PLO or bulk-coretime): /// - already have the pezpallet deployed: `pezframe_system::Pezpallet` /// - are freshly deploying this pezpallet: no strong recommendation. Both local and /// remote providers can be used. Relay provider can be a bit better in cases where the /// teyrchain is lagging its block production to avoid clock skew. type BlockNumberProvider: BlockNumberProvider>; /// Maximum number of vesting schedules an account may have at a given moment. const MAX_VESTING_SCHEDULES: u32; } #[pezpallet::extra_constants] impl Pezpallet { #[pezpallet::constant_name(MaxVestingSchedules)] fn max_vesting_schedules() -> u32 { T::MAX_VESTING_SCHEDULES } } #[pezpallet::hooks] impl Hooks> for Pezpallet { fn integrity_test() { assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must be greater than 0"); } } /// Information regarding the vesting of a given account. #[pezpallet::storage] pub type Vesting = StorageMap< _, Blake2_128Concat, T::AccountId, BoundedVec, BlockNumberFor>, MaxVestingSchedulesGet>, >; /// Storage version of the pezpallet. /// /// New networks start with latest version, as determined by the genesis build. #[pezpallet::storage] pub type StorageVersion = StorageValue<_, Releases, ValueQuery>; #[pezpallet::pezpallet] pub struct Pezpallet(_); #[pezpallet::genesis_config] #[derive(pezframe_support::DefaultNoBound)] pub struct GenesisConfig { pub vesting: Vec<(T::AccountId, BlockNumberFor, BlockNumberFor, BalanceOf)>, } #[pezpallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { use pezsp_runtime::traits::Saturating; // Genesis uses the latest storage version. StorageVersion::::put(Releases::V1); // Generate initial vesting configuration // * who - Account which we are generating vesting configuration for // * begin - Block when the account will start to vest // * length - Number of blocks from `begin` until fully vested // * liquid - Number of units which can be spent before vesting begins for &(ref who, begin, length, liquid) in self.vesting.iter() { let balance = T::Currency::free_balance(who); assert!(!balance.is_zero(), "Currencies must be init'd before vesting"); // Total genesis `balance` minus `liquid` equals funds locked for vesting let locked = balance.saturating_sub(liquid); let length_as_balance = T::BlockNumberToBalance::convert(length); let per_block = locked / length_as_balance.max(pezsp_runtime::traits::One::one()); let vesting_info = VestingInfo::new(locked, per_block, begin); if !vesting_info.is_valid() { panic!("Invalid VestingInfo params at genesis") }; Vesting::::try_append(who, vesting_info) .expect("Too many vesting schedules at genesis."); let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get()); T::Currency::set_lock(VESTING_ID, who, locked, reasons); } } } #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// A vesting schedule has been created. VestingCreated { account: T::AccountId, schedule_index: u32 }, /// The amount vested has been updated. This could indicate a change in funds available. /// The balance given is the amount which is left unvested (and thus locked). VestingUpdated { account: T::AccountId, unvested: BalanceOf }, /// An \[account\] has become fully vested. VestingCompleted { account: T::AccountId }, } /// Error for the vesting pezpallet. #[pezpallet::error] pub enum Error { /// The account given is not vesting. NotVesting, /// The account already has `MaxVestingSchedules` count of schedules and thus /// cannot add another one. Consider merging existing schedules in order to add another. AtMaxVestingSchedules, /// Amount being transferred is too low to create a vesting schedule. AmountLow, /// An index was out of bounds of the vesting schedules. ScheduleIndexOutOfBounds, /// Failed to create a new schedule because some parameter was invalid. InvalidScheduleParams, } #[pezpallet::call] impl Pezpallet { /// Unlock any vested funds of the sender account. /// /// The dispatch origin for this call must be _Signed_ and the sender must have funds still /// locked under this pezpallet. /// /// Emits either `VestingCompleted` or `VestingUpdated`. /// /// ## Complexity /// - `O(1)`. #[pezpallet::call_index(0)] #[pezpallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) .max(T::WeightInfo::vest_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) )] pub fn vest(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; Self::do_vest(who) } /// Unlock any vested funds of a `target` account. /// /// The dispatch origin for this call must be _Signed_. /// /// - `target`: The account whose vested funds should be unlocked. Must have funds still /// locked under this pezpallet. /// /// Emits either `VestingCompleted` or `VestingUpdated`. /// /// ## Complexity /// - `O(1)`. #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) .max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) )] pub fn vest_other(origin: OriginFor, target: AccountIdLookupOf) -> DispatchResult { ensure_signed(origin)?; let who = T::Lookup::lookup(target)?; Self::do_vest(who) } /// Create a vested transfer. /// /// The dispatch origin for this call must be _Signed_. /// /// - `target`: The account receiving the vested funds. /// - `schedule`: The vesting schedule attached to the transfer. /// /// Emits `VestingCreated`. /// /// NOTE: This will unlock all schedules through the current block. /// /// ## Complexity /// - `O(1)`. #[pezpallet::call_index(2)] #[pezpallet::weight( T::WeightInfo::vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) )] pub fn vested_transfer( origin: OriginFor, target: AccountIdLookupOf, schedule: VestingInfo, BlockNumberFor>, ) -> DispatchResult { let transactor = ensure_signed(origin)?; let target = T::Lookup::lookup(target)?; Self::do_vested_transfer(&transactor, &target, schedule) } /// Force a vested transfer. /// /// The dispatch origin for this call must be _Root_. /// /// - `source`: The account whose funds should be transferred. /// - `target`: The account that should be transferred the vested funds. /// - `schedule`: The vesting schedule attached to the transfer. /// /// Emits `VestingCreated`. /// /// NOTE: This will unlock all schedules through the current block. /// /// ## Complexity /// - `O(1)`. #[pezpallet::call_index(3)] #[pezpallet::weight( T::WeightInfo::force_vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) )] pub fn force_vested_transfer( origin: OriginFor, source: AccountIdLookupOf, target: AccountIdLookupOf, schedule: VestingInfo, BlockNumberFor>, ) -> DispatchResult { ensure_root(origin)?; let target = T::Lookup::lookup(target)?; let source = T::Lookup::lookup(source)?; Self::do_vested_transfer(&source, &target, schedule) } /// Merge two vesting schedules together, creating a new vesting schedule that unlocks over /// the highest possible start and end blocks. If both schedules have already started the /// current block will be used as the schedule start; with the caveat that if one schedule /// is finished by the current block, the other will be treated as the new merged schedule, /// unmodified. /// /// NOTE: If `schedule1_index == schedule2_index` this is a no-op. /// NOTE: This will unlock all schedules through the current block prior to merging. /// NOTE: If both schedules have ended by the current block, no new schedule will be created /// and both will be removed. /// /// Merged schedule attributes: /// - `starting_block`: `MAX(schedule1.starting_block, scheduled2.starting_block, /// current_block)`. /// - `ending_block`: `MAX(schedule1.ending_block, schedule2.ending_block)`. /// - `locked`: `schedule1.locked_at(current_block) + schedule2.locked_at(current_block)`. /// /// The dispatch origin for this call must be _Signed_. /// /// - `schedule1_index`: index of the first schedule to merge. /// - `schedule2_index`: index of the second schedule to merge. #[pezpallet::call_index(4)] #[pezpallet::weight( T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) .max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) )] pub fn merge_schedules( origin: OriginFor, schedule1_index: u32, schedule2_index: u32, ) -> DispatchResult { let who = ensure_signed(origin)?; if schedule1_index == schedule2_index { return Ok(()); }; let schedule1_index = schedule1_index as usize; let schedule2_index = schedule2_index as usize; let schedules = Vesting::::get(&who).ok_or(Error::::NotVesting)?; let merge_action = VestingAction::Merge { index1: schedule1_index, index2: schedule2_index }; let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?; Self::write_vesting(&who, schedules)?; Self::write_lock(&who, locked_now); Ok(()) } /// Force remove a vesting schedule /// /// The dispatch origin for this call must be _Root_. /// /// - `target`: An account that has a vesting schedule /// - `schedule_index`: The vesting schedule index that should be removed #[pezpallet::call_index(5)] #[pezpallet::weight( T::WeightInfo::force_remove_vesting_schedule(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) )] pub fn force_remove_vesting_schedule( origin: OriginFor, target: ::Source, schedule_index: u32, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; let who = T::Lookup::lookup(target)?; let schedules_count = Vesting::::decode_len(&who).unwrap_or_default(); ensure!(schedule_index < schedules_count as u32, Error::::InvalidScheduleParams); Self::remove_vesting_schedule(&who, schedule_index)?; Ok(Some(T::WeightInfo::force_remove_vesting_schedule( MaxLocksOf::::get(), schedules_count as u32, )) .into()) } } } impl Pezpallet { // Public function for accessing vesting storage pub fn vesting( account: T::AccountId, ) -> Option, BlockNumberFor>, MaxVestingSchedulesGet>> { Vesting::::get(account) } // Create a new `VestingInfo`, based off of two other `VestingInfo`s. // NOTE: We assume both schedules have had funds unlocked up through the current block. fn merge_vesting_info( now: BlockNumberFor, schedule1: VestingInfo, BlockNumberFor>, schedule2: VestingInfo, BlockNumberFor>, ) -> Option, BlockNumberFor>> { let schedule1_ending_block = schedule1.ending_block_as_balance::(); let schedule2_ending_block = schedule2.ending_block_as_balance::(); let now_as_balance = T::BlockNumberToBalance::convert(now); // Check if one or both schedules have ended. match (schedule1_ending_block <= now_as_balance, schedule2_ending_block <= now_as_balance) { // If both schedules have ended, we don't merge and exit early. (true, true) => return None, // If one schedule has ended, we treat the one that has not ended as the new // merged schedule. (true, false) => return Some(schedule2), (false, true) => return Some(schedule1), // If neither schedule has ended don't exit early. _ => {}, } let locked = schedule1 .locked_at::(now) .saturating_add(schedule2.locked_at::(now)); // This shouldn't happen because we know at least one ending block is greater than now, // thus at least a schedule a some locked balance. debug_assert!( !locked.is_zero(), "merge_vesting_info validation checks failed to catch a locked of 0" ); let ending_block = schedule1_ending_block.max(schedule2_ending_block); let starting_block = now.max(schedule1.starting_block()).max(schedule2.starting_block()); let per_block = { let duration = ending_block .saturating_sub(T::BlockNumberToBalance::convert(starting_block)) .max(One::one()); (locked / duration).max(One::one()) }; let schedule = VestingInfo::new(locked, per_block, starting_block); debug_assert!(schedule.is_valid(), "merge_vesting_info schedule validation check failed"); Some(schedule) } // Execute a vested transfer from `source` to `target` with the given `schedule`. fn do_vested_transfer( source: &T::AccountId, target: &T::AccountId, schedule: VestingInfo, BlockNumberFor>, ) -> DispatchResult { // Validate user inputs. ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::::AmountLow); if !schedule.is_valid() { return Err(Error::::InvalidScheduleParams.into()); }; // Check we can add to this account prior to any storage writes. Self::can_add_vesting_schedule( target, schedule.locked(), schedule.per_block(), schedule.starting_block(), )?; T::Currency::transfer(source, target, schedule.locked(), ExistenceRequirement::AllowDeath)?; // We can't let this fail because the currency transfer has already happened. // Must be successful as it has been checked before. // Better to return error on failure anyway. let res = Self::add_vesting_schedule( target, schedule.locked(), schedule.per_block(), schedule.starting_block(), ); debug_assert!(res.is_ok(), "Failed to add a schedule when we had to succeed."); Ok(()) } /// Iterate through the schedules to track the current locked amount and /// filter out completed and specified schedules. /// /// Returns a tuple that consists of: /// - Vec of vesting schedules, where completed schedules and those specified /// by filter are removed. (Note the vec is not checked for respecting /// bounded length.) /// - The amount locked at the current block number based on the given schedules. /// /// NOTE: the amount locked does not include any schedules that are filtered out via `action`. fn report_schedule_updates( schedules: Vec, BlockNumberFor>>, action: VestingAction, ) -> (Vec, BlockNumberFor>>, BalanceOf) { let now = T::BlockNumberProvider::current_block_number(); let mut total_locked_now: BalanceOf = Zero::zero(); let filtered_schedules = action .pick_schedules::(schedules) .filter(|schedule| { let locked_now = schedule.locked_at::(now); let keep = !locked_now.is_zero(); if keep { total_locked_now = total_locked_now.saturating_add(locked_now); } keep }) .collect::>(); (filtered_schedules, total_locked_now) } /// Write an accounts updated vesting lock to storage. fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf) { if total_locked_now.is_zero() { T::Currency::remove_lock(VESTING_ID, who); Self::deposit_event(Event::::VestingCompleted { account: who.clone() }); } else { let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get()); T::Currency::set_lock(VESTING_ID, who, total_locked_now, reasons); Self::deposit_event(Event::::VestingUpdated { account: who.clone(), unvested: total_locked_now, }); }; } /// Write an accounts updated vesting schedules to storage. fn write_vesting( who: &T::AccountId, schedules: Vec, BlockNumberFor>>, ) -> Result<(), DispatchError> { let schedules: BoundedVec< VestingInfo, BlockNumberFor>, MaxVestingSchedulesGet, > = schedules.try_into().map_err(|_| Error::::AtMaxVestingSchedules)?; if schedules.len() == 0 { Vesting::::remove(&who); } else { Vesting::::insert(who, schedules) } Ok(()) } /// Unlock any vested funds of `who`. fn do_vest(who: T::AccountId) -> DispatchResult { let schedules = Vesting::::get(&who).ok_or(Error::::NotVesting)?; let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; Self::write_vesting(&who, schedules)?; Self::write_lock(&who, locked_now); Ok(()) } /// Execute a `VestingAction` against the given `schedules`. Returns the updated schedules /// and locked amount. fn exec_action( schedules: Vec, BlockNumberFor>>, action: VestingAction, ) -> Result<(Vec, BlockNumberFor>>, BalanceOf), DispatchError> { let (schedules, locked_now) = match action { VestingAction::Merge { index1: idx1, index2: idx2 } => { // The schedule index is based off of the schedule ordering prior to filtering out // any schedules that may be ending at this block. let schedule1 = *schedules.get(idx1).ok_or(Error::::ScheduleIndexOutOfBounds)?; let schedule2 = *schedules.get(idx2).ok_or(Error::::ScheduleIndexOutOfBounds)?; // The length of `schedules` decreases by 2 here since we filter out 2 schedules. // Thus we know below that we can push the new merged schedule without error // (assuming initial state was valid). let (mut schedules, mut locked_now) = Self::report_schedule_updates(schedules.to_vec(), action); let now = T::BlockNumberProvider::current_block_number(); if let Some(new_schedule) = Self::merge_vesting_info(now, schedule1, schedule2) { // Merging created a new schedule so we: // 1) need to add it to the accounts vesting schedule collection, schedules.push(new_schedule); // (we use `locked_at` in case this is a schedule that started in the past) let new_schedule_locked = new_schedule.locked_at::(now); // and 2) update the locked amount to reflect the schedule we just added. locked_now = locked_now.saturating_add(new_schedule_locked); } // In the None case there was no new schedule to account for. (schedules, locked_now) }, _ => Self::report_schedule_updates(schedules.to_vec(), action), }; debug_assert!( locked_now > Zero::zero() && schedules.len() > 0 || locked_now == Zero::zero() && schedules.len() == 0 ); Ok((schedules, locked_now)) } } impl VestingSchedule for Pezpallet where BalanceOf: MaybeSerializeDeserialize + Debug, { type Currency = T::Currency; type Moment = BlockNumberFor; /// Get the amount that is currently being vested and cannot be transferred out of this account. fn vesting_balance(who: &T::AccountId) -> Option> { if let Some(v) = Vesting::::get(who) { let now = T::BlockNumberProvider::current_block_number(); let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| { schedule.locked_at::(now).saturating_add(total) }); Some(T::Currency::free_balance(who).min(total_locked_now)) } else { None } } /// Adds a vesting schedule to a given account. /// /// If the account has `MaxVestingSchedules`, an Error is returned and nothing /// is updated. /// /// On success, a linearly reducing amount of funds will be locked. In order to realise any /// reduction of the lock over time as it diminishes, the account owner must use `vest` or /// `vest_other`. /// /// It is a no-op if the amount to be vested is zero. /// /// NOTE: This doesn't alter the free balance of the account. fn add_vesting_schedule( who: &T::AccountId, locked: BalanceOf, per_block: BalanceOf, starting_block: BlockNumberFor, ) -> DispatchResult { if locked.is_zero() { return Ok(()); } let vesting_schedule = VestingInfo::new(locked, per_block, starting_block); // Check for `per_block` or `locked` of 0. if !vesting_schedule.is_valid() { return Err(Error::::InvalidScheduleParams.into()); }; let mut schedules = Vesting::::get(who).unwrap_or_default(); // NOTE: we must push the new schedule so that `exec_action` // will give the correct new locked amount. ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::::AtMaxVestingSchedules); debug_assert!(schedules.len() > 0, "schedules cannot be empty after insertion"); let schedule_index = schedules.len() - 1; Self::deposit_event(Event::::VestingCreated { account: who.clone(), schedule_index: schedule_index as u32, }); let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; Self::write_vesting(who, schedules)?; Self::write_lock(who, locked_now); Ok(()) } /// Ensure we can call `add_vesting_schedule` without error. This should always /// be called prior to `add_vesting_schedule`. fn can_add_vesting_schedule( who: &T::AccountId, locked: BalanceOf, per_block: BalanceOf, starting_block: BlockNumberFor, ) -> DispatchResult { // Check for `per_block` or `locked` of 0. if !VestingInfo::new(locked, per_block, starting_block).is_valid() { return Err(Error::::InvalidScheduleParams.into()); } ensure!( (Vesting::::decode_len(who).unwrap_or_default() as u32) < T::MAX_VESTING_SCHEDULES, Error::::AtMaxVestingSchedules ); Ok(()) } /// Remove a vesting schedule for a given account. fn remove_vesting_schedule(who: &T::AccountId, schedule_index: u32) -> DispatchResult { let schedules = Vesting::::get(who).ok_or(Error::::NotVesting)?; let remove_action = VestingAction::Remove { index: schedule_index as usize }; let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), remove_action)?; Self::write_vesting(who, schedules)?; Self::write_lock(who, locked_now); Ok(()) } } /// An implementation that allows the Vesting Pezpallet to handle a vested transfer /// on behalf of another Pezpallet. impl VestedTransfer for Pezpallet where BalanceOf: MaybeSerializeDeserialize + Debug, { type Currency = T::Currency; type Moment = BlockNumberFor; fn vested_transfer( source: &T::AccountId, target: &T::AccountId, locked: BalanceOf, per_block: BalanceOf, starting_block: BlockNumberFor, ) -> DispatchResult { use pezframe_support::storage::{with_transaction, TransactionOutcome}; let schedule = VestingInfo::new(locked, per_block, starting_block); with_transaction(|| -> TransactionOutcome { let result = Self::do_vested_transfer(source, target, schedule); match &result { Ok(()) => TransactionOutcome::Commit(result), _ => TransactionOutcome::Rollback(result), } }) } }