--- /dev/null +++ b/pallets/orml-vesting/ README.md @@ -0,0 +1,9 @@ +# Vesting Module + +## Overview + +Vesting module provides a means of scheduled balance lock on an account. It uses the *graded vesting* way, which unlocks a specific amount of balance every period of time, until all balance unlocked. + +### Vesting Schedule + +The schedule of a vesting is described by data structure `VestingSchedule`: from the block number of `start`, for every `period` amount of blocks, `per_period` amount of balance would unlocked, until number of periods `period_count` reached. Note in vesting schedules, *time* is measured by block number. All `VestingSchedule`s under an account could be queried in chain state. --- /dev/null +++ b/pallets/orml-vesting/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "orml-vesting" +description = "Provides scheduled balance locking mechanism, in a *graded vesting* way." +license = "Apache-2.0" +version = "0.4.1-dev" +authors = ["Laminar Developers "] +edition = "2018" + +[dependencies] +scale-info = { version = "1.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.124", optional = true } +codec = { package = "parity-scale-codec", version = "2.3.1", default-features = false, features = ["max-encoded-len"] } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false } + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12", default-features = false } + +[dev-dependencies] +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.12" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "sp-io/std", + "frame-support/std", + "frame-system/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] --- /dev/null +++ b/pallets/orml-vesting/src/default_weight.rs @@ -0,0 +1,27 @@ +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0 + +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::weights::{constants::RocksDbWeight as DbWeight, Weight}; + +impl crate::WeightInfo for () { + fn vested_transfer() -> Weight { + (310_862_000 as Weight) + .saturating_add(DbWeight::get().reads(4 as Weight)) + .saturating_add(DbWeight::get().writes(4 as Weight)) + } + fn claim(i: u32) -> Weight { + (158_614_000 as Weight) + .saturating_add((958_000 as Weight).saturating_mul(i as Weight)) + .saturating_add(DbWeight::get().reads(3 as Weight)) + .saturating_add(DbWeight::get().writes(3 as Weight)) + } + fn update_vesting_schedules(i: u32) -> Weight { + (119_811_000 as Weight) + .saturating_add((2_320_000 as Weight).saturating_mul(i as Weight)) + .saturating_add(DbWeight::get().reads(2 as Weight)) + .saturating_add(DbWeight::get().writes(3 as Weight)) + } +} --- /dev/null +++ b/pallets/orml-vesting/src/lib.rs @@ -0,0 +1,380 @@ +//! # Vesting Module +//! +//! ## Overview +//! +//! Vesting module provides a means of scheduled balance lock on an account. It +//! uses the *graded vesting* way, which unlocks a specific amount of balance +//! every period of time, until all balance unlocked. +//! +//! ### Vesting Schedule +//! +//! The schedule of a vesting is described by data structure `VestingSchedule`: +//! from the block number of `start`, for every `period` amount of blocks, +//! `per_period` amount of balance would unlocked, until number of periods +//! `period_count` reached. Note in vesting schedules, *time* is measured by +//! block number. All `VestingSchedule`s under an account could be queried in +//! chain state. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! - `vested_transfer` - Add a new vesting schedule for an account. +//! - `claim` - Claim unlocked balances. +//! - `update_vesting_schedules` - Update all vesting schedules under an +//! account, `root` origin required. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] + +use codec::{HasCompact, MaxEncodedLen}; +use frame_support::{ + ensure, + pallet_prelude::*, + traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, LockIdentifier, LockableCurrency, WithdrawReasons}, + transactional, BoundedVec, +}; +use frame_system::{ensure_root, ensure_signed, pallet_prelude::*}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{AtLeast32Bit, BlockNumberProvider, CheckedAdd, Saturating, StaticLookup, Zero}, + ArithmeticError, DispatchResult, RuntimeDebug, +}; +use sp_std::{ + cmp::{Eq, PartialEq}, + convert::TryInto, + vec::Vec, +}; + +mod mock; +mod tests; +mod weights; + +pub use module::*; +pub use weights::WeightInfo; + +pub const VESTING_LOCK_ID: LockIdentifier = *b"ormlvest"; + +/// The vesting schedule. +/// +/// Benefits would be granted gradually, `per_period` amount every `period` +/// of blocks after `start`. +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct VestingSchedule { + /// Vesting starting block + pub start: BlockNumber, + /// Number of blocks between vest + pub period: BlockNumber, + /// Number of vest + pub period_count: u32, + /// Amount of tokens to release per vest + #[codec(compact)] + pub per_period: Balance, +} + +impl VestingSchedule { + /// Returns the end of all periods, `None` if calculation overflows. + pub fn end(&self) -> Option { + // period * period_count + start + self.period + .checked_mul(&self.period_count.into())? + .checked_add(&self.start) + } + + /// Returns all locked amount, `None` if calculation overflows. + pub fn total_amount(&self) -> Option { + self.per_period.checked_mul(&self.period_count.into()) + } + + /// Returns locked amount for a given `time`. + /// + /// Note this func assumes schedule is a valid one(non-zero period and + /// non-overflow total amount), and it should be guaranteed by callers. + pub fn locked_amount(&self, time: BlockNumber) -> Balance { + // full = (time - start) / period + // unrealized = period_count - full + // per_period * unrealized + let full = time + .saturating_sub(self.start) + .checked_div(&self.period) + .expect("ensured non-zero period; qed"); + let unrealized = self.period_count.saturating_sub(full.unique_saturated_into()); + self.per_period + .checked_mul(&unrealized.into()) + .expect("ensured non-overflow total amount; qed") + } +} + +#[frame_support::pallet] +pub mod module { + use super::*; + + pub(crate) type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + pub(crate) type VestingScheduleOf = VestingSchedule<::BlockNumber, BalanceOf>; + pub type ScheduledItem = ( + ::AccountId, + ::BlockNumber, + ::BlockNumber, + u32, + BalanceOf, + ); + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + + type Currency: LockableCurrency; + + #[pallet::constant] + /// The minimum amount transferred to call `vested_transfer`. + type MinVestedTransfer: Get>; + + /// Required origin for vested transfer. + type VestedTransferOrigin: EnsureOrigin; + + /// Weight information for extrinsics in this module. + type WeightInfo: WeightInfo; + + /// The maximum vesting schedules + type MaxVestingSchedules: Get; + + // The block number provider + type BlockNumberProvider: BlockNumberProvider; + } + + #[pallet::error] + pub enum Error { + /// Vesting period is zero + ZeroVestingPeriod, + /// Number of vests is zero + ZeroVestingPeriodCount, + /// Insufficient amount of balance to lock + InsufficientBalanceToLock, + /// This account have too many vesting schedules + TooManyVestingSchedules, + /// The vested transfer amount is too low + AmountLow, + /// Failed because the maximum vesting schedules was exceeded + MaxVestingSchedulesExceeded, + } + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + /// Added new vesting schedule. \[from, to, vesting_schedule\] + VestingScheduleAdded(T::AccountId, T::AccountId, VestingScheduleOf), + /// Claimed vesting. \[who, locked_amount\] + Claimed(T::AccountId, BalanceOf), + /// Updated vesting schedules. \[who\] + VestingSchedulesUpdated(T::AccountId), + } + + /// Vesting schedules of an account. + /// + /// VestingSchedules: map AccountId => Vec + #[pallet::storage] + #[pallet::getter(fn vesting_schedules)] + pub type VestingSchedules = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, T::MaxVestingSchedules>, + ValueQuery, + >; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub vesting: Vec>, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { vesting: vec![] } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + self.vesting + .iter() + .for_each(|(who, start, period, period_count, per_period)| { + let mut bounded_schedules = VestingSchedules::::get(who); + bounded_schedules + .try_push(VestingSchedule { + start: *start, + period: *period, + period_count: *period_count, + per_period: *per_period, + }) + .expect("Max vesting schedules exceeded"); + let total_amount = bounded_schedules + .iter() + .try_fold::<_, _, Result, DispatchError>>(Zero::zero(), |acc_amount, schedule| { + let amount = ensure_valid_vesting_schedule::(schedule)?; + Ok(acc_amount + amount) + }) + .expect("Invalid vesting schedule"); + + assert!( + T::Currency::free_balance(who) >= total_amount, + "Account do not have enough balance" + ); + + T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all()); + VestingSchedules::::insert(who, bounded_schedules); + }); + } + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks for Pallet {} + + #[pallet::call] + impl Pallet { + #[pallet::weight(T::WeightInfo::claim((::MaxVestingSchedules::get() / 2) as u32))] + pub fn claim(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let locked_amount = Self::do_claim(&who); + + Self::deposit_event(Event::Claimed(who, locked_amount)); + Ok(()) + } + + #[pallet::weight(T::WeightInfo::vested_transfer())] + pub fn vested_transfer( + origin: OriginFor, + dest: ::Source, + schedule: VestingScheduleOf, + ) -> DispatchResult { + let from = T::VestedTransferOrigin::ensure_origin(origin)?; + let to = T::Lookup::lookup(dest)?; + Self::do_vested_transfer(&from, &to, schedule.clone())?; + + Self::deposit_event(Event::VestingScheduleAdded(from, to, schedule)); + Ok(()) + } + + #[pallet::weight(T::WeightInfo::update_vesting_schedules(vesting_schedules.len() as u32))] + pub fn update_vesting_schedules( + origin: OriginFor, + who: ::Source, + vesting_schedules: Vec>, + ) -> DispatchResult { + ensure_root(origin)?; + + let account = T::Lookup::lookup(who)?; + Self::do_update_vesting_schedules(&account, vesting_schedules)?; + + Self::deposit_event(Event::VestingSchedulesUpdated(account)); + Ok(()) + } + + #[pallet::weight(T::WeightInfo::claim((::MaxVestingSchedules::get() / 2) as u32))] + pub fn claim_for(origin: OriginFor, dest: ::Source) -> DispatchResult { + let _ = ensure_signed(origin)?; + let who = T::Lookup::lookup(dest)?; + let locked_amount = Self::do_claim(&who); + + Self::deposit_event(Event::Claimed(who, locked_amount)); + Ok(()) + } + } +} + +impl Pallet { + fn do_claim(who: &T::AccountId) -> BalanceOf { + let locked = Self::locked_balance(who); + if locked.is_zero() { + // cleanup the storage and unlock the fund + >::remove(who); + T::Currency::remove_lock(VESTING_LOCK_ID, who); + } else { + T::Currency::set_lock(VESTING_LOCK_ID, who, locked, WithdrawReasons::all()); + } + locked + } + + /// Returns locked balance based on current block number. + fn locked_balance(who: &T::AccountId) -> BalanceOf { + let now = T::BlockNumberProvider::current_block_number(); + >::mutate_exists(who, |maybe_schedules| { + let total = if let Some(schedules) = maybe_schedules.as_mut() { + let mut total: BalanceOf = Zero::zero(); + schedules.retain(|s| { + let amount = s.locked_amount(now); + total = total.saturating_add(amount); + !amount.is_zero() + }); + total + } else { + Zero::zero() + }; + if total.is_zero() { + *maybe_schedules = None; + } + total + }) + } + + #[transactional] + fn do_vested_transfer(from: &T::AccountId, to: &T::AccountId, schedule: VestingScheduleOf) -> DispatchResult { + let schedule_amount = ensure_valid_vesting_schedule::(&schedule)?; + + let total_amount = Self::locked_balance(to) + .checked_add(&schedule_amount) + .ok_or(ArithmeticError::Overflow)?; + + T::Currency::transfer(from, to, schedule_amount, ExistenceRequirement::AllowDeath)?; + T::Currency::set_lock(VESTING_LOCK_ID, to, total_amount, WithdrawReasons::all()); + >::try_append(to, schedule).map_err(|_| Error::::MaxVestingSchedulesExceeded)?; + Ok(()) + } + + fn do_update_vesting_schedules(who: &T::AccountId, schedules: Vec>) -> DispatchResult { + let bounded_schedules: BoundedVec, T::MaxVestingSchedules> = schedules + .try_into() + .map_err(|_| Error::::MaxVestingSchedulesExceeded)?; + + // empty vesting schedules cleanup the storage and unlock the fund + if bounded_schedules.len().is_zero() { + >::remove(who); + T::Currency::remove_lock(VESTING_LOCK_ID, who); + return Ok(()); + } + + let total_amount = bounded_schedules + .iter() + .try_fold::<_, _, Result, DispatchError>>(Zero::zero(), |acc_amount, schedule| { + let amount = ensure_valid_vesting_schedule::(schedule)?; + Ok(acc_amount + amount) + })?; + ensure!( + T::Currency::free_balance(who) >= total_amount, + Error::::InsufficientBalanceToLock, + ); + + T::Currency::set_lock(VESTING_LOCK_ID, who, total_amount, WithdrawReasons::all()); + >::insert(who, bounded_schedules); + + Ok(()) + } +} + +/// Returns `Ok(total_total)` if valid schedule, or error. +fn ensure_valid_vesting_schedule(schedule: &VestingScheduleOf) -> Result, DispatchError> { + ensure!(!schedule.period.is_zero(), Error::::ZeroVestingPeriod); + ensure!(!schedule.period_count.is_zero(), Error::::ZeroVestingPeriodCount); + ensure!(schedule.end().is_some(), ArithmeticError::Overflow); + + let total_total = schedule.total_amount().ok_or(ArithmeticError::Overflow)?; + + ensure!(total_total >= T::MinVestedTransfer::get(), Error::::AmountLow); + + Ok(total_total) +} --- /dev/null +++ b/pallets/orml-vesting/src/mock.rs @@ -0,0 +1,153 @@ +//! Mocks for the vesting module. + +#![cfg(test)] + +use super::*; +use frame_support::{ + construct_runtime, parameter_types, + traits::{EnsureOrigin, Everything}, +}; +use frame_system::RawOrigin; +use sp_core::H256; +use sp_runtime::{testing::Header, traits::IdentityLookup}; + +use crate as vesting; + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +pub type AccountId = u128; +impl frame_system::Config for Runtime { + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +type Balance = u64; + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +pub struct EnsureAliceOrBob; +impl EnsureOrigin for EnsureAliceOrBob { + type Success = AccountId; + + fn try_origin(o: Origin) -> Result { + Into::, Origin>>::into(o).and_then(|o| match o { + RawOrigin::Signed(ALICE) => Ok(ALICE), + RawOrigin::Signed(BOB) => Ok(BOB), + r => Err(Origin::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> Origin { + Origin::from(RawOrigin::Signed(Default::default())) + } +} + +parameter_types! { + pub const MaxVestingSchedule: u32 = 2; + pub const MinVestedTransfer: u64 = 5; + pub static MockBlockNumberProvider: u64 = 0; +} + +impl BlockNumberProvider for MockBlockNumberProvider { + type BlockNumber = u64; + + fn current_block_number() -> Self::BlockNumber { + Self::get() + } +} + +impl Config for Runtime { + type Event = Event; + type Currency = PalletBalances; + type MinVestedTransfer = MinVestedTransfer; + type VestedTransferOrigin = EnsureAliceOrBob; + type WeightInfo = (); + type MaxVestingSchedules = MaxVestingSchedule; + type BlockNumberProvider = MockBlockNumberProvider; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + Vesting: vesting::{Pallet, Storage, Call, Event, Config}, + PalletBalances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + } +); + +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; +pub const CHARLIE: AccountId = 3; + +#[derive(Default)] +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(ALICE, 100), (CHARLIE, 50)], + } + .assimilate_storage(&mut t) + .unwrap(); + + vesting::GenesisConfig:: { + vesting: vec![ + // who, start, period, period_count, per_period + (CHARLIE, 2, 3, 1, 5), + (CHARLIE, 2 + 3, 3, 3, 5), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} --- /dev/null +++ b/pallets/orml-vesting/src/test.rs @@ -0,0 +1,411 @@ +//! Unit tests for the vesting module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok, error::BadOrigin}; +use mock::{Event, *}; +use pallet_balances::{BalanceLock, Reasons}; + +#[test] +fn vesting_from_chain_spec_works() { + ExtBuilder::build().execute_with(|| { + assert_ok!(PalletBalances::ensure_can_withdraw( + &CHARLIE, + 10, + WithdrawReasons::TRANSFER, + 20 + )); + assert!(PalletBalances::ensure_can_withdraw(&CHARLIE, 11, WithdrawReasons::TRANSFER, 19).is_err()); + + assert_eq!( + Vesting::vesting_schedules(&CHARLIE), + vec![ + VestingSchedule { + start: 2u64, + period: 3u64, + period_count: 1u32, + per_period: 5u64, + }, + VestingSchedule { + start: 2u64 + 3u64, + period: 3u64, + period_count: 3u32, + per_period: 5u64, + } + ] + ); + + MockBlockNumberProvider::set(13); + + assert_ok!(Vesting::claim(Origin::signed(CHARLIE))); + + assert_ok!(PalletBalances::ensure_can_withdraw( + &CHARLIE, + 25, + WithdrawReasons::TRANSFER, + 5 + )); + assert!(PalletBalances::ensure_can_withdraw(&CHARLIE, 26, WithdrawReasons::TRANSFER, 4).is_err()); + + MockBlockNumberProvider::set(14); + + assert_ok!(Vesting::claim(Origin::signed(CHARLIE))); + + assert_ok!(PalletBalances::ensure_can_withdraw( + &CHARLIE, + 30, + WithdrawReasons::TRANSFER, + 0 + )); + }); +} + +#[test] +fn vested_transfer_works() { + ExtBuilder::build().execute_with(|| { + System::set_block_number(1); + + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 1u32, + per_period: 100u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); + assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule.clone()]); + System::assert_last_event(Event::Vesting(crate::Event::VestingScheduleAdded(ALICE, BOB, schedule))); + }); +} + +#[test] +fn add_new_vesting_schedule_merges_with_current_locked_balance_and_until() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule)); + + MockBlockNumberProvider::set(12); + + let another_schedule = VestingSchedule { + start: 10u64, + period: 13u64, + period_count: 1u32, + per_period: 7u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, another_schedule)); + + assert_eq!( + PalletBalances::locks(&BOB).get(0), + Some(&BalanceLock { + id: VESTING_LOCK_ID, + amount: 17u64, + reasons: Reasons::All, + }) + ); + }); +} + +#[test] +fn cannot_use_fund_if_not_claimed() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 10u64, + period: 10u64, + period_count: 1u32, + per_period: 50u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule)); + assert!(PalletBalances::ensure_can_withdraw(&BOB, 1, WithdrawReasons::TRANSFER, 49).is_err()); + }); +} + +#[test] +fn vested_transfer_fails_if_zero_period_or_count() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 1u64, + period: 0u64, + period_count: 1u32, + per_period: 100u64, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule), + Error::::ZeroVestingPeriod + ); + + let schedule = VestingSchedule { + start: 1u64, + period: 1u64, + period_count: 0u32, + per_period: 100u64, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule), + Error::::ZeroVestingPeriodCount + ); + }); +} + +#[test] +fn vested_transfer_fails_if_transfer_err() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 1u64, + period: 1u64, + period_count: 1u32, + per_period: 100u64, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(BOB), ALICE, schedule), + pallet_balances::Error::::InsufficientBalance, + ); + }); +} + +#[test] +fn vested_transfer_fails_if_overflow() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 1u64, + period: 1u64, + period_count: 2u32, + per_period: u64::MAX, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule), + ArithmeticError::Overflow, + ); + + let another_schedule = VestingSchedule { + start: u64::MAX, + period: 1u64, + period_count: 2u32, + per_period: 1u64, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(ALICE), BOB, another_schedule), + ArithmeticError::Overflow, + ); + }); +} + +#[test] +fn vested_transfer_fails_if_bad_origin() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 1u32, + per_period: 100u64, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(CHARLIE), BOB, schedule), + BadOrigin + ); + }); +} + +#[test] +fn claim_works() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule)); + + MockBlockNumberProvider::set(11); + // remain locked if not claimed + assert!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10).is_err()); + // unlocked after claiming + assert_ok!(Vesting::claim(Origin::signed(BOB))); + assert!(VestingSchedules::::contains_key(BOB)); + assert_ok!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10)); + // more are still locked + assert!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 1).is_err()); + + MockBlockNumberProvider::set(21); + // claim more + assert_ok!(Vesting::claim(Origin::signed(BOB))); + assert!(!VestingSchedules::::contains_key(BOB)); + assert_ok!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10)); + // all used up + assert_eq!(PalletBalances::free_balance(BOB), 0); + + // no locks anymore + assert_eq!(PalletBalances::locks(&BOB), vec![]); + }); +} + +#[test] +fn claim_for_works() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule)); + + assert_ok!(Vesting::claim_for(Origin::signed(ALICE), BOB)); + + assert_eq!( + PalletBalances::locks(&BOB).get(0), + Some(&BalanceLock { + id: VESTING_LOCK_ID, + amount: 20u64, + reasons: Reasons::All, + }) + ); + assert!(VestingSchedules::::contains_key(&BOB)); + + MockBlockNumberProvider::set(21); + + assert_ok!(Vesting::claim_for(Origin::signed(ALICE), BOB)); + + // no locks anymore + assert_eq!(PalletBalances::locks(&BOB), vec![]); + assert!(!VestingSchedules::::contains_key(&BOB)); + }); +} + +#[test] +fn update_vesting_schedules_works() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule)); + + let updated_schedule = VestingSchedule { + start: 0u64, + period: 20u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::update_vesting_schedules( + Origin::root(), + BOB, + vec![updated_schedule] + )); + + MockBlockNumberProvider::set(11); + assert_ok!(Vesting::claim(Origin::signed(BOB))); + assert!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 1).is_err()); + + MockBlockNumberProvider::set(21); + assert_ok!(Vesting::claim(Origin::signed(BOB))); + assert_ok!(PalletBalances::transfer(Origin::signed(BOB), ALICE, 10)); + + // empty vesting schedules cleanup the storage and unlock the fund + assert!(VestingSchedules::::contains_key(BOB)); + assert_eq!( + PalletBalances::locks(&BOB).get(0), + Some(&BalanceLock { + id: VESTING_LOCK_ID, + amount: 10u64, + reasons: Reasons::All, + }) + ); + assert_ok!(Vesting::update_vesting_schedules(Origin::root(), BOB, vec![])); + assert!(!VestingSchedules::::contains_key(BOB)); + assert_eq!(PalletBalances::locks(&BOB), vec![]); + }); +} + +#[test] +fn update_vesting_schedules_fails_if_unexpected_existing_locks() { + ExtBuilder::build().execute_with(|| { + assert_ok!(PalletBalances::transfer(Origin::signed(ALICE), BOB, 1)); + PalletBalances::set_lock(*b"prelocks", &BOB, 0u64, WithdrawReasons::all()); + }); +} + +#[test] +fn vested_transfer_check_for_min() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 1u64, + period: 1u64, + period_count: 1u32, + per_period: 3u64, + }; + assert_noop!( + Vesting::vested_transfer(Origin::signed(BOB), ALICE, schedule), + Error::::AmountLow + ); + }); +} + +#[test] +fn multiple_vesting_schedule_claim_works() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); + + let schedule2 = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 3u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule2.clone())); + + assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule, schedule2.clone()]); + + MockBlockNumberProvider::set(21); + + assert_ok!(Vesting::claim(Origin::signed(BOB))); + + assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule2]); + + MockBlockNumberProvider::set(31); + + assert_ok!(Vesting::claim(Origin::signed(BOB))); + + assert!(!VestingSchedules::::contains_key(&BOB)); + + assert_eq!(PalletBalances::locks(&BOB), vec![]); + }); +} + +#[test] +fn exceeding_maximum_schedules_should_fail() { + ExtBuilder::build().execute_with(|| { + let schedule = VestingSchedule { + start: 0u64, + period: 10u64, + period_count: 2u32, + per_period: 10u64, + }; + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); + assert_noop!( + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()), + Error::::MaxVestingSchedulesExceeded + ); + + let schedules = vec![schedule.clone(), schedule.clone(), schedule]; + + assert_noop!( + Vesting::update_vesting_schedules(Origin::root(), BOB, schedules), + Error::::MaxVestingSchedulesExceeded + ); + }); +} --- /dev/null +++ b/pallets/orml-vesting/src/weights.rs @@ -0,0 +1,59 @@ +//! Autogenerated weights for orml_vesting +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-05-04, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// /Users/xiliangchen/projects/acala/target/release/acala +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=orml_vesting +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./vesting/src/weights.rs +// --template +// ../templates/orml-weight-template.hbs + + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for orml_vesting. +pub trait WeightInfo { + fn vested_transfer() -> Weight; + fn claim(i: u32, ) -> Weight; + fn update_vesting_schedules(i: u32, ) -> Weight; +} + +/// Default weights. +impl WeightInfo for () { + fn vested_transfer() -> Weight { + (69_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + fn claim(i: u32, ) -> Weight { + (31_747_000 as Weight) + // Standard Error: 4_000 + .saturating_add((63_000 as Weight).saturating_mul(i as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn update_vesting_schedules(i: u32, ) -> Weight { + (29_457_000 as Weight) + // Standard Error: 4_000 + .saturating_add((117_000 as Weight).saturating_mul(i as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } +} --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -362,7 +362,7 @@ default-features = false [dependencies.orml-vesting] -git = "https://github.com/open-web3-stack/open-runtime-module-library" +path = '../pallets/orml-vesting' version = "0.4.1-dev" default-features = false