Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(lockup): add test of unlocking schedule with termination #191

Open
wants to merge 4 commits into
base: lockup-v2.0.0-branch
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions lockup/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,317 @@ mod tests {
);
}

#[test]
fn test_unlocking_schedule_with_termination() {
// https://wiki.near.org/getting-started/near-token/lockups#termination-of-vesting
// This test checks that termination of vesting
// fixes the amount of tokens that finally should become liquid (*),
// but does not change the schedule of unlocking the tokens.
// The contract should act as there was no termination at all,
// until we do not reach the amount (*).
// After we reach this amount, the unlocking process should stop.

// Taking bigger lockup amount because we compare the balances further
// and there is a comparison delta.
// We want to be sure that this delta does not grow on bigger numbers
let lockup_amount = to_yocto(LOCKUP_NEAR * 1000);
let mut context = basic_context();
context.account_balance = lockup_amount;
testing_env!(context.clone());

let vesting_cliff_offset = 100;
let vesting_schedule = new_vesting_schedule(vesting_cliff_offset);
let mut contract = new_contract(
true,
Some(vesting_schedule.clone()),
Some(to_nanos(YEAR * 4).into()),
true,
);

// Vesting starts at day 235
let ts_vesting_started = to_ts(GENESIS_TIME_IN_DAYS - YEAR + vesting_cliff_offset);
assert_eq!(vesting_schedule.start_timestamp.0, ts_vesting_started);

// We don't use lockup_timestamp,
// it means that lockup starts from the transfers enabled moment
assert_eq!(contract.lockup_information.lockup_timestamp, None);

// Transfers are enabled at day 500, lockup also starts here
if let TransfersInformation::TransfersEnabled {
transfers_timestamp,
} = &contract.lockup_information.transfers_information
{
assert_eq!(transfers_timestamp.0, to_ts(GENESIS_TIME_IN_DAYS));
} else {
assert!(false, "Transfers should be enabled");
}

// Vesting cliff ends at day 600
assert_eq!(
vesting_schedule.cliff_timestamp.0,
to_ts(GENESIS_TIME_IN_DAYS + vesting_cliff_offset)
);

// Lockup cliff ends at day 865
assert_eq!(contract.lockup_information.lockup_duration, to_nanos(YEAR));

// Everything is locked and unvested in the beginning
assert_eq!(
contract.get_vesting_information(),
VestingInformation::VestingHash(
VestingScheduleWithSalt {
vesting_schedule: vesting_schedule.clone(),
salt: SALT.to_vec().into(),
}
.hash()
.into()
)
);
assert_eq!(contract.get_owners_balance().0, 0);
assert_eq!(contract.get_liquid_owners_balance().0, 0);
assert_eq!(contract.get_locked_amount().0, lockup_amount);
assert_eq!(
contract.get_unvested_amount(vesting_schedule.clone()).0,
lockup_amount
);
assert_eq!(
contract
.get_locked_vested_amount(vesting_schedule.clone())
.0,
0
);

// *** day 599: day before vesting cliff is passed ***
let ts_1_day_before_vesting_cliff = to_ts(GENESIS_TIME_IN_DAYS + vesting_cliff_offset - 1);
assert_eq!(
ts_1_day_before_vesting_cliff,
vesting_schedule.cliff_timestamp.0 - to_nanos(1)
);
context.block_timestamp = ts_1_day_before_vesting_cliff;
testing_env!(context.clone());

// Everything is still locked and unvested
assert_eq!(contract.get_owners_balance().0, 0);
assert_eq!(contract.get_liquid_owners_balance().0, 0);
assert_eq!(contract.get_locked_amount().0, lockup_amount);
assert_eq!(
contract.get_unvested_amount(vesting_schedule.clone()).0,
lockup_amount
);
assert_eq!(
contract
.get_locked_vested_amount(vesting_schedule.clone())
.0,
0
);

// *** day 600: day of vesting cliff ***
let ts_day_of_vesting_cliff = to_ts(GENESIS_TIME_IN_DAYS + vesting_cliff_offset);
assert_eq!(ts_day_of_vesting_cliff, vesting_schedule.cliff_timestamp.0);
context.block_timestamp = ts_day_of_vesting_cliff;
testing_env!(context.clone());

// 25% is vested
let vesting_nanos_passed = (ts_day_of_vesting_cliff - ts_vesting_started) as u128;
let vesting_nanos_total =
(vesting_schedule.end_timestamp.0 - vesting_schedule.start_timestamp.0) as u128;

let expected_vested_amount_at_cliff_day =
lockup_amount / vesting_nanos_total * vesting_nanos_passed;
let vested_amount_at_cliff_day = contract
.get_locked_vested_amount(vesting_schedule.clone())
.0;
assert_almost_eq_with_max_delta(
expected_vested_amount_at_cliff_day,
vested_amount_at_cliff_day,
to_yocto(1),
);
assert_eq!(to_yocto(250000), vested_amount_at_cliff_day);

let expected_unvested_amount_at_cliff_day =
lockup_amount - expected_vested_amount_at_cliff_day;
let unvested_amount_at_cliff_day = contract.get_unvested_amount(vesting_schedule.clone()).0;
assert_almost_eq_with_max_delta(
expected_unvested_amount_at_cliff_day,
unvested_amount_at_cliff_day,
to_yocto(1),
);
assert_eq!(to_yocto(750000), unvested_amount_at_cliff_day);

// But tokens are still locked due to lockup
assert_eq!(contract.get_owners_balance().0, 0);
assert_eq!(contract.get_liquid_owners_balance().0, 0);
assert_eq!(contract.get_locked_amount().0, lockup_amount);

// *** day 800: day of termination ***
let ts_termination_day = to_ts(GENESIS_TIME_IN_DAYS + vesting_cliff_offset + 200);
context.block_timestamp = ts_termination_day;
testing_env!(context.clone());

assert_eq!(
contract.get_vesting_information(),
VestingInformation::VestingHash(
VestingScheduleWithSalt {
vesting_schedule: vesting_schedule.clone(),
salt: SALT.to_vec().into(),
}
.hash()
.into()
)
);

// Some tokens are vested
let vesting_nanos_passed = (ts_termination_day - ts_vesting_started) as u128;
let expected_vested_amount_at_termination_day =
lockup_amount / vesting_nanos_total * vesting_nanos_passed;
let locked_vested_amount_at_termination_day = contract
.get_locked_vested_amount(vesting_schedule.clone())
.0;

assert_almost_eq_with_max_delta(
expected_vested_amount_at_termination_day,
locked_vested_amount_at_termination_day,
to_yocto(1),
);
assert_almost_eq_with_max_delta(
to_yocto(386986),
locked_vested_amount_at_termination_day,
to_yocto(1),
);

let expected_unvested_amount_at_termination_day =
lockup_amount - expected_vested_amount_at_termination_day;
let unvested_amount_at_termination_day =
contract.get_unvested_amount(vesting_schedule.clone()).0;
assert_almost_eq_with_max_delta(
expected_unvested_amount_at_termination_day,
unvested_amount_at_termination_day,
to_yocto(1),
);
assert_almost_eq_with_max_delta(
to_yocto(613014),
unvested_amount_at_termination_day,
to_yocto(1),
);

// But tokens are still locked due to lockup
assert_eq!(contract.get_owners_balance().0, 0);
assert_eq!(contract.get_liquid_owners_balance().0, 0);
assert_eq!(contract.get_locked_amount().0, lockup_amount);

// Terminate the vesting
context.predecessor_account_id = account_foundation();
context.signer_account_pk = public_key(3).into();
context.is_view = false;
testing_env!(context.clone());
contract.terminate_vesting(Some(VestingScheduleWithSalt {
vesting_schedule: vesting_schedule.clone(),
salt: SALT.to_vec().into(),
}));

context.is_view = true;
testing_env!(context.clone());
assert_eq!(
contract.get_vesting_information(),
VestingInformation::Terminating(TerminationInformation {
unvested_amount: unvested_amount_at_termination_day.into(),
status: TerminationStatus::ReadyToWithdraw,
})
);

// *** day 801: 1 day after termination ***
let ts_1_day_after_termination = to_ts(GENESIS_TIME_IN_DAYS + vesting_cliff_offset + 201);
context.block_timestamp = ts_1_day_after_termination;
testing_env!(context.clone());

// All the tokens are still locked
assert_eq!(contract.get_owners_balance().0, 0);
assert_eq!(contract.get_liquid_owners_balance().0, 0);
assert_eq!(contract.get_locked_amount().0, lockup_amount);

// Nothing new is vested since termination
let unvested_amount_1_day_after_termination =
contract.get_unvested_amount(vesting_schedule.clone()).0;
assert_eq!(
unvested_amount_1_day_after_termination,
unvested_amount_at_termination_day
);

// *** day 864: 1 day before lockup cliff is passed ***
let ts_day_before_lockup_cliff = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 1);
context.block_timestamp = ts_day_before_lockup_cliff;
testing_env!(context.clone());

