mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-19 13:31:04 +00:00
pallet-vesting: Support multiple, merge-able vesting schedules (#9202)
* Support multiple, mergable vesting schedules * Update node runtime * Remove some TODO design questions and put them as commennts * Update frame/vesting/src/benchmarking.rs * Syntax and comment clean up * Create filter enum for removing schedules * Dry vesting calls with do_vest * Improve old benchmarks to account for max schedules * Update WeightInfo trait and make dummy fns * Add merge_schedule weights * Explicitly test multiple vesting scheudles * Make new vesting tests more more clear * Apply suggestions from code review * Update remove_vesting_schedule to error with no index * Try reduce spacing diff * Apply suggestions from code review * Use get on vesting for bounds check; check origin first * No filter tuple; various simplifications * unwrap or default when getting user schedules * spaces be gone * ReadMe fixes * Update frame/vesting/src/lib.rs Co-authored-by: Peter Goodspeed-Niklaus <coriolinus@users.noreply.github.com> * address some comments for docs * merge sched docs * Apply suggestions from code review Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> * log error when trying to push to vesting vec * use let Some, not is_some * remove_vesting_schedule u32, not optin * new not try_new, create validate builder; VestingInfo * Merge prep: break out tests and mock * Add files forgot to include in merge * revert some accidental changes to merged files * Revert remaining accidental file changes * More revert of accidental file change * Try to reduce diff on tests * namespace Vesting; check key when key should not exist; * ending_block throws error on per_block of 0 * Try improve merge vesting info comment * Update frame/vesting/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * add validate + correct; handle duration > blocknumber * Move vesting_info module to its own file * Seperate Vesting/locks updates from writing * Add can_add_vesting schedule * Adjust min vested transfer to be greater than all ED * Initial integrity test impl * merge_finished_and_yet_to_be_started_schedules * Make sure to assert storage items are cleaned up * Migration initial impl (not tested) * Correct try-runtime hooks * Apply suggestions from code review Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> * header * WIP: improve benchmarks * Benchmarking working * benchmarking: step over max schedules * cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_vesting --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/vesting/src/weights.rs --template=./.maintain/frame-weight-template.hbs * Simplify APIs by accepting vec; convert to bounded on write * Test: build_genesis_has_storage_version_v1 * Test more error cases * Hack to get polkadot weights to work; should revert later * Improve benchmarking; works on polkadot * cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_vesting --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/vesting/src/weights.rs --template=./.maintain/frame-weight-template.hbs * WIP override storage * Set storage not working example * Remove unused tests * VestingInfo: make public, derive MaxEndcodedLen * Rename ending_block to ending_block_as_balance * Superificial improvements * Check for end block infinite, not just duration * More superficial update * Update tests * Test vest with multi schedule * Don't use half max balance in benchmarks * Use debug_assert when locked is unexpected 0 * Implement exec_action * Simplify per_block calc in vesting_info * VestingInfo.validate in add_vesting_schedule & can_add_vesting_schedule * Simplify post migrate check * Remove merge event * Minor benchmarking updates * Remove VestingInfo.correct * per_block accesor max with 1 * Improve comment * Remoe debug * Fix add schedule comment * Apply suggestions from code review Co-authored-by: Peter Goodspeed-Niklaus <coriolinus@users.noreply.github.com> * no ref for should_remove param * Remove unused vestingaction derive * Asserts to show balance unlock in merge benchmark * Remove unused imports * trivial * Fix benchmark asserts to handle non-multiple of 20 locked * Add generate_storage_info * migration :facepalm * Remove per_block 0 logic * Update frame/vesting/src/lib.rs * Do not check for ending later than greatest block * Apply suggestions from code review * Benchmarks: simplify vesting schedule creation * Add log back for migration * Add note in ext docs explaining that all schedules will vest * Make integrity test work * Improve integrity test * Remove unnescary type param from VestingInfo::new * Remove unnescary resut for ending_block_as_balance * Remove T param from ending_block_as_balance * Reduce visibility of raw_per_block * Remove unused type param for validate * update old comment * Make log a dep; log warn in migrate * VestingInfo.validate returns Err(()), no T type param * Try improve report_schedule_updates * is_valid, not validate * revert node runtime reorg; * change schedule validity check to just warning * Simplify merge_vesting_info return type * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Add warning for migration * Fix indentation * Delete duplicate warnings * Reduce diff in node runtime * Fix benchmark build * Upgrade cargo.toml to use 4.0.0-dev * Cleanup * MaxVestingSchedulesGetter initial impl * MinVestedTransfer getter inintial impl * Test MaxVestingSchedules & MinVestedTransfer getters; use getters in benchmarks * Run cargo fmt * Revert MinVestedTransfer & MaxVestingSchedules getters; Add integrity test * Make MAX_VESTING_SCHEDULES a const * fmt * WIP: benchmark improvements * Finish benchmark update * Add test for transfer to account with less than ed * Rm min_new_account_transfer; move sp-io to dev-dep * Reduce cargo.toml diff * Explain MAX_VESTING_SCHEDULES choice * Fix after merge * Try fix CI complaints * cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_vesting --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/vesting/src/weights.rs --template=./.maintain/frame-weight-template.hbs * cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_vesting --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/vesting/src/weights.rs --template=./.maintain/frame-weight-template.hbs * fmt * trigger * fmt Co-authored-by: Parity Bot <admin@parity.io> Co-authored-by: Peter Goodspeed-Niklaus <coriolinus@users.noreply.github.com> Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: kianenigma <kian@parity.io>
This commit is contained in:
+460
-117
@@ -45,14 +45,16 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
mod benchmarking;
|
||||
mod migrations;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod vesting_info;
|
||||
|
||||
pub mod weights;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::{
|
||||
ensure,
|
||||
pallet_prelude::*,
|
||||
@@ -64,10 +66,14 @@ use frame_support::{
|
||||
use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
|
||||
pub use pallet::*;
|
||||
use sp_runtime::{
|
||||
traits::{AtLeast32BitUnsigned, Convert, MaybeSerializeDeserialize, StaticLookup, Zero},
|
||||
traits::{
|
||||
AtLeast32BitUnsigned, Bounded, Convert, MaybeSerializeDeserialize, One, Saturating,
|
||||
StaticLookup, Zero,
|
||||
},
|
||||
RuntimeDebug,
|
||||
};
|
||||
use sp_std::{fmt::Debug, prelude::*};
|
||||
use sp_std::{convert::TryInto, fmt::Debug, prelude::*};
|
||||
pub use vesting_info::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
type BalanceOf<T> =
|
||||
@@ -77,37 +83,62 @@ type MaxLocksOf<T> =
|
||||
|
||||
const VESTING_ID: LockIdentifier = *b"vesting ";
|
||||
|
||||
/// Struct to encode the vesting schedule of an individual account.
|
||||
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug)]
|
||||
pub struct VestingInfo<Balance, BlockNumber> {
|
||||
/// Locked amount at genesis.
|
||||
pub locked: Balance,
|
||||
/// Amount that gets unlocked every block after `starting_block`.
|
||||
pub per_block: Balance,
|
||||
/// Starting block for unlocking(vesting).
|
||||
pub starting_block: BlockNumber,
|
||||
// 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)]
|
||||
enum Releases {
|
||||
V0,
|
||||
V1,
|
||||
}
|
||||
|
||||
impl<Balance: AtLeast32BitUnsigned + Copy, BlockNumber: AtLeast32BitUnsigned + Copy>
|
||||
VestingInfo<Balance, BlockNumber>
|
||||
{
|
||||
/// Amount locked at block `n`.
|
||||
pub fn locked_at<BlockNumberToBalance: Convert<BlockNumber, Balance>>(
|
||||
&self,
|
||||
n: BlockNumber,
|
||||
) -> Balance {
|
||||
// Number of blocks that count toward vesting
|
||||
// Saturating to 0 when n < starting_block
|
||||
let vested_block_count = n.saturating_sub(self.starting_block);
|
||||
let vested_block_count = BlockNumberToBalance::convert(vested_block_count);
|
||||
// Return amount that is still locked in vesting
|
||||
let maybe_balance = vested_block_count.checked_mul(&self.per_block);
|
||||
if let Some(balance) = maybe_balance {
|
||||
self.locked.saturating_sub(balance)
|
||||
} else {
|
||||
Zero::zero()
|
||||
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(usize),
|
||||
/// Remove the two schedules, specified by index, so they can be merged.
|
||||
Merge(usize, 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(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<'a, T: Config>(
|
||||
&'a self,
|
||||
schedules: Vec<VestingInfo<BalanceOf<T>, T::BlockNumber>>,
|
||||
) -> impl Iterator<Item = VestingInfo<BalanceOf<T>, T::BlockNumber>> + 'a {
|
||||
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<T>(PhantomData<T>);
|
||||
impl<T: Config> Get<u32> for MaxVestingSchedulesGet<T> {
|
||||
fn get() -> u32 {
|
||||
T::MAX_VESTING_SCHEDULES
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
@@ -131,16 +162,65 @@ pub mod pallet {
|
||||
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// Maximum number of vesting schedules an account may have at a given moment.
|
||||
const MAX_VESTING_SCHEDULES: u32;
|
||||
}
|
||||
|
||||
#[pallet::extra_constants]
|
||||
impl<T: Config> Pallet<T> {
|
||||
// TODO: rename to snake case after https://github.com/paritytech/substrate/issues/8826 fixed.
|
||||
#[allow(non_snake_case)]
|
||||
fn MaxVestingSchedules() -> u32 {
|
||||
T::MAX_VESTING_SCHEDULES
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<(), &'static str> {
|
||||
migrations::v1::pre_migrate::<T>()
|
||||
}
|
||||
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
if StorageVersion::<T>::get() == Releases::V0 {
|
||||
StorageVersion::<T>::put(Releases::V1);
|
||||
migrations::v1::migrate::<T>().saturating_add(T::DbWeight::get().reads_writes(1, 1))
|
||||
} else {
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade() -> Result<(), &'static str> {
|
||||
migrations::v1::post_migrate::<T>()
|
||||
}
|
||||
|
||||
fn integrity_test() {
|
||||
assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must ge greater than 0");
|
||||
}
|
||||
}
|
||||
|
||||
/// Information regarding the vesting of a given account.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn vesting)]
|
||||
pub type Vesting<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, VestingInfo<BalanceOf<T>, T::BlockNumber>>;
|
||||
pub type Vesting<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
BoundedVec<VestingInfo<BalanceOf<T>, T::BlockNumber>, MaxVestingSchedulesGet<T>>,
|
||||
>;
|
||||
|
||||
/// Storage version of the pallet.
|
||||
///
|
||||
/// New networks start with latest version, as determined by the genesis build.
|
||||
#[pallet::storage]
|
||||
pub(crate) type StorageVersion<T: Config> = StorageValue<_, Releases, ValueQuery>;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
#[pallet::generate_storage_info]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::genesis_config]
|
||||
@@ -160,6 +240,9 @@ pub mod pallet {
|
||||
fn build(&self) {
|
||||
use sp_runtime::traits::Saturating;
|
||||
|
||||
// Genesis uses the latest storage version.
|
||||
StorageVersion::<T>::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
|
||||
@@ -172,8 +255,14 @@ pub mod pallet {
|
||||
let locked = balance.saturating_sub(liquid);
|
||||
let length_as_balance = T::BlockNumberToBalance::convert(length);
|
||||
let per_block = locked / length_as_balance.max(sp_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::<T>::try_append(who, vesting_info)
|
||||
.expect("Too many vesting schedules at genesis.");
|
||||
|
||||
Vesting::<T>::insert(who, VestingInfo { locked, per_block, starting_block: begin });
|
||||
let reasons = WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE;
|
||||
T::Currency::set_lock(VESTING_ID, who, locked, reasons);
|
||||
}
|
||||
@@ -182,13 +271,15 @@ pub mod pallet {
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
#[pallet::metadata(T::AccountId = "AccountId", BalanceOf<T> = "Balance")]
|
||||
#[pallet::metadata(
|
||||
T::AccountId = "AccountId", BalanceOf<T> = "Balance", T::BlockNumber = "BlockNumber"
|
||||
)]
|
||||
pub enum Event<T: Config> {
|
||||
/// The amount vested has been updated. This could indicate more funds are available. The
|
||||
/// balance given is the amount which is left unvested (and thus locked).
|
||||
/// 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).
|
||||
/// \[account, unvested\]
|
||||
VestingUpdated(T::AccountId, BalanceOf<T>),
|
||||
/// An \[account\] has become fully vested. No further vesting can happen.
|
||||
/// An \[account\] has become fully vested.
|
||||
VestingCompleted(T::AccountId),
|
||||
}
|
||||
|
||||
@@ -197,10 +288,15 @@ pub mod pallet {
|
||||
pub enum Error<T> {
|
||||
/// The account given is not vesting.
|
||||
NotVesting,
|
||||
/// An existing vesting schedule already exists for this account that cannot be clobbered.
|
||||
ExistingVestingSchedule,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
@@ -218,12 +314,12 @@ pub mod pallet {
|
||||
/// - Reads: Vesting Storage, Balances Locks, [Sender Account]
|
||||
/// - Writes: Vesting Storage, Balances Locks, [Sender Account]
|
||||
/// # </weight>
|
||||
#[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::<T>::get())
|
||||
.max(T::WeightInfo::vest_unlocked(MaxLocksOf::<T>::get()))
|
||||
#[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
|
||||
.max(T::WeightInfo::vest_unlocked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
|
||||
)]
|
||||
pub fn vest(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::update_lock(who)
|
||||
Self::do_vest(who)
|
||||
}
|
||||
|
||||
/// Unlock any vested funds of a `target` account.
|
||||
@@ -241,61 +337,46 @@ pub mod pallet {
|
||||
/// - Reads: Vesting Storage, Balances Locks, Target Account
|
||||
/// - Writes: Vesting Storage, Balances Locks, Target Account
|
||||
/// # </weight>
|
||||
#[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::<T>::get())
|
||||
.max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::<T>::get()))
|
||||
#[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
|
||||
.max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
|
||||
)]
|
||||
pub fn vest_other(
|
||||
origin: OriginFor<T>,
|
||||
target: <T::Lookup as StaticLookup>::Source,
|
||||
) -> DispatchResult {
|
||||
ensure_signed(origin)?;
|
||||
Self::update_lock(T::Lookup::lookup(target)?)
|
||||
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 that should be transferred the vested funds.
|
||||
/// - `amount`: The amount of funds to transfer and will be vested.
|
||||
/// - `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.
|
||||
///
|
||||
/// # <weight>
|
||||
/// - `O(1)`.
|
||||
/// - DbWeight: 3 Reads, 3 Writes
|
||||
/// - Reads: Vesting Storage, Balances Locks, Target Account, [Sender Account]
|
||||
/// - Writes: Vesting Storage, Balances Locks, Target Account, [Sender Account]
|
||||
/// # </weight>
|
||||
#[pallet::weight(T::WeightInfo::vested_transfer(MaxLocksOf::<T>::get()))]
|
||||
#[pallet::weight(
|
||||
T::WeightInfo::vested_transfer(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
|
||||
)]
|
||||
pub fn vested_transfer(
|
||||
origin: OriginFor<T>,
|
||||
target: <T::Lookup as StaticLookup>::Source,
|
||||
schedule: VestingInfo<BalanceOf<T>, T::BlockNumber>,
|
||||
) -> DispatchResult {
|
||||
let transactor = ensure_signed(origin)?;
|
||||
ensure!(schedule.locked >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
|
||||
|
||||
let who = T::Lookup::lookup(target)?;
|
||||
ensure!(!Vesting::<T>::contains_key(&who), Error::<T>::ExistingVestingSchedule);
|
||||
|
||||
T::Currency::transfer(
|
||||
&transactor,
|
||||
&who,
|
||||
schedule.locked,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
)?;
|
||||
|
||||
Self::add_vesting_schedule(
|
||||
&who,
|
||||
schedule.locked,
|
||||
schedule.per_block,
|
||||
schedule.starting_block,
|
||||
)
|
||||
.expect("user does not have an existing vesting schedule; q.e.d.");
|
||||
|
||||
Ok(())
|
||||
let transactor = <T::Lookup as StaticLookup>::unlookup(transactor);
|
||||
Self::do_vested_transfer(transactor, target, schedule)
|
||||
}
|
||||
|
||||
/// Force a vested transfer.
|
||||
@@ -304,18 +385,21 @@ pub mod pallet {
|
||||
///
|
||||
/// - `source`: The account whose funds should be transferred.
|
||||
/// - `target`: The account that should be transferred the vested funds.
|
||||
/// - `amount`: The amount of funds to transfer and will be vested.
|
||||
/// - `schedule`: The vesting schedule attached to the transfer.
|
||||
///
|
||||
/// Emits `VestingCreated`.
|
||||
///
|
||||
/// NOTE: This will unlock all schedules through the current block.
|
||||
///
|
||||
/// # <weight>
|
||||
/// - `O(1)`.
|
||||
/// - DbWeight: 4 Reads, 4 Writes
|
||||
/// - Reads: Vesting Storage, Balances Locks, Target Account, Source Account
|
||||
/// - Writes: Vesting Storage, Balances Locks, Target Account, Source Account
|
||||
/// # </weight>
|
||||
#[pallet::weight(T::WeightInfo::force_vested_transfer(MaxLocksOf::<T>::get()))]
|
||||
#[pallet::weight(
|
||||
T::WeightInfo::force_vested_transfer(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
|
||||
)]
|
||||
pub fn force_vested_transfer(
|
||||
origin: OriginFor<T>,
|
||||
source: <T::Lookup as StaticLookup>::Source,
|
||||
@@ -323,26 +407,53 @@ pub mod pallet {
|
||||
schedule: VestingInfo<BalanceOf<T>, T::BlockNumber>,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
ensure!(schedule.locked >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
|
||||
Self::do_vested_transfer(source, target, schedule)
|
||||
}
|
||||
|
||||
let target = T::Lookup::lookup(target)?;
|
||||
let source = T::Lookup::lookup(source)?;
|
||||
ensure!(!Vesting::<T>::contains_key(&target), Error::<T>::ExistingVestingSchedule);
|
||||
/// 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.
|
||||
#[pallet::weight(
|
||||
T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES)
|
||||
.max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::<T>::get(), T::MAX_VESTING_SCHEDULES))
|
||||
)]
|
||||
pub fn merge_schedules(
|
||||
origin: OriginFor<T>,
|
||||
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;
|
||||
|
||||
T::Currency::transfer(
|
||||
&source,
|
||||
&target,
|
||||
schedule.locked,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
)?;
|
||||
let schedules = Self::vesting(&who).ok_or(Error::<T>::NotVesting)?;
|
||||
let merge_action = VestingAction::Merge(schedule1_index, schedule2_index);
|
||||
|
||||
Self::add_vesting_schedule(
|
||||
&target,
|
||||
schedule.locked,
|
||||
schedule.per_block,
|
||||
schedule.starting_block,
|
||||
)
|
||||
.expect("user does not have an existing vesting schedule; q.e.d.");
|
||||
let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?;
|
||||
|
||||
Self::write_vesting(&who, schedules)?;
|
||||
Self::write_lock(&who, locked_now);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -350,39 +461,233 @@ pub mod pallet {
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// (Re)set or remove the pallet's currency lock on `who`'s account in accordance with their
|
||||
/// current unvested amount.
|
||||
fn update_lock(who: T::AccountId) -> DispatchResult {
|
||||
let vesting = Self::vesting(&who).ok_or(Error::<T>::NotVesting)?;
|
||||
let now = <frame_system::Pallet<T>>::block_number();
|
||||
let locked_now = vesting.locked_at::<T::BlockNumberToBalance>(now);
|
||||
// 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: T::BlockNumber,
|
||||
schedule1: VestingInfo<BalanceOf<T>, T::BlockNumber>,
|
||||
schedule2: VestingInfo<BalanceOf<T>, T::BlockNumber>,
|
||||
) -> Option<VestingInfo<BalanceOf<T>, T::BlockNumber>> {
|
||||
let schedule1_ending_block = schedule1.ending_block_as_balance::<T::BlockNumberToBalance>();
|
||||
let schedule2_ending_block = schedule2.ending_block_as_balance::<T::BlockNumberToBalance>();
|
||||
let now_as_balance = T::BlockNumberToBalance::convert(now);
|
||||
|
||||
if locked_now.is_zero() {
|
||||
T::Currency::remove_lock(VESTING_ID, &who);
|
||||
Vesting::<T>::remove(&who);
|
||||
Self::deposit_event(Event::<T>::VestingCompleted(who));
|
||||
// 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::<T::BlockNumberToBalance>(now)
|
||||
.saturating_add(schedule2.locked_at::<T::BlockNumberToBalance>(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::Lookup as StaticLookup>::Source,
|
||||
target: <T::Lookup as StaticLookup>::Source,
|
||||
schedule: VestingInfo<BalanceOf<T>, T::BlockNumber>,
|
||||
) -> DispatchResult {
|
||||
// Validate user inputs.
|
||||
ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::<T>::AmountLow);
|
||||
if !schedule.is_valid() {
|
||||
return Err(Error::<T>::InvalidScheduleParams.into())
|
||||
};
|
||||
let target = T::Lookup::lookup(target)?;
|
||||
let source = T::Lookup::lookup(source)?;
|
||||
|
||||
// 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.
|
||||
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<VestingInfo<BalanceOf<T>, T::BlockNumber>>,
|
||||
action: VestingAction,
|
||||
) -> (Vec<VestingInfo<BalanceOf<T>, T::BlockNumber>>, BalanceOf<T>) {
|
||||
let now = <frame_system::Pallet<T>>::block_number();
|
||||
|
||||
let mut total_locked_now: BalanceOf<T> = Zero::zero();
|
||||
let filtered_schedules = action
|
||||
.pick_schedules::<T>(schedules)
|
||||
.filter_map(|schedule| {
|
||||
let locked_now = schedule.locked_at::<T::BlockNumberToBalance>(now);
|
||||
if locked_now.is_zero() {
|
||||
None
|
||||
} else {
|
||||
total_locked_now = total_locked_now.saturating_add(locked_now);
|
||||
Some(schedule)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(filtered_schedules, total_locked_now)
|
||||
}
|
||||
|
||||
/// Write an accounts updated vesting lock to storage.
|
||||
fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf<T>) {
|
||||
if total_locked_now.is_zero() {
|
||||
T::Currency::remove_lock(VESTING_ID, who);
|
||||
Self::deposit_event(Event::<T>::VestingCompleted(who.clone()));
|
||||
} else {
|
||||
let reasons = WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE;
|
||||
T::Currency::set_lock(VESTING_ID, &who, locked_now, reasons);
|
||||
Self::deposit_event(Event::<T>::VestingUpdated(who, locked_now));
|
||||
T::Currency::set_lock(VESTING_ID, who, total_locked_now, reasons);
|
||||
Self::deposit_event(Event::<T>::VestingUpdated(who.clone(), total_locked_now));
|
||||
};
|
||||
}
|
||||
|
||||
/// Write an accounts updated vesting schedules to storage.
|
||||
fn write_vesting(
|
||||
who: &T::AccountId,
|
||||
schedules: Vec<VestingInfo<BalanceOf<T>, T::BlockNumber>>,
|
||||
) -> Result<(), DispatchError> {
|
||||
let schedules: BoundedVec<
|
||||
VestingInfo<BalanceOf<T>, T::BlockNumber>,
|
||||
MaxVestingSchedulesGet<T>,
|
||||
> = schedules.try_into().map_err(|_| Error::<T>::AtMaxVestingSchedules)?;
|
||||
|
||||
if schedules.len() == 0 {
|
||||
Vesting::<T>::remove(&who);
|
||||
} else {
|
||||
Vesting::<T>::insert(who, schedules)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unlock any vested funds of `who`.
|
||||
fn do_vest(who: T::AccountId) -> DispatchResult {
|
||||
let schedules = Self::vesting(&who).ok_or(Error::<T>::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<VestingInfo<BalanceOf<T>, T::BlockNumber>>,
|
||||
action: VestingAction,
|
||||
) -> Result<(Vec<VestingInfo<BalanceOf<T>, T::BlockNumber>>, BalanceOf<T>), DispatchError> {
|
||||
let (schedules, locked_now) = match action {
|
||||
VestingAction::Merge(idx1, 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::<T>::ScheduleIndexOutOfBounds)?;
|
||||
let schedule2 = *schedules.get(idx2).ok_or(Error::<T>::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 = <frame_system::Pallet<T>>::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::<T::BlockNumberToBalance>(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<T: Config> VestingSchedule<T::AccountId> for Pallet<T>
|
||||
where
|
||||
BalanceOf<T>: MaybeSerializeDeserialize + Debug,
|
||||
{
|
||||
type Moment = T::BlockNumber;
|
||||
type Currency = T::Currency;
|
||||
type Moment = T::BlockNumber;
|
||||
|
||||
/// Get the amount that is currently being vested and cannot be transferred out of this account.
|
||||
fn vesting_balance(who: &T::AccountId) -> Option<BalanceOf<T>> {
|
||||
if let Some(v) = Self::vesting(who) {
|
||||
let now = <frame_system::Pallet<T>>::block_number();
|
||||
let locked_now = v.locked_at::<T::BlockNumberToBalance>(now);
|
||||
Some(T::Currency::free_balance(who).min(locked_now))
|
||||
let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| {
|
||||
schedule.locked_at::<T::BlockNumberToBalance>(now).saturating_add(total)
|
||||
});
|
||||
Some(T::Currency::free_balance(who).min(total_locked_now))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -390,14 +695,16 @@ where
|
||||
|
||||
/// Adds a vesting schedule to a given account.
|
||||
///
|
||||
/// If there already exists a vesting schedule for the given account, an `Err` is returned
|
||||
/// and nothing is updated.
|
||||
/// 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`.
|
||||
///
|
||||
/// 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<T>,
|
||||
@@ -407,22 +714,58 @@ where
|
||||
if locked.is_zero() {
|
||||
return Ok(())
|
||||
}
|
||||
if Vesting::<T>::contains_key(who) {
|
||||
Err(Error::<T>::ExistingVestingSchedule)?
|
||||
|
||||
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::<T>::InvalidScheduleParams.into())
|
||||
};
|
||||
|
||||
let mut schedules = Self::vesting(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::<T>::AtMaxVestingSchedules);
|
||||
|
||||
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<T>,
|
||||
per_block: BalanceOf<T>,
|
||||
starting_block: T::BlockNumber,
|
||||
) -> DispatchResult {
|
||||
// Check for `per_block` or `locked` of 0.
|
||||
if !VestingInfo::new(locked, per_block, starting_block).is_valid() {
|
||||
return Err(Error::<T>::InvalidScheduleParams.into())
|
||||
}
|
||||
let vesting_schedule = VestingInfo { locked, per_block, starting_block };
|
||||
Vesting::<T>::insert(who, vesting_schedule);
|
||||
// it can't fail, but even if somehow it did, we don't really care.
|
||||
let res = Self::update_lock(who.clone());
|
||||
debug_assert!(res.is_ok());
|
||||
|
||||
ensure!(
|
||||
(Vesting::<T>::decode_len(who).unwrap_or_default() as u32) < T::MAX_VESTING_SCHEDULES,
|
||||
Error::<T>::AtMaxVestingSchedules
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a vesting schedule for a given account.
|
||||
fn remove_vesting_schedule(who: &T::AccountId) {
|
||||
Vesting::<T>::remove(who);
|
||||
// it can't fail, but even if somehow it did, we don't really care.
|
||||
let res = Self::update_lock(who.clone());
|
||||
debug_assert!(res.is_ok());
|
||||
fn remove_vesting_schedule(who: &T::AccountId, schedule_index: u32) -> DispatchResult {
|
||||
let schedules = Self::vesting(who).ok_or(Error::<T>::NotVesting)?;
|
||||
let remove_action = VestingAction::Remove(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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user