diff --git a/Cargo.lock b/Cargo.lock index 331e07e7b..601b0f580 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7403,7 +7403,7 @@ dependencies = [ [[package]] name = "pallet-dca" -version = "1.3.6" +version = "1.4.0" dependencies = [ "cumulus-pallet-parachain-system", "cumulus-primitives-core", diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index a9eb1df6b..19ceff68f 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -19,6 +19,7 @@ use hydradx_traits::router::Trade; use orml_traits::MultiCurrency; use orml_traits::MultiReservableCurrency; use pallet_dca::types::{Order, Schedule}; +use pallet_omnipool::types::Tradability; use pallet_stableswap::types::AssetAmount; use pallet_stableswap::MAX_ASSETS_IN_POOL; use primitives::{AssetId, Balance}; @@ -495,6 +496,85 @@ mod omnipool { }); } + #[test] + fn sell_schedule_be_retried_when_route_is_invalid() { + TestNet::reset(); + Hydra::execute_with(|| { + //Arrange + init_omnipool_with_oracle_for_block_10(); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + BOB.into(), + ETH, + 1_000 * UNITS as i128, + )); + let position_id = hydradx_runtime::Omnipool::next_position_id(); + + assert_ok!(hydradx_runtime::Omnipool::add_token( + hydradx_runtime::RuntimeOrigin::root(), + ETH, + FixedU128::from_rational(3, 10), + Permill::from_percent(60), + BOB.into(), + )); + + polkadot_run_to_block(11); + + let alice_init_hdx_balance = 5000 * UNITS; + assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + ALICE.into(), + alice_init_hdx_balance, + )); + + let dca_budget = 1100 * UNITS; + let amount_to_sell = 100 * UNITS; + let schedule1 = + schedule_fake_with_sell_order(ALICE, PoolType::Omnipool, dca_budget, HDX, ETH, amount_to_sell); + create_schedule(ALICE, schedule1); + + assert_balance!(ALICE.into(), HDX, alice_init_hdx_balance - dca_budget); + assert_balance!(ALICE.into(), DAI, ALICE_INITIAL_DAI_BALANCE); + assert_reserved_balance!(&ALICE.into(), HDX, dca_budget); + assert_balance!(&Treasury::account_id(), HDX, TREASURY_ACCOUNT_INIT_BALANCE); + + //Act + //Remove ETH token resulting invalid route + + assert_ok!(hydradx_runtime::Omnipool::set_asset_tradable_state( + hydradx_runtime::RuntimeOrigin::root(), + ETH, + Tradability::ADD_LIQUIDITY | Tradability::REMOVE_LIQUIDITY + )); + let position = + pallet_omnipool::Pallet::::load_position(position_id, BOB.into()).unwrap(); + assert_ok!(hydradx_runtime::Omnipool::remove_liquidity( + hydradx_runtime::RuntimeOrigin::signed(BOB.into()), + position_id, + position.shares, + )); + + assert_ok!(hydradx_runtime::Omnipool::set_asset_tradable_state( + hydradx_runtime::RuntimeOrigin::root(), + ETH, + Tradability::FROZEN + )); + assert_ok!(hydradx_runtime::Omnipool::remove_token( + hydradx_runtime::RuntimeOrigin::root(), + ETH, + BOB.into(), + )); + polkadot_run_to_block(12); + + //Assert + let schedule_id = 0; + let schedule = DCA::schedules(schedule_id); + assert!(schedule.is_some()); + assert_eq!(DCA::retries_on_error(schedule_id), 1); + }); + } + #[test] fn sell_schedule_should_sell_remaining_in_next_trade_when_there_is_not_enough_left() { TestNet::reset(); diff --git a/pallets/dca/Cargo.toml b/pallets/dca/Cargo.toml index 7c4401e02..6a379fcc5 100644 --- a/pallets/dca/Cargo.toml +++ b/pallets/dca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-dca' -version = "1.3.6" +version = "1.4.0" description = 'A pallet to manage DCA scheduling' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/dca/src/lib.rs b/pallets/dca/src/lib.rs index c13f1bff6..482f61e00 100644 --- a/pallets/dca/src/lib.rs +++ b/pallets/dca/src/lib.rs @@ -115,6 +115,7 @@ pub const FEE_MULTIPLIER_FOR_MIN_TRADE_LIMIT: Balance = 20; #[frame_support::pallet] pub mod pallet { use super::*; + use frame_support::traits::Contains; use frame_support::weights::WeightToFee; @@ -184,6 +185,7 @@ pub mod pallet { if error != Error::::TradeLimitReached.into() && error != Error::::SlippageLimitReached.into() + && !T::RetryOnError::contains(&error) { Self::terminate_schedule(schedule_id, &schedule, error); } else if let Err(retry_error) = @@ -242,6 +244,9 @@ pub mod pallet { ///Spot price provider to get the current price between two asset type RouteProvider: RouteProvider; + ///Errors we want to explicitly retry on, in case of failing DCA + type RetryOnError: Contains; + ///Max price difference allowed between blocks #[pallet::constant] type MaxPriceDifferenceBetweenBlocks: Get; diff --git a/pallets/dca/src/tests/mock.rs b/pallets/dca/src/tests/mock.rs index b412d1644..98fd8ddf9 100644 --- a/pallets/dca/src/tests/mock.rs +++ b/pallets/dca/src/tests/mock.rs @@ -690,6 +690,7 @@ impl Config for Test { type AmmTradeWeights = (); type MinimumTradingLimit = MinTradeAmount; type NativePriceOracle = NativePriceOracleMock; + type RetryOnError = (); } pub struct NativePriceOracleMock; diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index 2692694ae..65979704a 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -667,6 +667,18 @@ parameter_types! { } +pub struct RetryOnErrorForDca; + +impl Contains for RetryOnErrorForDca { + fn contains(t: &DispatchError) -> bool { + let errors: Vec = vec![ + pallet_omnipool::Error::::AssetNotFound.into(), + pallet_omnipool::Error::::NotAllowed.into(), + ]; + errors.contains(t) + } +} + impl pallet_dca::Config for Runtime { type RuntimeEvent = RuntimeEvent; type AssetId = AssetId; @@ -712,6 +724,7 @@ impl pallet_dca::Config for Runtime { MultiTransactionPayment, DCAOraclePeriod, >; + type RetryOnError = RetryOnErrorForDca; } // Provides weight info for the router. Router extrinsics can be executed with different AMMs, so we split the router weights into two parts: