Skip to content

Commit

Permalink
order-book: Add support for partial order fulfillment
Browse files Browse the repository at this point in the history
  • Loading branch information
cdamian committed Sep 19, 2023
1 parent 523b6d3 commit 84bfaa2
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 3 deletions.
106 changes: 104 additions & 2 deletions pallets/order-book/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ 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},
Perquintill,
};
use sp_runtime::{
traits::{
AtLeast32BitUnsigned, EnsureAdd, EnsureDiv, EnsureFixedPointNumber, EnsureMul,
Expand Down Expand Up @@ -127,7 +130,9 @@ pub mod pallet {
+ EnsureMul
+ EnsureDiv
+ TypeInfo
+ MaxEncodedLen;
+ MaxEncodedLen
+ From<u64>
+ sp_arithmetic::traits::Unsigned;

/// Type for currency orders can be made for
type TradeableAsset: AssetInspect<Self::AccountId, Balance = Self::Balance, AssetId = Self::AssetCurrencyId>
Expand Down Expand Up @@ -353,6 +358,11 @@ pub mod pallet {
/// Error when unable to convert fee balance to asset balance when asset
/// out matches fee currency
BalanceConversionErr,
/// Error when unable to calculate the remaining buy amount for a
/// partial order.
RemainingBuyAmountError,
/// Error when the ratio provided for a partial error is 0.
InvalidPartialOrderRatio,
}

#[pallet::call]
Expand Down Expand Up @@ -505,6 +515,98 @@ pub mod pallet {
Ok(())
}

/// Fill an existing order, based on the provided ratio.
#[pallet::call_index(7)]
#[pallet::weight(T::Weights::fill_order_partial())]
pub fn fill_order_partial(
origin: OriginFor<T>,
order_id: T::OrderIdNonce,
fulfillment_ratio: Perquintill,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let order = <Orders<T>>::get(order_id)?;

ensure!(
!fulfillment_ratio.is_zero(),
Error::<T>::InvalidPartialOrderRatio
);

let partial_buy_amount = fulfillment_ratio.mul_floor(order.buy_amount);
let partial_sell_amount = fulfillment_ratio.mul_floor(order.max_sell_amount);
let remaining_buy_amount = order
.buy_amount
.checked_sub(&partial_buy_amount)
.ok_or(Error::<T>::RemainingBuyAmountError)?;
let partial_fulfillment = !remaining_buy_amount.is_zero();

if partial_fulfillment {
Self::update_order(
order.placing_account.clone(),
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,
partial_sell_amount,
false,
)?;

Self::remove_order(order.order_id)?;
}

ensure!(
partial_buy_amount >= order.min_fulfillment_amount,
Error::<T>::InsufficientOrderSize,
);

ensure!(
T::TradeableAsset::can_hold(order.asset_in_id, &account_id, partial_buy_amount),
Error::<T>::InsufficientAssetFunds,
);

T::TradeableAsset::transfer(
order.asset_in_id,
&account_id,
&order.placing_account,
partial_buy_amount,
false,
)?;

T::TradeableAsset::transfer(
order.asset_out_id,
&order.placing_account,
&account_id,
partial_sell_amount,
false,
)?;

T::FulfilledOrderHook::notify_status_change(
order_id,
Swap {
amount: partial_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,
currency_in: order.asset_in_id,
currency_out: order.asset_out_id,
fulfillment_amount: partial_buy_amount,
sell_rate_limit: order.max_sell_rate,
});

Ok(())
}

/// Adds a valid trading pair.
#[pallet::call_index(4)]
#[pallet::weight(T::Weights::add_trading_pair())]
Expand Down
1 change: 0 additions & 1 deletion pallets/order-book/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ impl orml_tokens::Config for Runtime {
}

parameter_types! {

pub const NativeToken: CurrencyId = CurrencyId::Native;
}

Expand Down
198 changes: 198 additions & 0 deletions pallets/order-book/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

use cfg_primitives::conversion::convert_balance_decimals;
use cfg_types::tokens::CurrencyId;
use frame_support::{assert_err, assert_noop, assert_ok, dispatch::RawOrigin};
use orml_traits::asset_registry::Inspect;
use sp_arithmetic::Perquintill;
use sp_runtime::{traits::Zero, DispatchError, FixedPointNumber, FixedU128};

use super::*;
Expand Down Expand Up @@ -380,6 +383,201 @@ fn fill_order_full_works() {
});
}

#[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);

assert_ok!(OrderBook::fill_order_partial(
RuntimeOrigin::signed(ACCOUNT_1),
order_id,
fulfillment_ratio,
));

assert_eq!(
AssetPairOrders::<Runtime>::get(DEV_AUSD_CURRENCY_ID, DEV_USDT_CURRENCY_ID),
vec![order_id]
);

let ausd_decimals = RegistryMock::metadata(&DEV_AUSD_CURRENCY_ID)
.unwrap()
.decimals;
let usdt_decimals = RegistryMock::metadata(&DEV_USDT_CURRENCY_ID)
.unwrap()
.decimals;

let max_sell_amount = convert_balance_decimals(
ausd_decimals,
usdt_decimals,
sell_ratio.checked_mul_int(buy_amount).unwrap(),
)
.unwrap();

let expected_buy_amount = fulfillment_ratio.mul_floor(buy_amount);
let expected_sell_amount = fulfillment_ratio.mul_floor(max_sell_amount);
let remaining_buy_amount = buy_amount - expected_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: expected_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: expected_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_100_percent_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];

let fulfillment_ratio = Perquintill::from_percent(100);

assert_ok!(OrderBook::fill_order_partial(
RuntimeOrigin::signed(ACCOUNT_1),
order_id,
fulfillment_ratio,
));

assert_eq!(
AssetPairOrders::<Runtime>::get(DEV_AUSD_CURRENCY_ID, DEV_USDT_CURRENCY_ID),
vec![]
);

let ausd_decimals = RegistryMock::metadata(&DEV_AUSD_CURRENCY_ID)
.unwrap()
.decimals;
let usdt_decimals = RegistryMock::metadata(&DEV_USDT_CURRENCY_ID)
.unwrap()
.decimals;

let max_sell_amount = convert_balance_decimals(
ausd_decimals,
usdt_decimals,
sell_ratio.checked_mul_int(buy_amount).unwrap(),
)
.unwrap();

let expected_buy_amount = fulfillment_ratio.mul_floor(buy_amount);
let expected_sell_amount = fulfillment_ratio.mul_floor(max_sell_amount);

let _events = System::events();

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::OrmlTokens(orml_tokens::Event::Transfer {
currency_id: DEV_AUSD_CURRENCY_ID,
to: ACCOUNT_0,
from: ACCOUNT_1,
amount: expected_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: expected_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: expected_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_full_checks_asset_in_for_fulfiller() {
new_test_ext().execute_with(|| {
Expand Down
5 changes: 5 additions & 0 deletions pallets/order-book/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,6 +40,10 @@ impl WeightInfo for () {
Weight::zero()
}

fn fill_order_partial() -> Weight {
Weight::zero()
}

fn add_trading_pair() -> Weight {
Weight::zero()
}
Expand Down

0 comments on commit 84bfaa2

Please sign in to comment.