From e90fe6dd110f245caf6d82f6ae5680ffb6e5d856 Mon Sep 17 00:00:00 2001 From: dmoka Date: Wed, 17 Dec 2025 14:56:12 +0100 Subject: [PATCH 01/29] add shares limit to add_liquidity_stableswap_omnipool_and_join_farms --- .../src/omnipool_liquidity_mining.rs | 99 ++++++++- pallets/omnipool-liquidity-mining/src/lib.rs | 6 +- ...dity_stableswap_omnipool_and_join_farms.rs | 190 ++++++++++++++++++ .../benchmarking/omnipool_liquidity_mining.rs | 2 +- 4 files changed, 292 insertions(+), 5 deletions(-) diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index bb58931852..9b4691a842 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -841,7 +841,8 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_work_for_multiple_far ] .try_into() .unwrap(), - Some(farms.try_into().unwrap()) + Some(farms.try_into().unwrap()), + None, ) ); @@ -1045,7 +1046,8 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_fail_stableshare_goes ] .try_into() .unwrap(), - Some(farms.try_into().unwrap()) + Some(farms.try_into().unwrap()), + None, ), pallet_omnipool::Error::::InsufficientBalance ); @@ -1128,7 +1130,8 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_add_only_liquidty_whe ] .try_into() .unwrap(), - None + None, + None, ) ); @@ -1167,6 +1170,96 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_add_only_liquidty_whe }); } +#[test] +fn add_liquidity_stableswap_omnipool_and_join_farms_should_fail_when_slippage_limit_reached() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let global_farm_1_id = 1; + let global_farm_2_id = 2; + let global_farm_3_id = 3; + let yield_farm_1_id = 4; + let yield_farm_2_id = 5; + let yield_farm_3_id = 6; + + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //NOTE: necessary to get oracle price. + hydradx_run_to_block(100); + set_relaychain_block_number(100); + create_global_farm(None, None); + create_global_farm(None, None); + create_global_farm(None, None); + + set_relaychain_block_number(200); + create_yield_farm(global_farm_1_id, stable_pool_id); + create_yield_farm(global_farm_2_id, stable_pool_id); + create_yield_farm(global_farm_3_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + + set_relaychain_block_number(400); + let farms = vec![ + (global_farm_1_id, yield_farm_1_id), + (global_farm_2_id, yield_farm_2_id), + (global_farm_3_id, yield_farm_3_id), + ]; + + //Act and assert - try to add liquidity with impossible slippage limit + assert_noop!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + Some(100_000 * UNITS), // Impossibly high slippage limit + ), + pallet_omnipool::Error::::SlippageLimit + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + #[test] fn withdraw_shares_should_work_when_deposit_exists() { TestNet::reset(); diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 968639da66..fedc2db4cb 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1039,6 +1039,8 @@ pub mod pallet { /// - `stable_pool_id`: id of the stableswap pool to add liquidity to. /// - `stable_asset_amounts`: amount of each asset to be deposited into the stableswap pool. /// - `farm_entries`: list of farms to join. + /// - `min_shares_limit`: optional minimum Omnipool shares to receive (slippage protection). + /// Applies to Omnipool step only. None defaults to no protection. /// /// Emits `LiquidityAdded` events from both pool /// Emits `SharesDeposited` event for the first farm entry @@ -1058,16 +1060,18 @@ pub mod pallet { stable_pool_id: T::AssetId, stable_asset_amounts: BoundedVec, ConstU32>, farm_entries: Option>, + min_shares_limit: Option, ) -> DispatchResult { let who = ensure_signed(origin.clone())?; let stablepool_shares = T::Stableswap::add_liquidity(who, stable_pool_id, stable_asset_amounts.to_vec())?; + let min_shares_limit = min_shares_limit.unwrap_or(Balance::MIN); let position_id = OmnipoolPallet::::do_add_liquidity_with_limit( origin.clone(), stable_pool_id, stablepool_shares, - Balance::MIN, + min_shares_limit, )?; if let Some(farms) = farm_entries { diff --git a/pallets/omnipool-liquidity-mining/src/tests/add_liquidity_stableswap_omnipool_and_join_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/add_liquidity_stableswap_omnipool_and_join_farms.rs index 380a846bc1..c5078ea935 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/add_liquidity_stableswap_omnipool_and_join_farms.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/add_liquidity_stableswap_omnipool_and_join_farms.rs @@ -91,6 +91,7 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_work_with_single_yiel STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), Some(yield_farms.try_into().unwrap()), + None, )); //Assert that liquidity is added @@ -231,6 +232,7 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_work_with_multiple_yi STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), Some(yield_farms.try_into().unwrap()), + None, )); //Assert that liquidity is added @@ -404,8 +406,196 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_fail_when_origin_is_n STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), Some(yield_farms.try_into().unwrap()), + None, ), BadOrigin ); }); } + +#[test] +fn add_liquidity_stableswap_with_min_limit_should_work() { + let token_amount = 2000 * ONE; + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 5000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP1, DAI, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (CHARLIE, HDX, 100_000_000 * ONE), + (BOB, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let amount = 20 * ONE; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + Some(Balance::MIN), + )); + }); +} + +#[test] +fn add_liquidity_stableswap_with_exact_limit_should_work() { + let token_amount = 2000 * ONE; + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 5000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP1, DAI, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (CHARLIE, HDX, 100_000_000 * ONE), + (BOB, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let amount = 20 * ONE; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + Some(SHARES_FROM_STABLESWAP), + )); + }); +} + +#[test] +fn add_liquidity_stableswap_with_high_limit_should_fail() { + let token_amount = 2000 * ONE; + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 5000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP1, DAI, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (CHARLIE, HDX, 100_000_000 * ONE), + (BOB, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let amount = 20 * ONE; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + assert_noop!( + OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + Some(SHARES_FROM_STABLESWAP + 1), + ), + pallet_omnipool::Error::::SlippageLimit + ); + }); +} + +#[test] +fn add_liquidity_stableswap_with_none_limit_should_work() { + let token_amount = 2000 * ONE; + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 5000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP1, DAI, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (CHARLIE, HDX, 100_000_000 * ONE), + (BOB, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let amount = 20 * ONE; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + }); +} diff --git a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs index 77f72b47a5..8764d525ea 100644 --- a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs +++ b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs @@ -924,7 +924,7 @@ runtime_benchmarks! { let _ = Tokens::deposit(pool_id, &lp_provider, 50000000000000000);//We mint some share token so it wont fail with insufficience balance in adding liqudity to omnipool update_deposit_limit(pool_id, 1_000u128).expect("Failed to update deposit limit");//To trigger circuit breaker, leading to worst case update_deposit_limit(LRNA, 1_000u128).expect("Failed to update deposit limit");//To trigger circuit breaker, leading to worst case - }: _(RawOrigin::Signed(lp_provider),pool_id, added_liquidity.try_into().unwrap(), Some(farms.try_into().unwrap())) + }: _(RawOrigin::Signed(lp_provider),pool_id, added_liquidity.try_into().unwrap(), Some(farms.try_into().unwrap()), None) //NOTE: price adjustment reads route's price from oracle so pool type doesn't matter From ddfe432b207d0b657dc7856c9ae1dd0c3dde8287 Mon Sep 17 00:00:00 2001 From: dmoka Date: Fri, 19 Dec 2025 07:36:19 +0100 Subject: [PATCH 02/29] add remove_liquidity_stableswap_omnipool_and_exit_farms with tests --- .../src/omnipool_liquidity_mining.rs | 840 +++++++++++++++++- pallets/liquidity-mining/src/lib.rs | 4 + pallets/omnipool-liquidity-mining/src/lib.rs | 103 +++ .../src/tests/mock.rs | 22 + .../src/tests/mod.rs | 1 + ...dity_stableswap_omnipool_and_exit_farms.rs | 536 +++++++++++ pallets/omnipool/src/lib.rs | 297 ++++--- pallets/stableswap/src/lib.rs | 277 ++++-- .../xyk-liquidity-mining/src/tests/mock.rs | 10 + traits/src/liquidity_mining.rs | 3 + traits/src/stableswap.rs | 15 + 11 files changed, 1891 insertions(+), 217 deletions(-) create mode 100644 pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index 9b4691a842..57944f0fc4 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -1260,6 +1260,787 @@ fn add_liquidity_stableswap_omnipool_and_join_farms_should_fail_when_slippage_li }); } +mod remove_liquidity_stableswap_omnipool_and_exit_farms { + use super::*; + use hydradx_traits::stableswap::AssetAmount; + use pretty_assertions::assert_eq; + use sp_runtime::FixedU128; + + #[test] + fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_for_multiple_farms() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + let global_farm_1_id = 1; + let global_farm_2_id = 2; + let global_farm_3_id = 3; + let yield_farm_1_id = 4; + let yield_farm_2_id = 5; + let yield_farm_3_id = 6; + + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //NOTE: necessary to get oracle price. + hydradx_run_to_block(100); + set_relaychain_block_number(100); + create_global_farm(None, None); + create_global_farm(None, None); + create_global_farm(None, None); + + set_relaychain_block_number(200); + create_yield_farm(global_farm_1_id, stable_pool_id); + create_yield_farm(global_farm_2_id, stable_pool_id); + create_yield_farm(global_farm_3_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidity to make sure that it does not interfere with the new liquidity add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + let position_id = hydradx_runtime::Omnipool::next_position_id(); + + set_relaychain_block_number(400); + let deposit_id = 1; + let farms = vec![ + (global_farm_1_id, yield_farm_1_id), + (global_farm_2_id, yield_farm_2_id), + (global_farm_3_id, yield_farm_3_id), + ]; + + //Add liquidity first + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + None, + ) + ); + + // Wait some blocks + set_relaychain_block_number(500); + + // Store balance before removal + let charlie_asset_1_balance_before = + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(CHARLIE)); + let charlie_asset_2_balance_before = + hydradx_runtime::Currencies::free_balance(stable_asset_2, &AccountId::from(CHARLIE)); + + let asset_ids_without_slippage: Vec> = Stableswap::pools(stable_pool_id) + .into_iter() + .flat_map(|pool_info| pool_info.assets.into_iter()) + .map(|asset_id| AssetAmount::::new(asset_id.into(), 10000)) + .collect(); + + let asset_ids_without_slippage = create_bounded_vec(asset_ids_without_slippage); + + //Act - Remove liquidity and exit all farms + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(CHARLIE.into()), + deposit_id, + stable_pool_id, + asset_ids_without_slippage + ) + ); + + //Assert + // Verify deposit is destroyed + assert!(hydradx_runtime::OmnipoolWarehouseLM::deposit(deposit_id).is_none()); + + // Verify NFT was destroyed (all liquidity removed) + assert!( + hydradx_runtime::Uniques::owner( + hydradx_runtime::OmnipoolCollectionId::get(), + position_id + ) + .is_none(), + "NFT should be destroyed after removing all liquidity" + ); + + // Verify balances increased (approximately, accounting for fees) + let charlie_asset_1_balance_after = + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(CHARLIE)); + let charlie_asset_2_balance_after = + hydradx_runtime::Currencies::free_balance(stable_asset_2, &AccountId::from(CHARLIE)); + + assert!(charlie_asset_1_balance_after > charlie_asset_1_balance_before); + assert!(charlie_asset_2_balance_after > charlie_asset_2_balance_before); + + //Verify SharesWithdrawn events for all 3 farms + expect_shares_withdrawn_ln_events(vec![ + RuntimeEvent::OmnipoolLiquidityMining( + pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id: 1, + yield_farm_id: 4, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + }, + ), + RuntimeEvent::OmnipoolLiquidityMining( + pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id: 2, + yield_farm_id: 5, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + }, + ), + RuntimeEvent::OmnipoolLiquidityMining( + pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id: 3, + yield_farm_id: 6, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + }, + ), + ]); + + // Verify LiquidityRemoved events + expect_omnipool_liquidity_removed_events(vec![pallet_omnipool::Event::LiquidityRemoved { + who: CHARLIE.into(), + position_id, + asset_id: stable_pool_id, + shares_removed: 20044549999405, + fee: FixedU128::from_float(0.000100000000000000), + } + .into()]); + + expect_stableswap_liquidity_removed_events(vec![ + pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 20042545544405, + amounts: vec![ + AssetAmount::new(1000002, 3984601523849), + AssetAmount::new(1000003, 3984601484003), + AssetAmount::new(1000004, 3984601484003), + AssetAmount::new(1000005, 3984601484003), + AssetAmount::new(1000006, 3984601523849), + ], + fee: 0, + } + .into(), + ]); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + + #[test] + fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_asset_withdrawal() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + let global_farm_id = 1; + let yield_farm_id = 2; + + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //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_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidity to make sure that it does not interfere with the new liquidity add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + let position_id = hydradx_runtime::Omnipool::next_position_id(); + + set_relaychain_block_number(400); + let deposit_id = 1; + let farms = vec![(global_farm_id, yield_farm_id)]; + + //Add liquidity first + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + None, + ) + ); + + // Wait some blocks + set_relaychain_block_number(500); + + // Store balance before removal - only checking stable_asset_1 + let charlie_asset_1_balance_before = + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(CHARLIE)); + let charlie_asset_2_balance_before = + hydradx_runtime::Currencies::free_balance(stable_asset_2, &AccountId::from(CHARLIE)); + + //Act - Remove liquidity with single asset (only stable_asset_1 has non-zero min_amount) + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(CHARLIE.into()), + deposit_id, + stable_pool_id, + vec![AssetAmount::new(stable_asset_1, 0)].try_into().unwrap(), + ) + ); + + //Assert + // Verify deposit is destroyed + assert!(hydradx_runtime::OmnipoolWarehouseLM::deposit(deposit_id).is_none()); + + // Verify NFT was destroyed (all liquidity removed) + assert!( + hydradx_runtime::Uniques::owner( + hydradx_runtime::OmnipoolCollectionId::get(), + position_id + ) + .is_none(), + "NFT should be destroyed after removing all liquidity" + ); + + // Verify only asset_1 balance increased (single asset withdrawal) + let charlie_asset_1_balance_after = + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(CHARLIE)); + let charlie_asset_2_balance_after = + hydradx_runtime::Currencies::free_balance(stable_asset_2, &AccountId::from(CHARLIE)); + + assert!(charlie_asset_1_balance_after > charlie_asset_1_balance_before); + // asset_2 balance should remain unchanged for single asset withdrawal + assert_eq!(charlie_asset_2_balance_after, charlie_asset_2_balance_before); + + // Verify SharesWithdrawn and DepositDestroyed events + expect_shares_withdrawn_ln_events(vec![ + pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id, + yield_farm_id, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + } + .into() + ]); + + // Verify LiquidityRemoved event from omnipool + expect_omnipool_liquidity_removed_events(vec![pallet_omnipool::Event::LiquidityRemoved { + who: CHARLIE.into(), + position_id, + asset_id: stable_pool_id, + shares_removed: 20044549999405, + fee: FixedU128::from_float(0.000100000000000000), + } + .into()]); + + expect_stableswap_liquidity_removed_events(vec![ + pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 20042545544405, + amounts: vec![ + AssetAmount::new( stable_asset_1, 19823392461976), + ], + fee: 99615037340, + } + .into(), + ]); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + + #[test] + fn remove_liquidity_stableswap_omnipool_and_exit_farms_with_rewards_round_trip() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + let global_farm_id = 1; + let yield_farm_id = 2; + + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //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_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidity to make sure that it does not interfere with the new liquidity add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + let position_id = hydradx_runtime::Omnipool::next_position_id(); + + set_relaychain_block_number(400); + let deposit_id = 1; + let farms = vec![(global_farm_id, yield_farm_id)]; + + //Add liquidity first + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + None, + ) + ); + + // Store HDX balance before to verify rewards + let charlie_hdx_balance_before = + hydradx_runtime::Currencies::free_balance(HDX, &AccountId::from(CHARLIE)); + + // Wait significant number of blocks to accumulate rewards + set_relaychain_block_number(1000); + + //Act - Remove liquidity and exit all farms (should claim rewards) + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(CHARLIE.into()), + deposit_id, + stable_pool_id, + vec![AssetAmount::new(stable_asset_1, 0)] + .try_into() + .unwrap(), + ) + ); + + //Assert + // Verify deposit is destroyed + assert!(hydradx_runtime::OmnipoolWarehouseLM::deposit(deposit_id).is_none()); + + // Verify NFT was destroyed (all liquidity removed) + assert!( + hydradx_runtime::Uniques::owner( + hydradx_runtime::OmnipoolCollectionId::get(), + position_id + ) + .is_none(), + "NFT should be destroyed after removing all liquidity" + ); + + // Verify HDX rewards were received + let charlie_hdx_balance_after = + hydradx_runtime::Currencies::free_balance(HDX, &AccountId::from(CHARLIE)); + + assert!( + charlie_hdx_balance_after > charlie_hdx_balance_before, + "Should have received HDX rewards" + ); + + // Verify original liquidity returned (approximately) + let charlie_asset_1_balance_after = + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(CHARLIE)); + let charlie_asset_2_balance_after = + hydradx_runtime::Currencies::free_balance(stable_asset_2, &AccountId::from(CHARLIE)); + + // Should have approximately 100 UNITS (90 initial + ~10 returned, accounting for fees) + assert!(charlie_asset_1_balance_after > 95 * UNITS); + assert!(charlie_asset_2_balance_after > 95 * UNITS); + + // Verify LiquidityRemoved events + expect_omnipool_liquidity_removed_events(vec![pallet_omnipool::Event::LiquidityRemoved { + who: CHARLIE.into(), + position_id, + asset_id: stable_pool_id, + shares_removed: 10009699999606, + fee: FixedU128::from_float(0.000100000000000000), + } + .into()]); + + expect_stableswap_liquidity_removed_events(vec![pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 10008699029606, + amounts: vec![ + AssetAmount::new(stable_asset_1, 9899259975202), + ], + fee: 49745024898, + } + .into()]); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + + #[test] + fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_when_not_owner() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + let global_farm_id = 1; + let yield_farm_id = 2; + + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //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_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidity to make sure that it does not interfere with the new liquidity add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + set_relaychain_block_number(400); + let deposit_id = 1; + let farms = vec![(global_farm_id, yield_farm_id)]; + + //CHARLIE adds liquidity and joins farms + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + None, + ) + ); + + set_relaychain_block_number(500); + + //Act - DAVE tries to remove CHARLIE's liquidity (should fail) + assert_noop!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(DAVE.into()), + deposit_id, + stable_pool_id, + vec![AssetAmount::new(stable_asset_1, 0), AssetAmount::new(stable_asset_2, 0)] + .try_into() + .unwrap(), + ), + pallet_omnipool_liquidity_mining::Error::::Forbidden + ); + + //Assert - Deposit should still exist + assert!(hydradx_runtime::OmnipoolWarehouseLM::deposit(deposit_id).is_some()); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + + #[test] + fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_with_empty_assets() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + let global_farm_id = 1; + let yield_farm_id = 2; + + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //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_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidity to make sure that it does not interfere with the new liquidity add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + set_relaychain_block_number(400); + let deposit_id = 1; + let farms = vec![(global_farm_id, yield_farm_id)]; + + //CHARLIE adds liquidity and joins farms + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + None, + ) + ); + + set_relaychain_block_number(500); + + //Act - Try to remove with empty min_amounts_out (should fail) + assert_noop!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(CHARLIE.into()), + deposit_id, + stable_pool_id, + vec![].try_into().unwrap(), + ), + pallet_omnipool_liquidity_mining::Error::::NoAssetsSpecified + ); + + //Assert - Deposit should still exist + assert!(hydradx_runtime::OmnipoolWarehouseLM::deposit(deposit_id).is_some()); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + + use frame_support::traits::ConstU32; + pub fn create_bounded_vec( + assets: Vec>, + ) -> BoundedVec, ConstU32<{ MAX_ASSETS_IN_POOL }>> { + let bounded_vec: BoundedVec, ConstU32<{ MAX_ASSETS_IN_POOL }>> = assets.try_into().unwrap(); + + bounded_vec + } +} + #[test] fn withdraw_shares_should_work_when_deposit_exists() { TestNet::reset(); @@ -2459,7 +3240,7 @@ pub fn expect_reward_claimed_events(e: Vec) { } pub fn expect_lm_events(e: Vec) { - let last_events = test_utils::last_events::(10); + let last_events = test_utils::last_events::(40); let mut reward_claimed_events = vec![]; @@ -2474,7 +3255,7 @@ pub fn expect_lm_events(e: Vec) { >::SharesDeposited { .. }) | RuntimeEvent::OmnipoolLiquidityMining(pallet_omnipool_liquidity_mining::Event::< hydradx_runtime::Runtime, - >::SharesRedeposited { .. }) + >::SharesRedeposited { .. } ) ) { reward_claimed_events.push(e); } @@ -2483,6 +3264,25 @@ pub fn expect_lm_events(e: Vec) { pretty_assertions::assert_eq!(reward_claimed_events, e); } +pub fn expect_shares_withdrawn_ln_events(expected: Vec) { + let last_events = + test_utils::last_events::(80); + + let shares_withdrawn_events: Vec = last_events + .into_iter() + .filter(|event| { + matches!( + event, + RuntimeEvent::OmnipoolLiquidityMining( + pallet_omnipool_liquidity_mining::Event::::SharesWithdrawn { .. } + ) + ) + }) + .collect(); + + pretty_assertions::assert_eq!(shares_withdrawn_events, expected); +} + pub fn expect_stableswap_liquidity_added_events(e: Vec) { let last_events = test_utils::last_events::(40); @@ -2519,6 +3319,42 @@ pub fn expect_omnipool_liquidity_added_events(e: Vec) { pretty_assertions::assert_eq!(events, e); } +pub fn expect_stableswap_liquidity_removed_events(e: Vec) { + let last_events = test_utils::last_events::(40); + + let mut events = vec![]; + + for event in &last_events { + let e = event.clone(); + if matches!( + e, + RuntimeEvent::Stableswap(pallet_stableswap::Event::::LiquidityRemoved { .. }) + ) { + events.push(e); + } + } + + pretty_assertions::assert_eq!(events, e); +} + +pub fn expect_omnipool_liquidity_removed_events(e: Vec) { + let last_events = test_utils::last_events::(40); + + let mut events = vec![]; + + for event in &last_events { + let e = event.clone(); + if matches!( + e, + RuntimeEvent::Omnipool(pallet_omnipool::Event::::LiquidityRemoved { .. }) + ) { + events.push(e); + } + } + + pretty_assertions::assert_eq!(events, e); +} + //TODO: duplicated, remove duplication pub fn init_stableswap() -> Result<(AssetId, AssetId, AssetId), DispatchError> { let initial_liquidity = 1_000_000_000_000_000_000_000u128; diff --git a/pallets/liquidity-mining/src/lib.rs b/pallets/liquidity-mining/src/lib.rs index 89194668a0..2f9919bfed 100644 --- a/pallets/liquidity-mining/src/lib.rs +++ b/pallets/liquidity-mining/src/lib.rs @@ -1996,6 +1996,10 @@ impl, I: 'static> hydradx_traits::liquidity_mining::Mutate Option { Self::get_global_farm_id(deposit_id, yield_farm_id) } + + fn get_yield_farm_ids(deposit_id: DepositId) -> Option> { + Self::deposit(deposit_id).map(|deposit| deposit.yield_farm_entries.iter().map(|e| e.yield_farm_id).collect()) + } } impl, I: 'static> hydradx_traits::liquidity_mining::Inspect for Pallet { diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index fedc2db4cb..b87af99413 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -318,6 +318,9 @@ pub mod pallet { /// No farms specified to join NoFarmEntriesSpecified, + + /// No assets specified in the withdrawal + NoAssetsSpecified, } //NOTE: these errors should never happen. @@ -1080,6 +1083,106 @@ pub mod pallet { Ok(()) } + + /// Remove liquidity from stableswap and omnipool, exiting all associated yield farms. + /// + /// This extrinsic reverses the operation performed by `add_liquidity_stableswap_omnipool_and_join_farms`. + /// It performs the following steps in order: + /// 1. Exits from ALL yield farms associated with the deposit (claiming rewards) + /// 2. Removes liquidity from the omnipool to retrieve stableswap shares + /// 3. Removes liquidity from the stableswap pool to retrieve underlying assets + /// + /// The asset removal strategy is determined by the `min_amounts_out` parameter length: + /// - If 1 asset is specified: Uses `remove_liquidity_one_asset` (trading fee applies) + /// - If multiple assets: Uses `remove_liquidity` (proportional, no trading fee) + /// + /// Parameters: + /// - `origin`: Owner of the deposit NFT + /// - `deposit_id`: The liquidity mining deposit NFT ID to unwind + /// - `min_amounts_out`: Asset IDs and minimum amounts for slippage protection + /// + /// Emits multiple events: + /// - `RewardClaimed` for each farm (if rewards > 0) + /// - `SharesWithdrawn` for each farm + /// - `DepositDestroyed` when deposit is fully exited + /// - Omnipool's `LiquidityRemoved` + /// - Stableswap's `LiquidityRemoved` + #[pallet::call_index(17)] + #[pallet::weight(Weight::default())] + //TODO: BENCHMARK AND ADD WEIGHT + // #[pallet::weight({ + // let deposit_data = T::LiquidityMiningHandler::deposit(*deposit_id); + // let farm_count = match deposit_data { + // Some(data) => data.yield_farm_entries.len() as u32, + // None => 0, + // }; + // ::WeightInfo::exit_farms(farm_count) + // .saturating_add(::WeightInfo::price_adjustment_get().saturating_mul(farm_count as u64)) + // .saturating_add(::WeightInfo::remove_liquidity()) + // .saturating_add(if min_amounts_out.len() == 1 { + // ::WeightInfo::remove_liquidity_one_asset() + // } else { + // ::WeightInfo::remove_liquidity() + // }) + // })] + pub fn remove_liquidity_stableswap_omnipool_and_exit_farms( + origin: OriginFor, + deposit_id: DepositId, + stable_pool_id: T::AssetId, + min_amounts_out: BoundedVec, ConstU32>, + ) -> DispatchResult { + //TODO: ensure deposit owner is the same as origin + let who = ensure_signed(origin.clone())?; + ensure!(!min_amounts_out.is_empty(), Error::::NoAssetsSpecified); + + // Collect all yield farm IDs from deposit + let yield_farm_ids: BoundedVec = + T::LiquidityMiningHandler::get_yield_farm_ids(deposit_id) + .ok_or(Error::::InconsistentState( + InconsistentStateError::DepositDataNotFound, + ))? + .try_into() + .map_err(|_| { + Error::::InconsistentState(InconsistentStateError::DepositDataNotFound) + })?; + + // CRITICAL: Get position_id BEFORE exit_farms clears the storage mapping + let position_id = OmniPositionId::::get(deposit_id) + .ok_or(Error::::InconsistentState(InconsistentStateError::MissingLpPosition))?; + + Self::exit_farms(origin.clone(), deposit_id, yield_farm_ids)?; + + let omnipool_position = OmnipoolPallet::::load_position(position_id, who.clone())?; + let omnipool_shares_to_remove = omnipool_position.shares; + + let actual_stable_shares_received = OmnipoolPallet::::do_remove_liquidity_with_limit( + origin.clone(), + position_id, + omnipool_shares_to_remove, + Balance::MIN, // No slippage limit for omnipool removal + )?; + + if min_amounts_out.len() == 1 { + let asset_amount = &min_amounts_out[0]; + + T::Stableswap::remove_liquidity_one_asset( + who.clone(), + stable_pool_id, + asset_amount.asset_id, + actual_stable_shares_received, + asset_amount.amount, + )?; + } else { + T::Stableswap::remove_liquidity( + who, + stable_pool_id, + actual_stable_shares_received, + min_amounts_out.to_vec(), + )?; + } + + Ok(()) + } } } diff --git a/pallets/omnipool-liquidity-mining/src/tests/mock.rs b/pallets/omnipool-liquidity-mining/src/tests/mock.rs index dc9345a727..73e8091607 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/mock.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/mock.rs @@ -69,6 +69,7 @@ pub const DOT: AssetId = 1_000; pub const KSM: AssetId = 1_001; pub const ACA: AssetId = 1_002; pub const USDT: AssetId = 1_003; +pub const USDC: AssetId = 1_004; pub const LP1: AccountId = 1; pub const LP2: AccountId = 2; @@ -200,6 +201,27 @@ impl StableswapAddLiquidity for StableswapAddLiquid ) -> Result { Ok(SHARES_FROM_STABLESWAP) } + + fn remove_liquidity_one_asset( + _who: AccountId, + _pool_id: AssetId, + _asset_id: AssetId, + _share_amount: Balance, + _min_amount_out: Balance, + ) -> Result { + // For testing purposes, return a reasonable amount + Ok(_share_amount) + } + + fn remove_liquidity( + _who: AccountId, + _pool_id: AssetId, + _share_amount: Balance, + _min_amounts_out: Vec>, + ) -> Result<(), DispatchError> { + // For testing purposes, just return Ok + Ok(()) + } } parameter_types! { diff --git a/pallets/omnipool-liquidity-mining/src/tests/mod.rs b/pallets/omnipool-liquidity-mining/src/tests/mod.rs index 9022446e5a..6980829133 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/mod.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/mod.rs @@ -39,6 +39,7 @@ pub mod exit_farms; pub mod join_farms; pub mod mock; pub mod redeposit_shares; +pub mod remove_liquidity_stableswap_omnipool_and_exit_farms; pub mod resume_yield_farm; pub mod stop_yield_farm; pub mod terminate_global_farm; diff --git a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs new file mode 100644 index 0000000000..5ef5c10d3d --- /dev/null +++ b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs @@ -0,0 +1,536 @@ +// 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::*; + +use pallet_omnipool::types::{AssetReserveState, Tradability}; +use pretty_assertions::assert_eq; + +#[test] +fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_asset_removal() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // Add liquidity first + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + // Remove liquidity to single asset + assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + deposit_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + )); + + // Verify omnipool state returned to initial (all liquidity removed) + assert_asset_state!( + STABLESWAP_POOL_ID, + AssetReserveState { + reserve: token_amount, + hub_reserve: 1_300_000_000_000_000, + shares: token_amount, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + // Verify storage is cleaned up + assert_eq!(crate::OmniPositionId::::get(deposit_id), None); + assert_eq!( + pallet_liquidity_mining::Deposit::::get(deposit_id), + None + ); + + // Verify DepositDestroyed event was emitted + assert!(has_event( + crate::Event::DepositDestroyed { who: LP1, deposit_id }.into() + )); + }); +} + +#[test] +fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multi_asset_removal() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP1, USDC, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(USDC) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(USDC, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // Add liquidity first + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount), AssetAmount::new(USDC, amount)] + .try_into() + .unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + // Remove liquidity proportionally to multiple assets + assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + deposit_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1), AssetAmount::new(USDC, 1)] + .try_into() + .unwrap(), + )); + + // Verify omnipool state returned to initial (all liquidity removed) + assert_asset_state!( + STABLESWAP_POOL_ID, + AssetReserveState { + reserve: token_amount, + hub_reserve: 1_300_000_000_000_000, + shares: token_amount, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + // Verify storage is cleaned up + assert_eq!(crate::OmniPositionId::::get(deposit_id), None); + assert_eq!( + pallet_liquidity_mining::Deposit::::get(deposit_id), + None + ); + }); +} + +#[test] +fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multiple_farms() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + (CHARLIE, HDX, 100_000_000 * ONE), + (BOB, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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_global_farm( + 80_000_000 * ONE, + 2_628_000, + 1, + HDX, + CHARLIE, + Perquintill::from_float(0.000_000_15_f64), + 1_000, + FixedU128::one(), + ) + .with_global_farm( + 60_000_000 * ONE, + 2_428_000, + 1, + HDX, + BOB, + Perquintill::from_float(0.000_000_14_f64), + 1_000, + FixedU128::one(), + ) + .with_yield_farm(GC, 1, STABLESWAP_POOL_ID, FixedU128::one(), None) + .with_yield_farm(CHARLIE, 2, STABLESWAP_POOL_ID, FixedU128::one(), None) + .with_yield_farm(BOB, 3, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 4; + let charlie_g_farm_id = 2; + let charlie_y_farm_id = 5; + let bob_g_farm_id = 3; + let bob_y_farm_id = 6; + let deposit_id = 1; + let yield_farms = vec![ + (gc_g_farm_id, gc_y_farm_id), + (charlie_g_farm_id, charlie_y_farm_id), + (bob_g_farm_id, bob_y_farm_id), + ]; + + // Add liquidity and join 3 farms + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + // Remove liquidity - should exit all 3 farms automatically + assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + deposit_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + )); + + // Verify omnipool state returned to initial (all liquidity removed) + assert_asset_state!( + STABLESWAP_POOL_ID, + AssetReserveState { + reserve: token_amount, + hub_reserve: 1_300_000_000_000_000, + shares: token_amount, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + // Verify all farms were exited (SharesWithdrawn events) + assert!(has_event( + crate::Event::SharesWithdrawn { + global_farm_id: gc_g_farm_id, + yield_farm_id: gc_y_farm_id, + who: LP1, + amount: SHARES_FROM_STABLESWAP, + deposit_id, + } + .into() + )); + assert!(has_event( + crate::Event::SharesWithdrawn { + global_farm_id: charlie_g_farm_id, + yield_farm_id: charlie_y_farm_id, + who: LP1, + amount: SHARES_FROM_STABLESWAP, + deposit_id, + } + .into() + )); + assert!(has_event( + crate::Event::SharesWithdrawn { + global_farm_id: bob_g_farm_id, + yield_farm_id: bob_y_farm_id, + who: LP1, + amount: SHARES_FROM_STABLESWAP, + deposit_id, + } + .into() + )); + + // Verify storage is cleaned up + assert_eq!(crate::OmniPositionId::::get(deposit_id), None); + assert_eq!( + pallet_liquidity_mining::Deposit::::get(deposit_id), + None + ); + }); +} + +#[test] +fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_when_not_owner() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP2, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // LP1 adds liquidity + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + // LP2 tries to remove LP1's liquidity - should fail + assert_noop!( + OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP2), + deposit_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + ), + crate::Error::::Forbidden + ); + }); +} + +#[test] +fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_with_empty_assets() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // Add liquidity first + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + // Try to remove with empty assets list - should fail + assert_noop!( + OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + deposit_id, + STABLESWAP_POOL_ID, + vec![].try_into().unwrap(), + ), + Error::::NoAssetsSpecified + ); + }); +} + +#[test] +fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewards() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // Add liquidity + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + // Wait some blocks to accumulate rewards + set_block_number(100); + + let hdx_balance_before = Tokens::free_balance(HDX, &LP1); + + // Remove liquidity + assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + deposit_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + )); + + // Verify omnipool state returned to initial (all liquidity removed) + assert_asset_state!( + STABLESWAP_POOL_ID, + AssetReserveState { + reserve: token_amount, + hub_reserve: 1_300_000_000_000_000, + shares: token_amount, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + let hdx_balance_after = Tokens::free_balance(HDX, &LP1); + + // Verify user received HDX rewards (if any were generated) + // Note: rewards might be 0 in test depending on block time configuration + if hdx_balance_after > hdx_balance_before { + // Rewards were claimed + assert!(has_event( + crate::Event::RewardClaimed { + global_farm_id: gc_g_farm_id, + yield_farm_id: gc_y_farm_id, + who: LP1, + claimed: hdx_balance_after - hdx_balance_before, + reward_currency: HDX, + deposit_id, + } + .into() + )); + } + + // Verify all storage is cleaned up + assert_eq!(crate::OmniPositionId::::get(deposit_id), None); + assert_eq!( + pallet_liquidity_mining::Deposit::::get(deposit_id), + None + ); + }); +} diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index 0943683e8e..e6c192d103 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -679,148 +679,7 @@ pub mod pallet { amount: Balance, min_limit: Balance, ) -> DispatchResult { - let who = ensure_signed(origin.clone())?; - - ensure!(amount > Balance::zero(), Error::::InvalidSharesAmount); - - ensure!( - T::NFTHandler::owner(&T::NFTCollectionId::get(), &position_id) == Some(who.clone()), - Error::::Forbidden - ); - - let position = Positions::::get(position_id).ok_or(Error::::PositionNotFound)?; - - ensure!(position.shares >= amount, Error::::InsufficientShares); - - let asset_id = position.asset_id; - - let asset_state = Self::load_asset_state(asset_id)?; - - ensure!( - asset_state.tradable.contains(Tradability::REMOVE_LIQUIDITY), - Error::::NotAllowed - ); - - // We need to call this to ensure that the fee is calculated correctly - // Although we dont need, but we need the fee to update. - let _ = T::Fee::get_and_store((asset_id, asset_state.reserve)); - - let safe_withdrawal = asset_state.tradable.is_safe_withdrawal(); - // Skip price check if safe withdrawal - trading disabled. - if !safe_withdrawal { - T::PriceBarrier::ensure_price( - &who, - T::HubAssetId::get(), - asset_id, - EmaPrice::new(asset_state.hub_reserve, asset_state.reserve), - ) - .map_err(|_| Error::::PriceDifferenceTooHigh)?; - } - let ext_asset_price = T::ExternalPriceOracle::get_price(T::HubAssetId::get(), asset_id)?; - - if ext_asset_price.is_zero() { - return Err(Error::::InvalidOraclePrice.into()); - } - let withdrawal_fee = hydra_dx_math::omnipool::calculate_withdrawal_fee( - asset_state.price().ok_or(ArithmeticError::DivisionByZero)?, - FixedU128::checked_from_rational(ext_asset_price.n, ext_asset_price.d) - .defensive_ok_or(Error::::InvalidOraclePrice)?, - T::MinWithdrawalFee::get(), - ); - - let state_changes = hydra_dx_math::omnipool::calculate_remove_liquidity_state_changes( - &(&asset_state).into(), - amount, - &(&position).into(), - withdrawal_fee, - ) - .ok_or(ArithmeticError::Overflow)?; - - ensure!( - *state_changes.asset.delta_reserve >= min_limit, - Error::::SlippageLimit - ); - - let new_asset_state = asset_state - .delta_update(&state_changes.asset) - .ok_or(ArithmeticError::Overflow)?; - - // Update position state - let updated_position = position - .delta_update( - &state_changes.delta_position_reserve, - &state_changes.delta_position_shares, - ) - .ok_or(ArithmeticError::Overflow)?; - - T::Currency::transfer( - asset_id, - &Self::protocol_account(), - &who, - *state_changes.asset.delta_reserve, - )?; - - // burn only difference between delta hub and lp hub amount. - Self::update_hub_asset_liquidity( - &state_changes - .asset - .total_delta_hub_reserve() - .merge(BalanceUpdate::Increase(state_changes.lp_hub_amount)) - .ok_or(ArithmeticError::Overflow)?, - )?; - - // LP receives some hub asset - Self::process_hub_amount(state_changes.lp_hub_amount, &who)?; - - if updated_position.shares == Balance::zero() { - // All liquidity removed, remove position and burn NFT instance - - >::remove(position_id); - T::NFTHandler::burn(&T::NFTCollectionId::get(), &position_id, Some(&who))?; - - Self::deposit_event(Event::PositionDestroyed { - position_id, - owner: who.clone(), - }); - } else { - Self::deposit_event(Event::PositionUpdated { - position_id, - owner: who.clone(), - asset: asset_id, - amount: updated_position.amount, - shares: updated_position.shares, - price: updated_position - .price_from_rational() - .ok_or(ArithmeticError::DivisionByZero)?, - }); - - >::insert(position_id, updated_position); - } - - // Callback hook info - let info: AssetInfo = AssetInfo::new( - asset_id, - &asset_state, - &new_asset_state, - &state_changes.asset, - safe_withdrawal, - ); - - Self::set_asset_state(asset_id, new_asset_state); - - Self::deposit_event(Event::LiquidityRemoved { - who, - position_id, - asset_id, - shares_removed: amount, - fee: withdrawal_fee, - }); - - T::OmnipoolHooks::on_liquidity_changed(origin, info)?; - - #[cfg(any(feature = "try-runtime", test))] - Self::ensure_liquidity_invariant((asset_id, asset_state, new_asset_state)); - + Self::do_remove_liquidity_with_limit(origin, position_id, amount, min_limit)?; Ok(()) } @@ -1673,6 +1532,8 @@ pub mod pallet { } } +use crate::traits::ExternalPriceProvider; +use frame_support::traits::DefensiveOption; impl Pallet { /// Protocol account address pub fn protocol_account() -> T::AccountId { @@ -2258,6 +2119,158 @@ impl Pallet { Ok(instance_id) } + /// Internal method to remove liquidity with limit. + /// Returns the actual amount of asset transferred to the user. + /// + /// This is the core logic for liquidity removal, extracted to allow + /// other pallets to call it and receive the transferred amount. + #[require_transactional] + pub fn do_remove_liquidity_with_limit( + origin: OriginFor, + position_id: T::PositionItemId, + amount: Balance, + min_limit: Balance, + ) -> Result { + let who = ensure_signed(origin.clone())?; + + ensure!(amount > Balance::zero(), Error::::InvalidSharesAmount); + + ensure!( + T::NFTHandler::owner(&T::NFTCollectionId::get(), &position_id) == Some(who.clone()), + Error::::Forbidden + ); + + let position = Positions::::get(position_id).ok_or(Error::::PositionNotFound)?; + + ensure!(position.shares >= amount, Error::::InsufficientShares); + + let asset_id = position.asset_id; + + let asset_state = Self::load_asset_state(asset_id)?; + + ensure!( + asset_state.tradable.contains(Tradability::REMOVE_LIQUIDITY), + Error::::NotAllowed + ); + + // We need to call this to ensure that the fee is calculated correctly + // Although we dont need, but we need the fee to update. + let _ = T::Fee::get_and_store((asset_id, asset_state.reserve)); + + let safe_withdrawal = asset_state.tradable.is_safe_withdrawal(); + // Skip price check if safe withdrawal - trading disabled. + if !safe_withdrawal { + T::PriceBarrier::ensure_price( + &who, + T::HubAssetId::get(), + asset_id, + EmaPrice::new(asset_state.hub_reserve, asset_state.reserve), + ) + .map_err(|_| Error::::PriceDifferenceTooHigh)?; + } + let ext_asset_price = T::ExternalPriceOracle::get_price(T::HubAssetId::get(), asset_id)?; + + if ext_asset_price.is_zero() { + return Err(Error::::InvalidOraclePrice.into()); + } + let withdrawal_fee = hydra_dx_math::omnipool::calculate_withdrawal_fee( + asset_state.price().ok_or(ArithmeticError::DivisionByZero)?, + FixedU128::checked_from_rational(ext_asset_price.n, ext_asset_price.d) + .defensive_ok_or(Error::::InvalidOraclePrice)?, + T::MinWithdrawalFee::get(), + ); + + let state_changes = hydra_dx_math::omnipool::calculate_remove_liquidity_state_changes( + &(&asset_state).into(), + amount, + &(&position).into(), + withdrawal_fee, + ) + .ok_or(ArithmeticError::Overflow)?; + + ensure!( + *state_changes.asset.delta_reserve >= min_limit, + Error::::SlippageLimit + ); + + let new_asset_state = asset_state + .delta_update(&state_changes.asset) + .ok_or(ArithmeticError::Overflow)?; + + // Update position state + let updated_position = position + .delta_update( + &state_changes.delta_position_reserve, + &state_changes.delta_position_shares, + ) + .ok_or(ArithmeticError::Overflow)?; + + T::Currency::transfer(asset_id, &Self::protocol_account(), &who, *state_changes.asset.delta_reserve)?; + + // burn only difference between delta hub and lp hub amount. + Self::update_hub_asset_liquidity( + &state_changes + .asset + .total_delta_hub_reserve() + .merge(BalanceUpdate::Increase(state_changes.lp_hub_amount)) + .ok_or(ArithmeticError::Overflow)?, + )?; + + // LP receives some hub asset + Self::process_hub_amount(state_changes.lp_hub_amount, &who)?; + + if updated_position.shares == Balance::zero() { + // All liquidity removed, remove position and burn NFT instance + + >::remove(position_id); + T::NFTHandler::burn(&T::NFTCollectionId::get(), &position_id, Some(&who))?; + + Self::deposit_event(Event::PositionDestroyed { + position_id, + owner: who.clone(), + }); + } else { + Self::deposit_event(Event::PositionUpdated { + position_id, + owner: who.clone(), + asset: asset_id, + amount: updated_position.amount, + shares: updated_position.shares, + price: updated_position + .price_from_rational() + .ok_or(ArithmeticError::DivisionByZero)?, + }); + + >::insert(position_id, updated_position); + } + + // Callback hook info + let info: AssetInfo = AssetInfo::new( + asset_id, + &asset_state, + &new_asset_state, + &state_changes.asset, + safe_withdrawal, + ); + + Self::set_asset_state(asset_id, new_asset_state); + + Self::deposit_event(Event::LiquidityRemoved { + who, + position_id, + asset_id, + shares_removed: amount, + fee: withdrawal_fee, + }); + + T::OmnipoolHooks::on_liquidity_changed(origin, info)?; + + #[cfg(any(feature = "try-runtime", test))] + Self::ensure_liquidity_invariant((asset_id, asset_state, new_asset_state)); + + Ok(*state_changes.asset.delta_reserve) + } + #[cfg(any(feature = "try-runtime", test))] fn ensure_trade_invariant( asset_in: (T::AssetId, AssetReserveState, AssetReserveState), diff --git a/pallets/stableswap/src/lib.rs b/pallets/stableswap/src/lib.rs index 3c827fa151..51579ca447 100644 --- a/pallets/stableswap/src/lib.rs +++ b/pallets/stableswap/src/lib.rs @@ -596,80 +596,13 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - ensure!( - Self::is_asset_allowed(pool_id, asset_id, Tradability::REMOVE_LIQUIDITY), - Error::::NotAllowed - ); - ensure!(share_amount > Balance::zero(), Error::::InvalidAssetAmount); - - let current_share_balance = T::Currency::free_balance(pool_id, &who); - ensure!(current_share_balance >= share_amount, Error::::InsufficientShares); - - // Retrive pool state. - let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; - let asset_idx = pool.find_asset(asset_id).ok_or(Error::::AssetNotInPool)?; - let pool_account = Self::pool_account(pool_id); - let initial_reserves = pool - .reserves_with_decimals::(&pool_account) - .ok_or(Error::::UnknownDecimals)?; - let share_issuance = T::Currency::total_issuance(pool_id); - - ensure!( - share_issuance == share_amount - || share_issuance.saturating_sub(share_amount) >= T::MinPoolLiquidity::get(), - Error::::InsufficientLiquidityRemaining - ); - - let amplification = Self::get_amplification(&pool); - let (trade_fee, asset_pegs) = Self::update_and_return_pegs_and_trade_fee(pool_id, &pool)?; - - Self::save_snapshot(pool_id); - - //Calculate how much asset user will receive. Note that the fee is already subtracted from the amount. - let (amount, fee) = hydra_dx_math::stableswap::calculate_withdraw_one_asset::( - &initial_reserves, - share_amount, - asset_idx, - share_issuance, - amplification, - trade_fee, - &asset_pegs, - ) - .ok_or(ArithmeticError::Overflow)?; - - ensure!(amount >= min_amount_out, Error::::SlippageLimit); - - // Burn shares and transfer asset to user. - T::Currency::withdraw(pool_id, &who, share_amount)?; - T::Currency::transfer(asset_id, &pool_account, &who, amount)?; - - // All done and updated. let's call the on_liquidity_changed hook. - Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; - - Self::deposit_event(Event::LiquidityRemoved { + let _ = Self::do_remove_liquidity_one_asset( + &who, pool_id, - who: who.clone(), - shares: share_amount, - amounts: vec![AssetAmount { asset_id, amount }], - fee, - }); - - pallet_broadcast::Pallet::::deposit_trade_event( - who, - pool_account.clone(), - pallet_broadcast::types::Filler::Stableswap(pool_id.into()), - pallet_broadcast::types::TradeOperation::LiquidityRemove, - vec![Asset::new(pool_id.into(), share_amount)], - vec![Asset::new(asset_id.into(), amount)], - vec![Fee { - asset: pool_id.into(), - amount: fee, - destination: Destination::Account(pool_account), - }], - ); - - #[cfg(any(feature = "try-runtime", test))] - Self::ensure_remove_liquidity_invariant(pool_id, &initial_reserves, share_issuance); + asset_id, + share_amount, + min_amount_out, + )?; Ok(()) } @@ -1757,6 +1690,185 @@ impl Pallet { Ok(amount_in) } + #[require_transactional] + fn do_remove_liquidity_one_asset( + who: &T::AccountId, + pool_id: T::AssetId, + asset_id: T::AssetId, + share_amount: Balance, + min_amount_out: Balance, + ) -> Result { + ensure!( + Self::is_asset_allowed(pool_id, asset_id, Tradability::REMOVE_LIQUIDITY), + Error::::NotAllowed + ); + ensure!(share_amount > Balance::zero(), Error::::InvalidAssetAmount); + + let current_share_balance = T::Currency::free_balance(pool_id, who); + ensure!(current_share_balance >= share_amount, Error::::InsufficientShares); + + // Retrieve pool state. + let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; + let asset_idx = pool.find_asset(asset_id).ok_or(Error::::AssetNotInPool)?; + let pool_account = Self::pool_account(pool_id); + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; + let share_issuance = T::Currency::total_issuance(pool_id); + + ensure!( + share_issuance == share_amount || share_issuance.saturating_sub(share_amount) >= T::MinPoolLiquidity::get(), + Error::::InsufficientLiquidityRemaining + ); + + let amplification = Self::get_amplification(&pool); + let (trade_fee, asset_pegs) = Self::update_and_return_pegs_and_trade_fee(pool_id, &pool)?; + + Self::save_snapshot(pool_id); + + // Calculate how much asset user will receive. Note that the fee is already subtracted from the amount. + let (amount, fee) = hydra_dx_math::stableswap::calculate_withdraw_one_asset::( + &initial_reserves, + share_amount, + asset_idx, + share_issuance, + amplification, + trade_fee, + &asset_pegs, + ) + .ok_or(ArithmeticError::Overflow)?; + + ensure!(amount >= min_amount_out, Error::::SlippageLimit); + + // Burn shares and transfer asset to user. + T::Currency::withdraw(pool_id, who, share_amount)?; + T::Currency::transfer(asset_id, &pool_account, who, amount)?; + + // All done and updated. let's call the on_liquidity_changed hook. + Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; + + Self::deposit_event(Event::LiquidityRemoved { + pool_id, + who: who.clone(), + shares: share_amount, + amounts: vec![AssetAmount { asset_id, amount }], + fee, + }); + + pallet_broadcast::Pallet::::deposit_trade_event( + who.clone(), + pool_account.clone(), + pallet_broadcast::types::Filler::Stableswap(pool_id.into()), + pallet_broadcast::types::TradeOperation::LiquidityRemove, + vec![Asset::new(pool_id.into(), share_amount)], + vec![Asset::new(asset_id.into(), amount)], + vec![Fee { + asset: pool_id.into(), + amount: fee, + destination: Destination::Account(pool_account), + }], + ); + + #[cfg(any(feature = "try-runtime", test))] + Self::ensure_remove_liquidity_invariant(pool_id, &initial_reserves, share_issuance); + + Ok(amount) + } + + #[require_transactional] + fn do_remove_liquidity( + who: &T::AccountId, + pool_id: T::AssetId, + share_amount: Balance, + min_amounts_out: &[AssetAmount], + ) -> Result<(), DispatchError> { + ensure!(share_amount > Balance::zero(), Error::::InvalidAssetAmount); + + let current_share_balance = T::Currency::free_balance(pool_id, who); + ensure!(current_share_balance >= share_amount, Error::::InsufficientShares); + + let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; + let pool_account = Self::pool_account(pool_id); + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; + let share_issuance = T::Currency::total_issuance(pool_id); + + // We want to ensure that given min amounts are correct. It must contain all pool assets. + // We convert vec of min amounts to a map. + // We first ensure the length, and if any asset is not found later on, we can return an error. + ensure!(min_amounts_out.len() == pool.assets.len(), Error::::IncorrectAssets); + let mut min_amounts_out_map = BTreeMap::new(); + for v in min_amounts_out.iter() { + let r = min_amounts_out_map.insert(v.asset_id, v.amount); + ensure!(r.is_none(), Error::::IncorrectAssets); + } + + Self::save_snapshot(pool_id); + + // Store the amount of each asset that is transferred. Used as info in the event. + let mut amounts = Vec::with_capacity(pool.assets.len()); + + // 1. Calculate amount of each asset + // 2. ensure min amount is respected + // 3. transfer amount to user + for asset_id in pool.assets.iter() { + ensure!( + Self::is_asset_allowed(pool_id, *asset_id, Tradability::REMOVE_LIQUIDITY), + Error::::NotAllowed + ); + let min_amount = min_amounts_out_map + .remove(asset_id) + .ok_or(Error::::IncorrectAssets)?; + let reserve = T::Currency::free_balance(*asset_id, &pool_account); + + // Special case when withdrawing all remaining pool shares, so we can directly send all the remaining assets to the user. + let amount = if share_amount == share_issuance { + ensure!(reserve >= min_amount, Error::::SlippageLimit); + reserve + } else { + let amount = hydra_dx_math::stableswap::calculate_liquidity_out(reserve, share_amount, share_issuance) + .ok_or(ArithmeticError::Overflow)?; + ensure!(amount >= min_amount, Error::::SlippageLimit); + amount + }; + + T::Currency::transfer(*asset_id, &pool_account, who, amount)?; + amounts.push(AssetAmount { + asset_id: *asset_id, + amount, + }); + } + + // Burn shares + T::Currency::withdraw(pool_id, who, share_amount)?; + + // All done and updated. let's call the on_liquidity_changed hook. + if share_amount != share_issuance { + Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; + } else { + // Remove the pool. + Pools::::remove(pool_id); + PoolPegs::::remove(pool_id); + let _ = AssetTradability::::clear_prefix(pool_id, MAX_ASSETS_IN_POOL, None); + T::DustAccountHandler::remove_account(&Self::pool_account(pool_id))?; + Self::deposit_event(Event::PoolDestroyed { pool_id }); + } + + Self::deposit_event(Event::LiquidityRemoved { + pool_id, + who: who.clone(), + shares: share_amount, + amounts, + fee: Balance::zero(), + }); + + #[cfg(any(feature = "try-runtime", test))] + Self::ensure_remove_liquidity_invariant(pool_id, &initial_reserves, share_issuance); + + Ok(()) + } + #[inline] fn is_asset_allowed(pool_id: T::AssetId, asset_id: T::AssetId, operation: Tradability) -> bool { AssetTradability::::get(pool_id, asset_id).contains(operation) @@ -1964,6 +2076,25 @@ impl StableswapAddLiquidity for Pa ) -> Result { Self::do_add_liquidity(&who, pool_id, &assets, Balance::zero()) } + + fn remove_liquidity_one_asset( + who: T::AccountId, + pool_id: T::AssetId, + asset_id: T::AssetId, + share_amount: Balance, + min_amount_out: Balance, + ) -> Result { + Self::do_remove_liquidity_one_asset(&who, pool_id, asset_id, share_amount, min_amount_out) + } + + fn remove_liquidity( + who: T::AccountId, + pool_id: T::AssetId, + share_amount: Balance, + min_amounts_out: Vec>, + ) -> Result<(), DispatchError> { + Self::do_remove_liquidity(&who, pool_id, share_amount, &min_amounts_out) + } } // Peg support diff --git a/pallets/xyk-liquidity-mining/src/tests/mock.rs b/pallets/xyk-liquidity-mining/src/tests/mock.rs index 96bf9a6939..ad014c2a5b 100644 --- a/pallets/xyk-liquidity-mining/src/tests/mock.rs +++ b/pallets/xyk-liquidity-mining/src/tests/mock.rs @@ -841,6 +841,16 @@ impl hydradx_traits::liquidity_mining::Mutate f Ok(()) }) } + + fn get_yield_farm_ids(deposit_id: DepositId) -> Option> { + DEPOSITS.with(|v| { + let m = v.borrow(); + m.get(&deposit_id).map(|_deposit| { + // Return an empty vector for now - this is a dummy implementation + Vec::new() + }) + }) + } } impl hydradx_traits::liquidity_mining::Inspect for DummyLiquidityMining { diff --git a/traits/src/liquidity_mining.rs b/traits/src/liquidity_mining.rs index c22e680ad8..1144802fc7 100644 --- a/traits/src/liquidity_mining.rs +++ b/traits/src/liquidity_mining.rs @@ -171,6 +171,9 @@ pub trait Mutate { /// Returns `Some(global_farm_id)` for given `deposit_id` and `yield_farm_id` or `None`. fn get_global_farm_id(deposit_id: DepositId, yield_farm_id: YieldFarmId) -> Option; + + /// Returns `Some(Vec)` for given `deposit_id` or `None` if no existing deposit. + fn get_yield_farm_ids(deposit_id: DepositId) -> Option>; } /// Implementers of this trait provide `price_adjustment` for given `GlobalFarm`. diff --git a/traits/src/stableswap.rs b/traits/src/stableswap.rs index a20a761596..a7d52d6f1c 100644 --- a/traits/src/stableswap.rs +++ b/traits/src/stableswap.rs @@ -9,6 +9,21 @@ pub trait StableswapAddLiquidity { pool_id: AssetId, assets_amounts: Vec>, ) -> Result; + + fn remove_liquidity_one_asset( + who: AccountId, + pool_id: AssetId, + asset_id: AssetId, + share_amount: Balance, + min_amount_out: Balance, + ) -> Result; + + fn remove_liquidity( + who: AccountId, + pool_id: AssetId, + share_amount: Balance, + min_amounts_out: Vec>, + ) -> Result<(), DispatchError>; } #[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, TypeInfo)] From 3398651067cfaf83bca4c5ca6da867ca70443d3d Mon Sep 17 00:00:00 2001 From: dmoka Date: Fri, 19 Dec 2025 09:32:54 +0100 Subject: [PATCH 03/29] add exiting farms optional --- .../src/omnipool_liquidity_mining.rs | 169 ++++++++++- pallets/omnipool-liquidity-mining/src/lib.rs | 71 +++-- ...dity_stableswap_omnipool_and_exit_farms.rs | 278 +++++++++++++++++- 3 files changed, 475 insertions(+), 43 deletions(-) diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index 57944f0fc4..587f6e1b32 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -1388,9 +1388,10 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { assert_ok!( hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), - deposit_id, + position_id, stable_pool_id, - asset_ids_without_slippage + asset_ids_without_slippage, + Some(deposit_id) ) ); @@ -1479,6 +1480,153 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { }); } + #[test] + fn remove_liquidity_without_farm_exit_should_work() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //NOTE: necessary to get oracle price. + hydradx_run_to_block(100); + set_relaychain_block_number(100); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidiity to make sure that it does not interfere with the new liquidty add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + let position_id = hydradx_runtime::Omnipool::next_position_id(); + + set_relaychain_block_number(400); + + //Add liquidity first + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + None, + None, + ) + ); + + // Wait some blocks + set_relaychain_block_number(500); + + let asset_ids_without_slippage: Vec> = Stableswap::pools(stable_pool_id) + .into_iter() + .flat_map(|pool_info| pool_info.assets.into_iter()) + .map(|asset_id| AssetAmount::::new(asset_id.into(), 10000)) + .collect(); + + let asset_ids_without_slippage = create_bounded_vec(asset_ids_without_slippage); + + //Act - Remove liquidity and exit all farms + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(CHARLIE.into()), + position_id, + stable_pool_id, + asset_ids_without_slippage, + None + ) + ); + + //Assert + // Verify NFT was destroyed (all liquidity removed) + assert!( + hydradx_runtime::Uniques::owner( + hydradx_runtime::OmnipoolCollectionId::get(), + position_id + ) + .is_none(), + "NFT should be destroyed after removing all liquidity" + ); + + //Verify SharesWithdrawn events for all 3 farms + expect_shares_withdrawn_ln_events(vec![]); + + // Verify LiquidityRemoved events + expect_omnipool_liquidity_removed_events(vec![pallet_omnipool::Event::LiquidityRemoved { + who: CHARLIE.into(), + position_id, + asset_id: stable_pool_id, + shares_removed: 20044549999405, + fee: FixedU128::from_float(0.000100000000000000), + } + .into()]); + + expect_stableswap_liquidity_removed_events(vec![ + pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 20042545544405, + amounts: vec![ + AssetAmount::new(1000002, 3984601523849), + AssetAmount::new(1000003, 3984601484003), + AssetAmount::new(1000004, 3984601484003), + AssetAmount::new(1000005, 3984601484003), + AssetAmount::new(1000006, 3984601523849), + ], + fee: 0, + } + .into(), + ]); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + #[test] fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_asset_withdrawal() { TestNet::reset(); @@ -1580,9 +1728,10 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { assert_ok!( hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), - deposit_id, + position_id, stable_pool_id, vec![AssetAmount::new(stable_asset_1, 0)].try_into().unwrap(), + Some(deposit_id) ) ); @@ -1748,11 +1897,13 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { assert_ok!( hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), - deposit_id, + position_id, stable_pool_id, vec![AssetAmount::new(stable_asset_1, 0)] .try_into() .unwrap(), + Some(deposit_id) + ) ); @@ -1869,6 +2020,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { 100 * UNITS, )); + let position_id = hydradx_runtime::Omnipool::next_position_id(); + set_relaychain_block_number(400); let deposit_id = 1; let farms = vec![(global_farm_id, yield_farm_id)]; @@ -1907,11 +2060,12 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { assert_noop!( hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(DAVE.into()), - deposit_id, + position_id, stable_pool_id, vec![AssetAmount::new(stable_asset_1, 0), AssetAmount::new(stable_asset_2, 0)] .try_into() .unwrap(), + Some(deposit_id) ), pallet_omnipool_liquidity_mining::Error::::Forbidden ); @@ -1978,6 +2132,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { 100 * UNITS, )); + let position_id = hydradx_runtime::Omnipool::next_position_id(); + set_relaychain_block_number(400); let deposit_id = 1; let farms = vec![(global_farm_id, yield_farm_id)]; @@ -2016,9 +2172,10 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { assert_noop!( hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), - deposit_id, + position_id, stable_pool_id, vec![].try_into().unwrap(), + Some(deposit_id) ), pallet_omnipool_liquidity_mining::Error::::NoAssetsSpecified ); diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index b87af99413..309f26cb0e 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1084,11 +1084,13 @@ pub mod pallet { Ok(()) } - /// Remove liquidity from stableswap and omnipool, exiting all associated yield farms. + /// Remove liquidity from stableswap and omnipool, optionally exiting associated yield farms. + /// + /// This extrinsic reverses the operation performed by `add_liquidity_stableswap_omnipool_and_join_farms`, + /// with optional farm exit to match the optional farm join in the add function. /// - /// This extrinsic reverses the operation performed by `add_liquidity_stableswap_omnipool_and_join_farms`. /// It performs the following steps in order: - /// 1. Exits from ALL yield farms associated with the deposit (claiming rewards) + /// 1. [OPTIONAL] If deposit_id is provided: Exits from ALL yield farms associated with the deposit (claiming rewards) /// 2. Removes liquidity from the omnipool to retrieve stableswap shares /// 3. Removes liquidity from the stableswap pool to retrieve underlying assets /// @@ -1097,16 +1099,20 @@ pub mod pallet { /// - If multiple assets: Uses `remove_liquidity` (proportional, no trading fee) /// /// Parameters: - /// - `origin`: Owner of the deposit NFT - /// - `deposit_id`: The liquidity mining deposit NFT ID to unwind + /// - `origin`: Owner of the omnipool position + /// - `position_id`: The omnipool position NFT ID to remove liquidity from + /// - `stable_pool_id`: The stableswap pool ID containing the liquidity /// - `min_amounts_out`: Asset IDs and minimum amounts for slippage protection + /// - `deposit_id`: Optional liquidity mining deposit NFT ID. If provided, exits all farms first. + /// + /// Emits events: + /// - If deposit_id provided: `RewardClaimed`, `SharesWithdrawn`, `DepositDestroyed` + /// - Always: Omnipool's `LiquidityRemoved`, Stableswap's `LiquidityRemoved` /// - /// Emits multiple events: - /// - `RewardClaimed` for each farm (if rewards > 0) - /// - `SharesWithdrawn` for each farm - /// - `DepositDestroyed` when deposit is fully exited - /// - Omnipool's `LiquidityRemoved` - /// - Stableswap's `LiquidityRemoved` + /// Errors: + /// - `NoAssetsSpecified` if min_amounts_out is empty + /// - `Forbidden` if caller doesn't own the deposit NFT (when deposit_id provided) + /// - `InconsistentState(MissingLpPosition)` if deposit-position mismatch #[pallet::call_index(17)] #[pallet::weight(Weight::default())] //TODO: BENCHMARK AND ADD WEIGHT @@ -1127,30 +1133,39 @@ pub mod pallet { // })] pub fn remove_liquidity_stableswap_omnipool_and_exit_farms( origin: OriginFor, - deposit_id: DepositId, + position_id: T::PositionItemId, stable_pool_id: T::AssetId, min_amounts_out: BoundedVec, ConstU32>, + deposit_id: Option, ) -> DispatchResult { - //TODO: ensure deposit owner is the same as origin let who = ensure_signed(origin.clone())?; ensure!(!min_amounts_out.is_empty(), Error::::NoAssetsSpecified); - // Collect all yield farm IDs from deposit - let yield_farm_ids: BoundedVec = - T::LiquidityMiningHandler::get_yield_farm_ids(deposit_id) + if let Some(deposit_id) = deposit_id { + Self::ensure_nft_owner(origin.clone(), deposit_id)?; + + let stored_position_id = OmniPositionId::::get(deposit_id) .ok_or(Error::::InconsistentState( - InconsistentStateError::DepositDataNotFound, - ))? - .try_into() - .map_err(|_| { - Error::::InconsistentState(InconsistentStateError::DepositDataNotFound) - })?; - - // CRITICAL: Get position_id BEFORE exit_farms clears the storage mapping - let position_id = OmniPositionId::::get(deposit_id) - .ok_or(Error::::InconsistentState(InconsistentStateError::MissingLpPosition))?; + InconsistentStateError::MissingLpPosition + ))?; + ensure!( + stored_position_id == position_id, + Error::::InconsistentState(InconsistentStateError::MissingLpPosition) + ); + + let yield_farm_ids: BoundedVec = + T::LiquidityMiningHandler::get_yield_farm_ids(deposit_id) + .ok_or(Error::::InconsistentState( + InconsistentStateError::DepositDataNotFound, + ))? + .try_into() + .map_err(|_| { + Error::::InconsistentState(InconsistentStateError::DepositDataNotFound) + })?; + + Self::exit_farms(origin.clone(), deposit_id, yield_farm_ids)?; + } - Self::exit_farms(origin.clone(), deposit_id, yield_farm_ids)?; let omnipool_position = OmnipoolPallet::::load_position(position_id, who.clone())?; let omnipool_shares_to_remove = omnipool_position.shares; @@ -1159,7 +1174,7 @@ pub mod pallet { origin.clone(), position_id, omnipool_shares_to_remove, - Balance::MIN, // No slippage limit for omnipool removal + Balance::MIN, )?; if min_amounts_out.len() == 1 { diff --git a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs index 5ef5c10d3d..3ef46f1f19 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs @@ -63,12 +63,16 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_a None, )); + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + // Remove liquidity to single asset assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), - deposit_id, + position_id, STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_id), )); // Verify omnipool state returned to initial (all liquidity removed) @@ -148,14 +152,18 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multi_as None, )); + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + // Remove liquidity proportionally to multiple assets assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), - deposit_id, + position_id, STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1), AssetAmount::new(USDC, 1)] .try_into() .unwrap(), + Some(deposit_id), )); // Verify omnipool state returned to initial (all liquidity removed) @@ -257,12 +265,15 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multiple None, )); - // Remove liquidity - should exit all 3 farms automatically + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), - deposit_id, + position_id, STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_id), )); // Verify omnipool state returned to initial (all liquidity removed) @@ -365,13 +376,16 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_when_not_owne None, )); - // LP2 tries to remove LP1's liquidity - should fail + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + assert_noop!( OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP2), - deposit_id, + position_id, STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_id), ), crate::Error::::Forbidden ); @@ -423,13 +437,16 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_with_empty_as None, )); - // Try to remove with empty assets list - should fail + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + assert_noop!( OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), - deposit_id, + position_id, STABLESWAP_POOL_ID, vec![].try_into().unwrap(), + Some(deposit_id), ), Error::::NoAssetsSpecified ); @@ -481,6 +498,9 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa None, )); + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + // Wait some blocks to accumulate rewards set_block_number(100); @@ -489,9 +509,10 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa // Remove liquidity assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), - deposit_id, + position_id, STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_id), )); // Verify omnipool state returned to initial (all liquidity removed) @@ -534,3 +555,242 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa ); }); } + +#[test] +fn remove_liquidity_without_farm_exit_should_work() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .build() + .execute_with(|| { + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + None, // No farms + None, + )); + + let position_id = 4; + + assert!(pallet_omnipool::Pallet::::load_position(position_id, LP1).is_ok()); + + assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + position_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + None, // No deposit_id since we never joined farms + )); + + assert_asset_state!( + STABLESWAP_POOL_ID, + AssetReserveState { + reserve: token_amount, + hub_reserve: 1_300_000_000_000_000, + shares: token_amount, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + // Verify NO farm events were emitted + assert!(!has_event( + crate::Event::DepositDestroyed { who: LP1, deposit_id: 1 }.into() + )); + }); +} + +#[test] +fn should_fail_with_mismatched_deposit_and_position() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // Create first position with farms (deposit_id = 1) + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.clone().try_into().unwrap()), + None, + )); + + let deposit_1 = 1; + let position_1 = crate::OmniPositionId::::get(deposit_1) + .expect("Position 1 should exist"); + + // Create second position with farms (deposit_id = 2) + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + let deposit_2 = 2; + let position_2 = crate::OmniPositionId::::get(deposit_2) + .expect("Position 2 should exist"); + + assert_ne!(position_1, position_2); + + assert_noop!( + OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + position_1, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_2), // WRONG deposit for position_1 + ), + Error::::InconsistentState(InconsistentStateError::MissingLpPosition) + ); + }); +} + +#[test] +fn should_fail_with_nonexistent_deposit() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .build() + .execute_with(|| { + // Add liquidity WITHOUT farms + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + None, + None, + )); + + let position_id = 0; + + assert_noop!( + OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + position_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(999), // Non-existent deposit_id + ), + crate::Error::::Forbidden + ); + }); +} + +#[test] +fn should_fail_when_deposit_owner_differs() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (LP2, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + // LP1 creates position with farms + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + let position_id = crate::OmniPositionId::::get(deposit_id) + .expect("Position should be mapped to deposit"); + + assert_noop!( + OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP2), + position_id, + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_id), + ), + crate::Error::::Forbidden + ); + }); +} From 03ea97a4fbb17aed1b47e4fe5739638484c8a81e Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 12:28:09 +0100 Subject: [PATCH 04/29] add benhcmarking --- .../src/omnipool_liquidity_mining.rs | 210 +++++++--------- pallets/omnipool-liquidity-mining/src/lib.rs | 34 +-- ...dity_stableswap_omnipool_and_exit_farms.rs | 58 ++--- .../omnipool-liquidity-mining/src/weights.rs | 73 ++++++ .../benchmarking/omnipool_liquidity_mining.rs | 165 +++++++++++++ .../pallet_omnipool_liquidity_mining.rs | 227 ++++++++++++------ 6 files changed, 508 insertions(+), 259 deletions(-) diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index 587f6e1b32..67d6ed334b 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -1389,7 +1389,6 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, - stable_pool_id, asset_ids_without_slippage, Some(deposit_id) ) @@ -1401,11 +1400,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { // Verify NFT was destroyed (all liquidity removed) assert!( - hydradx_runtime::Uniques::owner( - hydradx_runtime::OmnipoolCollectionId::get(), - position_id - ) - .is_none(), + hydradx_runtime::Uniques::owner(hydradx_runtime::OmnipoolCollectionId::get(), position_id) + .is_none(), "NFT should be destroyed after removing all liquidity" ); @@ -1420,33 +1416,27 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { //Verify SharesWithdrawn events for all 3 farms expect_shares_withdrawn_ln_events(vec![ - RuntimeEvent::OmnipoolLiquidityMining( - pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { - global_farm_id: 1, - yield_farm_id: 4, - deposit_id: 1, - amount: 20044549999405, - who: CHARLIE.into(), - }, - ), - RuntimeEvent::OmnipoolLiquidityMining( - pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { - global_farm_id: 2, - yield_farm_id: 5, - deposit_id: 1, - amount: 20044549999405, - who: CHARLIE.into(), - }, - ), - RuntimeEvent::OmnipoolLiquidityMining( - pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { - global_farm_id: 3, - yield_farm_id: 6, - deposit_id: 1, - amount: 20044549999405, - who: CHARLIE.into(), - }, - ), + RuntimeEvent::OmnipoolLiquidityMining(pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id: 1, + yield_farm_id: 4, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + }), + RuntimeEvent::OmnipoolLiquidityMining(pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id: 2, + yield_farm_id: 5, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + }), + RuntimeEvent::OmnipoolLiquidityMining(pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id: 3, + yield_farm_id: 6, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + }), ]); // Verify LiquidityRemoved events @@ -1459,22 +1449,20 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { } .into()]); - expect_stableswap_liquidity_removed_events(vec![ - pallet_stableswap::Event::LiquidityRemoved { - pool_id: stable_pool_id, - who: CHARLIE.into(), - shares: 20042545544405, - amounts: vec![ - AssetAmount::new(1000002, 3984601523849), - AssetAmount::new(1000003, 3984601484003), - AssetAmount::new(1000004, 3984601484003), - AssetAmount::new(1000005, 3984601484003), - AssetAmount::new(1000006, 3984601523849), - ], - fee: 0, - } - .into(), - ]); + expect_stableswap_liquidity_removed_events(vec![pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 20042545544405, + amounts: vec![ + AssetAmount::new(1000002, 3984601523849), + AssetAmount::new(1000003, 3984601484003), + AssetAmount::new(1000004, 3984601484003), + AssetAmount::new(1000005, 3984601484003), + AssetAmount::new(1000006, 3984601523849), + ], + fee: 0, + } + .into()]); TransactionOutcome::Commit(DispatchResult::Ok(())) }); }); @@ -1576,7 +1564,6 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, - stable_pool_id, asset_ids_without_slippage, None ) @@ -1585,11 +1572,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { //Assert // Verify NFT was destroyed (all liquidity removed) assert!( - hydradx_runtime::Uniques::owner( - hydradx_runtime::OmnipoolCollectionId::get(), - position_id - ) - .is_none(), + hydradx_runtime::Uniques::owner(hydradx_runtime::OmnipoolCollectionId::get(), position_id) + .is_none(), "NFT should be destroyed after removing all liquidity" ); @@ -1606,22 +1590,20 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { } .into()]); - expect_stableswap_liquidity_removed_events(vec![ - pallet_stableswap::Event::LiquidityRemoved { - pool_id: stable_pool_id, - who: CHARLIE.into(), - shares: 20042545544405, - amounts: vec![ - AssetAmount::new(1000002, 3984601523849), - AssetAmount::new(1000003, 3984601484003), - AssetAmount::new(1000004, 3984601484003), - AssetAmount::new(1000005, 3984601484003), - AssetAmount::new(1000006, 3984601523849), - ], - fee: 0, - } - .into(), - ]); + expect_stableswap_liquidity_removed_events(vec![pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 20042545544405, + amounts: vec![ + AssetAmount::new(1000002, 3984601523849), + AssetAmount::new(1000003, 3984601484003), + AssetAmount::new(1000004, 3984601484003), + AssetAmount::new(1000005, 3984601484003), + AssetAmount::new(1000006, 3984601523849), + ], + fee: 0, + } + .into()]); TransactionOutcome::Commit(DispatchResult::Ok(())) }); }); @@ -1729,7 +1711,6 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, - stable_pool_id, vec![AssetAmount::new(stable_asset_1, 0)].try_into().unwrap(), Some(deposit_id) ) @@ -1741,11 +1722,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { // Verify NFT was destroyed (all liquidity removed) assert!( - hydradx_runtime::Uniques::owner( - hydradx_runtime::OmnipoolCollectionId::get(), - position_id - ) - .is_none(), + hydradx_runtime::Uniques::owner(hydradx_runtime::OmnipoolCollectionId::get(), position_id) + .is_none(), "NFT should be destroyed after removing all liquidity" ); @@ -1760,16 +1738,14 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { assert_eq!(charlie_asset_2_balance_after, charlie_asset_2_balance_before); // Verify SharesWithdrawn and DepositDestroyed events - expect_shares_withdrawn_ln_events(vec![ - pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { - global_farm_id, - yield_farm_id, - deposit_id: 1, - amount: 20044549999405, - who: CHARLIE.into(), - } - .into() - ]); + expect_shares_withdrawn_ln_events(vec![pallet_omnipool_liquidity_mining::Event::SharesWithdrawn { + global_farm_id, + yield_farm_id, + deposit_id: 1, + amount: 20044549999405, + who: CHARLIE.into(), + } + .into()]); // Verify LiquidityRemoved event from omnipool expect_omnipool_liquidity_removed_events(vec![pallet_omnipool::Event::LiquidityRemoved { @@ -1781,18 +1757,14 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { } .into()]); - expect_stableswap_liquidity_removed_events(vec![ - pallet_stableswap::Event::LiquidityRemoved { - pool_id: stable_pool_id, - who: CHARLIE.into(), - shares: 20042545544405, - amounts: vec![ - AssetAmount::new( stable_asset_1, 19823392461976), - ], - fee: 99615037340, - } - .into(), - ]); + expect_stableswap_liquidity_removed_events(vec![pallet_stableswap::Event::LiquidityRemoved { + pool_id: stable_pool_id, + who: CHARLIE.into(), + shares: 20042545544405, + amounts: vec![AssetAmount::new(stable_asset_1, 19823392461976)], + fee: 99615037340, + } + .into()]); TransactionOutcome::Commit(DispatchResult::Ok(())) }); @@ -1876,11 +1848,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( RuntimeOrigin::signed(CHARLIE.into()), stable_pool_id, - vec![ - AssetAmount::new(stable_asset_1, 10 * UNITS), - ] - .try_into() - .unwrap(), + vec![AssetAmount::new(stable_asset_1, 10 * UNITS),].try_into().unwrap(), Some(farms.try_into().unwrap()), None, ) @@ -1898,12 +1866,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, - stable_pool_id, - vec![AssetAmount::new(stable_asset_1, 0)] - .try_into() - .unwrap(), + vec![AssetAmount::new(stable_asset_1, 0)].try_into().unwrap(), Some(deposit_id) - ) ); @@ -1913,11 +1877,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { // Verify NFT was destroyed (all liquidity removed) assert!( - hydradx_runtime::Uniques::owner( - hydradx_runtime::OmnipoolCollectionId::get(), - position_id - ) - .is_none(), + hydradx_runtime::Uniques::owner(hydradx_runtime::OmnipoolCollectionId::get(), position_id) + .is_none(), "NFT should be destroyed after removing all liquidity" ); @@ -1954,10 +1915,8 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { pool_id: stable_pool_id, who: CHARLIE.into(), shares: 10008699029606, - amounts: vec![ - AssetAmount::new(stable_asset_1, 9899259975202), - ], - fee: 49745024898, + amounts: vec![AssetAmount::new(stable_asset_1, 9899259975202)], + fee: 49745024898, } .into()]); @@ -2061,11 +2020,10 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(DAVE.into()), position_id, - stable_pool_id, vec![AssetAmount::new(stable_asset_1, 0), AssetAmount::new(stable_asset_2, 0)] .try_into() .unwrap(), - Some(deposit_id) + Some(deposit_id) ), pallet_omnipool_liquidity_mining::Error::::Forbidden ); @@ -2173,7 +2131,6 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, - stable_pool_id, vec![].try_into().unwrap(), Some(deposit_id) ), @@ -3412,7 +3369,7 @@ pub fn expect_lm_events(e: Vec) { >::SharesDeposited { .. }) | RuntimeEvent::OmnipoolLiquidityMining(pallet_omnipool_liquidity_mining::Event::< hydradx_runtime::Runtime, - >::SharesRedeposited { .. } ) + >::SharesRedeposited { .. }) ) { reward_claimed_events.push(e); } @@ -3422,17 +3379,16 @@ pub fn expect_lm_events(e: Vec) { } pub fn expect_shares_withdrawn_ln_events(expected: Vec) { - let last_events = - test_utils::last_events::(80); + let last_events = test_utils::last_events::(80); let shares_withdrawn_events: Vec = last_events .into_iter() .filter(|event| { matches!( event, - RuntimeEvent::OmnipoolLiquidityMining( - pallet_omnipool_liquidity_mining::Event::::SharesWithdrawn { .. } - ) + RuntimeEvent::OmnipoolLiquidityMining(pallet_omnipool_liquidity_mining::Event::< + hydradx_runtime::Runtime, + >::SharesWithdrawn { .. }) ) }) .collect(); diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 309f26cb0e..0b6142908b 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -87,6 +87,7 @@ type PeriodOf = BlockNumberFor; pub mod pallet { use super::*; use frame_support::pallet_prelude::*; + use primitives::ItemId; #[pallet::pallet] pub struct Pallet(_); @@ -1114,27 +1115,14 @@ pub mod pallet { /// - `Forbidden` if caller doesn't own the deposit NFT (when deposit_id provided) /// - `InconsistentState(MissingLpPosition)` if deposit-position mismatch #[pallet::call_index(17)] - #[pallet::weight(Weight::default())] - //TODO: BENCHMARK AND ADD WEIGHT - // #[pallet::weight({ - // let deposit_data = T::LiquidityMiningHandler::deposit(*deposit_id); - // let farm_count = match deposit_data { - // Some(data) => data.yield_farm_entries.len() as u32, - // None => 0, - // }; - // ::WeightInfo::exit_farms(farm_count) - // .saturating_add(::WeightInfo::price_adjustment_get().saturating_mul(farm_count as u64)) - // .saturating_add(::WeightInfo::remove_liquidity()) - // .saturating_add(if min_amounts_out.len() == 1 { - // ::WeightInfo::remove_liquidity_one_asset() - // } else { - // ::WeightInfo::remove_liquidity() - // }) - // })] + #[pallet::weight({ + let with_farm = if deposit_id.is_some() { 1 } else { 0 }; + ::WeightInfo::remove_liquidity_stableswap_omnipool_and_exit_farms(with_farm) + .saturating_add(::WeightInfo::price_adjustment_get().saturating_mul(T::MaxFarmEntriesPerDeposit::get() as u64)) + })] pub fn remove_liquidity_stableswap_omnipool_and_exit_farms( origin: OriginFor, position_id: T::PositionItemId, - stable_pool_id: T::AssetId, min_amounts_out: BoundedVec, ConstU32>, deposit_id: Option, ) -> DispatchResult { @@ -1145,9 +1133,7 @@ pub mod pallet { Self::ensure_nft_owner(origin.clone(), deposit_id)?; let stored_position_id = OmniPositionId::::get(deposit_id) - .ok_or(Error::::InconsistentState( - InconsistentStateError::MissingLpPosition - ))?; + .ok_or(Error::::InconsistentState(InconsistentStateError::MissingLpPosition))?; ensure!( stored_position_id == position_id, Error::::InconsistentState(InconsistentStateError::MissingLpPosition) @@ -1159,16 +1145,14 @@ pub mod pallet { InconsistentStateError::DepositDataNotFound, ))? .try_into() - .map_err(|_| { - Error::::InconsistentState(InconsistentStateError::DepositDataNotFound) - })?; + .map_err(|_| Error::::InconsistentState(InconsistentStateError::DepositDataNotFound))?; Self::exit_farms(origin.clone(), deposit_id, yield_farm_ids)?; } - let omnipool_position = OmnipoolPallet::::load_position(position_id, who.clone())?; let omnipool_shares_to_remove = omnipool_position.shares; + let stable_pool_id = omnipool_position.asset_id; let actual_stable_shares_received = OmnipoolPallet::::do_remove_liquidity_with_limit( origin.clone(), diff --git a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs index 3ef46f1f19..b4c311991d 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs @@ -63,14 +63,13 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_a None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); // Remove liquidity to single asset assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), )); @@ -152,14 +151,13 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multi_as None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); // Remove liquidity proportionally to multiple assets assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1), AssetAmount::new(USDC, 1)] .try_into() .unwrap(), @@ -265,13 +263,12 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multiple None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), )); @@ -376,14 +373,13 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_when_not_owne None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); assert_noop!( OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP2), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), ), @@ -437,14 +433,13 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_with_empty_as None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); assert_noop!( OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![].try_into().unwrap(), Some(deposit_id), ), @@ -498,8 +493,8 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); // Wait some blocks to accumulate rewards set_block_number(100); @@ -510,7 +505,6 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), )); @@ -579,7 +573,7 @@ fn remove_liquidity_without_farm_exit_should_work() { RuntimeOrigin::signed(LP1), STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), - None, // No farms + None, // No farms None, )); @@ -590,9 +584,8 @@ fn remove_liquidity_without_farm_exit_should_work() { assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), - None, // No deposit_id since we never joined farms + None, // No deposit_id since we never joined farms )); assert_asset_state!( @@ -609,7 +602,11 @@ fn remove_liquidity_without_farm_exit_should_work() { // Verify NO farm events were emitted assert!(!has_event( - crate::Event::DepositDestroyed { who: LP1, deposit_id: 1 }.into() + crate::Event::DepositDestroyed { + who: LP1, + deposit_id: 1 + } + .into() )); }); } @@ -659,8 +656,7 @@ fn should_fail_with_mismatched_deposit_and_position() { )); let deposit_1 = 1; - let position_1 = crate::OmniPositionId::::get(deposit_1) - .expect("Position 1 should exist"); + let position_1 = crate::OmniPositionId::::get(deposit_1).expect("Position 1 should exist"); // Create second position with farms (deposit_id = 2) assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( @@ -672,8 +668,7 @@ fn should_fail_with_mismatched_deposit_and_position() { )); let deposit_2 = 2; - let position_2 = crate::OmniPositionId::::get(deposit_2) - .expect("Position 2 should exist"); + let position_2 = crate::OmniPositionId::::get(deposit_2).expect("Position 2 should exist"); assert_ne!(position_1, position_2); @@ -681,9 +676,8 @@ fn should_fail_with_mismatched_deposit_and_position() { OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_1, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), - Some(deposit_2), // WRONG deposit for position_1 + Some(deposit_2), // WRONG deposit for position_1 ), Error::::InconsistentState(InconsistentStateError::MissingLpPosition) ); @@ -724,9 +718,8 @@ fn should_fail_with_nonexistent_deposit() { OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), - Some(999), // Non-existent deposit_id + Some(999), // Non-existent deposit_id ), crate::Error::::Forbidden ); @@ -779,14 +772,13 @@ fn should_fail_when_deposit_owner_differs() { None, )); - let position_id = crate::OmniPositionId::::get(deposit_id) - .expect("Position should be mapped to deposit"); + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); assert_noop!( OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP2), position_id, - STABLESWAP_POOL_ID, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), ), diff --git a/pallets/omnipool-liquidity-mining/src/weights.rs b/pallets/omnipool-liquidity-mining/src/weights.rs index 21656463c7..82e004ad06 100644 --- a/pallets/omnipool-liquidity-mining/src/weights.rs +++ b/pallets/omnipool-liquidity-mining/src/weights.rs @@ -26,6 +26,7 @@ pub trait WeightInfo { fn join_farms(c: u32) -> Weight; fn add_liquidity_and_join_farms(c: u32) -> Weight; fn add_liquidity_stableswap_omnipool_and_join_farms(c: u32) -> Weight; + fn remove_liquidity_stableswap_omnipool_and_exit_farms(c: u32) -> Weight; fn price_adjustment_get() -> Weight; fn exit_farms(c: u32) -> Weight; @@ -546,6 +547,78 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(c.into()))) .saturating_add(Weight::from_parts(0, 2680).saturating_mul(c.into())) } + + /// Storage: `Uniques::Asset` (r:2 w:2) + /// Proof: `Uniques::Asset` (`max_values`: None, `max_size`: Some(146), added: 2621, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolLiquidityMining::OmniPositionId` (r:1 w:1) + /// Proof: `OmnipoolLiquidityMining::OmniPositionId` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolWarehouseLM::Deposit` (r:1 w:1) + /// Proof: `OmnipoolWarehouseLM::Deposit` (`max_values`: None, `max_size`: Some(385), added: 2860, mode: `MaxEncodedLen`) + /// Storage: `Omnipool::Positions` (r:1 w:1) + /// Proof: `Omnipool::Positions` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolWarehouseLM::YieldFarm` (r:5 w:5) + /// Proof: `OmnipoolWarehouseLM::YieldFarm` (`max_values`: None, `max_size`: Some(198), added: 2673, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:5 w:5) + /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) + /// Storage: `AssetRegistry::Assets` (r:8 w:0) + /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Uniques::Class` (r:2 w:2) + /// Proof: `Uniques::Class` (`max_values`: None, `max_size`: Some(190), added: 2665, mode: `MaxEncodedLen`) + /// Storage: `Omnipool::Assets` (r:1 w:1) + /// Proof: `Omnipool::Assets` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:13 w:13) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `DynamicFees::AssetFeeConfiguration` (r:1 w:0) + /// Proof: `DynamicFees::AssetFeeConfiguration` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `MaxEncodedLen`) + /// Storage: `DynamicFees::AssetFee` (r:1 w:0) + /// Proof: `DynamicFees::AssetFee` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `EmaOracle::Oracles` (r:2 w:0) + /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:2 w:0) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `AssetRegistry::BannedAssets` (r:6 w:0) + /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:2 w:2) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `EmaOracle::Accumulator` (r:1 w:1) + /// Proof: `EmaOracle::Accumulator` (`max_values`: Some(1), `max_size`: Some(6601), added: 7096, mode: `MaxEncodedLen`) + /// Storage: `CircuitBreaker::LiquidityAddLimitPerAsset` (r:1 w:0) + /// Proof: `CircuitBreaker::LiquidityAddLimitPerAsset` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `CircuitBreaker::LiquidityRemoveLimitPerAsset` (r:1 w:0) + /// Proof: `CircuitBreaker::LiquidityRemoveLimitPerAsset` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::Pools` (r:1 w:0) + /// Proof: `Stableswap::Pools` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::PoolSnapshots` (r:1 w:0) + /// Proof: `Stableswap::PoolSnapshots` (`max_values`: None, `max_size`: Some(328), added: 2803, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::AssetTradability` (r:5 w:0) + /// Proof: `Stableswap::AssetTradability` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::PoolPegs` (r:1 w:0) + /// Proof: `Stableswap::PoolPegs` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `Uniques::Account` (r:0 w:3) + /// Proof: `Uniques::Account` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) + /// Storage: `Uniques::ItemPriceOf` (r:0 w:2) + /// Proof: `Uniques::ItemPriceOf` (`max_values`: None, `max_size`: Some(113), added: 2588, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 1]`. + fn remove_liquidity_stableswap_omnipool_and_exit_farms(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `25250 + c * (5724 ±0)` + // Estimated: `34569 + c * (7195 ±0)` + // Minimum execution time: 571_000_000 picoseconds. + Weight::from_parts(626_842_857, 34569) + // Standard Error: 2_082_005 + .saturating_add(Weight::from_parts(261_557_142, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(53_u64)) + .saturating_add(RocksDbWeight::get().reads((16_u64).saturating_mul(c.into()))) + .saturating_add(RocksDbWeight::get().writes(22_u64)) + .saturating_add(RocksDbWeight::get().writes((17_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 7195).saturating_mul(c.into())) + } fn price_adjustment_get() -> Weight { // Proof Size summary in bytes: // Measured: `5397` diff --git a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs index 8764d525ea..5a9ca57d41 100644 --- a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs +++ b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs @@ -29,6 +29,7 @@ use hydradx_traits::registry::{AssetKind, Create}; use hydradx_traits::stableswap::AssetAmount; use orml_benchmarking::runtime_benchmarks; use primitives::AssetId; +use sp_runtime::traits::ConstU32; use sp_runtime::{traits::One, FixedU128, Permill}; use sp_runtime::{DispatchError, DispatchResult, Perquintill, TransactionOutcome}; use warehouse_liquidity_mining::LoyaltyCurve; @@ -926,6 +927,170 @@ runtime_benchmarks! { update_deposit_limit(LRNA, 1_000u128).expect("Failed to update deposit limit");//To trigger circuit breaker, leading to worst case }: _(RawOrigin::Signed(lp_provider),pool_id, added_liquidity.try_into().unwrap(), Some(farms.try_into().unwrap()), None) + remove_liquidity_stableswap_omnipool_and_exit_farms { + let c in 0..1; + let with_farms = c == 1; // if c == 1, we also join and exit from farms + + let max_entries = get_max_entries(); + + //Init stableswap first + let caller: AccountId = account("caller", 0, 1); + let lp_provider: AccountId = account("provider", 0, 1); + let initial_liquidity = 1_000_000_000_000_000u128; + let liquidity_added = 300_000_000_000_000u128; + + let mut initial: Vec> = vec![]; + let mut added_liquidity: Vec> = vec![]; + let mut asset_ids: Vec = Vec::new() ; + for idx in 0..pallet_stableswap::MAX_ASSETS_IN_POOL { + let name: Vec = idx.to_ne_bytes().to_vec(); + let asset_id = register_asset_with_decimals( + name, + 1u128, + 18u8 + ).unwrap(); + asset_ids.push(asset_id); + Currencies::update_balance(RawOrigin::Root.into(), caller.clone(),asset_id, 1_000_000_000_000_000i128)?; + Currencies::update_balance(RawOrigin::Root.into(), lp_provider.clone(),asset_id, 1_000_000_000_000_000_000_000i128)?; + initial.push(AssetAmount::new(asset_id, initial_liquidity)); + added_liquidity.push(AssetAmount::new(asset_id, liquidity_added)); + } + + let name : Vec = b"PO2".to_vec(); + let pool_id = register_asset_with_decimals( + name, + 1u128, + 18u8 + ).unwrap(); + + let amplification = 100u16; + let trade_fee = Permill::from_percent(1); + let successful_origin = ::AuthorityOrigin::try_successful_origin().unwrap(); + Stableswap::create_pool(successful_origin, + pool_id, + BoundedVec::truncate_from(asset_ids), + amplification, + trade_fee, + )?; + + // Worst case is adding additional liquidity and not initial liquidity + Stableswap::add_assets_liquidity(RawOrigin::Signed(caller).into(), + pool_id, + initial.try_into().unwrap(), + 0u128, + )?; + + let lp1 = create_funded_account("lp_1", 1, 10 * BTC_ONE, BTC); + let deposit_id = 1; + + //Init LM farms + let owner = create_funded_account("owner", 0, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY); + let owner2 = create_funded_account("owner2", 1, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY); + let owner3 = create_funded_account("owner3", 2, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY); + let owner4 = create_funded_account("owner4", 3, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY); + let owner5 = create_funded_account("owner5", 4, G_FARM_TOTAL_REWARDS, REWARD_CURRENCY); + + let deposit_id = 1; + + initialize_omnipool(Some(pool_id))?; + + CircuitBreaker::set_add_liquidity_limit(RuntimeOrigin::root(), pool_id, Some((99, 100))).unwrap(); + let liquidity_added = 100_000_000_000_000_u128; + let omni_lp_provider: AccountId = create_funded_account("provider", 1, liquidity_added * 10, pool_id); + Omnipool::add_liquidity(RawOrigin::Signed(omni_lp_provider.clone()).into(), pool_id, liquidity_added)?; + + //gId: 1, yId: 2 + initialize_global_farm(owner.clone())?; + initialize_yield_farm(owner, 1, pool_id)?; + let lp1 = create_funded_account("lp_1", 1, 10 * ONE, pool_id); + let lp1_position_id = omnipool_add_liquidity(lp1.clone(), pool_id, 10 * ONE)?; + lm_deposit_shares(lp1, 1, 2, lp1_position_id)?; + + //gId: 3, yId: 4 + initialize_global_farm(owner2.clone())?; + initialize_yield_farm(owner2, 3, pool_id)?; + let lp2 = create_funded_account("lp_2", 1, 10 * ONE, pool_id); + let lp2_position_id = omnipool_add_liquidity(lp2.clone(), pool_id, 10 * ONE)?; + lm_deposit_shares(lp2, 3, 4, lp2_position_id)?; + + //gId: 5, yId: 6 + initialize_global_farm(owner3.clone())?; + initialize_yield_farm(owner3, 5, pool_id)?; + let lp3 = create_funded_account("lp_3", 1, 10 * ONE, pool_id); + let lp3_position_id = omnipool_add_liquidity(lp3.clone(), pool_id, 10 * ONE)?; + lm_deposit_shares(lp3, 5, 6, lp3_position_id)?; + + //gId: 7, yId: 8 + initialize_global_farm(owner4.clone())?; + initialize_yield_farm(owner4, 7, pool_id)?; + let lp4 = create_funded_account("lp_4", 1, 10 * ONE, pool_id); + let lp4_position_id = omnipool_add_liquidity(lp4.clone(), pool_id, 10 * ONE)?; + lm_deposit_shares(lp4, 7, 8, lp4_position_id)?; + + //gId: 9, yId: 10 + initialize_global_farm(owner5.clone())?; + initialize_yield_farm(owner5, 9, pool_id)?; + let lp5 = create_funded_account("lp_5", 1, 10 * ONE, pool_id); + let lp5_position_id = omnipool_add_liquidity(lp5.clone(), pool_id, 10 * ONE)?; + lm_deposit_shares(lp5, 9, 10, lp5_position_id)?; + + let lp6 = create_funded_account("lp_6", 5, 10 * ONE, pool_id); + + set_period(200); + let farms_entries = [(1,2), (3,4), (5,6), (7,8), (9, 10)]; + let farms = farms_entries[0..max_entries as usize].to_vec(); + + + CircuitBreaker::set_add_liquidity_limit(RawOrigin::Root.into(),pool_id, None).unwrap(); + let _ = Tokens::deposit(pool_id, &lp_provider, 50000000000000000);//We mint some share token so it wont fail with insufficience balance in adding liqudity to omnipool + update_deposit_limit(pool_id, 1_000u128).expect("Failed to update deposit limit");//To trigger circuit breaker, leading to worst case + update_deposit_limit(LRNA, 1_000u128).expect("Failed to update deposit limit");//To trigger circuit breaker, leading to worst case + + let position_id = Omnipool::next_position_id(); + let deposit_id = if with_farms { + Some(OmnipoolWarehouseLM::deposit_id() + 1) + } else { + None + }; + let farms_to_join = if with_farms { + Some(farms.try_into().unwrap()) + } else { + None + }; + OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms(RawOrigin::Signed(lp_provider.clone()).into(),pool_id, added_liquidity.try_into().unwrap(), farms_to_join, None).unwrap(); + + let asset_ids: Vec> = Stableswap::pools(pool_id) + .into_iter() + .flat_map(|pool_info| pool_info.assets.into_iter()) + .map(|asset_id| AssetAmount::::new(asset_id.into(), 10000)) + .collect(); + + let asset_ids: BoundedVec, ConstU32<{ pallet_stableswap::MAX_ASSETS_IN_POOL }>> = asset_ids.try_into().unwrap(); + CircuitBreaker::set_remove_liquidity_limit(RawOrigin::Root.into(),pool_id, None).unwrap(); + }: { + OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms(RawOrigin::Signed(lp_provider).into(),position_id,asset_ids, deposit_id).unwrap(); + } + + verify{ + if with_farms { + let deposit_id = deposit_id.unwrap(); + assert!(OmnipoolWarehouseLM::deposit(deposit_id).is_none()); + } + + // Verify NFT was destroyed + assert!( + Uniques::owner( + OmnipoolCollectionId::get(), + position_id + ) + .is_none(), + "NFT should be destroyed after removing all liquidity" + ); + } + + + + //NOTE: price adjustment reads route's price from oracle so pool type doesn't matter price_adjustment_get { diff --git a/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs b/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs index 2ee682633c..37539832d4 100644 --- a/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs +++ b/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs @@ -19,13 +19,13 @@ //! Autogenerated weights for `pallet_omnipool_liquidity_mining` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 -//! DATE: 2025-07-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2025-12-22, 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` +//! HOSTNAME: `Mac.chello.hu`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` // Executed Command: -// ./bin/hydradx +// ./target/release/hydradx // benchmark // pallet // --wasm-execution=compiled @@ -66,16 +66,16 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `OmnipoolWarehouseLM::FarmSequencer` (r:1 w:1) /// Proof: `OmnipoolWarehouseLM::FarmSequencer` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Duster::AccountBlacklist` (r:0 w:1) - /// Proof: `Duster::AccountBlacklist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:0 w:1) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:0 w:1) /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) fn create_global_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `2196` + // Measured: `2262` // Estimated: `6196` - // Minimum execution time: 108_289_000 picoseconds. - Weight::from_parts(109_094_000, 6196) + // Minimum execution time: 76_000_000 picoseconds. + Weight::from_parts(78_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -89,10 +89,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) fn update_global_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `6746` + // Measured: `6812` // Estimated: `6328` - // Minimum execution time: 156_327_000 picoseconds. - Weight::from_parts(157_500_000, 6328) + // Minimum execution time: 113_000_000 picoseconds. + Weight::from_parts(115_000_000, 6328) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -100,14 +100,14 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// 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) - /// Proof: `Duster::AccountBlacklist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:1 w:1) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) fn terminate_global_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `5935` + // Measured: `5901` // Estimated: `6196` - // Minimum execution time: 120_075_000 picoseconds. - Weight::from_parts(121_332_000, 6196) + // Minimum execution time: 84_000_000 picoseconds. + Weight::from_parts(91_000_000, 6196) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -129,10 +129,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `OmnipoolWarehouseLM::YieldFarm` (`max_values`: None, `max_size`: Some(198), added: 2673, mode: `MaxEncodedLen`) fn create_yield_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `7242` + // Measured: `7341` // Estimated: `6328` - // Minimum execution time: 179_024_000 picoseconds. - Weight::from_parts(180_446_000, 6328) + // Minimum execution time: 140_000_000 picoseconds. + Weight::from_parts(152_000_000, 6328) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -152,10 +152,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) fn update_yield_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `7748` + // Measured: `7847` // Estimated: `6328` - // Minimum execution time: 183_426_000 picoseconds. - Weight::from_parts(184_631_000, 6328) + // Minimum execution time: 130_000_000 picoseconds. + Weight::from_parts(140_000_000, 6328) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -173,10 +173,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) fn stop_yield_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `7321` + // Measured: `7387` // Estimated: `6328` - // Minimum execution time: 173_546_000 picoseconds. - Weight::from_parts(175_017_000, 6328) + // Minimum execution time: 122_000_000 picoseconds. + Weight::from_parts(142_000_000, 6328) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -196,10 +196,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) fn resume_yield_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `7865` + // Measured: `7964` // Estimated: `6328` - // Minimum execution time: 180_989_000 picoseconds. - Weight::from_parts(182_303_000, 6328) + // Minimum execution time: 129_000_000 picoseconds. + Weight::from_parts(145_000_000, 6328) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -213,10 +213,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn terminate_yield_farm() -> Weight { // Proof Size summary in bytes: - // Measured: `6303` + // Measured: `6336` // Estimated: `6196` - // Minimum execution time: 115_170_000 picoseconds. - Weight::from_parts(116_663_000, 6196) + // Minimum execution time: 85_000_000 picoseconds. + Weight::from_parts(87_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -252,10 +252,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `OmnipoolWarehouseLM::Deposit` (`max_values`: None, `max_size`: Some(385), added: 2860, mode: `MaxEncodedLen`) fn deposit_shares() -> Weight { // Proof Size summary in bytes: - // Measured: `10672` + // Measured: `10771` // Estimated: `11666` - // Minimum execution time: 278_471_000 picoseconds. - Weight::from_parts(280_738_000, 11666) + // Minimum execution time: 204_000_000 picoseconds. + Weight::from_parts(208_000_000, 11666) .saturating_add(T::DbWeight::get().reads(17_u64)) .saturating_add(T::DbWeight::get().writes(14_u64)) } @@ -281,10 +281,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) fn redeposit_shares() -> Weight { // Proof Size summary in bytes: - // Measured: `13641` + // Measured: `13773` // Estimated: `11666` - // Minimum execution time: 239_448_000 picoseconds. - Weight::from_parts(241_722_000, 11666) + // Minimum execution time: 178_000_000 picoseconds. + Weight::from_parts(195_000_000, 11666) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -304,10 +304,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) fn claim_rewards() -> Weight { // Proof Size summary in bytes: - // Measured: `10886` + // Measured: `10952` // Estimated: `8799` - // Minimum execution time: 232_852_000 picoseconds. - Weight::from_parts(234_833_000, 8799) + // Minimum execution time: 174_000_000 picoseconds. + Weight::from_parts(180_000_000, 8799) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -337,10 +337,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `Uniques::ItemPriceOf` (`max_values`: None, `max_size`: Some(113), added: 2588, mode: `MaxEncodedLen`) fn withdraw_shares() -> Weight { // Proof Size summary in bytes: - // Measured: `9309` + // Measured: `9441` // Estimated: `8799` - // Minimum execution time: 325_934_000 picoseconds. - Weight::from_parts(328_198_000, 8799) + // Minimum execution time: 245_000_000 picoseconds. + Weight::from_parts(256_000_000, 8799) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)) } @@ -377,12 +377,12 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// The range of component `c` is `[1, 5]`. fn join_farms(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `18920 + c * (507 ±0)` + // Measured: `19019 + c * (507 ±0)` // Estimated: `11666 + c * (2680 ±0)` - // Minimum execution time: 283_303_000 picoseconds. - Weight::from_parts(181_077_609, 11666) - // Standard Error: 66_829 - .saturating_add(Weight::from_parts(106_346_152, 0).saturating_mul(c.into())) + // Minimum execution time: 203_000_000 picoseconds. + Weight::from_parts(144_844_392, 11666) + // Standard Error: 280_673 + .saturating_add(Weight::from_parts(81_133_177, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(11_u64)) @@ -409,8 +409,12 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `Uniques::Class` (`max_values`: None, `max_size`: Some(190), added: 2665, mode: `MaxEncodedLen`) /// Storage: `Uniques::CollectionMaxSupply` (r:2 w:0) /// Proof: `Uniques::CollectionMaxSupply` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) - /// Storage: `Duster::AccountBlacklist` (r:1 w:0) - /// Proof: `Duster::AccountBlacklist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:1 w:0) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::BannedAssets` (r:2 w:0) /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:7 w:7) @@ -452,13 +456,13 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// The range of component `c` is `[1, 5]`. fn add_liquidity_and_join_farms(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `20514 + c * (507 ±0)` + // Measured: `20554 + c * (507 ±0)` // Estimated: `14335 + c * (2680 ±0)` - // Minimum execution time: 600_241_000 picoseconds. - Weight::from_parts(496_570_839, 14335) - // Standard Error: 132_038 - .saturating_add(Weight::from_parts(110_218_143, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads(38_u64)) + // Minimum execution time: 432_000_000 picoseconds. + Weight::from_parts(397_502_278, 14335) + // Standard Error: 593_093 + .saturating_add(Weight::from_parts(83_494_334, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(40_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(26_u64)) .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(c.into()))) @@ -491,12 +495,12 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// The range of component `c` is `[1, 5]`. fn exit_farms(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `11332 + c * (518 ±0)` + // Measured: `11464 + c * (518 ±0)` // Estimated: `6328 + c * (2680 ±0)` - // Minimum execution time: 284_138_000 picoseconds. - Weight::from_parts(121_725_030, 6328) - // Standard Error: 275_513 - .saturating_add(Weight::from_parts(164_251_115, 0).saturating_mul(c.into())) + // Minimum execution time: 201_000_000 picoseconds. + Weight::from_parts(105_905_081, 6328) + // Standard Error: 516_125 + .saturating_add(Weight::from_parts(118_452_745, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) @@ -514,17 +518,21 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Storage: `Tokens::TotalIssuance` (r:2 w:2) /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) /// Storage: `Stableswap::PoolSnapshots` (r:1 w:0) - /// Proof: `Stableswap::PoolSnapshots` (`max_values`: None, `max_size`: Some(324), added: 2799, mode: `MaxEncodedLen`) + /// Proof: `Stableswap::PoolSnapshots` (`max_values`: None, `max_size`: Some(328), added: 2803, mode: `MaxEncodedLen`) /// Storage: `Stableswap::PoolPegs` (r:1 w:0) - /// Proof: `Stableswap::PoolPegs` (`max_values`: None, `max_size`: Some(351), added: 2826, mode: `MaxEncodedLen`) + /// Proof: `Stableswap::PoolPegs` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::BannedAssets` (r:7 w:0) /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) /// Storage: `CircuitBreaker::AssetLockdownState` (r:2 w:2) /// Proof: `CircuitBreaker::AssetLockdownState` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) /// Storage: `Tokens::Reserves` (r:2 w:2) /// Proof: `Tokens::Reserves` (`max_values`: None, `max_size`: Some(1261), added: 3736, mode: `MaxEncodedLen`) - /// Storage: `Duster::AccountBlacklist` (r:1 w:0) - /// Proof: `Duster::AccountBlacklist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:1 w:0) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:7 w:6) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `EmaOracle::Accumulator` (r:1 w:1) @@ -570,18 +578,89 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// The range of component `c` is `[1, 5]`. fn add_liquidity_stableswap_omnipool_and_join_farms(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `23148 + c * (507 ±0)` + // Measured: `23192 + c * (507 ±0)` // Estimated: `34569 + c * (2680 ±0)` - // Minimum execution time: 1_785_849_000 picoseconds. - Weight::from_parts(1_696_751_106, 34569) - // Standard Error: 162_168 - .saturating_add(Weight::from_parts(105_919_882, 0).saturating_mul(c.into())) - .saturating_add(T::DbWeight::get().reads(65_u64)) + // Minimum execution time: 1_148_000_000 picoseconds. + Weight::from_parts(1_087_377_102, 34569) + // Standard Error: 451_081 + .saturating_add(Weight::from_parts(78_431_308, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(67_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(37_u64)) .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(c.into()))) .saturating_add(Weight::from_parts(0, 2680).saturating_mul(c.into())) } + /// Storage: `Uniques::Asset` (r:2 w:2) + /// Proof: `Uniques::Asset` (`max_values`: None, `max_size`: Some(146), added: 2621, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolLiquidityMining::OmniPositionId` (r:1 w:1) + /// Proof: `OmnipoolLiquidityMining::OmniPositionId` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolWarehouseLM::Deposit` (r:1 w:1) + /// Proof: `OmnipoolWarehouseLM::Deposit` (`max_values`: None, `max_size`: Some(385), added: 2860, mode: `MaxEncodedLen`) + /// Storage: `Omnipool::Positions` (r:1 w:1) + /// Proof: `Omnipool::Positions` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolWarehouseLM::YieldFarm` (r:5 w:5) + /// Proof: `OmnipoolWarehouseLM::YieldFarm` (`max_values`: None, `max_size`: Some(198), added: 2673, mode: `MaxEncodedLen`) + /// Storage: `OmnipoolWarehouseLM::GlobalFarm` (r:5 w:5) + /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) + /// Storage: `AssetRegistry::Assets` (r:8 w:0) + /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:0) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Uniques::Class` (r:2 w:2) + /// Proof: `Uniques::Class` (`max_values`: None, `max_size`: Some(190), added: 2665, mode: `MaxEncodedLen`) + /// Storage: `Omnipool::Assets` (r:1 w:1) + /// Proof: `Omnipool::Assets` (`max_values`: None, `max_size`: Some(85), added: 2560, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:13 w:13) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `DynamicFees::AssetFeeConfiguration` (r:1 w:0) + /// Proof: `DynamicFees::AssetFeeConfiguration` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `MaxEncodedLen`) + /// Storage: `DynamicFees::AssetFee` (r:1 w:0) + /// Proof: `DynamicFees::AssetFee` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `EmaOracle::Oracles` (r:2 w:0) + /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Duster::AccountWhitelist` (r:2 w:0) + /// Proof: `Duster::AccountWhitelist` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `AssetRegistry::BannedAssets` (r:6 w:0) + /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:2 w:2) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(28), added: 2503, mode: `MaxEncodedLen`) + /// Storage: `EmaOracle::Accumulator` (r:1 w:1) + /// Proof: `EmaOracle::Accumulator` (`max_values`: Some(1), `max_size`: Some(6601), added: 7096, mode: `MaxEncodedLen`) + /// Storage: `CircuitBreaker::LiquidityAddLimitPerAsset` (r:1 w:0) + /// Proof: `CircuitBreaker::LiquidityAddLimitPerAsset` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `CircuitBreaker::LiquidityRemoveLimitPerAsset` (r:1 w:0) + /// Proof: `CircuitBreaker::LiquidityRemoveLimitPerAsset` (`max_values`: None, `max_size`: Some(29), added: 2504, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::Pools` (r:1 w:0) + /// Proof: `Stableswap::Pools` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::PoolSnapshots` (r:1 w:0) + /// Proof: `Stableswap::PoolSnapshots` (`max_values`: None, `max_size`: Some(328), added: 2803, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::AssetTradability` (r:5 w:0) + /// Proof: `Stableswap::AssetTradability` (`max_values`: None, `max_size`: Some(41), added: 2516, mode: `MaxEncodedLen`) + /// Storage: `Stableswap::PoolPegs` (r:1 w:0) + /// Proof: `Stableswap::PoolPegs` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `Uniques::Account` (r:0 w:3) + /// Proof: `Uniques::Account` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) + /// Storage: `Uniques::ItemPriceOf` (r:0 w:2) + /// Proof: `Uniques::ItemPriceOf` (`max_values`: None, `max_size`: Some(113), added: 2588, mode: `MaxEncodedLen`) + /// The range of component `c` is `[0, 1]`. + fn remove_liquidity_stableswap_omnipool_and_exit_farms(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `25250 + c * (5724 ±0)` + // Estimated: `34569 + c * (7195 ±0)` + // Minimum execution time: 571_000_000 picoseconds. + Weight::from_parts(626_842_857, 34569) + // Standard Error: 2_082_005 + .saturating_add(Weight::from_parts(261_557_142, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(53_u64)) + .saturating_add(T::DbWeight::get().reads((16_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(22_u64)) + .saturating_add(T::DbWeight::get().writes((17_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 7195).saturating_mul(c.into())) + } /// Storage: `AssetRegistry::Assets` (r:1 w:0) /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) /// Storage: `EmaOracle::Oracles` (r:19 w:0) @@ -590,10 +669,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `Router::Routes` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) fn price_adjustment_get() -> Weight { // Proof Size summary in bytes: - // Measured: `5703` + // Measured: `5736` // Estimated: `51701` - // Minimum execution time: 179_703_000 picoseconds. - Weight::from_parts(182_196_000, 51701) + // Minimum execution time: 120_000_000 picoseconds. + Weight::from_parts(122_000_000, 51701) .saturating_add(T::DbWeight::get().reads(21_u64)) } } \ No newline at end of file From dbf2b61529a3eb4a6f8a4a83dab6f5afd7ced4d1 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 13:21:44 +0100 Subject: [PATCH 05/29] remove unnecessary comments --- pallets/omnipool-liquidity-mining/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 0b6142908b..cc32186cb4 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1109,11 +1109,6 @@ pub mod pallet { /// Emits events: /// - If deposit_id provided: `RewardClaimed`, `SharesWithdrawn`, `DepositDestroyed` /// - Always: Omnipool's `LiquidityRemoved`, Stableswap's `LiquidityRemoved` - /// - /// Errors: - /// - `NoAssetsSpecified` if min_amounts_out is empty - /// - `Forbidden` if caller doesn't own the deposit NFT (when deposit_id provided) - /// - `InconsistentState(MissingLpPosition)` if deposit-position mismatch #[pallet::call_index(17)] #[pallet::weight({ let with_farm = if deposit_id.is_some() { 1 } else { 0 }; From 265af4c8a0c782c4216fdc045968d5c7439150a2 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 13:21:58 +0100 Subject: [PATCH 06/29] add more integration test --- .../src/omnipool_liquidity_mining.rs | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index 67d6ed334b..7ab5c8c744 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -1609,6 +1609,151 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { }); } + #[test] + fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_not_work_when_not_exiting_from_farms() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + let global_farm_1_id = 1; + let global_farm_2_id = 2; + let global_farm_3_id = 3; + let yield_farm_1_id = 4; + let yield_farm_2_id = 5; + let yield_farm_3_id = 6; + + //Arrange + let (stable_pool_id, stable_asset_1, stable_asset_2) = init_stableswap().unwrap(); + + init_omnipool(); + seed_lm_pot(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + Omnipool::protocol_account(), + stable_pool_id, + 30_000_000 * UNITS as i128, + )); + + assert_ok!(Omnipool::add_token( + RuntimeOrigin::root(), + stable_pool_id, + FixedU128::from_rational(50, 100), + Permill::from_percent(100), + AccountId::from(BOB), + )); + + //NOTE: necessary to get oracle price. + hydradx_run_to_block(100); + set_relaychain_block_number(100); + create_global_farm(None, None); + create_global_farm(None, None); + create_global_farm(None, None); + + set_relaychain_block_number(200); + create_yield_farm(global_farm_1_id, stable_pool_id); + create_yield_farm(global_farm_2_id, stable_pool_id); + create_yield_farm(global_farm_3_id, stable_pool_id); + + set_relaychain_block_number(300); + + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + ETH, + 10_000 * UNITS as i128, + )); + + //Add some liquidity to make sure that it does not interfere with the new liquidity add + assert_ok!(hydradx_runtime::Omnipool::add_liquidity( + RuntimeOrigin::signed(CHARLIE.into()), + ETH, + 100 * UNITS, + )); + + let position_id = hydradx_runtime::Omnipool::next_position_id(); + + set_relaychain_block_number(400); + let deposit_id = 1; + let farms = vec![ + (global_farm_1_id, yield_farm_1_id), + (global_farm_2_id, yield_farm_2_id), + (global_farm_3_id, yield_farm_3_id), + ]; + + //Add liquidity first + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_1, + 100 * UNITS as i128, + )); + assert_ok!(hydradx_runtime::Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + CHARLIE.into(), + stable_asset_2, + 100 * UNITS as i128, + )); + + assert_ok!( + hydradx_runtime::OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(CHARLIE.into()), + stable_pool_id, + vec![ + AssetAmount::new(stable_asset_1, 10 * UNITS), + AssetAmount::new(stable_asset_2, 10 * UNITS) + ] + .try_into() + .unwrap(), + Some(farms.try_into().unwrap()), + None, + ) + ); + + // Wait some blocks + set_relaychain_block_number(500); + + // Store balance before removal + let charlie_asset_1_balance_before = + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(CHARLIE)); + let charlie_asset_2_balance_before = + hydradx_runtime::Currencies::free_balance(stable_asset_2, &AccountId::from(CHARLIE)); + + let asset_ids_without_slippage: Vec> = Stableswap::pools(stable_pool_id) + .into_iter() + .flat_map(|pool_info| pool_info.assets.into_iter()) + .map(|asset_id| AssetAmount::::new(asset_id.into(), 10000)) + .collect(); + + let asset_ids_without_slippage = create_bounded_vec(asset_ids_without_slippage); + + //Act - Remove liquidity and exit all farms + assert_noop!( + hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(CHARLIE.into()), + position_id, + asset_ids_without_slippage, + None + ), + pallet_omnipool::Error::::Forbidden + ); + + //Assert + // Verify deposit is destroyed + assert!(hydradx_runtime::OmnipoolWarehouseLM::deposit(deposit_id).is_some()); + + // Verify NFT was destroyed (all liquidity removed) + assert!( + hydradx_runtime::Uniques::owner(hydradx_runtime::OmnipoolCollectionId::get(), position_id) + .is_some(), + "NFT should not be destroyed as no liquidity removed" + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } + #[test] fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_asset_withdrawal() { TestNet::reset(); From 9dc325f52450c57de00ecd8ced509a953324fe80 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 15:05:12 +0100 Subject: [PATCH 07/29] add slippage limit for omnipool when removing liquidity --- .../src/omnipool_liquidity_mining.rs | 7 ++ pallets/omnipool-liquidity-mining/src/lib.rs | 11 +-- ...dity_stableswap_omnipool_and_exit_farms.rs | 73 +++++++++++++++++++ .../benchmarking/omnipool_liquidity_mining.rs | 2 +- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/integration-tests/src/omnipool_liquidity_mining.rs b/integration-tests/src/omnipool_liquidity_mining.rs index 7ab5c8c744..ef393e767f 100644 --- a/integration-tests/src/omnipool_liquidity_mining.rs +++ b/integration-tests/src/omnipool_liquidity_mining.rs @@ -1389,6 +1389,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, + Balance::MIN, asset_ids_without_slippage, Some(deposit_id) ) @@ -1564,6 +1565,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, + Balance::MIN, asset_ids_without_slippage, None ) @@ -1732,6 +1734,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, + Balance::MIN, asset_ids_without_slippage, None ), @@ -1856,6 +1859,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, + Balance::MIN, vec![AssetAmount::new(stable_asset_1, 0)].try_into().unwrap(), Some(deposit_id) ) @@ -2011,6 +2015,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, + Balance::MIN, vec![AssetAmount::new(stable_asset_1, 0)].try_into().unwrap(), Some(deposit_id) ) @@ -2165,6 +2170,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(DAVE.into()), position_id, + Balance::MIN, vec![AssetAmount::new(stable_asset_1, 0), AssetAmount::new(stable_asset_2, 0)] .try_into() .unwrap(), @@ -2276,6 +2282,7 @@ mod remove_liquidity_stableswap_omnipool_and_exit_farms { hydradx_runtime::OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(CHARLIE.into()), position_id, + Balance::MIN, vec![].try_into().unwrap(), Some(deposit_id) ), diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index cc32186cb4..708e059c2a 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1092,8 +1092,8 @@ pub mod pallet { /// /// It performs the following steps in order: /// 1. [OPTIONAL] If deposit_id is provided: Exits from ALL yield farms associated with the deposit (claiming rewards) - /// 2. Removes liquidity from the omnipool to retrieve stableswap shares - /// 3. Removes liquidity from the stableswap pool to retrieve underlying assets + /// 2. Removes liquidity from the omnipool to retrieve stableswap shares (protected by omnipool_min_limit) + /// 3. Removes liquidity from the stableswap pool to retrieve underlying assets (protected by min_amounts_out) /// /// The asset removal strategy is determined by the `min_amounts_out` parameter length: /// - If 1 asset is specified: Uses `remove_liquidity_one_asset` (trading fee applies) @@ -1102,8 +1102,8 @@ pub mod pallet { /// Parameters: /// - `origin`: Owner of the omnipool position /// - `position_id`: The omnipool position NFT ID to remove liquidity from - /// - `stable_pool_id`: The stableswap pool ID containing the liquidity - /// - `min_amounts_out`: Asset IDs and minimum amounts for slippage protection + /// - `omnipool_min_limit`: Minimum stableswap shares to receive from omnipool (slippage protection for step 2) + /// - `min_amounts_out`: Asset IDs and minimum amounts for slippage protection (for step 3) /// - `deposit_id`: Optional liquidity mining deposit NFT ID. If provided, exits all farms first. /// /// Emits events: @@ -1118,6 +1118,7 @@ pub mod pallet { pub fn remove_liquidity_stableswap_omnipool_and_exit_farms( origin: OriginFor, position_id: T::PositionItemId, + omnipool_min_limit: Balance, min_amounts_out: BoundedVec, ConstU32>, deposit_id: Option, ) -> DispatchResult { @@ -1153,7 +1154,7 @@ pub mod pallet { origin.clone(), position_id, omnipool_shares_to_remove, - Balance::MIN, + omnipool_min_limit, )?; if min_amounts_out.len() == 1 { diff --git a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs index b4c311991d..abfc1bf6ed 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs @@ -15,6 +15,7 @@ use super::*; +use frame_support::BoundedVec; use pallet_omnipool::types::{AssetReserveState, Tradability}; use pretty_assertions::assert_eq; @@ -70,6 +71,7 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_single_a assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), )); @@ -158,6 +160,7 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multi_as assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1), AssetAmount::new(USDC, 1)] .try_into() .unwrap(), @@ -269,6 +272,7 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_work_with_multiple assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), )); @@ -380,6 +384,7 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_when_not_owne OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP2), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), ), @@ -440,6 +445,7 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_should_fail_with_empty_as OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![].try_into().unwrap(), Some(deposit_id), ), @@ -505,6 +511,7 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), )); @@ -584,6 +591,7 @@ fn remove_liquidity_without_farm_exit_should_work() { assert_ok!(OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), None, // No deposit_id since we never joined farms )); @@ -676,6 +684,7 @@ fn should_fail_with_mismatched_deposit_and_position() { OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_1, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_2), // WRONG deposit for position_1 ), @@ -718,6 +727,7 @@ fn should_fail_with_nonexistent_deposit() { OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP1), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(999), // Non-existent deposit_id ), @@ -779,6 +789,7 @@ fn should_fail_when_deposit_owner_differs() { OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( RuntimeOrigin::signed(LP2), position_id, + Balance::MIN, vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_id), ), @@ -786,3 +797,65 @@ fn should_fail_when_deposit_owner_differs() { ); }); } + +#[test] +fn remove_liquidity_should_fail_when_slippage_limit_exceeded_on_omnipool_step() { + let token_amount = 2000 * ONE; + let amount = 20 * ONE; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP1, STABLESWAP_POOL_ID, 500000 * ONE), + (LP1, USDT, 5000 * ONE), + (GC, HDX, 100_000_000 * ONE), + ]) + .with_registered_asset(USDT) + .with_registered_asset(STABLESWAP_POOL_ID) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(USDT, FixedU128::from_float(0.65), LP1, token_amount) + .with_token(STABLESWAP_POOL_ID, FixedU128::from_float(0.65), LP1, token_amount) + .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, STABLESWAP_POOL_ID, FixedU128::one(), None) + .build() + .execute_with(|| { + //Arrange + let gc_g_farm_id = 1; + let gc_y_farm_id = 2; + let deposit_id = 1; + let yield_farms = vec![(gc_g_farm_id, gc_y_farm_id)]; + + assert_ok!(OmnipoolMining::add_liquidity_stableswap_omnipool_and_join_farms( + RuntimeOrigin::signed(LP1), + STABLESWAP_POOL_ID, + vec![AssetAmount::new(USDT, amount)].try_into().unwrap(), + Some(yield_farms.try_into().unwrap()), + None, + )); + + let position_id = + crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); + + //Act and assert + assert_noop!( + OmnipoolMining::remove_liquidity_stableswap_omnipool_and_exit_farms( + RuntimeOrigin::signed(LP1), + position_id, + SHARES_FROM_STABLESWAP + 1, + vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), + Some(deposit_id), + ), + pallet_omnipool::Error::::SlippageLimit + ); + }); +} diff --git a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs index 5a9ca57d41..ef91b56a2e 100644 --- a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs +++ b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs @@ -1068,7 +1068,7 @@ runtime_benchmarks! { let asset_ids: BoundedVec, ConstU32<{ pallet_stableswap::MAX_ASSETS_IN_POOL }>> = asset_ids.try_into().unwrap(); CircuitBreaker::set_remove_liquidity_limit(RawOrigin::Root.into(),pool_id, None).unwrap(); }: { - OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms(RawOrigin::Signed(lp_provider).into(),position_id,asset_ids, deposit_id).unwrap(); + OmnipoolLiquidityMining::remove_liquidity_stableswap_omnipool_and_exit_farms(RawOrigin::Signed(lp_provider).into(),position_id,Balance::MIN,asset_ids, deposit_id).unwrap(); } verify{ From 762cafe249f51ccfd999cd45bc24b7d8c5d80075 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 15:11:42 +0100 Subject: [PATCH 08/29] renaming --- pallets/omnipool-liquidity-mining/src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 708e059c2a..121749a09f 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1093,17 +1093,17 @@ pub mod pallet { /// It performs the following steps in order: /// 1. [OPTIONAL] If deposit_id is provided: Exits from ALL yield farms associated with the deposit (claiming rewards) /// 2. Removes liquidity from the omnipool to retrieve stableswap shares (protected by omnipool_min_limit) - /// 3. Removes liquidity from the stableswap pool to retrieve underlying assets (protected by min_amounts_out) + /// 3. Removes liquidity from the stableswap pool to retrieve underlying assets (protected by stableswap_min_amounts_out) /// - /// The asset removal strategy is determined by the `min_amounts_out` parameter length: + /// The stabelswap liquidity asset removal strategy is determined by the `min_amounts_out` parameter length: /// - If 1 asset is specified: Uses `remove_liquidity_one_asset` (trading fee applies) /// - If multiple assets: Uses `remove_liquidity` (proportional, no trading fee) /// /// Parameters: /// - `origin`: Owner of the omnipool position /// - `position_id`: The omnipool position NFT ID to remove liquidity from - /// - `omnipool_min_limit`: Minimum stableswap shares to receive from omnipool (slippage protection for step 2) - /// - `min_amounts_out`: Asset IDs and minimum amounts for slippage protection (for step 3) + /// - `omnipool_min_limit`: The min amount of asset to be removed from omnipool (slippage protection) + /// - `stableswap_min_amounts_out`: Asset IDs and minimum amounts minimum amounts of each asset to receive from omnipool. /// - `deposit_id`: Optional liquidity mining deposit NFT ID. If provided, exits all farms first. /// /// Emits events: @@ -1119,11 +1119,11 @@ pub mod pallet { origin: OriginFor, position_id: T::PositionItemId, omnipool_min_limit: Balance, - min_amounts_out: BoundedVec, ConstU32>, + stableswap_min_amounts_out: BoundedVec, ConstU32>, deposit_id: Option, ) -> DispatchResult { let who = ensure_signed(origin.clone())?; - ensure!(!min_amounts_out.is_empty(), Error::::NoAssetsSpecified); + ensure!(!stableswap_min_amounts_out.is_empty(), Error::::NoAssetsSpecified); if let Some(deposit_id) = deposit_id { Self::ensure_nft_owner(origin.clone(), deposit_id)?; @@ -1157,8 +1157,8 @@ pub mod pallet { omnipool_min_limit, )?; - if min_amounts_out.len() == 1 { - let asset_amount = &min_amounts_out[0]; + if stableswap_min_amounts_out.len() == 1 { + let asset_amount = &stableswap_min_amounts_out[0]; T::Stableswap::remove_liquidity_one_asset( who.clone(), @@ -1172,7 +1172,7 @@ pub mod pallet { who, stable_pool_id, actual_stable_shares_received, - min_amounts_out.to_vec(), + stableswap_min_amounts_out.to_vec(), )?; } From 5d1690d6bd5000bb2247c006ab1849629cfdd991 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 15:13:31 +0100 Subject: [PATCH 09/29] formatting --- pallets/omnipool/src/lib.rs | 7 ++++++- pallets/stableswap/src/lib.rs | 8 +------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index e6c192d103..52bea7cea4 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -2205,7 +2205,12 @@ impl Pallet { ) .ok_or(ArithmeticError::Overflow)?; - T::Currency::transfer(asset_id, &Self::protocol_account(), &who, *state_changes.asset.delta_reserve)?; + T::Currency::transfer( + asset_id, + &Self::protocol_account(), + &who, + *state_changes.asset.delta_reserve, + )?; // burn only difference between delta hub and lp hub amount. Self::update_hub_asset_liquidity( diff --git a/pallets/stableswap/src/lib.rs b/pallets/stableswap/src/lib.rs index 51579ca447..4abcd591b2 100644 --- a/pallets/stableswap/src/lib.rs +++ b/pallets/stableswap/src/lib.rs @@ -596,13 +596,7 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; - let _ = Self::do_remove_liquidity_one_asset( - &who, - pool_id, - asset_id, - share_amount, - min_amount_out, - )?; + let _ = Self::do_remove_liquidity_one_asset(&who, pool_id, asset_id, share_amount, min_amount_out)?; Ok(()) } From e1e25b3eb17b38fdad8613c11a6781d5bc0f1b45 Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 29 Dec 2025 15:37:45 +0100 Subject: [PATCH 10/29] bump versions --- Cargo.lock | 18 ++++----- integration-tests/Cargo.toml | 2 +- pallets/liquidity-mining/Cargo.toml | 2 +- pallets/omnipool-liquidity-mining/Cargo.toml | 40 ++++++++++---------- pallets/omnipool/Cargo.toml | 2 +- pallets/stableswap/Cargo.toml | 4 +- pallets/xyk-liquidity-mining/Cargo.toml | 32 ++++++++-------- pallets/xyk/Cargo.toml | 2 +- runtime/hydradx/Cargo.toml | 3 +- runtime/hydradx/src/lib.rs | 2 +- traits/Cargo.toml | 2 +- 11 files changed, 54 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4986101c4d..2c08660801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "375.0.0" +version = "376.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -5764,7 +5764,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "4.4.2" +version = "4.5.0" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -10071,7 +10071,7 @@ dependencies = [ [[package]] name = "pallet-liquidity-mining" -version = "4.4.8" +version = "4.4.9" dependencies = [ "fixed", "frame-support", @@ -10393,7 +10393,7 @@ dependencies = [ [[package]] name = "pallet-omnipool" -version = "5.1.10" +version = "5.1.11" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", @@ -10422,7 +10422,7 @@ dependencies = [ [[package]] name = "pallet-omnipool-liquidity-mining" -version = "2.7.1" +version = "2.8.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -10993,7 +10993,7 @@ dependencies = [ [[package]] name = "pallet-stableswap" -version = "7.0.0" +version = "7.1.0" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", @@ -11483,7 +11483,7 @@ dependencies = [ [[package]] name = "pallet-xyk" -version = "8.0.0" +version = "8.0.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -11510,7 +11510,7 @@ dependencies = [ [[package]] name = "pallet-xyk-liquidity-mining" -version = "1.6.1" +version = "1.6.2" dependencies = [ "frame-support", "frame-system", @@ -14763,7 +14763,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.61.0" +version = "1.62.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index e560fbe430..7f7cee6774 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.61.0" +version = "1.62.0" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/pallets/liquidity-mining/Cargo.toml b/pallets/liquidity-mining/Cargo.toml index fa0f080da0..c9800d4ee0 100644 --- a/pallets/liquidity-mining/Cargo.toml +++ b/pallets/liquidity-mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-liquidity-mining" -version = "4.4.8" +version = "4.4.9" description = "Liquidity mining" authors = ["GalacticCouncil"] edition = "2021" diff --git a/pallets/omnipool-liquidity-mining/Cargo.toml b/pallets/omnipool-liquidity-mining/Cargo.toml index d273e4f240..1de92440e5 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.7.1" +version = "2.8.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" @@ -14,7 +14,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] # parity scale-info = { workspace = true } -codec = {workspace = true } +codec = { workspace = true } log = { workspace = true } # local @@ -61,26 +61,26 @@ polkadot-xcm = { workspace = true } [features] default = ["std"] std = [ - "codec/std", - "scale-info/std", - "sp-runtime/std", - "sp-std/std", - "frame-support/std", - "frame-system/std", - "sp-core/std", - "sp-io/std", - "pallet-balances/std", - "orml-tokens/std", + "codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-io/std", + "pallet-balances/std", + "orml-tokens/std", "pallet-omnipool/std", - "pallet-ema-oracle/std", - "pallet-liquidity-mining/std", - "primitives/std", + "pallet-ema-oracle/std", + "pallet-liquidity-mining/std", + "primitives/std", "hydra-dx-math/std", ] runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "sp-core", - "sp-io", - "pallet-balances", + "frame-benchmarking/runtime-benchmarks", + "sp-core", + "sp-io", + "pallet-balances", ] -try-runtime = [ "frame-support/try-runtime" ] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/omnipool/Cargo.toml b/pallets/omnipool/Cargo.toml index db0cab23ad..70616c290d 100644 --- a/pallets/omnipool/Cargo.toml +++ b/pallets/omnipool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool" -version = "5.1.10" +version = "5.1.11" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/stableswap/Cargo.toml b/pallets/stableswap/Cargo.toml index c2b4524119..237d73b28b 100644 --- a/pallets/stableswap/Cargo.toml +++ b/pallets/stableswap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-stableswap" -version = "7.0.0" +version = "7.1.0" description = "AMM for correlated assets" authors = ["GalacticCouncil"] edition = "2021" @@ -52,7 +52,7 @@ orml-tokens = { workspace = true, features = ["std"] } proptest = { workspace = true } test-utils = { workspace = true } pretty_assertions = "1.4.0" -pallet-circuit-breaker = { workspace = true} +pallet-circuit-breaker = { workspace = true } [features] default = ["std"] diff --git a/pallets/xyk-liquidity-mining/Cargo.toml b/pallets/xyk-liquidity-mining/Cargo.toml index b72f407303..13c1290d3d 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.6.1" +version = "1.6.2" description = "Liquidity mining" authors = ["GalacticCouncil"] edition = "2021" @@ -46,20 +46,20 @@ test-utils = { workspace = true } [features] default = ["std"] std = [ - "codec/std", - "scale-info/std", - "sp-runtime/std", - "sp-std/std", - "frame-support/std", - "frame-system/std", - "sp-core/std", - "sp-io/std", - "pallet-balances/std", - "orml-tokens/std", - "pallet-xyk/std", - "pallet-liquidity-mining/std", - "primitives/std", - "hydradx-traits/std", - "log/std", + "codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-io/std", + "pallet-balances/std", + "orml-tokens/std", + "pallet-xyk/std", + "pallet-liquidity-mining/std", + "primitives/std", + "hydradx-traits/std", + "log/std", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/xyk/Cargo.toml b/pallets/xyk/Cargo.toml index 376d765bd3..f0328d397f 100644 --- a/pallets/xyk/Cargo.toml +++ b/pallets/xyk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-xyk" -version = "8.0.0" +version = "8.0.1" description = "XYK automated market maker" authors = ["GalacticCouncil"] edition = "2021" diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 2056d73b86..2e9ffaf1f3 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "375.0.0" +version = "376.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" @@ -475,7 +475,6 @@ try-runtime = [ "pallet-broadcast/try-runtime", "pallet-dispatcher/try-runtime", "pallet-signet/try-runtime", - ] metadata-hash = [ diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 41d8fb7442..8b07aa9b2b 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 375, + spec_version: 376, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 78e766b59a..44f695638e 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "4.4.2" +version = "4.5.0" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" From 19867b44f1f55c66a506eb153e0ad0aef35394db Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 15:12:39 +0100 Subject: [PATCH 11/29] renaming --- pallets/omnipool-liquidity-mining/src/lib.rs | 2 +- pallets/omnipool/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 121749a09f..c8fd0289a4 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1150,7 +1150,7 @@ pub mod pallet { let omnipool_shares_to_remove = omnipool_position.shares; let stable_pool_id = omnipool_position.asset_id; - let actual_stable_shares_received = OmnipoolPallet::::do_remove_liquidity_with_limit( + let actual_stable_shares_received = OmnipoolPallet::::do_remove_liquidity( origin.clone(), position_id, omnipool_shares_to_remove, diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index 52bea7cea4..8b716dc290 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -679,7 +679,7 @@ pub mod pallet { amount: Balance, min_limit: Balance, ) -> DispatchResult { - Self::do_remove_liquidity_with_limit(origin, position_id, amount, min_limit)?; + Self::do_remove_liquidity(origin, position_id, amount, min_limit)?; Ok(()) } @@ -2125,7 +2125,7 @@ impl Pallet { /// This is the core logic for liquidity removal, extracted to allow /// other pallets to call it and receive the transferred amount. #[require_transactional] - pub fn do_remove_liquidity_with_limit( + pub fn do_remove_liquidity( origin: OriginFor, position_id: T::PositionItemId, amount: Balance, From 7602c163d3866d1b8ba623763d4f80953546c97d Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 15:14:10 +0100 Subject: [PATCH 12/29] renaming --- pallets/omnipool-liquidity-mining/src/lib.rs | 4 ++-- pallets/omnipool/src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index c8fd0289a4..6fb3ccbaa9 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -995,7 +995,7 @@ pub mod pallet { let min_shares_limit = min_shares_limit.unwrap_or(Balance::MIN); let position_id = - OmnipoolPallet::::do_add_liquidity_with_limit(origin.clone(), asset, amount, min_shares_limit)?; + OmnipoolPallet::::do_add_liquidity(origin.clone(), asset, amount, min_shares_limit)?; Self::join_farms(origin, farm_entries, position_id)?; @@ -1071,7 +1071,7 @@ pub mod pallet { let stablepool_shares = T::Stableswap::add_liquidity(who, stable_pool_id, stable_asset_amounts.to_vec())?; let min_shares_limit = min_shares_limit.unwrap_or(Balance::MIN); - let position_id = OmnipoolPallet::::do_add_liquidity_with_limit( + let position_id = OmnipoolPallet::::do_add_liquidity( origin.clone(), stable_pool_id, stablepool_shares, diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index 8b716dc290..9fdec95235 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -613,7 +613,7 @@ pub mod pallet { amount: Balance, min_shares_limit: Balance, ) -> DispatchResult { - let _ = Self::do_add_liquidity_with_limit(origin, asset, amount, min_shares_limit)?; + let _ = Self::do_add_liquidity(origin, asset, amount, min_shares_limit)?; Ok(()) } @@ -1999,7 +1999,7 @@ impl Pallet { } #[require_transactional] - pub fn do_add_liquidity_with_limit( + pub fn do_add_liquidity( origin: OriginFor, asset: T::AssetId, amount: Balance, From c6c967265a53145a35f11d7a9c84293028d599aa Mon Sep 17 00:00:00 2001 From: dmoka Date: Mon, 5 Jan 2026 19:09:48 +0100 Subject: [PATCH 13/29] renaming --- pallets/omnipool-liquidity-mining/src/lib.rs | 4 ++-- pallets/omnipool-liquidity-mining/src/tests/mock.rs | 4 ++-- pallets/stableswap/src/lib.rs | 4 ++-- traits/src/stableswap.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 6fb3ccbaa9..5ff13adb01 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -61,7 +61,7 @@ use frame_system::{ }; use hydra_dx_math::ema::EmaPrice as Price; use hydradx_traits::stableswap::AssetAmount; -use hydradx_traits::stableswap::StableswapAddLiquidity; +use hydradx_traits::stableswap::StableswapLiquidityMutation; use hydradx_traits::{ liquidity_mining::{GlobalFarmId, Mutate as LiquidityMiningMutate, YieldFarmId}, oracle::{AggregatedPriceOracle, OraclePeriod, Source}, @@ -149,7 +149,7 @@ pub mod pallet { Period = PeriodOf, >; - type Stableswap: StableswapAddLiquidity; + type Stableswap: StableswapLiquidityMutation; /// Identifier of oracle data soruce #[pallet::constant] diff --git a/pallets/omnipool-liquidity-mining/src/tests/mock.rs b/pallets/omnipool-liquidity-mining/src/tests/mock.rs index 73e8091607..ea3a752ef8 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/mock.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/mock.rs @@ -48,7 +48,7 @@ use warehouse_liquidity_mining::{GlobalFarmData, Instance1}; use hydradx_traits::{ oracle::{OraclePeriod, Source}, pools::DustRemovalAccountWhitelist, - stableswap::StableswapAddLiquidity, + stableswap::StableswapLiquidityMutation, AssetKind, }; @@ -193,7 +193,7 @@ pub const SHARES_FROM_STABLESWAP: u128 = 5 * ONE; pub const STABLESWAP_POOL_ID: u32 = 72; pub struct StableswapAddLiquidityStub; -impl StableswapAddLiquidity for StableswapAddLiquidityStub { +impl StableswapLiquidityMutation for StableswapAddLiquidityStub { fn add_liquidity( _who: AccountId, _pool_id: AssetId, diff --git a/pallets/stableswap/src/lib.rs b/pallets/stableswap/src/lib.rs index 4abcd591b2..7bd5f912ff 100644 --- a/pallets/stableswap/src/lib.rs +++ b/pallets/stableswap/src/lib.rs @@ -62,7 +62,7 @@ use frame_support::pallet_prelude::{DispatchResult, Get}; use frame_support::{ensure, require_transactional, transactional, BoundedVec, PalletId}; use frame_system::ensure_signed; use frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; -use hydradx_traits::{registry::Inspect, stableswap::StableswapAddLiquidity, AccountIdFor}; +use hydradx_traits::{registry::Inspect, stableswap::StableswapLiquidityMutation, AccountIdFor}; use num_traits::zero; pub use pallet::*; use sp_runtime::traits::{AccountIdConversion, BlockNumberProvider, Zero}; @@ -2062,7 +2062,7 @@ impl Pallet { } } -impl StableswapAddLiquidity for Pallet { +impl StableswapLiquidityMutation for Pallet { fn add_liquidity( who: T::AccountId, pool_id: T::AssetId, diff --git a/traits/src/stableswap.rs b/traits/src/stableswap.rs index a7d52d6f1c..1abe5a000b 100644 --- a/traits/src/stableswap.rs +++ b/traits/src/stableswap.rs @@ -3,7 +3,7 @@ use frame_support::__private::DispatchError; use frame_support::pallet_prelude::TypeInfo; use sp_std::vec::Vec; -pub trait StableswapAddLiquidity { +pub trait StableswapLiquidityMutation { fn add_liquidity( who: AccountId, pool_id: AssetId, From 23fcc2a4ae4e5dc2be277a9df8a76b6c62487dee Mon Sep 17 00:00:00 2001 From: dmoka Date: Wed, 7 Jan 2026 10:45:40 +0100 Subject: [PATCH 14/29] bump runtime --- Cargo.lock | 16 ++++++++-------- runtime/hydradx/Cargo.toml | 2 +- runtime/hydradx/src/lib.rs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 122fb9bcf0..35b8cde326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "377.0.0" +version = "378.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -5764,7 +5764,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "4.4.3" +version = "4.5.0" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -10071,7 +10071,7 @@ dependencies = [ [[package]] name = "pallet-liquidity-mining" -version = "4.4.8" +version = "4.4.9" dependencies = [ "fixed", "frame-support", @@ -10422,7 +10422,7 @@ dependencies = [ [[package]] name = "pallet-omnipool-liquidity-mining" -version = "2.7.1" +version = "2.8.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -10993,7 +10993,7 @@ dependencies = [ [[package]] name = "pallet-stableswap" -version = "7.0.0" +version = "7.1.0" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", @@ -11483,7 +11483,7 @@ dependencies = [ [[package]] name = "pallet-xyk" -version = "8.0.0" +version = "8.0.1" dependencies = [ "frame-benchmarking", "frame-support", @@ -11510,7 +11510,7 @@ dependencies = [ [[package]] name = "pallet-xyk-liquidity-mining" -version = "1.6.1" +version = "1.6.2" dependencies = [ "frame-support", "frame-system", @@ -14763,7 +14763,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "runtime-integration-tests" -version = "1.61.1" +version = "1.62.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index d950ab61b6..45a8b42450 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "377.0.0" +version = "378.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 2a6c63af37..8b1a8f8ad4 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 377, + spec_version: 378, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 641b1149dca76188903b0cd58e7809a3dc04c818 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 15:55:29 +0100 Subject: [PATCH 15/29] bump major version of omnipool lm pallet as breaking change --- Cargo.lock | 2 +- pallets/omnipool-liquidity-mining/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a2e2ab804..957d79847c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10422,7 +10422,7 @@ dependencies = [ [[package]] name = "pallet-omnipool-liquidity-mining" -version = "2.8.0" +version = "3.0.0" dependencies = [ "frame-benchmarking", "frame-support", diff --git a/pallets/omnipool-liquidity-mining/Cargo.toml b/pallets/omnipool-liquidity-mining/Cargo.toml index 1de92440e5..0119f895ff 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.8.0" +version = "3.0.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" From 88ddbf2a25d6abacf16ddb41dfeebd6656121b2b Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:17:53 +0100 Subject: [PATCH 16/29] bump main version as added new feature --- Cargo.lock | 2 +- pallets/liquidity-mining/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 957d79847c..d9eaf7dfc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10071,7 +10071,7 @@ dependencies = [ [[package]] name = "pallet-liquidity-mining" -version = "4.4.9" +version = "4.5.0" dependencies = [ "fixed", "frame-support", diff --git a/pallets/liquidity-mining/Cargo.toml b/pallets/liquidity-mining/Cargo.toml index c9800d4ee0..cb042826c6 100644 --- a/pallets/liquidity-mining/Cargo.toml +++ b/pallets/liquidity-mining/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-liquidity-mining" -version = "4.4.9" +version = "4.5.0" description = "Liquidity mining" authors = ["GalacticCouncil"] edition = "2021" From 5eefdd30ab90ea836dd4a4280d95469ac3431a4f Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:18:44 +0100 Subject: [PATCH 17/29] remove unused import --- pallets/omnipool-liquidity-mining/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 5ff13adb01..1bc12e378d 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -87,7 +87,6 @@ type PeriodOf = BlockNumberFor; pub mod pallet { use super::*; use frame_support::pallet_prelude::*; - use primitives::ItemId; #[pallet::pallet] pub struct Pallet(_); @@ -994,8 +993,7 @@ pub mod pallet { ensure!(!farm_entries.is_empty(), Error::::NoFarmEntriesSpecified); let min_shares_limit = min_shares_limit.unwrap_or(Balance::MIN); - let position_id = - OmnipoolPallet::::do_add_liquidity(origin.clone(), asset, amount, min_shares_limit)?; + let position_id = OmnipoolPallet::::do_add_liquidity(origin.clone(), asset, amount, min_shares_limit)?; Self::join_farms(origin, farm_entries, position_id)?; From 8f72a5c839f1afbc25fe743c3db0eea794af7c00 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:33:11 +0100 Subject: [PATCH 18/29] use dedicated error for position id mismatch --- pallets/omnipool-liquidity-mining/src/lib.rs | 5 ++++- .../remove_liquidity_stableswap_omnipool_and_exit_farms.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 1bc12e378d..1039ad86a2 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -321,6 +321,9 @@ pub mod pallet { /// No assets specified in the withdrawal NoAssetsSpecified, + + /// The provided position_id does not match the deposit's associated position. + PositionIdMismatch, } //NOTE: these errors should never happen. @@ -1130,7 +1133,7 @@ pub mod pallet { .ok_or(Error::::InconsistentState(InconsistentStateError::MissingLpPosition))?; ensure!( stored_position_id == position_id, - Error::::InconsistentState(InconsistentStateError::MissingLpPosition) + Error::::PositionIdMismatch ); let yield_farm_ids: BoundedVec = diff --git a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs index abfc1bf6ed..2193f55a85 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs @@ -688,7 +688,7 @@ fn should_fail_with_mismatched_deposit_and_position() { vec![AssetAmount::new(USDT, 1)].try_into().unwrap(), Some(deposit_2), // WRONG deposit for position_1 ), - Error::::InconsistentState(InconsistentStateError::MissingLpPosition) + Error::::PositionIdMismatch ); }); } From 2fdd503c7b46bc2fb5933a47d5a24ae9d7363cb4 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:39:33 +0100 Subject: [PATCH 19/29] remove unnecessary clones --- pallets/omnipool-liquidity-mining/src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 1039ad86a2..0d86198170 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1131,10 +1131,7 @@ pub mod pallet { let stored_position_id = OmniPositionId::::get(deposit_id) .ok_or(Error::::InconsistentState(InconsistentStateError::MissingLpPosition))?; - ensure!( - stored_position_id == position_id, - Error::::PositionIdMismatch - ); + ensure!(stored_position_id == position_id, Error::::PositionIdMismatch); let yield_farm_ids: BoundedVec = T::LiquidityMiningHandler::get_yield_farm_ids(deposit_id) @@ -1152,7 +1149,7 @@ pub mod pallet { let stable_pool_id = omnipool_position.asset_id; let actual_stable_shares_received = OmnipoolPallet::::do_remove_liquidity( - origin.clone(), + origin, position_id, omnipool_shares_to_remove, omnipool_min_limit, @@ -1162,7 +1159,7 @@ pub mod pallet { let asset_amount = &stableswap_min_amounts_out[0]; T::Stableswap::remove_liquidity_one_asset( - who.clone(), + who, stable_pool_id, asset_amount.asset_id, actual_stable_shares_received, From 2f662cd883900314b4a9e93d853b8d00c3f5b1ee Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:44:59 +0100 Subject: [PATCH 20/29] refactor to optimize call a bit --- pallets/omnipool-liquidity-mining/src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/lib.rs b/pallets/omnipool-liquidity-mining/src/lib.rs index 0d86198170..9538fa5ee7 100644 --- a/pallets/omnipool-liquidity-mining/src/lib.rs +++ b/pallets/omnipool-liquidity-mining/src/lib.rs @@ -1123,11 +1123,10 @@ pub mod pallet { stableswap_min_amounts_out: BoundedVec, ConstU32>, deposit_id: Option, ) -> DispatchResult { - let who = ensure_signed(origin.clone())?; ensure!(!stableswap_min_amounts_out.is_empty(), Error::::NoAssetsSpecified); - if let Some(deposit_id) = deposit_id { - Self::ensure_nft_owner(origin.clone(), deposit_id)?; + let who = if let Some(deposit_id) = deposit_id { + let who = Self::ensure_nft_owner(origin.clone(), deposit_id)?; let stored_position_id = OmniPositionId::::get(deposit_id) .ok_or(Error::::InconsistentState(InconsistentStateError::MissingLpPosition))?; @@ -1142,7 +1141,11 @@ pub mod pallet { .map_err(|_| Error::::InconsistentState(InconsistentStateError::DepositDataNotFound))?; Self::exit_farms(origin.clone(), deposit_id, yield_farm_ids)?; - } + + who + } else { + ensure_signed(origin.clone())? + }; let omnipool_position = OmnipoolPallet::::load_position(position_id, who.clone())?; let omnipool_shares_to_remove = omnipool_position.shares; From 80800bcb23274af9c0f9c56833fb8eac167facc8 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:46:38 +0100 Subject: [PATCH 21/29] revert xyk pallet version change --- pallets/xyk/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/xyk/Cargo.toml b/pallets/xyk/Cargo.toml index f0328d397f..376d765bd3 100644 --- a/pallets/xyk/Cargo.toml +++ b/pallets/xyk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-xyk" -version = "8.0.1" +version = "8.0.0" description = "XYK automated market maker" authors = ["GalacticCouncil"] edition = "2021" From 831ed827ce450d8d09ee14ab87c2c3d290b1c74b Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:50:28 +0100 Subject: [PATCH 22/29] update lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d9eaf7dfc7..2e79aa1b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11483,7 +11483,7 @@ dependencies = [ [[package]] name = "pallet-xyk" -version = "8.0.1" +version = "8.0.0" dependencies = [ "frame-benchmarking", "frame-support", From 1cce59d8091f078a8f6ef773646a3745be52b453 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:52:32 +0100 Subject: [PATCH 23/29] fix test for generate rewards for user so we can test if claim works --- ...dity_stableswap_omnipool_and_exit_farms.rs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs index 2193f55a85..8f7e495820 100644 --- a/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs +++ b/pallets/omnipool-liquidity-mining/src/tests/remove_liquidity_stableswap_omnipool_and_exit_farms.rs @@ -502,8 +502,8 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa let position_id = crate::OmniPositionId::::get(deposit_id).expect("Position should be mapped to deposit"); - // Wait some blocks to accumulate rewards - set_block_number(100); + // Wait enough blocks to accumulate rewards + set_block_number(1_000); let hdx_balance_before = Tokens::free_balance(HDX, &LP1); @@ -530,23 +530,22 @@ fn remove_liquidity_stableswap_omnipool_and_exit_farms_full_round_trip_with_rewa ); let hdx_balance_after = Tokens::free_balance(HDX, &LP1); + let expected_claimed_rewards = 243_506_250_u128; - // Verify user received HDX rewards (if any were generated) - // Note: rewards might be 0 in test depending on block time configuration - if hdx_balance_after > hdx_balance_before { - // Rewards were claimed - assert!(has_event( - crate::Event::RewardClaimed { - global_farm_id: gc_g_farm_id, - yield_farm_id: gc_y_farm_id, - who: LP1, - claimed: hdx_balance_after - hdx_balance_before, - reward_currency: HDX, - deposit_id, - } - .into() - )); - } + // Verify user received HDX rewards + assert_eq!(hdx_balance_after - hdx_balance_before, expected_claimed_rewards); + + assert!(has_event( + crate::Event::RewardClaimed { + global_farm_id: gc_g_farm_id, + yield_farm_id: gc_y_farm_id, + who: LP1, + claimed: expected_claimed_rewards, + reward_currency: HDX, + deposit_id, + } + .into() + )); // Verify all storage is cleaned up assert_eq!(crate::OmniPositionId::::get(deposit_id), None); From 8577619131f078d487a93c5ff1c30e7285aa4a51 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 16:54:33 +0100 Subject: [PATCH 24/29] bump patch to make CI happy --- pallets/xyk/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/xyk/Cargo.toml b/pallets/xyk/Cargo.toml index 376d765bd3..f0328d397f 100644 --- a/pallets/xyk/Cargo.toml +++ b/pallets/xyk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-xyk" -version = "8.0.0" +version = "8.0.1" description = "XYK automated market maker" authors = ["GalacticCouncil"] edition = "2021" From 5588bc17a9c5f21ca3914c4b6770024cbfc2d942 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 17:03:01 +0100 Subject: [PATCH 25/29] revert version change --- pallets/xyk/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/xyk/Cargo.toml b/pallets/xyk/Cargo.toml index f0328d397f..376d765bd3 100644 --- a/pallets/xyk/Cargo.toml +++ b/pallets/xyk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-xyk" -version = "8.0.1" +version = "8.0.0" description = "XYK automated market maker" authors = ["GalacticCouncil"] edition = "2021" From fda754087f2588eb13f77138c83f9a7f8e2595fe Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 17:03:03 +0100 Subject: [PATCH 26/29] revert version change --- runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs index ef91b56a2e..05beead1bb 100644 --- a/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs +++ b/runtime/hydradx/src/benchmarking/omnipool_liquidity_mining.rs @@ -1059,6 +1059,9 @@ runtime_benchmarks! { }; OmnipoolLiquidityMining::add_liquidity_stableswap_omnipool_and_join_farms(RawOrigin::Signed(lp_provider.clone()).into(),pool_id, added_liquidity.try_into().unwrap(), farms_to_join, None).unwrap(); + // Advance period to accumulate rewards for worst case (claiming rewards on exit) + set_period(400); + let asset_ids: Vec> = Stableswap::pools(pool_id) .into_iter() .flat_map(|pool_info| pool_info.assets.into_iter()) From c05c347772620d4d66a37db3ddcecc4396cf945b Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 17:10:51 +0100 Subject: [PATCH 27/29] make benchmark the worst case --- .../pallet_omnipool_liquidity_mining.rs | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs b/runtime/hydradx/src/weights/pallet_omnipool_liquidity_mining.rs index 37539832d4..8991b85244 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 43.0.0 -//! DATE: 2025-12-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-01-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `Mac.chello.hu`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -74,8 +74,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `2262` // Estimated: `6196` - // Minimum execution time: 76_000_000 picoseconds. - Weight::from_parts(78_000_000, 6196) + // Minimum execution time: 75_000_000 picoseconds. + Weight::from_parts(88_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -91,7 +91,7 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `6812` // Estimated: `6328` - // Minimum execution time: 113_000_000 picoseconds. + // Minimum execution time: 109_000_000 picoseconds. Weight::from_parts(115_000_000, 6328) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) @@ -106,8 +106,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `5901` // Estimated: `6196` - // Minimum execution time: 84_000_000 picoseconds. - Weight::from_parts(91_000_000, 6196) + // Minimum execution time: 82_000_000 picoseconds. + Weight::from_parts(85_000_000, 6196) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -131,8 +131,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `7341` // Estimated: `6328` - // Minimum execution time: 140_000_000 picoseconds. - Weight::from_parts(152_000_000, 6328) + // Minimum execution time: 123_000_000 picoseconds. + Weight::from_parts(135_000_000, 6328) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -154,8 +154,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `7847` // Estimated: `6328` - // Minimum execution time: 130_000_000 picoseconds. - Weight::from_parts(140_000_000, 6328) + // Minimum execution time: 126_000_000 picoseconds. + Weight::from_parts(132_000_000, 6328) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -175,8 +175,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `7387` // Estimated: `6328` - // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(142_000_000, 6328) + // Minimum execution time: 118_000_000 picoseconds. + Weight::from_parts(122_000_000, 6328) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -198,8 +198,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `7964` // Estimated: `6328` - // Minimum execution time: 129_000_000 picoseconds. - Weight::from_parts(145_000_000, 6328) + // Minimum execution time: 124_000_000 picoseconds. + Weight::from_parts(132_000_000, 6328) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -215,8 +215,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `6336` // Estimated: `6196` - // Minimum execution time: 85_000_000 picoseconds. - Weight::from_parts(87_000_000, 6196) + // Minimum execution time: 79_000_000 picoseconds. + Weight::from_parts(82_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -254,8 +254,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `10771` // Estimated: `11666` - // Minimum execution time: 204_000_000 picoseconds. - Weight::from_parts(208_000_000, 11666) + // Minimum execution time: 191_000_000 picoseconds. + Weight::from_parts(200_000_000, 11666) .saturating_add(T::DbWeight::get().reads(17_u64)) .saturating_add(T::DbWeight::get().writes(14_u64)) } @@ -283,8 +283,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `13773` // Estimated: `11666` - // Minimum execution time: 178_000_000 picoseconds. - Weight::from_parts(195_000_000, 11666) + // Minimum execution time: 167_000_000 picoseconds. + Weight::from_parts(175_000_000, 11666) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -306,8 +306,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `10952` // Estimated: `8799` - // Minimum execution time: 174_000_000 picoseconds. - Weight::from_parts(180_000_000, 8799) + // Minimum execution time: 164_000_000 picoseconds. + Weight::from_parts(172_000_000, 8799) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) } @@ -339,8 +339,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `9441` // Estimated: `8799` - // Minimum execution time: 245_000_000 picoseconds. - Weight::from_parts(256_000_000, 8799) + // Minimum execution time: 230_000_000 picoseconds. + Weight::from_parts(240_000_000, 8799) .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().writes(15_u64)) } @@ -379,10 +379,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `19019 + c * (507 ±0)` // Estimated: `11666 + c * (2680 ±0)` - // Minimum execution time: 203_000_000 picoseconds. - Weight::from_parts(144_844_392, 11666) - // Standard Error: 280_673 - .saturating_add(Weight::from_parts(81_133_177, 0).saturating_mul(c.into())) + // Minimum execution time: 195_000_000 picoseconds. + Weight::from_parts(133_414_135, 11666) + // Standard Error: 291_736 + .saturating_add(Weight::from_parts(79_817_406, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(14_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(11_u64)) @@ -458,10 +458,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `20554 + c * (507 ±0)` // Estimated: `14335 + c * (2680 ±0)` - // Minimum execution time: 432_000_000 picoseconds. - Weight::from_parts(397_502_278, 14335) - // Standard Error: 593_093 - .saturating_add(Weight::from_parts(83_494_334, 0).saturating_mul(c.into())) + // Minimum execution time: 397_000_000 picoseconds. + Weight::from_parts(369_734_637, 14335) + // Standard Error: 597_933 + .saturating_add(Weight::from_parts(84_366_413, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(40_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(26_u64)) @@ -497,10 +497,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `11464 + c * (518 ±0)` // Estimated: `6328 + c * (2680 ±0)` - // Minimum execution time: 201_000_000 picoseconds. - Weight::from_parts(105_905_081, 6328) - // Standard Error: 516_125 - .saturating_add(Weight::from_parts(118_452_745, 0).saturating_mul(c.into())) + // Minimum execution time: 205_000_000 picoseconds. + Weight::from_parts(102_603_212, 6328) + // Standard Error: 395_779 + .saturating_add(Weight::from_parts(124_297_137, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(10_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(2_u64)) @@ -580,10 +580,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `23192 + c * (507 ±0)` // Estimated: `34569 + c * (2680 ±0)` - // Minimum execution time: 1_148_000_000 picoseconds. - Weight::from_parts(1_087_377_102, 34569) - // Standard Error: 451_081 - .saturating_add(Weight::from_parts(78_431_308, 0).saturating_mul(c.into())) + // Minimum execution time: 1_069_000_000 picoseconds. + Weight::from_parts(1_097_806_483, 34569) + // Standard Error: 960_157 + .saturating_add(Weight::from_parts(98_206_950, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(67_u64)) .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) .saturating_add(T::DbWeight::get().writes(37_u64)) @@ -604,8 +604,10 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `OmnipoolWarehouseLM::GlobalFarm` (`max_values`: None, `max_size`: Some(205), added: 2680, mode: `MaxEncodedLen`) /// Storage: `AssetRegistry::Assets` (r:8 w:0) /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:3 w:0) + /// Storage: `System::Account` (r:9 w:7) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `EmaOracle::Oracles` (r:4 w:0) + /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) /// Storage: `Uniques::Class` (r:2 w:2) /// Proof: `Uniques::Class` (`max_values`: None, `max_size`: Some(190), added: 2665, mode: `MaxEncodedLen`) /// Storage: `Omnipool::Assets` (r:1 w:1) @@ -614,10 +616,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) /// Storage: `DynamicFees::AssetFeeConfiguration` (r:1 w:0) /// Proof: `DynamicFees::AssetFeeConfiguration` (`max_values`: None, `max_size`: Some(93), added: 2568, mode: `MaxEncodedLen`) - /// Storage: `DynamicFees::AssetFee` (r:1 w:0) + /// Storage: `DynamicFees::AssetFee` (r:1 w:1) /// Proof: `DynamicFees::AssetFee` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) - /// Storage: `EmaOracle::Oracles` (r:2 w:0) - /// Proof: `EmaOracle::Oracles` (`max_values`: None, `max_size`: Some(194), added: 2669, mode: `MaxEncodedLen`) /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `HSM::FlashMinter` (r:1 w:0) @@ -649,17 +649,17 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H /// The range of component `c` is `[0, 1]`. fn remove_liquidity_stableswap_omnipool_and_exit_farms(c: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `25250 + c * (5724 ±0)` - // Estimated: `34569 + c * (7195 ±0)` - // Minimum execution time: 571_000_000 picoseconds. - Weight::from_parts(626_842_857, 34569) - // Standard Error: 2_082_005 - .saturating_add(Weight::from_parts(261_557_142, 0).saturating_mul(c.into())) + // Measured: `24282 + c * (7204 ±0)` + // Estimated: `34569 + c * (18221 ±0)` + // Minimum execution time: 614_000_000 picoseconds. + Weight::from_parts(708_491_836, 34569) + // Standard Error: 3_101_253 + .saturating_add(Weight::from_parts(716_208_163, 0).saturating_mul(c.into())) .saturating_add(T::DbWeight::get().reads(53_u64)) - .saturating_add(T::DbWeight::get().reads((16_u64).saturating_mul(c.into()))) - .saturating_add(T::DbWeight::get().writes(22_u64)) - .saturating_add(T::DbWeight::get().writes((17_u64).saturating_mul(c.into()))) - .saturating_add(Weight::from_parts(0, 7195).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads((24_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(23_u64)) + .saturating_add(T::DbWeight::get().writes((24_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 18221).saturating_mul(c.into())) } /// Storage: `AssetRegistry::Assets` (r:1 w:0) /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) @@ -671,8 +671,8 @@ impl pallet_omnipool_liquidity_mining::WeightInfo for H // Proof Size summary in bytes: // Measured: `5736` // Estimated: `51701` - // Minimum execution time: 120_000_000 picoseconds. - Weight::from_parts(122_000_000, 51701) + // Minimum execution time: 121_000_000 picoseconds. + Weight::from_parts(125_000_000, 51701) .saturating_add(T::DbWeight::get().reads(21_u64)) } } \ No newline at end of file From 9ac4e1db3ef1fe0f24eab61e858e5954011d5a16 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 17:34:30 +0100 Subject: [PATCH 28/29] fix package pallet build --- Cargo.lock | 2 +- pallets/stableswap/Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e79aa1b9c..3832fff51f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10993,7 +10993,7 @@ dependencies = [ [[package]] name = "pallet-stableswap" -version = "7.1.0" +version = "7.0.0" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", diff --git a/pallets/stableswap/Cargo.toml b/pallets/stableswap/Cargo.toml index 237d73b28b..fd907b81a5 100644 --- a/pallets/stableswap/Cargo.toml +++ b/pallets/stableswap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-stableswap" -version = "7.1.0" +version = "7.0.0" description = "AMM for correlated assets" authors = ["GalacticCouncil"] edition = "2021" @@ -78,6 +78,7 @@ std = [ "frame-benchmarking/std", "orml-traits/std", "hydra-dx-math/std", + "hydradx-traits/std", "pallet-broadcast/std", ] try-runtime = ["frame-support/try-runtime"] From c5848a0a97e2bdba3045eba42774d82695188398 Mon Sep 17 00:00:00 2001 From: dmoka Date: Thu, 8 Jan 2026 18:16:51 +0100 Subject: [PATCH 29/29] bump versions --- Cargo.lock | 6 +++--- pallets/omnipool/Cargo.toml | 2 +- pallets/stableswap/Cargo.toml | 2 +- runtime/hydradx/Cargo.toml | 2 +- runtime/hydradx/src/lib.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3832fff51f..a3ebd68230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5598,7 +5598,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "378.0.0" +version = "379.0.0" dependencies = [ "alloy-primitives 0.7.7", "alloy-sol-types 0.7.7", @@ -10393,7 +10393,7 @@ dependencies = [ [[package]] name = "pallet-omnipool" -version = "5.1.11" +version = "5.2.0" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", @@ -10993,7 +10993,7 @@ dependencies = [ [[package]] name = "pallet-stableswap" -version = "7.0.0" +version = "7.1.0" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", diff --git a/pallets/omnipool/Cargo.toml b/pallets/omnipool/Cargo.toml index 70616c290d..4fa0b3fa93 100644 --- a/pallets/omnipool/Cargo.toml +++ b/pallets/omnipool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool" -version = "5.1.11" +version = "5.2.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/stableswap/Cargo.toml b/pallets/stableswap/Cargo.toml index fd907b81a5..9132621da8 100644 --- a/pallets/stableswap/Cargo.toml +++ b/pallets/stableswap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-stableswap" -version = "7.0.0" +version = "7.1.0" description = "AMM for correlated assets" authors = ["GalacticCouncil"] edition = "2021" diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 45a8b42450..a714dfce11 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "378.0.0" +version = "379.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 8b1a8f8ad4..36ce940340 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -122,7 +122,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 378, + spec_version: 379, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1,