// All the tokens are still locked
assert_eq!(contract.get_owners_balance().0, 0);
assert_eq!(contract.get_liquid_owners_balance().0, 0);
assert_eq!(contract.get_locked_amount().0, lockup_amount);

// Nothing new is vested since termination
let unvested_amount_day_before_lockup_cliff =
contract.get_unvested_amount(vesting_schedule.clone()).0;
assert_eq!(
unvested_amount_day_before_lockup_cliff,
unvested_amount_at_termination_day
);

// *** day 865: day of lockup cliff ***
let ts_day_of_lockup_cliff = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
context.block_timestamp = ts_day_of_lockup_cliff;
testing_env!(context.clone());

let unlocked_amount_day_of_lockup_cliff = contract.get_liquid_owners_balance().0;
let expected_unlocked_amount_day_of_lockup_cliff = lockup_amount
/ (contract.lockup_information.release_duration.unwrap() as u128)
* (to_nanos(YEAR) as u128);
assert_almost_eq_with_max_delta(
expected_unlocked_amount_day_of_lockup_cliff,
unlocked_amount_day_of_lockup_cliff,
to_yocto(1),
);
assert_eq!(to_yocto(250000), unlocked_amount_day_of_lockup_cliff);

// *** day 1064: 1 day before unlock stopped ***
let ts_1_day_before_unlock_stop = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 199);
context.block_timestamp = ts_1_day_before_unlock_stop;
testing_env!(context.clone());

