diff --git a/Cargo.lock b/Cargo.lock index d9baf8680..c64490657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4938,7 +4938,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "256.0.0" +version = "257.0.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", @@ -5073,7 +5073,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "3.5.0" +version = "3.6.0" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -8469,7 +8469,7 @@ dependencies = [ [[package]] name = "pallet-liquidity-mining" -version = "4.3.4" +version = "4.4.0" dependencies = [ "fixed", "frame-support", @@ -8692,7 +8692,7 @@ dependencies = [ [[package]] name = "pallet-omnipool" -version = "4.3.2" +version = "4.3.3" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", @@ -8719,7 +8719,7 @@ dependencies = [ [[package]] name = "pallet-omnipool-liquidity-mining" -version = "2.1.8" +version = "2.2.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -9424,7 +9424,7 @@ dependencies = [ [[package]] name = "pallet-xyk" -version = "6.4.4" +version = "6.4.5" dependencies = [ "frame-benchmarking", "frame-support", @@ -9450,7 +9450,7 @@ dependencies = [ [[package]] name = "pallet-xyk-liquidity-mining" -version = "1.1.11" +version = "1.1.12" dependencies = [ "frame-support", "frame-system", @@ -11929,7 +11929,7 @@ dependencies = [ [[package]] name = "runtime-integration-tests" -version = "1.23.3" +version = "1.23.4" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 2d816ba0a..c5a291a12 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.23.3" +version = "1.23.4" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index cd2149ebd..3a68f404c 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -1238,6 +1238,134 @@ fn liquidity_mining_should_work_when_farm_distribute_bonds() { }); } +#[test] +fn claim_rewards_should_work_when_farm_is_updated() { + TestNet::reset(); + + Hydra::execute_with(|| { + let global_farm_1_id = 1; + let yield_farm_1_id = 2; + let yield_farm_2_id = 3; + + //Arrange + init_omnipool(); + seed_lm_pot(); + //necessary for oracle to have a price. + do_lrna_hdx_trade(); + + //NOTE: necessary to get oracle price. + hydradx_run_to_block(100); + set_relaychain_block_number(100); + create_global_farm(None, None); + + set_relaychain_block_number(200); + create_yield_farm(global_farm_1_id, ETH); + create_yield_farm(global_farm_1_id, DOT); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + let position_id = omnipool_add_liquidity(CHARLIE.into(), ETH, 1_000 * UNITS); + assert_nft_owner!( + hydradx_runtime::OmnipoolCollectionId::get(), + position_id, + CHARLIE.into() + ); + + set_relaychain_block_number(400); + let deposit_id = 1; + assert_ok!(hydradx_runtime::OmnipoolLiquidityMining::deposit_shares( + RuntimeOrigin::signed(CHARLIE.into()), + global_farm_1_id, + yield_farm_1_id, + position_id + )); + + set_relaychain_block_number(500); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + DOT, + 10_000 * UNITS as i128, + )); + + let position_id = omnipool_add_liquidity(CHARLIE.into(), DOT, 1 * UNITS); + assert_nft_owner!( + hydradx_runtime::OmnipoolCollectionId::get(), + position_id, + CHARLIE.into() + ); + + set_relaychain_block_number(550); + let deposit_id_2 = 2; + assert_ok!(hydradx_runtime::OmnipoolLiquidityMining::deposit_shares( + RuntimeOrigin::signed(CHARLIE.into()), + global_farm_1_id, + yield_farm_2_id, + position_id + )); + + //Act - update farm + let planned_yielding_periods: BlockNumber = 2_000_000_u32; + let yield_per_period = Perquintill::from_parts(550_776_255_707); + let min_deposit = 5_000; + assert_ok!(hydradx_runtime::OmnipoolLiquidityMining::update_global_farm( + RuntimeOrigin::root(), + global_farm_1_id, + planned_yielding_periods, + yield_per_period, + min_deposit + )); + //Assert + let g_farm = + warehouse_liquidity_mining::GlobalFarm::::get(global_farm_1_id) + .unwrap(); + assert_eq!(g_farm.planned_yielding_periods, planned_yielding_periods); + assert_eq!(g_farm.yield_per_period, yield_per_period); + assert_eq!(g_farm.min_deposit, min_deposit); + + let charlie_hdx_balance_0 = hydradx_runtime::Currencies::free_balance(HDX, &CHARLIE.into()); + //Act 1 - claim rewards for 2-nd yield-farm-entry + set_relaychain_block_number(600); + assert_ok!(hydradx_runtime::OmnipoolLiquidityMining::claim_rewards( + RuntimeOrigin::signed(CHARLIE.into()), + deposit_id_2, + yield_farm_2_id + )); + + //Assert + //NOTE: can't assert state in the deposit because fields are private + let charlie_new_hdx_balance_after_first_claim = hydradx_runtime::Currencies::free_balance(HDX, &CHARLIE.into()); + assert!( + charlie_new_hdx_balance_after_first_claim > charlie_hdx_balance_0, + "Charlie's balance should be increased" + ); + assert_eq!(charlie_new_hdx_balance_after_first_claim, 1000030740535405); + + //Act 2 - claim rewards for differnt yield-farm-entry in the same period should work. + assert_ok!(hydradx_runtime::OmnipoolLiquidityMining::claim_rewards( + RuntimeOrigin::signed(CHARLIE.into()), + deposit_id, + yield_farm_1_id + )); + + //Assert + //NOTE: can't assert state in the deposit because fields are private + let charlie_new_hdx_balance_after_2nd_claim = hydradx_runtime::Currencies::free_balance(HDX, &CHARLIE.into()); + assert!( + charlie_new_hdx_balance_after_2nd_claim > charlie_new_hdx_balance_after_first_claim, + "Charlie's balance should be increased" + ); + assert_eq!(charlie_new_hdx_balance_after_2nd_claim, 1000031130694537); + }); +} + pub fn expect_reward_claimed_events(e: Vec) { let last_events = test_utils::last_events::(10); diff --git a/pallets/liquidity-mining/Cargo.toml b/pallets/liquidity-mining/Cargo.toml index 567b9ed39..25fd7c2bc 100644 --- a/pallets/liquidity-mining/Cargo.toml +++ b/pallets/liquidity-mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-liquidity-mining" -version = "4.3.4" +version = "4.4.0" description = "Liquidity mining" authors = ["GalacticCouncil"] edition = "2021" diff --git a/pallets/liquidity-mining/src/lib.rs b/pallets/liquidity-mining/src/lib.rs index 2197c9a5d..3123586c5 100644 --- a/pallets/liquidity-mining/src/lib.rs +++ b/pallets/liquidity-mining/src/lib.rs @@ -569,6 +569,57 @@ impl, I: 'static> Pallet { }) } + /// Update global farm's main fields. + /// + /// + /// Parameters: + /// - `global_farm_id`: global farm id. + /// - `planned_yielding_periods`: planned number of periods to distribute `total_rewards`. + /// - `yield_per_period`: percentage return on `reward_currency` of all pools. + /// - `min_deposit`: minimum amount of LP shares to be deposited into liquidity mining by each user. + fn update_global_farm( + global_farm_id: GlobalFarmId, + planned_yielding_periods: PeriodOf, + yield_per_period: Perquintill, + min_deposit: Balance, + ) -> Result<(), DispatchError> { + ensure!(min_deposit.ge(&MIN_DEPOSIT), Error::::InvalidMinDeposit); + ensure!( + planned_yielding_periods >= T::MinPlannedYieldingPeriods::get(), + Error::::InvalidPlannedYieldingPeriods + ); + ensure!(!yield_per_period.is_zero(), Error::::InvalidYieldPerPeriod); + + >::try_mutate(global_farm_id, |maybe_global_farm| { + let global_farm = maybe_global_farm.as_mut().ok_or(Error::::GlobalFarmNotFound)?; + + ensure!(global_farm.state.is_active(), Error::::GlobalFarmNotFound); + + let current_period = Self::get_current_period(global_farm.blocks_per_period)?; + Self::sync_global_farm(global_farm, current_period)?; + + //Calculate the new max reward period + let global_farm_account = Self::farm_account_id(global_farm.id)?; + let total_rewards = T::MultiCurrency::free_balance(global_farm.reward_currency, &global_farm_account); + let planned_periods = + TryInto::::try_into(planned_yielding_periods).map_err(|_| ArithmeticError::Overflow)?; + let new_max_reward_period = total_rewards + .checked_div(planned_periods) + .ok_or(Error::::InvalidPlannedYieldingPeriods)?; + ensure!( + !new_max_reward_period.is_zero(), + Error::::InvalidPlannedYieldingPeriods + ); + + global_farm.planned_yielding_periods = planned_yielding_periods; + global_farm.yield_per_period = yield_per_period; + global_farm.min_deposit = min_deposit; + global_farm.max_reward_per_period = new_max_reward_period; + + Ok(()) + }) + } + /// Terminate existing liquidity mining program. Undistributed rewards are transferred to /// owner(`who`). /// @@ -1816,6 +1867,15 @@ impl, I: 'static> hydradx_traits::liquidity_mining::Mutate Result<(), Self::Error> { + Self::update_global_farm(global_farm_id, planned_yielding_periods, yield_per_period, min_deposit) + } + fn terminate_global_farm( who: T::AccountId, global_farm_id: u32, diff --git a/pallets/liquidity-mining/src/tests/claim_rewards.rs b/pallets/liquidity-mining/src/tests/claim_rewards.rs index 28cb85db8..40b06970b 100644 --- a/pallets/liquidity-mining/src/tests/claim_rewards.rs +++ b/pallets/liquidity-mining/src/tests/claim_rewards.rs @@ -1263,3 +1263,36 @@ fn claim_rewards_should_claim_correct_amount_when_yield_was_resumed_multiple_tim }); }); } + +#[test] +fn claim_rewards_should_work_when_global_farm_was_updated() { + predefined_test_ext_with_deposits().execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let planned_yielding_periods: BlockNumber = 1_000u64; + let yield_per_period = Perquintill::from_percent(20); + let min_deposit = 20_000; + + set_block_number(100_000); + + //Act + assert_ok!(LiquidityMining::update_global_farm( + GC_FARM, + planned_yielding_periods, + yield_per_period, + min_deposit, + )); + + set_block_number(1_000_000); + + //Assert + assert_eq!( + LiquidityMining::claim_rewards(ALICE, PREDEFINED_DEPOSIT_IDS[0], GC_BSX_TKN1_YIELD_FARM_ID, false) + .unwrap(), + (GC_FARM, BSX, 28495477087879389002, 142022912120610998) + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} diff --git a/pallets/liquidity-mining/src/tests/full_run.rs b/pallets/liquidity-mining/src/tests/full_run.rs index ea8bd67dc..652d40ec0 100644 --- a/pallets/liquidity-mining/src/tests/full_run.rs +++ b/pallets/liquidity-mining/src/tests/full_run.rs @@ -24,7 +24,7 @@ use test_utils::assert_balance_approx; use rand::Rng; -//This test test full run LM. Global farm is not full but it's running longer than expected. Users +//This test tests full run LM. Global farm is not full but it's running longer than expected. Users //should be able to claim expected amount. //This test case is without loyalty factor. #[test] @@ -285,6 +285,197 @@ fn non_full_farm_distribute_everything_and_update_farms() { }); } +#[test] +fn non_full_farm_distribute_everything_and_update_global_farms_fields() { + new_test_ext().execute_with(|| { + let _ = with_transaction(|| { + const GLOBAL_FARM: GlobalFarmId = 1; + const YIELD_FARM_A: YieldFarmId = 2; + const YIELD_FARM_B: YieldFarmId = 3; + + const ALICE_DEPOSIT: DepositId = 1; + const BOB_DEPOSIT: DepositId = 2; + const CHARLIE_DEPOSIT: DepositId = 3; + + //initialize farms + set_block_number(100); + assert_ok!(LiquidityMining2::create_global_farm( + 200_000 * ONE, + 20, + 10, + BSX, + BSX, + GC, + Perquintill::from_float(0.5), + 1_000, + One::one(), + )); + + assert_ok!(LiquidityMining2::create_yield_farm( + GC, + GLOBAL_FARM, + FixedU128::from(2_u128), + None, + BSX_TKN1_AMM, + vec![BSX, TKN1], + )); + + assert_ok!(LiquidityMining2::create_yield_farm( + GC, + GLOBAL_FARM, + FixedU128::from(1_u128), + None, + BSX_TKN2_AMM, + vec![BSX, TKN2], + )); + + set_block_number(110); + //alice + assert_ok!(LiquidityMining2::deposit_lp_shares( + GLOBAL_FARM, + YIELD_FARM_A, + BSX_TKN1_AMM, + 5_000 * ONE, + |_, _, _| { Ok(5_000 * ONE) } + )); + + set_block_number(120); + + //bob + assert_ok!(LiquidityMining2::deposit_lp_shares( + GLOBAL_FARM, + YIELD_FARM_B, + BSX_TKN2_AMM, + 2_500 * ONE, + |_, _, _| { Ok(2_500 * ONE) } + )); + + //charlie + assert_ok!(LiquidityMining2::deposit_lp_shares( + GLOBAL_FARM, + YIELD_FARM_B, + BSX_TKN2_AMM, + 2_500 * ONE, + |_, _, _| { Ok(2_500 * ONE) } + )); + + set_block_number(130); + + //Claim rewards, leading to farms sync + let (_, _, _, unclaimeable) = + LiquidityMining2::claim_rewards(ALICE, ALICE_DEPOSIT, YIELD_FARM_A, false).unwrap(); + + //Withdraw and redeposit for ALICE + let (_, withdran_amount, _) = + LiquidityMining2::withdraw_lp_shares(ALICE_DEPOSIT, YIELD_FARM_A, unclaimeable).unwrap(); + let alice_new_deposit_id = LiquidityMining2::deposit_lp_shares( + GLOBAL_FARM, + YIELD_FARM_A, + BSX_TKN1_AMM, + withdran_amount, + |_, _, _| Ok(5_000 * ONE), + ) + .unwrap(); + + assert_eq!( + Tokens::free_balance(BSX, &LiquidityMining2::farm_account_id(GLOBAL_FARM).unwrap()), + 187500000000000000 + ); + + //We check that not everything has been claimed yet + assert_eq!( + Tokens::free_balance(BSX, &LiquidityMining2::pot_account_id().unwrap()), + 2500000000000000 + ); + + let planned_yielding_periods: BlockNumber = 30_u64; + let yield_per_period = Perquintill::from_float(0.45); + let min_deposit = 20_000; + assert_ok!(LiquidityMining2::update_global_farm( + GLOBAL_FARM, + planned_yielding_periods, + yield_per_period, + min_deposit + )); + + set_block_number(501); + + //Check that alice has things to claim + let (_, _, claimed, unclaimable) = + LiquidityMining2::claim_rewards(ALICE, alice_new_deposit_id, YIELD_FARM_A, false).unwrap(); + assert_eq!(claimed, 124999999999999333); + assert_eq!(unclaimable, 0); + + // Check that BOB has things to claim + let (_, _, claimed, unclaimable) = + LiquidityMining2::claim_rewards(BOB, BOB_DEPOSIT, YIELD_FARM_B, false).unwrap(); + assert_eq!(claimed, 32499999999999833); + assert_eq!(unclaimable, 0); + + // Check that Charlie has things to claim + let (_, _, claimed, unclaimable) = + LiquidityMining2::claim_rewards(CHARLIE, CHARLIE_DEPOSIT, YIELD_FARM_B, false).unwrap(); + assert_eq!(claimed, 32499999999999833); + assert_eq!(unclaimable, 0); + + assert_eq!( + Tokens::free_balance(BSX, &LiquidityMining2::pot_account_id().unwrap()), + 1 + ); + + assert_eq!(LiquidityMining2::global_farm(GLOBAL_FARM).unwrap().updated_at, 50); + assert_eq!( + LiquidityMining2::yield_farm((BSX_TKN1_AMM, GLOBAL_FARM, YIELD_FARM_A)) + .unwrap() + .updated_at, + 50 + ); + + //Assert that user has nothing else to claim + set_block_number(600); + let (_, _, claimed, unclaimable) = + LiquidityMining2::claim_rewards(ALICE, alice_new_deposit_id, YIELD_FARM_A, false).unwrap(); + assert_eq!(claimed, 0); + assert_eq!(unclaimable, 0); + assert_ok!(LiquidityMining2::withdraw_lp_shares( + alice_new_deposit_id, + YIELD_FARM_A, + unclaimable + )); + + let (_, _, claimed, unclaimable) = + LiquidityMining2::claim_rewards(BOB, BOB_DEPOSIT, YIELD_FARM_B, false).unwrap(); + assert_eq!(claimed, 0); + assert_eq!(unclaimable, 0); + assert_ok!(LiquidityMining2::withdraw_lp_shares( + BOB_DEPOSIT, + YIELD_FARM_B, + unclaimable + )); + + let (_, _, claimed, unclaimable) = + LiquidityMining2::claim_rewards(CHARLIE, CHARLIE_DEPOSIT, YIELD_FARM_B, false).unwrap(); + assert_eq!(claimed, 0); + assert_eq!(unclaimable, 0); + assert_ok!(LiquidityMining2::withdraw_lp_shares( + CHARLIE_DEPOSIT, + YIELD_FARM_B, + unclaimable + )); + + assert_eq!(LiquidityMining2::global_farm(GLOBAL_FARM).unwrap().updated_at, 60); + assert_eq!( + LiquidityMining2::yield_farm((BSX_TKN2_AMM, GLOBAL_FARM, YIELD_FARM_B)) + .unwrap() + .updated_at, + 60 + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + //This test tests `update_global_farm` and `update_yield_farm` after farm distributed all the //rewards. #[test] diff --git a/pallets/liquidity-mining/src/tests/invariants.rs b/pallets/liquidity-mining/src/tests/invariants.rs index 7ba945344..16390df59 100644 --- a/pallets/liquidity-mining/src/tests/invariants.rs +++ b/pallets/liquidity-mining/src/tests/invariants.rs @@ -886,3 +886,404 @@ fn invariant_4() { .unwrap(); }); } + +fn planned_yielding_periods() -> impl Strategy> { + 1_000u64..5_000_000u64 +} + +fn percentage() -> impl Strategy { + 1..40u64 +} + +fn min_deposit() -> impl Strategy { + 1000..10000000u128 +} + +#[test] +//Update global farm for invariant one +//https://www.notion.so/Liquidity-mining-spec-b30ccfe470a74173b82c3702b1e8fca1#87868f45e4d04ecb92374c5f795a493d + +fn update_global_farm_invariant_1() { + //Number of sucessfull test cases that must execute for the test as a whole to pass. + let successfull_cases = 1_000; + //Number of blocks added to current block number in each test case run. This number should be + //reasonable smaller than total of runned test to make sure lot of claims is executed and + //multiple claims for same deposit to happen. + let blocks_offset_range = 1..10_u64; + //Index of deposit in `deposit` vec. This idx is used in each test case run and execute claim + //if deposit exits. + + let deposit_idx_range = 0..500_usize; + + invariants_externalities().execute_with(|| { + let mut runner = TestRunner::new(Config { + cases: successfull_cases, + source_file: Some("liquidity-mining/src/tests/invariants.rs"), + test_name: Some("update_global_farm_invariant_1"), + ..Config::default() + }); + let deposits: RefCell> = RefCell::new(Vec::new()); + //NOTE: farm ids start with 1 so skip 0, farm's id is used as index into array + let distributed_before_update: RefCell<[u128; 4]> = RefCell::new([0, 0, 0, 0]); + + runner + .run( + &( + arb_deposit(), + blocks_offset_range, + deposit_idx_range, + planned_yielding_periods(), + percentage(), + ), + |(d, blocks_offset, deposit_idx, planned_yielding_period, percent)| { + deposits.borrow_mut().push(d.clone()); + + let yield_per_period = Perquintill::from_percent(percent); + + //Act + let _ = with_transaction(|| { + LiquidityMining::update_global_farm( + d.global_farm_id, + planned_yielding_period, + yield_per_period, + 1_000, + ) + .unwrap(); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + let g_farm = LiquidityMining::global_farm(d.global_farm_id).unwrap(); + //NOTE: `update_global_farm()` distributes rewards before update so this must be done after function execution but before any interaction with farm. + distributed_before_update.borrow_mut()[d.global_farm_id as usize] = + g_farm.accumulated_paid_rewards + g_farm.pending_rewards; + + let _ = with_transaction(|| { + assert_ok!(LiquidityMining::deposit_lp_shares( + d.global_farm_id, + d.yield_farm_id, + d.amm_pool_id, + d.shares, + |_, _, _| -> Result { Ok(d.valued_shares) } + )); + + set_block_number(mock::System::block_number() + blocks_offset); + + //claim rewards only if deposit exists + if deposit_idx < deposits.borrow().len() { + let d = &deposits.borrow()[deposit_idx]; + let deposit_id = deposit_idx as u128 + 1; + + assert_ok!(LiquidityMining::claim_rewards(ALICE, deposit_id, d.yield_farm_id, true)); + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //Assert: + G_FARMS.with(|v| { + v.borrow().clone().into_iter().for_each(|gf| { + let g_farm_balance = Tokens::free_balance( + gf.reward_currency, + &LiquidityMining::farm_account_id(gf.id).unwrap(), + ); + let g_farm_1 = LiquidityMining::global_farm(gf.id).unwrap(); + + //1.1 assert + let s_1 = g_farm_balance + g_farm_1.accumulated_paid_rewards + g_farm_1.pending_rewards; + //NOTE: This should be precise. + assert_eq!(gf.total_rewards, s_1); + + //1.2 assert + let s_1: u128 = g_farm_1.max_reward_per_period * g_farm_1.planned_yielding_periods as u128 + + distributed_before_update.borrow()[gf.id as usize]; + + assert!( + gf.total_rewards >= s_1, + "total_rewards >= distributed + max_reward_per_period * planned_yielding_periods" + ); + //NOTE: Approax becasue of div in max_reward_per_period calculation. + assert_eq_approx!( + gf.total_rewards, + s_1, + 10_000_000, + "total_rewards >= distributed + max_reward_per_period * planned_yielding_periods" + ); + }) + }); + + Ok(()) + }, + ) + .unwrap(); + }); +} + +#[test] +//Update global farm for invariants 2,3 +//https://www.notion.so/Liquidity-mining-spec-b30ccfe470a74173b82c3702b1e8fca1#422dea2e23744859baeb704dbdb3caca +// +fn update_global_farm_invariant_2_3() { + //Number of sucessfull test cases that must execute for the test as a whole to pass. + let successfull_cases = 1_000; + //Number of blocks added to current block number in each test case run. This number should be + //reasonable smaller than total of runned test to make sure lot of claims is executed and + //multiple claims for same deposit to happen. + let blocks_offset_range = 1..10_u64; + //Index of deposit in `deposit` vec. This idx is used in each test case run and execute claim + //if deposit exits. + + let deposit_idx_range = 0..500_usize; + + invariants_externalities().execute_with(|| { + let mut runner = TestRunner::new(Config { + cases: successfull_cases, + source_file: Some("liquidity-mining/src/tests/invariants.rs"), + test_name: Some("update_global_farm_invariant_2_3"), + ..Config::default() + }); + let deposits: RefCell> = RefCell::new(Vec::new()); + let pot = LiquidityMining::pot_account_id().unwrap(); + + runner + .run( + &( + arb_deposit(), + blocks_offset_range, + deposit_idx_range, + planned_yielding_periods(), + percentage(), + min_deposit(), + ), + |(d, blocks_offset, deposit_idx, planned_yielding_period, percent, min_deposit)| { + deposits.borrow_mut().push(d.clone()); + + let yield_per_period = Perquintill::from_percent(percent); + + //Act + let _ = with_transaction(|| { + LiquidityMining::update_global_farm( + d.global_farm_id, + planned_yielding_period, + yield_per_period, + min_deposit, + ) + .unwrap(); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //We deposit and roll block to have claimable rewards + let _ = with_transaction(|| { + assert_ok!(LiquidityMining::deposit_lp_shares( + d.global_farm_id, + d.yield_farm_id, + d.amm_pool_id, + d.shares, + |_, _, _| -> Result { Ok(d.valued_shares) } + )); + set_block_number(mock::System::block_number() + blocks_offset); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + let _ = with_transaction(|| { + //claim rewards only if deposit exists + if deposit_idx < deposits.borrow().len() { + let d = &deposits.borrow()[deposit_idx]; + let deposit_id = deposit_idx as u128 + 1; + + assert_ok!(LiquidityMining::claim_rewards(ALICE, deposit_id, d.yield_farm_id, true)); + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //INVARIANT 2 + //Calculate necessary values and assert + let (total_rewards_sum, farm_balances_sum, pot_balance_sum) = G_FARMS.with(|v| { + let mut total_rewards_sum = 0_u128; + let mut farm_balances_sum = 0_u128; + let mut pot_balance_sum = 0_u128; + let mut already_summed_balances: Vec = Vec::new(); + + v.borrow().clone().into_iter().for_each(|gf| { + farm_balances_sum += Tokens::free_balance( + gf.reward_currency, + &LiquidityMining::farm_account_id(gf.id).unwrap(), + ); + + total_rewards_sum += gf.total_rewards; + + if !already_summed_balances.contains(&gf.reward_currency) { + pot_balance_sum += Tokens::total_balance(gf.reward_currency, &pot); + already_summed_balances.push(gf.reward_currency); + } + }); + (total_rewards_sum, farm_balances_sum, pot_balance_sum) + }); + + let last_deposit_id = LiquidityMining::deposit_id(); + let mut claimed_by_users_sum = 0_u128; + for i in 1..=last_deposit_id { + let d = LiquidityMining::deposit(i).unwrap(); + + let claimed_amount = d.yield_farm_entries[0].accumulated_claimed_rewards; + claimed_by_users_sum += claimed_amount; + } + + //WARN: There is no room for rounding errors in this invariant. Any discrepancy + //in this assert means we are loosing tokens somewhere. + assert_eq!( + total_rewards_sum, + farm_balances_sum + pot_balance_sum + claimed_by_users_sum, + ); + + //INVARIANT 3 + //Calculate necessary values and assert + let last_deposit_id = LiquidityMining::deposit_id(); + let mut claimed_by_user_per_yield_farm: HashMap<(GlobalFarmId, YieldFarmId), u128> = HashMap::new(); + for i in 1..=last_deposit_id { + let d = LiquidityMining::deposit(i).unwrap(); + + let claimed_amount = d.yield_farm_entries[0].accumulated_claimed_rewards; + let global_farm_id = d.yield_farm_entries[0].global_farm_id; + let yield_farm_id = d.yield_farm_entries[0].yield_farm_id; + *claimed_by_user_per_yield_farm + .entry((global_farm_id, yield_farm_id)) + .or_insert(0) += claimed_amount; + } + + G_FARMS.with(|v| { + v.borrow().clone().into_iter().for_each(|gf| { + let g_farm = LiquidityMining::global_farm(gf.id).unwrap(); + let mut y_farms_let_to_distribute_sum = 0_u128; + + let mut claimed_by_yield_farms_in_global_farm = 0_u128; + gf.yield_farms.iter().for_each(|yf| { + y_farms_let_to_distribute_sum += LiquidityMining::yield_farm((yf.1, gf.id, yf.0)) + .unwrap() + .left_to_distribute; + + //NOTE this run for each iteration of test so record in HashMap may + //not exists if deposit doesn't exists yet. + claimed_by_yield_farms_in_global_farm += + claimed_by_user_per_yield_farm.get(&(gf.id, yf.0)).unwrap_or(&0_u128); + }); + + assert_eq!( + g_farm.accumulated_paid_rewards, + y_farms_let_to_distribute_sum + claimed_by_yield_farms_in_global_farm + ); + }) + }); + + Ok(()) + }, + ) + .unwrap(); + }); +} + +#[test] +//Update global farm for invariant 4 https://www.notion.so/Liquidity-mining-spec-b30ccfe470a74173b82c3702b1e8fca1#422dea2e23744859baeb704dbdb3caca +// +fn update_global_farm_invariant_4() { + //Number of sucessfull test cases that must execute for the test as a whole to pass. + let successfull_cases = 1_000; + //Number of blocks added to current block number in each test case run. This number should be + //reasonable smaller than total of runned test to make sure lot of claims is executed and + //multiple claims for same deposit to happen. + let blocks_offset_range = 1..10_u64; + //Index of deposit in `deposit` vec. This idx is used in each test case run and execute claim + //if deposit exits. + + let deposit_idx_range = 0..500_usize; + + invariants_externalities().execute_with(|| { + let mut runner = TestRunner::new(Config { + cases: successfull_cases, + source_file: Some("liquidity-mining/src/tests/invariants.rs"), + test_name: Some("update_global_farm_invariant_4"), + ..Config::default() + }); + let deposits: RefCell> = RefCell::new(Vec::new()); + let paid_until_now: RefCell> = RefCell::new(HashMap::new()); + + runner + .run( + &(arb_deposit(), blocks_offset_range, deposit_idx_range, planned_yielding_periods(), percentage(), min_deposit()), + |(d, blocks_offset, deposit_idx, planned_yielding_period, percent, min_deposit)| { + deposits.borrow_mut().push(d.clone()); + let g_farm_0 = LiquidityMining::global_farm(d.global_farm_id).unwrap(); + + let yield_per_period = Perquintill::from_percent(percent); + + //Act + let _ = with_transaction(|| { + LiquidityMining::update_global_farm(d.global_farm_id, planned_yielding_period, yield_per_period, min_deposit).unwrap(); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //We deposit + let _ = with_transaction(|| { + assert_ok!(LiquidityMining::deposit_lp_shares( + d.global_farm_id, + d.yield_farm_id, + d.amm_pool_id, + d.shares, + |_, _, _| -> Result { Ok(d.valued_shares) } + )); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //INVARIANT 4 (part 1) + let _ = with_transaction(|| { + let g_farm_1 = LiquidityMining::global_farm(g_farm_0.id).unwrap(); + let s_0 = g_farm_1.accumulated_paid_rewards + g_farm_1.pending_rewards; + *paid_until_now.borrow_mut().entry(g_farm_1.id).or_insert(0_u128) += (g_farm_1.accumulated_rpz + - g_farm_0.accumulated_rpz) + .checked_mul_int(g_farm_0.total_shares_z) + .unwrap(); + + //NOTE: global-farm is updated before deposit so in this case we need to + //use Z{now-1} instead of Z{now} which includes deposited shares. + assert_eq_approx!(s_0, *paid_until_now.borrow().get(&d.global_farm_id).unwrap(), 1_000_000, "accumulated_paid_rewards + pending_rewards = sum(rpz{now} - rpz{now-1} * Z{now-1}) for all periods"); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //We roll block to have claimable rewards + let _ = with_transaction(|| { + set_block_number(mock::System::block_number() + blocks_offset); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + //Invariant 4 (part 2) + let _ = with_transaction(|| { + //claim rewards only if deposit exists + if deposit_idx < deposits.borrow().len() { + let d = &deposits.borrow()[deposit_idx]; + let deposit_id = deposit_idx as u128 + 1; + + let g_farm_0 = LiquidityMining::global_farm(d.global_farm_id).unwrap(); + + assert_ok!(LiquidityMining::claim_rewards(ALICE, deposit_id, d.yield_farm_id, true)); + + let g_farm_1 = LiquidityMining::global_farm(g_farm_0.id).unwrap(); + let s_0 = g_farm_1.accumulated_paid_rewards + g_farm_1.pending_rewards; + *paid_until_now.borrow_mut().entry(g_farm_1.id).or_insert(0_u128) += (g_farm_1.accumulated_rpz + - g_farm_0.accumulated_rpz) + .checked_mul_int(g_farm_1.total_shares_z) + .unwrap(); + + //NOTE: global-farm is updated before claim so RPZ includes all Z so Z{now} + //must be used in this case. + assert_eq_approx!(s_0, *paid_until_now.borrow().get(&d.global_farm_id).unwrap(), 1_000_000, "accumulated_paid_rewards + pending_rewards = sum(rpz{now} - rpz{now-1} * Z{now}) for all periods"); + } + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + + Ok(()) + }, + ) + .unwrap(); + }); +} diff --git a/pallets/liquidity-mining/src/tests/update_global_farm.rs b/pallets/liquidity-mining/src/tests/update_global_farm.rs index d567303a8..caa0da2c5 100644 --- a/pallets/liquidity-mining/src/tests/update_global_farm.rs +++ b/pallets/liquidity-mining/src/tests/update_global_farm.rs @@ -16,6 +16,7 @@ // limitations under the License. use super::*; +use crate::tests::mock::MinPlannedYieldingPeriods; use pretty_assertions::assert_eq; use test_ext::*; @@ -182,3 +183,124 @@ fn update_global_farm_price_adjustment_should_fail_when_farm_is_already_terminat }); }) } + +#[test] +fn update_global_farm_should_work() { + predefined_test_ext_with_deposits().execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let planned_yielding_periods: BlockNumber = 1_000_000_000_u64; + let yield_per_period = Perquintill::from_percent(20); + let min_deposit = 20_000; + + let global_farm_0 = LiquidityMining::global_farm(GC_FARM).unwrap(); + + set_block_number(100_000); + + //Act + assert_ok!(LiquidityMining::update_global_farm( + GC_FARM, + planned_yielding_periods, + yield_per_period, + min_deposit, + )); + + //Assert that global farm is updated + assert_eq!( + LiquidityMining::global_farm(GC_FARM).unwrap(), + GlobalFarmData { + updated_at: 1_000, + planned_yielding_periods, + yield_per_period, + min_deposit, + accumulated_rpz: FixedU128::from_inner(491_000_000_000_000_000_000_u128), + pending_rewards: 343195125000000000000, + max_reward_per_period: 29655521325000, + ..global_farm_0 + }, + ); + frame_system::Pallet::::assert_has_event(mock::RuntimeEvent::LiquidityMining( + Event::GlobalFarmAccRPZUpdated { + global_farm_id: global_farm_0.id, + accumulated_rpz: FixedU128::from_inner(491_000_000_000_000_000_000_u128), + total_shares_z: global_farm_0.total_shares_z, + }, + )); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn update_global_farm_should_fail_with_invalid_deposit() { + predefined_test_ext_with_deposits().execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let planned_yielding_periods: BlockNumber = 1_000_000_000_u64; + let yield_per_period = Perquintill::from_percent(20); + + set_block_number(100_000); + + //Act + assert_noop!( + LiquidityMining::update_global_farm( + GC_FARM, + planned_yielding_periods, + yield_per_period, + MIN_DEPOSIT - 1, + ), + Error::::InvalidMinDeposit + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn update_global_farm_should_fail_when_planning_yield_period_is_too_small() { + predefined_test_ext_with_deposits().execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let planned_yielding_periods: BlockNumber = MinPlannedYieldingPeriods::get() - 1; + let yield_per_period = Perquintill::from_percent(20); + + set_block_number(100_000); + + //Act + assert_noop!( + LiquidityMining::update_global_farm(GC_FARM, planned_yielding_periods, yield_per_period, MIN_DEPOSIT,), + Error::::InvalidPlannedYieldingPeriods + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn update_global_farm_should_fail_when_yield_period_is_zero() { + predefined_test_ext_with_deposits().execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let planned_yielding_periods: BlockNumber = MinPlannedYieldingPeriods::get(); + let zero_yield_per_period = Perquintill::from_percent(0); + + set_block_number(100_000); + + //Act + assert_noop!( + LiquidityMining::update_global_farm( + GC_FARM, + planned_yielding_periods, + zero_yield_per_period, + MIN_DEPOSIT, + ), + Error::::InvalidYieldPerPeriod + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} diff --git a/pallets/liquidity-mining/src/types.rs b/pallets/liquidity-mining/src/types.rs index e4d11091d..1b646bcb5 100644 --- a/pallets/liquidity-mining/src/types.rs +++ b/pallets/liquidity-mining/src/types.rs @@ -54,13 +54,13 @@ pub struct GlobalFarmData, I: 'static = ()> { pub reward_currency: T::AssetId, pub(super) pending_rewards: Balance, pub(super) accumulated_paid_rewards: Balance, - pub(super) yield_per_period: Perquintill, - pub(super) planned_yielding_periods: PeriodOf, + pub yield_per_period: Perquintill, + pub planned_yielding_periods: PeriodOf, pub(super) blocks_per_period: BlockNumberFor, pub incentivized_asset: T::AssetId, pub(super) max_reward_per_period: Balance, // min. LP shares user must deposit to start yield farming. - pub(super) min_deposit: Balance, + pub min_deposit: Balance, // This include `active` and `stopped` yield farms. pub(super) live_yield_farms_count: u32, // This include `active`, `stopped`, `terminated` - this count is decreased only if yield diff --git a/pallets/omnipool-liquidity-mining/Cargo.toml b/pallets/omnipool-liquidity-mining/Cargo.toml index 67ea84795..e90e2ac97 100644 --- a/pallets/omnipool-liquidity-mining/Cargo.toml +++ b/pallets/omnipool-liquidity-mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool-liquidity-mining" -version = "2.1.8" +version = "2.2.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/omnipool-liquidity-mining/src/benchmarks.rs b/pallets/omnipool-liquidity-mining/src/benchmarks.rs index 7e01ed4a7..badbadbf5 100644 --- a/pallets/omnipool-liquidity-mining/src/benchmarks.rs +++ b/pallets/omnipool-liquidity-mining/src/benchmarks.rs @@ -291,6 +291,23 @@ benchmarks! { }: _(RawOrigin::Root, G_FARM_TOTAL_REWARDS, planned_yielding_periods, blocks_per_period, REWARD_CURRENCY.into(), owner, yield_per_period, min_deposit, FixedU128::one()) + update_global_farm { + let owner = create_funded_account::("owner", 0, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY.into()); + let global_farm_id = 1; + let yield_farm_id = 2; + + initialize_omnipool::()?; + + initialize_global_farm::(owner.clone())?; + initialize_yield_farm::(owner.clone(), global_farm_id, BTC.into())?; + + let planned_yielding_periods = BlockNumberFor::::from(100_000_u32); + let yield_per_period = Perquintill::from_percent(20); + let min_deposit = 1_000; + + }: _(RawOrigin::Root, global_farm_id, planned_yielding_periods, yield_per_period, min_deposit) + + terminate_global_farm { let owner = create_funded_account::("owner", 0, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY.into()); let global_farm_id = 1; diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 9ed62972d..dbee10206 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -181,6 +181,14 @@ pub mod pallet { lrna_price_adjustment: FixedU128, }, + /// Global farm was updated + GlobalFarmUpdated { + id: GlobalFarmId, + planned_yielding_periods: PeriodOf, + yield_per_period: Perquintill, + min_deposit: Balance, + }, + /// Global farm was terminated. GlobalFarmTerminated { global_farm_id: GlobalFarmId, @@ -878,6 +886,48 @@ pub mod pallet { Ok(()) } + + /// This extrinsic updates global farm's main parameters. + /// + /// The dispatch origin for this call must be `T::CreateOrigin`. + /// !!!WARN: `T::CreateOrigin` has power over funds of `owner`'s account and it should be + /// configured to trusted origin e.g Sudo or Governance. + /// + /// Parameters: + /// - `origin`: account allowed to create new liquidity mining program(root, governance). + /// - `global_farm_id`: id of the global farm to update. + /// - `planned_yielding_periods`: planned number of periods to distribute `total_rewards`. + /// - `yield_per_period`: percentage return on `reward_currency` of all farms. + /// - `min_deposit`: minimum amount of LP shares to be deposited into the liquidity mining by each user. + /// + /// Emits `GlobalFarmUpdated` event when successful. + #[pallet::call_index(12)] + #[pallet::weight(::WeightInfo::update_global_farm())] + pub fn update_global_farm( + origin: OriginFor, + global_farm_id: GlobalFarmId, + planned_yielding_periods: crate::PeriodOf, + yield_per_period: Perquintill, + min_deposit: Balance, + ) -> DispatchResult { + T::CreateOrigin::ensure_origin(origin)?; + + T::LiquidityMiningHandler::update_global_farm( + global_farm_id, + planned_yielding_periods, + yield_per_period, + min_deposit, + )?; + + Self::deposit_event(Event::GlobalFarmUpdated { + id: global_farm_id, + planned_yielding_periods, + yield_per_period, + min_deposit, + }); + + Ok(()) + } } } diff --git a/pallets/omnipool-liquidity-mining/src/tests/mod.rs b/pallets/omnipool-liquidity-mining/src/tests/mod.rs index d180b5d84..0a0249c56 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/mod.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/mod.rs @@ -38,5 +38,6 @@ pub mod resume_yield_farm; pub mod stop_yield_farm; pub mod terminate_global_farm; pub mod terminate_yield_farm; +pub mod update_global_farm; pub mod update_yield_farm; pub mod withdraw_shares; diff --git a/pallets/omnipool-liquidity-mining/src/tests/update_global_farm.rs b/pallets/omnipool-liquidity-mining/src/tests/update_global_farm.rs new file mode 100644 index 000000000..1f0a0a6a1 --- /dev/null +++ b/pallets/omnipool-liquidity-mining/src/tests/update_global_farm.rs @@ -0,0 +1,168 @@ +// Copyright (C) 2020-2023 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; +#[test] +fn update_global_farm_should_work() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, KSM, 5000 * ONE), + (LP2, DOT, 2000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (ALICE, KSM, 10_000 * ONE), + (BOB, DOT, 10_000 * ONE), + ]) + .with_registered_asset(KSM) + .with_registered_asset(DOT) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(KSM, FixedU128::from_float(0.65), LP1, 2000 * ONE) + .with_global_farm( + 80_000_000 * ONE, + 2_628_000, + 1, + HDX, + GC, + Perquintill::from_float(0.000_000_15_f64), + 1_000, + FixedU128::one(), + ) + .with_yield_farm(GC, 1, KSM, FixedU128::one(), None) + .build() + .execute_with(|| { + let global_farm_id = 1; + + let planned_yielding_periods: BlockNumber = 1_000_000_000_u64; + let yield_per_period = Perquintill::from_percent(20); + let min_deposit = 20_000; + + //Act + assert_ok!(OmnipoolMining::update_global_farm( + RuntimeOrigin::root(), + global_farm_id, + planned_yielding_periods, + yield_per_period, + min_deposit, + ),); + + //Assert + assert_last_event!(crate::Event::GlobalFarmUpdated { + id: 1, + planned_yielding_periods, + yield_per_period, + min_deposit, + } + .into()); + }); +} + +#[test] +fn update_global_farm_should_fail_when_origin_is_not_allowed() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, KSM, 5000 * ONE), + (LP2, DOT, 2000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (ALICE, KSM, 10_000 * ONE), + (BOB, DOT, 10_000 * ONE), + ]) + .with_registered_asset(KSM) + .with_registered_asset(DOT) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(KSM, FixedU128::from_float(0.65), LP1, 2000 * ONE) + .with_global_farm( + 80_000_000 * ONE, + 2_628_000, + 1, + HDX, + GC, + Perquintill::from_float(0.000_000_15_f64), + 1_000, + FixedU128::one(), + ) + .with_yield_farm(GC, 1, KSM, FixedU128::one(), None) + .build() + .execute_with(|| { + let global_farm_id = 1; + + let planned_yielding_periods: BlockNumber = 1_000_000_000_u64; + let yield_per_period = Perquintill::from_percent(20); + let min_deposit = 20_000; + + //Act and assert + assert_noop!( + OmnipoolMining::update_global_farm( + RuntimeOrigin::signed(ALICE), + global_farm_id, + planned_yielding_periods, + yield_per_period, + min_deposit, + ), + BadOrigin + ); + }); +} + +#[test] +fn update_global_farm_should_fail_when_origin_is_none() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, KSM, 5000 * ONE), + (LP2, DOT, 2000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (ALICE, KSM, 10_000 * ONE), + (BOB, DOT, 10_000 * ONE), + ]) + .with_registered_asset(KSM) + .with_registered_asset(DOT) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(KSM, FixedU128::from_float(0.65), LP1, 2000 * ONE) + .with_global_farm( + 80_000_000 * ONE, + 2_628_000, + 1, + HDX, + GC, + Perquintill::from_float(0.000_000_15_f64), + 1_000, + FixedU128::one(), + ) + .with_yield_farm(GC, 1, KSM, FixedU128::one(), None) + .build() + .execute_with(|| { + let global_farm_id = 1; + + let planned_yielding_periods: BlockNumber = 1_000_000_000_u64; + let yield_per_period = Perquintill::from_percent(20); + let min_deposit = 20_000; + + //Act and assert + assert_noop!( + OmnipoolMining::update_global_farm( + RuntimeOrigin::none(), + global_farm_id, + planned_yielding_periods, + yield_per_period, + min_deposit, + ), + BadOrigin + ); + }); +} diff --git a/pallets/omnipool-liquidity-mining/src/weights.rs b/pallets/omnipool-liquidity-mining/src/weights.rs index b145e46ee..8b0265865 100644 --- a/pallets/omnipool-liquidity-mining/src/weights.rs +++ b/pallets/omnipool-liquidity-mining/src/weights.rs @@ -55,6 +55,7 @@ use sp_std::marker::PhantomData; /// Weight functions needed for pallet_omnipool_liquidity_mining. pub trait WeightInfo { fn create_global_farm() -> Weight; + fn update_global_farm() -> Weight; fn terminate_global_farm() -> Weight; fn create_yield_farm() -> Weight; fn update_yield_farm() -> Weight; @@ -88,6 +89,21 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } + + /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:1 w:1) + /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn update_global_farm() -> Weight { + // Proof Size summary in bytes: + // Measured: `573` + // Estimated: `3670` + // Minimum execution time: 22_855_000 picoseconds. + Weight::from_parts(23_088_000, 3670) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:1 w:1) /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:2 w:2) diff --git a/pallets/omnipool/Cargo.toml b/pallets/omnipool/Cargo.toml index ec37db74f..d7307ad76 100644 --- a/pallets/omnipool/Cargo.toml +++ b/pallets/omnipool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool" -version = "4.3.2" +version = "4.3.3" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/xyk-liquidity-mining/Cargo.toml b/pallets/xyk-liquidity-mining/Cargo.toml index caac8a418..2e09a71d6 100644 --- a/pallets/xyk-liquidity-mining/Cargo.toml +++ b/pallets/xyk-liquidity-mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-xyk-liquidity-mining" -version = "1.1.11" +version = "1.1.12" description = "Liquidity mining" authors = ["GalacticCouncil"] edition = "2021" diff --git a/pallets/xyk-liquidity-mining/src/tests/mock.rs b/pallets/xyk-liquidity-mining/src/tests/mock.rs index bc58c68c0..914ddb45b 100644 --- a/pallets/xyk-liquidity-mining/src/tests/mock.rs +++ b/pallets/xyk-liquidity-mining/src/tests/mock.rs @@ -161,13 +161,13 @@ thread_local! { #[derive(Copy, Clone)] pub struct DymmyGlobalFarm { total_rewards: Balance, - _planned_yielding_periods: PeriodOf, + planned_yielding_periods: PeriodOf, _blocks_per_period: BlockNumber, incentivized_asset: AssetId, reward_currency: AssetId, _owner: AccountId, - _yield_per_period: Perquintill, - _min_deposit: Balance, + yield_per_period: Perquintill, + min_deposit: Balance, price_adjustment: FixedU128, _max_reward_per_period: Balance, } @@ -529,13 +529,13 @@ impl hydradx_traits::liquidity_mining::Mutate f farm_id, DymmyGlobalFarm { total_rewards, - _planned_yielding_periods: planned_yielding_periods, + planned_yielding_periods, _blocks_per_period: blocks_per_period, incentivized_asset, reward_currency, _owner: owner, - _yield_per_period: yield_per_period, - _min_deposit: min_deposit, + yield_per_period, + min_deposit, price_adjustment, _max_reward_per_period: max_reward_per_period, }, @@ -812,6 +812,25 @@ impl hydradx_traits::liquidity_mining::Mutate f //NOTE: Basilisk is not using this fn. Err(sp_runtime::DispatchError::Other("Not implemented")) } + + fn update_global_farm( + global_farm_id: GlobalFarmId, + planned_yielding_periods: Self::Period, + yield_per_period: Perquintill, + min_deposit: Self::Balance, + ) -> Result<(), Self::Error> { + GLOBAL_FARMS.with(|v| { + let mut p = v.borrow_mut(); + + let global_farm = p.get_mut(&global_farm_id).unwrap(); + + global_farm.planned_yielding_periods = planned_yielding_periods; + global_farm.yield_per_period = yield_per_period; + global_farm.min_deposit = min_deposit; + + Ok(()) + }) + } } impl hydradx_traits::liquidity_mining::Inspect for DummyLiquidityMining { diff --git a/pallets/xyk/Cargo.toml b/pallets/xyk/Cargo.toml index 99c415ede..cc43a5eb1 100644 --- a/pallets/xyk/Cargo.toml +++ b/pallets/xyk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-xyk' -version = "6.4.4" +version = "6.4.5" description = 'XYK automated market maker' authors = ['GalacticCouncil'] edition = '2021' diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 285bfed06..cc8c89077 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "256.0.0" +version = "257.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 8e5c13482..601894f5f 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -113,7 +113,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 256, + spec_version: 257, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs b/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs index 0ce1a51ed..2c7a76dbb 100644 --- a/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs +++ b/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs @@ -19,7 +19,7 @@ //! Autogenerated weights for `pallet_omnipool_liquidity_mining` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-05-23, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2024-09-18, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `bench-bot`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -30,7 +30,7 @@ // pallet // --wasm-execution=compiled // --pallet -// * +// pallet_omnipool_liquidity_mining // --extrinsic // * // --heap-pages @@ -41,7 +41,7 @@ // 20 // --template=scripts/pallet-weight-template.hbs // --output -// weights/ +// ol.rs #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -71,13 +71,26 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `557` // Estimated: `6196` - // Minimum execution time: 80_933_000 picoseconds. - Weight::from_parts(81_673_000, 6196) + // Minimum execution time: 83_911_000 picoseconds. + Weight::from_parts(84_650_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:1 w:1) /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn update_global_farm() -> Weight { + // Proof Size summary in bytes: + // Measured: `573` + // Estimated: `3670` + // Minimum execution time: 22_855_000 picoseconds. + Weight::from_parts(23_088_000, 3670) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:1 w:1) + /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `Duster::AccountBlacklist` (r:1 w:1) @@ -86,8 +99,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `964` // Estimated: `6196` - // Minimum execution time: 79_070_000 picoseconds. - Weight::from_parts(79_648_000, 6196) + // Minimum execution time: 82_043_000 picoseconds. + Weight::from_parts(82_866_000, 6196) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -111,8 +124,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `2337` // Estimated: `6294` - // Minimum execution time: 123_093_000 picoseconds. - Weight::from_parts(124_230_000, 6294) + // Minimum execution time: 126_484_000 picoseconds. + Weight::from_parts(127_302_000, 6294) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -134,8 +147,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `2507` // Estimated: `6294` - // Minimum execution time: 126_053_000 picoseconds. - Weight::from_parts(127_134_000, 6294) + // Minimum execution time: 129_979_000 picoseconds. + Weight::from_parts(130_956_000, 6294) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -155,8 +168,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `2146` // Estimated: `6294` - // Minimum execution time: 120_037_000 picoseconds. - Weight::from_parts(120_995_000, 6294) + // Minimum execution time: 124_701_000 picoseconds. + Weight::from_parts(125_711_000, 6294) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -178,8 +191,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `2543` // Estimated: `6294` - // Minimum execution time: 124_191_000 picoseconds. - Weight::from_parts(124_920_000, 6294) + // Minimum execution time: 127_337_000 picoseconds. + Weight::from_parts(128_237_000, 6294) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -195,8 +208,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `924` // Estimated: `6196` - // Minimum execution time: 75_542_000 picoseconds. - Weight::from_parts(75_959_000, 6196) + // Minimum execution time: 77_685_000 picoseconds. + Weight::from_parts(78_427_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -234,8 +247,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `4072` // Estimated: `11598` - // Minimum execution time: 207_096_000 picoseconds. - Weight::from_parts(208_255_000, 11598) + // Minimum execution time: 213_332_000 picoseconds. + Weight::from_parts(215_220_000, 11598) .saturating_add(T::DbWeight::get().reads(17_u64)) .saturating_add(T::DbWeight::get().writes(14_u64)) } @@ -263,8 +276,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `4381` // Estimated: `11598` - // Minimum execution time: 171_971_000 picoseconds. - Weight::from_parts(173_516_000, 11598) + // Minimum execution time: 177_225_000 picoseconds. + Weight::from_parts(178_486_000, 11598) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -286,8 +299,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `2946` // Estimated: `8799` - // Minimum execution time: 165_734_000 picoseconds. - Weight::from_parts(167_004_000, 8799) + // Minimum execution time: 176_191_000 picoseconds. + Weight::from_parts(177_601_000, 8799) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -319,8 +332,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `3940` // Estimated: `8799` - // Minimum execution time: 250_173_000 picoseconds. - Weight::from_parts(251_222_000, 8799) + // Minimum execution time: 260_267_000 picoseconds. + Weight::from_parts(263_572_000, 8799) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)) } diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 2a08d0e31..e815c38ba 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "3.5.0" +version = "3.6.0" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" diff --git a/traits/src/liquidity_mining.rs b/traits/src/liquidity_mining.rs index d79a8b809..c22e680ad 100644 --- a/traits/src/liquidity_mining.rs +++ b/traits/src/liquidity_mining.rs @@ -52,6 +52,14 @@ pub trait Mutate { price_adjustment: FixedU128, ) -> Result<(), Self::Error>; + /// Update global farm parameters + fn update_global_farm( + global_farm_id: GlobalFarmId, + planned_yielding_periods: Self::Period, + yield_per_period: Perquintill, + min_deposit: Self::Balance, + ) -> Result<(), Self::Error>; + /// Terminate existing global farm. /// /// Returns: `(reward currency, undistributed rewards, destination account)`