diff --git a/Cargo.lock b/Cargo.lock index 98874be20f..6db19bedb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,6 +849,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "node-primitives 0.8.0", "orml-currencies", "orml-tokens 0.4.1-dev (git+https://github.com/open-web3-stack/open-runtime-module-library?rev=17a791edf431d7d7aee1ea3dfaeeb7bc21944301)", @@ -857,6 +858,7 @@ dependencies = [ "pallet-collective", "parity-scale-codec", "scale-info", + "serde", "sp-core", ] @@ -5195,7 +5197,7 @@ dependencies = [ [[package]] name = "node-cli" -version = "0.9.20" +version = "0.9.22" dependencies = [ "cumulus-client-cli", "cumulus-client-service", diff --git a/node/cli/Cargo.toml b/node/cli/Cargo.toml index a2129bbb65..dd7e3ed320 100644 --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node-cli" -version = "0.9.20" +version = "0.9.22" authors = ["Liebi Technologies "] description = "Bifrost Parachain Node" build = "build.rs" diff --git a/pallets/liquidity-mining/Cargo.toml b/pallets/liquidity-mining/Cargo.toml index fcdb7f558f..a3dd4a2e86 100644 --- a/pallets/liquidity-mining/Cargo.toml +++ b/pallets/liquidity-mining/Cargo.toml @@ -14,6 +14,8 @@ frame-support = { git = "https://github.com/paritytech/substrate", branch = "pol frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false, optional = true } node-primitives = { path = "../../node/primitives", default-features = false } orml-traits = { version = "0.4.1-dev", default-features = false } +log = { version = "0.4.14", default-features = false } +serde = { version = "1.0.126", optional = true, features = ["derive"] } [dev-dependencies] sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" } @@ -31,6 +33,8 @@ std = [ "frame-support/std", "node-primitives/std", "orml-traits/std", + "log/std", + "serde", ] runtime-benchmarks = [ @@ -41,4 +45,6 @@ runtime-benchmarks = [ local-benchmarks = [ "pallet-collective/runtime-benchmarks", -] \ No newline at end of file +] + +try-runtime = ["frame-support/try-runtime"] \ No newline at end of file diff --git a/pallets/liquidity-mining/src/benchmarking.rs b/pallets/liquidity-mining/src/benchmarking.rs index ba030f261e..d9afe1ba17 100644 --- a/pallets/liquidity-mining/src/benchmarking.rs +++ b/pallets/liquidity-mining/src/benchmarking.rs @@ -21,7 +21,11 @@ use frame_benchmarking::{ account, benchmarks_instance_pallet, impl_benchmark_test_suite, whitelisted_caller, }; -use frame_support::{assert_ok, sp_runtime::sp_std::convert::TryInto, sp_std::prelude::*}; +use frame_support::{ + assert_ok, + sp_runtime::{sp_std::convert::TryInto, traits::Zero}, + sp_std::prelude::*, +}; use frame_system::RawOrigin; use node_primitives::Balance; @@ -63,7 +67,9 @@ benchmarks_instance_pallet! { PoolType::Farming, duration, min_deposit_to_start, - 0u128.saturated_into() + 0u128.saturated_into(), + Zero::zero(), + 0, )); }: _(RawOrigin::Signed(caller.clone()), 0) @@ -87,7 +93,9 @@ benchmarks_instance_pallet! { PoolType::Farming, duration, min_deposit_to_start, - 0u128.saturated_into() + 0u128.saturated_into(), + Zero::zero(), + 0, )); assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); @@ -114,7 +122,9 @@ benchmarks_instance_pallet! { PoolType::Farming, duration, min_deposit_to_start, - 0u128.saturated_into() + 0u128.saturated_into(), + Zero::zero(), + 0, )); assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); @@ -152,7 +162,9 @@ benchmarks_instance_pallet! { PoolType::Farming, duration, min_deposit_to_start, - 0u128.saturated_into() + 0u128.saturated_into(), + Zero::zero(), + 0, )); assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); @@ -190,7 +202,9 @@ benchmarks_instance_pallet! { PoolType::Farming, duration, min_deposit_to_start, - 0u128.saturated_into() + 0u128.saturated_into(), + Zero::zero(), + 0, )); assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); @@ -230,7 +244,9 @@ benchmarks_instance_pallet! { PoolType::Farming, duration, min_deposit_to_start, - 0u128.saturated_into() + 0u128.saturated_into(), + Zero::zero(), + 0, )); assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); @@ -241,6 +257,97 @@ benchmarks_instance_pallet! { run_to_block::(1u128.saturated_into()); }: _(RawOrigin::Signed(caller.clone()), 0) + + unlock { + let duration = T::MinimumDuration::get().saturating_add(1u128.saturated_into()); + let min_deposit_to_start = T::MinimumDepositOfUser::get(); + let amount: BalanceOf = UNIT.saturated_into(); + let redeem_limit_time: BlockNumberFor = 100u32.saturated_into(); + let unlock_limit_nums = 3; + + let investor: T::AccountId = account("lm", 0, 0); + assert_ok!(T::MultiCurrency::deposit(REWARD_1, &investor, amount)); + assert_ok!(T::MultiCurrency::deposit(REWARD_2, &investor, amount)); + + let caller: T::AccountId = whitelisted_caller(); + assert_ok!(T::MultiCurrency::deposit(FARMING_DEPOSIT_1, &caller, amount)); + assert_ok!(T::MultiCurrency::deposit(FARMING_DEPOSIT_2, &caller, amount)); + + assert_ok!(LM::::create_pool( + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, amount), + vec![(REWARD_2, amount)].try_into().unwrap(), + PoolType::Farming, + duration, + min_deposit_to_start, + 0u128.saturated_into(), + redeem_limit_time, + unlock_limit_nums, + )); + + assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); + + assert_ok!(LM::::deposit(RawOrigin::Signed(caller.clone()).into(), 0, amount)); + + // Run to block + run_to_block::(duration); + + assert_ok!(LM::::redeem_all(RawOrigin::Signed(caller.clone()).into(), 0)); + + run_to_block::(duration.saturating_add(redeem_limit_time)); + + }: _(RawOrigin::Signed(caller.clone()), 0) + verify { + let pool = LM::::pool(0); + let deposit_data = LM::::user_deposit_data(0, caller.clone()); + assert!(pool.is_none()); + assert!(deposit_data.is_none()); + } + + cancel_unlock{ + let duration = T::MinimumDuration::get().saturating_add(1u128.saturated_into()); + let min_deposit_to_start = T::MinimumDepositOfUser::get(); + let amount: BalanceOf = UNIT.saturated_into(); + let redeem_limit_time: BlockNumberFor = 100u32.saturated_into(); + let unlock_limit_nums = 3; + + let investor: T::AccountId = account("lm", 0, 0); + assert_ok!(T::MultiCurrency::deposit(REWARD_1, &investor, amount)); + assert_ok!(T::MultiCurrency::deposit(REWARD_2, &investor, amount)); + + let caller: T::AccountId = whitelisted_caller(); + let double_amount = amount.saturating_mul(2u128.saturated_into()); + assert_ok!(T::MultiCurrency::deposit(FARMING_DEPOSIT_1, &caller, double_amount)); + assert_ok!(T::MultiCurrency::deposit(FARMING_DEPOSIT_2, &caller, double_amount)); + + assert_ok!(LM::::create_pool( + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, amount), + vec![(REWARD_2, amount)].try_into().unwrap(), + PoolType::Farming, + duration, + min_deposit_to_start, + 0u128.saturated_into(), + redeem_limit_time, + unlock_limit_nums, + )); + + assert_ok!(LM::::charge(RawOrigin::Signed(investor).into(), 0)); + + assert_ok!(LM::::deposit(RawOrigin::Signed(caller.clone()).into(), 0, double_amount)); + + // Run to block + run_to_block::(1u32.saturated_into()); + + assert_ok!(LM::::redeem(RawOrigin::Signed(caller.clone()).into(), 0, amount)); + + }: _(RawOrigin::Signed(caller.clone()), 0, 0) + verify { + let pool = LM::::pool(0); + let deposit_data = LM::::user_deposit_data(0, caller.clone()); + assert!(pool.unwrap().pending_unlock_nums == 0); + assert!(deposit_data.unwrap().pending_unlocks.len() == 0); + } } impl_benchmark_test_suite!(LM, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/pallets/liquidity-mining/src/lib.rs b/pallets/liquidity-mining/src/lib.rs index 340e907221..f11924d262 100644 --- a/pallets/liquidity-mining/src/lib.rs +++ b/pallets/liquidity-mining/src/lib.rs @@ -23,19 +23,22 @@ use frame_support::{ pallet_prelude::*, sp_runtime::{ traits::{ - AccountIdConversion, AtLeast32BitUnsigned, SaturatedConversion, Saturating, Zero, + AccountIdConversion, AtLeast32BitUnsigned, CheckedAdd, CheckedSub, SaturatedConversion, + Saturating, Zero, }, FixedPointNumber, FixedU128, }, sp_std::{ cmp::{max, min}, - collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + collections::{btree_map::BTreeMap, btree_set::BTreeSet, vec_deque::VecDeque}, convert::TryFrom, vec::Vec, }, traits::EnsureOrigin, - transactional, PalletId, + transactional, PalletId, RuntimeDebug, }; +#[cfg(feature = "std")] +use frame_support::{Deserialize, Serialize}; use frame_system::pallet_prelude::*; use node_primitives::{CurrencyId, CurrencyIdExt, LeasePeriod, ParaId, TokenInfo, TokenSymbol}; use orml_traits::{MultiCurrency, MultiLockableCurrency, MultiReservableCurrency}; @@ -48,6 +51,8 @@ mod benchmarking; mod mock; #[cfg(test)] mod tests; + +pub mod migration; pub mod weights; pub use weights::*; @@ -95,6 +100,13 @@ where block_startup: Option, /// The block number when the liquidity-pool retired block_retired: Option, + + /// The balance of redeeming will be locked util exceeding the limit time; + redeem_limit_time: BlockNumberOf, + /// The max number of pending-unlocks at the same time; + unlock_limit_nums: u32, + /// The number of pending-unlocks belong to the pool; + pending_unlock_nums: u32, } impl PoolInfo @@ -287,6 +299,13 @@ where /// - Arg1: The block number updated lastest gain_avgs: BTreeMap, update_b: BlockNumberOf, + /// (unlock_height, unlock_amount) + /// + /// unlock_height: When the block reaches the height, the balance was redeemed previously can + /// be unlocked; + /// + /// unlock_amount: The amount that can be unlocked after reaching the `unlock-height`; + pending_unlocks: VecDeque<(BlockNumberOf, BalanceOf)>, } impl DepositData @@ -303,7 +322,12 @@ where gain_avgs.insert(*rtoken, reward.gain_avg); } - Self { deposit: Zero::zero(), gain_avgs, update_b: pool.update_b } + Self { + deposit: Zero::zero(), + gain_avgs, + update_b: pool.update_b, + pending_unlocks: Default::default(), + } } } @@ -396,14 +420,29 @@ where } } +#[derive(Encode, Decode, Clone, Copy, RuntimeDebug, PartialEq, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum StorageVersion { + // Default + V1_0_0, + // After add time limit to `lm::redeem_*` + V2_0_0, +} + +impl Default for StorageVersion { + fn default() -> Self { + StorageVersion::V1_0_0 + } +} + #[allow(type_alias_bounds)] -type AccountIdOf = ::AccountId; +pub(crate) type AccountIdOf = ::AccountId; #[allow(type_alias_bounds)] -type BalanceOf, I: 'static = ()> = +pub(crate) type BalanceOf, I: 'static = ()> = <>::MultiCurrency as MultiCurrency>>::Balance; -type PoolId = u32; +pub(crate) type PoolId = u32; #[frame_support::pallet] pub mod pallet { @@ -474,6 +513,8 @@ pub mod pallet { NotEnoughToDeposit, /// Keeper doesn't have enough balance to be redeemed by the user(VERY SCARY ERR) NotEnoughToRedeem, + /// Keeper doesn't have enough balance to be unlocked by the user(VERY SCARY ERR) + NotEnoughToUnlock, /// User has nothing be deposited to the pool NoDepositOfUser, /// The balance which was tried to deposit to the pool less than `MinimumDepositOfUser` @@ -484,8 +525,17 @@ pub mod pallet { TooShortBetweenTwoClaim, /// The pool has been charged already PoolChargedAlready, + /// The number of pending-unlocks reaches the limit; + ExceedMaximumUnlock, + /// Not have pending-unlocks; + NoPendingUnlocks, + /// Input wrong index to `cancel_unlock`; + WrongIndex, /// __NOTE__: ERROR HAPPEN Unexpected, + /// On lazy-migration + OnMigration, + NoMigration, } #[pallet::event] @@ -511,14 +561,26 @@ pub mod pallet { /// /// [pool_id, pool_type, trading_pair] PoolRetiredForcefully(PoolId, PoolType, (CurrencyId, CurrencyId)), + /// The liquidity-pool was edited + /// + /// [pool_id, old_redeem_limit_time, old_unlock_limit_nums, new_redeem_limit_time, + /// new_unlock_limit_nums] + PoolEdited(PoolId, BlockNumberFor, u32, BlockNumberFor, u32), /// User deposited tokens to a liquidity-pool /// /// [pool_id, pool_type, trading_pair, amount_deposited, user] UserDeposited(PoolId, PoolType, (CurrencyId, CurrencyId), BalanceOf, AccountIdOf), /// User redeemed tokens from a liquidity-mining /// - /// [pool_id, pool_type, trading_pair, amount_redeemed, user] - UserRedeemed(PoolId, PoolType, (CurrencyId, CurrencyId), BalanceOf, AccountIdOf), + /// [pool_id, pool_type, trading_pair, amount_redeemed, unlock_height, user] + UserRedeemed( + PoolId, + PoolType, + (CurrencyId, CurrencyId), + BalanceOf, + BlockNumberFor, + AccountIdOf, + ), /// User withdrew the rewards whose deserved from a liquidity-pool /// /// [pool_id, pool_type, trading_pair, rewards, user] @@ -529,6 +591,24 @@ pub mod pallet { Vec<(CurrencyId, BalanceOf)>, AccountIdOf, ), + /// User unlock tokens from a liquidity-pool + /// + /// [pool_id, pool_type, trading_pair, amount_redeemed, user] + UserUnlocked(PoolId, PoolType, (CurrencyId, CurrencyId), BalanceOf, AccountIdOf), + /// User cancels a pending-unlock from a liquidity-pool + /// + /// [pool_id, pool_type, trading_pair, amount_canceled, user] + UserCancelUnlock( + PoolId, + PoolType, + (CurrencyId, CurrencyId), + BalanceOf, + AccountIdOf, + ), + /// Lazy migration event + /// + /// [deposit_data_migration_nums, pool_info_migration_nums] + LazyMigration(u32, u32), } #[pallet::storage] @@ -547,6 +627,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn pool)] + #[pallet::storage_prefix = "TotalPoolInfosV200"] pub(crate) type TotalPoolInfos, I: 'static = ()> = StorageMap< _, Twox64Concat, @@ -556,6 +637,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn user_deposit_data)] + #[pallet::storage_prefix = "TotalDepositDataV200"] pub(crate) type TotalDepositData, I: 'static = ()> = StorageDoubleMap< _, Blake2_128Concat, @@ -565,6 +647,31 @@ pub mod pallet { DepositData, BlockNumberFor>, >; + #[pallet::storage] + #[pallet::getter(fn storage_version)] + pub(crate) type PalletVersion, I: 'static = ()> = + StorageValue<_, StorageVersion, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig, I: 'static = ()> { + pub pallet_version: StorageVersion, + pub _phantom: PhantomData<(T, I)>, + } + + #[cfg(feature = "std")] + impl, I: 'static> Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { pallet_version: Default::default(), _phantom: PhantomData } + } + } + + #[pallet::genesis_build] + impl, I: 'static> GenesisBuild for GenesisConfig { + fn build(&self) { + PalletVersion::::put(self.pallet_version); + } + } + #[pallet::pallet] pub struct Pallet(PhantomData<(T, I)>); @@ -585,6 +692,8 @@ pub mod pallet { #[pallet::compact] duration: BlockNumberFor, #[pallet::compact] min_deposit_to_start: BalanceOf, #[pallet::compact] after_block_to_start: BlockNumberFor, + #[pallet::compact] redeem_limit_time: BlockNumberFor, + #[pallet::compact] unlock_limit_nums: u32, ) -> DispatchResultWithPostInfo { let _ = T::ControlOrigin::ensure_origin(origin)?; @@ -598,6 +707,8 @@ pub mod pallet { duration, min_deposit_to_start, after_block_to_start, + redeem_limit_time, + unlock_limit_nums, ) } @@ -616,6 +727,8 @@ pub mod pallet { #[pallet::compact] duration: BlockNumberFor, #[pallet::compact] min_deposit_to_start: BalanceOf, #[pallet::compact] after_block_to_start: BlockNumberFor, + #[pallet::compact] redeem_limit_time: BlockNumberFor, + #[pallet::compact] unlock_limit_nums: u32, ) -> DispatchResultWithPostInfo { let _ = T::ControlOrigin::ensure_origin(origin)?; @@ -636,6 +749,8 @@ pub mod pallet { duration, min_deposit_to_start, after_block_to_start, + redeem_limit_time, + unlock_limit_nums, ) } @@ -656,6 +771,8 @@ pub mod pallet { #[pallet::compact] duration: BlockNumberFor, #[pallet::compact] min_deposit_to_start: BalanceOf, #[pallet::compact] after_block_to_start: BlockNumberFor, + #[pallet::compact] redeem_limit_time: BlockNumberFor, + #[pallet::compact] unlock_limit_nums: u32, ) -> DispatchResultWithPostInfo { let _ = T::ControlOrigin::ensure_origin(origin)?; @@ -671,6 +788,8 @@ pub mod pallet { duration, min_deposit_to_start, after_block_to_start, + redeem_limit_time, + unlock_limit_nums, ) } @@ -706,6 +825,8 @@ pub mod pallet { duration, min_deposit_to_start, after_block_to_start, + Zero::zero(), + 0, ) } @@ -716,6 +837,8 @@ pub mod pallet { /// `PoolState::Charged`. #[pallet::weight(T::WeightInfo::charge())] pub fn charge(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + let investor = ensure_signed(origin)?; let num = Self::charged_pids().len() as u32; @@ -824,6 +947,40 @@ pub mod pallet { Ok(().into()) } + /// Edit the parameters of a liquidity-pool. + /// + /// __NOTE__: Forbid editing the liquidity-pool which type is `PoolType::EBFarming`; + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn edit_pool( + origin: OriginFor, + pid: PoolId, + redeem_limit_time: BlockNumberFor, + unlock_limit_nums: u32, + ) -> DispatchResultWithPostInfo { + let _ = T::ControlOrigin::ensure_origin(origin)?; + + let pool: PoolInfo<_, _, _> = Self::pool(pid).ok_or(Error::::InvalidPoolId)?; + + ensure!(pool.r#type != PoolType::EBFarming, Error::::InvalidPoolType); + + let pool_edited = PoolInfo { redeem_limit_time, unlock_limit_nums, ..pool }; + TotalPoolInfos::::insert(pid, pool_edited); + + Self::deposit_event(Event::PoolEdited( + pid, + pool.redeem_limit_time, + pool.unlock_limit_nums, + redeem_limit_time, + unlock_limit_nums, + )); + + Ok(().into()) + } + /// Caller deposits some token to a liquidity-pool. /// /// __NOTE__: The unclaimed rewards of caller will be withdrawn automatically if there has. @@ -839,6 +996,8 @@ pub mod pallet { pid: PoolId, value: BalanceOf, ) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + let user = ensure_signed(origin)?; let mut pool = Self::pool(pid) @@ -860,8 +1019,9 @@ pub mod pallet { pool.try_settle_and_transfer::(&mut deposit_data, user.clone())?; } - deposit_data.deposit = deposit_data.deposit.saturating_add(value); - pool.deposit = pool.deposit.saturating_add(value); + deposit_data.deposit = + deposit_data.deposit.checked_add(&value).ok_or(Error::::Unexpected)?; + pool.deposit = pool.deposit.checked_add(&value).ok_or(Error::::Unexpected)?; ensure!( pool.deposit <= T::MaximumDepositInPool::get(), Error::::ExceedMaximumDeposit @@ -939,6 +1099,8 @@ pub mod pallet { pid: PoolId, value: BalanceOf, ) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + if value == Zero::zero() { return Ok(().into()); } @@ -967,6 +1129,8 @@ pub mod pallet { #[transactional] #[pallet::weight(T::WeightInfo::redeem_all())] pub fn redeem_all(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + let user = ensure_signed(origin)?; Self::redeem_inner(user, pid, None) @@ -990,6 +1154,8 @@ pub mod pallet { pid: PoolId, account: Option>, ) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + let pool = Self::pool(pid).ok_or(Error::::InvalidPoolId)?.try_retire::(); ensure!(pool.state == PoolState::Retired, Error::::InvalidPoolState); @@ -1018,6 +1184,8 @@ pub mod pallet { #[transactional] #[pallet::weight(T::WeightInfo::claim())] pub fn claim(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + let user = ensure_signed(origin)?; let mut pool = Self::pool(pid) @@ -1038,6 +1206,264 @@ pub mod pallet { Ok(().into()) } + + /// Caller unlocks the locked deposit in the liquidity-pool. + /// + /// __NOTE__: The extrinsic will retire the pool, which is reached the end of life. + /// + /// The conditions to unlock: + /// - The pool type is not `PoolType::EBFarming`. + /// - There are pending-unlocks in the deposit_data. + /// - The current block-height exceeded the unlock-height; + #[transactional] + #[pallet::weight(T::WeightInfo::unlock())] + pub fn unlock(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + + let user = ensure_signed(origin)?; + + let mut pool: PoolInfo<_, _, _> = Self::pool(pid) + .ok_or(Error::::InvalidPoolId)? + .try_retire::() + .try_update::(); + + let mut deposit_data: DepositData<_, _> = + Self::user_deposit_data(pid, user.clone()).ok_or(Error::::NoDepositOfUser)?; + + let pending_len = deposit_data.pending_unlocks.len(); + + ensure!(pending_len > 0, Error::::NoPendingUnlocks); + + let unlock_all = pool.redeem_limit_time == Zero::zero() || pool.unlock_limit_nums == 0; + + let cur_height = frame_system::Pallet::::block_number(); + + let mut total_unlock_amount: BalanceOf = Zero::zero(); + for _ in 0..pending_len { + if let Some((unlock_height, unlock_amount)) = + deposit_data.pending_unlocks.pop_front() + { + if unlock_all || cur_height >= unlock_height { + match pool.r#type { + PoolType::Mining => { + let lpt = Self::convert_to_lptoken(pool.trading_pair)?; + + T::MultiCurrency::transfer(lpt, &pool.keeper, &user, unlock_amount) + .map_err(|_e| Error::::NotEnoughToUnlock)?; + + pool.pending_unlock_nums -= 1; + }, + PoolType::Farming => { + let (token_a, token_b) = pool.trading_pair; + + T::MultiCurrency::transfer( + token_a, + &pool.keeper, + &user, + unlock_amount, + ) + .map_err(|_e| Error::::NotEnoughToUnlock)?; + T::MultiCurrency::transfer( + token_b, + &pool.keeper, + &user, + unlock_amount, + ) + .map_err(|_e| Error::::NotEnoughToUnlock)?; + + pool.pending_unlock_nums -= 1; + }, + PoolType::EBFarming => {}, + PoolType::SingleToken => { + let token = pool.trading_pair.0; + + T::MultiCurrency::transfer( + token, + &pool.keeper, + &user, + unlock_amount, + ) + .map_err(|_e| Error::::NotEnoughToUnlock)?; + + pool.pending_unlock_nums -= 1; + }, + } + + total_unlock_amount = total_unlock_amount + .checked_add(&unlock_amount) + .ok_or(Error::::Unexpected)?; + } else { + deposit_data.pending_unlocks.push_back((unlock_height, unlock_amount)); + } + } + } + + let r#type = pool.r#type; + let trading_pair = pool.trading_pair; + + Self::post_process(pool, user.clone(), deposit_data)?; + + Self::deposit_event(Event::UserUnlocked( + pid, + r#type, + trading_pair, + total_unlock_amount, + user, + )); + + Ok(().into()) + } + + /// Caller cancels the specific pending-unlock. + /// + /// __NOTE__: The extrinsic will retire the pool, which is reached the end of life. + /// + /// The conditions to cancel: + /// - The pool state is `PoolState::Ongoing`. + /// - There is a `pending-unlock` that is specific by the parameter `index`; + #[transactional] + #[pallet::weight(T::WeightInfo::cancel_unlock())] + pub fn cancel_unlock( + origin: OriginFor, + pid: PoolId, + index: u32, + ) -> DispatchResultWithPostInfo { + ensure!(Self::storage_version() == StorageVersion::V2_0_0, Error::::OnMigration); + + let user = ensure_signed(origin)?; + + let mut pool: PoolInfo<_, _, _> = Self::pool(pid) + .ok_or(Error::::InvalidPoolId)? + .try_retire::() + .try_update::(); + + ensure!(pool.state == PoolState::Ongoing, Error::::InvalidPoolState); + + let mut deposit_data: DepositData<_, _> = + Self::user_deposit_data(pid, user.clone()).ok_or(Error::::NoDepositOfUser)?; + + ensure!(deposit_data.pending_unlocks.len() as u32 > index, Error::::WrongIndex); + + // redeposit + if let Some((_, unlock_amount)) = deposit_data.pending_unlocks.remove(index as usize) { + if pool.update_b != deposit_data.update_b { + pool.try_settle_and_transfer::(&mut deposit_data, user.clone())?; + } + + deposit_data.deposit = deposit_data + .deposit + .checked_add(&unlock_amount) + .ok_or(Error::::Unexpected)?; + pool.deposit = + pool.deposit.checked_add(&unlock_amount).ok_or(Error::::Unexpected)?; + ensure!( + pool.deposit <= T::MaximumDepositInPool::get(), + Error::::ExceedMaximumDeposit + ); + + pool.pending_unlock_nums -= 1; + + let r#type = pool.r#type; + let trading_pair = pool.trading_pair; + + Self::post_process(pool, user.clone(), deposit_data)?; + + Self::deposit_event(Event::UserCancelUnlock( + pid, + r#type, + trading_pair, + unlock_amount, + user, + )); + } + + Ok(().into()) + } + + #[pallet::weight(1_000_000)] + pub fn lazy_migration_v2_0_0( + _origin: OriginFor, + max_nums: u32, + ) -> DispatchResultWithPostInfo { + use migration::v2::deprecated::{ + TotalDepositData as TotalDepositDataV1_0_0, TotalPoolInfos as TotalPoolInfosV1_0_0, + }; + + ensure!(Self::storage_version() == StorageVersion::V1_0_0, Error::::NoMigration); + + let max_nums = min(200, max_nums); + + let (mut dd_nums, mut pi_nums) = (0, 0); + + if max_nums > 0 { + let mut left = max_nums as usize; + + let double_keys: Vec<(PoolId, AccountIdOf)> = + TotalDepositDataV1_0_0::::iter_keys().take(left).collect(); + + for (pid, user) in double_keys.iter() { + let dd_old = TotalDepositDataV1_0_0::::get(*pid, user.clone()) + .ok_or(Error::::Unexpected)?; + TotalDepositDataV1_0_0::::remove(*pid, user.clone()); + + let dd_new = DepositData { + deposit: dd_old.deposit, + gain_avgs: dd_old.gain_avgs, + update_b: dd_old.update_b, + pending_unlocks: Default::default(), + }; + + TotalDepositData::::insert(*pid, user.clone(), dd_new); + } + + left = left - double_keys.len(); + dd_nums = double_keys.len(); + + if left > 0 { + let keys: Vec = + TotalPoolInfosV1_0_0::::iter_keys().take(left).collect(); + + for pid in keys.iter() { + let pi_old = TotalPoolInfosV1_0_0::::get(*pid) + .ok_or(Error::::Unexpected)?; + TotalPoolInfosV1_0_0::::remove(*pid); + + let pi_new = PoolInfo { + pool_id: pi_old.pool_id, + keeper: pi_old.keeper, + investor: pi_old.investor, + trading_pair: pi_old.trading_pair, + duration: pi_old.duration, + r#type: pi_old.r#type, + min_deposit_to_start: pi_old.min_deposit_to_start, + after_block_to_start: pi_old.after_block_to_start, + deposit: pi_old.deposit, + rewards: pi_old.rewards, + update_b: pi_old.update_b, + state: pi_old.state, + block_startup: pi_old.block_startup, + block_retired: pi_old.block_retired, + redeem_limit_time: Zero::zero(), + unlock_limit_nums: 0, + pending_unlock_nums: 0, + }; + + TotalPoolInfos::::insert(*pid, pi_new); + } + + left = left - keys.len(); + pi_nums = keys.len(); + } + + if left > 0 { + PalletVersion::::put(StorageVersion::V2_0_0); + } + } + + Self::deposit_event(Event::LazyMigration(dd_nums as u32, pi_nums as u32)); + + Ok(().into()) + } } impl, I: 'static> Pallet { @@ -1049,6 +1475,8 @@ pub mod pallet { duration: BlockNumberFor, min_deposit_to_start: BalanceOf, after_block_to_start: BlockNumberFor, + redeem_limit_time: BlockNumberFor, + unlock_limit_nums: u32, ) -> DispatchResultWithPostInfo { // Check the trading-pair if r#type != PoolType::SingleToken { @@ -1103,6 +1531,10 @@ pub mod pallet { state: PoolState::UnCharged, block_startup: None, block_retired: None, + + redeem_limit_time, + unlock_limit_nums, + pending_unlock_nums: 0, }; TotalPoolInfos::::insert(pool_id, mining_pool); @@ -1117,7 +1549,7 @@ pub mod pallet { pid: PoolId, value: Option>, ) -> DispatchResultWithPostInfo { - let mut pool = Self::pool(pid) + let mut pool: PoolInfo<_, _, _> = Self::pool(pid) .ok_or(Error::::InvalidPoolId)? .try_retire::() .try_update::(); @@ -1127,7 +1559,7 @@ pub mod pallet { Error::::InvalidPoolState ); - let mut deposit_data = + let mut deposit_data: DepositData<_, _> = Self::user_deposit_data(pid, user.clone()).ok_or(Error::::NoDepositOfUser)?; if pool.update_b != deposit_data.update_b { @@ -1141,7 +1573,8 @@ pub mod pallet { _ => return Err(Error::::Unexpected.into()), }; - let pool_can_redeem = pool.deposit.saturating_sub(minimum_in_pool); + let pool_can_redeem = + pool.deposit.checked_sub(&minimum_in_pool).ok_or(Error::::Unexpected)?; let user_can_redeem = min(deposit_data.deposit, pool_can_redeem); let try_redeem = match value { @@ -1156,20 +1589,46 @@ pub mod pallet { Error::::TooLowToRedeem ); - pool.deposit = pool.deposit.saturating_sub(try_redeem); - deposit_data.deposit = deposit_data.deposit.saturating_sub(try_redeem); + pool.deposit = + pool.deposit.checked_sub(&try_redeem).ok_or(Error::::Unexpected)?; + deposit_data.deposit = + deposit_data.deposit.checked_sub(&try_redeem).ok_or(Error::::Unexpected)?; // To unlock the deposit + let cur_height = frame_system::Pallet::::block_number(); + let unlock_height = if pool.redeem_limit_time == Zero::zero() || + pool.unlock_limit_nums == 0 || + pool.r#type == PoolType::EBFarming + { + cur_height + } else { + cur_height + .checked_add(&pool.redeem_limit_time) + .ok_or(Error::::Unexpected)? + }; + + ensure!( + cur_height == unlock_height || + pool.unlock_limit_nums > deposit_data.pending_unlocks.len() as u32, + Error::::ExceedMaximumUnlock + ); + match pool.r#type { PoolType::Mining => { let lpt = Self::convert_to_lptoken(pool.trading_pair)?; let lpt_ed = T::MultiCurrency::minimum_balance(lpt); - let lpt_total = - T::MultiCurrency::total_balance(lpt, &user).saturating_add(try_redeem); + let lpt_total = T::MultiCurrency::total_balance(lpt, &user) + .checked_add(&try_redeem) + .ok_or(Error::::Unexpected)?; if lpt_total >= lpt_ed { - T::MultiCurrency::transfer(lpt, &pool.keeper, &user, try_redeem) - .map_err(|_e| Error::::NotEnoughToRedeem)?; + if cur_height == unlock_height { + T::MultiCurrency::transfer(lpt, &pool.keeper, &user, try_redeem) + .map_err(|_e| Error::::NotEnoughToRedeem)?; + } else { + deposit_data.pending_unlocks.push_back((unlock_height, try_redeem)); + pool.pending_unlock_nums += 1; + } } }, PoolType::Farming => { @@ -1178,53 +1637,89 @@ pub mod pallet { let ta_ed = T::MultiCurrency::minimum_balance(token_a); let tb_ed = T::MultiCurrency::minimum_balance(token_b); - let ta_total = - T::MultiCurrency::total_balance(token_a, &user).saturating_add(try_redeem); - let tb_total = - T::MultiCurrency::total_balance(token_b, &user).saturating_add(try_redeem); - - if ta_total > ta_ed { - T::MultiCurrency::transfer(token_a, &pool.keeper, &user, try_redeem) - .map_err(|_e| Error::::NotEnoughToRedeem)?; - } - if tb_total > tb_ed { - T::MultiCurrency::transfer(token_b, &pool.keeper, &user, try_redeem) - .map_err(|_e| Error::::NotEnoughToRedeem)?; + let ta_total = T::MultiCurrency::total_balance(token_a, &user) + .checked_add(&try_redeem) + .ok_or(Error::::Unexpected)?; + let tb_total = T::MultiCurrency::total_balance(token_b, &user) + .checked_add(&try_redeem) + .ok_or(Error::::Unexpected)?; + + if ta_total >= ta_ed && tb_total >= tb_ed { + if cur_height == unlock_height { + T::MultiCurrency::transfer(token_a, &pool.keeper, &user, try_redeem) + .map_err(|_e| Error::::NotEnoughToRedeem)?; + T::MultiCurrency::transfer(token_b, &pool.keeper, &user, try_redeem) + .map_err(|_e| Error::::NotEnoughToRedeem)?; + } else { + deposit_data.pending_unlocks.push_back((unlock_height, try_redeem)); + pool.pending_unlock_nums += 1; + } } }, PoolType::EBFarming => {}, PoolType::SingleToken => { let token = pool.trading_pair.0; let token_ed = T::MultiCurrency::minimum_balance(token); - let token_total = - T::MultiCurrency::total_balance(token, &user).saturating_add(try_redeem); + let token_total = T::MultiCurrency::total_balance(token, &user) + .checked_add(&try_redeem) + .ok_or(Error::::Unexpected)?; if token_total >= token_ed { - T::MultiCurrency::transfer(token, &pool.keeper, &user, try_redeem) - .map_err(|_e| Error::::NotEnoughToRedeem)?; + if cur_height == unlock_height { + T::MultiCurrency::transfer(token, &pool.keeper, &user, try_redeem) + .map_err(|_e| Error::::NotEnoughToRedeem)?; + } else { + deposit_data.pending_unlocks.push_back((unlock_height, try_redeem)); + pool.pending_unlock_nums += 1; + } } }, }; - if pool.state == PoolState::Retired && pool.deposit == Zero::zero() { - pool.try_withdraw_remain::()?; - pool.state = PoolState::Dead; - } - let r#type = pool.r#type; let trading_pair = pool.trading_pair; - match pool.deposit.saturated_into() { - 0u128 => TotalPoolInfos::::remove(pid), - _ => TotalPoolInfos::::insert(pid, pool), - } + Self::post_process(pool, user.clone(), deposit_data)?; + + Self::deposit_event(Event::UserRedeemed( + pid, + r#type, + trading_pair, + try_redeem, + unlock_height, + user, + )); - match deposit_data.deposit.saturated_into() { - 0u128 => TotalDepositData::::remove(pid, user.clone()), - _ => TotalDepositData::::insert(pid, user.clone(), deposit_data), + Ok(().into()) + } + + /// Delete the pool from storage if there is no reason to exist; Otherwise save it back to + /// storage. + /// + /// Same as the deposit_data; + pub(crate) fn post_process( + mut pool: PoolInfo, BalanceOf, BlockNumberFor>, + user: AccountIdOf, + deposit_data: DepositData, BlockNumberFor>, + ) -> DispatchResult { + let pid = pool.pool_id; + + if pool.state == PoolState::Retired && + pool.deposit == Zero::zero() && + pool.pending_unlock_nums == 0 + { + pool.try_withdraw_remain::()?; + pool.state = PoolState::Dead; + TotalPoolInfos::::remove(pid); + } else { + TotalPoolInfos::::insert(pid, pool); } - Self::deposit_event(Event::UserRedeemed(pid, r#type, trading_pair, try_redeem, user)); + if deposit_data.deposit == Zero::zero() && deposit_data.pending_unlocks.len() == 0 { + TotalDepositData::::remove(pid, user.clone()); + } else { + TotalDepositData::::insert(pid, user.clone(), deposit_data); + } Ok(().into()) } diff --git a/pallets/liquidity-mining/src/migration.rs b/pallets/liquidity-mining/src/migration.rs new file mode 100644 index 0000000000..13d33d1920 --- /dev/null +++ b/pallets/liquidity-mining/src/migration.rs @@ -0,0 +1,261 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod v2 { + use frame_support::traits::{OnRuntimeUpgrade, PalletInfo}; + + use crate::*; + + type PoolInfoOld = + deprecated::PoolInfo, BalanceOf, BlockNumberFor>; + type PoolInfoNew = PoolInfo, BalanceOf, BlockNumberFor>; + type DepositDataOld = deprecated::DepositData, BlockNumberFor>; + type DepositDataNew = DepositData, BlockNumberFor>; + + pub(crate) mod deprecated { + use super::PalletInfo; + use crate::*; + + #[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] + pub(crate) struct PoolInfo + where + AccountIdOf: Clone, + BalanceOf: AtLeast32BitUnsigned + Copy, + BlockNumberOf: AtLeast32BitUnsigned + Copy, + { + /// Id of the liquidity-pool + pub(crate) pool_id: PoolId, + /// The keeper of the liquidity-pool + pub(crate) keeper: AccountIdOf, + /// The man who charges the rewards to the pool + pub(crate) investor: Option, + /// The trading-pair supported by the liquidity-pool + pub(crate) trading_pair: (CurrencyId, CurrencyId), + /// The length of time the liquidity-pool releases rewards + pub(crate) duration: BlockNumberOf, + /// The liquidity-pool type + pub(crate) r#type: PoolType, + + /// The First Condition + /// + /// When starts the liquidity-pool, the amount deposited in the liquidity-pool + /// should be greater than the value. + pub(crate) min_deposit_to_start: BalanceOf, + /// The Second Condition + /// + /// When starts the liquidity-pool, the current block should be greater than the value. + pub(crate) after_block_to_start: BlockNumberOf, + + /// The total amount deposited in the liquidity-pool + pub(crate) deposit: BalanceOf, + + /// The reward infos about the liquidity-pool + pub(crate) rewards: BTreeMap>, + /// The block of the last update of the rewards + pub(crate) update_b: BlockNumberOf, + /// The liquidity-pool state + pub(crate) state: PoolState, + /// The block number when the liquidity-pool startup + pub(crate) block_startup: Option, + /// The block number when the liquidity-pool retired + pub(crate) block_retired: Option, + } + + #[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] + pub(crate) struct DepositData + where + BalanceOf: AtLeast32BitUnsigned + Copy, + BlockNumberOf: AtLeast32BitUnsigned + Copy, + { + /// The amount of trading-pair deposited in the liquidity-pool + pub(crate) deposit: BalanceOf, + /// The average gain in pico by 1 pico deposited from the startup of the + /// liquidity-pool, updated when the `DepositData`'s owner deposits/redeems/claims from + /// the liquidity-pool. + /// + /// - Arg0: The average gain in pico by 1 pico deposited from the startup of the + /// liquidity-pool + /// - Arg1: The block number updated lastest + pub(crate) gain_avgs: BTreeMap, + pub(crate) update_b: BlockNumberOf, + } + + pub(crate) struct TotalPoolInfosPrefix(PhantomData<(T, I)>); + impl, I: 'static> frame_support::traits::StorageInstance + for TotalPoolInfosPrefix + { + fn pallet_prefix() -> &'static str { + T::PalletInfo::name::>().unwrap_or("none") + } + const STORAGE_PREFIX: &'static str = "TotalPoolInfos"; + } + + pub(crate) struct TotalDepositDataPrefix(PhantomData<(T, I)>); + impl, I: 'static> frame_support::traits::StorageInstance + for TotalDepositDataPrefix + { + fn pallet_prefix() -> &'static str { + T::PalletInfo::name::>().unwrap_or("none") + } + const STORAGE_PREFIX: &'static str = "TotalDepositData"; + } + + #[allow(type_alias_bounds)] + pub(crate) type TotalPoolInfos, I: 'static = ()> = StorageMap< + TotalPoolInfosPrefix, + Twox64Concat, + PoolId, + self::PoolInfo, BalanceOf, BlockNumberFor>, + >; + + #[allow(type_alias_bounds)] + pub(crate) type TotalDepositData, I: 'static = ()> = StorageDoubleMap< + TotalDepositDataPrefix, + Blake2_128Concat, + PoolId, + Blake2_128Concat, + AccountIdOf, + self::DepositData, BlockNumberFor>, + >; + } + + pub struct Upgrade(PhantomData<(T, I)>); + + impl, I: 'static> OnRuntimeUpgrade for Upgrade { + fn on_runtime_upgrade() -> Weight { + let pallet_name = T::PalletInfo::name::>().unwrap_or("none"); + log::info!("{} on processing", pallet_name); + + if Pallet::::storage_version() == StorageVersion::V1_0_0 { + let tp_nums = deprecated::TotalPoolInfos::::iter().count() as u32; + let td_nums = deprecated::TotalDepositData::::iter().count() as u32; + + log::info!(" >>> update `PoolInfo` storage: Migrating {} pool", tp_nums); + + TotalPoolInfos::::translate::, _>(|id, pool| { + log::info!(" migrated pool-info for {}", id); + + Some(PoolInfoNew:: { + pool_id: pool.pool_id, + keeper: pool.keeper, + investor: pool.investor, + trading_pair: pool.trading_pair, + duration: pool.duration, + r#type: pool.r#type, + + min_deposit_to_start: pool.min_deposit_to_start, + after_block_to_start: pool.after_block_to_start, + + deposit: pool.deposit, + + rewards: pool.rewards, + update_b: pool.update_b, + state: pool.state, + block_startup: pool.block_startup, + block_retired: pool.block_retired, + + redeem_limit_time: Zero::zero(), + unlock_limit_nums: 0, + pending_unlock_nums: 0, + }) + }); + + log::info!(" >>> update `DepositData` storage: Migrating {} user", td_nums); + + TotalDepositData::::translate::, _>(|id, user, data| { + log::info!(" migrated deposit-data for {}: {:?}", id, user); + + Some(DepositDataNew:: { + deposit: data.deposit, + gain_avgs: data.gain_avgs, + update_b: data.update_b, + + pending_unlocks: Default::default(), + }) + }); + + PalletVersion::::put(StorageVersion::V2_0_0); + + log::info!(" >>> migration completed!"); + + let total_nums = tp_nums + td_nums; + T::DbWeight::get().reads_writes(total_nums as Weight + 1, total_nums as Weight + 1) + } else { + log::info!(" >>> unused migration!"); + 0 + } + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<(), &'static str> { + use frame_support::traits::OnRuntimeUpgradeHelpersExt; + + let pallet_name = T::PalletInfo::name::>().unwrap_or("none"); + + ensure!( + Pallet::::storage_version() == StorageVersion::V1_0_0, + "❌ liquidity-mining upgrade to V2_0_0: not right version", + ); + + let tp_nums_old = deprecated::TotalPoolInfos::::iter().count() as u32; + let td_nums_old = deprecated::TotalDepositData::::iter().count() as u32; + Self::set_temp_storage((tp_nums_old, td_nums_old), pallet_name); + + log::info!( + "✅ liquidity-mining({}) upgrade to V2_0_0: pass PRE migrate checks", + pallet_name + ); + + Ok(()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + use frame_support::traits::OnRuntimeUpgradeHelpersExt; + + let pallet_name = T::PalletInfo::name::>().unwrap_or("none"); + + let (tp_nums_old, td_nums_old) = + Self::get_temp_storage::<(u32, u32)>(pallet_name).unwrap(); + let (tp_nums_new, td_nums_new) = ( + TotalPoolInfos::::iter().count() as u32, + TotalDepositData::::iter().count() as u32, + ); + + ensure!( + tp_nums_old == tp_nums_new, + "❌ liquidity-mining upgrade to V2_0_0: pool quantity does not match" + ); + + ensure!( + td_nums_old == td_nums_new, + "❌ liquidity-mining upgrade to V2_0_0: user quantity does not match" + ); + + log::info!( + "✅ liquidity-mining({}) upgrade to V2_0_0: pass POST migrate checks", + pallet_name + ); + + Ok(()) + } + } +} diff --git a/pallets/liquidity-mining/src/mock.rs b/pallets/liquidity-mining/src/mock.rs index e09cb8f7d1..0ec9562f57 100644 --- a/pallets/liquidity-mining/src/mock.rs +++ b/pallets/liquidity-mining/src/mock.rs @@ -31,7 +31,7 @@ use node_primitives::{traits::CheckSubAccount, Amount, Balance, CurrencyId, Toke use sp_core::H256; use crate as lm; -use crate::PoolId; +use crate::{PoolId, StorageVersion}; pub(crate) type AccountId = <::Signer as IdentifyAccount>::AccountId; pub(crate) type Block = frame_system::mocking::MockBlock; @@ -51,7 +51,7 @@ construct_runtime!( Currencies: orml_currencies::{Pallet, Call, Event}, Tokens: orml_tokens::{Pallet, Call, Storage, Event, Config}, Collective: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config}, - LM: lm::{Pallet, Call, Storage, Event}, + LM: lm::{Pallet, Call, Storage, Event, Config}, } ); @@ -227,6 +227,10 @@ pub(crate) fn new_test_ext() -> TestExternalities { members: vec![TC_MEMBER_1, TC_MEMBER_2, TC_MEMBER_3], phantom: Default::default(), }, + lm: crate::GenesisConfig { + pallet_version: StorageVersion::V2_0_0, + _phantom: Default::default(), + }, } .build_storage() .unwrap() @@ -237,6 +241,9 @@ pub(crate) const MINUTES: BlockNumber = 60 / (12 as BlockNumber); pub(crate) const HOURS: BlockNumber = MINUTES * 60; pub(crate) const DAYS: BlockNumber = HOURS * 24; +pub(crate) const REDEEM_LIMIT_TIME: BlockNumber = 100; +pub(crate) const UNLOCK_LIMIT_NUMS: u32 = 3; + pub(crate) const UNIT: Balance = 1_000_000_000_000; pub(crate) const MINING_TRADING_PAIR: (CurrencyId, CurrencyId) = diff --git a/pallets/liquidity-mining/src/tests.rs b/pallets/liquidity-mining/src/tests.rs index cd565d20e1..57d33db1e0 100644 --- a/pallets/liquidity-mining/src/tests.rs +++ b/pallets/liquidity-mining/src/tests.rs @@ -21,7 +21,7 @@ use std::convert::TryInto; use frame_support::{ assert_noop, assert_ok, dispatch::DispatchError, - sp_runtime::{FixedPointNumber, FixedU128}, + sp_runtime::{traits::Zero, FixedPointNumber, FixedU128}, traits::Hooks, }; use frame_system::pallet_prelude::OriginFor; @@ -55,7 +55,9 @@ fn create_farming_pool_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(0).unwrap(); @@ -82,7 +84,9 @@ fn create_mining_pool_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(0).unwrap(); @@ -114,6 +118,8 @@ fn create_mining_pool_with_wrong_currency_should_fail() { DAYS, 1_000 * UNIT, 0, + Zero::zero(), + 0, ), Error::::InvalidTradingPair, ); @@ -130,6 +136,8 @@ fn create_mining_pool_with_wrong_currency_should_fail() { DAYS, 1_000 * UNIT, 0, + Zero::zero(), + 0, ), Error::::InvalidTradingPair, ); @@ -148,7 +156,9 @@ fn increase_pid_when_create_pool_should_work() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(pid).unwrap(); assert_eq!(pool.pool_id, pid); @@ -171,7 +181,9 @@ fn create_pool_with_wrong_origin_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, ), DispatchError::BadOrigin, ); @@ -186,7 +198,9 @@ fn create_pool_with_wrong_origin_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, ), DispatchError::BadOrigin ); @@ -201,7 +215,7 @@ fn create_pool_with_wrong_origin_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, ), DispatchError::BadOrigin ); @@ -220,7 +234,9 @@ fn create_pool_with_duplicate_trading_pair_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, ), Error::::InvalidTradingPair, ); @@ -238,7 +254,9 @@ fn create_pool_with_too_small_duration_should_fail() { PoolType::Farming, MinimumDuration::get() - 1, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, ), Error::::InvalidDuration, ); @@ -256,7 +274,9 @@ fn create_pool_with_wrong_condition_should_fail() { PoolType::Farming, DAYS, MinimumDeposit::get() - 1, - 0 + 0, + Zero::zero(), + 0, ), Error::::InvalidDepositLimit, ); @@ -269,7 +289,9 @@ fn create_pool_with_wrong_condition_should_fail() { PoolType::Farming, DAYS, MaximumDepositInPool::get() + 1, - 0 + 0, + Zero::zero(), + 0, ), Error::::InvalidDepositLimit, ); @@ -287,7 +309,9 @@ fn create_pool_with_too_small_per_block_should_fail() { PoolType::Farming, (REWARD_AMOUNT + 1) as BlockNumber, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, ), Error::::InvalidRewardPerBlock, ); @@ -305,6 +329,8 @@ fn create_pool_with_duplicate_reward_should_fail() { DAYS, 1_000 * UNIT, 0, + Zero::zero(), + 0, ); assert_noop!(result, Error::::DuplicateReward,); }); @@ -320,7 +346,9 @@ fn charge_should_work() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(0).unwrap(); @@ -365,7 +393,9 @@ fn charge_with_wrong_origin_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!( @@ -387,7 +417,9 @@ fn charge_with_wrong_state_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); @@ -408,7 +440,9 @@ fn charge_exceed_maximum_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_ok!(LM::charge(Some(INVESTOR).into(), i)); @@ -425,7 +459,9 @@ fn charge_exceed_maximum_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!( @@ -447,7 +483,9 @@ fn charge_without_enough_balance_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(0).unwrap(); @@ -469,7 +507,9 @@ fn kill_pool_should_work() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(0).unwrap(); @@ -491,7 +531,9 @@ fn kill_pool_with_wrong_origin_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!(LM::kill_pool(Some(USER_1).into(), 0), DispatchError::BadOrigin); @@ -510,7 +552,9 @@ fn kill_pool_with_wrong_state_should_fail() { PoolType::Farming, DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); @@ -532,7 +576,9 @@ fn deposit_to_mining_pool_charged_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -570,7 +616,9 @@ fn deposit_to_farming_pool_charged_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -614,7 +662,9 @@ fn startup_pool_meet_conditions_should_auto_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -643,7 +693,9 @@ fn deposit_to_pool_ongoing_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -683,7 +735,9 @@ fn deposit_to_pool_ongoing_with_init_deposit_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -734,7 +788,9 @@ fn double_deposit_to_pool_ongoing_in_diff_block_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -788,7 +844,9 @@ fn double_deposit_to_pool_ongoing_in_same_block_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -834,7 +892,9 @@ fn deposit_with_wrong_pid_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -856,7 +916,9 @@ fn deposit_with_wrong_state_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!(LM::deposit(Some(USER_1).into(), 0, 1_000_000), Error::::InvalidPoolState); @@ -884,7 +946,9 @@ fn deposit_too_little_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -909,7 +973,9 @@ fn deposit_with_wrong_origin_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -932,7 +998,9 @@ fn deposit_exceed_the_limit_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -956,7 +1024,9 @@ fn redeem_from_pool_ongoing_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let keeper = LM::pool(0).unwrap().keeper; @@ -1069,7 +1139,9 @@ fn redeem_from_pool_retired_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1150,7 +1222,9 @@ fn double_redeem_from_pool_in_diff_state_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1271,7 +1345,9 @@ fn redeem_with_wrong_pid_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1295,7 +1371,9 @@ fn redeem_with_wrong_state_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!(LM::redeem_all(Some(USER_1).into(), 0), Error::::InvalidPoolState); @@ -1318,7 +1396,9 @@ fn redeem_without_deposit_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!(LM::redeem_all(Some(USER_1).into(), 0), Error::::InvalidPoolState); @@ -1345,7 +1425,9 @@ fn redeem_all_deposit_from_pool_ongoing_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!(LM::redeem_all(Some(USER_1).into(), 0), Error::::InvalidPoolState); @@ -1372,7 +1454,9 @@ fn redeem_some_more_than_user_can_redeem_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1401,7 +1485,9 @@ fn volunteer_to_redeem_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1467,7 +1553,9 @@ fn volunteer_to_redeem_with_wrong_pid_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1491,7 +1579,9 @@ fn volunteer_to_redeem_with_wrong_state_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!( @@ -1528,7 +1618,9 @@ fn claim_from_pool_ongoing_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1579,7 +1671,9 @@ fn claim_from_pool_retired_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1604,7 +1698,9 @@ fn claim_with_wrong_pid_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1629,7 +1725,9 @@ fn claim_with_wrong_origin_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1655,7 +1753,9 @@ fn claim_with_wrong_state_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!(LM::claim(Some(USER_1).into(), 0), Error::::InvalidPoolState); @@ -1684,7 +1784,9 @@ fn claim_without_deposit_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1709,7 +1811,9 @@ fn double_claim_in_same_block_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1734,7 +1838,9 @@ fn force_retire_pool_charged_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1804,7 +1910,9 @@ fn force_retire_pool_charged_without_deposit_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1851,7 +1959,9 @@ fn force_retire_pool_ongoing_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1934,7 +2044,9 @@ fn force_retire_pool_with_wrong_origin_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -1956,7 +2068,9 @@ fn force_retire_pool_with_wrong_pool_state_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); assert_noop!( @@ -1989,7 +2103,7 @@ fn create_eb_farming_pool_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, )); let pool = LM::pool(0).unwrap(); @@ -2017,7 +2131,7 @@ fn deposit_to_eb_farming_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2068,7 +2182,7 @@ fn redeem_from_eb_farming_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2136,7 +2250,7 @@ fn claim_from_eb_farming_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2200,7 +2314,9 @@ fn create_single_token_pool_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let pool = LM::pool(0).unwrap(); @@ -2226,7 +2342,9 @@ fn deposit_to_single_token_pool_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2265,7 +2383,9 @@ fn redeem_from_single_token_pool_ongoing_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); let keeper = LM::pool(0).unwrap().keeper; @@ -2381,7 +2501,9 @@ fn redeem_from_single_token_pool_retired_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2462,7 +2584,9 @@ fn claim_from_single_token_pool_ongoing_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2504,7 +2628,7 @@ fn claim_from_single_token_pool_ongoing_should_work() { } #[test] -fn claim_from_single_tokenpool_retired_should_fail() { +fn claim_from_single_token_pool_retired_should_fail() { new_test_ext().execute_with(|| { assert_ok!(LM::create_single_token_pool( pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), @@ -2513,7 +2637,9 @@ fn claim_from_single_tokenpool_retired_should_fail() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2543,7 +2669,7 @@ fn discard_reward_lower_than_ed_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1_000_000, - 0 + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2602,7 +2728,9 @@ fn discard_deposit_lower_than_ed_should_work() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + Zero::zero(), + 0, )); // It is unable to call Collective::execute(..) which is private; @@ -2648,10 +2776,8 @@ fn discard_deposit_lower_than_ed_should_work() { } #[test] -fn simple_integration_test() { +fn unlock_from_mining_pool_should_work() { new_test_ext().execute_with(|| { - const PER_BLOCK: Balance = REWARD_AMOUNT / DAYS as Balance; - assert_ok!(LM::create_mining_pool( pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), MINING_TRADING_PAIR, @@ -2659,80 +2785,918 @@ fn simple_integration_test() { vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), DAYS, 1 * UNIT, - 0 + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, )); + let keeper = LM::pool(0).unwrap().keeper; + // It is unable to call Collective::execute(..) which is private; assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); - let pool = LM::pool(0).unwrap(); - let keeper = pool.keeper.clone(); - let kept = PER_BLOCK * DAYS as Balance; - - assert_eq!(Tokens::accounts(INVESTOR, REWARD_1).free, REWARD_AMOUNT - kept); - assert_eq!(Tokens::accounts(INVESTOR, REWARD_1).frozen, 0); - assert_eq!(Tokens::accounts(INVESTOR, REWARD_1).reserved, 0); - assert_eq!(Tokens::accounts(INVESTOR, REWARD_2).free, REWARD_AMOUNT - kept); - assert_eq!(Tokens::accounts(INVESTOR, REWARD_2).frozen, 0); - assert_eq!(Tokens::accounts(INVESTOR, REWARD_2).reserved, 0); - - assert_eq!(Tokens::accounts(keeper.clone(), REWARD_1).free, kept); - assert_eq!(Tokens::accounts(keeper.clone(), REWARD_1).frozen, 0); - assert_eq!(Tokens::accounts(keeper.clone(), REWARD_1).reserved, 0); - assert_eq!(Tokens::accounts(keeper.clone(), REWARD_2).free, kept); - assert_eq!(Tokens::accounts(keeper.clone(), REWARD_2).frozen, 0); - assert_eq!(Tokens::accounts(keeper.clone(), REWARD_2).reserved, 0); - - assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); - assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); - assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, 0); - assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + run_to_block(100); - assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, UNIT); - assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).frozen, 0); - assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).reserved, 0); + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); - run_to_block(100); + let redeem_amount = DEPOSIT_AMOUNT / 2; + let deposit_left = DEPOSIT_AMOUNT - redeem_amount; - assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); - assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, 0); - assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); - - assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * UNIT); - assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).frozen, 0); - assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); run_to_block(200); - assert_ok!(LM::claim(Some(USER_1).into(), 0)); - assert_ok!(LM::claim(Some(USER_2).into(), 0)); - - let pbpd_1 = FixedU128::from((PER_BLOCK, UNIT)); - let reward_step_1 = (pbpd_1 * (100 * UNIT).into()).into_inner() / FixedU128::accuracy(); - - let pbpd_2 = FixedU128::from((PER_BLOCK, 2 * UNIT)); - let reward_step_2 = (pbpd_2 * (100 * UNIT).into()).into_inner() / FixedU128::accuracy(); - - assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_step_1 + reward_step_2); - assert_eq!(Tokens::accounts(USER_1, REWARD_1).frozen, 0); - assert_eq!(Tokens::accounts(USER_1, REWARD_1).reserved, 0); - assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_step_1 + reward_step_2); - assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); - assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); - assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_step_2); - assert_eq!(Tokens::accounts(USER_2, REWARD_1).frozen, 0); - assert_eq!(Tokens::accounts(USER_2, REWARD_1).reserved, 0); - assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_step_2); - assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); - assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, redeem_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); run_to_block(DAYS); - assert_ok!(LM::redeem_all(Some(USER_1).into(), 0)); + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * ((DAYS - 100) as u128 * DEPOSIT_AMOUNT / 2).into()).into_inner() / + FixedU128::accuracy(); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + run_to_block(DAYS + 100); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert!(LM::user_deposit_data(0, USER_1).is_none()); + assert!(LM::user_deposit_data(0, USER_2).is_none()); + assert!(LM::pool(0).is_none()); + }); +} + +#[test] +fn unlock_from_single_token_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_single_token_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + SINGLE_TOKEN_DEPOSIT, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 2; + let deposit_left = DEPOSIT_AMOUNT - redeem_amount; + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), SINGLE_TOKEN_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + run_to_block(200); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), SINGLE_TOKEN_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, SINGLE_TOKEN_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, SINGLE_TOKEN_DEPOSIT).free, redeem_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); + + run_to_block(DAYS); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * ((DAYS - 100) as u128 * DEPOSIT_AMOUNT / 2).into()).into_inner() / + FixedU128::accuracy(); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), SINGLE_TOKEN_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, SINGLE_TOKEN_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, SINGLE_TOKEN_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + run_to_block(DAYS + 100); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, SINGLE_TOKEN_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, SINGLE_TOKEN_DEPOSIT).free, DEPOSIT_AMOUNT); + assert!(LM::user_deposit_data(0, USER_1).is_none()); + assert!(LM::user_deposit_data(0, USER_2).is_none()); + assert!(LM::pool(0).is_none()); + }); +} + +#[test] +fn unlock_from_farming_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_farming_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + 2001, + 13, + 20, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 2; + let deposit_left = DEPOSIT_AMOUNT - redeem_amount; + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_1).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_2).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + run_to_block(200); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_1).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_2).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_1).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_2).free, redeem_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); + + run_to_block(DAYS); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * ((DAYS - 100) as u128 * DEPOSIT_AMOUNT / 2).into()).into_inner() / + FixedU128::accuracy(); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_1).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_2).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_1).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_2).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + run_to_block(DAYS + 100); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_1).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_2).free, DEPOSIT_AMOUNT); + assert!(LM::user_deposit_data(0, USER_1).is_none()); + assert!(LM::user_deposit_data(0, USER_2).is_none()); + assert!(LM::pool(0).is_none()); + }); +} + +#[test] +fn unlock_soon_after_edit_pool() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 2; + let deposit_left = DEPOSIT_AMOUNT - redeem_amount; + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + assert_ok!(LM::edit_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + 0, + 0, + 0 + )); + assert_eq!(LM::pool(0).unwrap().redeem_limit_time, 0); + assert_eq!(LM::pool(0).unwrap().unlock_limit_nums, 0); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, redeem_amount); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, redeem_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); + + run_to_block(DAYS); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * ((DAYS - 100) as u128 * DEPOSIT_AMOUNT / 2).into()).into_inner() / + FixedU128::accuracy(); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert!(LM::user_deposit_data(0, USER_1).is_none()); + assert!(LM::user_deposit_data(0, USER_2).is_none()); + assert!(LM::pool(0).is_none()); + }); +} + +#[test] +fn unlock_exceed_limit_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 4; + let deposit_left = DEPOSIT_AMOUNT - 3 * redeem_amount; + + for _ in 0..UNLOCK_LIMIT_NUMS { + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + } + + assert_noop!( + LM::redeem(Some(USER_1).into(), 0, redeem_amount), + Error::::ExceedMaximumUnlock + ); + assert_noop!( + LM::redeem(Some(USER_2).into(), 0, redeem_amount), + Error::::ExceedMaximumUnlock + ); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 3); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 3); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 6); + }); +} + +#[test] +fn cancel_unlock_from_mining_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 4; + + for _ in 0..UNLOCK_LIMIT_NUMS { + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + } + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + + let deposit_left = DEPOSIT_AMOUNT - 2 * redeem_amount; + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 4); + + run_to_block(400); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * (300 * deposit_left).into()).into_inner() / FixedU128::accuracy(); + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 0)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 0)); + + let deposit_left = DEPOSIT_AMOUNT; + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); + }); +} + +#[test] +fn cancel_unlock_from_farming_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_farming_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + 2001, + 13, + 20, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 4; + + for _ in 0..UNLOCK_LIMIT_NUMS { + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + } + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + + let deposit_left = DEPOSIT_AMOUNT - 2 * redeem_amount; + + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_1).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_2).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 4); + + run_to_block(400); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * (300 * deposit_left).into()).into_inner() / FixedU128::accuracy(); + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 0)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 0)); + + let deposit_left = DEPOSIT_AMOUNT; + + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_1).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(keeper.clone(), FARMING_DEPOSIT_2).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_1).free, 0); + assert_eq!(Tokens::accounts(USER_2, FARMING_DEPOSIT_2).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); + }); +} + +#[test] +fn cancel_unlock_from_single_token_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_single_token_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + SINGLE_TOKEN_DEPOSIT, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 4; + + for _ in 0..UNLOCK_LIMIT_NUMS { + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + } + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + + let deposit_left = DEPOSIT_AMOUNT - 2 * redeem_amount; + + assert_eq!(Tokens::accounts(keeper.clone(), SINGLE_TOKEN_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 4); + + run_to_block(400); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * (300 * deposit_left).into()).into_inner() / FixedU128::accuracy(); + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 0)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 0)); + + let deposit_left = DEPOSIT_AMOUNT; + + assert_eq!(Tokens::accounts(keeper.clone(), SINGLE_TOKEN_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, SINGLE_TOKEN_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 0); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 0); + }); +} + +#[test] +fn cancel_unlock_from_pool_retired_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + REDEEM_LIMIT_TIME, + UNLOCK_LIMIT_NUMS, + )); + + let keeper = LM::pool(0).unwrap().keeper; + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = FixedU128::from((per_block, 2 * DEPOSIT_AMOUNT)); + let reward_amount = + (pbpd * (100 * DEPOSIT_AMOUNT).into()).into_inner() / FixedU128::accuracy(); + + let redeem_amount = DEPOSIT_AMOUNT / 4; + + for _ in 0..UNLOCK_LIMIT_NUMS { + assert_ok!(LM::redeem(Some(USER_1).into(), 0, redeem_amount)); + assert_ok!(LM::redeem(Some(USER_2).into(), 0, redeem_amount)); + } + + assert_ok!(LM::cancel_unlock(Some(USER_1).into(), 0, 1)); + assert_ok!(LM::cancel_unlock(Some(USER_2).into(), 0, 1)); + + let deposit_left = DEPOSIT_AMOUNT - 2 * redeem_amount; + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, deposit_left); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 2); + assert_eq!(LM::pool(0).unwrap().deposit, 2 * deposit_left); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 4); + + run_to_block(DAYS); + + let pbpd = FixedU128::from((per_block, DEPOSIT_AMOUNT)); + let reward_amount = reward_amount + + (pbpd * ((DAYS - 100) as u128 * deposit_left).into()).into_inner() / + FixedU128::accuracy(); + + assert_noop!(LM::cancel_unlock(Some(USER_1).into(), 0, 1), Error::::InvalidPoolState); + assert_noop!(LM::cancel_unlock(Some(USER_2).into(), 0, 1), Error::::InvalidPoolState); + assert_noop!(LM::cancel_unlock(Some(USER_1).into(), 0, 0), Error::::InvalidPoolState); + assert_noop!(LM::cancel_unlock(Some(USER_2).into(), 0, 0), Error::::InvalidPoolState); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_ok!(LM::redeem_all(Some(USER_1).into(), 0)); + assert_ok!(LM::redeem_all(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, deposit_left); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, deposit_left); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_amount); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_amount); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_amount); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().deposit, 0); + assert_eq!(LM::user_deposit_data(0, USER_1).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::user_deposit_data(0, USER_2).unwrap().pending_unlocks.len(), 1); + assert_eq!(LM::pool(0).unwrap().deposit, 0); + assert_eq!(LM::pool(0).unwrap().pending_unlock_nums, 2); + + run_to_block(DAYS + 100); + + assert_ok!(LM::unlock(Some(USER_1).into(), 0)); + assert_ok!(LM::unlock(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert!(LM::pool(0).is_none()); + assert!(LM::user_deposit_data(0, USER_1).is_none()); + assert!(LM::user_deposit_data(0, USER_2).is_none()); + }); +} + +#[test] +fn simple_integration_test() { + new_test_ext().execute_with(|| { + const PER_BLOCK: Balance = REWARD_AMOUNT / DAYS as Balance; + + assert_ok!(LM::create_mining_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)].try_into().unwrap(), + DAYS, + 1 * UNIT, + 0, + Zero::zero(), + 0, + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::charge(Some(INVESTOR).into(), 0)); + + let pool = LM::pool(0).unwrap(); + let keeper = pool.keeper.clone(); + let kept = PER_BLOCK * DAYS as Balance; + + assert_eq!(Tokens::accounts(INVESTOR, REWARD_1).free, REWARD_AMOUNT - kept); + assert_eq!(Tokens::accounts(INVESTOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(INVESTOR, REWARD_1).reserved, 0); + assert_eq!(Tokens::accounts(INVESTOR, REWARD_2).free, REWARD_AMOUNT - kept); + assert_eq!(Tokens::accounts(INVESTOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(INVESTOR, REWARD_2).reserved, 0); + + assert_eq!(Tokens::accounts(keeper.clone(), REWARD_1).free, kept); + assert_eq!(Tokens::accounts(keeper.clone(), REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(keeper.clone(), REWARD_1).reserved, 0); + assert_eq!(Tokens::accounts(keeper.clone(), REWARD_2).free, kept); + assert_eq!(Tokens::accounts(keeper.clone(), REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(keeper.clone(), REWARD_2).reserved, 0); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, UNIT); + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).reserved, 0); + + run_to_block(100); + + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).free, 2 * UNIT); + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(keeper.clone(), MINING_DEPOSIT).reserved, 0); + + run_to_block(200); + + assert_ok!(LM::claim(Some(USER_1).into(), 0)); + assert_ok!(LM::claim(Some(USER_2).into(), 0)); + + let pbpd_1 = FixedU128::from((PER_BLOCK, UNIT)); + let reward_step_1 = (pbpd_1 * (100 * UNIT).into()).into_inner() / FixedU128::accuracy(); + + let pbpd_2 = FixedU128::from((PER_BLOCK, 2 * UNIT)); + let reward_step_2 = (pbpd_2 * (100 * UNIT).into()).into_inner() / FixedU128::accuracy(); + + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_step_1 + reward_step_2); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).reserved, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).free, reward_step_1 + reward_step_2); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_step_2); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).reserved, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).free, reward_step_2); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + + run_to_block(DAYS); + + assert_ok!(LM::redeem_all(Some(USER_1).into(), 0)); assert_ok!(LM::redeem_all(Some(USER_2).into(), 0)); let reward_step_3 = @@ -2816,7 +3780,7 @@ fn fuck_bug() { vec![].try_into().unwrap(), 23, UNIT, - 0 + 0, )); run_to_block(135); @@ -2847,7 +3811,7 @@ fn fuck_bug() { vec![].try_into().unwrap(), 23, UNIT, - 0 + 0, )); run_to_block(235); diff --git a/pallets/liquidity-mining/src/weights.rs b/pallets/liquidity-mining/src/weights.rs index cd96dcf5c8..b92e17026c 100644 --- a/pallets/liquidity-mining/src/weights.rs +++ b/pallets/liquidity-mining/src/weights.rs @@ -33,6 +33,8 @@ pub trait WeightInfo { fn redeem_all() -> Weight; fn volunteer_to_redeem() -> Weight; fn claim() -> Weight; + fn unlock() -> Weight; + fn cancel_unlock() -> Weight; } // For backwards compatibility and tests @@ -60,4 +62,12 @@ impl WeightInfo for () { fn claim() -> Weight { (50_000_000 as Weight) } + + fn unlock() -> Weight { + (50_000_000 as Weight) + } + + fn cancel_unlock() -> Weight { + (50_000_000 as Weight) + } } diff --git a/runtime/bifrost-kusama/Cargo.toml b/runtime/bifrost-kusama/Cargo.toml index c3a58c1289..1ee0ae3171 100644 --- a/runtime/bifrost-kusama/Cargo.toml +++ b/runtime/bifrost-kusama/Cargo.toml @@ -221,4 +221,5 @@ try-runtime = [ "frame-try-runtime", "frame-system/try-runtime", "pallet-vesting/try-runtime", + "bifrost-liquidity-mining/try-runtime", ] diff --git a/runtime/bifrost-kusama/src/lib.rs b/runtime/bifrost-kusama/src/lib.rs index f7adf1a7d8..9ba7f9290f 100644 --- a/runtime/bifrost-kusama/src/lib.rs +++ b/runtime/bifrost-kusama/src/lib.rs @@ -136,7 +136,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("bifrost"), impl_name: create_runtime_str!("bifrost"), authoring_version: 1, - spec_version: 920, + spec_version: 922, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -2024,6 +2024,50 @@ impl_runtime_apis! { } } +pub struct LatestRuntimeUpgrade; +impl OnRuntimeUpgrade for LatestRuntimeUpgrade { + fn on_runtime_upgrade() -> Weight { + let w_ksm = bifrost_liquidity_mining::migration::v2::Upgrade::< + Runtime, + bifrost_liquidity_mining::Instance1, + >::on_runtime_upgrade(); + let w_dot = bifrost_liquidity_mining::migration::v2::Upgrade::< + Runtime, + bifrost_liquidity_mining::Instance2, + >::on_runtime_upgrade(); + + w_ksm + w_dot + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<(), &'static str> { + bifrost_liquidity_mining::migration::v2::Upgrade::< + Runtime, + bifrost_liquidity_mining::Instance1, + >::pre_upgrade()?; + bifrost_liquidity_mining::migration::v2::Upgrade::< + Runtime, + bifrost_liquidity_mining::Instance2, + >::pre_upgrade()?; + + Ok(()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + bifrost_liquidity_mining::migration::v2::Upgrade::< + Runtime, + bifrost_liquidity_mining::Instance1, + >::post_upgrade()?; + bifrost_liquidity_mining::migration::v2::Upgrade::< + Runtime, + bifrost_liquidity_mining::Instance2, + >::post_upgrade()?; + + Ok(()) + } +} + struct CheckInherents; impl cumulus_pallet_parachain_system::CheckInherents for CheckInherents { diff --git a/runtime/bifrost-kusama/src/weights/bifrost_liquidity_mining.rs b/runtime/bifrost-kusama/src/weights/bifrost_liquidity_mining.rs index 80b653114f..bd76cd72d5 100644 --- a/runtime/bifrost-kusama/src/weights/bifrost_liquidity_mining.rs +++ b/runtime/bifrost-kusama/src/weights/bifrost_liquidity_mining.rs @@ -125,4 +125,14 @@ impl bifrost_liquidity_mining::WeightInfo for WeightInf .saturating_add(T::DbWeight::get().reads(9 as Weight)) .saturating_add(T::DbWeight::get().writes(7 as Weight)) } + + // By Hand + fn unlock() -> Weight { + 1_000 + } + + // By Hand + fn cancel_unlock() -> Weight { + 1_000 + } }