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(&currency_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(&currency_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(&currency_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")
+	}
 }