From 828c8b6ca0844e097b289c2b874cf47cf4720fd2 Mon Sep 17 00:00:00 2001 From: William Freudenberger <w.freude@icloud.com> Date: Wed, 27 Sep 2023 10:42:36 +0200 Subject: [PATCH] fix: min fulfillment amount for partial fulfillment (#1570) * tests: add checks for executed_collect_* * tests: check for ExecutedDecrease* dispatch * fix: support partial swaps for FI * fix: min fulfillment in order-book * docs: fix * fix: orderbook benches * fix: docs * fix: FI mock tests * fix: orderbook benches * docs: remove remnant --- libs/mocks/src/token_swaps.rs | 14 +- libs/traits/src/lib.rs | 75 +- pallets/foreign-investments/src/hooks.rs | 34 +- pallets/foreign-investments/src/impls/mod.rs | 4 - pallets/foreign-investments/src/tests.rs | 45 +- pallets/order-book/src/benchmarking.rs | 13 +- pallets/order-book/src/lib.rs | 113 ++- pallets/order-book/src/mock.rs | 41 +- pallets/order-book/src/tests.rs | 81 +- runtime/altair/src/lib.rs | 4 + runtime/centrifuge/src/lib.rs | 4 + runtime/common/src/lib.rs | 78 +- runtime/development/src/lib.rs | 4 + .../liquidity_pools/foreign_investments.rs | 715 +++++++++++++++++- 14 files changed, 1023 insertions(+), 202 deletions(-) diff --git a/libs/mocks/src/token_swaps.rs b/libs/mocks/src/token_swaps.rs index cd6eef7aee..831dd1b60a 100644 --- a/libs/mocks/src/token_swaps.rs +++ b/libs/mocks/src/token_swaps.rs @@ -33,18 +33,16 @@ pub mod pallet { T::CurrencyId, T::Balance, T::SellRatio, - T::Balance, ) -> Result<T::OrderId, DispatchError> + 'static, ) { - register_call!(move |(a, b, c, d, e, g)| f(a, b, c, d, e, g)); + register_call!(move |(a, b, c, d, e)| f(a, b, c, d, e)); } pub fn mock_update_order( - f: impl Fn(T::AccountId, T::OrderId, T::Balance, T::SellRatio, T::Balance) -> DispatchResult - + 'static, + f: impl Fn(T::AccountId, T::OrderId, T::Balance, T::SellRatio) -> DispatchResult + 'static, ) { - register_call!(move |(a, b, c, d, e)| f(a, b, c, d, e)); + register_call!(move |(a, b, c, d)| f(a, b, c, d)); } pub fn mock_cancel_order(f: impl Fn(T::OrderId) -> DispatchResult + 'static) { @@ -79,9 +77,8 @@ pub mod pallet { c: Self::CurrencyId, d: Self::Balance, e: Self::SellRatio, - f: Self::Balance, ) -> Result<Self::OrderId, DispatchError> { - execute_call!((a, b, c, d, e, f)) + execute_call!((a, b, c, d, e)) } fn update_order( @@ -89,9 +86,8 @@ pub mod pallet { b: Self::OrderId, c: Self::Balance, d: Self::SellRatio, - e: Self::Balance, ) -> DispatchResult { - execute_call!((a, b, c, d, e)) + execute_call!((a, b, c, d)) } fn cancel_order(a: Self::OrderId) -> DispatchResult { diff --git a/libs/traits/src/lib.rs b/libs/traits/src/lib.rs index d6e04c5b85..212a70317a 100644 --- a/libs/traits/src/lib.rs +++ b/libs/traits/src/lib.rs @@ -472,11 +472,15 @@ pub trait TokenSwaps<Account> { /// `sell_rate_limit` defines the highest price acceptable for /// `currency_in` currency when buying with `currency_out`. This /// protects order placer if market changes unfavourably for swap order. - /// For example, with a `sell_rate_limit` of `3/2` one asset in should never - /// cost more than 1.5 units of asset out. Returns `Result` with `OrderId` - /// upon successful order creation. + /// For example, with a `sell_rate_limit` of `3/2`, one `asset_in` + /// should never cost more than 1.5 units of `asset_out`. Returns `Result` + /// with `OrderId` upon successful order creation. /// - /// Example usage with pallet_order_book impl: + /// NOTE: The minimum fulfillment amount is implicitly set by the + /// implementor. + /// + /// Example usage with `pallet_order_book` impl: + /// ```ignore /// OrderBook::place_order( /// {AccountId}, /// CurrencyId::ForeignAsset(0), @@ -485,8 +489,9 @@ pub trait TokenSwaps<Account> { /// Quantity::checked_from_rational(3u32, 2u32).unwrap(), /// 100 * FOREIGN_ASSET_0_DECIMALS /// ) - /// Would return Ok({OrderId}) - /// and create the following order in storage: + /// ``` + /// Would return `Ok({OrderId}` and create the following order in storage: + /// ```ignore /// Order { /// order_id: {OrderId}, /// placing_account: {AccountId}, @@ -494,31 +499,31 @@ pub trait TokenSwaps<Account> { /// asset_out_id: CurrencyId::ForeignAsset(1), /// buy_amount: 100 * FOREIGN_ASSET_0_DECIMALS, /// initial_buy_amount: 100 * FOREIGN_ASSET_0_DECIMALS, - /// sell_rate_limit: Quantity::checked_from_rational(3u32, - /// 2u32).unwrap(), min_fulfillment_amount: 100 * - /// FOREIGN_ASSET_0_DECIMALS, max_sell_amount: 150 * - /// FOREIGN_ASSET_1_DECIMALS } + /// sell_rate_limit: Quantity::checked_from_rational(3u32, 2u32).unwrap(), + /// max_sell_amount: 150 * FOREIGN_ASSET_1_DECIMALS, + /// min_fulfillment_amount: 10 * CFG * FOREIGN_ASSET_0_DECIMALS, + /// } + /// ``` fn place_order( account: Account, currency_in: Self::CurrencyId, currency_out: Self::CurrencyId, buy_amount: Self::Balance, sell_rate_limit: Self::SellRatio, - min_fulfillment_amount: Self::Balance, ) -> Result<Self::OrderId, DispatchError>; /// Update an existing active order. - /// As with create order `sell_rate_limit` defines the highest price - /// acceptable for `currency_in` currency when buying with `currency_out`. - /// Returns a Dispatch result. + /// As with creating an order, the `sell_rate_limit` defines the highest + /// price acceptable for `currency_in` currency when buying with + /// `currency_out`. Returns a Dispatch result. /// - /// This Can fail for various reasons + /// NOTE: The minimum fulfillment amount is implicitly set by the + /// implementor. /// - /// E.g. min_fulfillment_amount is lower and - /// the system has already fulfilled up to the previous - /// one. + /// This Can fail for various reasons. /// - /// Example usage with pallet_order_book impl: + /// Example usage with `pallet_order_book` impl: + /// ```ignore /// OrderBook::update_order( /// {AccountId}, /// {OrderId}, @@ -526,8 +531,9 @@ pub trait TokenSwaps<Account> { /// Quantity::checked_from_integer(2u32).unwrap(), /// 6 * FOREIGN_ASSET_0_DECIMALS /// ) - /// Would return Ok(()) - /// and update the following order in storage: + /// ``` + /// Would return `Ok(())` and update the following order in storage: + /// ```ignore /// Order { /// order_id: {OrderId}, /// placing_account: {AccountId}, @@ -536,15 +542,15 @@ pub trait TokenSwaps<Account> { /// buy_amount: 15 * FOREIGN_ASSET_0_DECIMALS, /// initial_buy_amount: 100 * FOREIGN_ASSET_0_DECIMALS, /// sell_rate_limit: Quantity::checked_from_integer(2u32).unwrap(), - /// min_fulfillment_amount: 6 * FOREIGN_ASSET_0_DECIMALS, /// max_sell_amount: 30 * FOREIGN_ASSET_1_DECIMALS + /// min_fulfillment_amount: 10 * CFG * FOREIGN_ASSET_0_DECIMALS, /// } + /// ``` fn update_order( account: Account, order_id: Self::OrderId, buy_amount: Self::Balance, sell_rate_limit: Self::SellRatio, - min_fulfillment_amount: Self::Balance, ) -> DispatchResult; /// A sanity check that can be used for validating that a trading pair @@ -596,7 +602,8 @@ pub trait IdentityCurrencyConversion { } /// A trait for trying to convert between two types. -// TODO: Remove usage for the one from Polkadot once we are on the same version +// TODO: Remove usage for the one from sp_runtime::traits once we are on +// the same Polkadot version pub trait TryConvert<A, B> { type Error; @@ -604,3 +611,23 @@ pub trait TryConvert<A, B> { /// always be `a`. fn try_convert(a: A) -> Result<B, Self::Error>; } + +/// Converts a balance value into an asset balance. +// TODO: Remove usage for the one from frame_support::traits::tokens once we are +// on the same Polkadot version +pub trait ConversionToAssetBalance<InBalance, AssetId, AssetBalance> { + type Error; + fn to_asset_balance(balance: InBalance, asset_id: AssetId) + -> Result<AssetBalance, Self::Error>; +} + +/// Converts an asset balance value into balance. +// TODO: Remove usage for the one from frame_support::traits::tokens once we are +// on the same Polkadot version +pub trait ConversionFromAssetBalance<AssetBalance, AssetId, OutBalance> { + type Error; + fn from_asset_balance( + balance: AssetBalance, + asset_id: AssetId, + ) -> Result<OutBalance, Self::Error>; +} diff --git a/pallets/foreign-investments/src/hooks.rs b/pallets/foreign-investments/src/hooks.rs index 2a7a4ca56e..17daddd943 100644 --- a/pallets/foreign-investments/src/hooks.rs +++ b/pallets/foreign-investments/src/hooks.rs @@ -79,18 +79,28 @@ impl<T: Config> StatusNotificationHook for FulfilledSwapOrderHook<T> { Error::<T>::FulfilledTokenSwapAmountOverflow ); - let invest_swap = SwapOf::<T> { - amount: active_invest_swap_amount, - ..status - }; - let redeem_swap = SwapOf::<T> { - amount: status.amount.ensure_sub(active_invest_swap_amount)?, - ..status - }; - - // NOTE: Fulfillment of invest swap before redeem one for no particular reason - Self::fulfill_invest_swap_order(&info.owner, info.id, invest_swap, false)?; - Self::fulfill_redeem_swap_order(&info.owner, info.id, redeem_swap) + // Order was fulfilled at least for invest swap amount + if status.amount > active_invest_swap_amount { + let invest_swap = SwapOf::<T> { + amount: active_invest_swap_amount, + ..status + }; + let redeem_swap = SwapOf::<T> { + amount: status.amount.ensure_sub(active_invest_swap_amount)?, + ..status + }; + + // NOTE: Fulfillment of invest swap before redeem one for no particular reason. + // If we wanted to fulfill the min swap amount, we would have to add support for + // oppression of for swap updates to `fulfill_redeem_swap_order` as well in case + // redeem_swap.amount < status.amount < invest_swap.amount + Self::fulfill_invest_swap_order(&info.owner, info.id, invest_swap, false)?; + Self::fulfill_redeem_swap_order(&info.owner, info.id, redeem_swap) + } + // Order was fulfilled below invest swap amount + else { + Self::fulfill_invest_swap_order(&info.owner, info.id, status, true) + } } _ => { log::debug!("Fulfilled token swap order id {:?} without advancing foreign investment because swap reason does not exist", id); diff --git a/pallets/foreign-investments/src/impls/mod.rs b/pallets/foreign-investments/src/impls/mod.rs index d5b30c598f..8a2779b9f0 100644 --- a/pallets/foreign-investments/src/impls/mod.rs +++ b/pallets/foreign-investments/src/impls/mod.rs @@ -798,8 +798,6 @@ impl<T: Config> Pallet<T> { swap.amount, // The max accepted sell rate is independent of the asset type for now T::DefaultTokenSellRatio::get(), - // The minimum fulfillment must be everything - swap.amount, )?; ForeignInvestmentInfo::<T>::insert( swap_order_id, @@ -827,8 +825,6 @@ impl<T: Config> Pallet<T> { swap.amount, // The max accepted sell rate is independent of the asset type for now T::DefaultTokenSellRatio::get(), - // The minimum fulfillment must be everything - swap.amount, )?; TokenSwapOrderIds::<T>::insert(who, investment_id, swap_order_id); ForeignInvestmentInfo::<T>::insert( diff --git a/pallets/foreign-investments/src/tests.rs b/pallets/foreign-investments/src/tests.rs index 81f3a0fafc..2d5ca939d8 100644 --- a/pallets/foreign-investments/src/tests.rs +++ b/pallets/foreign-investments/src/tests.rs @@ -22,7 +22,7 @@ mod util { MockInvestment::mock_investment_requires_collect(|_, _| false); MockInvestment::mock_investment(|_, _| Ok(0)); MockInvestment::mock_update_investment(|_, _, _| Ok(())); - MockTokenSwaps::mock_place_order(move |_, _, _, _, _, _| Ok(order_id)); + MockTokenSwaps::mock_place_order(move |_, _, _, _, _| Ok(order_id)); MockCurrencyConversion::mock_stable_to_stable(move |_, _, _| Ok(amount) /* 1:1 */); ForeignInvestment::increase_foreign_investment( @@ -37,7 +37,7 @@ mod util { MockInvestment::mock_investment_requires_collect(|_, _| unimplemented!("no mock")); MockInvestment::mock_investment(|_, _| unimplemented!("no mock")); MockInvestment::mock_update_investment(|_, _, _| unimplemented!("no mock")); - MockTokenSwaps::mock_place_order(|_, _, _, _, _, _| unimplemented!("no mock")); + MockTokenSwaps::mock_place_order(|_, _, _, _, _| unimplemented!("no mock")); MockCurrencyConversion::mock_stable_to_stable(|_, _, _| unimplemented!("no mock")); } @@ -92,17 +92,14 @@ mod increase_investment { assert_eq!(amount, 0); // We still do not have the swap done. Ok(()) }); - MockTokenSwaps::mock_place_order( - |account_id, curr_in, curr_out, amount, limit, min| { - assert_eq!(account_id, USER); - assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount, AMOUNT); - assert_eq!(limit, DefaultTokenSellRatio::get()); - assert_eq!(min, AMOUNT); - Ok(ORDER_ID) - }, - ); + MockTokenSwaps::mock_place_order(|account_id, curr_in, curr_out, amount, limit| { + assert_eq!(account_id, USER); + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount, AMOUNT); + assert_eq!(limit, DefaultTokenSellRatio::get()); + Ok(ORDER_ID) + }); MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { assert_eq!(curr_in, POOL_CURR); assert_eq!(curr_out, USER_CURR); @@ -173,12 +170,11 @@ mod increase_investment { amount: INITIAL_AMOUNT, }) }); - MockTokenSwaps::mock_update_order(|account_id, order_id, amount, limit, min| { + MockTokenSwaps::mock_update_order(|account_id, order_id, amount, limit| { assert_eq!(account_id, USER); assert_eq!(order_id, ORDER_ID); assert_eq!(amount, INITIAL_AMOUNT + INCREASE_AMOUNT); assert_eq!(limit, DefaultTokenSellRatio::get()); - assert_eq!(min, INITIAL_AMOUNT + INCREASE_AMOUNT); Ok(()) }); MockCurrencyConversion::mock_stable_to_stable(|curr_in, curr_out, amount_out| { @@ -224,17 +220,14 @@ mod increase_investment { assert_eq!(order_id, ORDER_ID); false }); - MockTokenSwaps::mock_place_order( - |account_id, curr_in, curr_out, amount, limit, min| { - assert_eq!(account_id, USER); - assert_eq!(curr_in, POOL_CURR); - assert_eq!(curr_out, USER_CURR); - assert_eq!(amount, INCREASE_AMOUNT); - assert_eq!(limit, DefaultTokenSellRatio::get()); - assert_eq!(min, INCREASE_AMOUNT); - Ok(ORDER_ID) - }, - ); + MockTokenSwaps::mock_place_order(|account_id, curr_in, curr_out, amount, limit| { + assert_eq!(account_id, USER); + assert_eq!(curr_in, POOL_CURR); + assert_eq!(curr_out, USER_CURR); + assert_eq!(amount, INCREASE_AMOUNT); + assert_eq!(limit, DefaultTokenSellRatio::get()); + Ok(ORDER_ID) + }); MockInvestment::mock_update_investment(|_, _, amount| { assert_eq!(amount, 0); Ok(()) diff --git a/pallets/order-book/src/benchmarking.rs b/pallets/order-book/src/benchmarking.rs index fc088372c7..41989b26b3 100644 --- a/pallets/order-book/src/benchmarking.rs +++ b/pallets/order-book/src/benchmarking.rs @@ -12,6 +12,7 @@ #![cfg(feature = "runtime-benchmarks")] +use cfg_primitives::CFG; use cfg_traits::benchmarking::OrderBookBenchmarkHelper; use cfg_types::tokens::{CurrencyId, CustomMetadata}; use frame_benchmarking::*; @@ -21,8 +22,8 @@ use sp_runtime::FixedPointNumber; use super::*; -const AMOUNT_IN: u128 = 1_000_000; -const AMOUNT_OUT: u128 = 1_000_000_000_000; +const AMOUNT_IN: u128 = 100 * CFG; +const AMOUNT_OUT: u128 = 100_000_000 * CFG; const BUY_AMOUNT: u128 = 100 * AMOUNT_IN; const ASSET_IN: CurrencyId = CurrencyId::ForeignAsset(1); const ASSET_OUT: CurrencyId = CurrencyId::ForeignAsset(2); @@ -44,28 +45,28 @@ benchmarks! { user_update_order { let (account_out, _) = Pallet::<T>::bench_setup_trading_pair(ASSET_IN, ASSET_OUT, 1000 * AMOUNT_IN, 1000 * AMOUNT_OUT, DECIMALS_IN, DECIMALS_OUT); - let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into(), BUY_AMOUNT)?; + let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into())?; }:user_update_order(RawOrigin::Signed(account_out.clone()), order_id, 10 * BUY_AMOUNT, T::SellRatio::saturating_from_integer(1)) user_cancel_order { let (account_out, _) = Pallet::<T>::bench_setup_trading_pair(ASSET_IN, ASSET_OUT, 1000 * AMOUNT_IN, 1000 * AMOUNT_OUT, DECIMALS_IN, DECIMALS_OUT); - let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into(), BUY_AMOUNT)?; + let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into())?; }:user_cancel_order(RawOrigin::Signed(account_out.clone()), order_id) fill_order_full { let (account_out, account_in) = Pallet::<T>::bench_setup_trading_pair(ASSET_IN, ASSET_OUT, 1000 * AMOUNT_IN, 1000 * AMOUNT_OUT, DECIMALS_IN, DECIMALS_OUT); - let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into(), BUY_AMOUNT)?; + let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into())?; }:fill_order_full(RawOrigin::Signed(account_in.clone()), order_id) fill_order_partial { let (account_out, account_in) = Pallet::<T>::bench_setup_trading_pair(ASSET_IN, ASSET_OUT, 1000 * AMOUNT_IN, 1000 * AMOUNT_OUT, DECIMALS_IN, DECIMALS_OUT); - let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into(), BUY_AMOUNT / 10)?; + let order_id = Pallet::<T>::place_order(account_out.clone(), ASSET_IN, ASSET_OUT, BUY_AMOUNT, T::SellRatio::saturating_from_integer(2).into())?; }:fill_order_partial(RawOrigin::Signed(account_in.clone()), order_id, BUY_AMOUNT / 2) diff --git a/pallets/order-book/src/lib.rs b/pallets/order-book/src/lib.rs index 30ec3ae549..997015dd4e 100644 --- a/pallets/order-book/src/lib.rs +++ b/pallets/order-book/src/lib.rs @@ -40,7 +40,7 @@ pub mod pallet { use core::fmt::Debug; use cfg_primitives::conversion::convert_balance_decimals; - use cfg_traits::StatusNotificationHook; + use cfg_traits::{ConversionToAssetBalance, StatusNotificationHook}; use cfg_types::{investments::Swap, tokens::CustomMetadata}; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ @@ -151,6 +151,25 @@ pub mod pallet { #[pallet::constant] type OrderPairVecSize: Get<u32>; + /// The default minimum fulfillment amount for orders. + /// + /// NOTE: The amount is expected to be denominated in native currency. + /// When applying to a swap order, it will be re-denominated into the + /// target currency. + #[pallet::constant] + type MinFulfillmentAmountNative: Get<Self::Balance>; + + /// Type which provides a decimal conversion from native to another + /// currency. + /// + /// NOTE: Required for `MinFulfillmentAmountNative`. + type DecimalConverter: cfg_traits::ConversionToAssetBalance< + Self::Balance, + Self::AssetCurrencyId, + Self::Balance, + Error = DispatchError, + >; + /// The hook which acts upon a (partially) fulfilled order type FulfilledOrderHook: StatusNotificationHook< Id = Self::OrderIdNonce, @@ -362,8 +381,7 @@ pub mod pallet { where <T as frame_system::Config>::Hash: PartialEq<<T as frame_system::Config>::Hash>, { - /// Create an order, with the minimum fulfillment amount set to the buy - /// amount, as the first iteration will not have partial fulfillment + /// Create an order with the default min fulfillment amount. #[pallet::call_index(0)] #[pallet::weight(T::Weights::create_order())] pub fn create_order( @@ -374,6 +392,10 @@ pub mod pallet { price: T::SellRatio, ) -> DispatchResult { let account_id = ensure_signed(origin)?; + let min_fulfillment_amount = T::DecimalConverter::to_asset_balance( + T::MinFulfillmentAmountNative::get(), + asset_in, + )?; Self::inner_place_order( account_id, @@ -381,7 +403,7 @@ pub mod pallet { asset_out, buy_amount, price, - buy_amount, + min_fulfillment_amount, |order| { let min_amount = TradingPair::<T>::get(&asset_in, &asset_out)?; Self::is_valid_order( @@ -408,12 +430,18 @@ pub mod pallet { price: T::SellRatio, ) -> DispatchResult { let account_id = ensure_signed(origin)?; + let order = Orders::<T>::get(order_id)?; + let min_fulfillment_amount = T::DecimalConverter::to_asset_balance( + T::MinFulfillmentAmountNative::get(), + order.asset_in_id, + )?; + Self::inner_update_order( account_id.clone(), order_id, buy_amount, price, - buy_amount, + min_fulfillment_amount, |order| { ensure!( account_id == order.placing_account, @@ -588,7 +616,7 @@ pub mod pallet { let partial_fulfillment = !remaining_buy_amount.is_zero(); if partial_fulfillment { - Self::update_order( + Self::update_order_with_fulfillment( order.placing_account.clone(), order.order_id, remaining_buy_amount, @@ -867,6 +895,40 @@ pub mod pallet { Ok(order_id) } + + /// Update an existing order. + /// + /// Update outgoing asset currency reserved to match new amount or price + /// if either have changed. + pub(crate) fn update_order_with_fulfillment( + account: T::AccountId, + order_id: T::OrderIdNonce, + buy_amount: T::Balance, + sell_rate_limit: T::SellRatio, + min_fulfillment_amount: T::Balance, + ) -> DispatchResult { + Self::inner_update_order( + account, + order_id, + buy_amount, + sell_rate_limit, + min_fulfillment_amount, + |order| { + // We only check if the trading pair exists not if the minimum amount is + // reached. + let _min_amount = + TradingPair::<T>::get(&order.asset_in_id, &order.asset_out_id)?; + Self::is_valid_order( + order.asset_in_id, + order.asset_out_id, + order.buy_amount, + order.max_sell_rate, + order.min_fulfillment_amount, + T::Balance::zero(), + ) + }, + ) + } } impl<T: Config> TokenSwaps<T::AccountId> for Pallet<T> @@ -879,18 +941,18 @@ pub mod pallet { type OrderId = T::OrderIdNonce; type SellRatio = T::SellRatio; - /// Creates an order. - /// Verify funds available in, and reserve for both chains fee currency - /// for storage fee, and amount of outgoing currency as determined by - /// the buy amount and price. fn place_order( account: T::AccountId, currency_in: T::AssetCurrencyId, currency_out: T::AssetCurrencyId, buy_amount: T::Balance, sell_rate_limit: T::SellRatio, - min_fulfillment_amount: T::Balance, ) -> Result<Self::OrderId, DispatchError> { + let min_fulfillment_amount = T::DecimalConverter::to_asset_balance( + T::MinFulfillmentAmountNative::get(), + currency_in, + )?; + Self::inner_place_order( account, currency_in, @@ -914,8 +976,6 @@ pub mod pallet { ) } - /// Cancel an existing order. - /// Unreserve currency reserved for trade as well storage fee. fn cancel_order(order: Self::OrderId) -> DispatchResult { let order = <Orders<T>>::get(order)?; let account_id = order.placing_account.clone(); @@ -930,40 +990,27 @@ pub mod pallet { Ok(()) } - /// Update an existing order. - /// Update outgoing asset currency reserved to match new amount or price - /// if either have changed. fn update_order( account: T::AccountId, order_id: Self::OrderId, buy_amount: T::Balance, sell_rate_limit: T::SellRatio, - min_fulfillment_amount: T::Balance, ) -> DispatchResult { - Self::inner_update_order( + let order = Orders::<T>::get(order_id)?; + let min_fulfillment_amount = T::DecimalConverter::to_asset_balance( + T::MinFulfillmentAmountNative::get(), + order.asset_in_id, + )?; + + Self::update_order_with_fulfillment( account, order_id, buy_amount, sell_rate_limit, min_fulfillment_amount, - |order| { - // We only check if the trading pair exists not if the minimum amount is - // reached. - let _min_amount = - TradingPair::<T>::get(&order.asset_in_id, &order.asset_out_id)?; - Self::is_valid_order( - order.asset_in_id, - order.asset_out_id, - order.buy_amount, - order.max_sell_rate, - order.min_fulfillment_amount, - T::Balance::zero(), - ) - }, ) } - /// Check whether an order is active. fn is_active(order: Self::OrderId) -> bool { <Orders<T>>::contains_key(order) } diff --git a/pallets/order-book/src/mock.rs b/pallets/order-book/src/mock.rs index 058ad3b322..593e643c14 100644 --- a/pallets/order-book/src/mock.rs +++ b/pallets/order-book/src/mock.rs @@ -11,8 +11,8 @@ // GNU General Public License for more details. use cfg_mocks::pallet_mock_fees; -use cfg_primitives::CFG; -use cfg_traits::StatusNotificationHook; +use cfg_primitives::{conversion::convert_balance_decimals, CFG}; +use cfg_traits::{ConversionToAssetBalance, StatusNotificationHook}; use cfg_types::{ investments::Swap, tokens::{CurrencyId, CustomMetadata}, @@ -23,12 +23,15 @@ use frame_support::{ traits::{ConstU128, ConstU32, GenesisBuild}, }; use frame_system::EnsureRoot; -use orml_traits::{asset_registry::AssetMetadata, parameter_type_with_key}; +use orml_traits::{ + asset_registry::{AssetMetadata, Inspect}, + parameter_type_with_key, +}; use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, - FixedU128, + DispatchError, FixedU128, }; use crate as order_book; @@ -47,6 +50,7 @@ pub(crate) const CURRENCY_USDT_DECIMALS: u128 = 1_000_000; pub(crate) const CURRENCY_AUSD_DECIMALS: u128 = 1_000_000_000_000; pub(crate) const CURRENCY_NO_MIN_DECIMALS: u128 = 1_000_000_000_000; pub(crate) const CURRENCY_NATIVE_DECIMALS: Balance = CFG; +pub(crate) const MIN_AUSD_FULFILLMENT_AMOUNT: u128 = CURRENCY_AUSD_DECIMALS / 100; const DEFAULT_DEV_MIN_ORDER: u128 = 5; const MIN_DEV_USDT_ORDER: Balance = DEFAULT_DEV_MIN_ORDER * CURRENCY_USDT_DECIMALS; @@ -182,6 +186,7 @@ impl pallet_restricted_tokens::Config for Runtime { parameter_types! { pub const OrderPairVecSize: u32 = 1_000_000u32; + pub MinFulfillmentAmountNative: Balance = CURRENCY_NATIVE_DECIMALS / 100; } pub struct DummyHook; @@ -209,12 +214,40 @@ parameter_type_with_key! { }; } +pub struct DecimalConverter; +impl ConversionToAssetBalance<Balance, CurrencyId, Balance> for DecimalConverter { + type Error = DispatchError; + + fn to_asset_balance( + balance: Balance, + currency_in: CurrencyId, + ) -> Result<Balance, DispatchError> { + match currency_in { + CurrencyId::Native => Ok(balance), + CurrencyId::ForeignAsset(_) => { + let to_decimals = RegistryMock::metadata(¤cy_in) + .ok_or(DispatchError::CannotLookup)? + .decimals; + convert_balance_decimals( + cfg_primitives::currency_decimals::NATIVE, + to_decimals, + balance, + ) + .map_err(DispatchError::from) + } + _ => Err(DispatchError::Token(sp_runtime::TokenError::Unsupported)), + } + } +} + impl order_book::Config for Runtime { type AdminOrigin = EnsureRoot<MockAccountId>; type AssetCurrencyId = CurrencyId; type AssetRegistry = RegistryMock; type Balance = Balance; + type DecimalConverter = DecimalConverter; type FulfilledOrderHook = DummyHook; + type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; type RuntimeEvent = RuntimeEvent; diff --git a/pallets/order-book/src/tests.rs b/pallets/order-book/src/tests.rs index 087f70d42c..879fe10467 100644 --- a/pallets/order-book/src/tests.rs +++ b/pallets/order-book/src/tests.rs @@ -177,7 +177,7 @@ fn create_order_works() { buy_amount: 100 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 100 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - min_fulfillment_amount: 100 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 150 * CURRENCY_USDT_DECIMALS }) ); @@ -191,7 +191,7 @@ fn create_order_works() { buy_amount: 100 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 100 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - min_fulfillment_amount: 100 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 150 * CURRENCY_USDT_DECIMALS }) ); @@ -230,7 +230,7 @@ fn user_update_order_works() { buy_amount: 15 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 10 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_integer(2u32).unwrap(), - min_fulfillment_amount: 15 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 30 * CURRENCY_USDT_DECIMALS }) ); @@ -319,7 +319,7 @@ fn user_cancel_order_only_works_for_valid_account() { buy_amount: 100 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 100 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - min_fulfillment_amount: 100 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 150 * CURRENCY_USDT_DECIMALS }) ); @@ -396,7 +396,6 @@ mod fill_order_partial { for fulfillment_ratio in 1..100 { new_test_ext().execute_with(|| { let buy_amount = 100 * CURRENCY_AUSD_DECIMALS; - let min_fulfillment_amount = 1 * CURRENCY_AUSD_DECIMALS; let sell_ratio = FixedU128::checked_from_rational(3u32, 2u32).unwrap(); assert_ok!(OrderBook::place_order( @@ -405,7 +404,6 @@ mod fill_order_partial { DEV_USDT_CURRENCY_ID, buy_amount, sell_ratio, - min_fulfillment_amount, )); let (order_id, order) = get_account_orders(ACCOUNT_0).unwrap()[0]; @@ -491,7 +489,6 @@ mod fill_order_partial { fn fill_order_partial_with_full_amount_works() { new_test_ext().execute_with(|| { let buy_amount = 100 * CURRENCY_AUSD_DECIMALS; - let min_fulfillment_amount = 1 * CURRENCY_AUSD_DECIMALS; let sell_ratio = FixedU128::checked_from_rational(3u32, 2u32).unwrap(); assert_ok!(OrderBook::place_order( @@ -500,7 +497,6 @@ mod fill_order_partial { DEV_USDT_CURRENCY_ID, buy_amount, sell_ratio, - min_fulfillment_amount, )); let (order_id, order) = get_account_orders(ACCOUNT_0).unwrap()[0]; @@ -607,7 +603,6 @@ mod fill_order_partial { fn fill_order_partial_insufficient_order_size() { new_test_ext().execute_with(|| { let buy_amount = 100 * CURRENCY_AUSD_DECIMALS; - let min_fulfillment_amount = 10 * CURRENCY_AUSD_DECIMALS; let sell_ratio = FixedU128::checked_from_rational(3u32, 2u32).unwrap(); assert_ok!(OrderBook::place_order( @@ -616,7 +611,6 @@ mod fill_order_partial { DEV_USDT_CURRENCY_ID, buy_amount, sell_ratio, - min_fulfillment_amount, )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; @@ -625,7 +619,7 @@ mod fill_order_partial { OrderBook::fill_order_partial( RuntimeOrigin::signed(ACCOUNT_1), order_id, - min_fulfillment_amount - 1 * CURRENCY_AUSD_DECIMALS, + MIN_AUSD_FULFILLMENT_AMOUNT - 1, ), Error::<Runtime>::InsufficientOrderSize ); @@ -636,7 +630,6 @@ mod fill_order_partial { fn fill_order_partial_insufficient_asset_funds() { new_test_ext().execute_with(|| { let buy_amount = 100 * CURRENCY_AUSD_DECIMALS; - let min_fulfillment_amount = 1 * CURRENCY_AUSD_DECIMALS; let sell_ratio = FixedU128::checked_from_rational(3u32, 2u32).unwrap(); assert_ok!(OrderBook::place_order( @@ -645,7 +638,6 @@ mod fill_order_partial { DEV_USDT_CURRENCY_ID, buy_amount, sell_ratio, - min_fulfillment_amount, )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; @@ -672,7 +664,6 @@ mod fill_order_partial { fn fill_order_partial_buy_amount_too_big() { new_test_ext().execute_with(|| { let buy_amount = 100 * CURRENCY_AUSD_DECIMALS; - let min_fulfillment_amount = 1 * CURRENCY_AUSD_DECIMALS; let sell_ratio = FixedU128::checked_from_rational(3u32, 2u32).unwrap(); assert_ok!(OrderBook::place_order( @@ -681,7 +672,6 @@ mod fill_order_partial { DEV_USDT_CURRENCY_ID, buy_amount, sell_ratio, - min_fulfillment_amount, )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; @@ -727,7 +717,6 @@ fn place_order_works() { DEV_USDT_CURRENCY_ID, 100 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 100 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_eq!( @@ -740,7 +729,7 @@ fn place_order_works() { buy_amount: 100 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 100 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - min_fulfillment_amount: 100 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 150 * CURRENCY_USDT_DECIMALS }) ); @@ -755,7 +744,7 @@ fn place_order_works() { buy_amount: 100 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 100 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - min_fulfillment_amount: 100 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 150 * CURRENCY_USDT_DECIMALS }) ); @@ -781,7 +770,7 @@ fn place_order_works() { currency_in: DEV_AUSD_CURRENCY_ID, currency_out: DEV_USDT_CURRENCY_ID, buy_amount: 100 * CURRENCY_AUSD_DECIMALS, - min_fulfillment_amount: 100 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, sell_rate_limit: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), }) ); @@ -797,7 +786,6 @@ fn place_order_bases_max_sell_off_buy() { DEV_USDT_CURRENCY_ID, 100 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 10 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_eq!( @@ -810,7 +798,7 @@ fn place_order_bases_max_sell_off_buy() { buy_amount: 100 * CURRENCY_AUSD_DECIMALS, initial_buy_amount: 100 * CURRENCY_AUSD_DECIMALS, max_sell_rate: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - min_fulfillment_amount: 10 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, max_sell_amount: 150 * CURRENCY_USDT_DECIMALS }) ); @@ -823,7 +811,7 @@ fn place_order_bases_max_sell_off_buy() { currency_in: DEV_AUSD_CURRENCY_ID, currency_out: DEV_USDT_CURRENCY_ID, buy_amount: 100 * CURRENCY_AUSD_DECIMALS, - min_fulfillment_amount: 10 * CURRENCY_AUSD_DECIMALS, + min_fulfillment_amount: MIN_AUSD_FULFILLMENT_AMOUNT, sell_rate_limit: FixedU128::checked_from_rational(3u32, 2u32).unwrap(), }) ); @@ -839,7 +827,6 @@ fn ensure_nonce_updates_order_correctly() { DEV_USDT_CURRENCY_ID, 100 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 100 * CURRENCY_AUSD_DECIMALS )); assert_ok!(OrderBook::place_order( ACCOUNT_0, @@ -847,7 +834,6 @@ fn ensure_nonce_updates_order_correctly() { DEV_USDT_CURRENCY_ID, 100 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 100 * CURRENCY_AUSD_DECIMALS )); let [(order_id_0, _), (order_id_1, _)] = get_account_orders(ACCOUNT_0) .unwrap() @@ -866,7 +852,6 @@ fn place_order_requires_no_min_buy() { DEV_USDT_CURRENCY_ID, 1 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 1 * CURRENCY_AUSD_DECIMALS, ),); }) } @@ -897,30 +882,12 @@ fn place_order_requires_pair_with_defined_min() { FOREIGN_CURRENCY_NO_MIN_ID, 10 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 1 * CURRENCY_AUSD_DECIMALS, ), Error::<Runtime>::InvalidTradingPair ); }) } -#[test] -fn place_order_requires_non_zero_min_fulfillment() { - new_test_ext().execute_with(|| { - assert_err!( - OrderBook::place_order( - ACCOUNT_0, - DEV_AUSD_CURRENCY_ID, - DEV_USDT_CURRENCY_ID, - 10 * CURRENCY_AUSD_DECIMALS, - FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 0 - ), - Error::<Runtime>::InvalidMinimumFulfillment - ); - }) -} - #[test] fn place_order_min_fulfillment_cannot_be_less_than_buy() { new_test_ext().execute_with(|| { @@ -929,9 +896,8 @@ fn place_order_min_fulfillment_cannot_be_less_than_buy() { ACCOUNT_0, DEV_AUSD_CURRENCY_ID, DEV_USDT_CURRENCY_ID, - 10 * CURRENCY_AUSD_DECIMALS, + MIN_AUSD_FULFILLMENT_AMOUNT - 1, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 11 * CURRENCY_AUSD_DECIMALS ), Error::<Runtime>::InvalidBuyAmount ); @@ -948,7 +914,6 @@ fn place_order_requires_non_zero_price() { DEV_USDT_CURRENCY_ID, 100 * CURRENCY_AUSD_DECIMALS, FixedU128::zero(), - 100 * CURRENCY_AUSD_DECIMALS ), Error::<Runtime>::InvalidMaxPrice ); @@ -964,7 +929,6 @@ fn cancel_order_works() { DEV_USDT_CURRENCY_ID, 100 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 100 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_ok!(OrderBook::cancel_order(order_id)); @@ -1009,10 +973,9 @@ fn update_order_works_with_order_increase() { DEV_USDT_CURRENCY_ID, 10 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; - assert_ok!(OrderBook::update_order( + assert_ok!(OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 15 * CURRENCY_AUSD_DECIMALS, @@ -1093,10 +1056,9 @@ fn update_order_updates_min_fulfillment() { DEV_USDT_CURRENCY_ID, 10 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; - assert_ok!(OrderBook::update_order( + assert_ok!(OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 10 * CURRENCY_AUSD_DECIMALS, @@ -1159,10 +1121,9 @@ fn update_order_works_with_order_decrease() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; - assert_ok!(OrderBook::update_order( + assert_ok!(OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 10 * CURRENCY_AUSD_DECIMALS, @@ -1241,10 +1202,9 @@ fn update_order_requires_no_min_buy() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; - assert_ok!(OrderBook::update_order( + assert_ok!(OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 1 * CURRENCY_AUSD_DECIMALS, @@ -1263,7 +1223,6 @@ fn user_update_order_requires_min_buy() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_err!( @@ -1287,11 +1246,10 @@ fn update_order_requires_non_zero_min_fulfillment() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_err!( - OrderBook::update_order( + OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 10 * CURRENCY_AUSD_DECIMALS, @@ -1312,11 +1270,10 @@ fn update_order_min_fulfillment_cannot_be_less_than_buy() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_err!( - OrderBook::update_order( + OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 10 * CURRENCY_AUSD_DECIMALS, @@ -1337,11 +1294,10 @@ fn update_order_requires_non_zero_price() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_err!( - OrderBook::update_order( + OrderBook::update_order_with_fulfillment( ACCOUNT_0, order_id, 10 * CURRENCY_AUSD_DECIMALS, @@ -1362,7 +1318,6 @@ fn get_order_details_works() { DEV_USDT_CURRENCY_ID, 15 * CURRENCY_AUSD_DECIMALS, FixedU128::checked_from_rational(3u32, 2u32).unwrap(), - 5 * CURRENCY_AUSD_DECIMALS )); let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; assert_eq!( diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 49f6c8cbfc..a6b9fecaac 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -1775,6 +1775,7 @@ impl pallet_keystore::pallet::Config for Runtime { parameter_types! { pub const OrderPairVecSize: u32 = 1_000_000u32; + pub MinFulfillmentAmountNative: Balance = 10 * CFG; } impl pallet_order_book::Config for Runtime { @@ -1782,7 +1783,10 @@ impl pallet_order_book::Config for Runtime { type AssetCurrencyId = CurrencyId; type AssetRegistry = OrmlAssetRegistry; type Balance = Balance; + type DecimalConverter = + runtime_common::foreign_investments::NativeBalanceDecimalConverter<OrmlAssetRegistry>; type FulfilledOrderHook = pallet_foreign_investments::hooks::FulfilledSwapOrderHook<Runtime>; + type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; type RuntimeEvent = RuntimeEvent; diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index 32b95ace8c..39ded0c764 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -1925,6 +1925,7 @@ impl pallet_uniques::Config for Runtime { parameter_types! { pub const OrderPairVecSize: u32 = 1_000u32; + pub MinFulfillmentAmountNative: Balance = 10 * CFG; } impl pallet_order_book::Config for Runtime { @@ -1932,7 +1933,10 @@ impl pallet_order_book::Config for Runtime { type AssetCurrencyId = CurrencyId; type AssetRegistry = OrmlAssetRegistry; type Balance = Balance; + type DecimalConverter = + runtime_common::foreign_investments::NativeBalanceDecimalConverter<OrmlAssetRegistry>; type FulfilledOrderHook = pallet_foreign_investments::hooks::FulfilledSwapOrderHook<Runtime>; + type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; type RuntimeEvent = RuntimeEvent; diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 3886440513..2b97e51757 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -423,7 +423,9 @@ pub mod xcm_transactor { pub mod foreign_investments { use cfg_primitives::{conversion::convert_balance_decimals, Balance}; - use cfg_traits::IdentityCurrencyConversion; + use cfg_traits::{ + ConversionFromAssetBalance, ConversionToAssetBalance, IdentityCurrencyConversion, + }; use cfg_types::tokens::CurrencyId; use frame_support::pallet_prelude::PhantomData; use orml_traits::asset_registry::Inspect; @@ -484,6 +486,80 @@ pub mod foreign_investments { } } } + + /// Provides means of applying the decimals of an incoming currency to the + /// amount of an outgoing currency. + /// + /// NOTE: Either the incoming (in case of `ConversionFromAssetBalance`) or + /// outgoing currency (in case of `ConversionToAssetBalance`) is assumed + /// to be `CurrencyId::Native`. + pub struct NativeBalanceDecimalConverter<AssetRegistry>(PhantomData<AssetRegistry>); + + impl<AssetRegistry> ConversionToAssetBalance<Balance, CurrencyId, Balance> + for NativeBalanceDecimalConverter<AssetRegistry> + where + AssetRegistry: Inspect< + AssetId = CurrencyId, + Balance = Balance, + CustomMetadata = cfg_types::tokens::CustomMetadata, + >, + { + type Error = DispatchError; + + fn to_asset_balance( + balance: Balance, + currency_in: CurrencyId, + ) -> Result<Balance, DispatchError> { + match currency_in { + CurrencyId::Native => Ok(balance), + CurrencyId::ForeignAsset(_) => { + let to_decimals = AssetRegistry::metadata(¤cy_in) + .ok_or(DispatchError::CannotLookup)? + .decimals; + convert_balance_decimals( + cfg_primitives::currency_decimals::NATIVE, + to_decimals, + balance, + ) + .map_err(DispatchError::from) + } + _ => Err(DispatchError::Token(sp_runtime::TokenError::Unsupported)), + } + } + } + + impl<AssetRegistry> ConversionFromAssetBalance<Balance, CurrencyId, Balance> + for NativeBalanceDecimalConverter<AssetRegistry> + where + AssetRegistry: Inspect< + AssetId = CurrencyId, + Balance = Balance, + CustomMetadata = cfg_types::tokens::CustomMetadata, + >, + { + type Error = DispatchError; + + fn from_asset_balance( + balance: Balance, + currency_out: CurrencyId, + ) -> Result<Balance, DispatchError> { + match currency_out { + CurrencyId::Native => Ok(balance), + CurrencyId::ForeignAsset(_) => { + let from_decimals = AssetRegistry::metadata(¤cy_out) + .ok_or(DispatchError::CannotLookup)? + .decimals; + convert_balance_decimals( + from_decimals, + cfg_primitives::currency_decimals::NATIVE, + balance, + ) + .map_err(DispatchError::from) + } + _ => Err(DispatchError::Token(sp_runtime::TokenError::Unsupported)), + } + } + } } pub mod origin { diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 8ca13335d9..05a64f99ff 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -1850,6 +1850,7 @@ impl pallet_transfer_allowlist::Config for Runtime { parameter_types! { pub const OrderPairVecSize: u32 = 1_000u32; + pub MinFulfillmentAmountNative: Balance = 10 * CFG; } impl pallet_order_book::Config for Runtime { @@ -1857,7 +1858,10 @@ impl pallet_order_book::Config for Runtime { type AssetCurrencyId = CurrencyId; type AssetRegistry = OrmlAssetRegistry; type Balance = Balance; + type DecimalConverter = + runtime_common::foreign_investments::NativeBalanceDecimalConverter<OrmlAssetRegistry>; type FulfilledOrderHook = pallet_foreign_investments::hooks::FulfilledSwapOrderHook<Runtime>; + type MinFulfillmentAmountNative = MinFulfillmentAmountNative; type OrderIdNonce = u64; type OrderPairVecSize = OrderPairVecSize; type RuntimeEvent = RuntimeEvent; diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs index 5ff52acadd..e4a3060714 100644 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs +++ b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs @@ -42,8 +42,8 @@ use cfg_types::{ }; use development_runtime::{ Balances, ForeignInvestments, Investments, LiquidityPools, LocationToAccountId, - OrmlAssetRegistry, Permissions, PoolSystem, Runtime as DevelopmentRuntime, RuntimeOrigin, - System, Tokens, + MinFulfillmentAmountNative, OrmlAssetRegistry, Permissions, PoolSystem, + Runtime as DevelopmentRuntime, RuntimeOrigin, System, Tokens, TreasuryAccount, }; use frame_support::{ assert_noop, assert_ok, @@ -75,6 +75,7 @@ use crate::{ tests::liquidity_pools::{ foreign_investments::setup::{ do_initial_increase_investment, do_initial_increase_redemption, + ensure_executed_collect_redeem_not_dispatched, }, setup::{ asset_metadata, create_ausd_pool, create_currency_pool, @@ -454,6 +455,24 @@ mod same_currencies { } .into() })); + + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + currency_payout: amount, + tranche_tokens_payout: amount, + remaining_invest_amount: 0, + }, + } + .into() + })); }); } @@ -520,7 +539,6 @@ mod same_currencies { investor.clone(), default_investment_id() )); - assert_eq!( InvestmentPaymentCurrency::<DevelopmentRuntime>::get( &investor, @@ -565,6 +583,23 @@ mod same_currencies { } .into() })); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + currency_payout: invest_amount / 2, + tranche_tokens_payout: invest_amount * 2, + remaining_invest_amount: invest_amount / 2, + }, + } + .into() + })); // Process rest of investment at 50% rate (1 pool currency = 2 tranche tokens) assert_ok!(Investments::process_invest_orders(default_investment_id())); @@ -652,6 +687,23 @@ mod same_currencies { } .into() })); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + currency_payout: invest_amount / 2, + tranche_tokens_payout: invest_amount, + remaining_invest_amount: 0, + }, + } + .into() + })); // Clearing of foreign InvestState should have been dispatched exactly once assert_eq!( System::events() @@ -828,6 +880,23 @@ mod same_currencies { .amount, final_amount ); + + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedDecreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + tranche_tokens_payout: decrease_amount, + remaining_redeem_amount: final_amount, + }, + } + .into() + })); }); } @@ -1061,6 +1130,23 @@ mod same_currencies { } .into() })); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + currency_payout: amount, + tranche_tokens_payout: amount, + remaining_redeem_amount: 0, + }, + } + .into() + })); }); } @@ -1137,6 +1223,23 @@ mod same_currencies { } .into() })); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + currency_payout: redeem_amount / 8, + tranche_tokens_payout: redeem_amount / 2, + remaining_redeem_amount: redeem_amount / 2, + }, + } + .into() + })); assert!(!Investments::redemption_requires_collect( &investor, default_investment_id() @@ -1243,6 +1346,23 @@ mod same_currencies { .count(), 1 ); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(currency_id), + currency_payout: redeem_amount / 4, + tranche_tokens_payout: redeem_amount / 2, + remaining_redeem_amount: 0, + }, + } + .into() + })); }); } @@ -1480,9 +1600,7 @@ mod same_currencies { mod payment_payout_currency { use super::*; use crate::{ - liquidity_pools::pallet::development::tests::{ - liquidity_pools::foreign_investments::setup::enable_usdt_trading, - }, + liquidity_pools::pallet::development::tests::liquidity_pools::foreign_investments::setup::enable_usdt_trading, utils::USDT_CURRENCY_ID, }; @@ -1750,7 +1868,10 @@ mod mismatching_currencies { liquidity_pools::pallet::development::{ setup::CHARLIE, tests::{ - liquidity_pools::foreign_investments::setup::enable_usdt_trading, register_usdt, + liquidity_pools::foreign_investments::setup::{ + enable_usdt_trading, min_fulfillment_amount, + }, + register_usdt, }, }, utils::{GLMR_CURRENCY_ID, USDT_CURRENCY_ID}, @@ -1800,7 +1921,7 @@ mod mismatching_currencies { tranche_id: default_tranche_id(pool_id), investor: investor.clone().into(), currency: general_currency_index(foreign_currency), - amount: 1, + amount: invest_amount_foreign_denominated, }; assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); @@ -1831,6 +1952,23 @@ mod mismatching_currencies { Tokens::balance(default_investment_id().into(), &sending_domain_locator), invest_amount_pool_denominated * 2 ); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectInvest { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated, + tranche_tokens_payout: invest_amount_pool_denominated * 2, + remaining_invest_amount: 0, + }, + } + .into() + })); // Should not be cleared as invest state is swapping into pool currency assert_eq!( @@ -2018,10 +2156,10 @@ mod mismatching_currencies { }); } - #[test] /// Propagate swaps only via OrderBook fulfillments. /// /// Flow: Increase, fulfill, decrease, fulfill + #[test] fn invest_swaps_happy_path() { TestNet::reset(); Development::execute_with(|| { @@ -2169,17 +2307,30 @@ mod mismatching_currencies { .is_none() ); assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); - - // TODO: Check for event that ExecutedDecreaseInvestOrder was - // dispatched + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedDecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 2, + remaining_invest_amount: invest_amount_foreign_denominated / 2, + }, + } + .into() + })); }); } - #[test] /// Verify handling concurrent swap orders works if /// * Invest is swapping from pool to foreign after decreasing an /// unprocessed investment /// * Redeem is swapping from pool to foreign after collecting + #[test] fn concurrent_swap_orders_same_direction() { TestNet::reset(); Development::execute_with(|| { @@ -2330,10 +2481,11 @@ mod mismatching_currencies { account: investor.clone(), buy_amount: swap_amount, sell_rate_limit: Ratio::one(), - min_fulfillment_amount: swap_amount, + min_fulfillment_amount: min_fulfillment_amount(foreign_currency), } .into() })); + ensure_executed_collect_redeem_not_dispatched(); // Process remaining redemption at 25% rate, i.e. 1 pool currency = // 4 tranche tokens @@ -2379,7 +2531,7 @@ mod mismatching_currencies { account: investor.clone(), buy_amount: swap_amount, sell_rate_limit: Ratio::one(), - min_fulfillment_amount: swap_amount, + min_fulfillment_amount: min_fulfillment_amount(foreign_currency), } .into() })); @@ -2422,13 +2574,30 @@ mod mismatching_currencies { ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) .is_none() ); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 4, + tranche_tokens_payout: invest_amount_pool_denominated, + remaining_redeem_amount: 0, + }, + } + .into() + })); }); } - #[test] /// Verify handling concurrent swap orders works if /// * Invest is swapping from foreign to pool after increasing /// * Redeem is swapping from pool to foreign after collecting + #[test] fn concurrent_swap_orders_opposite_direction() { TestNet::reset(); Development::execute_with(|| { @@ -2570,6 +2739,11 @@ mod mismatching_currencies { redeem_amount: invest_amount_pool_denominated / 2, } ); + dbg!(System::events()); + dbg!(min_fulfillment_amount(foreign_currency)); + dbg!(invest_amount_pool_denominated / 8); + dbg!(min_fulfillment_amount(pool_currency)); + assert!(System::events().iter().any(|e| { e.event == pallet_order_book::Event::<DevelopmentRuntime>::OrderUpdated { @@ -2577,7 +2751,24 @@ mod mismatching_currencies { account: investor.clone(), buy_amount: invest_amount_pool_denominated / 8 * 7, sell_rate_limit: Ratio::one(), - min_fulfillment_amount: invest_amount_pool_denominated / 8 * 7, + min_fulfillment_amount: min_fulfillment_amount(pool_currency), + } + .into() + })); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 8, + tranche_tokens_payout: invest_amount_pool_denominated / 2, + remaining_redeem_amount: invest_amount_pool_denominated / 2, + }, } .into() })); @@ -2619,7 +2810,24 @@ mod mismatching_currencies { account: investor.clone(), buy_amount: invest_amount_pool_denominated / 4 * 3, sell_rate_limit: Ratio::one(), - min_fulfillment_amount: invest_amount_pool_denominated / 4 * 3, + min_fulfillment_amount: min_fulfillment_amount(pool_currency), + } + .into() + })); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 8, + tranche_tokens_payout: invest_amount_pool_denominated / 2, + remaining_redeem_amount: 0, + }, } .into() })); @@ -2686,6 +2894,7 @@ mod mismatching_currencies { } } ); + ensure_executed_collect_redeem_not_dispatched(); // Fulfilling order should the invest assert_ok!(OrderBook::fill_order_full( @@ -2717,6 +2926,23 @@ mod mismatching_currencies { ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) .is_none() ); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated * 2, + tranche_tokens_payout: invest_amount_pool_denominated, + remaining_redeem_amount: 0, + }, + } + .into() + })); }); } @@ -2908,6 +3134,21 @@ mod mismatching_currencies { } } ); + // ExecutedCollectRedeem should not have been dispatched + assert!(System::events().iter().any(|e| { + match &e.event { + development_runtime::RuntimeEvent::LiquidityPoolsGateway( + pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + message, + .. + }, + ) => match message { + pallet_liquidity_pools::Message::ExecutedCollectRedeem { .. } => false, + _ => true, + }, + _ => true, + } + })); // Process remaining redemption at 25% rate, i.e. 1 pool currency = 4 tranche // tokens @@ -2931,7 +3172,6 @@ mod mismatching_currencies { &investor, default_investment_id() )); - // TODO: Assert ExecutedCollectRedeem was not dispatched assert_eq!( RedemptionState::<DevelopmentRuntime>::get(&investor, default_investment_id()), RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { @@ -2943,12 +3183,423 @@ mod mismatching_currencies { } } ); + // ExecutedCollectRedeem should not have been dispatched as RedemptionState is + // still swapping + ensure_executed_collect_redeem_not_dispatched(); + + // Fulfill redemption swap + assert_ok!(OrderBook::fill_order_full( + RuntimeOrigin::signed(trader.clone()), + swap_order_id + 1 + )); + assert!(!RedemptionState::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + )); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: redeem_amount_foreign_denominated / 8 * 3, + tranche_tokens_payout: redeem_amount_pool_denominated, + remaining_redeem_amount: 0, + }, + } + .into() + })); + }); + } + + /// Similar to [concurrent_swap_orders_same_direction] but with partial + /// fulfillment + #[test] + fn partial_fulfillment_concurrent_swap_orders_same_direction() { + TestNet::reset(); + Development::execute_with(|| { + // Increase invest setup + setup_pre_requirements(); + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = + AccountConverter::<DevelopmentRuntime, LocationToAccountId>::convert(( + DOMAIN_MOONBEAM, + BOB, + )); + let trader: AccountId = ALICE.into(); + let pool_currency: CurrencyId = AUSD_CURRENCY_ID; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let pool_currency_decimals = currency_decimals::AUSD; + let invest_amount_pool_denominated: u128 = 10 * dollar(18); + let swap_order_id = 1; + create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); + let invest_amount_foreign_denominated: u128 = enable_usdt_trading( + pool_currency, + invest_amount_pool_denominated, + true, + true, + true, + || {}, + ); + // invest in pool currency to reach `InvestmentOngoing` quickly + do_initial_increase_investment( + pool_id, + invest_amount_pool_denominated, + investor.clone(), + pool_currency, + true, + ); + // Manually set payment currency since we removed it in the above shortcut setup + InvestmentPaymentCurrency::<DevelopmentRuntime>::insert( + &investor, + default_investment_id(), + foreign_currency, + ); + assert_ok!(Tokens::mint_into( + foreign_currency, + &trader, + invest_amount_foreign_denominated * 2 + )); + + // Decrease invest setup to have invest order swapping into foreign currency + let msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: invest_amount_foreign_denominated, + }; + assert_ok!(LiquidityPools::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + )); + + // Redeem setup: Increase and process + assert_ok!(Tokens::mint_into( + default_investment_id().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + invest_amount_pool_denominated + )); + let msg = LiquidityPoolMessage::IncreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: invest_amount_pool_denominated, + }; + assert_ok!(LiquidityPools::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + )); + let pool_account = + pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); + assert_ok!(Tokens::mint_into( + pool_currency, + &pool_account, + invest_amount_pool_denominated + )); + assert_ok!(Investments::process_redeem_orders(default_investment_id())); + // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche + // tokens + assert_ok!(Investments::redeem_fulfillment( + default_investment_id(), + FulfillmentWithPrice { + of_amount: Perquintill::from_percent(50), + price: Ratio::checked_from_rational(1, 4).unwrap(), + } + )); + assert_eq!( + ForeignInvestments::foreign_investment_info(swap_order_id) + .unwrap() + .last_swap_reason + .unwrap(), + TokenSwapReason::Investment + ); + assert_ok!(Investments::collect_redemptions_for( + RuntimeOrigin::signed(CHARLIE.into()), + investor.clone(), + default_investment_id() + )); + assert_eq!( + ForeignInvestments::foreign_investment_info(swap_order_id) + .unwrap() + .last_swap_reason + .unwrap(), + TokenSwapReason::InvestmentAndRedemption + ); + assert_eq!( + InvestmentState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + InvestState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + assert_eq!( + RedemptionState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + RedeemState::RedeemingAndActiveSwapIntoForeignCurrency { + redeem_amount: invest_amount_pool_denominated / 2, + swap: Swap { + amount: invest_amount_foreign_denominated / 8, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + assert_eq!( + RedemptionPayoutCurrency::<DevelopmentRuntime>::get( + &investor, + default_investment_id() + ) + .unwrap(), + foreign_currency + ); + let swap_amount = + invest_amount_foreign_denominated + invest_amount_foreign_denominated / 8; + dbg!(System::events()); + dbg!(swap_amount); + dbg!(MinFulfillmentAmountNative::get()); + assert!(System::events().iter().any(|e| { + e.event + == pallet_order_book::Event::<DevelopmentRuntime>::OrderUpdated { + order_id: swap_order_id, + account: investor.clone(), + buy_amount: swap_amount, + sell_rate_limit: Ratio::one(), + min_fulfillment_amount: min_fulfillment_amount(foreign_currency), + } + .into() + })); + ensure_executed_collect_redeem_not_dispatched(); + + // Process remaining redemption at 25% rate, i.e. 1 pool currency = + // 4 tranche tokens + assert_ok!(Investments::process_redeem_orders(default_investment_id())); + assert_ok!(Investments::redeem_fulfillment( + default_investment_id(), + FulfillmentWithPrice { + of_amount: Perquintill::from_percent(100), + price: Ratio::checked_from_rational(1, 4).unwrap(), + } + )); + assert_ok!(Investments::collect_redemptions_for( + RuntimeOrigin::signed(CHARLIE.into()), + investor.clone(), + default_investment_id() + )); + assert_eq!( + InvestmentState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + InvestState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + assert_eq!( + RedemptionState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + let swap_amount = + invest_amount_foreign_denominated + invest_amount_foreign_denominated / 4; + assert!(System::events().iter().any(|e| { + e.event + == pallet_order_book::Event::<DevelopmentRuntime>::OrderUpdated { + order_id: swap_order_id, + account: investor.clone(), + buy_amount: swap_amount, + sell_rate_limit: Ratio::one(), + min_fulfillment_amount: min_fulfillment_amount(foreign_currency), + } + .into() + })); + + // Partially fulfilling the swap order below the invest swapping amount should + // still have both states swapping into foreign + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 2 + )); + assert!(System::events().iter().any(|e| { + e.event + == pallet_order_book::Event::<DevelopmentRuntime>::OrderFulfillment { + order_id: swap_order_id, + placing_account: investor.clone(), + fulfilling_account: trader.clone(), + partial_fulfillment: true, + fulfillment_amount: invest_amount_foreign_denominated / 2, + currency_in: foreign_currency, + currency_out: pool_currency, + sell_rate_limit: Ratio::one(), + } + .into() + })); + assert_eq!( + InvestmentState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + swap: Swap { + amount: invest_amount_foreign_denominated / 2, + currency_in: foreign_currency, + currency_out: pool_currency + }, + done_amount: invest_amount_foreign_denominated / 2 + } + ); + assert_eq!( + RedemptionState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency + }, + } + ); + assert!( + RedemptionPayoutCurrency::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_some() + ); + ensure_executed_collect_redeem_not_dispatched(); + + // Partially fulfilling the swap order for the remaining invest swap amount + // should still clear the investment state + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 2 + )); + assert!(!InvestmentState::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ),); + assert_eq!( + RedemptionState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency + }, + } + ); + assert!( + RedemptionPayoutCurrency::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_some() + ); + ensure_executed_collect_redeem_not_dispatched(); + + // Partially fulfilling the swap order below the redeem swap amount should still + // clear the investment state + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 8 + )); + assert!(!InvestmentState::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ),); + assert_eq!( + RedemptionState::<DevelopmentRuntime>::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + swap: Swap { + amount: invest_amount_foreign_denominated / 8, + currency_in: foreign_currency, + currency_out: pool_currency + }, + done_amount: invest_amount_foreign_denominated / 8 + } + ); + assert!( + RedemptionPayoutCurrency::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_some() + ); + ensure_executed_collect_redeem_not_dispatched(); + + // Partially fulfilling the swap order below the redeem swap amount should still + // clear the investment state + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 8 + )); + assert!(!InvestmentState::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ),); + assert!(!RedemptionState::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ),); + assert!( + !RedemptionPayoutCurrency::<DevelopmentRuntime>::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_none() + ); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 4, + tranche_tokens_payout: invest_amount_pool_denominated, + remaining_redeem_amount: 0, + }, + } + .into() + })); }); } } mod setup { - use cfg_traits::investments::ForeignInvestment; + use cfg_traits::{investments::ForeignInvestment, ConversionToAssetBalance}; use development_runtime::OrderBook; use super::*; @@ -3281,4 +3932,28 @@ mod setup { amount_foreign_denominated } + + pub(crate) fn ensure_executed_collect_redeem_not_dispatched() { + assert!(System::events().iter().any(|e| { + match &e.event { + development_runtime::RuntimeEvent::LiquidityPoolsGateway( + pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + message, .. + }, + ) => match message { + pallet_liquidity_pools::Message::ExecutedCollectRedeem { .. } => false, + _ => true, + }, + _ => true, + } + })); + } + + pub(crate) fn min_fulfillment_amount(currency_id: CurrencyId) -> Balance { + runtime_common::foreign_investments::NativeBalanceDecimalConverter::<OrmlAssetRegistry>::to_asset_balance( + MinFulfillmentAmountNative::get(), + currency_id, + ) + .expect("CurrencyId should be registered in AssetRegistry") + } }