diff --git a/Cargo.lock b/Cargo.lock index a8732adf0..0ed5ed215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10163,6 +10163,22 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vesting-logic" +version = "0.1.0" +dependencies = [ + "frame-support", + "vesting-schedule", +] + +[[package]] +name = "vesting-schedule" +version = "0.1.0" +dependencies = [ + "frame-support", + "sp-arithmetic", +] + [[package]] name = "void" version = "1.0.2" diff --git a/crates/vesting-logic/Cargo.toml b/crates/vesting-logic/Cargo.toml new file mode 100644 index 000000000..fc539ddf3 --- /dev/null +++ b/crates/vesting-logic/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "vesting-logic" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +vesting-schedule = { version = "0.1", path = "../vesting-schedule", default-features = false } + +frame-support = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "vesting-schedule/std", +] diff --git a/crates/vesting-logic/src/lib.rs b/crates/vesting-logic/src/lib.rs new file mode 100644 index 000000000..dace4bba3 --- /dev/null +++ b/crates/vesting-logic/src/lib.rs @@ -0,0 +1,6 @@ +//! The vesting. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod traits; +pub use traits::*; diff --git a/crates/vesting-logic/src/traits.rs b/crates/vesting-logic/src/traits.rs new file mode 100644 index 000000000..92af92942 --- /dev/null +++ b/crates/vesting-logic/src/traits.rs @@ -0,0 +1,46 @@ +//! Generic vesting related traits to abstract away the implementations. + +use frame_support::{dispatch::DispatchResult, traits::Currency}; +use vesting_schedule::VestingSchedule; + +/// A general vesting logic. +pub trait Vesting { + /// Defines logic of vesting schedule to be used. + type VestingSchedule: VestingSchedule; + + /// Get the amount that is currently being vested and cannot be transferred out of this account. + /// Returns `None` if the account has no vesting schedule. + fn vesting_balance( + who: &AccountId, + ) -> Option< + <>::Currency as Currency>::Balance, + >; + + /// Adds a vesting schedule to a given account. + /// + /// If the account has `MaxVestingSchedules`, an Error is returned and nothing + /// is updated. + /// + /// Is a no-op if the amount to be vested is zero. + fn add_vesting_schedule( + who: &AccountId, + locked: <>::Currency as Currency< + AccountId, + >>::Balance, + start: >::Moment, + vesting_schedule: Self::VestingSchedule, + ) -> DispatchResult; + + /// Checks if `add_vesting_schedule` would work against `who`. + fn can_add_vesting_schedule( + who: &AccountId, + locked: <>::Currency as Currency< + AccountId, + >>::Balance, + start: >::Moment, + vesting_schedule: Self::VestingSchedule, + ) -> DispatchResult; + + /// Remove a vesting schedule for a given account. + fn remove_vesting_schedule(who: &AccountId, schedule_index: u32) -> DispatchResult; +} diff --git a/crates/vesting-schedule/Cargo.toml b/crates/vesting-schedule/Cargo.toml new file mode 100644 index 000000000..0c3d98025 --- /dev/null +++ b/crates/vesting-schedule/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "vesting-schedule" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +frame-support = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } +sp-arithmetic = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "sp-arithmetic/std", +] diff --git a/crates/vesting-schedule/src/lib.rs b/crates/vesting-schedule/src/lib.rs new file mode 100644 index 000000000..9c4345394 --- /dev/null +++ b/crates/vesting-schedule/src/lib.rs @@ -0,0 +1,113 @@ +//! The vesting schedule. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::Currency as CurrencyT; +use sp_arithmetic::traits::{ + AtLeast32BitUnsigned, CheckedDiv, CheckedMul, Saturating, UniqueSaturatedFrom, + UniqueSaturatedInto, Zero, +}; + +mod traits; +pub use traits::*; + +/// Implements linear westing logic with cliff. +pub struct LinearWithCliff> { + /// Vesting cliff. + cliff: Moment, + /// Vesting period. + period: Moment, + /// Amount that should be unlocked per one vesting period. (!= 0) + per_period: Currency::Balance, +} + +/// An error that can occur at linear with cliff vesting schedule logic. +pub enum LinearWithCliffError { + /// We don't let `per_period` be less than 1, or else the vesting will never end. + ZeroPerPeriod, + /// Genesis locked shouldn't be zero. + ZeroGenesisLocked, +} + +impl VestingSchedule + for LinearWithCliff +where + Currency: CurrencyT, + Moment: AtLeast32BitUnsigned + Copy, +{ + type Moment = Moment; + + type Currency = Currency; + + type Error = LinearWithCliffError; + + fn validate( + &self, + genesis_locked: Currency::Balance, + _start: Self::Moment, + ) -> Result<(), Self::Error> { + if self.per_period == Zero::zero() { + return Err(LinearWithCliffError::ZeroPerPeriod); + } + if genesis_locked == Zero::zero() { + return Err(LinearWithCliffError::ZeroGenesisLocked); + } + Ok(()) + } + + fn locked_at( + &self, + genesis_locked: Currency::Balance, + start: Self::Moment, + moment: Self::Moment, + ) -> Currency::Balance { + let actual_start = start.saturating_add(self.cliff); + if actual_start > moment { + return genesis_locked; + } + + let actual_end = self.end(genesis_locked, start); + if actual_end < moment { + return Zero::zero(); + } + + let actual_vesting_time = moment.saturating_sub(actual_start); + let periods_number = >::unique_saturated_into( + actual_vesting_time + .checked_div(&self.period) + .unwrap_or_else(Zero::zero), + ); + let vested_balance = self + .per_period + .checked_mul( + &>::unique_saturated_from( + periods_number, + ), + ) + .unwrap_or_else(Zero::zero); + genesis_locked.saturating_sub(vested_balance) + } + + fn end(&self, genesis_locked: Currency::Balance, start: Self::Moment) -> Self::Moment { + let periods_number = >::unique_saturated_into( + genesis_locked + .checked_div(&self.per_period) + .unwrap_or_else(Zero::zero), + ) + if (genesis_locked % self.per_period).is_zero() { + 0 + } else { + // `per_period` does not perfectly divide `locked`, so we need an extra period to + // unlock some amount less than `per_period`. + 1 + }; + + let actual_start = start.saturating_add(self.cliff); + let actual_vesting_time = self + .period + .checked_mul( + &>::unique_saturated_from(periods_number), + ) + .unwrap_or_else(Zero::zero); + actual_start.saturating_add(actual_vesting_time) + } +} diff --git a/crates/vesting-schedule/src/traits.rs b/crates/vesting-schedule/src/traits.rs new file mode 100644 index 000000000..d295d3898 --- /dev/null +++ b/crates/vesting-schedule/src/traits.rs @@ -0,0 +1,32 @@ +//! Generic vesting schedule related traits to abstract away the implementations. + +use frame_support::traits::Currency; + +/// [`VestingSchedule`] defines logic for currency vesting(unlocking). +pub trait VestingSchedule { + /// The type used to denote time: Timestamp, BlockNumber, etc. + type Moment; + /// The currency that this schedule applies to. + type Currency: Currency; + /// An error that can occur at vesting schedule logic. + type Error; + /// Validate the schedule. + fn validate( + &self, + genesis_locked: >::Balance, + start: Self::Moment, + ) -> Result<(), Self::Error>; + /// Locked amount at provided moment. + fn locked_at( + &self, + genesis_locked: >::Balance, + start: Self::Moment, + moment: Self::Moment, + ) -> >::Balance; + /// Moment at which the schedule ends. + fn end( + &self, + genesis_locked: >::Balance, + start: Self::Moment, + ) -> Self::Moment; +}