diff --git a/pallets/order-book/src/benchmarking.rs b/pallets/order-book/src/benchmarking.rs index 7e5190ac54..5469981204 100644 --- a/pallets/order-book/src/benchmarking.rs +++ b/pallets/order-book/src/benchmarking.rs @@ -58,6 +58,13 @@ benchmarks! { }:fill_order_full(RawOrigin::Signed(account_1.clone()), order_id) + fill_order_partial { + let (account_0, account_1, asset_0, asset_1) = set_up_users_currencies::()?; + + let order_id = Pallet::::place_order(account_0.clone(), asset_0, asset_1, 100 * CURRENCY_0, T::SellRatio::saturating_from_integer(2).into(), 10 * CURRENCY_0)?; + + }:fill_order_partial(RawOrigin::Signed(account_1.clone()), order_id, 40 * CURRENCY_0) + add_trading_pair { let asset_0 = CurrencyId::ForeignAsset(1); let asset_1 = CurrencyId::ForeignAsset(2); diff --git a/pallets/order-book/src/lib.rs b/pallets/order-book/src/lib.rs index 1b97fe1c57..b268e85155 100644 --- a/pallets/order-book/src/lib.rs +++ b/pallets/order-book/src/lib.rs @@ -54,7 +54,7 @@ pub mod pallet { use frame_system::pallet_prelude::{OriginFor, *}; use orml_traits::asset_registry::{self, Inspect as _}; use scale_info::TypeInfo; - use sp_arithmetic::traits::BaseArithmetic; + use sp_arithmetic::traits::{BaseArithmetic, CheckedSub}; use sp_runtime::{ traits::{ AtLeast32BitUnsigned, EnsureAdd, EnsureDiv, EnsureFixedPointNumber, EnsureMul, @@ -353,6 +353,8 @@ pub mod pallet { /// Error when unable to convert fee balance to asset balance when asset /// out matches fee currency BalanceConversionErr, + /// Error when the provided partial buy amount is too large. + BuyAmountTooLarge, } #[pallet::call] @@ -459,50 +461,9 @@ pub mod pallet { pub fn fill_order_full(origin: OriginFor, order_id: T::OrderIdNonce) -> DispatchResult { let account_id = ensure_signed(origin)?; let order = >::get(order_id)?; + let buy_amount = order.buy_amount; - ensure!( - T::TradeableAsset::can_hold(order.asset_in_id, &account_id, order.buy_amount), - Error::::InsufficientAssetFunds, - ); - - Self::unreserve_order(&order)?; - T::TradeableAsset::transfer( - order.asset_in_id, - &account_id, - &order.placing_account, - order.buy_amount, - false, - )?; - T::TradeableAsset::transfer( - order.asset_out_id, - &order.placing_account, - &account_id, - order.max_sell_amount, - false, - )?; - Self::remove_order(order.order_id)?; - - T::FulfilledOrderHook::notify_status_change( - order_id, - Swap { - amount: order.buy_amount, - currency_in: order.asset_in_id, - currency_out: order.asset_out_id, - }, - )?; - - Self::deposit_event(Event::OrderFulfillment { - order_id, - placing_account: order.placing_account, - fulfilling_account: account_id, - partial_fulfillment: false, - currency_in: order.asset_in_id, - currency_out: order.asset_out_id, - fulfillment_amount: order.buy_amount, - sell_rate_limit: order.max_sell_rate, - }); - - Ok(()) + Self::fulfill_order_with_amount(order, buy_amount, account_id) } /// Adds a valid trading pair. @@ -582,9 +543,107 @@ pub mod pallet { Ok(()) } + + /// Fill an existing order, based on the provided partial buy amount. + #[pallet::call_index(7)] + #[pallet::weight(T::Weights::fill_order_partial())] + pub fn fill_order_partial( + origin: OriginFor, + order_id: T::OrderIdNonce, + buy_amount: T::Balance, + ) -> DispatchResult { + let account_id = ensure_signed(origin)?; + let order = >::get(order_id)?; + + Self::fulfill_order_with_amount(order, buy_amount, account_id) + } } impl Pallet { + pub fn fulfill_order_with_amount( + order: OrderOf, + buy_amount: T::Balance, + account_id: T::AccountId, + ) -> DispatchResult { + ensure!( + buy_amount >= order.min_fulfillment_amount, + Error::::InsufficientOrderSize, + ); + + ensure!( + T::TradeableAsset::can_hold(order.asset_in_id, &account_id, buy_amount), + Error::::InsufficientAssetFunds, + ); + + let sell_amount = Self::convert_with_ratio( + order.asset_in_id, + order.asset_out_id, + order.max_sell_rate, + buy_amount, + )?; + let remaining_buy_amount = order + .buy_amount + .checked_sub(&buy_amount) + .ok_or(Error::::BuyAmountTooLarge)?; + let partial_fulfillment = !remaining_buy_amount.is_zero(); + + if partial_fulfillment { + Self::update_order( + order.placing_account.clone(), + order.order_id, + remaining_buy_amount, + order.max_sell_rate, + remaining_buy_amount.min(order.min_fulfillment_amount), + )?; + } else { + T::TradeableAsset::release( + order.asset_out_id, + &order.placing_account, + sell_amount, + false, + )?; + + Self::remove_order(order.order_id)?; + } + + T::TradeableAsset::transfer( + order.asset_in_id, + &account_id, + &order.placing_account, + buy_amount, + false, + )?; + T::TradeableAsset::transfer( + order.asset_out_id, + &order.placing_account, + &account_id, + sell_amount, + false, + )?; + + T::FulfilledOrderHook::notify_status_change( + order.order_id, + Swap { + amount: buy_amount, + currency_in: order.asset_in_id, + currency_out: order.asset_out_id, + }, + )?; + + Self::deposit_event(Event::OrderFulfillment { + order_id: order.order_id, + placing_account: order.placing_account, + fulfilling_account: account_id, + partial_fulfillment, + currency_in: order.asset_in_id, + currency_out: order.asset_out_id, + fulfillment_amount: buy_amount, + sell_rate_limit: order.max_sell_rate, + }); + + Ok(()) + } + /// Remove an order from storage pub fn remove_order(order_id: T::OrderIdNonce) -> DispatchResult { let order = >::get(order_id)?; diff --git a/pallets/order-book/src/mock.rs b/pallets/order-book/src/mock.rs index d558b87aea..058ad3b322 100644 --- a/pallets/order-book/src/mock.rs +++ b/pallets/order-book/src/mock.rs @@ -154,7 +154,6 @@ impl orml_tokens::Config for Runtime { } parameter_types! { - pub const NativeToken: CurrencyId = CurrencyId::Native; } diff --git a/pallets/order-book/src/tests.rs b/pallets/order-book/src/tests.rs index 67f6f4b32b..087f70d42c 100644 --- a/pallets/order-book/src/tests.rs +++ b/pallets/order-book/src/tests.rs @@ -11,8 +11,16 @@ // GNU General Public License for more details. use cfg_types::tokens::CurrencyId; -use frame_support::{assert_err, assert_noop, assert_ok, dispatch::RawOrigin}; -use sp_runtime::{traits::Zero, DispatchError, FixedPointNumber, FixedU128}; +use frame_support::{ + assert_err, assert_noop, assert_ok, + dispatch::RawOrigin, + traits::fungibles::{Inspect, MutateHold}, +}; +use sp_arithmetic::Perquintill; +use sp_runtime::{ + traits::{BadOrigin, Zero}, + DispatchError, FixedPointNumber, FixedU128, +}; use super::*; use crate::mock::*; @@ -380,6 +388,316 @@ fn fill_order_full_works() { }); } +mod fill_order_partial { + use super::*; + + #[test] + fn fill_order_partial_works() { + 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( + ACCOUNT_0, + DEV_AUSD_CURRENCY_ID, + DEV_USDT_CURRENCY_ID, + buy_amount, + sell_ratio, + min_fulfillment_amount, + )); + + let (order_id, order) = get_account_orders(ACCOUNT_0).unwrap()[0]; + + let fulfillment_ratio = Perquintill::from_percent(fulfillment_ratio); + let partial_buy_amount = fulfillment_ratio.mul_floor(buy_amount); + + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(ACCOUNT_1), + order_id, + partial_buy_amount, + )); + + assert_eq!( + AssetPairOrders::::get(DEV_AUSD_CURRENCY_ID, DEV_USDT_CURRENCY_ID), + vec![order_id] + ); + + let expected_sell_amount = OrderBook::convert_with_ratio( + order.asset_in_id, + order.asset_out_id, + order.max_sell_rate, + partial_buy_amount, + ) + .unwrap(); + + let remaining_buy_amount = buy_amount - partial_buy_amount; + + assert_eq!( + System::events()[2].event, + RuntimeEvent::OrmlTokens(orml_tokens::Event::Unreserved { + currency_id: DEV_USDT_CURRENCY_ID, + who: ACCOUNT_0, + amount: expected_sell_amount + }) + ); + assert_eq!( + System::events()[3].event, + RuntimeEvent::OrderBook(Event::OrderUpdated { + order_id, + account: order.placing_account, + buy_amount: remaining_buy_amount, + sell_rate_limit: order.max_sell_rate, + min_fulfillment_amount: order.min_fulfillment_amount, + }) + ); + assert_eq!( + System::events()[4].event, + RuntimeEvent::OrmlTokens(orml_tokens::Event::Transfer { + currency_id: DEV_AUSD_CURRENCY_ID, + to: ACCOUNT_0, + from: ACCOUNT_1, + amount: partial_buy_amount + }) + ); + assert_eq!( + System::events()[5].event, + RuntimeEvent::OrmlTokens(orml_tokens::Event::Transfer { + currency_id: DEV_USDT_CURRENCY_ID, + to: ACCOUNT_1, + from: ACCOUNT_0, + amount: expected_sell_amount + }) + ); + assert_eq!( + System::events()[6].event, + RuntimeEvent::OrderBook(Event::OrderFulfillment { + order_id, + placing_account: order.placing_account, + fulfilling_account: ACCOUNT_1, + partial_fulfillment: true, + fulfillment_amount: partial_buy_amount, + currency_in: order.asset_in_id, + currency_out: order.asset_out_id, + sell_rate_limit: order.max_sell_rate, + }) + ); + }); + } + } + + #[test] + 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( + ACCOUNT_0, + DEV_AUSD_CURRENCY_ID, + DEV_USDT_CURRENCY_ID, + buy_amount, + sell_ratio, + min_fulfillment_amount, + )); + + let (order_id, order) = get_account_orders(ACCOUNT_0).unwrap()[0]; + + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(ACCOUNT_1), + order_id, + buy_amount, + )); + + assert_eq!( + AssetPairOrders::::get(DEV_AUSD_CURRENCY_ID, DEV_USDT_CURRENCY_ID), + vec![] + ); + + let max_sell_amount = OrderBook::convert_with_ratio( + order.asset_in_id, + order.asset_out_id, + order.max_sell_rate, + buy_amount, + ) + .unwrap(); + + assert_err!( + UserOrders::::get(order.placing_account, order_id), + Error::::OrderNotFound + ); + assert_err!( + Orders::::get(order_id), + Error::::OrderNotFound + ); + + assert_eq!( + System::events()[2].event, + RuntimeEvent::OrmlTokens(orml_tokens::Event::Unreserved { + currency_id: DEV_USDT_CURRENCY_ID, + who: ACCOUNT_0, + amount: max_sell_amount + }) + ); + assert_eq!( + System::events()[3].event, + RuntimeEvent::OrmlTokens(orml_tokens::Event::Transfer { + currency_id: DEV_AUSD_CURRENCY_ID, + to: ACCOUNT_0, + from: ACCOUNT_1, + amount: buy_amount + }) + ); + assert_eq!( + System::events()[4].event, + RuntimeEvent::OrmlTokens(orml_tokens::Event::Transfer { + currency_id: DEV_USDT_CURRENCY_ID, + to: ACCOUNT_1, + from: ACCOUNT_0, + amount: max_sell_amount + }) + ); + assert_eq!( + System::events()[5].event, + RuntimeEvent::OrderBook(Event::OrderFulfillment { + order_id, + placing_account: order.placing_account, + fulfilling_account: ACCOUNT_1, + partial_fulfillment: false, + fulfillment_amount: buy_amount, + currency_in: order.asset_in_id, + currency_out: order.asset_out_id, + sell_rate_limit: order.max_sell_rate, + }) + ); + }); + } + + #[test] + fn fill_order_partial_bad_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + OrderBook::fill_order_partial( + RawOrigin::None.into(), + 1, + 10 * CURRENCY_AUSD_DECIMALS, + ), + BadOrigin + ); + }); + } + + #[test] + fn fill_order_partial_invalid_order() { + new_test_ext().execute_with(|| { + assert_noop!( + OrderBook::fill_order_partial( + RuntimeOrigin::signed(ACCOUNT_1), + 1234, + 10 * CURRENCY_AUSD_DECIMALS, + ), + Error::::OrderNotFound + ); + }); + } + + #[test] + 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( + ACCOUNT_0, + DEV_AUSD_CURRENCY_ID, + DEV_USDT_CURRENCY_ID, + buy_amount, + sell_ratio, + min_fulfillment_amount, + )); + + let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; + + assert_noop!( + OrderBook::fill_order_partial( + RuntimeOrigin::signed(ACCOUNT_1), + order_id, + min_fulfillment_amount - 1 * CURRENCY_AUSD_DECIMALS, + ), + Error::::InsufficientOrderSize + ); + }); + } + + #[test] + 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( + ACCOUNT_0, + DEV_AUSD_CURRENCY_ID, + DEV_USDT_CURRENCY_ID, + buy_amount, + sell_ratio, + min_fulfillment_amount, + )); + + let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; + + let total_balance = OrmlTokens::balance(DEV_AUSD_CURRENCY_ID, &ACCOUNT_1); + assert_ok!(OrmlTokens::hold( + DEV_AUSD_CURRENCY_ID, + &ACCOUNT_1, + total_balance + )); + + assert_noop!( + OrderBook::fill_order_partial( + RuntimeOrigin::signed(ACCOUNT_1), + order_id, + buy_amount, + ), + Error::::InsufficientAssetFunds, + ); + }); + } + + #[test] + 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( + ACCOUNT_0, + DEV_AUSD_CURRENCY_ID, + DEV_USDT_CURRENCY_ID, + buy_amount, + sell_ratio, + min_fulfillment_amount, + )); + + let (order_id, _) = get_account_orders(ACCOUNT_0).unwrap()[0]; + + assert_noop!( + OrderBook::fill_order_partial( + RuntimeOrigin::signed(ACCOUNT_1), + order_id, + buy_amount + 1 * CURRENCY_AUSD_DECIMALS, + ), + Error::::BuyAmountTooLarge + ); + }); + } +} + #[test] fn fill_order_full_checks_asset_in_for_fulfiller() { new_test_ext().execute_with(|| { diff --git a/pallets/order-book/src/weights.rs b/pallets/order-book/src/weights.rs index 1503017fd3..9aab9ea17e 100644 --- a/pallets/order-book/src/weights.rs +++ b/pallets/order-book/src/weights.rs @@ -17,6 +17,7 @@ pub trait WeightInfo { fn user_cancel_order() -> Weight; fn user_update_order() -> Weight; fn fill_order_full() -> Weight; + fn fill_order_partial() -> Weight; fn add_trading_pair() -> Weight; fn rm_trading_pair() -> Weight; fn update_min_order() -> Weight; @@ -39,6 +40,10 @@ impl WeightInfo for () { Weight::zero() } + fn fill_order_partial() -> Weight { + Weight::zero() + } + fn add_trading_pair() -> Weight { Weight::zero() } diff --git a/pallets/restricted-tokens/src/impl_fungibles.rs b/pallets/restricted-tokens/src/impl_fungibles.rs index f5b15cddc4..6531c9f9ac 100644 --- a/pallets/restricted-tokens/src/impl_fungibles.rs +++ b/pallets/restricted-tokens/src/impl_fungibles.rs @@ -151,12 +151,15 @@ impl InspectHold for Pallet { if asset == T::NativeToken::get() { as fungible::InspectHold>::can_hold(who, amount) } else { + let can_hold = + >::can_hold(asset, who, amount); + T::PreFungiblesInspectHold::check(FungiblesInspectHoldEffects::CanHold( asset, who.clone(), amount, - >::can_hold(asset, who, amount), - )) + can_hold, + )) && can_hold } } } diff --git a/runtime/altair/src/weights/pallet_order_book.rs b/runtime/altair/src/weights/pallet_order_book.rs index a4ef7881a2..713ab2e3c8 100644 --- a/runtime/altair/src/weights/pallet_order_book.rs +++ b/runtime/altair/src/weights/pallet_order_book.rs @@ -140,4 +140,13 @@ impl pallet_order_book::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + fn fill_order_partial() -> Weight { + // Proof Size summary in bytes: + // Measured: `1702` + // Estimated: `8020828` + // Minimum execution time: 64_000 nanoseconds. + Weight::from_parts(64_000_000, 8020828) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(7)) + } } diff --git a/runtime/centrifuge/src/weights/pallet_order_book.rs b/runtime/centrifuge/src/weights/pallet_order_book.rs index a4ef7881a2..713ab2e3c8 100644 --- a/runtime/centrifuge/src/weights/pallet_order_book.rs +++ b/runtime/centrifuge/src/weights/pallet_order_book.rs @@ -140,4 +140,13 @@ impl pallet_order_book::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + fn fill_order_partial() -> Weight { + // Proof Size summary in bytes: + // Measured: `1702` + // Estimated: `8020828` + // Minimum execution time: 64_000 nanoseconds. + Weight::from_parts(64_000_000, 8020828) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(7)) + } } diff --git a/runtime/development/src/weights/pallet_order_book.rs b/runtime/development/src/weights/pallet_order_book.rs index 16dedf49a9..f805a82bef 100644 --- a/runtime/development/src/weights/pallet_order_book.rs +++ b/runtime/development/src/weights/pallet_order_book.rs @@ -140,4 +140,13 @@ impl pallet_order_book::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + fn fill_order_partial() -> Weight { + // Proof Size summary in bytes: + // Measured: `1702` + // Estimated: `28826` + // Minimum execution time: 64_000 nanoseconds. + Weight::from_parts(65_000_000, 28826) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(7)) + } } diff --git a/runtime/integration-tests/src/evm/precompile.rs b/runtime/integration-tests/src/evm/precompile.rs index 2abb953735..9a58b61e3d 100644 --- a/runtime/integration-tests/src/evm/precompile.rs +++ b/runtime/integration-tests/src/evm/precompile.rs @@ -156,6 +156,7 @@ async fn axelar_precompile_execute() { let command_id = H256::from_low_u64_be(5678); + #[allow(deprecated)] let test_input = Contract { constructor: None, functions: BTreeMap::>::from([(