diff --git a/pallets/proof-of-stake/src/lib.rs b/pallets/proof-of-stake/src/lib.rs index 12258cf33d..9448a9ac16 100644 --- a/pallets/proof-of-stake/src/lib.rs +++ b/pallets/proof-of-stake/src/lib.rs @@ -55,14 +55,20 @@ pub use pallet::*; pub mod weights; pub use weights::WeightInfo; +type AccountIdOf = ::AccountId; + + +/// Wrapper over origin ActivateKind that is used in [`Pallet::activat_liquidity`] +/// with extension that allows activating liquidity that was already used for: +/// - `ActivatedLiquidity` - already activated liquidity (for scheduled rewards) +/// - `LiquidityMining` - already activated liquidity (for liquidity mining rewards) #[derive(Eq, PartialEq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] pub enum ScheduleActivationKind { ActivateKind(Option), - ActivatedLiquidity(TokenId) + ActivatedLiquidity(TokenId), + LiquidityMining, } -type AccountIdOf = ::AccountId; - const PALLET_ID: frame_support::PalletId = frame_support::PalletId(*b"rewards!"); #[frame_support::pallet] @@ -104,8 +110,11 @@ use super::*; type LiquidityMiningIssuanceVault: Get; #[pallet::constant] type RewardsDistributionPeriod: Get; + /// The maximum number of schedules that can be active at one moment type RewardsSchedulesLimit: Get; + /// The minimum number of rewards per session for schedule rewards type MinRewardsPerSession: Get; + /// The maximum number of reward tokens per pool type MaxRewardTokensPerPool: Get; type WeightInfo: WeightInfo; type ValuationApi: Valuate; @@ -130,11 +139,14 @@ use super::*; CalculateRewardsAllMathError, MissingRewardsInfoError, DeprecatedExtrinsic, + /// Cannot schedule rewards in past CannotScheduleRewardsInPast, + /// Pool does not exist PoolDoesNotExist, + /// Too many schedules TooManySchedules, + /// Too little rewards per session TooLittleRewardsPerSession, - PoolRewardTokensLimitExceeded, } #[pallet::event] @@ -158,17 +170,6 @@ use super::*; ValueQuery, >; - #[pallet::storage] - pub type UserRewards3rdPartyInfo = StorageDoubleMap< - _, - Twox64Concat, - AccountIdOf, - Twox64Concat, - (TokenId, TokenId), - RewardInfo, - ValueQuery, - >; - #[pallet::storage] /// Stores information about pool weight and accumulated rewards. The accumulated /// rewards amount is the number of rewards that can be claimed per liquidity @@ -177,31 +178,6 @@ use super::*; pub type PromotedPoolRewards = StorageValue<_, BTreeMap, ValueQuery>; - #[pallet::storage] - pub type PromotedPoolRewards3rdParty = StorageValue<_, BTreeMap<(TokenId, TokenId), U256>, ValueQuery>; - - // pub type PromotedPoolRewards3rdParty = StorageValue<_, BTreeMap<(TokenId), U256>, ValueQuery>; - - #[pallet::storage] - #[pallet::getter(fn schedules)] - pub type RewardsSchedules = - StorageValue<_, BoundedBTreeMap<(T::BlockNumber, TokenId, TokenId, Balance, u64), (), T::RewardsSchedulesLimit>, ValueQuery>; - - #[pallet::storage] - pub type RewardTokensPerPool = StorageMap<_, Twox64Concat, TokenId, BoundedBTreeMap, ValueQuery>; - - #[pallet::storage] - pub type ScheduleId = StorageValue<_, u64, ValueQuery>; - - - // #[pallet::storage] - // pub type Rewards3rdParty = - // StorageValue<_, BTreeMap, ValueQuery>; - // - // #[pallet::storage] - // pub type Rewards3rdParty = - // StorageValue<_, BTreeMap, ValueQuery>; - #[derive(Encode, Decode, Clone, Default, RuntimeDebug, PartialEq, Eq, TypeInfo)] /// Information about single token rewards pub struct PromotedPools { @@ -218,11 +194,77 @@ use super::*; pub type TotalActivatedLiquidity = StorageMap<_, Twox64Concat, TokenId, u128, ValueQuery>; - // NOTE: possibly merge with above + // ////////////////////////////////////////////////////////////////////////////////////////////// + // 3rd Party Rewards + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /// Stores information about pool weight and accumulated rewards #[pallet::storage] - pub type TotalActivatedLiquidity3rdParty = + pub type RewardsInfoForScheduleRewards = StorageDoubleMap< + _, + Twox64Concat, + AccountIdOf, + Twox64Concat, + (TokenId, TokenId), + RewardInfo, + ValueQuery, + >; + + /// How much scheduled rewards per single liquidty_token should be distribute_rewards + /// the **value is multiplied by u128::MAX** to avoid floating point arithmetic + #[pallet::storage] + pub type ScheduleRewardsPerSingleLiquidity = StorageValue<_, BTreeMap<(TokenId, TokenId), U256>, ValueQuery>; + + /// List of activated schedules sorted by expiry date + #[pallet::storage] + #[pallet::getter(fn schedules)] + pub type RewardsSchedules = + StorageValue<_, BoundedBTreeMap<(T::BlockNumber, TokenId, TokenId, Balance, u64), (), T::RewardsSchedulesLimit>, ValueQuery>; + + /// Unique id of the schedule + #[pallet::storage] + pub type ScheduleId = StorageValue<_, u64, ValueQuery>; + + /// Maps liquidity token to list of tokens that it ever was rewarded with + #[pallet::storage] + pub type RewardTokensPerPool = StorageDoubleMap<_, Twox64Concat, TokenId, Twox64Concat, TokenId, (), ValueQuery>; + + /// Total amount of activated liquidity for each schedule + /// `BTreeMap` is used because of storage reads write optiomization in `distribute_rewards` + #[pallet::storage] + pub type TotalActivatedLiquidityForSchedules = StorageMap<_, Twox64Concat, TokenId, BTreeMap, ValueQuery>; + /// Tracks how much liquidity user activated for particular (liq token, reward token) pair + /// StorageNMap was used because it only require single read to know if user deactivated all + /// liquidity associated with particular liquidity_token that is rewarded. If so part of the + /// liquididty tokens can be unlocked. + #[pallet::storage] + pub type ActivatedLiquidityForSchedules = StorageNMap< + _, + ( + NMapKey>, + NMapKey, + NMapKey + ), + u128, + OptionQuery, + >; + + /// Tracks how much of the liquidity was activated for schedule rewards and not yet + /// liquidity mining rewards. That information is essential to properly handle tocken unlcocks + /// when liquidity is deactivated. + #[pallet::storage] + pub type ActivatedLockedLiquidityForSchedules = StorageDoubleMap< + _, + Twox64Concat, + AccountIdOf, + Twox64Concat, + TokenId, + u128, + ValueQuery, + >; + #[pallet::call] impl Pallet { /// Claims liquidity mining rewards @@ -310,10 +352,11 @@ use super::*; /// Schedules rewards for selected liquidity token /// - tokens - pair of tokens /// - amount - amount of the token - /// - schedule_end - till when the rewards should be distributed, rewards are - /// distributed starting from the *next* session till + /// - schedule_end - id of the last rewarded seession. Rewards will be distributedd equally between sessions in range (now .. + /// schedule_end). Distribution starts from the *next* session till `schedule_end`. #[transactional] #[pallet::call_index(4)] + // NOTE: implement benchmark #[pallet::weight(<::WeightInfo>::claim_rewards_all())] pub fn reward_pool( origin: OriginFor, @@ -342,12 +385,7 @@ use super::*; ensure!(amount_per_session >= T::MinRewardsPerSession::get(), Error::::TooLittleRewardsPerSession); - RewardTokensPerPool::::try_mutate(rewarded_token, |tokens| { - - tokens.try_insert(token_id,()) - }).or(Err(Error::::PoolRewardTokensLimitExceeded))?; - - + RewardTokensPerPool::::insert(rewarded_token, token_id, ()); T::Currency::transfer( token_id.into(), @@ -386,7 +424,8 @@ use super::*; /// Parameters: /// - liquidity_token_id - id of the token /// - amount - amount of the token - /// - use_balance_from - where from tokens should be used + /// - use_balance_from - where from tokens should be used. If set to `None` then tokens will + /// be taken from available balance #[transactional] #[pallet::call_index(5)] #[pallet::weight(<::WeightInfo>::activate_liquidity())] @@ -443,59 +482,6 @@ pub enum RewardsKind{ impl Pallet { - fn activate_liquidity_for_schedule( - user: AccountIdOf, - liquidity_asset_id: TokenId, - amount: Balance, - use_balance_from: ScheduleActivationKind, - reward_token: TokenId - ) -> DispatchResult { - Self::ensure_is_promoted_pool(liquidity_asset_id)?; - - match use_balance_from{ - ScheduleActivationKind::ActivateKind(ref use_balance_from) => { - ensure!( - ::ActivationReservesProvider::can_activate( - liquidity_asset_id, - &user, - amount, - use_balance_from.clone(), - ), - Error::::NotEnoughAssets - ); - }, - ScheduleActivationKind::ActivatedLiquidity(token_id) => { - let already_activated_amount = - UserRewards3rdPartyInfo::::get(user.clone(), (liquidity_asset_id, reward_token)).activated_amount; - let available_amount = - UserRewards3rdPartyInfo::::get(user.clone(), (liquidity_asset_id, token_id)).activated_amount; - println!("already_activated_amount : {already_activated_amount:?}"); - println!("available_amount : {available_amount:?}"); - - ensure!( - already_activated_amount + amount <= available_amount , - Error::::NotEnoughAssets - ); - } - } - - Self::set_liquidity_minting_checkpoint_3rdparty(user.clone(), liquidity_asset_id, amount, reward_token)?; - - if let ScheduleActivationKind::ActivateKind(use_balance_from) = use_balance_from { - ::ActivationReservesProvider::activate( - liquidity_asset_id, - &user, - amount, - use_balance_from, - )?; - } - - Pallet::::deposit_event(Event::LiquidityActivated(user, liquidity_asset_id, amount)); - - Ok(()) - } - - fn activate_liquidity_for_liquidity_minting( user: AccountIdOf, liquidity_asset_id: TokenId, @@ -516,7 +502,6 @@ impl Pallet { Self::set_liquidity_minting_checkpoint(user.clone(), liquidity_asset_id, amount)?; - // This must not fail due storage edits above ::ActivationReservesProvider::activate( liquidity_asset_id, &user, @@ -546,6 +531,73 @@ impl Pallet { } + fn activate_liquidity_for_schedule( + user: AccountIdOf, + liquidity_asset_id: TokenId, + amount: Balance, + use_balance_from: ScheduleActivationKind, + reward_token: TokenId + ) -> DispatchResult { + Self::ensure_is_promoted_pool(liquidity_asset_id)?; + + match use_balance_from{ + ScheduleActivationKind::ActivateKind(ref use_balance_from) => { + ensure!( + ::ActivationReservesProvider::can_activate( + liquidity_asset_id, + &user, + amount, + use_balance_from.clone(), + ), + Error::::NotEnoughAssets + ); + ActivatedLockedLiquidityForSchedules::::mutate(user.clone(), liquidity_asset_id, |val| *val += amount); + }, + ScheduleActivationKind::ActivatedLiquidity(token_id) => { + let already_activated_amount = + RewardsInfoForScheduleRewards::::get(user.clone(), (liquidity_asset_id, reward_token)).activated_amount; + let available_amount = + RewardsInfoForScheduleRewards::::get(user.clone(), (liquidity_asset_id, token_id)).activated_amount; + ensure!( + already_activated_amount + amount <= available_amount , + Error::::NotEnoughAssets + ); + } + ScheduleActivationKind::LiquidityMining => { + let already_activated_amount = + RewardsInfoForScheduleRewards::::get(user.clone(), (liquidity_asset_id, reward_token)).activated_amount; + let available_amount = + RewardsInfo::::get(user.clone(), liquidity_asset_id).activated_amount; + ensure!( + already_activated_amount + amount <= available_amount , + Error::::NotEnoughAssets + ); + }, + } + + Self::set_liquidity_minting_checkpoint_3rdparty(user.clone(), liquidity_asset_id, amount, reward_token)?; + + match use_balance_from { + ScheduleActivationKind::ActivateKind(use_balance_from) => { + ::ActivationReservesProvider::activate( + liquidity_asset_id, + &user, + amount, + use_balance_from, + )?; + }, + ScheduleActivationKind::LiquidityMining => { + }, + _ => {} + + } + + Pallet::::deposit_event(Event::LiquidityActivated(user, liquidity_asset_id, amount)); + + Ok(()) + } + + fn deactivate_liquidity_for_schedule( user: AccountIdOf, liquidity_asset_id: TokenId, @@ -575,31 +627,11 @@ impl Pallet { ) -> Result { Self::ensure_is_promoted_pool(liquidity_asset_id)?; - // let mut result: sp_std::vec::vec<_> = default::default(); - // - // // todo: get rid of collect - // let reward_tokens = rewardtokensperpool::::get(liquidity_asset_id) - // .iter() - // .filter_map(|(token_id,_)| - // userrewards3rdpartyinfo::::try_get(user.clone(), (liquidity_asset_id, token_id)) - // ) - // .map(|info| (info.last_checkpoint, info.activated_amount) - // .sort() - // .first - // .collect::>(); - // - // - - - // let rewards_info = UserRewards3rdPartyInfo::::try_get(user.clone(), (liquidity_asset_id, rewards_asset_id)); - // - println!("hello world 0000"); - if let Ok(info) = UserRewards3rdPartyInfo::::try_get(user.clone(), (liquidity_asset_id, rewards_asset_id)){ - println!("hello world"); + if let Ok(info) = RewardsInfoForScheduleRewards::::try_get(user.clone(), (liquidity_asset_id, rewards_asset_id)){ let current_rewards = match info.activated_amount { 0 => 0u128, _ => { - let calc = RewardsCalculator::::new2::( + let calc = RewardsCalculator::schedule_rewards::( user.clone(), liquidity_asset_id, rewards_asset_id @@ -644,15 +676,6 @@ impl Pallet { .ok_or(Error::::NotAPromotedPool)?) } - fn get_pool_rewards_3rdparty(liquidity_asset_id: TokenId, reward_asset_id: TokenId) -> Result { - - Ok(*PromotedPoolRewards3rdParty::::get() - .get(&(liquidity_asset_id, reward_asset_id)) - //TODO: no error or some dedicated error - .ok_or(Error::::NotAPromotedPool)?) - } - - fn get_current_rewards_time() -> Result { >::block_number() .saturated_into::() @@ -662,12 +685,9 @@ impl Pallet { } fn ensure_is_promoted_pool(liquidity_asset_id: TokenId) -> Result<(), DispatchError> { - - if Self::get_pool_rewards(liquidity_asset_id).is_ok() || !RewardTokensPerPool::::get(liquidity_asset_id).is_empty() { - + if Self::get_pool_rewards(liquidity_asset_id).is_ok() || RewardTokensPerPool::::iter_prefix_values(liquidity_asset_id).next().is_some() { Ok(()) } else { - Err(DispatchError::from(Error::::NotAPromotedPool)) } } @@ -680,14 +700,13 @@ impl Pallet { Self::ensure_is_promoted_pool(liquidity_asset_id)?; { - let calc = RewardsCalculator::::new::( + let calc = RewardsCalculator::mining_rewards::( user.clone(), liquidity_asset_id, )?; let rewards_info = calc .activate_more(liquidity_assets_added) .map_err(|err| Into::>::into(err))?; - println!("liq minting rewards_info : {rewards_info:?}"); RewardsInfo::::insert(user.clone(), liquidity_asset_id, rewards_info); } @@ -716,7 +735,7 @@ impl Pallet { Self::ensure_is_promoted_pool(liquidity_asset_id)?; { - let calc = RewardsCalculator::::new2::( + let calc = RewardsCalculator::schedule_rewards::( user.clone(), liquidity_asset_id, liquidity_assets_reward @@ -724,12 +743,26 @@ impl Pallet { let rewards_info = calc .activate_more(liquidity_assets_added) .map_err(|err| Into::>::into(err))?; - - UserRewards3rdPartyInfo::::insert(user.clone(), (liquidity_asset_id, liquidity_assets_reward), rewards_info); + RewardsInfoForScheduleRewards::::insert(user.clone(), (liquidity_asset_id, liquidity_assets_reward), rewards_info); } + ActivatedLiquidityForSchedules::::try_mutate_exists( + (user.clone(), liquidity_asset_id, liquidity_assets_reward ), + |v| + { + match v { + Some(x) => { + v.as_mut().map(|a| *a += liquidity_assets_added); + }, + None => { + *v = Some(liquidity_assets_added); + }, + }; + Ok::<(),Error>(()) + } + )?; - TotalActivatedLiquidity3rdParty::::mutate(liquidity_asset_id, |activations| { + TotalActivatedLiquidityForSchedules::::mutate(liquidity_asset_id, |activations| { activations .entry(liquidity_assets_reward) // NOTE: handle overflow @@ -757,7 +790,7 @@ impl Pallet { Error::::NotEnoughAssets ); - let calc = RewardsCalculator::::new::( + let calc = RewardsCalculator::mining_rewards::( user.clone(), liquidity_asset_id, )?; @@ -795,7 +828,7 @@ impl Pallet { ) -> DispatchResult { Self::ensure_is_promoted_pool(liquidity_asset_id)?; - let calc = RewardsCalculator::::new2::( + let calc = RewardsCalculator::schedule_rewards::( user.clone(), liquidity_asset_id, reward_token, @@ -805,19 +838,48 @@ impl Pallet { .activate_less(liquidity_assets_burned) .map_err(|err| Into::>::into(err))?; - UserRewards3rdPartyInfo::::insert(user.clone(), (liquidity_asset_id, reward_token), rewards_info); + RewardsInfoForScheduleRewards::::insert(user.clone(), (liquidity_asset_id, reward_token), rewards_info); - TotalActivatedLiquidity3rdParty::::mutate(liquidity_asset_id, |activations| { + TotalActivatedLiquidityForSchedules::::mutate(liquidity_asset_id, |activations| { activations .entry(reward_token) .and_modify(|val| *val -= liquidity_assets_burned); }); - ::ActivationReservesProvider::deactivate( - liquidity_asset_id, - &user, - liquidity_assets_burned, - ); + ActivatedLiquidityForSchedules::::try_mutate_exists( + (user.clone(), liquidity_asset_id, reward_token), + |v| + { + v.and_then(|a| a.checked_sub(liquidity_assets_burned) + .and_then(|val| { + if val > 0 { + *v = Some(val); + }else{ + *v = None; + } + Some(val) + }) + ).ok_or(Error::::MathOverflow) + } + )?; + + + + if let None = ActivatedLiquidityForSchedules::::iter_prefix_values( (user.clone(), liquidity_asset_id), + ).next(){ + + let amount = ActivatedLockedLiquidityForSchedules::::mutate(user.clone(), liquidity_asset_id, |val| { + let prev = *val; + *val = 0; + prev + }); + + ::ActivationReservesProvider::deactivate( + liquidity_asset_id, + &user, + amount, + ); + } Ok(()) } @@ -860,7 +922,7 @@ impl ProofOfStakeRewardsApi for Pallet { let mut rewards_info = RewardsInfo::::try_get(user.clone(), liquidity_asset_id) .or(Err(DispatchError::from(Error::::MissingRewardsInfoError)))?; - let calc = RewardsCalculator::::new::( + let calc = RewardsCalculator::mining_rewards::( user.clone(), liquidity_asset_id, )?; @@ -932,7 +994,7 @@ impl ProofOfStakeRewardsApi for Pallet { let current_rewards = match rewards_info.activated_amount { 0 => 0u128, _ => { - let calc = RewardsCalculator::::new::( + let calc = RewardsCalculator::mining_rewards::( user.clone(), liquidity_asset_id, )?; @@ -952,7 +1014,7 @@ impl LiquidityMiningApi for Pallet { fn distribute_rewards(liquidity_mining_rewards: Balance) { let schedules = RewardsSchedules::::get(); - let mut pools = PromotedPoolRewards3rdParty::::get(); + let mut pools = ScheduleRewardsPerSingleLiquidity::::get(); let it = schedules .iter() @@ -966,7 +1028,7 @@ impl LiquidityMiningApi for Pallet { for (staked_token, token, amount) in it { - let activated_3rdparty_rewards = TotalActivatedLiquidity3rdParty::::get(staked_token); + let activated_3rdparty_rewards = TotalActivatedLiquidityForSchedules::::get(staked_token); if let Some(activated_amount) = activated_3rdparty_rewards.get(&token){ let activated_amount = U256::from(*activated_amount); @@ -985,8 +1047,7 @@ impl LiquidityMiningApi for Pallet { } } - println!("POOLS : {:?}", pools); - PromotedPoolRewards3rdParty::::put(pools); + ScheduleRewardsPerSingleLiquidity::::put(pools); let _ = PromotedPoolRewards::::try_mutate(|promoted_pools| -> DispatchResult { diff --git a/pallets/proof-of-stake/src/reward_info.rs b/pallets/proof-of-stake/src/reward_info.rs index e5321722bd..d2707536b8 100644 --- a/pallets/proof-of-stake/src/reward_info.rs +++ b/pallets/proof-of-stake/src/reward_info.rs @@ -44,8 +44,8 @@ pub struct RewardsCalculator { _curve: sp_std::marker::PhantomData, } -impl RewardsCalculator { - pub fn new( +impl RewardsCalculator { + pub fn mining_rewards( user: T::AccountId, asset_id: TokenId, ) -> sp_std::result::Result { @@ -74,17 +74,27 @@ impl RewardsCalculator { } } -impl RewardsCalculator { - pub fn new2( +impl RewardsCalculator { + pub fn schedule_rewards( user: T::AccountId, asset_id: TokenId, reward_asset_id: TokenId, ) -> sp_std::result::Result { let current_time: u32 = Pallet::::get_current_rewards_time()?; + ensure!( + crate::RewardTokensPerPool::::try_get(asset_id, reward_asset_id).is_ok(), + crate::Error::::NotAPromotedPool + ); + + let pool_map = crate::ScheduleRewardsPerSingleLiquidity::::get(); + + let pool_ratio_current = pool_map.get(&(asset_id, reward_asset_id)) + .cloned() + .unwrap_or(U256::from(0)); + + - // TODO: do not ignore error - let pool_ratio_current = Pallet::::get_pool_rewards_3rdparty(asset_id, reward_asset_id).unwrap_or_default(); let default_rewards = RewardInfo { activated_amount: 0_u128, rewards_not_yet_claimed: 0_u128, @@ -95,7 +105,7 @@ impl RewardsCalculator { }; - let rewards_info = crate::UserRewards3rdPartyInfo::::try_get( + let rewards_info = crate::RewardsInfoForScheduleRewards::::try_get( user.clone(), (asset_id, reward_asset_id) ).unwrap_or(default_rewards); diff --git a/pallets/proof-of-stake/src/tests.rs b/pallets/proof-of-stake/src/tests.rs index a7255d1daa..ed9bfb4521 100644 --- a/pallets/proof-of-stake/src/tests.rs +++ b/pallets/proof-of-stake/src/tests.rs @@ -1312,36 +1312,6 @@ fn reject_schedule_with_too_little_rewards_per_session() { }); } -#[test] -#[serial] -fn number_of_reward_tokens_per_pool_is_limited() { - new_test_ext().execute_with(|| { - System::set_block_number(1); - let valuation_mock = MockValuationApi::get_liquidity_asset_context(); - valuation_mock.expect().return_const(Ok(10u32)); - - let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); - let amount = 10_000u128; - - for _ in 0..<::MaxRewardTokensPerPool as sp_core::Get>::get() { - let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); - assert_ok!( - ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()) - ); - } - - let token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); - assert_err!( - ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, token_id, amount, 5u32.into()), - Error::::PoolRewardTokensLimitExceeded - ); - - - - }); -} - - #[test] #[serial] fn user_can_claim_3rdparty_rewards() { @@ -1555,3 +1525,197 @@ fn deactivate_3rdparty_rewards() { }); } + +#[test] +#[serial] +fn claim_rewards_from_multiple_schedules_using_single_liquidity() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + const LIQUIDITY_TOKEN : u32 = 5; + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(LIQUIDITY_TOKEN)); + + assert_eq!(0, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(1, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(2, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(3, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(4, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(5, TokensOf::::create(&ALICE, 0).unwrap()); + + let first_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let second_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + TokensOf::::mint(LIQUIDITY_TOKEN, &BOB, 100).unwrap(); + + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + ProofOfStake::update_pool_promotion(RuntimeOrigin::root(), LIQUIDITY_TOKEN, 1u8).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, first_token_id, amount, 10u32.into()).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, second_token_id, 2 * amount, 10u32.into()).unwrap(); + + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, first_token_id, None).unwrap(); + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, second_token_id, Some(ScheduleActivationKind::ActivatedLiquidity(first_token_id))).unwrap(); + + assert_eq!( + ProofOfStake::calculate_rewards_amount_3rdparty(BOB, LIQUIDITY_TOKEN, first_token_id), + Ok(0) + ); + assert_eq!( + ProofOfStake::calculate_rewards_amount_3rdparty(BOB, LIQUIDITY_TOKEN, second_token_id), + Ok(0) + ); + + roll_to_session(1); + + assert_eq!( + ProofOfStake::calculate_rewards_amount_3rdparty(BOB, LIQUIDITY_TOKEN, first_token_id), + Ok(1000) + ); + assert_eq!( + ProofOfStake::calculate_rewards_amount_3rdparty(BOB, LIQUIDITY_TOKEN, second_token_id), + Ok(2000) + ); + + }); +} + +#[test] +#[serial] +fn liquidity_minting_liquidity_can_be_resused() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + const LIQUIDITY_TOKEN : u32 = 5; + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(LIQUIDITY_TOKEN)); + + assert_eq!(0, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(1, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(2, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(3, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(4, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(5, TokensOf::::create(&ALICE, 0).unwrap()); + + let first_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let second_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + TokensOf::::mint(LIQUIDITY_TOKEN, &BOB, 100).unwrap(); + + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + ProofOfStake::update_pool_promotion(RuntimeOrigin::root(), LIQUIDITY_TOKEN, 1u8).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, first_token_id, amount, 10u32.into()).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, second_token_id, 2 * amount, 10u32.into()).unwrap(); + + ProofOfStake::activate_liquidity(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, None).unwrap(); + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, first_token_id, Some(ScheduleActivationKind::LiquidityMining)).unwrap(); + + roll_to_session(1); + + assert_eq!( + ProofOfStake::calculate_rewards_amount(BOB, LIQUIDITY_TOKEN), + Ok(200) + ); + assert_eq!( + ProofOfStake::calculate_rewards_amount_3rdparty(BOB, LIQUIDITY_TOKEN, first_token_id), + Ok(1000) + ); + + }); +} + +#[test] +#[serial] +fn when_liquidity_mining_is_reused_it_is_unlocked_properly() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + const LIQUIDITY_TOKEN : u32 = 5; + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(LIQUIDITY_TOKEN)); + + assert_eq!(0, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(1, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(2, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(3, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(4, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(5, TokensOf::::create(&ALICE, 0).unwrap()); + + let first_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let second_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + TokensOf::::mint(LIQUIDITY_TOKEN, &BOB, 100).unwrap(); + + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + ProofOfStake::update_pool_promotion(RuntimeOrigin::root(), LIQUIDITY_TOKEN, 1u8).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, first_token_id, amount, 10u32.into()).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, second_token_id, 2 * amount, 10u32.into()).unwrap(); + + ProofOfStake::activate_liquidity(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, None).unwrap(); + assert_err!( + TokensOf::::transfer(LIQUIDITY_TOKEN, &BOB, &CHARLIE, 100, ExistenceRequirement::AllowDeath), + orml_tokens::Error::::BalanceTooLow + ); + + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, first_token_id, Some(ScheduleActivationKind::LiquidityMining)).unwrap(); + + TokensOf::::mint(LIQUIDITY_TOKEN, &BOB, 100).unwrap(); + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, first_token_id, None).unwrap(); + + ProofOfStake::deactivate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 200, first_token_id).unwrap(); + assert_err!( + TokensOf::::transfer(LIQUIDITY_TOKEN, &BOB, &CHARLIE, 101, ExistenceRequirement::AllowDeath), + orml_tokens::Error::::BalanceTooLow + ); + + assert_ok!( + TokensOf::::transfer(LIQUIDITY_TOKEN, &BOB, &CHARLIE, 100, ExistenceRequirement::AllowDeath) + ); + }); +} + +#[test] +#[serial] +fn liquidity_can_be_deactivated_when_all_reward_participation_were_deactivated() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + const LIQUIDITY_TOKEN : u32 = 5; + let valuation_mock = MockValuationApi::get_liquidity_asset_context(); + valuation_mock.expect().return_const(Ok(LIQUIDITY_TOKEN)); + + assert_eq!(0, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(1, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(2, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(3, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(4, TokensOf::::create(&ALICE, 0).unwrap()); + assert_eq!(5, TokensOf::::create(&ALICE, 0).unwrap()); + + let first_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + let second_token_id = TokensOf::::create(&ALICE, MILLION).unwrap(); + TokensOf::::mint(LIQUIDITY_TOKEN, &BOB, 100).unwrap(); + + let pair: (TokenId, TokenId) = (0u32.into(), 4u32.into()); + let amount = 10_000u128; + + ProofOfStake::update_pool_promotion(RuntimeOrigin::root(), LIQUIDITY_TOKEN, 1u8).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, first_token_id, amount, 10u32.into()).unwrap(); + ProofOfStake::reward_pool(RuntimeOrigin::signed(ALICE), pair, second_token_id, amount, 10u32.into()).unwrap(); + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, first_token_id, None).unwrap(); + ProofOfStake::activate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, second_token_id, Some(ScheduleActivationKind::ActivatedLiquidity(first_token_id))).unwrap(); + + assert_err!( + TokensOf::::transfer(0, &BOB, &CHARLIE, 100, ExistenceRequirement::AllowDeath), + orml_tokens::Error::::BalanceTooLow + ); + ProofOfStake::deactivate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, first_token_id).unwrap(); + assert_err!( + TokensOf::::transfer(LIQUIDITY_TOKEN, &BOB, &CHARLIE, 100, ExistenceRequirement::AllowDeath), + orml_tokens::Error::::BalanceTooLow + ); + ProofOfStake::deactivate_liquidity_for_rewards_schedule(RuntimeOrigin::signed(BOB), LIQUIDITY_TOKEN, 100, second_token_id).unwrap(); + + assert_ok!( + TokensOf::::transfer(LIQUIDITY_TOKEN, &BOB, &CHARLIE, 100, ExistenceRequirement::AllowDeath), + ); + }); +} +