From 420d610b46b9819128a4c58b45a65d75fa54342b Mon Sep 17 00:00:00 2001 From: Xiliang Chen <xlchen1291@gmail.com> Date: Tue, 30 Jun 2020 17:16:19 +1200 Subject: [PATCH] fix orml-vesting (#223) * fix vesting * add tests --- traits/src/currency.rs | 5 +- vesting/src/lib.rs | 63 +++++++++++++++++++------ vesting/src/mock.rs | 2 + vesting/src/tests.rs | 103 +++++++++++++++++++++++++++-------------- 4 files changed, 120 insertions(+), 53 deletions(-) diff --git a/traits/src/currency.rs b/traits/src/currency.rs index 83f73f3..b51c118 100644 --- a/traits/src/currency.rs +++ b/traits/src/currency.rs @@ -289,14 +289,11 @@ pub trait BasicReservableCurrency<AccountId>: BasicCurrency<AccountId> { ) -> result::Result<Self::Balance, DispatchError>; } +#[impl_trait_for_tuples::impl_for_tuples(30)] pub trait OnDustRemoval<CurrencyId, Balance> { fn on_dust_removal(currency_id: CurrencyId, balance: Balance); } -impl<CurrencyId, Balance> OnDustRemoval<CurrencyId, Balance> for () { - fn on_dust_removal(_: CurrencyId, _: Balance) {} -} - #[impl_trait_for_tuples::impl_for_tuples(30)] pub trait OnReceived<AccountId, CurrencyId, Balance> { fn on_received(account: &AccountId, currency: CurrencyId, amount: Balance); diff --git a/vesting/src/lib.rs b/vesting/src/lib.rs index 69d2748..e5b40c0 100644 --- a/vesting/src/lib.rs +++ b/vesting/src/lib.rs @@ -16,7 +16,7 @@ //! //! ### Dispatchable Functions //! -//! - `add_vesting_schedule` - Add a new vesting schedule for an account. +//! - `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. @@ -37,13 +37,16 @@ use sp_std::{ // #3295 https://github.com/paritytech/substrate/issues/3295 use frame_system::{self as system, ensure_root, ensure_signed}; use sp_runtime::{ - traits::{AtLeast32Bit, CheckedAdd, StaticLookup, Zero}, + traits::{AtLeast32Bit, CheckedAdd, Saturating, StaticLookup, Zero}, DispatchResult, RuntimeDebug, }; mod mock; mod tests; +/// The maximum number of vesting schedules an account can have. +pub const MAX_VESTINGS: usize = 20; + /// The vesting schedule. /// /// Benefits would be granted gradually, `per_period` amount every `period` of blocks @@ -60,6 +63,7 @@ pub struct VestingSchedule<BlockNumber, Balance: HasCompact> { impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + Copy> VestingSchedule<BlockNumber, Balance> { /// Returns the end of all periods, `None` if calculation overflows. pub fn end(&self) -> Option<BlockNumber> { + // period * period_count + start self.period .checked_mul(&self.period_count.into())? .checked_add(&self.start) @@ -75,6 +79,9 @@ impl<BlockNumber: AtLeast32Bit + Copy, Balance: AtLeast32Bit + Copy> VestingSche /// 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) @@ -99,6 +106,9 @@ pub type ScheduledItem<T> = ( pub trait Trait: frame_system::Trait { type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>; type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>; + + /// The minimum amount transferred to call `vested_transfer`. + type MinVestedTransfer: Get<BalanceOf<Self>>; } decl_storage! { @@ -140,6 +150,8 @@ decl_error! { ZeroVestingPeriodCount, NumOverflow, InsufficientBalanceToLock, + TooManyVestingSchedules, + AmountLow, } } @@ -147,6 +159,9 @@ decl_module! { pub struct Module<T: Trait> for enum Call where origin: T::Origin { type Error = Error<T>; + /// The minimum amount to be transferred to create a new vesting schedule. + const MinVestedTransfer: BalanceOf<T> = T::MinVestedTransfer::get(); + fn deposit_event() = default; /// # <weight> @@ -176,14 +191,14 @@ decl_module! { /// Base Weight: 47.26 µs /// # </weight> #[weight = 48 * WEIGHT_PER_MICROS + T::DbWeight::get().reads_writes(4, 4)] - pub fn add_vesting_schedule( + pub fn vested_transfer( origin, dest: <T::Lookup as StaticLookup>::Source, schedule: VestingScheduleOf<T>, ) { let from = ensure_signed(origin)?; let to = T::Lookup::lookup(dest)?; - Self::do_add_vesting_schedule(&from, &to, schedule.clone())?; + Self::do_vested_transfer(&from, &to, schedule.clone())?; Self::deposit_event(RawEvent::VestingScheduleAdded(from, to, schedule)); } @@ -229,24 +244,40 @@ impl<T: Trait> Module<T> { /// Returns locked balance based on current block number. fn locked_balance(who: &T::AccountId) -> BalanceOf<T> { let now = <frame_system::Module<T>>::block_number(); - Self::vesting_schedules(who) - .iter() - .fold(Zero::zero(), |acc, s| acc + s.locked_amount(now)) + <VestingSchedules<T>>::mutate_exists(who, |maybe_schedules| { + let total = if let Some(schedules) = maybe_schedules.as_mut() { + let mut total: BalanceOf<T> = 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 + }) } - fn do_add_vesting_schedule( - from: &T::AccountId, - to: &T::AccountId, - schedule: VestingScheduleOf<T>, - ) -> DispatchResult { + fn do_vested_transfer(from: &T::AccountId, to: &T::AccountId, schedule: VestingScheduleOf<T>) -> DispatchResult { let schedule_amount = Self::ensure_valid_vesting_schedule(&schedule)?; + + ensure!( + <VestingSchedules<T>>::decode_len(to).unwrap_or(0) < MAX_VESTINGS, + Error::<T>::TooManyVestingSchedules + ); + let total_amount = Self::locked_balance(to) .checked_add(&schedule_amount) .ok_or(Error::<T>::NumOverflow)?; T::Currency::transfer(from, to, schedule_amount, ExistenceRequirement::AllowDeath)?; T::Currency::set_lock(VESTING_LOCK_ID, to, total_amount, WithdrawReasons::all()); - <VestingSchedules<T>>::mutate(to, |v| (*v).push(schedule)); + <VestingSchedules<T>>::append(to, schedule); Ok(()) } @@ -276,6 +307,10 @@ impl<T: Trait> Module<T> { ensure!(!schedule.period_count.is_zero(), Error::<T>::ZeroVestingPeriodCount); ensure!(schedule.end().is_some(), Error::<T>::NumOverflow); - schedule.total_amount().ok_or(Error::<T>::NumOverflow) + let total = schedule.total_amount().ok_or(Error::<T>::NumOverflow)?; + + ensure!(total >= T::MinVestedTransfer::get(), Error::<T>::AmountLow); + + Ok(total) } } diff --git a/vesting/src/mock.rs b/vesting/src/mock.rs index 953f3eb..9b4d0c8 100644 --- a/vesting/src/mock.rs +++ b/vesting/src/mock.rs @@ -67,6 +67,7 @@ type Balance = u64; parameter_types! { pub const ExistentialDeposit: u64 = 1; + pub const MinVestedTransfer: u64 = 5; } impl pallet_balances::Trait for Runtime { @@ -81,6 +82,7 @@ pub type PalletBalances = pallet_balances::Module<Runtime>; impl Trait for Runtime { type Event = TestEvent; type Currency = PalletBalances; + type MinVestedTransfer = MinVestedTransfer; } pub type Vesting = Module<Runtime>; diff --git a/vesting/src/tests.rs b/vesting/src/tests.rs index ecf65b2..25a5c34 100644 --- a/vesting/src/tests.rs +++ b/vesting/src/tests.rs @@ -8,7 +8,7 @@ use mock::{ExtBuilder, Origin, PalletBalances, Runtime, System, TestEvent, Vesti use pallet_balances::{BalanceLock, Reasons}; #[test] -fn add_vesting_schedule_works() { +fn vested_transfer_works() { ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { System::set_block_number(1); @@ -18,11 +18,7 @@ fn add_vesting_schedule_works() { period_count: 1u32, per_period: 100u64, }; - assert_ok!(Vesting::add_vesting_schedule( - Origin::signed(ALICE), - BOB, - schedule.clone() - )); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule.clone()]); let vested_event = TestEvent::vesting(RawEvent::VestingScheduleAdded(ALICE, BOB, schedule)); @@ -39,7 +35,7 @@ fn add_new_vesting_schedule_merges_with_current_locked_balance_and_until() { period_count: 2u32, per_period: 10u64, }; - assert_ok!(Vesting::add_vesting_schedule(Origin::signed(ALICE), BOB, schedule)); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule)); System::set_block_number(12); @@ -49,11 +45,7 @@ fn add_new_vesting_schedule_merges_with_current_locked_balance_and_until() { period_count: 1u32, per_period: 7u64, }; - assert_ok!(Vesting::add_vesting_schedule( - Origin::signed(ALICE), - BOB, - another_schedule - )); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, another_schedule)); assert_eq!( PalletBalances::locks(&BOB).pop(), @@ -75,17 +67,13 @@ fn cannot_use_fund_if_not_claimed() { period_count: 1u32, per_period: 50u64, }; - assert_ok!(Vesting::add_vesting_schedule( - Origin::signed(ALICE), - BOB, - schedule.clone() - )); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); assert!(PalletBalances::ensure_can_withdraw(&BOB, 1, WithdrawReason::Transfer.into(), 49).is_err()); }); } #[test] -fn add_vesting_schedule_fails_if_zero_period_or_count() { +fn vested_transfer_fails_if_zero_period_or_count() { ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { let schedule = VestingSchedule { start: 1u64, @@ -94,7 +82,7 @@ fn add_vesting_schedule_fails_if_zero_period_or_count() { per_period: 100u64, }; assert_err!( - Vesting::add_vesting_schedule(Origin::signed(ALICE), BOB, schedule.clone()), + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()), Error::<Runtime>::ZeroVestingPeriod ); @@ -105,14 +93,14 @@ fn add_vesting_schedule_fails_if_zero_period_or_count() { per_period: 100u64, }; assert_err!( - Vesting::add_vesting_schedule(Origin::signed(ALICE), BOB, schedule.clone()), + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone()), Error::<Runtime>::ZeroVestingPeriodCount ); }); } #[test] -fn add_vesting_schedule_fails_if_transfer_err() { +fn vested_transfer_fails_if_transfer_err() { ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { let schedule = VestingSchedule { start: 1u64, @@ -121,14 +109,14 @@ fn add_vesting_schedule_fails_if_transfer_err() { per_period: 100u64, }; assert_err!( - Vesting::add_vesting_schedule(Origin::signed(BOB), ALICE, schedule.clone()), + Vesting::vested_transfer(Origin::signed(BOB), ALICE, schedule.clone()), pallet_balances::Error::<Runtime, _>::InsufficientBalance, ); }); } #[test] -fn add_vesting_schedule_fails_if_overflow() { +fn vested_transfer_fails_if_overflow() { ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { let schedule = VestingSchedule { start: 1u64, @@ -137,7 +125,7 @@ fn add_vesting_schedule_fails_if_overflow() { per_period: u64::max_value(), }; assert_err!( - Vesting::add_vesting_schedule(Origin::signed(ALICE), BOB, schedule), + Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule), Error::<Runtime>::NumOverflow ); @@ -148,7 +136,7 @@ fn add_vesting_schedule_fails_if_overflow() { per_period: 1u64, }; assert_err!( - Vesting::add_vesting_schedule(Origin::signed(ALICE), BOB, another_schedule), + Vesting::vested_transfer(Origin::signed(ALICE), BOB, another_schedule), Error::<Runtime>::NumOverflow ); }); @@ -163,11 +151,7 @@ fn claim_works() { period_count: 2u32, per_period: 10u64, }; - assert_ok!(Vesting::add_vesting_schedule( - Origin::signed(ALICE), - BOB, - schedule.clone() - )); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); System::set_block_number(11); // remain locked if not claimed @@ -199,11 +183,7 @@ fn update_vesting_schedules_works() { period_count: 2u32, per_period: 10u64, }; - assert_ok!(Vesting::add_vesting_schedule( - Origin::signed(ALICE), - BOB, - schedule.clone() - )); + assert_ok!(Vesting::vested_transfer(Origin::signed(ALICE), BOB, schedule.clone())); let updated_schedule = VestingSchedule { start: 0u64, @@ -234,3 +214,56 @@ fn update_vesting_schedules_fails_if_unexpected_existing_locks() { PalletBalances::set_lock(*b"prelocks", &BOB, 0u64, WithdrawReasons::all()); }); } + +#[test] +fn vested_transfer_check_for_min() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + let schedule = VestingSchedule { + start: 1u64, + period: 1u64, + period_count: 1u32, + per_period: 3u64, + }; + assert_err!( + Vesting::vested_transfer(Origin::signed(BOB), ALICE, schedule.clone()), + Error::<Runtime>::AmountLow + ); + }); +} + +#[test] +fn multiple_vesting_schedule_claim_works() { + ExtBuilder::default().one_hundred_for_alice().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()]); + + System::set_block_number(21); + + assert_ok!(Vesting::claim(Origin::signed(BOB))); + + assert_eq!(Vesting::vesting_schedules(&BOB), vec![schedule2]); + + System::set_block_number(31); + + assert_ok!(Vesting::claim(Origin::signed(BOB))); + + assert_eq!(VestingSchedules::<Runtime>::contains_key(&BOB), false); + + assert_eq!(PalletBalances::locks(&BOB), vec![]); + }); +} -- GitLab