let locked_amount_1_day_before_unlock_stop = contract.get_locked_amount().0;

// *** day 1065, unlock stopped ***
let ts_day_unlock_stopped = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 200);
context.block_timestamp = ts_day_unlock_stopped;
testing_env!(context.clone());

let locked_amount_day_unlock_stopped = contract.get_locked_amount().0;
assert!(
locked_amount_day_unlock_stopped < locked_amount_1_day_before_unlock_stop,
"Locked amount should decrease"
);
assert_eq!(
locked_amount_day_unlock_stopped,
unvested_amount_at_termination_day
);

// We unlock 684 tokens in one day,
// it means that our delta (1 token) is small enough to detect any issue
assert_almost_eq_with_max_delta(
to_yocto(684),
locked_amount_1_day_before_unlock_stop - locked_amount_day_unlock_stopped,
to_yocto(1),
);

// *** day 1066, 1 day after unlock stopped ***
let ts_1_day_after_unlock_stopped = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 201);
context.block_timestamp = ts_1_day_after_unlock_stopped;
testing_env!(context.clone());

assert_eq!(
contract.get_locked_amount().0,
unvested_amount_at_termination_day
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add assert on get_unlocked() == initial lockup amount - terminated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I will not add this check because both possible methods are broken
#192 (comment)
I guess they will work for this specific case, but anyway, I don't want to use them, that's a matter of coincidence that they give us the expected result here

P.S. It's still too early to review this; I will ping you separately when I finish the work here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per our last discussion, let's add this assert anyway, and if it crashes (supposedly in the next test), let's comment it out and leave an explanation why we believe it is not critical

}

#[test]
fn test_termination_with_staking() {
let lockup_amount = to_yocto(1000);
Expand Down