diff --git a/Cargo.lock b/Cargo.lock index 4b87bec4f7..36013769f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,7 @@ dependencies = [ "bifrost-bancor-runtime-api", "bifrost-flexible-fee", "bifrost-flexible-fee-rpc-runtime-api", + "bifrost-liquidity-mining", "bifrost-minter-reward", "bifrost-runtime-common", "bifrost-salp", @@ -706,6 +707,23 @@ dependencies = [ "sp-api", ] +[[package]] +name = "bifrost-liquidity-mining" +version = "0.8.0" +dependencies = [ + "frame-support", + "frame-system", + "node-primitives", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "pallet-collective", + "parity-scale-codec", + "sp-core", + "substrate-fixed", +] + [[package]] name = "bifrost-minter-reward" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 16d45c2a6e..5ce3fc12ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "node/service", "pallets/bancor", "pallets/bid", + "pallets/liquidity-mining", "pallets/flexible-fee", "pallets/minter-reward", "pallets/vesting", diff --git a/pallets/liquidity-mining/Cargo.toml b/pallets/liquidity-mining/Cargo.toml new file mode 100644 index 0000000000..12fec2bed4 --- /dev/null +++ b/pallets/liquidity-mining/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "bifrost-liquidity-mining" +version = "0.8.0" +authors = ["Allen Pocket "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +substrate-fixed = { git = "https://github.com/encointer/substrate-fixed", default-features = false } +frame-system = { version = "3.0.0", default-features = false } +frame-support = { version = "3.0.0", default-features = false } +node-primitives = { path = "../../node/primitives", default-features = false } +orml-traits = { version = "0.4.1-dev", default-features = false } + +[dev-dependencies] +sp-core = "3.0.0" +pallet-balances = "3.0.0" +pallet-collective = "3.0.0" +orml-tokens = "0.4.1-dev" +orml-currencies = "0.4.1-dev" + +[features] +default = ["std"] +std = [ + "codec/std", + "substrate-fixed/std", + "frame-system/std", + "frame-support/std", + "node-primitives/std", + "orml-traits/std", +] \ No newline at end of file diff --git a/pallets/liquidity-mining/src/lib.rs b/pallets/liquidity-mining/src/lib.rs new file mode 100644 index 0000000000..ef758563df --- /dev/null +++ b/pallets/liquidity-mining/src/lib.rs @@ -0,0 +1,909 @@ +// 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)] + +use frame_support::{ + pallet_prelude::*, + sp_runtime::traits::{SaturatedConversion, Saturating, Zero}, + sp_std::{ + cmp::{max, min}, + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + convert::TryFrom, + vec::Vec, + }, + traits::{BalanceStatus, EnsureOrigin}, +}; +use frame_system::pallet_prelude::*; +use node_primitives::{CurrencyId, CurrencyIdExt, LeasePeriod, ParaId, TokenInfo, TokenSymbol}; +use orml_traits::{LockIdentifier, MultiCurrency, MultiLockableCurrency, MultiReservableCurrency}; +pub use pallet::*; +use substrate_fixed::{traits::FromFixed, types::U64F64}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +const DEPOSIT_ID: LockIdentifier = *b"deposit "; + +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct PoolInfo { + /// Id of the liquidity-pool + pool_id: PoolId, + /// The creator of the liquidity-pool + creator: AccountIdOf, + /// The trading-pair supported by the liquidity-pool + trading_pair: (CurrencyId, CurrencyId), + /// The length of time the liquidity-pool releases rewards + duration: BlockNumberFor, + /// The liquidity-pool type + r#type: PoolType, + + /// The First Condition + /// + /// When starts the liquidity-pool, the amount deposited in the liquidity-pool + /// should be greater than the value. + min_deposit_to_start: BalanceOf, + /// The Second Condition + /// + /// When starts the liquidity-pool, the current block should be greater than the value. + after_block_to_start: BlockNumberFor, + + /// The total amount deposited in the liquidity-pool + deposit: BalanceOf, + + /// The reward infos about the liquidity-pool + rewards: BTreeMap>, + /// The block of the last update of the rewards + update_b: BlockNumberFor, + /// The liquidity-pool state + state: PoolState, + /// The block number when the liquidity-pool startup + block_startup: Option>, +} + +impl PoolInfo { + /// Trying to update the rewards + pub(crate) fn try_update(mut self) -> Self { + // When pool in `PoolState::Ongoing` or `PoolState::Retired` + if let Some(block_startup) = self.block_startup { + let block_end = self.duration + block_startup; + let n = min(frame_system::Pallet::::block_number(), block_end); + + for (_, reward) in self.rewards.iter_mut() { + reward.update(self.deposit, block_startup, self.update_b, n); + } + + self.update_b = n; + } + + self + } + + /// Trying to change the state from `PoolState::Approved` to `PoolState::Ongoing` + /// + /// __NOTE__: Only called in the `Hook` + pub(crate) fn try_startup(mut self, pid: PoolId, n: BlockNumberFor) -> Self { + if self.state == PoolState::Approved { + if n >= self.after_block_to_start && self.deposit >= self.min_deposit_to_start { + self.block_startup = Some(n); + self.state = PoolState::Ongoing; + + Pallet::::deposit_event(Event::PoolStarted(pid, self.r#type, self.trading_pair)); + } + } + + self + } + + /// Trying to change the state from `PoolState::Ongoing` to `PoolState::Retired` + pub(crate) fn try_retire(mut self, pid: PoolId) -> Self { + if self.state == PoolState::Ongoing { + let n = frame_system::Pallet::::block_number(); + + if let Some(block_startup) = self.block_startup { + if n >= block_startup + self.duration { + self.state = PoolState::Retired; + + Pallet::::deposit_event(Event::PoolRetired( + pid, + self.r#type, + self.trading_pair, + )); + } + } + } + + self + } + + /// Trying account & transfer the rewards to user + pub(crate) fn try_settle_and_transfer( + &mut self, + deposit_data: &mut DepositData, + pid: PoolId, + user: AccountIdOf, + ) -> Result<(), DispatchError> { + let mut to_rewards = Vec::<(CurrencyId, BalanceOf)>::new(); + + if self.state == PoolState::Ongoing || self.state == PoolState::Retired { + for (rtoken, reward) in self.rewards.iter_mut() { + let v_new = reward.gain_avg; + if let Some(gain_avg) = deposit_data.gain_avgs.get(rtoken) { + let v_old = *gain_avg; + + let user_deposit: u128 = deposit_data.deposit.saturated_into(); + let amount = BalanceOf::::saturated_from(u128::from_fixed( + ((v_new - v_old) * user_deposit).floor(), + )); + + // Update the claimed of the reward + reward.claimed = reward.claimed.saturating_add(amount); + // Sync the gain_avg between `DepositData` and `RewardData` + deposit_data.gain_avgs.insert(*rtoken, v_new); + deposit_data.update_b = self.update_b; + + to_rewards.push((*rtoken, amount)); + } + } + } + + for (rtoken, amount) in to_rewards.iter() { + let remain = T::MultiCurrency::repatriate_reserved( + *rtoken, + &self.creator, + &user, + *amount, + BalanceStatus::Free, + )?; + + ensure!(remain == Zero::zero(), Error::::Unexpected); + } + + Pallet::::deposit_event(Event::UserClaimed( + pid, + self.r#type, + self.trading_pair, + to_rewards, + user, + )); + + Ok(().into()) + } +} + +#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)] +pub enum PoolType { + Mining, + Farming, +} + +#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)] +pub enum PoolState { + UnderAudit, + Approved, + Ongoing, + Retired, + Dead, +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct DepositData { + /// The amount of trading-pair deposited in the liquidity-pool + 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 + gain_avgs: BTreeMap, + update_b: BlockNumberFor, +} + +impl DepositData { + pub(crate) fn from_pool(pool: &PoolInfo) -> Self { + let mut gain_avgs = BTreeMap::::new(); + + for (rtoken, reward) in pool.rewards.iter() { + gain_avgs.insert(*rtoken, reward.gain_avg); + } + + Self { deposit: Zero::zero(), gain_avgs, update_b: pool.update_b } + } +} + +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct RewardData { + /// The total amount of token to reward + total: BalanceOf, + /// The amount of token to reward per block + per_block: BalanceOf, + + /// The amount of token was already rewarded + claimed: BalanceOf, + /// The average gain in pico by 1 pico deposited from the startup of the liquidity-pool, + /// updated when anyone deposits to / redeems from / claims from the liquidity-pool. + gain_avg: U64F64, +} + +impl RewardData { + fn new(total: BalanceOf, duration: BlockNumberFor) -> Result { + let total: u128 = total.saturated_into(); + let (per_block, total) = { + let duration: u128 = duration.saturated_into(); + + let per_block = total / duration; + let total = per_block * duration; + + (BalanceOf::::saturated_from(per_block), BalanceOf::::saturated_from(total)) + }; + + ensure!(per_block > T::MinimumRewardPerBlock::get(), Error::::InvalidRewardPerBlock); + + Ok(RewardData { total, per_block, claimed: Zero::zero(), gain_avg: U64F64::from_num(0) }) + } + + pub(crate) fn per_block_per_deposited(&self, deposited: BalanceOf) -> U64F64 { + let per_block: u128 = self.per_block.saturated_into(); + let deposit: u128 = deposited.saturated_into(); + + match deposit { + 0 => U64F64::from_num(0), + _ => U64F64::from_num(per_block) / deposit, + } + } + + /// Trying to update the gain_avg + pub(crate) fn update( + &mut self, + deposit: BalanceOf, + block_startup: BlockNumberFor, + block_last_updated: BlockNumberFor, + n: BlockNumberFor, + ) { + let pbpd = self.per_block_per_deposited(deposit); + + let b_prev = max(block_last_updated, block_startup); + let b_past: u128 = (n - b_prev).saturated_into(); + + let gain_avg_new = self.gain_avg + pbpd * b_past; + + self.gain_avg = gain_avg_new; + } +} + +impl core::fmt::Debug for RewardData { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("") + .field(&self.total) + .field(&self.per_block) + .field(&self.claimed) + .field(&self.gain_avg) + .finish() + } +} + +#[allow(type_alias_bounds)] +type AccountIdOf = ::AccountId; + +#[allow(type_alias_bounds)] +type BalanceOf = + <::MultiCurrency as MultiCurrency>>::Balance; + +type PoolId = u128; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + + /// Origin for anyone able to create/approve/kill the liquidity-pool. + type ControlOrigin: EnsureOrigin; + + type MultiCurrency: MultiCurrency, CurrencyId = CurrencyId> + + MultiReservableCurrency, CurrencyId = CurrencyId> + + MultiLockableCurrency, CurrencyId = CurrencyId>; + + /// The value used to construct vsbond when creating a farming-liquidity-pool + #[pallet::constant] + type RelayChainTokenSymbol: Get; + + /// The amount deposited into a liquidity-pool should be less than the value + #[pallet::constant] + type MaximumDepositInPool: Get>; + + /// The amount deposited by a user to a liquidity-pool should be greater than the value + #[pallet::constant] + type MinimumDepositOfUser: Get>; + + /// The amount of token to reward per block should be greater than the value + #[pallet::constant] + type MinimumRewardPerBlock: Get>; + + /// The duration of a liquidity-pool should be greater than the value + #[pallet::constant] + type MinimumDuration: Get>; + + /// The number of liquidity-pool approved should be less than the value + #[pallet::constant] + type MaximumApproved: Get; + } + + #[pallet::error] + pub enum Error { + InvalidTradingPair, + InvalidDuration, + InvalidRewardPerBlock, + InvalidDepositLimit, + InvalidPoolId, + InvalidPoolState, + InvalidPoolOwner, + /// Find duplicate reward when creating the liquidity-pool + DuplicateReward, + /// When the amount deposited in a liquidity-pool exceeds the `MaximumDepositInPool` + ExceedMaximumDeposit, + /// When the number of pool-approved exceeds the `MaximumApproved` + ExceedMaximumApproved, + /// Not enough balance to deposit + NotEnoughToDeposit, + /// Not enough balance of reward to unreserve + FailOnUnReserve, + /// Not enough deposit of the user in the liquidity-pool + NoDepositOfUser, + /// Too low balance to deposit + TooLowToDeposit, + /// The deposit in liquidity-pool ongoing should be greater than `T::MinimumDeposit` + TooLowDepositInPoolToRedeem, + /// The interval between two claims is short + TooShortBetweenTwoClaim, + /// __NOTE__: ERROR HAPPEN + Unexpected, + } + + #[pallet::event] + #[pallet::generate_deposit(pub (crate) fn deposit_event)] + pub enum Event { + /// The liquidity-pool has been created + /// + /// [pool_id, pool_type, trading_pair, creator] + PoolCreated(PoolId, PoolType, (CurrencyId, CurrencyId), AccountIdOf), + /// The liquidity-pool has been approved + /// + /// [pool_id, pool_type, trading_pair] + PoolApproved(PoolId, PoolType, (CurrencyId, CurrencyId)), + /// The liquidity-pool has been started up + /// + /// [pool_id, pool_type, trading_pair] + PoolStarted(PoolId, PoolType, (CurrencyId, CurrencyId)), + /// The liquidity-pool has been killed + /// + /// [pool_id, pool_type, trading_pair] + PoolKilled(PoolId, PoolType, (CurrencyId, CurrencyId)), + /// The liquidity-pool has been retired + /// + /// [pool_id, pool_type, trading_pair] + PoolRetired(PoolId, PoolType, (CurrencyId, CurrencyId)), + /// User has deposited some trading-pair to a liquidity-pool + /// + /// [pool_id, pool_type, trading_pair, amount_deposited, user] + UserDeposited(PoolId, PoolType, (CurrencyId, CurrencyId), BalanceOf, AccountIdOf), + /// User has been redeemed some trading-pair from a liquidity-pool + /// + /// [pool_id, pool_type, trading_pair, amount_redeemed, user] + UserRedeemed(PoolId, PoolType, (CurrencyId, CurrencyId), BalanceOf, AccountIdOf), + /// User has been claimed the rewards from a liquidity-pool + /// + /// [pool_id, pool_type, trading_pair, rewards, user] + UserClaimed( + PoolId, + PoolType, + (CurrencyId, CurrencyId), + Vec<(CurrencyId, BalanceOf)>, + AccountIdOf, + ), + } + + #[pallet::storage] + #[pallet::getter(fn pool_id)] + pub(crate) type NextOrderId = StorageValue<_, PoolId, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn approved_pids)] + pub(crate) type ApprovedPoolIds = StorageValue<_, BTreeSet, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn pool)] + pub(crate) type TotalPoolInfos = StorageMap<_, Twox64Concat, PoolId, PoolInfo>; + + #[pallet::storage] + #[pallet::getter(fn user_deposit_data)] + pub(crate) type TotalDepositData = StorageDoubleMap< + _, + Blake2_128Concat, + AccountIdOf, + Blake2_128Concat, + PoolId, + DepositData, + >; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::call] + impl Pallet { + #[pallet::weight(1_000)] + pub fn create_mining_pool( + origin: OriginFor, + trading_pair: (CurrencyId, CurrencyId), + main_reward: (CurrencyId, BalanceOf), + option_rewards: Vec<(CurrencyId, BalanceOf)>, + #[pallet::compact] duration: BlockNumberFor, + #[pallet::compact] min_deposit_to_start: BalanceOf, + #[pallet::compact] after_block_to_start: BlockNumberFor, + ) -> DispatchResultWithPostInfo { + // Order the trading_pair + let (token1, token2) = trading_pair; + + ensure!(!token1.is_vsbond() && !token1.is_lptoken(), Error::::InvalidTradingPair); + ensure!(!token2.is_vsbond() && !token2.is_lptoken(), Error::::InvalidTradingPair); + + let (id1, id2) = (token1.currency_id(), token2.currency_id()); + let trading_pair = if id1 <= id2 { (token1, token2) } else { (token2, token1) }; + + Self::create_pool( + origin, + trading_pair, + main_reward, + option_rewards, + PoolType::Mining, + duration, + min_deposit_to_start, + after_block_to_start, + )?; + + Ok(().into()) + } + + #[pallet::weight(1_000)] + pub fn create_farming_pool( + origin: OriginFor, + index: ParaId, + first_slot: LeasePeriod, + last_slot: LeasePeriod, + main_reward: (CurrencyId, BalanceOf), + option_rewards: Vec<(CurrencyId, BalanceOf)>, + #[pallet::compact] duration: BlockNumberFor, + #[pallet::compact] min_deposit_to_start: BalanceOf, + #[pallet::compact] after_block_to_start: BlockNumberFor, + ) -> DispatchResultWithPostInfo { + #[allow(non_snake_case)] + let trading_pair = Self::vsAssets(index, first_slot, last_slot); + + Self::create_pool( + origin, + trading_pair, + main_reward, + option_rewards, + PoolType::Farming, + duration, + min_deposit_to_start, + after_block_to_start, + )?; + + Ok(().into()) + } + + #[pallet::weight(1_000)] + pub fn approve_pool(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + let _ = T::ControlOrigin::ensure_origin(origin)?; + + let num = Self::approved_pids().len() as u32; + ensure!(num < T::MaximumApproved::get(), Error::::ExceedMaximumApproved); + + let pool: PoolInfo = Self::pool(pid).ok_or(Error::::InvalidPoolId)?; + + ensure!(pool.state == PoolState::UnderAudit, Error::::InvalidPoolState); + + ApprovedPoolIds::::mutate(|pids| pids.insert(pid)); + + let r#type = pool.r#type; + let trading_pair = pool.trading_pair; + + let pool_approved = PoolInfo { state: PoolState::Approved, ..pool }; + TotalPoolInfos::::insert(pid, pool_approved); + + Self::deposit_event(Event::PoolApproved(pid, r#type, trading_pair)); + + Ok(().into()) + } + + #[pallet::weight(1_000)] + pub fn kill_pool(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + let signed = ensure_signed(origin)?; + + let pool: PoolInfo = Self::pool(pid).ok_or(Error::::InvalidPoolId)?; + + ensure!(signed == pool.creator, Error::::InvalidPoolOwner); + + ensure!(pool.state == PoolState::UnderAudit, Error::::InvalidPoolState); + + for (token, reward) in pool.rewards.iter() { + let total = reward.total; + let remain = T::MultiCurrency::unreserve(*token, &signed, total); + ensure!(remain == Zero::zero(), Error::::FailOnUnReserve); + } + + let pool_killed = PoolInfo { state: PoolState::Dead, ..pool }; + TotalPoolInfos::::remove(pid); + + Self::deposit_event(Event::PoolKilled( + pid, + pool_killed.r#type, + pool_killed.trading_pair, + )); + + Ok(().into()) + } + + /// User deposits some token to a liquidity-pool. + /// + /// The extrinsic will: + /// - Try to retire the liquidity-pool which has reached the end of life. + /// - Try to settle the rewards when the liquidity-pool in `Ongoing`. + /// + /// The conditions to deposit: + /// - User should deposit enough(greater than `T::MinimumDeposit`) token to liquidity-pool; + /// - The liquidity-pool should be in special state: `Approved`, `Ongoing`; + #[pallet::weight(1_000)] + pub fn deposit( + origin: OriginFor, + pid: PoolId, + value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let user = ensure_signed(origin)?; + + let mut pool: PoolInfo = + Self::pool(pid).ok_or(Error::::InvalidPoolId)?.try_retire(pid).try_update(); + + ensure!( + pool.state == PoolState::Approved || pool.state == PoolState::Ongoing, + Error::::InvalidPoolState + ); + + ensure!(value >= T::MinimumDepositOfUser::get(), Error::::TooLowToDeposit); + + let mut deposit_data: DepositData = + Self::user_deposit_data(&user, &pid).unwrap_or(DepositData::::from_pool(&pool)); + + if pool.state == PoolState::Ongoing && pool.update_b != deposit_data.update_b { + pool.try_settle_and_transfer(&mut deposit_data, pid, user.clone())?; + } + + deposit_data.deposit = deposit_data.deposit.saturating_add(value); + pool.deposit = pool.deposit.saturating_add(value); + ensure!( + pool.deposit <= T::MaximumDepositInPool::get(), + Error::::ExceedMaximumDeposit + ); + + // To lock the deposit + if pool.r#type == PoolType::Mining { + let lpt = Self::convert_to_lptoken(pool.trading_pair)?; + + T::MultiCurrency::ensure_can_withdraw(lpt, &user, value) + .map_err(|_e| Error::::NotEnoughToDeposit)?; + + T::MultiCurrency::extend_lock(DEPOSIT_ID, lpt, &user, deposit_data.deposit)?; + } else { + let (token_a, token_b) = pool.trading_pair; + + T::MultiCurrency::ensure_can_withdraw(token_a, &user, value) + .map_err(|_e| Error::::NotEnoughToDeposit)?; + T::MultiCurrency::ensure_can_withdraw(token_b, &user, value) + .map_err(|_e| Error::::NotEnoughToDeposit)?; + + T::MultiCurrency::extend_lock(DEPOSIT_ID, token_a, &user, deposit_data.deposit)?; + T::MultiCurrency::extend_lock(DEPOSIT_ID, token_b, &user, deposit_data.deposit)?; + } + + let r#type = pool.r#type; + let trading_pair = pool.trading_pair; + + TotalPoolInfos::::insert(pid, pool); + TotalDepositData::::insert(user.clone(), pid, deposit_data); + + Self::deposit_event(Event::UserDeposited(pid, r#type, trading_pair, value, user)); + + Ok(().into()) + } + + /// User redeems all deposit from a liquidity-pool. + /// The deposit in the liquidity-pool should be greater than `T::MinimumDeposit` when the + /// liquidity-pool is on `Ongoing` state; So user may not be able to redeem completely + /// until the liquidity-pool is on `Retire` state. + /// + /// The extrinsic will: + /// - Try to retire the liquidity-pool which has reached the end of life. + /// - Try to settle the rewards when the liquidity-pool in `Ongoing`. + /// - Try to unreserve the remaining rewards to the pool creator when the deposit in the + /// liquidity-pool is clear. + /// - Try to delete the liquidity-pool in which the deposit becomes zero. + /// - Try to delete the deposit-data in which the deposit becomes zero. + /// + /// The condition to redeem: + /// - User should have some deposit in the liquidity-pool; + /// - The liquidity-pool should be in special state: `Ongoing`, `Retired`; + #[pallet::weight(1_000)] + pub fn redeem(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + let user = ensure_signed(origin)?; + + let mut pool: PoolInfo = + Self::pool(pid).ok_or(Error::::InvalidPoolId)?.try_retire(pid).try_update(); + + ensure!( + pool.state == PoolState::Ongoing || pool.state == PoolState::Retired, + Error::::InvalidPoolState + ); + + let mut deposit_data: DepositData = + Self::user_deposit_data(&user, &pid).ok_or(Error::::NoDepositOfUser)?; + + if pool.update_b != deposit_data.update_b { + pool.try_settle_and_transfer(&mut deposit_data, pid, user.clone())?; + } + + // Keep minimum deposit in pool when the pool is ongoing. + let minimum_in_pool = match pool.state { + PoolState::Ongoing => T::MinimumDepositOfUser::get(), + PoolState::Retired => Zero::zero(), + _ => return Err(Error::::InvalidPoolState.into()), + }; + + let try_redeemed = deposit_data.deposit; + let left_in_pool = max(pool.deposit - try_redeemed, minimum_in_pool); + let can_redeemed = pool.deposit - left_in_pool; + let left_in_user = deposit_data.deposit - can_redeemed; + + ensure!(can_redeemed != Zero::zero(), Error::::TooLowDepositInPoolToRedeem); + + // To unlock the deposit + match pool.r#type { + PoolType::Mining => { + let lpt = Self::convert_to_lptoken(pool.trading_pair)?; + match left_in_user.saturated_into() { + 0u128 => T::MultiCurrency::remove_lock(DEPOSIT_ID, lpt, &user)?, + _ => T::MultiCurrency::set_lock(DEPOSIT_ID, lpt, &user, left_in_user)?, + } + }, + PoolType::Farming => { + let (token_a, token_b) = pool.trading_pair; + match left_in_user.saturated_into() { + 0u128 => { + T::MultiCurrency::remove_lock(DEPOSIT_ID, token_a, &user)?; + T::MultiCurrency::remove_lock(DEPOSIT_ID, token_b, &user)?; + }, + _ => { + T::MultiCurrency::set_lock(DEPOSIT_ID, token_a, &user, left_in_user)?; + T::MultiCurrency::set_lock(DEPOSIT_ID, token_b, &user, left_in_user)?; + }, + } + }, + }; + + deposit_data.deposit = left_in_user; + pool.deposit = left_in_pool; + + if pool.state == PoolState::Retired && pool.deposit == Zero::zero() { + for (rtoken, reward) in pool.rewards.iter() { + let remain = reward.total - reward.claimed; + ensure!( + T::MultiCurrency::unreserve(*rtoken, &pool.creator, remain) == Zero::zero(), + Error::::Unexpected + ); + } + + 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), + } + + match deposit_data.deposit.saturated_into() { + 0u128 => TotalDepositData::::remove(user.clone(), pid), + _ => TotalDepositData::::insert(user.clone(), pid, deposit_data), + } + + Self::deposit_event(Event::UserRedeemed(pid, r#type, trading_pair, try_redeemed, user)); + + Ok(().into()) + } + + /// User claims the rewards from a liquidity-pool. + /// + /// The extrinsic will: + /// - Try to retire the liquidity-pool which has reached the end of life. + /// + /// The conditions to claim: + /// - User should have enough token deposited in the liquidity-pool; + /// - The liquidity-pool should be in special states: `Ongoing`, `Retired`; + #[pallet::weight(1_000)] + pub fn claim(origin: OriginFor, pid: PoolId) -> DispatchResultWithPostInfo { + let user = ensure_signed(origin)?; + + let mut pool: PoolInfo = + Self::pool(pid).ok_or(Error::::InvalidPoolId)?.try_retire(pid).try_update(); + + ensure!( + pool.state == PoolState::Ongoing || pool.state == PoolState::Retired, + Error::::InvalidPoolState + ); + + let mut deposit_data: DepositData = + Self::user_deposit_data(&user, &pid).ok_or(Error::::NoDepositOfUser)?; + + ensure!(pool.update_b != deposit_data.update_b, Error::::TooShortBetweenTwoClaim); + pool.try_settle_and_transfer(&mut deposit_data, pid, user.clone())?; + + TotalPoolInfos::::insert(pid, pool); + TotalDepositData::::insert(user, pid, deposit_data); + + Ok(().into()) + } + } + + impl Pallet { + pub(crate) fn create_pool( + origin: OriginFor, + trading_pair: (CurrencyId, CurrencyId), + main_reward: (CurrencyId, BalanceOf), + option_rewards: Vec<(CurrencyId, BalanceOf)>, + r#type: PoolType, + duration: BlockNumberFor, + min_deposit_to_start: BalanceOf, + after_block_to_start: BlockNumberFor, + ) -> DispatchResult { + let creator = ensure_signed(origin)?; + + // Check the trading-pair + ensure!(trading_pair.0 != trading_pair.1, Error::::InvalidTradingPair); + + // Check the duration + ensure!(duration > T::MinimumDuration::get(), Error::::InvalidDuration); + + // Check the condition + ensure!( + min_deposit_to_start >= T::MinimumDepositOfUser::get(), + Error::::InvalidDepositLimit + ); + ensure!( + min_deposit_to_start <= T::MaximumDepositInPool::get(), + Error::::InvalidDepositLimit + ); + + // Check & Construct the rewards + let raw_rewards: Vec<(CurrencyId, BalanceOf)> = + option_rewards.into_iter().chain(Some(main_reward).into_iter()).collect(); + let mut rewards: BTreeMap> = BTreeMap::new(); + for (token, total) in raw_rewards.into_iter() { + ensure!(!rewards.contains_key(&token), Error::::DuplicateReward); + + let reward = RewardData::new(total, duration)?; + + // Reserve the reward + T::MultiCurrency::reserve(token, &creator, reward.total)?; + + rewards.insert(token, reward); + } + + // Construct the PoolInfo + let pool_id = Self::next_pool_id(); + let mining_pool = PoolInfo { + pool_id, + creator: creator.clone(), + trading_pair, + duration, + r#type, + + min_deposit_to_start, + after_block_to_start, + + deposit: Zero::zero(), + + rewards, + update_b: Zero::zero(), + state: PoolState::UnderAudit, + block_startup: None, + }; + + TotalPoolInfos::::insert(pool_id, mining_pool); + + Self::deposit_event(Event::PoolCreated(pool_id, r#type, trading_pair, creator)); + + Ok(().into()) + } + + pub(crate) fn next_pool_id() -> PoolId { + let next_pool_id = Self::pool_id(); + NextOrderId::::mutate(|current| *current += 1); + next_pool_id + } + + pub(crate) fn convert_to_lptoken( + trading_pair_ordered: (CurrencyId, CurrencyId), + ) -> Result { + let (token1, token2) = trading_pair_ordered; + let (discr1, discr2) = (token1.discriminant(), token2.discriminant()); + let (sid1, sid2) = ( + (token1.currency_id() & 0x0000_0000_0000_00ff) as u8, + (token2.currency_id() & 0x0000_0000_0000_00ff) as u8, + ); + let (sym1, sym2) = ( + TokenSymbol::try_from(sid1).map_err(|_| Error::::InvalidTradingPair)?, + TokenSymbol::try_from(sid2).map_err(|_| Error::::InvalidTradingPair)?, + ); + + Ok(CurrencyId::LPToken(sym1, discr1, sym2, discr2)) + } + + #[allow(non_snake_case)] + pub(crate) fn vsAssets( + index: ParaId, + first_slot: LeasePeriod, + last_slot: LeasePeriod, + ) -> (CurrencyId, CurrencyId) { + let token_symbol = T::RelayChainTokenSymbol::get(); + + let vsToken = CurrencyId::VSToken(token_symbol); + let vsBond = CurrencyId::VSBond(token_symbol, index, first_slot, last_slot); + + (vsToken, vsBond) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(n: BlockNumberFor) { + // Check whether pool-activated is meet the startup condition + for pid in Self::approved_pids() { + if let Some(mut pool) = Self::pool(pid) { + pool = pool.try_startup(pid, n); + + if pool.state == PoolState::Ongoing { + ApprovedPoolIds::::mutate(|pids| pids.remove(&pid)); + TotalPoolInfos::::insert(pid, pool); + } + } + } + } + + fn on_initialize(_n: BlockNumberFor) -> frame_support::weights::Weight { + // TODO estimate weight + Zero::zero() + } + } +} diff --git a/pallets/liquidity-mining/src/mock.rs b/pallets/liquidity-mining/src/mock.rs new file mode 100644 index 0000000000..3d2d30803c --- /dev/null +++ b/pallets/liquidity-mining/src/mock.rs @@ -0,0 +1,231 @@ +// 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 . + +use frame_support::{ + construct_runtime, parameter_types, + sp_io::TestExternalities, + sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, + }, +}; +use node_primitives::{Amount, Balance, CurrencyId, TokenSymbol}; +use sp_core::H256; + +use crate as lm; + +pub(crate) type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub(crate) type Block = frame_system::mocking::MockBlock; +pub(crate) type BlockNumber = u32; +pub(crate) type Index = u32; +pub(crate) type Signature = MultiSignature; +pub(crate) type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +construct_runtime!( + pub enum T where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Event}, + Currencies: orml_currencies::{Pallet, Call, Event}, + Tokens: orml_tokens::{Pallet, Call, Storage, Event, Config}, + TechnicalCommittee: pallet_collective::::{Pallet, Call, Storage, Origin, Event, Config}, + LM: lm::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const NativeCurrencyId: CurrencyId = CurrencyId::Native(TokenSymbol::ASG); + pub const RelayCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); +} + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); +} + +impl frame_system::Config for T { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = (); + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockNumber = BlockNumber; + type BlockWeights = (); + type Call = Call; + type DbWeight = (); + type Event = Event; + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = generic::Header; + type Index = Index; + type Lookup = IdentityLookup; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type Origin = Origin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; + pub const TransferFee: u128 = 0; + pub const CreationFee: u128 = 0; + pub const TransactionByteFee: u128 = 0; + pub const MaxLocks: u32 = 999_999; + pub const MaxReserves: u32 = 999_999; +} + +impl pallet_balances::Config for T { + type AccountStore = System; + /// The type for recording an account's balance. + type Balance = Balance; + type DustRemoval = (); + /// The ubiquitous event type. + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = pallet_balances::weights::SubstrateWeight; +} + +pub type BifrostToken = orml_currencies::BasicCurrencyAdapter; + +impl orml_currencies::Config for T { + type Event = Event; + type GetNativeCurrencyId = NativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BifrostToken; + type WeightInfo = (); +} + +orml_traits::parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + 0 + }; +} + +impl orml_tokens::Config for T { + type Amount = Amount; + type Balance = Balance; + type CurrencyId = CurrencyId; + type Event = Event; + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = MaxLocks; + type OnDust = (); + type WeightInfo = (); +} + +parameter_types! { + pub const TechnicalMotionDuration: BlockNumber = 3 * DAYS; + pub const TechnicalMaxProposals: u32 = 100; + pub const TechnicalMaxMembers: u32 = 100; +} + +type TechnicalCollective = pallet_collective::Instance1; +impl pallet_collective::Config for T { + type DefaultVote = pallet_collective::PrimeDefaultVote; + type Event = Event; + type MaxMembers = TechnicalMaxMembers; + type MaxProposals = TechnicalMaxProposals; + type MotionDuration = TechnicalMotionDuration; + type Origin = Origin; + type Proposal = Call; + type WeightInfo = pallet_collective::weights::SubstrateWeight; +} + +parameter_types! { + pub const RelayChainTokenSymbol: TokenSymbol = TokenSymbol::KSM; + pub const MaximumDepositInPool: Balance = 1_000_000 * UNIT; + pub const MinimumDeposit: Balance = 1_000_000; + pub const MinimumRewardPerBlock: Balance = 1_000; + pub const MinimumDuration: BlockNumber = MINUTES; + pub const MaximumApproved: u32 = 4; +} + +impl lm::Config for T { + type Event = Event; + type ControlOrigin = pallet_collective::EnsureMember; + type MultiCurrency = Tokens; + type RelayChainTokenSymbol = RelayChainTokenSymbol; + type MaximumDepositInPool = MaximumDepositInPool; + type MinimumDepositOfUser = MinimumDeposit; + type MinimumRewardPerBlock = MinimumRewardPerBlock; + type MinimumDuration = MinimumDuration; + type MaximumApproved = MaximumApproved; +} + +pub(crate) fn new_test_ext() -> TestExternalities { + GenesisConfig { + tokens: orml_tokens::GenesisConfig:: { + balances: vec![ + (CREATOR, REWARD_1, REWARD_AMOUNT), + (CREATOR, REWARD_2, REWARD_AMOUNT), + (USER_1, FARMING_DEPOSIT_1, DEPOSIT_AMOUNT), + (USER_1, FARMING_DEPOSIT_2, DEPOSIT_AMOUNT), + (USER_2, FARMING_DEPOSIT_1, DEPOSIT_AMOUNT), + (USER_2, FARMING_DEPOSIT_2, DEPOSIT_AMOUNT), + (USER_1, MINING_DEPOSIT, DEPOSIT_AMOUNT), + (USER_2, MINING_DEPOSIT, DEPOSIT_AMOUNT), + (RICHER, FARMING_DEPOSIT_1, 1_000_000_000_000 * UNIT), + (RICHER, FARMING_DEPOSIT_2, 1_000_000_000_000 * UNIT), + (RICHER, MINING_DEPOSIT, 1_000_000_000_000 * UNIT), + ], + }, + technical_committee: pallet_collective::GenesisConfig { + members: vec![TC_MEMBER_1, TC_MEMBER_2, TC_MEMBER_3], + phantom: Default::default(), + }, + } + .build_storage() + .unwrap() + .into() +} + +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 UNIT: Balance = 1_000_000_000_000; + +pub(crate) const MINING_TRADING_PAIR: (CurrencyId, CurrencyId) = + (CurrencyId::Token(TokenSymbol::DOT), CurrencyId::Token(TokenSymbol::KSM)); +pub(crate) const MINING_DEPOSIT: CurrencyId = + CurrencyId::LPToken(TokenSymbol::DOT, 2u8, TokenSymbol::KSM, 2u8); +pub(crate) const FARMING_DEPOSIT_1: CurrencyId = CurrencyId::VSToken(TokenSymbol::KSM); +pub(crate) const FARMING_DEPOSIT_2: CurrencyId = CurrencyId::VSBond(TokenSymbol::KSM, 2001, 13, 20); +pub(crate) const DEPOSIT_AMOUNT: Balance = UNIT; +pub(crate) const REWARD_1: CurrencyId = CurrencyId::Native(TokenSymbol::BNC); +pub(crate) const REWARD_2: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); +pub(crate) const REWARD_AMOUNT: Balance = UNIT; + +pub(crate) const CREATOR: AccountId = AccountId::new([0u8; 32]); +pub(crate) const USER_1: AccountId = AccountId::new([1u8; 32]); +pub(crate) const USER_2: AccountId = AccountId::new([2u8; 32]); +pub(crate) const TC_MEMBER_1: AccountId = AccountId::new([3u8; 32]); +pub(crate) const TC_MEMBER_2: AccountId = AccountId::new([4u8; 32]); +pub(crate) const TC_MEMBER_3: AccountId = AccountId::new([5u8; 32]); +pub(crate) const RICHER: AccountId = AccountId::new([6u8; 32]); diff --git a/pallets/liquidity-mining/src/tests.rs b/pallets/liquidity-mining/src/tests.rs new file mode 100644 index 0000000000..b03ff93f9e --- /dev/null +++ b/pallets/liquidity-mining/src/tests.rs @@ -0,0 +1,1468 @@ +// 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 . + +use frame_support::{assert_noop, assert_ok, dispatch::DispatchError, traits::Hooks}; +use node_primitives::{Balance, CurrencyId, TokenSymbol}; +use substrate_fixed::{traits::FromFixed, types::U64F64}; + +use crate::{mock::*, Error, PoolId, PoolInfo, PoolState, PoolType, TotalPoolInfos}; + +fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + LM::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + LM::on_initialize(System::block_number()); + } +} + +#[test] +fn create_farming_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_farming_pool( + Some(CREATOR).into(), + 2001, + 13, + 20, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000 * UNIT, + 0 + )); + + let pool = LM::pool(0).unwrap(); + + assert_eq!(pool.r#type, PoolType::Farming); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let reserved = per_block * DAYS as Balance; + let free = REWARD_AMOUNT - reserved; + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, reserved); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, reserved); + }); +} + +#[test] +fn create_mining_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000 * UNIT, + 0 + )); + + let pool = LM::pool(0).unwrap(); + + assert_eq!(pool.r#type, PoolType::Mining); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let reserved = per_block * DAYS as Balance; + let free = REWARD_AMOUNT - reserved; + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, reserved); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, reserved); + }); +} + +#[test] +fn create_mining_pool_with_wrong_currency_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + LM::create_mining_pool( + Some(CREATOR).into(), + ( + CurrencyId::VSBond(RelayChainTokenSymbol::get(), 2001, 13, 20), + CurrencyId::VSToken(RelayChainTokenSymbol::get()), + ), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000 * UNIT, + 0, + ), + Error::::InvalidTradingPair, + ); + + assert_noop!( + LM::create_mining_pool( + Some(CREATOR).into(), + ( + CurrencyId::LPToken(TokenSymbol::KSM, 1u8, TokenSymbol::DOT, 2u8), + CurrencyId::VSToken(RelayChainTokenSymbol::get()), + ), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000 * UNIT, + 0, + ), + Error::::InvalidTradingPair, + ); + }); +} + +#[test] +fn increase_pid_when_create_pool_should_work() { + new_test_ext().execute_with(|| { + const NUM: PoolId = 8; + for pid in 0..NUM { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT / NUM), + vec![(REWARD_2, REWARD_AMOUNT / NUM)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + let pool = LM::pool(pid).unwrap(); + assert_eq!(pool.pool_id, pid); + } + }); +} + +#[test] +fn create_pool_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + LM::create_pool( + Origin::root(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + ), + DispatchError::BadOrigin, + ); + + assert_noop!( + LM::create_pool( + Origin::none(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + ), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn create_pool_with_duplicate_trading_pair_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_1), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + ), + Error::::InvalidTradingPair, + ); + }); +} + +#[test] +fn create_pool_with_too_small_duration_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + MinimumDuration::get() - 1, + 1_000 * UNIT, + 0 + ), + Error::::InvalidDuration, + ); + }); +} + +#[test] +fn create_pool_with_wrong_condition_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + MinimumDeposit::get() - 1, + 0 + ), + Error::::InvalidDepositLimit, + ); + + assert_noop!( + LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + MaximumDepositInPool::get() + 1, + 0 + ), + Error::::InvalidDepositLimit, + ); + }); +} + +#[test] +fn create_pool_with_too_small_per_block_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + (REWARD_AMOUNT + 1) as BlockNumber, + 1_000 * UNIT, + 0 + ), + Error::::InvalidRewardPerBlock, + ); + }); +} + +#[test] +fn create_pool_with_duplicate_reward_should_fail() { + new_test_ext().execute_with(|| { + let result = LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_1, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0, + ); + assert_noop!(result, Error::::DuplicateReward,); + }); +} + +#[test] +fn approve_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.state, PoolState::UnderAudit); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.state, PoolState::Approved); + + assert!(LM::approved_pids().contains(&0)); + }); +} + +#[test] +fn approve_pool_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + assert_noop!(LM::approve_pool(Some(TC_MEMBER_1).into(), 0), DispatchError::BadOrigin); + assert_noop!(LM::approve_pool(Origin::root(), 0), DispatchError::BadOrigin); + assert_noop!(LM::approve_pool(Origin::none(), 0), DispatchError::BadOrigin); + }); +} + +#[test] +fn approve_pool_with_wrong_state_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_noop!( + LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0), + Error::::InvalidPoolState + ); + }); +} + +#[test] +fn approve_pool_exceed_maximum_should_fail() { + new_test_ext().execute_with(|| { + for i in 0..MaximumApproved::get() as u128 { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT / (MaximumApproved::get() + 1) as u128), + vec![(REWARD_2, REWARD_AMOUNT / (MaximumApproved::get() + 1) as u128)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + assert_ok!(LM::approve_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + i + )); + + assert!(LM::approved_pids().contains(&i)); + } + + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT / (MaximumApproved::get() + 1) as u128), + vec![(REWARD_2, REWARD_AMOUNT / (MaximumApproved::get() + 1) as u128)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + assert_noop!( + LM::approve_pool( + pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), + MaximumApproved::get() as u128, + ), + Error::::ExceedMaximumApproved + ); + + assert!(!LM::approved_pids().contains(&(MaximumApproved::get() as u128))); + }); +} + +#[test] +fn kill_pool_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.state, PoolState::UnderAudit); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let reserved = per_block * DAYS as Balance; + let free = REWARD_AMOUNT - reserved; + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, reserved); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, reserved); + + assert_ok!(LM::kill_pool(Some(CREATOR).into(), 0)); + + assert!(!TotalPoolInfos::::contains_key(0)); + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, REWARD_AMOUNT); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, REWARD_AMOUNT); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, 0); + }); +} + +#[test] +fn kill_pool_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + assert_noop!(LM::kill_pool(Some(USER_1).into(), 0), Error::::InvalidPoolOwner); + assert_noop!(LM::kill_pool(Origin::root(), 0), DispatchError::BadOrigin); + assert_noop!(LM::kill_pool(Origin::none(), 0), DispatchError::BadOrigin); + }); +} + +#[test] +fn kill_pool_with_wrong_state_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_pool( + Some(CREATOR).into(), + (FARMING_DEPOSIT_1, FARMING_DEPOSIT_2), + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + PoolType::Farming, + DAYS, + 1_000 * UNIT, + 0 + )); + + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_noop!(LM::kill_pool(Some(CREATOR).into(), 0), Error::::InvalidPoolState); + }); +} + +#[test] +fn deposit_to_mining_pool_approved_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + let deposit = 1_000_000 as Balance; + assert_ok!(LM::deposit(Some(USER_1).into(), 0, deposit)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, deposit)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, 2 * deposit); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.deposit, 2 * deposit); + + let deposit_data = LM::user_deposit_data(USER_1, 0).unwrap(); + assert_eq!(deposit_data.deposit, 2 * deposit); + }); +} + +#[test] +fn deposit_to_farming_pool_approved_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_farming_pool( + Some(CREATOR).into(), + 2001, + 13, + 20, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + let deposit = 1_000_000 as Balance; + assert_ok!(LM::deposit(Some(USER_1).into(), 0, deposit)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, deposit)); + + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).frozen, 2 * deposit); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_1).reserved, 0); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).frozen, 2 * deposit); + assert_eq!(Tokens::accounts(USER_1, FARMING_DEPOSIT_2).reserved, 0); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.deposit, 2 * deposit); + + let deposit_data = LM::user_deposit_data(USER_1, 0).unwrap(); + assert_eq!(deposit_data.deposit, 2 * deposit); + }); +} + +#[test] +fn startup_pool_meet_conditions_should_auto_work() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(101); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.state, PoolState::Ongoing); + + assert!(!LM::approved_pids().contains(&0)); + }); +} + +#[test] +fn deposit_to_pool_ongoing_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, DEPOSIT_AMOUNT)); + + run_to_block(101); + + assert_ok!(LM::deposit(Some(USER_2).into(), 0, DEPOSIT_AMOUNT)); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + + let pool = LM::pool(0).unwrap(); + assert_eq!(pool.deposit, 2 * DEPOSIT_AMOUNT); + + let deposit_data = LM::user_deposit_data(USER_2, 0).unwrap(); + assert_eq!(deposit_data.deposit, DEPOSIT_AMOUNT); + }); +} + +#[test] +fn deposit_to_pool_ongoing_with_init_deposit_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, 1_000_000)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, 1_000_000)); + + run_to_block(200); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, 1_000_000)); + + let per_block: Balance = REWARD_AMOUNT / DAYS as Balance; + let pbpd: U64F64 = U64F64::from_num(per_block) / (2 * 1_000_000); + let reward_to_user_1: Balance = u128::from_fixed(pbpd * 100 * 1_000_000); + + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, reward_to_user_1); + 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_to_user_1); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + + let pool: PoolInfo = LM::pool(0).unwrap(); + assert_eq!(pool.deposit, 3_000_000); + + for (_rtoken, reward) in pool.rewards.iter() { + assert_eq!(reward.claimed, reward_to_user_1); + } + + let deposit_data_1 = LM::user_deposit_data(USER_1, 0).unwrap(); + assert_eq!(deposit_data_1.deposit, 2_000_000); + let deposit_data_2 = LM::user_deposit_data(USER_2, 0).unwrap(); + assert_eq!(deposit_data_2.deposit, 1_000_000); + }); +} + +#[test] +fn double_deposit_to_pool_ongoing_in_diff_block_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, 1_000_000)); + + run_to_block(200); + + assert_ok!(LM::deposit(Some(USER_2).into(), 0, 1_000_000)); + + System::set_block_number(300); + + assert_ok!(LM::deposit(Some(USER_2).into(), 0, 1_000_000)); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = U64F64::from_num(per_block) / 2_000_000; + let reward_to_user_2: Balance = u128::from_fixed(pbpd * 100 * 1_000_000); + + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, reward_to_user_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_to_user_2); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + + let pool: PoolInfo = LM::pool(0).unwrap(); + assert_eq!(pool.deposit, 3_000_000); + + for (_rtoken, reward) in pool.rewards.iter() { + assert_eq!(reward.claimed, reward_to_user_2); + } + + let deposit_data_1 = LM::user_deposit_data(USER_1, 0).unwrap(); + assert_eq!(deposit_data_1.deposit, 1_000_000); + let deposit_data_2 = LM::user_deposit_data(USER_2, 0).unwrap(); + assert_eq!(deposit_data_2.deposit, 2_000_000); + }); +} + +#[test] +fn double_deposit_to_pool_ongoing_in_same_block_should_work() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, 1_000_000)); + + run_to_block(200); + + assert_ok!(LM::deposit(Some(USER_2).into(), 0, 1_000_000)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, 1_000_000)); + + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, 0); + 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, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + + let pool: PoolInfo = LM::pool(0).unwrap(); + assert_eq!(pool.deposit, 3_000_000); + + for (_rtoken, reward) in pool.rewards.iter() { + assert_eq!(reward.claimed, 0); + } + + let deposit_data_1 = LM::user_deposit_data(USER_1, 0).unwrap(); + assert_eq!(deposit_data_1.deposit, 1_000_000); + let deposit_data_2 = LM::user_deposit_data(USER_2, 0).unwrap(); + assert_eq!(deposit_data_2.deposit, 2_000_000); + }); +} + +#[test] +fn deposit_with_wrong_pid_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!(LM::deposit(Some(USER_1).into(), 0, 1_000_000), Error::::InvalidPoolId); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_noop!(LM::deposit(Some(USER_1).into(), 1, 1_000_000), Error::::InvalidPoolId); + }); +} + +#[test] +fn deposit_with_wrong_state_should_fail() { + new_test_ext().execute_with(|| { + System::set_block_number(100); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + assert_noop!(LM::deposit(Some(USER_1).into(), 0, 1_000_000), Error::::InvalidPoolState); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, 1_000_000)); + + run_to_block(100 + DAYS); + + let result = LM::deposit(Some(USER_1).into(), 0, 1_000_000); + assert_noop!(result, Error::::InvalidPoolState); + }); +} + +#[test] +fn deposit_too_little_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!(LM::deposit(Some(USER_1).into(), 0, 1_000_000), Error::::InvalidPoolId); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_noop!( + LM::deposit(Some(USER_1).into(), 0, MinimumDeposit::get() - 1), + Error::::TooLowToDeposit + ); + }); +} + +#[test] +fn deposit_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!(LM::deposit(Some(USER_1).into(), 0, 1_000_000), Error::::InvalidPoolId); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_noop!(LM::deposit(Origin::root(), 0, 1_000_000), DispatchError::BadOrigin); + assert_noop!(LM::deposit(Origin::none(), 0, 1_000_000), DispatchError::BadOrigin); + }); +} + +#[test] +fn deposit_exceed_the_limit_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!(LM::deposit(Some(USER_1).into(), 0, 1_000_000), Error::::InvalidPoolId); + + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1_000_000, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, 1_000_000)); + assert_noop!( + LM::deposit(Some(RICHER).into(), 0, MaximumDepositInPool::get() + 1), + Error::::ExceedMaximumDeposit + ); + }); +} + +#[test] +fn redeem_from_pool_ongoing_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, UNIT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, UNIT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = U64F64::from_num(per_block) / (2 * UNIT); + let rewarded: Balance = u128::from_fixed((pbpd * 100 * UNIT).floor()); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, rewarded); + 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, rewarded); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + assert!(LM::user_deposit_data(USER_1, 0).is_none()); + + assert_ok!(LM::redeem(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, MinimumDeposit::get()); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, rewarded); + 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, rewarded); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + assert_eq!(LM::user_deposit_data(USER_2, 0).unwrap().deposit, MinimumDeposit::get()); + + assert_eq!(LM::pool(0).unwrap().deposit, MinimumDeposit::get()); + }); +} + +#[test] +fn redeem_from_pool_retired_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, UNIT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, UNIT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + + run_to_block(DAYS); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = U64F64::from_num(per_block) / (2 * UNIT); + let rewarded: Balance = u128::from_fixed((pbpd * DAYS as Balance * UNIT).floor()); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, rewarded); + 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, rewarded); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + assert!(LM::user_deposit_data(USER_1, 0).is_none()); + + assert_ok!(LM::redeem(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, rewarded); + 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, rewarded); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + assert!(LM::user_deposit_data(USER_2, 0).is_none()); + + assert!(LM::pool(0).is_none()); + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, REWARD_AMOUNT - 2 * rewarded); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, REWARD_AMOUNT - 2 * rewarded); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, 0); + }); +} + +#[test] +fn double_redeem_from_pool_in_diff_state_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, UNIT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, UNIT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + + run_to_block(100); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + let pbpd = U64F64::from_num(per_block) / (2 * UNIT); + let old_rewarded: Balance = u128::from_fixed((pbpd * 100 * UNIT).floor()); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0)); + + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, old_rewarded); + 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, old_rewarded); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + assert!(LM::user_deposit_data(USER_1, 0).is_none()); + + assert_ok!(LM::redeem(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, MinimumDeposit::get()); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, old_rewarded); + 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, old_rewarded); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + assert_eq!(LM::user_deposit_data(USER_2, 0).unwrap().deposit, MinimumDeposit::get()); + + assert_eq!(LM::pool(0).unwrap().deposit, MinimumDeposit::get()); + + // USER_2 didn't remember to redeem until the seventh day + run_to_block(7 * DAYS); + let pbpd = U64F64::from_num(per_block) / MinimumDeposit::get(); + let new_rewarded: Balance = + u128::from_fixed((pbpd * (DAYS - 100) as Balance * MinimumDeposit::get()).floor()); + + assert_ok!(LM::redeem(Some(USER_2).into(), 0)); + + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).free, DEPOSIT_AMOUNT); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, MINING_DEPOSIT).reserved, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_1).free, old_rewarded + new_rewarded); + 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, old_rewarded + new_rewarded); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_2, REWARD_2).reserved, 0); + assert!(LM::user_deposit_data(USER_2, 0).is_none()); + + assert!(LM::pool(0).is_none()); + + assert_eq!( + Tokens::accounts(CREATOR, REWARD_1).free, + REWARD_AMOUNT - (2 * old_rewarded + new_rewarded) + ); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, 0); + assert_eq!( + Tokens::accounts(CREATOR, REWARD_2).free, + REWARD_AMOUNT - (2 * old_rewarded + new_rewarded) + ); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, 0); + }); +} + +#[test] +fn redeem_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!(LM::redeem(Origin::root(), 0), DispatchError::BadOrigin); + assert_noop!(LM::redeem(Origin::none(), 0), DispatchError::BadOrigin); + }); +} + +#[test] +fn redeem_with_wrong_pid_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + + run_to_block(100); + + assert_noop!(LM::redeem(Some(USER_1).into(), 1), Error::::InvalidPoolId); + }); +} + +#[test] +fn redeem_with_wrong_state_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + assert_noop!(LM::redeem(Some(USER_1).into(), 0), Error::::InvalidPoolState); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + + assert_noop!(LM::redeem(Some(USER_1).into(), 0), Error::::InvalidPoolState); + }); +} + +#[test] +fn redeem_without_deposit_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + assert_noop!(LM::redeem(Some(USER_1).into(), 0), Error::::InvalidPoolState); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + run_to_block(100); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0)); + assert_noop!(LM::redeem(Some(USER_1).into(), 0), Error::::NoDepositOfUser); + }); +} + +#[test] +fn redeem_all_deposit_from_pool_ongoing_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + assert_noop!(LM::redeem(Some(USER_1).into(), 0), Error::::InvalidPoolState); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + + run_to_block(100); + + assert_ok!(LM::redeem(Some(USER_1).into(), 0)); + let result = LM::redeem(Some(USER_1).into(), 0); + assert_noop!(result, Error::::TooLowDepositInPoolToRedeem); + }); +} + +#[test] +fn claim_from_pool_ongoing_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + run_to_block(100); + + assert_ok!(LM::claim(Some(USER_1).into(), 0)); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + + let reserved = per_block * DAYS as Balance; + let free = REWARD_AMOUNT - reserved; + + let pbpd = U64F64::from_num(per_block) / (2 * UNIT); + let rewarded: Balance = u128::from_fixed((pbpd * 100 * UNIT).floor()); + + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, rewarded); + 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, rewarded); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, reserved - rewarded); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, reserved - rewarded); + + let pool: PoolInfo = LM::pool(0).unwrap(); + assert_eq!(pool.rewards.get(&REWARD_1).unwrap().claimed, rewarded); + assert_eq!(pool.rewards.get(&REWARD_2).unwrap().claimed, rewarded); + }); +} + +#[test] +fn claim_from_pool_retired_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + run_to_block(DAYS); + + assert_ok!(LM::claim(Some(USER_1).into(), 0)); + + let per_block = REWARD_AMOUNT / DAYS as Balance; + + let reserved = per_block * DAYS as Balance; + let free = REWARD_AMOUNT - reserved; + + let pbpd = U64F64::from_num(per_block) / (2 * UNIT); + let rewarded: Balance = u128::from_fixed((pbpd * DAYS as Balance * UNIT).floor()); + + assert_eq!(Tokens::accounts(USER_1, REWARD_1).free, rewarded); + 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, rewarded); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(USER_1, REWARD_2).reserved, 0); + + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_1).reserved, reserved - rewarded); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).free, free); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).frozen, 0); + assert_eq!(Tokens::accounts(CREATOR, REWARD_2).reserved, reserved - rewarded); + + let pool: PoolInfo = LM::pool(0).unwrap(); + assert_eq!(pool.rewards.get(&REWARD_1).unwrap().claimed, rewarded); + assert_eq!(pool.rewards.get(&REWARD_2).unwrap().claimed, rewarded); + }); +} + +#[test] +fn claim_with_wrong_pid_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + run_to_block(DAYS); + + assert_noop!(LM::claim(Some(USER_1).into(), 1), Error::::InvalidPoolId); + }); +} + +#[test] +fn claim_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + run_to_block(DAYS); + + assert_noop!(LM::claim(Origin::root(), 0), DispatchError::BadOrigin); + assert_noop!(LM::claim(Origin::none(), 0), DispatchError::BadOrigin); + }); +} + +#[test] +fn claim_with_wrong_state_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + assert_noop!(LM::claim(Some(USER_1).into(), 0), Error::::InvalidPoolState); + assert_noop!(LM::claim(Some(USER_2).into(), 0), Error::::InvalidPoolState); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + assert_ok!(LM::deposit(Some(USER_2).into(), 0, UNIT)); + + assert_noop!(LM::claim(Some(USER_1).into(), 0), Error::::InvalidPoolState); + assert_noop!(LM::claim(Some(USER_2).into(), 0), Error::::InvalidPoolState); + + run_to_block(DAYS); + }); +} + +#[test] +fn claim_without_deposit_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + + run_to_block(DAYS); + + let result = LM::claim(Some(USER_2).into(), 0); + assert_noop!(result, Error::::NoDepositOfUser); + }); +} + +#[test] +fn double_claim_in_same_block_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(LM::create_mining_pool( + Some(CREATOR).into(), + MINING_TRADING_PAIR, + (REWARD_1, REWARD_AMOUNT), + vec![(REWARD_2, REWARD_AMOUNT)], + DAYS, + 1 * UNIT, + 0 + )); + + // It is unable to call Collective::execute(..) which is private; + assert_ok!(LM::approve_pool(pallet_collective::RawOrigin::Member(TC_MEMBER_1).into(), 0)); + + assert_ok!(LM::deposit(Some(USER_1).into(), 0, UNIT)); + + run_to_block(DAYS); + + assert_ok!(LM::claim(Some(USER_1).into(), 0)); + assert_noop!(LM::claim(Some(USER_1).into(), 0), Error::::TooShortBetweenTwoClaim); + }); +} diff --git a/runtime/asgard/Cargo.toml b/runtime/asgard/Cargo.toml index a363a7ccba..293d73848e 100644 --- a/runtime/asgard/Cargo.toml +++ b/runtime/asgard/Cargo.toml @@ -83,6 +83,7 @@ bifrost-bancor= { path = "../../pallets/bancor",default-features = false } bifrost-bancor-runtime-api = { path = "../../pallets/bancor/rpc/runtime-api", default-features = false } bifrost-flexible-fee = { path = "../../pallets/flexible-fee", default-features = false } bifrost-flexible-fee-rpc-runtime-api = { path = "../../pallets/flexible-fee/rpc/runtime-api", default-features = false } +bifrost-liquidity-mining = { path = "../../pallets/liquidity-mining", default-features = false } bifrost-minter-reward = { path = "../../pallets/minter-reward", default-features = false } bifrost-runtime-common = { package = "bifrost-runtime-common", path = "../common", default-features = false } bifrost-salp = { path = "../../pallets/salp", default-features = false } diff --git a/runtime/asgard/src/lib.rs b/runtime/asgard/src/lib.rs index 020971e68f..190ef52fdc 100644 --- a/runtime/asgard/src/lib.rs +++ b/runtime/asgard/src/lib.rs @@ -1012,6 +1012,28 @@ impl bifrost_vsbond_auction::Config for Runtime { type WeightInfo = weights::bifrost_vsbond_auction::WeightInfo; } +parameter_types! { + pub const RelayChainTokenSymbol: TokenSymbol = TokenSymbol::KSM; + pub const MaximumDepositInPool: Balance = 1_000_000_000 * DOLLARS; + pub const MinimumDepositOfUser: Balance = 1_000_000; + pub const MinimumRewardPerBlock: Balance = 1_000; + pub const MinimumDuration: BlockNumber = DAYS; + pub const MaximumApproved: u32 = 8; +} + +impl bifrost_liquidity_mining::Config for Runtime { + type Event = Event; + type ControlOrigin = + pallet_collective::EnsureProportionAtLeast<_2, _3, AccountId, TechnicalCollective>; + type MultiCurrency = Currencies; + type RelayChainTokenSymbol = RelayChainTokenSymbol; + type MaximumDepositInPool = MaximumDepositInPool; + type MinimumDepositOfUser = MinimumDepositOfUser; + type MinimumRewardPerBlock = MinimumRewardPerBlock; + type MinimumDuration = MinimumDuration; + type MaximumApproved = MaximumApproved; +} + // bifrost runtime end // zenlink runtime start @@ -1210,6 +1232,7 @@ construct_runtime! { Salp: bifrost_salp::{Pallet, Call, Storage, Event} = 105, Bancor: bifrost_bancor::{Pallet, Call, Storage, Event, Config} = 106, VSBondAuction: bifrost_vsbond_auction::{Pallet, Call, Storage, Event} = 107, + LiquidityMining: bifrost_liquidity_mining::{Pallet, Call, Storage, Event} = 108, } }