diff --git a/Cargo.lock b/Cargo.lock index 12035b11c..d9baf8680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5073,7 +5073,7 @@ dependencies = [ [[package]] name = "hydradx-traits" -version = "3.4.0" +version = "3.5.0" dependencies = [ "frame-support", "impl-trait-for-tuples", @@ -8936,7 +8936,7 @@ dependencies = [ [[package]] name = "pallet-route-executor" -version = "2.5.1" +version = "2.6.0" dependencies = [ "frame-benchmarking", "frame-support", @@ -11929,7 +11929,7 @@ dependencies = [ [[package]] name = "runtime-integration-tests" -version = "1.23.2" +version = "1.23.3" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 15b8ee223..2d816ba0a 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.23.2" +version = "1.23.3" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" diff --git a/integration-tests/src/router.rs b/integration-tests/src/router.rs index d6eba45cc..b69445dc2 100644 --- a/integration-tests/src/router.rs +++ b/integration-tests/src/router.rs @@ -1865,7 +1865,7 @@ mod omnipool_router_tests { } #[test] - fn sell_should_with_selling_nonnaitve_when_account_providers_increases_during_trade() { + fn sell_should_work_with_selling_nonnaitve_when_account_providers_increases_during_trade() { TestNet::reset(); Hydra::execute_with(|| { @@ -5293,6 +5293,142 @@ mod route_spot_price { } } +mod sell_all { + use super::*; + use hydradx_runtime::Currencies; + use hydradx_traits::router::PoolType; + + #[test] + fn sell_should_sell_all_user_native_balance() { + TestNet::reset(); + + let limit = 0; + let amount_out = 26577363534770086553; + + Hydra::execute_with(|| { + let bob_hdx_balance = Currencies::free_balance(HDX, &BOB.into()); + + //Arrange + init_omnipool(); + + let trades = vec![Trade { + pool: PoolType::Omnipool, + asset_in: HDX, + asset_out: DAI, + }]; + + //Act + assert_ok!(Router::sell_all( + RuntimeOrigin::signed(BOB.into()), + HDX, + DAI, + limit, + trades + )); + + //Assert + assert_balance!(BOB.into(), HDX, 0); + + expect_hydra_last_events(vec![pallet_route_executor::Event::Executed { + asset_in: HDX, + asset_out: DAI, + amount_in: bob_hdx_balance, + amount_out, + } + .into()]); + }); + } + + #[test] + fn sell_all_should_sell_all_user_nonnative_balance() { + TestNet::reset(); + + let limit = 0; + let amount_out = 35227901268414708; + + Hydra::execute_with(|| { + let bob_nonnative_balance = Currencies::free_balance(DAI, &BOB.into()); + + //Arrange + init_omnipool(); + + let trades = vec![Trade { + pool: PoolType::Omnipool, + asset_in: DAI, + asset_out: HDX, + }]; + + //Act + assert_ok!(Router::sell_all( + RuntimeOrigin::signed(BOB.into()), + DAI, + HDX, + limit, + trades + )); + + //Assert + assert_balance!(BOB.into(), DAI, 0); + + expect_hydra_last_events(vec![pallet_route_executor::Event::Executed { + asset_in: DAI, + asset_out: HDX, + amount_in: bob_nonnative_balance, + amount_out, + } + .into()]); + }); + } + + #[test] + fn sell_all_should_work_when_selling_all_nonnative_in_stableswap() { + TestNet::reset(); + + Hydra::execute_with(|| { + let _ = with_transaction(|| { + //Arrange + let (pool_id, stable_asset_1, _) = init_stableswap().unwrap(); + + init_omnipool(); + + let init_balance = 3000 * UNITS + 1; + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + ALICE.into(), + stable_asset_1, + init_balance as i128, + )); + + let trades = vec![Trade { + pool: PoolType::Stableswap(pool_id), + asset_in: stable_asset_1, + asset_out: pool_id, + }]; + + assert_balance!(ALICE.into(), pool_id, 0); + + //Act + let amount_to_sell = 3000 * UNITS; + assert_ok!(Router::sell_all( + hydradx_runtime::RuntimeOrigin::signed(ALICE.into()), + stable_asset_1, + pool_id, + 0, + trades + )); + + //Assert + assert_eq!( + hydradx_runtime::Currencies::free_balance(stable_asset_1, &AccountId::from(ALICE)), + 0 + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + } +} + fn create_lbp_pool(accumulated_asset: u32, distributed_asset: u32) { assert_ok!(Currencies::update_balance( hydradx_runtime::RuntimeOrigin::root(), diff --git a/pallets/route-executor/Cargo.toml b/pallets/route-executor/Cargo.toml index 839ef0e51..16894edfd 100644 --- a/pallets/route-executor/Cargo.toml +++ b/pallets/route-executor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-route-executor' -version = '2.5.1' +version = '2.6.0' description = 'A pallet to execute a route containing a sequence of trades' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/route-executor/README.md b/pallets/route-executor/README.md index 09ad44f81..d732cbd27 100644 --- a/pallets/route-executor/README.md +++ b/pallets/route-executor/README.md @@ -14,7 +14,7 @@ The comparison happens by calculating sell amount_outs for the routes, but also The route is stored in an ordered manner, based on the oder of the ids in the asset pair. -If the route is set successfully, then the fee is payed back. +If the route is set successfully, then the fee is paid back. If the route setting fails, it emits event `RouteUpdateIsNotSuccessful` @@ -36,5 +36,7 @@ If not on-chain is present, then omnipool is used as default Both buy and sell trades are supported. +There is also a `sell_all` extrinsic, which sells all the reducible `asset_in` balance of the user. + ### Weight calculation The extrinsic weights are calculated based on the size of the route. diff --git a/pallets/route-executor/src/lib.rs b/pallets/route-executor/src/lib.rs index cf078c736..ac64928dd 100644 --- a/pallets/route-executor/src/lib.rs +++ b/pallets/route-executor/src/lib.rs @@ -209,71 +209,7 @@ pub mod pallet { min_amount_out: T::Balance, route: Vec>, ) -> DispatchResult { - let who = ensure_signed(origin.clone())?; - - ensure!(asset_in != asset_out, Error::::NotAllowed); - - Self::ensure_route_size(route.len())?; - - let asset_pair = AssetPair::new(asset_in, asset_out); - let route = Self::get_route_or_default(route, asset_pair)?; - Self::ensure_route_arguments(&asset_pair, &route)?; - - let user_balance_of_asset_out_before_trade = - T::Currency::reducible_balance(asset_out, &who, Preservation::Preserve, Fortitude::Polite); - - let trade_amounts = Self::calculate_sell_trade_amounts(&route, amount_in)?; - - let last_trade_amount = trade_amounts.last().ok_or(Error::::RouteCalculationFailed)?; - ensure!( - last_trade_amount.amount_out >= min_amount_out, - Error::::TradingLimitReached - ); - - let route_length = route.len(); - for (trade_index, (trade_amount, trade)) in trade_amounts.iter().zip(route.clone()).enumerate() { - Self::disable_ed_handling_for_insufficient_assets(route_length, trade_index, trade); - - let user_balance_of_asset_in_before_trade = - T::Currency::reducible_balance(trade.asset_in, &who, Preservation::Expendable, Fortitude::Polite); - - let execution_result = T::AMM::execute_sell( - origin.clone(), - trade.pool, - trade.asset_in, - trade.asset_out, - trade_amount.amount_in, - trade_amount.amount_out, - ); - - handle_execution_error!(execution_result); - - Self::ensure_that_user_spent_asset_in_at_least( - who.clone(), - trade.asset_in, - user_balance_of_asset_in_before_trade, - trade_amount.amount_in, - )?; - } - - SkipEd::::kill(); - - Self::ensure_that_user_received_asset_out_at_most( - who, - asset_in, - asset_out, - user_balance_of_asset_out_before_trade, - last_trade_amount.amount_out, - )?; - - Self::deposit_event(Event::Executed { - asset_in, - asset_out, - amount_in, - amount_out: last_trade_amount.amount_out, - }); - - Ok(()) + Self::do_sell(origin, asset_in, asset_out, amount_in, min_amount_out, route) } /// Executes a buy with a series of trades specified in the route. @@ -479,6 +415,36 @@ pub mod pallet { Self::insert_route(asset_pair, new_route) } + + /// Executes a sell with a series of trades specified in the route. + /// It sells all reducible user balance of `asset_in` + /// The price for each trade is determined by the corresponding AMM. + /// + /// - `origin`: The executor of the trade + /// - `asset_in`: The identifier of the asset to sell + /// - `asset_out`: The identifier of the asset to receive + /// - `min_amount_out`: The minimum amount of `asset_out` to receive. + /// - `route`: Series of [`Trade`] to be executed. A [`Trade`] specifies the asset pair (`asset_in`, `asset_out`) and the AMM (`pool`) in which the trade is executed. + /// If not specified, than the on-chain route is used. + /// If no on-chain is present, then omnipool route is used as default + /// + /// Emits `RouteExecuted` when successful. + /// + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::sell_weight(route))] + #[transactional] + pub fn sell_all( + origin: OriginFor, + asset_in: T::AssetId, + asset_out: T::AssetId, + min_amount_out: T::Balance, + route: Vec>, + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + let amount_in = T::Currency::reducible_balance(asset_in, &who, Preservation::Expendable, Fortitude::Polite); + + Self::do_sell(origin, asset_in, asset_out, amount_in, min_amount_out, route) + } } } @@ -488,6 +454,81 @@ impl Pallet { PalletId(*b"routerex").into_account_truncating() } + fn do_sell( + origin: T::RuntimeOrigin, + asset_in: T::AssetId, + asset_out: T::AssetId, + amount_in: T::Balance, + min_amount_out: T::Balance, + route: Vec>, + ) -> Result<(), DispatchError> { + let who = ensure_signed(origin.clone())?; + + ensure!(asset_in != asset_out, Error::::NotAllowed); + + Self::ensure_route_size(route.len())?; + + let asset_pair = AssetPair::new(asset_in, asset_out); + let route = Self::get_route_or_default(route, asset_pair)?; + Self::ensure_route_arguments(&asset_pair, &route)?; + + let user_balance_of_asset_out_before_trade = + T::Currency::reducible_balance(asset_out, &who, Preservation::Preserve, Fortitude::Polite); + + let trade_amounts = Self::calculate_sell_trade_amounts(&route, amount_in)?; + + let last_trade_amount = trade_amounts.last().ok_or(Error::::RouteCalculationFailed)?; + ensure!( + last_trade_amount.amount_out >= min_amount_out, + Error::::TradingLimitReached + ); + + let route_length = route.len(); + for (trade_index, (trade_amount, trade)) in trade_amounts.iter().zip(route.clone()).enumerate() { + Self::disable_ed_handling_for_insufficient_assets(route_length, trade_index, trade); + + let user_balance_of_asset_in_before_trade = + T::Currency::reducible_balance(trade.asset_in, &who, Preservation::Expendable, Fortitude::Polite); + + let execution_result = T::AMM::execute_sell( + origin.clone(), + trade.pool, + trade.asset_in, + trade.asset_out, + trade_amount.amount_in, + trade_amount.amount_out, + ); + + handle_execution_error!(execution_result); + + Self::ensure_that_user_spent_asset_in_at_least( + who.clone(), + trade.asset_in, + user_balance_of_asset_in_before_trade, + trade_amount.amount_in, + )?; + } + + SkipEd::::kill(); + + Self::ensure_that_user_received_asset_out_at_most( + who, + asset_in, + asset_out, + user_balance_of_asset_out_before_trade, + last_trade_amount.amount_out, + )?; + + Self::deposit_event(Event::Executed { + asset_in, + asset_out, + amount_in, + amount_out: last_trade_amount.amount_out, + }); + + Ok(()) + } + fn ensure_route_size(route_length: usize) -> Result<(), DispatchError> { ensure!( (route_length as u32) <= MAX_NUMBER_OF_TRADES, @@ -775,6 +816,16 @@ impl RouterT::sell(origin, asset_in, asset_out, amount_in, min_amount_out, route) } + fn sell_all( + origin: T::RuntimeOrigin, + asset_in: T::AssetId, + asset_out: T::AssetId, + min_amount_out: T::Balance, + route: Vec>, + ) -> DispatchResult { + Pallet::::sell_all(origin, asset_in, asset_out, min_amount_out, route) + } + fn buy( origin: T::RuntimeOrigin, asset_in: T::AssetId, @@ -832,6 +883,16 @@ impl RouterT>, + ) -> sp_runtime::DispatchResult { + Ok(()) + } + fn buy( _origin: T::RuntimeOrigin, _asset_in: T::AssetId, diff --git a/pallets/route-executor/src/tests/mod.rs b/pallets/route-executor/src/tests/mod.rs index 48fb30e0c..735e9dabe 100644 --- a/pallets/route-executor/src/tests/mod.rs +++ b/pallets/route-executor/src/tests/mod.rs @@ -2,5 +2,6 @@ pub mod buy; pub mod force_insert_route; pub mod mock; pub mod sell; +pub mod sell_all; pub mod set_route; pub mod spot_price; diff --git a/pallets/route-executor/src/tests/sell_all.rs b/pallets/route-executor/src/tests/sell_all.rs new file mode 100644 index 000000000..a95700ae4 --- /dev/null +++ b/pallets/route-executor/src/tests/sell_all.rs @@ -0,0 +1,459 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2022 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 crate::tests::mock::*; +use crate::{Error, Event, Trade}; +use frame_support::{assert_noop, assert_ok}; +use hydradx_traits::router::AssetPair; +use hydradx_traits::router::PoolType; +use orml_traits::MultiCurrency; +use pretty_assertions::assert_eq; +use sp_runtime::DispatchError::BadOrigin; + +#[test] +fn sell_should_work_when_route_has_single_trade() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let limit = 5; + + let trades = vec![HDX_AUSD_TRADE_IN_XYK]; + + let alice_balance = Currencies::free_balance(HDX, &ALICE); + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, AUSD, limit, trades)); + + //Assert + assert_executed_sell_trades(vec![(PoolType::XYK, alice_balance, HDX, AUSD)]); + expect_events(vec![Event::Executed { + asset_in: HDX, + asset_out: AUSD, + amount_in: alice_balance, + amount_out: XYK_SELL_CALCULATION_RESULT, + } + .into()]); + }); +} + +#[test] +fn sell_should_work_with_omnipool_when_no_specified_or_onchain_route_exist() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let limit = 1; + + let alice_balance = Currencies::free_balance(HDX, &ALICE); + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, AUSD, limit, vec![])); + + //Assert + assert_executed_sell_trades(vec![(PoolType::Omnipool, alice_balance, HDX, AUSD)]); + expect_events(vec![Event::Executed { + asset_in: HDX, + asset_out: AUSD, + amount_in: alice_balance, + amount_out: OMNIPOOL_SELL_CALCULATION_RESULT, + } + .into()]); + }); +} + +#[test] +fn sell_should_work_when_route_has_single_trade_without_native_balance() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, KSM, 1000)]) + .build() + .execute_with(|| { + //Arrange + let limit = 5; + + let alice_nonnative_balance = Currencies::free_balance(KSM, &ALICE); + + let trades = vec![Trade { + pool: PoolType::XYK, + asset_in: KSM, + asset_out: AUSD, + }]; + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), KSM, AUSD, limit, trades)); + + //Assert + assert_executed_sell_trades(vec![(PoolType::XYK, alice_nonnative_balance, KSM, AUSD)]); + }); +} + +#[test] +fn sell_should_work_when_route_has_multiple_trades_with_same_pooltype() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let alice_native_balance = Currencies::free_balance(HDX, &ALICE); + + let limit = 5; + let trade1 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: AUSD, + }; + let trade2 = Trade { + pool: PoolType::XYK, + asset_in: AUSD, + asset_out: MOVR, + }; + let trade3 = Trade { + pool: PoolType::XYK, + asset_in: MOVR, + asset_out: KSM, + }; + let trades = vec![trade1, trade2, trade3]; + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, KSM, limit, trades)); + + //Assert + assert_executed_sell_trades(vec![ + (PoolType::XYK, alice_native_balance, HDX, AUSD), + (PoolType::XYK, XYK_SELL_CALCULATION_RESULT, AUSD, MOVR), + (PoolType::XYK, XYK_SELL_CALCULATION_RESULT, MOVR, KSM), + ]); + expect_events(vec![Event::Executed { + asset_in: HDX, + asset_out: KSM, + amount_in: alice_native_balance, + amount_out: XYK_SELL_CALCULATION_RESULT, + } + .into()]); + }); +} + +#[test] +fn sell_should_work_when_route_has_multiple_trades_with_different_pool_type() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let alice_native_balance = Currencies::free_balance(HDX, &ALICE); + + let limit = 1; + let trade1 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: MOVR, + }; + let trade2 = Trade { + pool: PoolType::Stableswap(AUSD), + asset_in: MOVR, + asset_out: AUSD, + }; + let trade3 = Trade { + pool: PoolType::Omnipool, + asset_in: AUSD, + asset_out: KSM, + }; + let trades = vec![trade1, trade2, trade3]; + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, KSM, limit, trades)); + + //Assert + assert_executed_sell_trades(vec![ + (PoolType::XYK, alice_native_balance, HDX, MOVR), + (PoolType::Stableswap(AUSD), XYK_SELL_CALCULATION_RESULT, MOVR, AUSD), + (PoolType::Omnipool, STABLESWAP_SELL_CALCULATION_RESULT, AUSD, KSM), + ]); + + expect_events(vec![Event::Executed { + asset_in: HDX, + asset_out: KSM, + amount_in: alice_native_balance, + amount_out: OMNIPOOL_SELL_CALCULATION_RESULT, + } + .into()]); + }); +} + +#[test] +fn sell_should_work_with_onchain_route_when_no_routes_specified() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let alice_native_balance = Currencies::free_balance(HDX, &ALICE); + let limit = 1; + let trade1 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: MOVR, + }; + let trade2 = Trade { + pool: PoolType::Stableswap(AUSD), + asset_in: MOVR, + asset_out: AUSD, + }; + let trade3 = Trade { + pool: PoolType::XYK, + asset_in: AUSD, + asset_out: KSM, + }; + let trades = vec![trade1, trade2, trade3]; + assert_ok!(Router::set_route( + RuntimeOrigin::signed(ALICE), + AssetPair::new(HDX, KSM), + trades, + )); + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, KSM, limit, vec![])); + + //Assert + assert_last_executed_sell_trades( + 3, + vec![ + (PoolType::XYK, alice_native_balance, HDX, MOVR), + (PoolType::Stableswap(AUSD), XYK_SELL_CALCULATION_RESULT, MOVR, AUSD), + (PoolType::XYK, STABLESWAP_SELL_CALCULATION_RESULT, AUSD, KSM), + ], + ); + + expect_events(vec![Event::Executed { + asset_in: HDX, + asset_out: KSM, + amount_in: alice_native_balance, + amount_out: XYK_SELL_CALCULATION_RESULT, + } + .into()]); + }); +} + +#[test] +fn sell_should_work_with_onchain_route_when_onchain_route_present_in_reverse_order() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, KSM, 2000)]) + .build() + .execute_with(|| { + //Arrange + let alice_nonnative_balance = Currencies::free_balance(KSM, &ALICE); + + let limit = 1; + let trade1 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: MOVR, + }; + let trade2 = Trade { + pool: PoolType::Stableswap(AUSD), + asset_in: MOVR, + asset_out: AUSD, + }; + let trade3 = Trade { + pool: PoolType::XYK, + asset_in: AUSD, + asset_out: KSM, + }; + let trades = vec![trade1, trade2, trade3]; + assert_ok!(Router::set_route( + RuntimeOrigin::signed(ALICE), + AssetPair::new(HDX, KSM), + trades, + )); + + //Act + //it fails, the amount out is not there after all three trades. + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), KSM, HDX, limit, vec![])); + + //Assert + assert_last_executed_sell_trades( + 3, + vec![ + (PoolType::XYK, alice_nonnative_balance, KSM, AUSD), + (PoolType::Stableswap(AUSD), XYK_SELL_CALCULATION_RESULT, AUSD, MOVR), + (PoolType::XYK, STABLESWAP_SELL_CALCULATION_RESULT, MOVR, HDX), + ], + ); + + expect_events(vec![Event::Executed { + asset_in: KSM, + asset_out: HDX, + amount_in: alice_nonnative_balance, + amount_out: XYK_SELL_CALCULATION_RESULT, + } + .into()]); + }); +} + +#[test] +fn sell_should_work_when_first_trade_is_not_supported_in_the_first_pool() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let alice_native_balance = Currencies::free_balance(HDX, &ALICE); + let limit = 5; + let trade1 = Trade { + pool: PoolType::Stableswap(AUSD), + asset_in: HDX, + asset_out: AUSD, + }; + let trade2 = Trade { + pool: PoolType::XYK, + asset_in: AUSD, + asset_out: KSM, + }; + let trades = vec![trade1, trade2]; + + //Act + assert_ok!(Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, KSM, limit, trades)); + + //Assert + assert_executed_sell_trades(vec![ + (PoolType::Stableswap(AUSD), alice_native_balance, HDX, AUSD), + (PoolType::XYK, STABLESWAP_SELL_CALCULATION_RESULT, AUSD, KSM), + ]); + }); +} + +#[test] +fn sell_should_fail_when_max_limit_for_trade_reached() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 1000)]) + .build() + .execute_with(|| { + //Arrange + let trade1 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: AUSD, + }; + let trade2 = Trade { + pool: PoolType::XYK, + asset_in: AUSD, + asset_out: MOVR, + }; + let trade3 = Trade { + pool: PoolType::XYK, + asset_in: MOVR, + asset_out: KSM, + }; + let trade4 = Trade { + pool: PoolType::XYK, + asset_in: KSM, + asset_out: RMRK, + }; + let trade5 = Trade { + pool: PoolType::XYK, + asset_in: RMRK, + asset_out: SDN, + }; + let trade6 = Trade { + pool: PoolType::XYK, + asset_in: SDN, + asset_out: STABLE_SHARE_ASSET, + }; + let trades = vec![trade1, trade2, trade3, trade4, trade5, trade6]; + + //Act and Assert + assert_noop!( + Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, SDN, 5, trades), + Error::::MaxTradesExceeded + ); + }); +} + +#[test] +fn sell_should_fail_when_called_with_non_signed_origin() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let limit = 5; + let trades = vec![HDX_AUSD_TRADE_IN_XYK]; + + //Act and Assert + assert_noop!( + Router::sell_all(RuntimeOrigin::none(), HDX, AUSD, limit, trades), + BadOrigin + ); + }); +} + +#[test] +fn sell_should_fail_when_min_limit_to_receive_is_not_reached() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let limit = XYK_SELL_CALCULATION_RESULT + 1; + + let trades = vec![HDX_AUSD_TRADE_IN_XYK]; + + //Act and Assert + assert_noop!( + Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, AUSD, limit, trades), + Error::::TradingLimitReached + ); + }); +} + +#[test] +fn sell_should_fail_when_assets_dont_correspond_to_route() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, AUSD, 1000)]) + .build() + .execute_with(|| { + //Arrange + let limit = 5; + + let trades = vec![ + Trade { + pool: PoolType::XYK, + asset_in: AUSD, + asset_out: HDX, + }, + Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: MOVR, + }, + ]; + + //Act and assert + assert_noop!( + Router::sell_all(RuntimeOrigin::signed(ALICE), MOVR, AUSD, limit, trades), + Error::::InvalidRoute + ); + }); +} + +#[test] +fn sell_should_fail_when_intermediare_assets_are_inconsistent() { + ExtBuilder::default().build().execute_with(|| { + //Arrange + let limit = 5; + let trade1 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: AUSD, + }; + let trade2 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: MOVR, + }; + let trade3 = Trade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: KSM, + }; + let trades = vec![trade1, trade2, trade3]; + + //Act + assert_noop!( + Router::sell_all(RuntimeOrigin::signed(ALICE), HDX, KSM, limit, trades), + Error::::InvalidRoute + ); + }); +} diff --git a/traits/Cargo.toml b/traits/Cargo.toml index d162a8230..2a08d0e31 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-traits" -version = "3.4.0" +version = "3.5.0" description = "Shared traits" authors = ["GalacticCouncil"] edition = "2021" diff --git a/traits/src/router.rs b/traits/src/router.rs index 6ded7e27d..e8d6d615b 100644 --- a/traits/src/router.rs +++ b/traits/src/router.rs @@ -112,6 +112,14 @@ pub trait RouterT { route: Vec, ) -> DispatchResult; + fn sell_all( + origin: Origin, + asset_in: AssetId, + asset_out: AssetId, + min_amount_out: Balance, + route: Vec, + ) -> DispatchResult; + fn buy( origin: Origin, asset_in: AssetId,