diff --git a/Move.toml b/Move.toml index cba698f..36fdfc4 100644 --- a/Move.toml +++ b/Move.toml @@ -8,7 +8,7 @@ harvest = "0xb247ddeee87e848315caf9a33b8e4c71ac53db888cb88143d62d2370cca0ead2" stake_emergency_admin = "0x63e39817ec41fad2e8d0713cc906a5f792e4cd2cf704f8b5fab6b2961281fa11" # Test addresses. -treasury = "0x13" +treasury = "0x41" alice = "0x10" bob = "0x11" collection_owner = "0x12" diff --git a/liquidswap_staking_tests/Move.toml b/liquidswap_staking_tests/Move.toml index 56a78e2..94a2e4d 100644 --- a/liquidswap_staking_tests/Move.toml +++ b/liquidswap_staking_tests/Move.toml @@ -10,4 +10,4 @@ local = ".." [dependencies.Liquidswap] git = 'https://github.com/pontem-network/liquidswap.git' -rev = 'v0.4.4' \ No newline at end of file +rev = 'v0.4.5' \ No newline at end of file diff --git a/liquidswap_staking_tests/tests/lp_staking_tests.move b/liquidswap_staking_tests/tests/lp_staking_tests.move index e9e1e36..74f858b 100644 --- a/liquidswap_staking_tests/tests/lp_staking_tests.move +++ b/liquidswap_staking_tests/tests/lp_staking_tests.move @@ -78,7 +78,7 @@ module lp_staking_admin::lp_staking_tests { let aptos_coins = coin::withdraw(&harvest_acc, 50000000000); let duration = 5000000; stake::register_pool, AptosCoin>(&harvest_acc, - aptos_coins, duration, option::none()); + aptos_coins, duration, WEEK_IN_SECONDS, option::none(), vector[]); // stake 999.999 LP from alice stake::stake, AptosCoin>(&alice_acc, @harvest, lp_coins); @@ -106,7 +106,7 @@ module lp_staking_admin::lp_staking_tests { coin::deposit>(@alice, coins); // 0.00000001 APT lost during calculations - let (reward_per_sec, _, _, _, _) = stake::get_pool_info, AptosCoin>(@harvest); + let (reward_per_sec, _, _, _, _, _) = stake::get_pool_info, AptosCoin>(@harvest); let total_rewards = WEEK_IN_SECONDS * reward_per_sec; let losed_rewards = total_rewards - coin::balance(@alice); diff --git a/sources/scripts.move b/sources/scripts.move index c77adc9..6e7942c 100644 --- a/sources/scripts.move +++ b/sources/scripts.move @@ -14,29 +14,41 @@ module harvest::scripts { /// * `pool_owner` - account which will be used as a pool storage. /// * `reward_amount` - reward amount in R coins. /// * `duration` - pool life duration, can be increased by depositing more rewards. - public entry fun register_pool(pool_owner: &signer, reward_amount: u64, duration: u64) { + /// * `lockup_period` - blocking withdrawal of stake in seconds. + /// * `whitelist` - list of accounts allowed to stake. All are allowed if empty. + public entry fun register_pool( + pool_owner: &signer, + reward_amount: u64, + duration: u64, + lockup_period: u64, + whitelist: vector
+ ) { let rewards = coin::withdraw(pool_owner, reward_amount); - stake::register_pool(pool_owner, rewards, duration, option::none()); + stake::register_pool(pool_owner, rewards, duration, lockup_period, option::none(), whitelist); } /// Register new staking pool with staking coin `S` and reward coin `R` with nft boost. /// * `pool_owner` - account which will be used as a pool storage. /// * `reward_amount` - reward amount in R coins. /// * `duration` - pool life duration, can be increased by depositing more rewards. + /// * `lockup_period` - blocking withdrawal of stake in seconds. /// * `collection_owner` - address of nft collection creator. /// * `collection_name` - nft collection name. /// * `boost_percent` - percentage of increasing user stake "power" after nft stake. + /// * `whitelist` - list of accounts allowed to stake. All are allowed if empty. public entry fun register_pool_with_collection( pool_owner: &signer, reward_amount: u64, duration: u64, + lockup_period: u64, collection_owner: address, collection_name: String, - boost_percent: u128 + boost_percent: u128, + whitelist: vector
) { let rewards = coin::withdraw(pool_owner, reward_amount); let boost_config = stake::create_boost_config(collection_owner, collection_name, boost_percent); - stake::register_pool(pool_owner, rewards, duration, option::some(boost_config)); + stake::register_pool(pool_owner, rewards, duration, lockup_period, option::some(boost_config), whitelist); } /// Stake an `amount` of `Coin` to the pool of stake coin `S` and reward coin `R` on the address `pool_addr`. @@ -115,9 +127,10 @@ module harvest::scripts { /// * `depositor` - account with the `R` reward coins in the balance. /// * `pool_addr` - address of the pool. /// * `reward_amount` - amount of the reward coin `R` to deposit. - public entry fun deposit_reward_coins(depositor: &signer, pool_addr: address, reward_amount: u64) { + /// * `duration` - pool life duration. + public entry fun deposit_reward_coins(depositor: &signer, pool_addr: address, reward_amount: u64, duration: u64) { let reward_coins = coin::withdraw(depositor, reward_amount); - stake::deposit_reward_coins(depositor, pool_addr, reward_coins); + stake::deposit_reward_coins(depositor, pool_addr, reward_coins, duration); } /// Boosts user stake with nft. @@ -149,6 +162,21 @@ module harvest::scripts { token::deposit_token(user, nft); } + /// Add user into whitelist. + /// * `pool_owner` - pool creator account. + /// * `users` - list of users to whitelist. + public entry fun add_into_whitelist(pool_owner: &signer, users: vector
) { + stake::add_into_whitelist(pool_owner, users); + } + + /// Remove user from whitelist. + /// * `owner` - pool creator account. + /// * `user` - address of user to remove from whitelist. + /// Note: If no users left in whitelist it become deactivated. + public entry fun remove_from_whitelist(pool_owner: &signer, user: address) { + stake::remove_from_whitelist(pool_owner, user); + } + /// Enable "emergency state" for a pool on a `pool_addr` address. This state cannot be disabled /// and removes all operations except for `emergency_unstake()`, which unstakes all the coins for a user. /// * `admin` - current emergency admin account. diff --git a/sources/stake.move b/sources/stake.move index 3efe735..a50c165 100644 --- a/sources/stake.move +++ b/sources/stake.move @@ -4,11 +4,13 @@ module harvest::stake { use std::option::{Self, Option}; use std::signer; use std::string::String; + use std::vector; use aptos_std::event::{Self, EventHandle}; use aptos_std::math64; use aptos_std::math128; use aptos_std::table; + use aptos_std::table_with_length::{Self, TableWithLength}; use aptos_framework::account; use aptos_framework::coin::{Self, Coin}; use aptos_framework::timestamp; @@ -60,38 +62,38 @@ module harvest::stake { /// Duration can't be zero. const ERR_DURATION_CANNOT_BE_ZERO: u64 = 112; - /// When harvest finished for a pool. - const ERR_HARVEST_FINISHED: u64 = 113; - /// When withdrawing at wrong period. - const ERR_NOT_WITHDRAW_PERIOD: u64 = 114; + const ERR_NOT_WITHDRAW_PERIOD: u64 = 113; /// When not treasury withdrawing. - const ERR_NOT_TREASURY: u64 = 115; + const ERR_NOT_TREASURY: u64 = 114; /// When NFT collection does not exist. - const ERR_NO_COLLECTION: u64 = 116; + const ERR_NO_COLLECTION: u64 = 115; /// When boost percent is not in required range. - const ERR_INVALID_BOOST_PERCENT: u64 = 117; + const ERR_INVALID_BOOST_PERCENT: u64 = 116; /// When boosting stake in pool without specified nft collection. - const ERR_NON_BOOST_POOL: u64 = 118; + const ERR_NON_BOOST_POOL: u64 = 117; /// When boosting same stake again. - const ERR_ALREADY_BOOSTED: u64 = 119; + const ERR_ALREADY_BOOSTED: u64 = 118; /// When token collection not match pool. - const ERR_WRONG_TOKEN_COLLECTION: u64 = 120; + const ERR_WRONG_TOKEN_COLLECTION: u64 = 119; /// When removing boost from non boosted stake. - const ERR_NO_BOOST: u64 = 121; + const ERR_NO_BOOST: u64 = 120; /// When amount of NFT for boost is more than one. - const ERR_NFT_AMOUNT_MORE_THAN_ONE: u64 = 122; + const ERR_NFT_AMOUNT_MORE_THAN_ONE: u64 = 121; /// When reward coin has more than 10 decimals. - const ERR_INVALID_REWARD_DECIMALS: u64 = 123; + const ERR_INVALID_REWARD_DECIMALS: u64 = 122; + + /// When not whitelisted user try to stake. + const ERR_NOT_WHITELISTED: u64 = 123; // // Constants @@ -116,23 +118,34 @@ module harvest::stake { // Core data structures // - /// Stake pool, stores stake, reward coins and related info. - struct StakePool has key { + struct Epoch has store { + rewards_amount: u64, reward_per_sec: u64, // pool reward ((reward_per_sec * time) / total_staked) + accum_reward (previous period) accum_reward: u128, + + // start timestamp + start_time: u64, // last accum_reward update time - last_updated: u64, - // start timestamp. - start_timestamp: u64, - // when harvest will be finished. - end_timestamp: u64, + last_update_time: u64, + end_time: u64, + + // stats + distributed: u64, + ended_at: u64, + } + + /// Stake pool, stores stake, reward coins and related info. + struct StakePool has key { + epochs: vector>, stakes: table::Table, stake_coins: Coin, reward_coins: Coin, // multiplier to handle decimals scale: u128, + // Blocking withdrawal of stake in seconds + lockup_period: u64, total_boosted: u128, @@ -140,6 +153,9 @@ module harvest::stake { /// Pool creator can give ability for users to increase their stake profitability /// by staking nft's from specified collection. nft_boost_config: Option, + /// Table of users which are allowed to stake. + /// All users are allowed if length is 0. + whitelist: TableWithLength, /// This field set to `true` only in case of emergency: /// * only `emergency_unstake()` operation is available in the state of emergency @@ -164,7 +180,7 @@ module harvest::stake { struct UserStake has store { amount: u64, // contains the value of rewards that cannot be harvested by the user - unobtainable_reward: u128, + unobtainable_rewards: vector, earned_reward: u64, unlock_time: u64, // optionaly contains token that boosts stake @@ -200,19 +216,24 @@ module harvest::stake { /// * `owner` - pool creator account, under which the pool will be stored. /// * `reward_coins` - R coins which are used in distribution as reward. /// * `duration` - pool life duration, can be increased by depositing more rewards. + /// * `lockup_period` - blocking withdrawal of stake in seconds. /// * `nft_boost_config` - optional boost configuration. Allows users to stake nft and get more rewards. + /// * `whitelist` - list of accounts allowed to stake. All are allowed if empty. public fun register_pool( owner: &signer, reward_coins: Coin, duration: u64, - nft_boost_config: Option + lockup_period: u64, + nft_boost_config: Option, + whitelist: vector
) { assert!(!exists>(signer::address_of(owner)), ERR_POOL_ALREADY_EXISTS); assert!(coin::is_coin_initialized() && coin::is_coin_initialized(), ERR_IS_NOT_COIN); assert!(!stake_config::is_global_emergency(), ERR_EMERGENCY); assert!(duration > 0, ERR_DURATION_CANNOT_BE_ZERO); - let reward_per_sec = coin::value(&reward_coins) / duration; + let rewards_amount = coin::value(&reward_coins); + let reward_per_sec = rewards_amount / duration; assert!(reward_per_sec > 0, ERR_REWARD_CANNOT_BE_ZERO); let current_time = timestamp::now_seconds(); @@ -225,20 +246,35 @@ module harvest::stake { let stake_scale = math128::pow(10, (coin::decimals() as u128)); let scale = stake_scale * reward_scale; - let pool = StakePool { + let epoch = Epoch { + rewards_amount, + reward_per_sec, accum_reward: 0, - last_updated: current_time, - start_timestamp: current_time, - end_timestamp, + + start_time: current_time, + last_update_time: current_time, + end_time: end_timestamp, + + distributed: 0, + ended_at: 0, + }; + + let pool = StakePool { + epochs: vector[epoch], + stakes: table::new(), stake_coins: coin::zero(), reward_coins, scale, + lockup_period, total_boosted: 0, + nft_boost_config, + whitelist: table_with_length::new(), emergency_locked: false, + stake_events: account::new_event_handle(owner), unstake_events: account::new_event_handle(owner), deposit_events: account::new_event_handle(owner), @@ -246,6 +282,9 @@ module harvest::stake { boost_events: account::new_event_handle(owner), remove_boost_events: account::new_event_handle(owner), }; + + add_into_whitelist_inner(&mut pool, whitelist); + move_to(owner, pool); } @@ -253,34 +292,82 @@ module harvest::stake { /// * `depositor` - rewards depositor account. /// * `pool_addr` - address under which pool are stored. /// * `coins` - R coins which are used in distribution as reward. - public fun deposit_reward_coins(depositor: &signer, pool_addr: address, coins: Coin) acquires StakePool { + /// * `duration` - new pool life duration. + public fun deposit_reward_coins( + depositor: &signer, + pool_addr: address, + coins: Coin, + duration: u64, + ) acquires StakePool { assert!(exists>(pool_addr), ERR_NO_POOL); + assert!(duration > 0, ERR_DURATION_CANNOT_BE_ZERO); let pool = borrow_global_mut>(pool_addr); assert!(!is_emergency_inner(pool), ERR_EMERGENCY); - // it's forbidden to deposit more rewards (extend pool duration) after previous pool duration passed - // preventing unfair reward distribution - assert!(!is_finished_inner(pool), ERR_HARVEST_FINISHED); - let amount = coin::value(&coins); assert!(amount > 0, ERR_AMOUNT_CANNOT_BE_ZERO); - let additional_duration = amount / pool.reward_per_sec; - assert!(additional_duration > 0, ERR_DURATION_CANNOT_BE_ZERO); + // update epoch + update_accum_reward(pool); + + let current_time = timestamp::now_seconds(); + let current_epoch = vector::length(&pool.epochs) - 1; + let epochs = &mut pool.epochs; + let epoch = vector::borrow_mut(epochs, current_epoch); + + let undistrib_rewards_amount = 0; + + // close ghost epoch or redirect rewards from reward epoch + if (epoch.reward_per_sec == 0) { + epoch.ended_at = current_time; + epoch.end_time = current_time; + } else { + let epoch_time_left = epoch.end_time - epoch.last_update_time; + + // get undistributed rewards from prev epoch + if (epoch_time_left > 0) { + undistrib_rewards_amount = epoch.rewards_amount - epoch.distributed; + }; + + // finish current epoch + epoch.ended_at = current_time; + }; - pool.end_timestamp = pool.end_timestamp + additional_duration; + // merge undistributed & curr rewards into new reward_per_sec + let total_rewards = coin::value(&coins) + undistrib_rewards_amount; + let reward_per_sec = total_rewards / duration; + assert!(reward_per_sec > 0, ERR_REWARD_CANNOT_BE_ZERO); + // add new rewards to pool coin::merge(&mut pool.reward_coins, coins); - let depositor_addr = signer::address_of(depositor); + // create new epoch + let epoch_duration = current_time + duration; + let next_epoch = Epoch { + rewards_amount: total_rewards, + + reward_per_sec, + accum_reward: 0, + + start_time: current_time, + last_update_time: current_time, + end_time: epoch_duration, + + distributed: 0, + ended_at: 0, + }; + + vector::push_back(epochs, next_epoch); + let depositor_addr = signer::address_of(depositor); event::emit_event( &mut pool.deposit_events, DepositRewardEvent { user_address: depositor_addr, - amount, - new_end_timestamp: pool.end_timestamp, + new_amount: amount, + prev_amount: undistrib_rewards_amount, + epoch_duration, }, ); } @@ -301,34 +388,44 @@ module harvest::stake { let pool = borrow_global_mut>(pool_addr); assert!(!is_emergency_inner(pool), ERR_EMERGENCY); - assert!(!is_finished_inner(pool), ERR_HARVEST_FINISHED); + + // check user is able to stake + let user_address = signer::address_of(user); + assert!(is_whitelisted_inner(pool, user_address), ERR_NOT_WHITELISTED); // update pool accum_reward and timestamp update_accum_reward(pool); let current_time = timestamp::now_seconds(); - let user_address = signer::address_of(user); - let accum_reward = pool.accum_reward; - if (!table::contains(&pool.stakes, user_address)) { let new_stake = UserStake { amount, - unobtainable_reward: 0, + unobtainable_rewards: vector[], earned_reward: 0, - unlock_time: current_time + WEEK_IN_SECONDS, + unlock_time: current_time + pool.lockup_period, nft: option::none(), boosted_amount: 0, }; // calculate unobtainable reward for new stake - new_stake.unobtainable_reward = (accum_reward * (amount as u128)) / pool.scale; + let epoch_count = get_pool_current_epoch_inner(pool) + 1; + let epochs = &mut pool.epochs; + let i = 0; + while (i < epoch_count) { + let accum_reward = vector::borrow(epochs, i).accum_reward; + let unobt_rew = (accum_reward * (amount as u128)) / pool.scale; + + vector::push_back(&mut new_stake.unobtainable_rewards, unobt_rew); + + i = i + 1; + }; + table::add(&mut pool.stakes, user_address, new_stake); } else { - let user_stake = table::borrow_mut(&mut pool.stakes, user_address); - // update earnings - update_user_earnings(accum_reward, pool.scale, user_stake); + update_earnings_epochs(pool, user_address); + let user_stake = table::borrow_mut(&mut pool.stakes, user_address); user_stake.amount = user_stake.amount + amount; if (option::is_some(&user_stake.nft)) { @@ -341,10 +438,14 @@ module harvest::stake { }; // recalculate unobtainable reward after stake amount changed - user_stake.unobtainable_reward = - (accum_reward * user_stake_amount_with_boosted(user_stake)) / pool.scale; - - user_stake.unlock_time = current_time + WEEK_IN_SECONDS; + update_unobtainable_reward( + pool.scale, + vector::length(&pool.epochs), + &pool.epochs, + user_stake + ); + + user_stake.unlock_time = current_time + pool.lockup_period; }; coin::merge(&mut pool.stake_coins, coins); @@ -381,14 +482,12 @@ module harvest::stake { assert!(amount <= user_stake.amount, ERR_NOT_ENOUGH_S_BALANCE); // check unlock timestamp - let current_time = timestamp::now_seconds(); - if (pool.end_timestamp >= current_time) { - assert!(current_time >= user_stake.unlock_time, ERR_TOO_EARLY_UNSTAKE); - }; + assert!(timestamp::now_seconds() >= user_stake.unlock_time, ERR_TOO_EARLY_UNSTAKE); // update earnings - update_user_earnings(pool.accum_reward, pool.scale, user_stake); + update_earnings_epochs(pool, user_address); + let user_stake = table::borrow_mut(&mut pool.stakes, user_address); user_stake.amount = user_stake.amount - amount; if (option::is_some(&user_stake.nft)) { @@ -401,8 +500,12 @@ module harvest::stake { }; // recalculate unobtainable reward after stake amount changed - user_stake.unobtainable_reward = - (pool.accum_reward * user_stake_amount_with_boosted(user_stake)) / pool.scale; + update_unobtainable_reward( + pool.scale, + vector::length(&pool.epochs), + &pool.epochs, + user_stake + ); event::emit_event( &mut pool.unstake_events, @@ -428,11 +531,10 @@ module harvest::stake { // update pool accum_reward and timestamp update_accum_reward(pool); - let user_stake = table::borrow_mut(&mut pool.stakes, user_address); - // update earnings - update_user_earnings(pool.accum_reward, pool.scale, user_stake); + update_earnings_epochs(pool, user_address); + let user_stake = table::borrow_mut(&mut pool.stakes, user_address); let earned = user_stake.earned_reward; assert!(earned > 0, ERR_NOTHING_TO_HARVEST); @@ -480,12 +582,11 @@ module harvest::stake { // recalculate pool update_accum_reward(pool); - let user_stake = table::borrow_mut(&mut pool.stakes, user_address); - - // recalculate stake - update_user_earnings(pool.accum_reward, pool.scale, user_stake); + // update earnings + update_earnings_epochs(pool, user_address); // check if stake boosted before + let user_stake = table::borrow_mut(&mut pool.stakes, user_address); assert!(option::is_none(&user_stake.nft), ERR_ALREADY_BOOSTED); option::fill(&mut user_stake.nft, nft); @@ -494,9 +595,13 @@ module harvest::stake { user_stake.boosted_amount = ((user_stake.amount as u128) * boost_percent) / 100; pool.total_boosted = pool.total_boosted + user_stake.boosted_amount; - // recalculate unobtainable reward after stake boosted changed - user_stake.unobtainable_reward = - (pool.accum_reward * user_stake_amount_with_boosted(user_stake)) / pool.scale; + // recalculate unobtainable reward after stake amount changed + update_unobtainable_reward( + pool.scale, + vector::length(&pool.epochs), + &pool.epochs, + user_stake + ); event::emit_event( &mut pool.boost_events, @@ -523,16 +628,21 @@ module harvest::stake { let user_stake = table::borrow_mut(&mut pool.stakes, user_address); assert!(option::is_some(&user_stake.nft), ERR_NO_BOOST); - // recalculate stake - update_user_earnings(pool.accum_reward, pool.scale, user_stake); + // update earnings + update_earnings_epochs(pool, user_address); // update user stake and pool after nft claim + let user_stake = table::borrow_mut(&mut pool.stakes, user_address); pool.total_boosted = pool.total_boosted - user_stake.boosted_amount; user_stake.boosted_amount = 0; - // recalculate unobtainable reward after stake boosted changed - user_stake.unobtainable_reward = - (pool.accum_reward * user_stake_amount_with_boosted(user_stake)) / pool.scale; + // recalculate unobtainable reward after stake amount changed + update_unobtainable_reward( + pool.scale, + vector::length(&pool.epochs), + &pool.epochs, + user_stake + ); event::emit_event( &mut pool.remove_boost_events, @@ -542,6 +652,29 @@ module harvest::stake { option::extract(&mut user_stake.nft) } + /// Add user into whitelist. + /// * `owner` - pool creator account. + /// * `users` - list of users to whitelist. + public fun add_into_whitelist(owner: &signer, users: vector
) acquires StakePool { + let pool_creator = signer::address_of(owner); + assert!(exists>(pool_creator), ERR_NO_POOL); + + let pool = borrow_global_mut>(pool_creator); + add_into_whitelist_inner(pool, users); + } + + /// Remove user from whitelist. + /// * `owner` - pool creator account. + /// * `user` - address of user to remove from whitelist. + /// Note: If no users left in whitelist it become deactivated. + public fun remove_from_whitelist(owner: &signer, user: address) acquires StakePool { + let pool_creator = signer::address_of(owner); + assert!(exists>(pool_creator), ERR_NO_POOL); + + let pool = borrow_global_mut>(pool_creator); + table_with_length::remove(&mut pool.whitelist, user); + } + /// Enables local "emergency state" for the specific `` pool at `pool_addr`. Cannot be disabled. /// * `admin` - current emergency admin account. /// * `pool_addr` - address under which pool are stored. @@ -574,7 +707,7 @@ module harvest::stake { let user_stake = table::remove(&mut pool.stakes, user_addr); let UserStake { amount, - unobtainable_reward: _, + unobtainable_rewards: _, earned_reward: _, unlock_time: _, nft, @@ -597,7 +730,8 @@ module harvest::stake { if (!is_emergency_inner(pool)) { let now = timestamp::now_seconds(); - assert!(now >= (pool.end_timestamp + WITHDRAW_REWARD_PERIOD_IN_SECONDS), ERR_NOT_WITHDRAW_PERIOD); + let last_epoch_endtime = vector::borrow(&pool.epochs, get_pool_current_epoch_inner(pool)).end_time; + assert!(now >= (last_epoch_endtime + WITHDRAW_REWARD_PERIOD_IN_SECONDS), ERR_NOT_WITHDRAW_PERIOD); }; coin::extract(&mut pool.reward_coins, amount) @@ -607,6 +741,7 @@ module harvest::stake { // Getter functions // + #[view] /// Get timestamp of pool creation. /// * `pool_addr` - address under which pool are stored. /// Returns timestamp contains date when pool created. @@ -614,9 +749,10 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - pool.start_timestamp + vector::borrow(&pool.epochs, 0).start_time } + #[view] /// Checks if user can boost own stake in pool. /// * `pool_addr` - address under which pool are stored. /// Returns true if pool accepts boosts. @@ -627,6 +763,7 @@ module harvest::stake { option::is_some(&pool.nft_boost_config) } + #[view] /// Get NFT boost config parameters for pool. /// * `pool_addr` - the pool with with NFT boost collection enabled. /// Returns both `collection_owner`, `collection_name` and boost percent. @@ -640,16 +777,7 @@ module harvest::stake { (boost_config.collection_owner, boost_config.collection_name, boost_config.boost_percent) } - /// Checks if harvest on the pool finished. - /// * `pool_addr` - address under which pool are stored. - /// Returns true if harvest finished for the pool. - public fun is_finished(pool_addr: address): bool acquires StakePool { - assert!(exists>(pool_addr), ERR_NO_POOL); - - let pool = borrow_global>(pool_addr); - is_finished_inner(pool) - } - + #[view] /// Gets timestamp when harvest will be finished for the pool. /// * `pool_addr` - address under which pool are stored. /// Returns timestamp. @@ -657,9 +785,10 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - pool.end_timestamp + vector::borrow(&pool.epochs, get_pool_current_epoch_inner(pool)).end_time } + #[view] /// Checks if pool exists. /// * `pool_addr` - address under which pool are stored. /// Returns true if pool exists. @@ -667,6 +796,7 @@ module harvest::stake { exists>(pool_addr) } + #[view] /// Checks if stake exists. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -675,19 +805,21 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - table::contains(&pool.stakes, user_addr) } + #[view] /// Checks current total staked amount in pool. /// * `pool_addr` - address under which pool are stored. /// Returns total staked amount. public fun get_pool_total_stake(pool_addr: address): u64 acquires StakePool { assert!(exists>(pool_addr), ERR_NO_POOL); - coin::value(&borrow_global>(pool_addr).stake_coins) + let pool = borrow_global>(pool_addr); + coin::value(&pool.stake_coins) } + #[view] /// Checks current total boosted amount in pool. /// * `pool_addr` - address under which pool are stored. /// Returns total pool boosted amount. @@ -697,6 +829,18 @@ module harvest::stake { borrow_global>(pool_addr).total_boosted } + #[view] + /// Checks current epoch id in pool. + /// * `pool_addr` - address under which pool are stored. + /// Returns epoch id. + public fun get_pool_current_epoch(pool_addr: address): u64 acquires StakePool { + assert!(exists>(pool_addr), ERR_NO_POOL); + let pool = borrow_global>(pool_addr); + + get_pool_current_epoch_inner(pool) + } + + #[view] /// Checks current amount staked by user in specific pool. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -705,12 +849,12 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - assert!(table::contains(&pool.stakes, user_addr), ERR_NO_STAKE); table::borrow(&pool.stakes, user_addr).amount } + #[view] /// Checks if user user stake is boosted. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -719,12 +863,12 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - assert!(table::contains(&pool.stakes, user_addr), ERR_NO_STAKE); option::is_some(&table::borrow(&pool.stakes, user_addr).nft) } + #[view] /// Checks current user boosted amount in specific pool. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -733,12 +877,12 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - assert!(table::contains(&pool.stakes, user_addr), ERR_NO_STAKE); table::borrow(&pool.stakes, user_addr).boosted_amount } + #[view] /// Checks current pending user reward in specific pool. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -746,21 +890,48 @@ module harvest::stake { public fun get_pending_user_rewards(pool_addr: address, user_addr: address): u64 acquires StakePool { assert!(exists>(pool_addr), ERR_NO_POOL); - let pool = borrow_global>(pool_addr); + let pool = borrow_global_mut>(pool_addr); assert!(table::contains(&pool.stakes, user_addr), ERR_NO_STAKE); - let user_stake = table::borrow(&pool.stakes, user_addr); - let current_time = get_time_for_last_update(pool); - let new_accum_rewards = accum_rewards_since_last_updated(pool, current_time); + let user_stake = table::borrow_mut(&mut pool.stakes, user_addr); + let current_time = timestamp::now_seconds(); - let earned_since_last_update = user_earned_since_last_update( - pool.accum_reward + new_accum_rewards, - pool.scale, - user_stake, - ); - user_stake.earned_reward + (earned_since_last_update as u64) + let earnings = 0; + let scale = pool.scale; + let epoch_count = vector::length(&pool.epochs); + let epochs = &mut pool.epochs; + let i = 0; + while (i < epoch_count) { + let epoch = vector::borrow_mut(epochs, i); + + // get new accum reward for last epoch + let new_earnings = if (i + 1 == epoch_count) { + let epoch_end_time = epoch.end_time; + let reward_time = math64::min(epoch_end_time, current_time); + + let pool_total_staked_with_boosted = + (coin::value(&pool.stake_coins) as u128) + pool.total_boosted; + let new_accum_rewards = + accum_rewards_since_last_updated( + pool_total_staked_with_boosted, + epoch.last_update_time, + epoch.reward_per_sec, + reward_time, + pool.scale + ); + let accum_reward = epoch.accum_reward + new_accum_rewards; + user_earned_since_last_update(accum_reward, scale, user_stake, i) + } else { + user_earned_since_last_update(epoch.accum_reward, scale, user_stake, i) + }; + earnings = earnings + new_earnings; + i = i + 1; + }; + + user_stake.earned_reward + (earnings as u64) } + #[view] /// Checks stake unlock time in specific pool. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -769,12 +940,12 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - assert!(table::contains(&pool.stakes, user_addr), ERR_NO_STAKE); - math64::min(pool.end_timestamp, table::borrow(&pool.stakes, user_addr).unlock_time) + table::borrow(&pool.stakes, user_addr).unlock_time } + #[view] /// Checks if stake is unlocked. /// * `pool_addr` - address under which pool are stored. /// * `user_addr` - stake owner address. @@ -783,29 +954,44 @@ module harvest::stake { assert!(exists>(pool_addr), ERR_NO_POOL); let pool = borrow_global>(pool_addr); - assert!(table::contains(&pool.stakes, user_addr), ERR_NO_STAKE); let current_time = timestamp::now_seconds(); - let unlock_time = math64::min(pool.end_timestamp, table::borrow(&pool.stakes, user_addr).unlock_time); + let unlock_time = table::borrow(&pool.stakes, user_addr).unlock_time; current_time >= unlock_time } + #[view] + /// Checks if user are whitelisted. + /// * `pool_addr` - address under which pool are stored. + /// * `user_addr` - user address. + /// Returns true if user can stake. + public fun is_whitelisted(pool_addr: address, user_addr: address): bool acquires StakePool { + assert!(exists>(pool_addr), ERR_NO_POOL); + + let pool = borrow_global>(pool_addr); + is_whitelisted_inner(pool, user_addr) + } + + #[view] /// Checks whether "emergency state" is enabled. In that state, only `emergency_unstake()` function is enabled. /// * `pool_addr` - address under which pool are stored. /// Returns true if emergency happened (local or global). public fun is_emergency(pool_addr: address): bool acquires StakePool { assert!(exists>(pool_addr), ERR_NO_POOL); + let pool = borrow_global>(pool_addr); is_emergency_inner(pool) } + #[view] /// Checks whether a specific `` pool at the `pool_addr` has an "emergency state" enabled. /// * `pool_addr` - address of the pool to check emergency. /// Returns true if local emergency enabled for pool. public fun is_local_emergency(pool_addr: address): bool acquires StakePool { assert!(exists>(pool_addr), ERR_NO_POOL); + let pool = borrow_global>(pool_addr); pool.emergency_locked } @@ -814,6 +1000,27 @@ module harvest::stake { // Private functions. // + /// Checks if user is whitelisted. + /// * `pool` - pool to check whitelist. + /// Returns true if user in whitelist. + fun is_whitelisted_inner(pool: &StakePool, user_addr: address): bool { + if (table_with_length::length(&pool.whitelist) > 0) { + return table_with_length::contains(&pool.whitelist, user_addr) + }; + true + } + + /// Add list of users into whitelist. + /// * `pool` - pool to add users. + /// * `users` - list of users. + fun add_into_whitelist_inner(pool: &mut StakePool, users: vector
) { + let whitelist = &mut pool.whitelist; + + vector::for_each(users, | addr | { + table_with_length::add(whitelist, addr, true); + }); + } + /// Checks if local pool or global emergency enabled. /// * `pool` - pool to check emergency. /// Returns true of any kind or both of emergency enabled. @@ -821,52 +1028,127 @@ module harvest::stake { pool.emergency_locked || stake_config::is_global_emergency() } - /// Internal function to check if harvest finished on the pool. - /// * `pool` - the pool itself. - /// Returns true if harvest finished for the pool. - fun is_finished_inner(pool: &StakePool): bool { - let now = timestamp::now_seconds(); - now >= pool.end_timestamp + /// Checks pool current epoch. + /// * `pool` - pool to check emergency. + /// Returns current epoch id. + fun get_pool_current_epoch_inner(pool: &StakePool): u64 { + vector::length(&pool.epochs) - 1 } /// Calculates pool accumulated reward, updating pool. /// * `pool` - pool to update rewards. fun update_accum_reward(pool: &mut StakePool) { - let current_time = get_time_for_last_update(pool); - let new_accum_rewards = accum_rewards_since_last_updated(pool, current_time); + let current_epoch = vector::length(&pool.epochs) - 1; + let epoch = vector::borrow_mut(&mut pool.epochs, current_epoch); + let current_time = timestamp::now_seconds(); + + if (epoch.reward_per_sec == 0) { + // handle ghost epoch + epoch.last_update_time = current_time; + epoch.end_time = current_time; + } else { + // handle reward epoch + let epoch_end_time = epoch.end_time; + let reward_time = math64::min(epoch_end_time, current_time); + + let pool_total_staked_with_boosted = + (coin::value(&pool.stake_coins) as u128) + pool.total_boosted; + let new_accum_rewards = + accum_rewards_since_last_updated( + pool_total_staked_with_boosted, + epoch.last_update_time, + epoch.reward_per_sec, + reward_time, + pool.scale + ); + if (new_accum_rewards != 0) { + epoch.accum_reward = epoch.accum_reward + new_accum_rewards; + }; - pool.last_updated = current_time; + epoch.last_update_time = current_time; - if (new_accum_rewards != 0) { - pool.accum_reward = pool.accum_reward + new_accum_rewards; + // calculate distributed rewards amount + let undistrib_rewards_amount = 0; + if (epoch_end_time > current_time) { + let epoch_time_left = epoch_end_time - current_time; + undistrib_rewards_amount = epoch_time_left * epoch.reward_per_sec + }; + epoch.distributed = epoch.rewards_amount - undistrib_rewards_amount; + + // create ghost epoch to fill up empty period + if (epoch_end_time <= current_time) { + epoch.ended_at = current_time; + let ghost_epoch = Epoch { + rewards_amount: 0, + + reward_per_sec: 0, + accum_reward: 0, + + start_time: epoch_end_time, + last_update_time: current_time, + end_time: current_time, + + distributed: 0, + ended_at: 0, + }; + vector::push_back(&mut pool.epochs, ghost_epoch); + }; }; } /// Calculates accumulated reward without pool update. - /// * `pool` - pool to calculate rewards. - /// * `current_time` - execution timestamp. + /// * `total_boosted_stake` - total amount of staked coins with boosts. + /// * `last_update_time` - last update time of epoch `accum_reward` field. + /// * `reward_per_sec` - rewards to distribute per second of epoch duration. + /// * `reward_time` - time passed since last update or epoch end time. + /// * `scale` - multiplier to handle decimals. /// Returns new accumulated reward. - fun accum_rewards_since_last_updated(pool: &StakePool, current_time: u64): u128 { - let seconds_passed = current_time - pool.last_updated; + fun accum_rewards_since_last_updated( + total_boosted_stake: u128, + last_update_time: u64, + reward_per_sec: u64, + reward_time: u64, + scale: u128, + ): u128 { + let seconds_passed = reward_time - last_update_time; if (seconds_passed == 0) return 0; - let total_boosted_stake = pool_total_staked_with_boosted(pool); if (total_boosted_stake == 0) return 0; let total_rewards = - (pool.reward_per_sec as u128) * (seconds_passed as u128) * pool.scale; + (reward_per_sec as u128) * (seconds_passed as u128) * scale; total_rewards / total_boosted_stake } + /// Updates user earnings. + /// * `pool` - pool to get epochs and stakes. + /// * `user_address` - address of user to update earnings for. + fun update_earnings_epochs(pool: &mut StakePool, user_address: address) { + let epoch_count = get_pool_current_epoch_inner(pool) + 1; + let epochs = &mut pool.epochs; + let user_stake = table::borrow_mut(&mut pool.stakes, user_address); + + let i = 0; + while (i < epoch_count) { + let epoch = vector::borrow_mut(epochs, i); + + update_user_earnings(epoch.accum_reward, pool.scale, user_stake, i); + i = i + 1; + }; + } + /// Calculates user earnings, updating user stake. /// * `accum_reward` - reward accumulated by pool. /// * `scale` - multiplier to handle decimals. /// * `user_stake` - stake to update earnings. - fun update_user_earnings(accum_reward: u128, scale: u128, user_stake: &mut UserStake) { + fun update_user_earnings(accum_reward: u128, scale: u128, user_stake: &mut UserStake, epoch: u64) { let earned = - user_earned_since_last_update(accum_reward, scale, user_stake); + user_earned_since_last_update(accum_reward, scale, user_stake, epoch); user_stake.earned_reward = user_stake.earned_reward + (earned as u64); - user_stake.unobtainable_reward = user_stake.unobtainable_reward + earned; + + // update unobtainable_reward for specific epoch + let unobtainable_reward = vector::borrow_mut(&mut user_stake.unobtainable_rewards, epoch); + *unobtainable_reward = *unobtainable_reward + earned; } /// Calculates user earnings without stake update. @@ -877,24 +1159,42 @@ module harvest::stake { fun user_earned_since_last_update( accum_reward: u128, scale: u128, - user_stake: &UserStake + user_stake: &mut UserStake, + epoch: u64, ): u128 { + // create a slot for unobtainable reward if needed + let unobtainable_reward = if (vector::length(&user_stake.unobtainable_rewards) < epoch + 1) { + vector::push_back(&mut user_stake.unobtainable_rewards, 0); + 0 + } else { + *vector::borrow(&user_stake.unobtainable_rewards, epoch) + }; + ((accum_reward * user_stake_amount_with_boosted(user_stake)) / scale) - - user_stake.unobtainable_reward + - unobtainable_reward } - /// Get time for last pool update: current time if the pool is not finished or end timmestamp. - /// * `pool` - pool to get time. - /// Returns timestamp. - fun get_time_for_last_update(pool: &StakePool): u64 { - math64::min(pool.end_timestamp, timestamp::now_seconds()) - } + /// Calculates unobtainable reward for user. + /// * `scale` - multiplier to handle decimals. + /// * `epoch_count` - count of epochs in pool. + /// * `epochs` - vector of pool epochs. + /// * `user_stake` - the user stake. + fun update_unobtainable_reward( + scale: u128, + epoch_count: u64, + epochs: &vector>, + user_stake: &mut UserStake + ) { + let i = 0; + while (i < epoch_count) { + let accum_reward = vector::borrow(epochs, i).accum_reward; + let unobt_rew = (accum_reward * user_stake_amount_with_boosted(user_stake)) / scale; - /// Get total staked amount + boosted amount in the pool. - /// * `pool` - the pool itself. - /// Returns amount. - fun pool_total_staked_with_boosted(pool: &StakePool): u128 { - (coin::value(&pool.stake_coins) as u128) + pool.total_boosted + let el = vector::borrow_mut(&mut user_stake.unobtainable_rewards, i); + *el = unobt_rew; + + i = i + 1; + }; } /// Get total staked amount + boosted amount by the user. @@ -928,8 +1228,9 @@ module harvest::stake { struct DepositRewardEvent has drop, store { user_address: address, - amount: u64, - new_end_timestamp: u64, + new_amount: u64, + prev_amount: u64, + epoch_duration: u64, } struct HarvestEvent has drop, store { @@ -941,29 +1242,55 @@ module harvest::stake { /// Access unobtainable_reward field in user stake. public fun get_unobtainable_reward( pool_addr: address, - user_addr: address + user_addr: address, ): u128 acquires StakePool { let pool = borrow_global>(pool_addr); + let user_stake = table::borrow(&pool.stakes, user_addr); + + let total_unobt_rew = 0; + let unobt_len = vector::length(&user_stake.unobtainable_rewards); + let epoch_count = get_pool_current_epoch_inner(pool) + 1; + let i = 0; + while (i < epoch_count) { + let unobt_rew = 0; + if (i < unobt_len) { + unobt_rew = *vector::borrow(&user_stake.unobtainable_rewards, i); + }; + + total_unobt_rew = total_unobt_rew + unobt_rew; + i = i + 1; + }; + + total_unobt_rew + } + + #[test_only] + /// Access staking pool fields with no getters. + public fun get_pool_info(pool_addr: address): (u64, u128, u64, u64, u128, u64) acquires StakePool { + let pool = borrow_global>(pool_addr); + let epoch = vector::borrow(&pool.epochs, get_pool_current_epoch_inner(pool)); - table::borrow(&pool.stakes, user_addr).unobtainable_reward + (epoch.reward_per_sec, epoch.accum_reward, epoch.last_update_time, + coin::value(&pool.reward_coins), pool.scale, pool.lockup_period) } #[test_only] /// Access staking pool fields with no getters. - public fun get_pool_info(pool_addr: address): (u64, u128, u64, u64, u128) acquires StakePool { + public fun get_epoch_info(pool_addr: address, epoch: u64): + (u64, u64, u128, u64, u64, u64, u64, u64) acquires StakePool { let pool = borrow_global>(pool_addr); + let epoch = vector::borrow(&pool.epochs, epoch); - (pool.reward_per_sec, pool.accum_reward, pool.last_updated, - coin::value(&pool.reward_coins), pool.scale) + (epoch.rewards_amount, epoch.reward_per_sec, epoch.accum_reward, epoch.start_time, + epoch.last_update_time, epoch.end_time, epoch.distributed, epoch.ended_at) } #[test_only] /// Force pool & user stake recalculations. public fun recalculate_user_stake(pool_addr: address, user_addr: address) acquires StakePool { let pool = borrow_global_mut>(pool_addr); - update_accum_reward(pool); - let user_stake = table::borrow_mut(&mut pool.stakes, user_addr); - update_user_earnings(pool.accum_reward, pool.scale, user_stake); + update_accum_reward(pool); + update_earnings_epochs(pool, user_addr); } } diff --git a/tests/emergency_tests.move b/tests/emergency_tests.move index f9119ad..b5ac3d3 100644 --- a/tests/emergency_tests.move +++ b/tests/emergency_tests.move @@ -74,7 +74,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); } #[test] @@ -87,7 +89,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::enable_emergency(&emergency_admin, @harvest); @@ -106,7 +110,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -126,12 +132,14 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::enable_emergency(&emergency_admin, @harvest); let reward_coins = mint_default_coin(12345 * ONE_COIN); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 12345); } #[test] @@ -144,7 +152,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -172,12 +182,14 @@ module harvest::emergency_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -202,12 +214,14 @@ module harvest::emergency_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -233,7 +247,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake_config::enable_global_emergency(&emergency_admin); @@ -252,7 +268,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -272,12 +290,14 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period ,option::none(), vector[]); stake_config::enable_global_emergency(&emergency_admin); let reward_coins = mint_default_coin(12345 * ONE_COIN); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 12345); } #[test] @@ -290,7 +310,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -318,12 +340,14 @@ module harvest::emergency_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -348,12 +372,14 @@ module harvest::emergency_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -377,7 +403,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake_config::enable_global_emergency(&emergency_admin); @@ -394,7 +422,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::enable_emergency(&alice_acc, @harvest); } @@ -407,7 +437,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::enable_emergency(&emergency_admin, @harvest); stake::enable_emergency(&emergency_admin, @harvest); @@ -422,7 +454,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -453,12 +487,14 @@ module harvest::emergency_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -488,7 +524,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -509,8 +547,11 @@ module harvest::emergency_tests { let reward_coins_1 = mint_default_coin(12345 * ONE_COIN); let reward_coins_2 = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins_1, duration, option::none()); - stake::register_pool(&harvest, reward_coins_2, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins_1, + duration, lockup_period, option::none(), vector[]); + stake::register_pool(&harvest, reward_coins_2, + duration, lockup_period, option::none(), vector[]); stake::enable_emergency(&emergency_admin, @harvest); @@ -528,7 +569,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake_config::enable_global_emergency(&emergency_admin); stake_config::enable_global_emergency(&emergency_admin); @@ -543,7 +586,9 @@ module harvest::emergency_tests { // register staking pool let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -575,12 +620,14 @@ module harvest::emergency_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 1 * ONE_COIN); @@ -625,7 +672,9 @@ module harvest::emergency_tests { let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&alice, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&alice, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::enable_emergency(&alice, @alice); @@ -654,7 +703,9 @@ module harvest::emergency_tests { let reward_coins = mint_default_coin(12345 * ONE_COIN); let duration = 12345; - stake::register_pool(&alice, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&alice, reward_coins, + duration, lockup_period, option::none(), vector[]); stake_config::enable_global_emergency(&alice); diff --git a/tests/scripts_tests.move b/tests/scripts_tests.move index 6b3cca0..183ba6a 100644 --- a/tests/scripts_tests.move +++ b/tests/scripts_tests.move @@ -29,11 +29,12 @@ module harvest::scripts_tests { let duration = 100000000; coin::deposit(@harvest, reward_coins); assert!(coin::balance(@harvest) == 1000000000, 1); - scripts::register_pool(&harvest, 1000 * ONE_COIN, duration); + scripts::register_pool(&harvest, 1000 * ONE_COIN, + duration, WEEK_IN_SECONDS, vector[]); assert!(coin::balance(@harvest) == 0, 1); - let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale) = + let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale, lockup_period) = stake::get_pool_info(@harvest); let end_ts = stake::get_end_timestamp(@harvest); assert!(end_ts == START_TIME + duration, 1); @@ -42,9 +43,45 @@ module harvest::scripts_tests { assert!(last_updated == 682981200, 1); assert!(reward_coin_amount == 1000 * ONE_COIN, 1); assert!(scale == 1000000000000, 1); + assert!(lockup_period == WEEK_IN_SECONDS, 1); assert!(stake::pool_exists(@harvest), 1); } + #[test] + fun test_script_register_pool_with_whitelist() { + let (harvest, _) = initialize_test(); + + coin::register(&harvest); + + let reward_coins = mint_default_coin(1000 * ONE_COIN); + let duration = 100000000; + coin::deposit(@harvest, reward_coins); + assert!(coin::balance(@harvest) == 1000000000, 1); + scripts::register_pool(&harvest, 1000 * ONE_COIN, + duration, 0, vector[@alice, @bob, @0x41]); + + assert!(coin::balance(@harvest) == 0, 1); + + let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale, lockup_period) = + stake::get_pool_info(@harvest); + let end_ts = stake::get_end_timestamp(@harvest); + assert!(end_ts == START_TIME + duration, 1); + assert!(reward_per_sec == 10, 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == 682981200, 1); + assert!(reward_coin_amount == 1000 * ONE_COIN, 1); + assert!(scale == 1000000000000, 1); + assert!(lockup_period == 0, 1); + assert!(stake::pool_exists(@harvest), 1); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + assert!(stake::is_whitelisted(@harvest, @0x41), 1); + assert!(!stake::is_whitelisted(@harvest, @0x42), 1); + assert!(!stake::is_whitelisted(@harvest, @0x43), 1); + } + #[test] fun test_script_register_pool_with_nft_collection() { let (harvest, _) = initialize_test(); @@ -56,20 +93,23 @@ module harvest::scripts_tests { let reward_coins = mint_default_coin(1000 * ONE_COIN); let duration = 100000000; + let lockup_period = 100000000; coin::deposit(@harvest, reward_coins); assert!(coin::balance(@harvest) == 1000000000, 1); scripts::register_pool_with_collection( &harvest, 1000 * ONE_COIN, duration, + lockup_period, @collection_owner, collection_name, 10, + vector[], ); assert!(coin::balance(@harvest) == 0, 1); - let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale) = + let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale, pool_lockup_period) = stake::get_pool_info(@harvest); let end_ts = stake::get_end_timestamp(@harvest); assert!(end_ts == START_TIME + duration, 1); @@ -78,6 +118,7 @@ module harvest::scripts_tests { assert!(last_updated == 682981200, 1); assert!(reward_coin_amount == 1000 * ONE_COIN, 1); assert!(scale == 1000000000000, 1); + assert!(pool_lockup_period == lockup_period, 1); assert!(stake::pool_exists(@harvest), 1); let (collection_owner_addr, coll_name, boost_percent) = @@ -87,6 +128,59 @@ module harvest::scripts_tests { assert!(boost_percent == 10, 1); } + #[test] + fun test_script_register_pool_with_nft_collection_and_whitelist() { + let (harvest, _) = initialize_test(); + + coin::register(&harvest); + + let collection_name = string::utf8(b"Test Collection"); + create_collecton(@collection_owner, collection_name); + + let reward_coins = mint_default_coin(1000 * ONE_COIN); + let duration = 100000000; + let lockup_period = 100000000; + coin::deposit(@harvest, reward_coins); + assert!(coin::balance(@harvest) == 1000000000, 1); + scripts::register_pool_with_collection( + &harvest, + 1000 * ONE_COIN, + duration, + lockup_period, + @collection_owner, + collection_name, + 10, + vector[@alice, @bob, @0x41], + ); + + assert!(coin::balance(@harvest) == 0, 1); + + let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale, pool_lockup_period) = + stake::get_pool_info(@harvest); + let end_ts = stake::get_end_timestamp(@harvest); + assert!(end_ts == START_TIME + duration, 1); + assert!(reward_per_sec == 10, 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == 682981200, 1); + assert!(reward_coin_amount == 1000 * ONE_COIN, 1); + assert!(scale == 1000000000000, 1); + assert!(pool_lockup_period == lockup_period, 1); + assert!(stake::pool_exists(@harvest), 1); + + let (collection_owner_addr, coll_name, boost_percent) = + stake::get_boost_config(@harvest); + assert!(collection_owner_addr == @collection_owner, 1); + assert!(coll_name == collection_name, 1); + assert!(boost_percent == 10, 1); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + assert!(stake::is_whitelisted(@harvest, @0x41), 1); + assert!(!stake::is_whitelisted(@harvest, @0x42), 1); + assert!(!stake::is_whitelisted(@harvest, @0x43), 1); + } + #[test] fun test_scripts_end_to_end() { let (harvest, emergency_admin) = initialize_test(); @@ -100,11 +194,12 @@ module harvest::scripts_tests { coin::deposit(@harvest, reward_coins); assert!(coin::balance(@harvest) == 1000000000, 1); - scripts::register_pool(&harvest, 1000 * ONE_COIN, duration); + scripts::register_pool(&harvest, 1000 * ONE_COIN, + duration, WEEK_IN_SECONDS, vector[]); assert!(coin::balance(@harvest) == 0, 1); - let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale) = + let (reward_per_sec, accum_reward, last_updated, reward_coin_amount, scale, lockup_period) = stake::get_pool_info(pool_address); let end_ts = stake::get_end_timestamp(@harvest); assert!(end_ts == START_TIME + duration, 1); @@ -113,6 +208,7 @@ module harvest::scripts_tests { assert!(last_updated == 682981200, 1); assert!(reward_coin_amount == 1000 * ONE_COIN, 1); assert!(scale == 1000000000000, 1); + assert!(lockup_period == WEEK_IN_SECONDS, 1); let alice_acc = new_account_with_stake_coins(@alice, 100 * ONE_COIN); @@ -162,6 +258,8 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, + vector[], ); scripts::stake( @@ -195,6 +293,8 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + WEEK_IN_SECONDS, + vector[], ); scripts::stake( @@ -237,6 +337,8 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, + vector[], ); scripts::stake( @@ -272,6 +374,8 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, + vector[], ); scripts::stake( @@ -312,9 +416,11 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, @collection_owner, collection_name, 5, + vector[], ); scripts::stake_and_boost( @@ -360,9 +466,11 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + WEEK_IN_SECONDS, @collection_owner, collection_name, 5, + vector[], ); scripts::stake_and_boost( @@ -417,9 +525,11 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, @collection_owner, collection_name, 5, + vector[], ); scripts::stake(&alice_acc, @harvest, 10 * ONE_COIN); @@ -460,9 +570,11 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, @collection_owner, collection_name, 5, + vector[], ); scripts::stake(&alice_acc, @harvest, 10 * ONE_COIN); @@ -493,19 +605,71 @@ module harvest::scripts_tests { let reward_coins = mint_default_coin(1000 * ONE_COIN); let duration = 100000000; + let lockup_period = 100000000; coin::deposit(@harvest, reward_coins); - scripts::register_pool(&harvest, 1000 * ONE_COIN, duration); + scripts::register_pool(&harvest, 1000 * ONE_COIN, + duration, lockup_period, vector[]); let reward_coins = mint_default_coin(1 * ONE_COIN); coin::deposit(@alice, reward_coins); assert!(coin::balance(@alice) == 1000000, 1); - scripts::deposit_reward_coins(&alice_acc, @harvest, 1 * ONE_COIN); + scripts::deposit_reward_coins(&alice_acc, @harvest, 1 * ONE_COIN, 1); - let (_, _, _, reward_coin_amount, _) = stake::get_pool_info(@harvest); + let (_, _, _, reward_coin_amount, _, _) = stake::get_pool_info(@harvest); assert!(reward_coin_amount == 1001000000, 1); assert!(coin::balance(@alice) == 0, 1); } + #[test] + fun test_script_add_and_remove_from_whitelist() { + let (harvest, _) = initialize_test(); + let alice_acc = new_account_with_stake_coins(@alice, 10 * ONE_COIN); + + let reward_coins = mint_default_coin(15768000000000); + coin::register(&harvest); + coin::deposit(@harvest, reward_coins); + + // register staking pool with rewards and boost config + let duration = 15768000; + let lockup_period = 15768000; + scripts::register_pool( + &harvest, + 15768000000000, + 15768000, + lockup_period, + vector[@bob], + ); + + // check alice not in whitelist + assert!(!stake::is_whitelisted(@harvest, @alice), 1); + // check bob in whitelist + assert!(stake::is_whitelisted(@harvest, @bob), 1); + + // add alice remove bob + scripts::add_into_whitelist(&harvest, vector[@alice]); + scripts::remove_from_whitelist(&harvest, @bob); + + // check alice in whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + // check bob not in whitelist + assert!(!stake::is_whitelisted(@harvest, @bob), 1); + + scripts::stake( + &alice_acc, + @harvest, + 10 * ONE_COIN, + ); + + timestamp::update_global_time_for_test_secs(START_TIME + duration); + + scripts::harvest( + &alice_acc, + @harvest + ); + + assert!(coin::balance(@alice) == 15768000000000, 1); + } + #[test] fun test_script_enable_emergency() { let (harvest, emergency_admin) = initialize_test(); @@ -515,7 +679,8 @@ module harvest::scripts_tests { coin::deposit(@harvest, reward_coins); // register staking pool with rewards - scripts::register_pool(&harvest, 15768000000000, 15768000); + scripts::register_pool(&harvest, 15768000000000, + 15768000, 0, vector[]); scripts::enable_emergency(&emergency_admin, @harvest); @@ -532,7 +697,8 @@ module harvest::scripts_tests { coin::deposit(@harvest, reward_coins); // register staking pool with rewards - scripts::register_pool(&harvest, 15768000000000, 15768000); + scripts::register_pool(&harvest, 15768000000000, + 15768000, 0, vector[]); scripts::stake(&alice_acc, @harvest, 10 * ONE_COIN); scripts::enable_emergency(&emergency_admin, @harvest); @@ -562,9 +728,11 @@ module harvest::scripts_tests { &harvest, 15768000000000, 15768000, + 0, @collection_owner, collection_name, 5, + vector[], ); scripts::stake(&alice_acc, @harvest, 10 * ONE_COIN); @@ -593,8 +761,10 @@ module harvest::scripts_tests { let reward_coins = mint_default_coin(1000 * ONE_COIN); let duration = 100000000; + let lockup_period = 100000000; coin::deposit(@harvest, reward_coins); - scripts::register_pool(&harvest, 1000 * ONE_COIN, duration); + scripts::register_pool(&harvest, 1000 * ONE_COIN, + duration, lockup_period, vector[]); timestamp::update_global_time_for_test_secs(START_TIME + duration + 7257600); @@ -612,7 +782,8 @@ module harvest::scripts_tests { let reward_coins = mint_default_coin(1000 * ONE_COIN); let duration = 100000000; coin::deposit(@harvest, reward_coins); - scripts::register_pool(&harvest, 1000 * ONE_COIN, duration); + scripts::register_pool(&harvest, 1000 * ONE_COIN, + duration, 0, vector[]); timestamp::update_global_time_for_test_secs(START_TIME + duration + 7257600); diff --git a/tests/stake_decimals_tests.move b/tests/stake_decimals_tests.move index 7fe8a09..4205dfa 100644 --- a/tests/stake_decimals_tests.move +++ b/tests/stake_decimals_tests.move @@ -44,7 +44,8 @@ module harvest::stake_decimals_tests { // register staking pool with 10 000 000 RewardCoins let reward_coins = mint_default_coin(10000000); let duration = 2000000; - stake::register_pool(&harvest_acc, reward_coins, duration, option::none()); + stake::register_pool(&harvest_acc, reward_coins, + duration, WEEK_IN_SECONDS, option::none(), vector[]); // stake 19 StakeCoins from alice let coins = @@ -52,7 +53,7 @@ module harvest::stake_decimals_tests { stake::stake(&alice_acc, @harvest, coins); // check pool parameters after first stake - let (reward_per_sec, accum_reward, last_updated, _, _) = + let (reward_per_sec, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // pool_rewards_amount / duration // 5 RewardCoins @@ -73,7 +74,7 @@ module harvest::stake_decimals_tests { stake::recalculate_user_stake(@harvest, @alice); // check pool parameters after 10 seconds - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 2.631578947368 RewardCoins @@ -123,7 +124,7 @@ module harvest::stake_decimals_tests { // check pool parameters // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 104278.493647912885 RewardCoins @@ -151,7 +152,7 @@ module harvest::stake_decimals_tests { // check pool parameters after full unstake // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 104284.049203468440 RewardCoins @@ -221,7 +222,8 @@ module harvest::stake_decimals_tests { // register staking pool, deposit 10 000 000 RewardCoins let reward_coins = mint_default_coin(1000000000000000); let duration = 5000000; - stake::register_pool(&harvest_acc, reward_coins, duration, option::none()); + stake::register_pool(&harvest_acc, reward_coins, + duration, WEEK_IN_SECONDS, option::none(), vector[]); // stake 19.99 StakeCoins from alice let coins = @@ -229,7 +231,7 @@ module harvest::stake_decimals_tests { stake::stake(&alice_acc, @harvest, coins); // check pool parameters after first stake - let (reward_per_sec, accum_reward, last_updated, _, _) = + let (reward_per_sec, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // pool_rewards_amount / duration // 2 RewardCoins @@ -250,7 +252,7 @@ module harvest::stake_decimals_tests { stake::recalculate_user_stake(@harvest, @alice); // check pool parameters after 10 seconds - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 1.000500250125 RewardCoins @@ -300,7 +302,7 @@ module harvest::stake_decimals_tests { // check pool parameters // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 40334.444981743956 RewardCoins @@ -328,7 +330,7 @@ module harvest::stake_decimals_tests { // check pool parameters after full unstake // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 40336.446983745958 RewardCoins @@ -398,7 +400,8 @@ module harvest::stake_decimals_tests { // register staking pool, deposit 1 000 000 RewardCoins let reward_coins = mint_default_coin(10000000000000000); let duration = 1000000; - stake::register_pool(&harvest_acc, reward_coins, duration, option::none()); + stake::register_pool(&harvest_acc, reward_coins, + duration, WEEK_IN_SECONDS, option::none(), vector[]); // stake 19.999999 StakeCoins from alice let coins = @@ -406,7 +409,7 @@ module harvest::stake_decimals_tests { stake::stake(&alice_acc, @harvest, coins); // check pool parameters after first stake - let (reward_per_sec, accum_reward, last_updated, _, _) = + let (reward_per_sec, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // pool_rewards_amount / duration // 1 RewardCoin @@ -427,7 +430,7 @@ module harvest::stake_decimals_tests { stake::recalculate_user_stake(@harvest, @alice); // check pool parameters after 10 seconds - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 0.500000025000 RewardCoins @@ -477,7 +480,7 @@ module harvest::stake_decimals_tests { // check pool parameters // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 20160.500672025022 RewardCoins @@ -505,7 +508,7 @@ module harvest::stake_decimals_tests { // check pool parameters after full unstake // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 20161.500672125022 RewardCoins @@ -574,7 +577,8 @@ module harvest::stake_decimals_tests { // register staking pool, deposit 10 000 004 RewardCoins let reward_coins = mint_default_coin(1000000400); let duration = 2857144; - stake::register_pool(&harvest_acc, reward_coins, duration, option::none()); + stake::register_pool(&harvest_acc, reward_coins, + duration, 0, option::none(), vector[]); // stake 19.99999999 StakeCoins from alice let coins = @@ -582,7 +586,7 @@ module harvest::stake_decimals_tests { stake::stake(&alice_acc, @harvest, coins); // check pool parameters after first stake - let (reward_per_sec, accum_reward, last_updated, _, _) = + let (reward_per_sec, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // pool_rewards_amount / duration // 3.5 RewardCoins @@ -603,7 +607,7 @@ module harvest::stake_decimals_tests { stake::recalculate_user_stake(@harvest, @alice); // check pool parameters after 10 seconds - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 1,750000000875 RewardCoins @@ -653,7 +657,7 @@ module harvest::stake_decimals_tests { // check pool parameters // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 70561.750023520875 RewardCoins @@ -681,7 +685,7 @@ module harvest::stake_decimals_tests { // check pool parameters after full unstake // note: accum_reward recalculated before total_stake was decreased by user unstake - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 70565.250023524375 RewardCoins @@ -758,7 +762,8 @@ module harvest::stake_decimals_tests { // register staking pool with 100 RewardCoins let reward_coins = mint_default_coin(100); let duration = 100; - stake::register_pool(&harvest_acc, reward_coins, duration, option::none()); + stake::register_pool(&harvest_acc, reward_coins, + duration, 0, option::none(), vector[]); // wait 15 seconds timestamp::update_global_time_for_test_secs(START_TIME + 15); @@ -769,7 +774,7 @@ module harvest::stake_decimals_tests { stake::stake(&alice_acc, @harvest, coins); // check pool parameters after first stake - let (reward_per_sec, accum_reward, last_updated, _, _) = + let (reward_per_sec, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // pool_rewards_amount / duration // 1 RewardCoins @@ -792,7 +797,7 @@ module harvest::stake_decimals_tests { stake::stake(&bob_acc, @harvest, coins); // check pool parameters after new stake and 15 seconds - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 0.75 RewardCoins @@ -825,7 +830,7 @@ module harvest::stake_decimals_tests { stake::stake(&carol_acc, @harvest, coins); // check pool parameters after new stake and 25 seconds - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + accum_reward(previous) // 1.25 RewardCoins @@ -896,7 +901,8 @@ module harvest::stake_decimals_tests { let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); } #[test] @@ -916,6 +922,7 @@ module harvest::stake_decimals_tests { let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); } } diff --git a/tests/stake_nft_boost_tests.move b/tests/stake_nft_boost_tests.move index c04f756..1644b0f 100644 --- a/tests/stake_nft_boost_tests.move +++ b/tests/stake_nft_boost_tests.move @@ -66,18 +66,21 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; + let lockup_period = 15768000; let boost_config = stake::create_boost_config( @collection_owner, collection_name, 5 ); - stake::register_pool(&alice_acc, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&alice_acc, reward_coins, + duration, lockup_period, option::some(boost_config), vector[]); // check pool statistics - let (reward_per_sec, accum_reward, last_updated, reward_amount, scale) = + let (reward_per_sec, accum_reward, last_updated, reward_amount, scale, pool_lockup_period) = stake::get_pool_info(@alice); let end_ts = stake::get_end_timestamp(@alice); assert!(end_ts == START_TIME + duration, 1); + assert!(pool_lockup_period == lockup_period, 1); assert!(reward_per_sec == 1000000, 1); assert!(accum_reward == 0, 1); assert!(last_updated == START_TIME, 1); @@ -110,7 +113,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -160,7 +164,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -203,7 +208,8 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 100 StakeCoins from alice let coins = @@ -314,7 +320,8 @@ module harvest::stake_nft_boost_tests { collection_name, 1 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -413,12 +420,14 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // register staking pool 2 with rewards no boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // check is boostable assert!(stake::is_boostable(@harvest), 1); @@ -442,7 +451,8 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -525,7 +535,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); stake::boost(&harvest, @harvest, nft); } @@ -541,7 +552,8 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); let nft = stake::remove_boost(&harvest, @harvest); token::deposit_token(&harvest, nft); @@ -555,7 +567,8 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); stake::get_user_boosted(@harvest, @alice); } @@ -568,7 +581,8 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); stake::get_user_boosted(@harvest, @alice); } @@ -585,7 +599,8 @@ module harvest::stake_nft_boost_tests { string::utf8(b"Wrong Collection"), 5 ); - stake::register_pool(&harvest, coin::zero(), 12345, option::some(boost_config)); + stake::register_pool(&harvest, coin::zero(), + 12345, 0, option::some(boost_config), vector[]); } #[test] @@ -598,7 +613,8 @@ module harvest::stake_nft_boost_tests { string::utf8(b"Test Collection"), 5 ); - stake::register_pool(&harvest, coin::zero(), 12345, option::some(boost_config)); + stake::register_pool(&harvest, coin::zero(), + 12345, 0, option::some(boost_config), vector[]); } #[test] @@ -614,7 +630,8 @@ module harvest::stake_nft_boost_tests { collection_name, 0 ); - stake::register_pool(&harvest, coin::zero(), 12345, option::some(boost_config)); + stake::register_pool(&harvest, coin::zero(), + 12345, 0, option::some(boost_config), vector[]); } #[test] @@ -630,7 +647,8 @@ module harvest::stake_nft_boost_tests { collection_name, 101 ); - stake::register_pool(&harvest, coin::zero(), 12345, option::some(boost_config)); + stake::register_pool(&harvest, coin::zero(), + 12345, 0, option::some(boost_config), vector[]); } #[test] @@ -646,7 +664,8 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 500 StakeCoins from alice let coins = @@ -665,7 +684,8 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); stake::get_boost_config(@harvest); } @@ -689,7 +709,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -721,7 +742,8 @@ module harvest::stake_nft_boost_tests { collection_name_1, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -751,7 +773,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -771,7 +794,8 @@ module harvest::stake_nft_boost_tests { // register staking pool with rewards and boost config let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 500 StakeCoins from alice let coins = @@ -800,7 +824,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -830,7 +855,8 @@ module harvest::stake_nft_boost_tests { collection_name, 5 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 500 StakeCoins from alice let coins = @@ -883,7 +909,8 @@ module harvest::stake_nft_boost_tests { collection_name, 1 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); // stake 800 StakeCoins from bob let coins = @@ -917,7 +944,8 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 500000000); @@ -988,7 +1016,8 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 500000000); @@ -1062,7 +1091,8 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 500000000); @@ -1129,7 +1159,8 @@ module harvest::stake_nft_boost_tests { collection_name, 100 ); - stake::register_pool(&harvest, reward_coins, duration, option::some(boost_config)); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::some(boost_config), vector[]); let coins = coin::withdraw(&alice_acc, 500000000); diff --git a/tests/stake_test_helpers.move b/tests/stake_test_helpers.move index 7f783c1..8e719e3 100644 --- a/tests/stake_test_helpers.move +++ b/tests/stake_test_helpers.move @@ -2,9 +2,9 @@ module harvest::stake_test_helpers { use std::signer; use std::string::{String, utf8}; - + use aptos_std::math64; use aptos_framework::account; - use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability}; + use aptos_framework::coin::{Self, BurnCapability, Coin, MintCapability}; // Coins. @@ -97,4 +97,12 @@ module harvest::stake_test_helpers { public fun to_u128(num: u64): u128 { (num as u128) } + + public fun amount(b: u64, f: u64): u64 { + let decimals_multiplier = math64::pow(10, (coin::decimals() as u64)); + // common sense assert + assert!(f < decimals_multiplier * 10, 1); + let base = b * decimals_multiplier; + base + f + } } diff --git a/tests/stake_tests.move b/tests/stake_tests.move index 3962e3f..bfe3ddf 100644 --- a/tests/stake_tests.move +++ b/tests/stake_tests.move @@ -6,7 +6,7 @@ module harvest::stake_tests { use aptos_framework::genesis; use aptos_framework::timestamp; - use harvest::stake::{Self, is_finished}; + use harvest::stake; use harvest::stake_config; use harvest::stake_test_helpers::{new_account, initialize_reward_coin, initialize_stake_coin, mint_default_coin, StakeCoin, RewardCoin, new_account_with_stake_coins}; @@ -40,13 +40,16 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&alice_acc, reward_coins, duration, option::none()); + let lockup_period = 15768000; + stake::register_pool(&alice_acc, reward_coins, + duration, lockup_period, option::none(), vector[]); // check pool statistics - let (reward_per_sec, accum_reward, last_updated, reward_amount, scale) = + let (reward_per_sec, accum_reward, last_updated, reward_amount, scale, pool_lockup_period) = stake::get_pool_info(@alice); let end_ts = stake::get_end_timestamp(@alice); assert!(end_ts == START_TIME + duration, 1); + assert!(pool_lockup_period == lockup_period, 1); assert!(reward_per_sec == 1000000, 1); assert!(accum_reward == 0, 1); assert!(last_updated == START_TIME, 1); @@ -66,12 +69,14 @@ module harvest::stake_tests { // register staking pool 1 with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&alice_acc, reward_coins, duration, option::none()); + stake::register_pool(&alice_acc, reward_coins, + duration, 0, option::none(), vector[]); // register staking pool 2 with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&bob_acc, reward_coins, duration, option::none()); + stake::register_pool(&bob_acc, reward_coins, + duration, 0, option::none(), vector[]); // check pools exist assert!(stake::pool_exists(@alice), 1); @@ -86,11 +91,12 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // check pool statistics let pool_finish_time = START_TIME + duration; - let (reward_per_sec, _, _, reward_amount, _) = + let (reward_per_sec, _, _, reward_amount, _, _) = stake::get_pool_info(@harvest); let end_ts = stake::get_end_timestamp(@harvest); assert!(end_ts == pool_finish_time, 1); @@ -99,27 +105,27 @@ module harvest::stake_tests { // deposit more rewards let reward_coins = mint_default_coin(604800000000); - stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 15768000 + 604800); // check pool statistics let pool_finish_time = pool_finish_time + 604800; - let (reward_per_sec, _, _, reward_amount, _) = + let (reward_per_sec, _, _, reward_amount, _, _) = stake::get_pool_info(@harvest); let end_ts = stake::get_end_timestamp(@harvest); assert!(end_ts == pool_finish_time, 1); assert!(reward_per_sec == 1000000, 1); assert!(reward_amount == 16372800000000, 1); - // wait to a second before pool duration end - timestamp::update_global_time_for_test_secs(pool_finish_time - 1); + // wait to a pool end second + timestamp::update_global_time_for_test_secs(pool_finish_time); // deposit more rewards let reward_coins = mint_default_coin(604800000000); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 604800); // check pool statistics let pool_finish_time = pool_finish_time + 604800; - let (reward_per_sec, _, _, reward_amount, _) = + let (reward_per_sec, _, _, reward_amount, _, _) = stake::get_pool_info(@harvest); let end_ts = stake::get_end_timestamp(@harvest); assert!(end_ts == pool_finish_time, 1); @@ -137,7 +143,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // check no stakes assert!(!stake::stake_exists(@harvest, @alice), 1); @@ -187,6 +194,84 @@ module harvest::stake_tests { coin::deposit(@bob, coins); } + #[test] + public fun test_stake_and_unstake_custom_lockup_period_1() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 900000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 7000000, option::none(), vector[]); + + // check no stakes + assert!(!stake::stake_exists(@harvest, @alice), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(coin::balance(@alice) == 400000000, 1); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + assert!(!stake::is_unlocked(@harvest, @alice), 1); + + // wait one week + timestamp::update_global_time_for_test_secs(START_TIME + WEEK_IN_SECONDS); + assert!(!stake::is_unlocked(@harvest, @alice), 1); + + // wait 7000000 + timestamp::update_global_time_for_test_secs(START_TIME + 7000000); + assert!(stake::is_unlocked(@harvest, @alice), 1); + + // unstake 400 StakeCoins from alice + let coins = + stake::unstake(&alice_acc, @harvest, 500000000); + assert!(coin::value(&coins) == 500000000, 1); + coin::deposit(@alice, coins); + } + + #[test] + public fun test_stake_and_unstake_custom_lockup_period_2() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 900000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 2, option::none(), vector[]); + + // check no stakes + assert!(!stake::stake_exists(@harvest, @alice), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(coin::balance(@alice) == 400000000, 1); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + assert!(!stake::is_unlocked(@harvest, @alice), 1); + + // wait 1 sec + timestamp::update_global_time_for_test_secs(START_TIME + 1); + assert!(!stake::is_unlocked(@harvest, @alice), 1); + + // wait 2 sec + timestamp::update_global_time_for_test_secs(START_TIME + 2); + assert!(stake::is_unlocked(@harvest, @alice), 1); + + // unstake 400 StakeCoins from alice + let coins = + stake::unstake(&alice_acc, @harvest, 500000000); + assert!(coin::value(&coins) == 500000000, 1); + coin::deposit(@alice, coins); + } + #[test] public fun test_unstake_works_after_pool_duration_end() { let (harvest, _) = initialize_test(); @@ -196,7 +281,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake from alice let coins = @@ -225,7 +311,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, WEEK_IN_SECONDS, option::none(), vector[]); // stake from alice let coins = @@ -302,7 +389,8 @@ module harvest::stake_tests { let reward_coins = mint_default_coin(604805000000); let duration = 604805; let start_ts = timestamp::now_seconds(); - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); assert!(stake::get_start_timestamp(@harvest) == start_ts, 1); @@ -326,7 +414,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(604805000000); let duration = 604805; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, WEEK_IN_SECONDS, option::none(), vector[]); // stake from alice let coins = @@ -352,64 +441,6 @@ module harvest::stake_tests { assert!(stake::is_unlocked(@harvest, @alice), 1); assert!(!stake::is_unlocked(@harvest, @bob), 1); - - // wait until pool expired - timestamp::update_global_time_for_test_secs(START_TIME + duration); - - assert!(stake::is_unlocked(@harvest, @alice), 1); - assert!(stake::is_unlocked(@harvest, @bob), 1); - - // wait a week more - timestamp::update_global_time_for_test_secs(START_TIME + duration + WEEK_IN_SECONDS); - - assert!(stake::is_unlocked(@harvest, @alice), 1); - assert!(stake::is_unlocked(@harvest, @bob), 1); - } - - #[test] - public fun test_is_unlocked_early() { - let (harvest, _) = initialize_test(); - - let alice_acc = new_account_with_stake_coins(@alice, 500000); - - // register staking pool with rewards - let reward_coins = mint_default_coin(604805000000); - let duration = 3600; - stake::register_pool(&harvest, reward_coins, duration, option::none()); - - // stake from alice - let coins = - coin::withdraw(&alice_acc, 500000); - stake::stake(&alice_acc, @harvest, coins); - - assert!(!stake::is_unlocked(@harvest, @alice), 1); - - // wait almost a hour - timestamp::update_global_time_for_test_secs(START_TIME + 3600 - 1); - assert!(!stake::is_unlocked(@harvest, @alice), 1); - - // wait a hour - timestamp::update_global_time_for_test_secs(START_TIME + 3600); - assert!(stake::is_unlocked(@harvest, @alice), 1); - } - - #[test] - public fun test_get_unlock_time_early() { - let (harvest, _) = initialize_test(); - - let alice_acc = new_account_with_stake_coins(@alice, 500000); - - // register staking pool with rewards - let reward_coins = mint_default_coin(604805000000); - let duration = 3600; - stake::register_pool(&harvest, reward_coins, duration, option::none()); - - // stake from alice - let coins = - coin::withdraw(&alice_acc, 500000); - stake::stake(&alice_acc, @harvest, coins); - - assert!(stake::get_unlock_time(@harvest, @alice) == START_TIME + 3600, 1); } #[test] @@ -422,7 +453,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -442,7 +474,7 @@ module harvest::stake_tests { stake::recalculate_user_stake(@harvest, @alice); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // (reward_per_sec_rate * time passed / total_staked) + previous period assert!(accum_reward == 1000000000000, 1); @@ -480,7 +512,7 @@ module harvest::stake_tests { stake::recalculate_user_stake(@harvest, @bob); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 1400000000000, 1); assert!(last_updated == START_TIME + 20, 1); @@ -505,7 +537,7 @@ module harvest::stake_tests { stake::recalculate_user_stake(@harvest, @bob); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 24193400000000000, 1); assert!(last_updated == START_TIME + 20 + WEEK_IN_SECONDS, 1); @@ -541,7 +573,7 @@ module harvest::stake_tests { stake::recalculate_user_stake(@harvest, @bob); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 24194066666666666, 1); assert!(last_updated == START_TIME + 30 + WEEK_IN_SECONDS, 1); @@ -577,13 +609,14 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // wait one week with empty pool timestamp::update_global_time_for_test_secs(START_TIME + WEEK_IN_SECONDS); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 0, 1); assert!(last_updated == START_TIME, 1); @@ -600,7 +633,7 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 0, 1); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 0, 1); assert!(last_updated == START_TIME + WEEK_IN_SECONDS, 1); @@ -619,7 +652,7 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 6048000000000, 1); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // 604800 seconds * 10 rew_per_second / 100 total_staked assert!(accum_reward == 60480000000000000, 1); @@ -638,7 +671,7 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 6048000000000, 1); // check pool parameters - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); // 604800 seconds * 10 rew_per_second / 100 total_staked assert!(accum_reward == 60480000000000000, 1); @@ -659,7 +692,7 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 6048000000000, 1); // check pool parameters, pool should not accumulate rewards when no stakes in it - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 60480000000000000, 1); assert!(last_updated == START_TIME + (WEEK_IN_SECONDS * 5), 1); @@ -677,7 +710,7 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 12096000000000, 1); // check pool parameters, pool should not accumulate rewards when no stakes in it - let (_, accum_reward, last_updated, _, _) = + let (_, accum_reward, last_updated, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 120960000000000000, 1); assert!(last_updated == START_TIME + (WEEK_IN_SECONDS * 6), 1); @@ -696,7 +729,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -774,7 +808,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -806,8 +841,9 @@ module harvest::stake_tests { coin::register(&bob_acc); let reward_coins = mint_default_coin(302400000000); - let duration = 302400; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let duration = 604800; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 100000000); @@ -864,7 +900,8 @@ module harvest::stake_tests { let reward_coins = mint_default_coin(1000000000000000000); // 1 week. let duration = WEEK_IN_SECONDS; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake alice. let coins = @@ -923,7 +960,8 @@ module harvest::stake_tests { let reward_coins = mint_default_coin(1000000000000000000); // 10 years. let duration = 31536000 * 10; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake alice. let coins = @@ -954,43 +992,6 @@ module harvest::stake_tests { coin::deposit(@bob, coins); } - #[test] - public fun test_premature_unstake_and_harvest() { - let (harvest, _) = initialize_test(); - - let alice_acc = new_account_with_stake_coins(@alice, 100000000); - - coin::register(&alice_acc); - - // register staking pool with rewards - let reward_coins = mint_default_coin(157680000000000); - let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); - - timestamp::update_global_time_for_test_secs(START_TIME + duration - 1); - - // stake 100 StakeCoins from alice - let coins = - coin::withdraw(&alice_acc, 100000000); - stake::stake(&alice_acc, @harvest, coins); - - // wait until pool expired and almost a week more - timestamp::update_global_time_for_test_secs(START_TIME + duration + WEEK_IN_SECONDS / 2); - - let coins = stake::unstake(&alice_acc, @harvest, 100000000); - coin::deposit(@alice, coins); - - // harvest from alice - let coins = - stake::harvest(&alice_acc, @harvest); - - // check amounts - assert!(stake::get_pending_user_rewards(@harvest, @alice) == 0, 1); - assert!(coin::value(&coins) == 10000000, 1); - - coin::deposit(@alice, coins); - } - #[test] public fun test_stake_and_get_all_rewards_from_start_to_end() { let (harvest, _) = initialize_test(); @@ -1003,7 +1004,8 @@ module harvest::stake_tests { let reward_coins_val = 157680000000000; let reward_coins = mint_default_coin(reward_coins_val); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -1034,14 +1036,16 @@ module harvest::stake_tests { let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 100000000); stake::stake(&alice_acc, @harvest, coins); stake::recalculate_user_stake(@harvest, @alice); - let (_, accum_reward, last_updated, _, _) = stake::get_pool_info(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 0); let reward_val = stake::get_pending_user_rewards(@harvest, @alice); assert!(reward_val == 0, 1); assert!(accum_reward == 0, 1); @@ -1049,7 +1053,8 @@ module harvest::stake_tests { timestamp::update_global_time_for_test_secs(START_TIME + duration / 2); stake::recalculate_user_stake(@harvest, @alice); - let (_, accum_reward, last_updated, _, _) = stake::get_pool_info(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 0); let reward_val = stake::get_pending_user_rewards(@harvest, @alice); assert!(reward_val == 78840000000000, 1); assert!(accum_reward == 788400000000000000, 1); @@ -1057,7 +1062,8 @@ module harvest::stake_tests { timestamp::update_global_time_for_test_secs(START_TIME + duration); stake::recalculate_user_stake(@harvest, @alice); - let (_, accum_reward, last_updated, _, _) = stake::get_pool_info(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 0); let reward_val = stake::get_pending_user_rewards(@harvest, @alice); assert!(reward_val == 157680000000000, 1); assert!(accum_reward == 1576800000000000000, 1); @@ -1065,19 +1071,21 @@ module harvest::stake_tests { timestamp::update_global_time_for_test_secs(START_TIME + duration + 1); stake::recalculate_user_stake(@harvest, @alice); - let (_, accum_reward, last_updated, _, _) = stake::get_pool_info(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 1); let reward_val = stake::get_pending_user_rewards(@harvest, @alice); assert!(reward_val == 157680000000000, 1); - assert!(accum_reward == 1576800000000000000, 1); - assert!(last_updated == START_TIME + duration, 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == START_TIME + duration + 1, 1); timestamp::update_global_time_for_test_secs(START_TIME + duration + WEEK_IN_SECONDS * 200); stake::recalculate_user_stake(@harvest, @alice); - let (_, accum_reward, last_updated, _, _) = stake::get_pool_info(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 1); let reward_val = stake::get_pending_user_rewards(@harvest, @alice); assert!(reward_val == 157680000000000, 1); - assert!(accum_reward == 1576800000000000000, 1); - assert!(last_updated == START_TIME + duration, 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == START_TIME + duration + WEEK_IN_SECONDS * 200, 1); } #[test] @@ -1091,7 +1099,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // check pool exists after register let exists = stake::pool_exists(@harvest); @@ -1107,7 +1116,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // check stake exists before alice stake let exists = stake::stake_exists(@harvest, @alice); @@ -1132,7 +1142,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 50 StakeCoins from alice let coins = @@ -1173,7 +1184,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -1181,7 +1193,7 @@ module harvest::stake_tests { stake::stake(&alice_acc, @harvest, coins); // check stake earned and pool accum_reward - let (_, accum_reward, _, _, _) = + let (_, accum_reward, _, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 0, 1); assert!(stake::get_pending_user_rewards(@harvest, @alice) == 0, 1); @@ -1193,7 +1205,7 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 604800000000, 1); // check get_pending_user_rewards calculations didn't affect pool accum_reward - let (_, accum_reward, _, _, _) = + let (_, accum_reward, _, _, _, _) = stake::get_pool_info(@harvest); assert!(accum_reward == 0, 1); @@ -1218,31 +1230,6 @@ module harvest::stake_tests { assert!(stake::get_pending_user_rewards(@harvest, @alice) == 0, 1); } - #[test] - public fun test_is_finished() { - let (harvest, _) = initialize_test(); - - // register staking pool with rewards - let reward_coins = mint_default_coin(15768000000000); - let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); - - // check is finished - assert!(!is_finished(@harvest), 1); - - // wait to a second before pool duration end - timestamp::update_global_time_for_test_secs(START_TIME + duration - 1); - - // check is finished - assert!(!is_finished(@harvest), 1); - - // wait one second - timestamp::update_global_time_for_test_secs(START_TIME + duration); - - // check is finished - assert!(is_finished(@harvest), 1); - } - #[test] public fun test_get_end_timestamp() { let (harvest, _) = initialize_test(); @@ -1250,7 +1237,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // check pool expiration date let end_ts = stake::get_end_timestamp(@harvest); @@ -1258,7 +1246,7 @@ module harvest::stake_tests { // deposit more rewards let reward_coins = mint_default_coin(604800000000); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 15768000 + 604800); // check pool expiration date let end_ts = stake::get_end_timestamp(@harvest); @@ -1274,7 +1262,7 @@ module harvest::stake_tests { initialize_reward_coin(&harvest, 6); let reward_coins = mint_default_coin(100); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 12345); } #[test] @@ -1318,6 +1306,12 @@ module harvest::stake_tests { stake::get_pool_total_stake(@harvest); } + #[test] + #[expected_failure(abort_code = stake::ERR_NO_POOL)] + public fun test_get_pool_current_epoch_fails_if_pool_does_not_exist() { + stake::get_pool_current_epoch(@harvest); + } + #[test] #[expected_failure(abort_code = stake::ERR_NO_POOL)] public fun test_get_user_stake_fails_if_pool_does_not_exist() { @@ -1342,12 +1336,6 @@ module harvest::stake_tests { stake::is_unlocked(@harvest, @alice); } - #[test] - #[expected_failure(abort_code = stake::ERR_NO_POOL)] - public fun test_is_finished_fails_if_pool_does_not_exist() { - stake::is_finished(@harvest); - } - #[test] #[expected_failure(abort_code = stake::ERR_NO_POOL)] public fun test_get_end_timestamp_fails_if_pool_does_not_exist() { @@ -1367,8 +1355,10 @@ module harvest::stake_tests { // register staking pool twice let duration = 12345; - stake::register_pool(&alice_acc, reward_coins_1, duration, option::none()); - stake::register_pool(&alice_acc, reward_coins_2, duration, option::none()); + stake::register_pool(&alice_acc, reward_coins_1, + duration, 0, option::none(), vector[]); + stake::register_pool(&alice_acc, reward_coins_2, + duration, 0, option::none(), vector[]); } #[test] @@ -1379,7 +1369,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = coin::zero(); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); } #[test] @@ -1390,7 +1382,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::get_user_stake(@harvest, @alice); } @@ -1403,7 +1397,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::get_pending_user_rewards(@harvest, @alice); } @@ -1416,7 +1412,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::get_unlock_time(@harvest, @alice); } @@ -1429,7 +1427,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); stake::is_unlocked(@harvest, @alice); } @@ -1442,7 +1442,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); // unstake when stake not exists let coins = @@ -1458,7 +1460,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 12345; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); // harvest when stake not exists let coins = @@ -1476,7 +1480,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 99 StakeCoins from alice let coins = @@ -1500,7 +1505,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 0 StakeCoins coin::register(&harvest); @@ -1517,7 +1523,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // unstake 0 StakeCoins let coins = @@ -1533,11 +1540,12 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // deposit 0 RewardCoins let reward_coins = coin::zero(); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 12345); } #[test] @@ -1552,7 +1560,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -1577,7 +1586,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -1609,7 +1619,8 @@ module harvest::stake_tests { // register staking pool without stake coin let reward_coins = mint_default_coin(12345); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); } #[test] @@ -1625,7 +1636,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = coin::zero(); let duration = 12345; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); } #[test] @@ -1638,7 +1650,9 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + let lockup_period = 3000; + stake::register_pool(&harvest, reward_coins, + duration, lockup_period, option::none(), vector[]); // stake from alice let coins = @@ -1646,7 +1660,7 @@ module harvest::stake_tests { stake::stake(&alice_acc, @harvest, coins); // wait almost a week - timestamp::update_global_time_for_test_secs(START_TIME + WEEK_IN_SECONDS - 1); + timestamp::update_global_time_for_test_secs(START_TIME + lockup_period - 1); // unstake from alice let coins = @@ -1662,7 +1676,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(12345); let duration = 0; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); } #[test] @@ -1673,50 +1688,12 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // deposit rewards less than rew_per_sec pool rate let reward_coins = mint_default_coin(999999); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); - } - - #[test] - #[expected_failure(abort_code = stake::ERR_HARVEST_FINISHED)] - public fun test_deposit_reward_coins_fails_after_harvest_is_finished() { - let (harvest, _) = initialize_test(); - - // register staking pool with rewards - let reward_coins = mint_default_coin(15768000000000); - let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); - - // wait until pool expired - timestamp::update_global_time_for_test_secs(START_TIME + duration); - - // deposit rewards less than rew_per_sec pool rate - let reward_coins = mint_default_coin(1000000); - stake::deposit_reward_coins(&harvest, @harvest, reward_coins); - } - - #[test] - #[expected_failure(abort_code = stake::ERR_HARVEST_FINISHED)] - public fun test_stake_fails_after_harvest_is_finished() { - let (harvest, _) = initialize_test(); - - let alice_acc = new_account_with_stake_coins(@alice, 12345); - - // register staking pool with rewards - let reward_coins = mint_default_coin(15768000000000); - let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); - - // wait until pool expired - timestamp::update_global_time_for_test_secs(START_TIME + duration); - - // stake from alice - let coins = - coin::withdraw(&alice_acc, 12345); - stake::stake(&alice_acc, @harvest, coins); + stake::deposit_reward_coins(&harvest, @harvest, reward_coins, 0); } #[test] @@ -1728,7 +1705,8 @@ module harvest::stake_tests { let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); } // Withdraw rewards tests. @@ -1742,7 +1720,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); let reward_coins = stake::withdraw_to_treasury(&treasury, @harvest, 157680000000000); coin::deposit(@treasury, reward_coins); @@ -1756,7 +1735,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); stake_config::enable_global_emergency(&emergency); @@ -1772,7 +1752,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); stake_config::enable_global_emergency(&emergency); @@ -1790,7 +1771,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); timestamp::update_global_time_for_test_secs(START_TIME + duration + 7257600); @@ -1808,7 +1790,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); timestamp::update_global_time_for_test_secs(START_TIME + duration + 7257600); stake_config::enable_global_emergency(&emergency); @@ -1828,7 +1811,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(157680000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); timestamp::update_global_time_for_test_secs(START_TIME + duration + 7257599); @@ -1851,7 +1835,8 @@ module harvest::stake_tests { // register staking pool with rewards let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); // stake 100 StakeCoins from alice let coins = @@ -1891,7 +1876,8 @@ module harvest::stake_tests { let reward_coins = mint_default_coin(15768000000000); let duration = 15768000; - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, WEEK_IN_SECONDS, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 100000000); @@ -1945,7 +1931,8 @@ module harvest::stake_tests { coin::register(&alice_acc); let reward_coins = mint_default_coin(606000000000); - stake::register_pool(&harvest, reward_coins, duration, option::none()); + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); let coins = coin::withdraw(&alice_acc, 30000000000000); diff --git a/tests/staking_epochs_tests.move b/tests/staking_epochs_tests.move new file mode 100644 index 0000000..97992d4 --- /dev/null +++ b/tests/staking_epochs_tests.move @@ -0,0 +1,607 @@ +#[test_only] +module harvest::staking_epochs_tests_move { + use std::option; + + use aptos_framework::coin; + use aptos_framework::timestamp; + + use harvest::stake; + use harvest::stake_test_helpers::{amount, mint_default_coin, StakeCoin as S, RewardCoin as R, new_account_with_stake_coins}; + use harvest::stake_tests::initialize_test; + + // week in seconds, lockup period + const WEEK_IN_SECONDS: u64 = 604800; + + const START_TIME: u64 = 682981200; + + // Deposit reward on different epoch stages test + + #[test] + public fun test_deposit_twice_same_sec() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + coin::register(&alice_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(amount(1000, 0)); + let duration = 500; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // check 0 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(rewards_amount == amount(1000, 0), 1); + assert!(reward_per_sec == amount(2, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME, 1); + assert!(last_update_time == START_TIME, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // stake 100 from alice + stake::stake(&alice_acc, @harvest, coin::withdraw(&alice_acc, amount(100, 0))); + + // create new epoch, take some rewards from previous + let reward_coins = mint_default_coin(amount(1000, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 500); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + assert!(curr_epoch == 1, 0); + + // check 0 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(accum_reward == 0, 1); + assert!(last_update_time == START_TIME, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == 0, 1); + assert!(ended_at == START_TIME, 1); + + // check 1 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 1); + // 1000 new + 1000 from prev epoch + assert!(rewards_amount == amount(2000, 0), 1); + assert!(reward_per_sec == amount(4, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME, 1); + assert!(last_update_time == START_TIME, 1); + assert!(end_time == START_TIME + 500, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // wait full second epoch + timestamp::update_global_time_for_test_secs(START_TIME + 500); + + // check all rewards was distributed + let rew = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&rew) == amount(2000, 0), 1); + + // check 1 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 1); + assert!(accum_reward == 2000_0000_000000, 1); + assert!(last_update_time == START_TIME + 500, 1); + assert!(end_time == START_TIME + 500, 1); + assert!(distributed == amount(2000, 0), 1); + assert!(ended_at == START_TIME + 500, 1); + + coin::deposit(@alice, rew); + } + + #[test] + public fun test_deposit_rew_on_unfinished_rew_epoch() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + coin::register(&alice_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(amount(1000, 0)); + let duration = 500; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // check 0 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(rewards_amount == amount(1000, 0), 1); + assert!(reward_per_sec == amount(2, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME, 1); + assert!(last_update_time == START_TIME, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // stake 100 from alice + stake::stake(&alice_acc, @harvest, coin::withdraw(&alice_acc, amount(100, 0))); + + // wait half of epoch + timestamp::update_global_time_for_test_secs(START_TIME + 250); + + // create new epoch, take some rewards from previous + let reward_coins = mint_default_coin(amount(1000, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 500); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + assert!(curr_epoch == 1, 0); + + // check 0 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(accum_reward == 500_0000_000000, 1); + assert!(last_update_time == START_TIME + 250, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == amount(500, 0), 1); + assert!(ended_at == START_TIME + 250, 1); + + // check 1 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 1); + // 1000 new + 500 from prev epoch + assert!(rewards_amount == amount(1500, 0), 1); + assert!(reward_per_sec == amount(3, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME + 250, 1); + assert!(last_update_time == START_TIME + 250, 1); + assert!(end_time == START_TIME + 250 + 500, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // wait full epoch 1 + timestamp::update_global_time_for_test_secs(START_TIME + 250 + 500); + + // check all rewards was distributed + let rew = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&rew) == amount(2000, 0), 1); + + // check 1 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 1); + assert!(accum_reward == 1500_0000_000000, 1); + assert!(last_update_time == START_TIME + 250 + 500, 1); + assert!(end_time == START_TIME + 250 + 500, 1); + assert!(distributed == amount(1500, 0), 1); + assert!(ended_at == START_TIME + 250 + 500, 1); + + coin::deposit(@alice, rew); + } + + #[test] + public fun test_deposit_rew_on_finished_this_sec_rew_epoch() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + coin::register(&alice_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(amount(1000, 0)); + let duration = 500; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // check 0 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(rewards_amount == amount(1000, 0), 1); + assert!(reward_per_sec == amount(2, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME, 1); + assert!(last_update_time == START_TIME, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // stake 100 from alice + stake::stake(&alice_acc, @harvest, coin::withdraw(&alice_acc, amount(100, 0))); + + // wait full epoch + timestamp::update_global_time_for_test_secs(START_TIME + 500); + + // create new epoch + let reward_coins = mint_default_coin(amount(1000, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 500); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + assert!(curr_epoch == 2, 0); + + // check 0 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(accum_reward == 1000_0000_000000, 1); + assert!(last_update_time == START_TIME + duration, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == amount(1000, 0), 1); + assert!(ended_at == START_TIME + duration, 1); + + // Below epoch appeared as a result of an edge case when the epoch creation + // transaction came at the second of the end of the previous epoch + + // check 1 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 1); + assert!(rewards_amount == 0, 1); + assert!(reward_per_sec == 0, 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME + 500, 1); + assert!(last_update_time == START_TIME + 500, 1); + assert!(end_time == START_TIME + 500, 1); + assert!(distributed == 0, 1); + assert!(ended_at == START_TIME + 500, 1); + + // check 2 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 2); + assert!(rewards_amount == amount(1000, 0), 1); + assert!(reward_per_sec == amount(2, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME + 500, 1); + assert!(last_update_time == START_TIME + 500, 1); + assert!(end_time == START_TIME + 500 + 500, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // wait full epoch 2 + timestamp::update_global_time_for_test_secs(START_TIME + 500 + 500); + + // check all rewards was distributed + let rew = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&rew) == amount(2000, 0), 1); + + // check 1 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 2); + assert!(accum_reward == 1000_0000_000000, 1); + assert!(last_update_time == START_TIME + 500 + 500, 1); + assert!(end_time == START_TIME + 500 + 500, 1); + assert!(distributed == amount(1000, 0), 1); + assert!(ended_at == START_TIME + 500 + 500, 1); + + coin::deposit(@alice, rew); + } + + #[test] + public fun test_deposit_rew_on_long_time_ago_finished_rew_epoch() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + coin::register(&alice_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(amount(1000, 0)); + let duration = 500; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // check 0 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(rewards_amount == amount(1000, 0), 1); + assert!(reward_per_sec == amount(2, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME, 1); + assert!(last_update_time == START_TIME, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // stake 100 from alice + stake::stake(&alice_acc, @harvest, coin::withdraw(&alice_acc, amount(100, 0))); + + // wait full epoch + one year + timestamp::update_global_time_for_test_secs(START_TIME + WEEK_IN_SECONDS * 52); + + // create new epoch + let reward_coins = mint_default_coin(amount(1000, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 500); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + assert!(curr_epoch == 2, 0); + + // check 0 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(accum_reward == 1000_0000_000000, 1); + assert!(last_update_time == START_TIME + WEEK_IN_SECONDS * 52, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == amount(1000, 0), 1); + assert!(ended_at == START_TIME + WEEK_IN_SECONDS * 52, 1); + + // check 1 (ghost) epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 1); + assert!(rewards_amount == 0, 1); + assert!(reward_per_sec == 0, 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME + duration, 1); + assert!(last_update_time == START_TIME + WEEK_IN_SECONDS * 52, 1); + assert!(end_time == START_TIME + WEEK_IN_SECONDS * 52, 1); + assert!(distributed == 0, 1); + assert!(ended_at == START_TIME + WEEK_IN_SECONDS * 52, 1); + + // check 2 epoch fields + let (rewards_amount, reward_per_sec, accum_reward,start_time, + last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 2); + assert!(rewards_amount == amount(1000, 0), 1); + assert!(reward_per_sec == amount(2, 0), 1); + assert!(accum_reward == 0, 1); + assert!(start_time == START_TIME + WEEK_IN_SECONDS * 52, 1); + assert!(last_update_time == START_TIME + WEEK_IN_SECONDS * 52, 1); + assert!(end_time == START_TIME + WEEK_IN_SECONDS * 52 + 500, 1); + assert!(distributed == 0, 1); + assert!(ended_at == 0, 1); + + // wait full epoch 2 + timestamp::update_global_time_for_test_secs(START_TIME + WEEK_IN_SECONDS * 52 + 500); + + // check all rewards was distributed + let rew = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&rew) == amount(2000, 0), 1); + + // check 2 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 2); + assert!(accum_reward == 1000_0000_000000, 1); + assert!(last_update_time == START_TIME + WEEK_IN_SECONDS * 52 + 500, 1); + assert!(end_time == START_TIME + WEEK_IN_SECONDS * 52 + 500, 1); + assert!(distributed == amount(1000, 0), 1); + assert!(ended_at == START_TIME + WEEK_IN_SECONDS * 52 + 500, 1); + + coin::deposit(@alice, rew); + } + + #[test] + public fun test_rewards_accumulating_after_ghost_epoch() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + coin::register(&alice_acc); + + // create pool + let reward_coins = mint_default_coin(amount(157680000, 0)); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // stake some coins + let coins = + coin::withdraw(&alice_acc, amount(100, 0)); + stake::stake(&alice_acc, @harvest, coins); + + // check accum reward + stake::recalculate_user_stake(@harvest, @alice); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, curr_epoch); + let reward_val = stake::get_pending_user_rewards(@harvest, @alice); + assert!(reward_val == 0, 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == START_TIME, 1); + assert!(curr_epoch == 0, 1); + + // wait half of duration & check accum reward + timestamp::update_global_time_for_test_secs(START_TIME + duration / 2); + stake::recalculate_user_stake(@harvest, @alice); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, curr_epoch); + let reward_val = stake::get_pending_user_rewards(@harvest, @alice); + assert!(reward_val == amount(78840000, 0), 1); + assert!(accum_reward == 788400000000000000, 1); + assert!(last_updated == START_TIME + duration / 2, 1); + assert!(curr_epoch == 0, 1); + + // wait full duration & check accum reward + timestamp::update_global_time_for_test_secs(START_TIME + duration); + stake::recalculate_user_stake(@harvest, @alice); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 0); + let reward_val = stake::get_pending_user_rewards(@harvest, @alice); + assert!(reward_val == amount(157680000, 0), 1); + assert!(accum_reward == 1576800000000000000, 1); + assert!(last_updated == START_TIME + duration, 1); + assert!(curr_epoch == 1, 1); + + // wait full duration + 1 sec & check accum reward + timestamp::update_global_time_for_test_secs(START_TIME + duration + 1); + stake::recalculate_user_stake(@harvest, @alice); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, curr_epoch); + let reward_val = stake::get_pending_user_rewards(@harvest, @alice); + assert!(reward_val == amount(157680000, 0), 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == START_TIME + duration + 1, 1); + assert!(curr_epoch == 1, 1); + + // wait full duration + 200 weeks & check accum reward + timestamp::update_global_time_for_test_secs(START_TIME + duration + WEEK_IN_SECONDS * 200); + stake::recalculate_user_stake(@harvest, @alice); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, curr_epoch); + let reward_val = stake::get_pending_user_rewards(@harvest, @alice); + assert!(reward_val == amount(157680000, 0), 1); + assert!(accum_reward == 0, 1); + assert!(last_updated == START_TIME + duration + WEEK_IN_SECONDS * 200, 1); + assert!(curr_epoch == 1, 1); + + // check user can get rewards after ghost epoch + + // create new epoch + let reward_coins = mint_default_coin(amount(150, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 150); + + // wait full epoch duration & check accum reward + timestamp::update_global_time_for_test_secs(START_TIME + duration + WEEK_IN_SECONDS * 200 + 150); + stake::recalculate_user_stake(@harvest, @alice); + let curr_epoch = stake::get_pool_current_epoch(@harvest); + let (_, _, accum_reward, _, last_updated, _, _, _) + = stake::get_epoch_info(@harvest, 2); + let reward_val = stake::get_pending_user_rewards(@harvest, @alice); + assert!(reward_val == amount(157680000, 0) + amount(150, 0), 1); + assert!(accum_reward == 1500000000000, 1); + assert!(last_updated == START_TIME + duration + WEEK_IN_SECONDS * 200 + 150, 1); + assert!(curr_epoch == 3, 1); + + let gain = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&gain) == amount(157680000, 0) + amount(150, 0), 1); + coin::deposit(@alice, gain); + } + + // tests with few users + + #[test] + public fun test_few_users_rewards_changing() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + let bob_acc = new_account_with_stake_coins(@bob, amount(100, 0)); + coin::register(&alice_acc); + coin::register(&bob_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(amount(1000, 0)); + let duration = 5_000_000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // stake 100 from alice + stake::stake(&alice_acc, @harvest, coin::withdraw(&alice_acc, amount(100, 0))); + + // wait half of first epoch + timestamp::update_global_time_for_test_secs(START_TIME + duration / 2); + + // stake 100 from bob + stake::stake(&bob_acc, @harvest, coin::withdraw(&bob_acc, amount(100, 0))); + + // wait till the end of first epoch + timestamp::update_global_time_for_test_secs(START_TIME + duration); + + // create new epoch + let reward_coins = mint_default_coin(amount(5000, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 500); + assert!(stake::get_pool_current_epoch(@harvest) == 2, 0); + + // wait half of second epoch + timestamp::update_global_time_for_test_secs(START_TIME + duration + 500 / 2); + + // unstake for alice + let coins = stake::unstake(&alice_acc, @harvest, amount(100, 0)); + coin::deposit(@alice, coins); + + // wait till the end of second epoch + timestamp::update_global_time_for_test_secs(START_TIME + duration + 500); + + let coins = stake::unstake(&bob_acc, @harvest, amount(100, 0)); + coin::deposit(@bob, coins); + + let rewards = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&rewards) == amount(2000, 0), 1); + coin::deposit(@alice, rewards); + + let rewards = stake::harvest(&bob_acc, @harvest); + assert!(coin::value(&rewards) == amount(4000, 0), 1); + coin::deposit(@bob, rewards); + + // check 0 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 0); + assert!(accum_reward == 7_500000000000, 1); + assert!(last_update_time == START_TIME + duration, 1); + assert!(end_time == START_TIME + duration, 1); + assert!(distributed == amount(1000, 0), 1); + assert!(ended_at == START_TIME + duration, 1); + + // check 1 (ghost) epoch fields + let (rewards_amount, reward_per_sec, accum_reward, _, _, _, _, _) + = stake::get_epoch_info(@harvest, 1); + assert!(rewards_amount == 0, 1); + assert!(reward_per_sec == 0, 1); + assert!(accum_reward == 0, 1); + + // check 2 epoch fields + let (_, _, accum_reward, _, last_update_time,end_time, distributed, ended_at) + = stake::get_epoch_info(@harvest, 2); + assert!(accum_reward == 37_500000000000, 1); + assert!(last_update_time == START_TIME + duration + 500, 1); + assert!(end_time == START_TIME + duration + 500, 1); + assert!(distributed == amount(5000, 0), 1); + assert!(ended_at == START_TIME + duration + 500, 1); + } + + #[test] + public fun test_few_rew_epochs_no_users_then_two_users() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, amount(100, 0)); + let bob_acc = new_account_with_stake_coins(@bob, amount(100, 0)); + coin::register(&alice_acc); + coin::register(&bob_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(amount(1000, 0)); + let duration = 500; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + // wait till the end of first epoch and a minute more + timestamp::update_global_time_for_test_secs(START_TIME + duration + 60); + + // create new epoch + let reward_coins = mint_default_coin(amount(1000, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 500); + assert!(stake::get_pool_current_epoch(@harvest) == 2, 0); + + // wait till the end of first epoch and a minute more + timestamp::update_global_time_for_test_secs(START_TIME + duration * 2 + 120); + + // create new epoch + let reward_coins = mint_default_coin(amount(1200, 0)); + stake::deposit_reward_coins(&alice_acc, @harvest, reward_coins, 6_000_000); + assert!(stake::get_pool_current_epoch(@harvest) == 4, 0); + + // wait 1/3 of third epoch + timestamp::update_global_time_for_test_secs(START_TIME + duration * 2 + 120 + 2_000_000); + + // stake 100 from alice + stake::stake(&alice_acc, @harvest, coin::withdraw(&alice_acc, amount(100, 0))); + // stake 100 from bob + stake::stake(&bob_acc, @harvest, coin::withdraw(&bob_acc, amount(100, 0))); + + // wait 2/3 of third epoch + timestamp::update_global_time_for_test_secs(START_TIME + duration * 2 + 120 + 4_000_000); + + let stake_coins = stake::unstake(&bob_acc, @harvest, amount(100, 0)); + coin::deposit(@bob, stake_coins); + + // wait till the end of third epoch and a week more + timestamp::update_global_time_for_test_secs(START_TIME + duration * 2 + 120 + 6_000_000 + WEEK_IN_SECONDS); + + let stake_coins = stake::unstake(&alice_acc, @harvest, amount(100, 0)); + coin::deposit(@alice, stake_coins); + + let alice_rewards = stake::harvest(&alice_acc, @harvest); + // as alice were in epoch only 2/3 of time and 1/3 with bob + // ((1200 / 3) * 0) + ((1200 / 3) * 0.5) + ((1200 / 3) * 1) = 600 + assert!(coin::value(&alice_rewards) == amount(600,0), 1); + coin::deposit(@alice, alice_rewards); + + let bob_rewards = stake::harvest(&bob_acc, @harvest); + // as bob were in epoch only with alice and 1/3 of epoch time + // ((1200 / 3) * 0) + ((1200 / 3) * 0.5) + ((1200 / 3) * 0) = 200 + assert!(coin::value(&bob_rewards) == amount(200,0), 1); + coin::deposit(@bob, bob_rewards); + } +} diff --git a/tests/whitelist_tests.move b/tests/whitelist_tests.move new file mode 100644 index 0000000..fff5e9e --- /dev/null +++ b/tests/whitelist_tests.move @@ -0,0 +1,349 @@ +#[test_only] +module harvest::whitelist_tests { + use std::option; + + use aptos_framework::coin; + use aptos_framework::genesis; + use aptos_framework::timestamp; + + use harvest::stake; + use harvest::stake_config; + use harvest::stake_test_helpers::{ + new_account, + initialize_reward_coin, + initialize_stake_coin, + mint_default_coin, + StakeCoin as S, + RewardCoin as R, + new_account_with_stake_coins + }; + + // week in seconds, lockup period + const WEEK_IN_SECONDS: u64 = 604800; + + const START_TIME: u64 = 682981200; + + public fun initialize_test(): (signer, signer) { + genesis::setup(); + + timestamp::update_global_time_for_test_secs(START_TIME); + + let harvest = new_account(@harvest); + + // create coins for pool to be valid + initialize_reward_coin(&harvest, 6); + initialize_stake_coin(&harvest, 6); + + let emergency_admin = new_account(@stake_emergency_admin); + stake_config::initialize(&emergency_admin, @treasury); + (harvest, emergency_admin) + } + + #[test] + public fun test_stake_with_whitelist_enabled() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 500000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@alice]); + + // check no stakes + assert!(!stake::stake_exists(@harvest, @alice), 1); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + + // check stake + assert!(stake::stake_exists(@harvest, @alice), 1); + } + + #[test] + public fun test_stake_two_users_with_whitelist_enabled() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 500000000); + let bob_acc = new_account_with_stake_coins(@bob, 500000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@alice, @bob]); + + // check no stakes + assert!(!stake::stake_exists(@harvest, @alice), 1); + assert!(!stake::stake_exists(@harvest, @bob), 1); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + + // stake 500 StakeCoins from bob + let coins = + coin::withdraw(&bob_acc, 500000000); + stake::stake(&bob_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @bob) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 1000000000, 1); + + // check stake + assert!(stake::stake_exists(@harvest, @alice), 1); + assert!(stake::stake_exists(@harvest, @bob), 1); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NOT_WHITELISTED)] + public fun test_stake_from_not_whitelisted_user_should_fail() { + let (harvest, _) = initialize_test(); + + let bob_acc = new_account_with_stake_coins(@bob, 500000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@alice]); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(!stake::is_whitelisted(@harvest, @bob), 1); + + // stake 500 StakeCoins from bob + let coins = + coin::withdraw(&bob_acc, 500000000); + stake::stake(&bob_acc, @harvest, coins); + } + + #[test] + public fun test_add_two_users_later_to_whitelist() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 500000000); + let bob_acc = new_account_with_stake_coins(@bob, 500000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@0x41, @0x42]); + + // check whitelist + assert!(!stake::is_whitelisted(@harvest, @alice), 1); + assert!(!stake::is_whitelisted(@harvest, @bob), 1); + + stake::add_into_whitelist(&harvest, vector[@alice, @bob]); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + + // check no stakes + assert!(!stake::stake_exists(@harvest, @alice), 1); + assert!(!stake::stake_exists(@harvest, @bob), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + + // stake 500 StakeCoins from bob + let coins = + coin::withdraw(&bob_acc, 500000000); + stake::stake(&bob_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @bob) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 1000000000, 1); + + // check stake + assert!(stake::stake_exists(@harvest, @alice), 1); + assert!(stake::stake_exists(@harvest, @bob), 1); + } + + #[test] + public fun test_user_can_harvest_and_unstake_after_been_removed_from_whitelist() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 500000000); + coin::register(&alice_acc); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@alice, @bob]); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + + // remove alice from whitelist + stake::remove_from_whitelist(&harvest, @alice); + + // check whitelist + assert!(!stake::is_whitelisted(@harvest, @alice), 1); + + // wait one week with empty pool + timestamp::update_global_time_for_test_secs(START_TIME + WEEK_IN_SECONDS); + + let coins = stake::unstake(&alice_acc, @harvest, 500000000); + assert!(coin::value(&coins) == 500000000, 1); + coin::deposit(@alice, coins); + + let rewards = stake::harvest(&alice_acc, @harvest); + assert!(coin::value(&rewards) > 0, 1); + coin::deposit(@alice, rewards); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NOT_WHITELISTED)] + public fun test_stake_from_removed_from_whitelist_user_should_fail() { + let (harvest, _) = initialize_test(); + + let bob_acc = new_account_with_stake_coins(@bob, 500000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@alice, @bob]); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + + // remove from whilelist + stake::remove_from_whitelist(&harvest, @bob); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(!stake::is_whitelisted(@harvest, @bob), 1); + + // stake 500 StakeCoins from bob + let coins = + coin::withdraw(&bob_acc, 500000000); + stake::stake(&bob_acc, @harvest, coins); + } + + #[test] + public fun test_whitelist_are_deactivated_when_no_users_left() { + let (harvest, _) = initialize_test(); + + let alice_acc = new_account_with_stake_coins(@alice, 500000000); + let bob_acc = new_account_with_stake_coins(@bob, 500000000); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[@bob]); + + // check whitelist + assert!(!stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + assert!(!stake::is_whitelisted(@harvest, @0x12345), 1); + + // remove bob, deactivate whitelist + stake::remove_from_whitelist(&harvest, @bob); + + // check whitelist + assert!(stake::is_whitelisted(@harvest, @alice), 1); + assert!(stake::is_whitelisted(@harvest, @bob), 1); + assert!(stake::is_whitelisted(@harvest, @0x12345), 1); + + // check no stakes + assert!(!stake::stake_exists(@harvest, @alice), 1); + assert!(!stake::stake_exists(@harvest, @bob), 1); + + // stake 500 StakeCoins from alice + let coins = + coin::withdraw(&alice_acc, 500000000); + stake::stake(&alice_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @alice) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 500000000, 1); + + // stake 500 StakeCoins from bob + let coins = + coin::withdraw(&bob_acc, 500000000); + stake::stake(&bob_acc, @harvest, coins); + assert!(stake::get_user_stake(@harvest, @bob) == 500000000, 1); + assert!(stake::get_pool_total_stake(@harvest) == 1000000000, 1); + + // check stake + assert!(stake::stake_exists(@harvest, @alice), 1); + assert!(stake::stake_exists(@harvest, @bob), 1); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NO_POOL)] + public fun test_attempt_to_add_to_whitelist_when_no_pool_should_fail() { + let (harvest, _) = initialize_test(); + stake::add_into_whitelist(&harvest, vector[@alice]); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NO_POOL)] + public fun test_attempt_to_remove_from_whitelist_when_no_pool_should_fail() { + let (harvest, _) = initialize_test(); + stake::remove_from_whitelist(&harvest, @alice); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NO_POOL)] + public fun test_check_whitelist_when_no_pool_should_fail() { + stake::is_whitelisted(@harvest, @alice); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NO_POOL)] + public fun test_attempt_to_add_to_whitelist_using_non_pool_owner_acc_should_fail() { + let (harvest, _) = initialize_test(); + let alice_acc = new_account(@alice); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + stake::add_into_whitelist(&alice_acc, vector[@alice]); + } + + #[test] + #[expected_failure(abort_code = stake::ERR_NO_POOL)] + public fun test_attempt_to_remove_from_whitelist_using_non_pool_owner_acc_should_fail() { + let (harvest, _) = initialize_test(); + let alice_acc = new_account(@alice); + + // register staking pool with rewards + let reward_coins = mint_default_coin(15768000000000); + let duration = 15768000; + stake::register_pool(&harvest, reward_coins, + duration, 0, option::none(), vector[]); + + stake::remove_from_whitelist(&alice_acc, @alice); + } +} \ No newline at end of file