diff --git a/pallets/proof-of-stake/src/lib.rs b/pallets/proof-of-stake/src/lib.rs index 12258cf33d..9f2904bab6 100644 --- a/pallets/proof-of-stake/src/lib.rs +++ b/pallets/proof-of-stake/src/lib.rs @@ -58,7 +58,8 @@ pub use weights::WeightInfo; #[derive(Eq, PartialEq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] pub enum ScheduleActivationKind { ActivateKind(Option), - ActivatedLiquidity(TokenId) + ActivatedLiquidity(TokenId), + LiquidityMining, } type AccountIdOf = ::AccountId; @@ -104,8 +105,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 3rdparty rewards type MinRewardsPerSession: Get; + /// The maximum number of reward tokens per pool type MaxRewardTokensPerPool: Get; type WeightInfo: WeightInfo; type ValuationApi: Valuate; @@ -180,8 +184,8 @@ use super::*; #[pallet::storage] pub type PromotedPoolRewards3rdParty = StorageValue<_, BTreeMap<(TokenId, TokenId), U256>, ValueQuery>; - // pub type PromotedPoolRewards3rdParty = StorageValue<_, BTreeMap<(TokenId), U256>, ValueQuery>; + /// Stores information about set of active schedules #[pallet::storage] #[pallet::getter(fn schedules)] pub type RewardsSchedules = @@ -223,6 +227,32 @@ use super::*; pub type TotalActivatedLiquidity3rdParty = StorageMap<_, Twox64Concat, TokenId, BTreeMap, ValueQuery>; + + #[pallet::storage] + pub type ActivatedScheduleLiquidity = StorageNMap< + _, + ( + NMapKey>, + NMapKey, + NMapKey + ), + u128, + OptionQuery, + >; + + #[pallet::storage] + pub type MaxActivatedLiquidity = StorageDoubleMap< + _, + Twox64Concat, + AccountIdOf, + Twox64Concat, + TokenId, + u128, + ValueQuery, + >; + + + #[pallet::call] impl Pallet { /// Claims liquidity mining rewards @@ -463,31 +493,45 @@ impl Pallet { ), Error::::NotEnoughAssets ); + MaxActivatedLiquidity::::mutate(user.clone(), liquidity_asset_id, |val| *val += amount); }, 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 ); } + ScheduleActivationKind::LiquidityMining => { + let already_activated_amount = + UserRewards3rdPartyInfo::::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)?; - if let ScheduleActivationKind::ActivateKind(use_balance_from) = use_balance_from { - ::ActivationReservesProvider::activate( - liquidity_asset_id, - &user, - amount, - use_balance_from, - )?; + 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)); @@ -516,7 +560,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, @@ -575,27 +618,7 @@ 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"); let current_rewards = match info.activated_amount { 0 => 0u128, _ => { @@ -687,7 +710,6 @@ impl Pallet { 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); } @@ -724,10 +746,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); } + ActivatedScheduleLiquidity::::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>(()) + } + )?; + + let x = ActivatedScheduleLiquidity::::get( (user.clone(), liquidity_asset_id, liquidity_assets_reward)); TotalActivatedLiquidity3rdParty::::mutate(liquidity_asset_id, |activations| { activations @@ -813,11 +851,40 @@ impl Pallet { .and_modify(|val| *val -= liquidity_assets_burned); }); - ::ActivationReservesProvider::deactivate( - liquidity_asset_id, - &user, - liquidity_assets_burned, - ); + ActivatedScheduleLiquidity::::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 = ActivatedScheduleLiquidity::::iter_prefix_values( (user.clone(), liquidity_asset_id), + ).next(){ + + let amount = MaxActivatedLiquidity::::mutate(user.clone(), liquidity_asset_id, |val| { + let prev = *val; + *val = 0; + prev + }); + + ::ActivationReservesProvider::deactivate( + liquidity_asset_id, + &user, + amount, + ); + } Ok(()) } @@ -985,7 +1052,6 @@ impl LiquidityMiningApi for Pallet { } } - println!("POOLS : {:?}", pools); PromotedPoolRewards3rdParty::::put(pools); diff --git a/pallets/proof-of-stake/src/tests.rs b/pallets/proof-of-stake/src/tests.rs index a7255d1daa..777ef047ad 100644 --- a/pallets/proof-of-stake/src/tests.rs +++ b/pallets/proof-of-stake/src/tests.rs @@ -1555,3 +1555,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), + ); + }); +} +