Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
Introduce vesting offsets (#3094)
Browse files Browse the repository at this point in the history
* Introduce vesting offsets

Closes #3090

* Fix logic

* Bump impl verfsion

* Initial rewrite of vesting

* Test for liquidity with delayed vesting

* Bump Spec, Fix line width

* More line width fix

* Small nit to documentation
  • Loading branch information
gavofyork authored Jul 16, 2019
1 parent b6e6707 commit ca9f9e5
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 64 deletions.
2 changes: 1 addition & 1 deletion node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// and set impl_version to equal spec_version. If only runtime
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
spec_version: 110,
spec_version: 111,
impl_version: 111,
apis: RUNTIME_API_VERSIONS,
};
Expand Down
52 changes: 33 additions & 19 deletions srml/balances/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,20 +277,26 @@ decl_event!(
/// Struct to encode the vesting schedule of an individual account.
#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct VestingSchedule<Balance> {
pub struct VestingSchedule<Balance, BlockNumber> {
/// Locked amount at genesis.
pub offset: Balance,
/// Amount that gets unlocked every block from 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,
}

impl<Balance: SimpleArithmetic + Copy> VestingSchedule<Balance> {
impl<Balance: SimpleArithmetic + Copy, BlockNumber: SimpleArithmetic + Copy> VestingSchedule<Balance, BlockNumber> {
/// Amount locked at block `n`.
pub fn locked_at<BlockNumber>(&self, n: BlockNumber) -> Balance
pub fn locked_at(&self, n: BlockNumber) -> Balance
where Balance: From<BlockNumber>
{
if let Some(x) = Balance::from(n).checked_mul(&self.per_block) {
self.offset.max(x) - x
// Number of blocks that count toward vesting
// Saturating to 0 when n < starting_block
let vested_block_count = n.saturating_sub(self.starting_block);
// Return amount that is still locked in vesting
if let Some(x) = Balance::from(vested_block_count).checked_mul(&self.per_block) {
self.locked.max(x) - x
} else {
Zero::zero()
}
Expand All @@ -315,23 +321,30 @@ decl_storage! {

/// Information regarding the vesting of a given account.
pub Vesting get(vesting) build(|config: &GenesisConfig<T, I>| {
config.vesting.iter().filter_map(|&(ref who, begin, length)| {
let begin = <T::Balance as From<T::BlockNumber>>::from(begin);
// 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
config.vesting.iter().filter_map(|&(ref who, begin, length, liquid)| {
let length = <T::Balance as From<T::BlockNumber>>::from(length);

config.balances.iter()
.find(|&&(ref w, _)| w == who)
.map(|&(_, balance)| {
// <= begin it should be >= balance
// >= begin+length it should be <= 0

let per_block = balance / length.max(primitives::traits::One::one());
let offset = begin * per_block + balance;

(who.clone(), VestingSchedule { offset, per_block })
// Total genesis `balance` minus `liquid` equals funds locked for vesting
let locked = balance.saturating_sub(liquid);
// Number of units unlocked per block after `begin`
let per_block = locked / length.max(primitives::traits::One::one());

(who.clone(), VestingSchedule {
locked: locked,
per_block: per_block,
starting_block: begin
})
})
}).collect::<Vec<_>>()
}): map T::AccountId => Option<VestingSchedule<T::Balance>>;
}): map T::AccountId => Option<VestingSchedule<T::Balance, T::BlockNumber>>;

/// The 'free' balance of a given account.
///
Expand Down Expand Up @@ -366,7 +379,8 @@ decl_storage! {
}
add_extra_genesis {
config(balances): Vec<(T::AccountId, T::Balance)>;
config(vesting): Vec<(T::AccountId, T::BlockNumber, T::BlockNumber)>; // begin, length
config(vesting): Vec<(T::AccountId, T::BlockNumber, T::BlockNumber, T::Balance)>;
// ^^ begin, length, amount liquid at genesis
}
}

Expand Down Expand Up @@ -471,7 +485,7 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
pub fn vesting_balance(who: &T::AccountId) -> T::Balance {
if let Some(v) = Self::vesting(who) {
Self::free_balance(who)
.min(v.locked_at::<T::BlockNumber>(<system::Module<T>>::block_number()))
.min(v.locked_at(<system::Module<T>>::block_number()))
} else {
Zero::zero()
}
Expand Down
14 changes: 12 additions & 2 deletions srml/balances/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,22 @@ impl ExtBuilder {
let mut t = system::GenesisConfig::default().build_storage::<Runtime>().unwrap().0;
t.extend(GenesisConfig::<Runtime> {
balances: if self.monied {
vec![(1, 10 * self.existential_deposit), (2, 20 * self.existential_deposit), (3, 30 * self.existential_deposit), (4, 40 * self.existential_deposit)]
vec![
(1, 10 * self.existential_deposit),
(2, 20 * self.existential_deposit),
(3, 30 * self.existential_deposit),
(4, 40 * self.existential_deposit),
(12, 10 * self.existential_deposit)
]
} else {
vec![]
},
vesting: if self.vesting && self.monied {
vec![(1, 0, 10), (2, 10, 20)]
vec![
(1, 0, 10, 5 * self.existential_deposit),
(2, 10, 20, 0),
(12, 10, 20, 5 * self.existential_deposit)
]
} else {
vec![]
},
Expand Down
157 changes: 115 additions & 42 deletions srml/balances/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,31 +111,37 @@ fn lock_value_extension_should_work() {

#[test]
fn lock_reasons_should_work() {
with_externalities(&mut ExtBuilder::default().existential_deposit(1).monied(true).transaction_fees(0, 1).build(), || {
Balances::set_lock(ID_1, &1, 10, u64::max_value(), WithdrawReason::Transfer.into());
assert_noop!(
<Balances as Currency<_>>::transfer(&1, &2, 1),
"account liquidity restrictions prevent withdrawal"
);
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&1, 1));
assert_ok!(<Balances as MakePayment<_>>::make_payment(&1, 1));
with_externalities(
&mut ExtBuilder::default()
.existential_deposit(1)
.monied(true).transaction_fees(0, 1)
.build(),
|| {
Balances::set_lock(ID_1, &1, 10, u64::max_value(), WithdrawReason::Transfer.into());
assert_noop!(
<Balances as Currency<_>>::transfer(&1, &2, 1),
"account liquidity restrictions prevent withdrawal"
);
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&1, 1));
assert_ok!(<Balances as MakePayment<_>>::make_payment(&1, 1));

Balances::set_lock(ID_1, &1, 10, u64::max_value(), WithdrawReason::Reserve.into());
assert_ok!(<Balances as Currency<_>>::transfer(&1, &2, 1));
assert_noop!(
<Balances as ReservableCurrency<_>>::reserve(&1, 1),
"account liquidity restrictions prevent withdrawal"
);
assert_ok!(<Balances as MakePayment<_>>::make_payment(&1, 1));
Balances::set_lock(ID_1, &1, 10, u64::max_value(), WithdrawReason::Reserve.into());
assert_ok!(<Balances as Currency<_>>::transfer(&1, &2, 1));
assert_noop!(
<Balances as ReservableCurrency<_>>::reserve(&1, 1),
"account liquidity restrictions prevent withdrawal"
);
assert_ok!(<Balances as MakePayment<_>>::make_payment(&1, 1));

Balances::set_lock(ID_1, &1, 10, u64::max_value(), WithdrawReason::TransactionPayment.into());
assert_ok!(<Balances as Currency<_>>::transfer(&1, &2, 1));
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&1, 1));
assert_noop!(
<Balances as MakePayment<_>>::make_payment(&1, 1),
"account liquidity restrictions prevent withdrawal"
);
});
Balances::set_lock(ID_1, &1, 10, u64::max_value(), WithdrawReason::TransactionPayment.into());
assert_ok!(<Balances as Currency<_>>::transfer(&1, &2, 1));
assert_ok!(<Balances as ReservableCurrency<_>>::reserve(&1, 1));
assert_noop!(
<Balances as MakePayment<_>>::make_payment(&1, 1),
"account liquidity restrictions prevent withdrawal"
);
}
);
}

#[test]
Expand Down Expand Up @@ -204,8 +210,9 @@ fn default_indexing_on_new_accounts_should_not_work2() {
.monied(true)
.build(),
|| {

assert_eq!(Balances::is_dead_account(&5), true); // account 5 should not exist
// account 1 has 256 * 10 = 2560, account 5 is not exist, ext_deposit is 10, value is 9, not satisfies for ext_deposit
// ext_deposit is 10, value is 9, not satisfies for ext_deposit
assert_noop!(
Balances::transfer(Some(1).into(), 5, 9),
"value too low to create account"
Expand Down Expand Up @@ -235,16 +242,19 @@ fn reserved_balance_should_prevent_reclaim_count() {
assert_eq!(Balances::is_dead_account(&2), false);
assert_eq!(System::account_nonce(&2), 1);

assert_ok!(Balances::transfer(Some(4).into(), 5, 256 * 1 + 0x69)); // account 4 tries to take index 1 for account 5.
// account 4 tries to take index 1 for account 5.
assert_ok!(Balances::transfer(Some(4).into(), 5, 256 * 1 + 0x69));
assert_eq!(Balances::total_balance(&5), 256 * 1 + 0x69);
assert_eq!(Balances::is_dead_account(&5), false);

assert!(Balances::slash(&2, 256 * 18 + 2).1.is_zero()); // account 2 gets slashed
assert_eq!(Balances::total_balance(&2), 0); // "reserve" account reduced to 255 (below ED) so account deleted
// "reserve" account reduced to 255 (below ED) so account deleted
assert_eq!(Balances::total_balance(&2), 0);
assert_eq!(System::account_nonce(&2), 0); // nonce zero
assert_eq!(Balances::is_dead_account(&2), true);

assert_ok!(Balances::transfer(Some(4).into(), 6, 256 * 1 + 0x69)); // account 4 tries to take index 1 again for account 6.
// account 4 tries to take index 1 again for account 6.
assert_ok!(Balances::transfer(Some(4).into(), 6, 256 * 1 + 0x69));
assert_eq!(Balances::total_balance(&6), 256 * 1 + 0x69);
assert_eq!(Balances::is_dead_account(&6), false);
},
Expand All @@ -258,7 +268,7 @@ fn reward_should_work() {
assert_eq!(Balances::total_balance(&1), 10);
assert_ok!(Balances::deposit_into_existing(&1, 10).map(drop));
assert_eq!(Balances::total_balance(&1), 20);
assert_eq!(<TotalIssuance<Runtime>>::get(), 110);
assert_eq!(<TotalIssuance<Runtime>>::get(), 120);
});
}

Expand Down Expand Up @@ -294,7 +304,8 @@ fn dust_account_removal_should_work2() {
System::inc_account_nonce(&2);
assert_eq!(System::account_nonce(&2), 1);
assert_eq!(Balances::total_balance(&2), 2000);
assert_ok!(Balances::transfer(Some(2).into(), 5, 1851)); // index 1 (account 2) becomes zombie for 256*10 + 50(fee) < 256 * 10 (ext_deposit)
// index 1 (account 2) becomes zombie for 256*10 + 50(fee) < 256 * 10 (ext_deposit)
assert_ok!(Balances::transfer(Some(2).into(), 5, 1851));
assert_eq!(Balances::total_balance(&2), 0);
assert_eq!(Balances::total_balance(&5), 1851);
assert_eq!(System::account_nonce(&2), 0);
Expand Down Expand Up @@ -571,32 +582,52 @@ fn check_vesting_status() {
assert_eq!(System::block_number(), 1);
let user1_free_balance = Balances::free_balance(&1);
let user2_free_balance = Balances::free_balance(&2);
let user12_free_balance = Balances::free_balance(&12);
assert_eq!(user1_free_balance, 256 * 10); // Account 1 has free balance
assert_eq!(user2_free_balance, 256 * 20); // Account 2 has free balance
assert_eq!(user12_free_balance, 256 * 10); // Account 12 has free balance
let user1_vesting_schedule = VestingSchedule {
offset: 256 * 10,
per_block: 256,
locked: 256 * 5,
per_block: 128, // Vesting over 10 blocks
starting_block: 0,
};
let user2_vesting_schedule = VestingSchedule {
offset: 256 * 30,
per_block: 256,
locked: 256 * 20,
per_block: 256, // Vesting over 20 blocks
starting_block: 10,
};
let user12_vesting_schedule = VestingSchedule {
locked: 256 * 5,
per_block: 64, // Vesting over 20 blocks
starting_block: 10,
};
assert_eq!(Balances::vesting(&1), Some(user1_vesting_schedule)); // Account 1 has a vesting schedule
assert_eq!(Balances::vesting(&2), Some(user2_vesting_schedule)); // Account 2 has a vesting schedule
assert_eq!(Balances::vesting(&12), Some(user12_vesting_schedule)); // Account 12 has a vesting schedule

assert_eq!(Balances::vesting_balance(&1), user1_free_balance - 256); // Account 1 has only 256 units vested at block 1
// Account 1 has only 128 units vested from their illiquid 256 * 5 units at block 1
assert_eq!(Balances::vesting_balance(&1), 128 * 9);
// Account 2 has their full balance locked
assert_eq!(Balances::vesting_balance(&2), user2_free_balance);
// Account 12 has only their illiquid funds locked
assert_eq!(Balances::vesting_balance(&12), user12_free_balance - 256 * 5);

System::set_block_number(10);
assert_eq!(System::block_number(), 10);

assert_eq!(Balances::vesting_balance(&1), 0); // Account 1 has fully vested by block 10
assert_eq!(Balances::vesting_balance(&2), user2_free_balance); // Account 2 has started vesting by block 10
// Account 1 has fully vested by block 10
assert_eq!(Balances::vesting_balance(&1), 0);
// Account 2 has started vesting by block 10
assert_eq!(Balances::vesting_balance(&2), user2_free_balance);
// Account 12 has started vesting by block 10
assert_eq!(Balances::vesting_balance(&12), user12_free_balance - 256 * 5);

System::set_block_number(30);
assert_eq!(System::block_number(), 30);

assert_eq!(Balances::vesting_balance(&1), 0); // Account 1 is still fully vested, and not negative
assert_eq!(Balances::vesting_balance(&2), 0); // Account 2 has fully vested by block 30
assert_eq!(Balances::vesting_balance(&12), 0); // Account 2 has fully vested by block 30

}
);
Expand All @@ -614,9 +645,10 @@ fn unvested_balance_should_not_transfer() {
assert_eq!(System::block_number(), 1);
let user1_free_balance = Balances::free_balance(&1);
assert_eq!(user1_free_balance, 100); // Account 1 has free balance
assert_eq!(Balances::vesting_balance(&1), 90); // Account 1 has only 10 units vested at block 1
// Account 1 has only 5 units vested at block 1 (plus 50 unvested)
assert_eq!(Balances::vesting_balance(&1), 45);
assert_noop!(
Balances::transfer(Some(1).into(), 2, 11),
Balances::transfer(Some(1).into(), 2, 56),
"vesting balance too high to send value"
); // Account 1 cannot send more than vested amount
}
Expand All @@ -635,8 +667,9 @@ fn vested_balance_should_transfer() {
assert_eq!(System::block_number(), 1);
let user1_free_balance = Balances::free_balance(&1);
assert_eq!(user1_free_balance, 100); // Account 1 has free balance
assert_eq!(Balances::vesting_balance(&1), 90); // Account 1 has only 10 units vested at block 1
assert_ok!(Balances::transfer(Some(1).into(), 2, 10));
// Account 1 has only 5 units vested at block 1 (plus 50 unvested)
assert_eq!(Balances::vesting_balance(&1), 45);
assert_ok!(Balances::transfer(Some(1).into(), 2, 55));
}
);
}
Expand All @@ -652,11 +685,51 @@ fn extra_balance_should_transfer() {
|| {
assert_eq!(System::block_number(), 1);
assert_ok!(Balances::transfer(Some(3).into(), 1, 100));
assert_ok!(Balances::transfer(Some(3).into(), 2, 100));

let user1_free_balance = Balances::free_balance(&1);
assert_eq!(user1_free_balance, 200); // Account 1 has 100 more free balance than normal

assert_eq!(Balances::vesting_balance(&1), 90); // Account 1 has 90 units vested at block 1
assert_ok!(Balances::transfer(Some(1).into(), 2, 105)); // Account 1 can send extra units gained
let user2_free_balance = Balances::free_balance(&2);
assert_eq!(user2_free_balance, 300); // Account 2 has 100 more free balance than normal

// Account 1 has only 5 units vested at block 1 (plus 150 unvested)
assert_eq!(Balances::vesting_balance(&1), 45);
assert_ok!(Balances::transfer(Some(1).into(), 3, 155)); // Account 1 can send extra units gained

// Account 2 has no units vested at block 1, but gained 100
assert_eq!(Balances::vesting_balance(&2), 200);
assert_ok!(Balances::transfer(Some(2).into(), 3, 100)); // Account 2 can send extra units gained
}
);
}

#[test]
fn liquid_funds_should_transfer_with_delayed_vesting() {
with_externalities(
&mut ExtBuilder::default()
.existential_deposit(256)
.monied(true)
.vesting(true)
.build(),
|| {
assert_eq!(System::block_number(), 1);
let user12_free_balance = Balances::free_balance(&12);

assert_eq!(user12_free_balance, 2560); // Account 12 has free balance
// Account 12 has liquid funds
assert_eq!(Balances::vesting_balance(&12), user12_free_balance - 256 * 5);

// Account 12 has delayed vesting
let user12_vesting_schedule = VestingSchedule {
locked: 256 * 5,
per_block: 64, // Vesting over 20 blocks
starting_block: 10,
};
assert_eq!(Balances::vesting(&12), Some(user12_vesting_schedule));

// Account 12 can still send liquid funds
assert_ok!(Balances::transfer(Some(12).into(), 3, 256 * 5));
}
);
}

0 comments on commit ca9f9e5

Please sign in to comment.