From a6777b9f58422ef1c56b5ca8bc7d3447de9f342c Mon Sep 17 00:00:00 2001 From: Chralt Date: Tue, 3 Oct 2023 15:06:19 +0200 Subject: [PATCH 1/2] Update weight templates to reflect weights-v2 (#1109) * update weight templates to reflect weights-v2 * Apply suggestions from code review Co-authored-by: Harald Heckmann * Update orml_weight_template.hbs * Update weight_template.hbs --------- Co-authored-by: Harald Heckmann --- misc/frame_weight_template.hbs | 20 +++++++++++--------- misc/orml_weight_template.hbs | 20 +++++++++++--------- misc/weight_template.hbs | 20 +++++++++++--------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/misc/frame_weight_template.hbs b/misc/frame_weight_template.hbs index 373da00e6..d2524d4e8 100644 --- a/misc/frame_weight_template.hbs +++ b/misc/frame_weight_template.hbs @@ -25,6 +25,9 @@ impl {{pallet}}::weights::WeightInfo for WeightInfo {{#each benchmark.comments as |comment|}} /// {{comment}} {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} fn {{benchmark.name~}} ( {{~#each benchmark.components as |c| ~}} @@ -34,27 +37,26 @@ impl {{pallet}}::weights::WeightInfo for WeightInfo // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` // Minimum execution time: {{underscore benchmark.min_execution_time}} nanoseconds. - {{#if (ne benchmark.base_calculated_proof_size "0")}} Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) - {{else}} - Weight::from_ref_time({{underscore benchmark.base_weight}}) - {{/if}} {{#each benchmark.component_weight as |cw|}} // Standard Error: {{underscore cw.error}} - .saturating_add(Weight::from_ref_time({{underscore cw.slope}}).saturating_mul({{cw.name}}.into())) + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) {{/each}} {{#if (ne benchmark.base_reads "0")}} - .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}}_u64)) + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}})) {{/if}} {{#each benchmark.component_reads as |cr|}} .saturating_add(T::DbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) {{/each}} {{#if (ne benchmark.base_writes "0")}} - .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}}_u64)) + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}})) {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(T::DbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} {{#each benchmark.component_calculated_proof_size as |cp|}} - .saturating_add(Weight::from_proof_size({{cp.slope}}).saturating_mul({{cp.name}}.into())) - {{/each}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} } {{/each}} } diff --git a/misc/orml_weight_template.hbs b/misc/orml_weight_template.hbs index eec55f462..2da62c06e 100644 --- a/misc/orml_weight_template.hbs +++ b/misc/orml_weight_template.hbs @@ -25,6 +25,9 @@ impl {{pallet}}::WeightInfo for WeightInfo { {{#each benchmark.comments as |comment|}} /// {{comment}} {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} fn {{benchmark.name~}} ( {{~#each benchmark.components as |c| ~}} @@ -34,27 +37,26 @@ impl {{pallet}}::WeightInfo for WeightInfo { // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` // Minimum execution time: {{underscore benchmark.min_execution_time}} nanoseconds. - {{#if (ne benchmark.base_calculated_proof_size "0")}} Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) - {{else}} - Weight::from_ref_time({{underscore benchmark.base_weight}}) - {{/if}} {{#each benchmark.component_weight as |cw|}} // Standard Error: {{underscore cw.error}} - .saturating_add(Weight::from_ref_time({{underscore cw.slope}}).saturating_mul({{cw.name}}.into())) + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) {{/each}} {{#if (ne benchmark.base_reads "0")}} - .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}}_u64)) + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}})) {{/if}} {{#each benchmark.component_reads as |cr|}} .saturating_add(T::DbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) {{/each}} {{#if (ne benchmark.base_writes "0")}} - .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}}_u64)) + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}})) {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(T::DbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} {{#each benchmark.component_calculated_proof_size as |cp|}} - .saturating_add(Weight::from_proof_size({{cp.slope}}).saturating_mul({{cp.name}}.into())) - {{/each}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} } {{/each}} } diff --git a/misc/weight_template.hbs b/misc/weight_template.hbs index 30c93b830..875819dda 100644 --- a/misc/weight_template.hbs +++ b/misc/weight_template.hbs @@ -37,6 +37,9 @@ impl WeightInfoZeitgeist for WeightInfo { {{#each benchmark.comments as |comment|}} /// {{comment}} {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} fn {{benchmark.name~}} ( {{~#each benchmark.components as |c| ~}} @@ -46,27 +49,26 @@ impl WeightInfoZeitgeist for WeightInfo { // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` // Minimum execution time: {{underscore benchmark.min_execution_time}} nanoseconds. - {{#if (ne benchmark.base_calculated_proof_size "0")}} Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) - {{else}} - Weight::from_ref_time({{underscore benchmark.base_weight}}) - {{/if}} {{#each benchmark.component_weight as |cw|}} // Standard Error: {{underscore cw.error}} - .saturating_add(Weight::from_ref_time({{underscore cw.slope}}).saturating_mul({{cw.name}}.into())) + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) {{/each}} {{#if (ne benchmark.base_reads "0")}} - .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}}_u64)) + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}})) {{/if}} {{#each benchmark.component_reads as |cr|}} .saturating_add(T::DbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) {{/each}} {{#if (ne benchmark.base_writes "0")}} - .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}}_u64)) + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}})) {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(T::DbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} {{#each benchmark.component_calculated_proof_size as |cp|}} - .saturating_add(Weight::from_proof_size({{cp.slope}}).saturating_mul({{cp.name}}.into())) - {{/each}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} } {{/each}} } From 575ebecff7b160866e76fe546b8f83dbf84d1d41 Mon Sep 17 00:00:00 2001 From: Chralt Date: Wed, 4 Oct 2023 08:32:36 +0200 Subject: [PATCH 2/2] Implement partial fills for orderbook pallet (#1099) * implement partial fills * wip * update tests benchmarks and mock * integrate orderbook with prediction markets logic * integrate orderbook into runtimes * update weights * remove market id from fill_order * add copyright notices * Update zrml/orderbook-v1/src/lib.rs Co-authored-by: Malte Kliemann * Update runtime/zeitgeist/src/lib.rs Co-authored-by: Malte Kliemann * Update runtime/zeitgeist/src/lib.rs Co-authored-by: Malte Kliemann * use two asset amounts and calc the price from it * add copyright * correct weights * apply review suggestions * add test cases * integrate check for reserves * apply review comments * apply review comments * cargo fmt * fix clippy * fmt * fix orderbook fuzz --------- Co-authored-by: Malte Kliemann --- Cargo.lock | 6 + primitives/src/constants.rs | 3 + primitives/src/constants/mock.rs | 5 + primitives/src/pool.rs | 2 + runtime/battery-station/Cargo.toml | 4 + runtime/battery-station/src/parameters.rs | 3 + runtime/common/src/lib.rs | 18 + runtime/zeitgeist/Cargo.toml | 4 + runtime/zeitgeist/src/parameters.rs | 3 + zrml/orderbook-v1/Cargo.toml | 7 + .../fuzz/orderbook_v1_full_workflow.rs | 78 +-- zrml/orderbook-v1/src/benchmarks.rs | 86 +-- zrml/orderbook-v1/src/lib.rs | 644 ++++++++++-------- zrml/orderbook-v1/src/mock.rs | 55 +- zrml/orderbook-v1/src/tests.rs | 401 ++++++++++- zrml/orderbook-v1/src/types.rs | 40 ++ zrml/orderbook-v1/src/utils.rs | 66 ++ zrml/orderbook-v1/src/weights.rs | 168 +++-- zrml/prediction-markets/src/lib.rs | 19 +- zrml/swaps/src/lib.rs | 11 + zrml/swaps/src/utils.rs | 9 + 21 files changed, 1170 insertions(+), 462 deletions(-) create mode 100644 zrml/orderbook-v1/src/types.rs create mode 100644 zrml/orderbook-v1/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 22b4fa212..7babea139 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,6 +617,7 @@ dependencies = [ "zrml-global-disputes", "zrml-liquidity-mining", "zrml-market-commons", + "zrml-orderbook-v1", "zrml-prediction-markets", "zrml-rikiddo", "zrml-simple-disputes", @@ -14430,6 +14431,7 @@ dependencies = [ "zrml-global-disputes", "zrml-liquidity-mining", "zrml-market-commons", + "zrml-orderbook-v1", "zrml-prediction-markets", "zrml-rikiddo", "zrml-simple-disputes", @@ -14560,14 +14562,18 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "orml-currencies", "orml-tokens", "orml-traits", "pallet-balances", + "pallet-timestamp", "parity-scale-codec", "scale-info", "sp-io", "sp-runtime", + "test-case", "zeitgeist-primitives", + "zrml-market-commons", "zrml-orderbook-v1", ] diff --git a/primitives/src/constants.rs b/primitives/src/constants.rs index 6e0b6d3a3..d5e32ef71 100644 --- a/primitives/src/constants.rs +++ b/primitives/src/constants.rs @@ -113,6 +113,9 @@ pub const MAX_ASSETS: u16 = MAX_CATEGORIES + 1; /// Pallet identifier, mainly used for named balance reserves. pub const SWAPS_PALLET_ID: PalletId = PalletId(*b"zge/swap"); +// Orderbook +pub const ORDERBOOK_PALLET_ID: PalletId = PalletId(*b"zge/ordb"); + // Treasury /// Pallet identifier, used to derive treasury account pub const TREASURY_PALLET_ID: PalletId = PalletId(*b"zge/tsry"); diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs index 2bfae8ecc..5f1ec243c 100644 --- a/primitives/src/constants/mock.rs +++ b/primitives/src/constants/mock.rs @@ -120,6 +120,11 @@ parameter_types! { pub const SwapsPalletId: PalletId = PalletId(*b"zge/swap"); } +// Orderbook parameters +parameter_types! { + pub const OrderbookPalletId: PalletId = PalletId(*b"zge/ordb"); +} + // Shared within tests // Balance parameter_types! { diff --git a/primitives/src/pool.rs b/primitives/src/pool.rs index bafa0e4af..62ca56d80 100644 --- a/primitives/src/pool.rs +++ b/primitives/src/pool.rs @@ -1,3 +1,4 @@ +// Copyright 2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -83,4 +84,5 @@ where pub enum ScoringRule { CPMM, RikiddoSigmoidFeeMarketEma, + Orderbook, } diff --git a/runtime/battery-station/Cargo.toml b/runtime/battery-station/Cargo.toml index ade6c68de..b6e040f1d 100644 --- a/runtime/battery-station/Cargo.toml +++ b/runtime/battery-station/Cargo.toml @@ -113,6 +113,7 @@ zrml-court = { workspace = true } zrml-global-disputes = { workspace = true, optional = true } zrml-liquidity-mining = { workspace = true } zrml-market-commons = { workspace = true } +zrml-orderbook-v1 = { workspace = true } zrml-prediction-markets = { workspace = true } zrml-rikiddo = { workspace = true } zrml-simple-disputes = { workspace = true } @@ -214,6 +215,7 @@ runtime-benchmarks = [ "zrml-global-disputes/runtime-benchmarks", "zrml-styx/runtime-benchmarks", "zrml-swaps/runtime-benchmarks", + "zrml-orderbook-v1/runtime-benchmarks", ] std = [ "frame-executive/std", @@ -328,6 +330,7 @@ std = [ "zrml-styx/std", "zrml-swaps-runtime-api/std", "zrml-swaps/std", + "zrml-orderbook-v1/std", ] try-runtime = [ "frame-executive/try-runtime", @@ -380,6 +383,7 @@ try-runtime = [ "zrml-global-disputes/try-runtime", "zrml-styx/try-runtime", "zrml-swaps/try-runtime", + "zrml-orderbook-v1/try-runtime", # Parachain "pallet-author-inherent?/try-runtime", diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs index adf67ddaa..2eb042b1e 100644 --- a/runtime/battery-station/src/parameters.rs +++ b/runtime/battery-station/src/parameters.rs @@ -305,6 +305,9 @@ parameter_types! { /// Pallet identifier, mainly used for named balance reserves. pub const SwapsPalletId: PalletId = SWAPS_PALLET_ID; + // Orderbook parameters + pub const OrderbookPalletId: PalletId = ORDERBOOK_PALLET_ID; + // System pub const BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 73; diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index bbace19cb..f311977a1 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -193,6 +193,7 @@ macro_rules! decl_common_types { CourtPalletId::get(), GlobalDisputesPalletId::get(), LiquidityMiningPalletId::get(), + OrderbookPalletId::get(), PmPalletId::get(), SimpleDisputesPalletId::get(), SwapsPalletId::get(), @@ -310,6 +311,7 @@ macro_rules! create_runtime { PredictionMarkets: zrml_prediction_markets::{Call, Event, Pallet, Storage} = 57, Styx: zrml_styx::{Call, Event, Pallet, Storage} = 58, GlobalDisputes: zrml_global_disputes::{Call, Event, Pallet, Storage} = 59, + Orderbook: zrml_orderbook_v1::{Call, Event, Pallet, Storage} = 60, $($additional_pallets)* } @@ -855,6 +857,9 @@ macro_rules! impl_config_traits { c, RuntimeCall::Swaps(zrml_swaps::Call::swap_exact_amount_in { .. }) | RuntimeCall::Swaps(zrml_swaps::Call::swap_exact_amount_out { .. }) + | RuntimeCall::Orderbook(zrml_orderbook_v1::Call::place_order { .. }) + | RuntimeCall::Orderbook(zrml_orderbook_v1::Call::fill_order { .. }) + | RuntimeCall::Orderbook(zrml_orderbook_v1::Call::remove_order { .. }) ), ProxyType::HandleAssets => matches!( c, @@ -874,6 +879,9 @@ macro_rules! impl_config_traits { | RuntimeCall::PredictionMarkets( zrml_prediction_markets::Call::deploy_swap_pool_and_additional_liquidity { .. } ) + | RuntimeCall::Orderbook(zrml_orderbook_v1::Call::place_order { .. }) + | RuntimeCall::Orderbook(zrml_orderbook_v1::Call::fill_order { .. }) + | RuntimeCall::Orderbook(zrml_orderbook_v1::Call::remove_order { .. }) ), } } @@ -1228,6 +1236,14 @@ macro_rules! impl_config_traits { type Currency = Balances; type WeightInfo = zrml_styx::weights::WeightInfo; } + + impl zrml_orderbook_v1::Config for Runtime { + type AssetManager = AssetManager; + type RuntimeEvent = RuntimeEvent; + type MarketCommons = MarketCommons; + type PalletId = OrderbookPalletId; + type WeightInfo = zrml_orderbook_v1::weights::WeightInfo; + } } } @@ -1336,6 +1352,7 @@ macro_rules! create_runtime_api { list_benchmark!(list, extra, zrml_court, Court); list_benchmark!(list, extra, zrml_simple_disputes, SimpleDisputes); list_benchmark!(list, extra, zrml_global_disputes, GlobalDisputes); + list_benchmark!(list, extra, zrml_orderbook_v1, Orderbook); #[cfg(not(feature = "parachain"))] list_benchmark!(list, extra, zrml_prediction_markets, PredictionMarkets); list_benchmark!(list, extra, zrml_liquidity_mining, LiquidityMining); @@ -1437,6 +1454,7 @@ macro_rules! create_runtime_api { add_benchmark!(params, batches, zrml_court, Court); add_benchmark!(params, batches, zrml_simple_disputes, SimpleDisputes); add_benchmark!(params, batches, zrml_global_disputes, GlobalDisputes); + add_benchmark!(params, batches, zrml_orderbook_v1, Orderbook); #[cfg(not(feature = "parachain"))] add_benchmark!(params, batches, zrml_prediction_markets, PredictionMarkets); add_benchmark!(params, batches, zrml_liquidity_mining, LiquidityMining); diff --git a/runtime/zeitgeist/Cargo.toml b/runtime/zeitgeist/Cargo.toml index 0798fd41c..d1771f947 100644 --- a/runtime/zeitgeist/Cargo.toml +++ b/runtime/zeitgeist/Cargo.toml @@ -111,6 +111,7 @@ zrml-court = { workspace = true } zrml-global-disputes = { workspace = true, optional = true } zrml-liquidity-mining = { workspace = true } zrml-market-commons = { workspace = true } +zrml-orderbook-v1 = { workspace = true } zrml-prediction-markets = { workspace = true } zrml-rikiddo = { workspace = true } zrml-simple-disputes = { workspace = true } @@ -211,6 +212,7 @@ runtime-benchmarks = [ "zrml-global-disputes/runtime-benchmarks", "zrml-styx/runtime-benchmarks", "zrml-swaps/runtime-benchmarks", + "zrml-orderbook-v1/runtime-benchmarks", ] std = [ "frame-executive/std", @@ -317,6 +319,7 @@ std = [ "zrml-swaps-runtime-api/std", "zrml-styx/std", "zrml-swaps/std", + "zrml-orderbook-v1/std", ] try-runtime = [ "frame-executive/try-runtime", @@ -369,6 +372,7 @@ try-runtime = [ "zrml-global-disputes/try-runtime", "zrml-styx/try-runtime", "zrml-swaps/try-runtime", + "zrml-orderbook-v1/try-runtime", # Parachain "pallet-author-inherent?/try-runtime", diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs index 2406637e2..b4a907ec3 100644 --- a/runtime/zeitgeist/src/parameters.rs +++ b/runtime/zeitgeist/src/parameters.rs @@ -305,6 +305,9 @@ parameter_types! { /// Pallet identifier, mainly used for named balance reserves. DO NOT CHANGE. pub const SwapsPalletId: PalletId = SWAPS_PALLET_ID; + // Orderbook parameters + pub const OrderbookPalletId: PalletId = ORDERBOOK_PALLET_ID; + // System pub const BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 73; diff --git a/zrml/orderbook-v1/Cargo.toml b/zrml/orderbook-v1/Cargo.toml index 4debd5bbd..da2e40ef6 100644 --- a/zrml/orderbook-v1/Cargo.toml +++ b/zrml/orderbook-v1/Cargo.toml @@ -10,11 +10,15 @@ sp-runtime = { workspace = true } zeitgeist-primitives = { workspace = true } # Mock +orml-currencies = { workspace = true, optional = true } orml-tokens = { workspace = true, optional = true } pallet-balances = { workspace = true, optional = true } +pallet-timestamp = { workspace = true, optional = true } sp-io = { workspace = true, optional = true } +zrml-market-commons = { workspace = true, optional = true } [dev-dependencies] +test-case = { workspace = true } zrml-orderbook-v1 = { workspace = true, features = ["mock", "default"] } [features] @@ -22,6 +26,9 @@ default = ["std"] mock = [ "orml-tokens/default", "pallet-balances/default", + "pallet-timestamp/default", + "zrml-market-commons/default", + "orml-currencies/default", "sp-io/default", "zeitgeist-primitives/mock", ] diff --git a/zrml/orderbook-v1/fuzz/orderbook_v1_full_workflow.rs b/zrml/orderbook-v1/fuzz/orderbook_v1_full_workflow.rs index 7f43e5f9f..b26eb3225 100644 --- a/zrml/orderbook-v1/fuzz/orderbook_v1_full_workflow.rs +++ b/zrml/orderbook-v1/fuzz/orderbook_v1_full_workflow.rs @@ -1,3 +1,4 @@ +// Copyright 2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -17,12 +18,11 @@ #![no_main] -use frame_system::ensure_signed; use libfuzzer_sys::fuzz_target; use zeitgeist_primitives::types::{Asset, ScalarPosition, SerdeWrapper}; use zrml_orderbook_v1::{ mock::{ExtBuilder, Orderbook, RuntimeOrigin}, - OrderSide, + types::OrderSide, }; #[cfg(feature = "arbitrary")] @@ -32,44 +32,36 @@ fuzz_target!(|data: Data| { let mut ext = ExtBuilder::default().build(); ext.execute_with(|| { // Make arbitrary order and attempt to fill - let order_asset = asset(data.make_fill_order_asset); - let order_hash = Orderbook::order_hash( - &ensure_signed(RuntimeOrigin::signed(data.make_fill_order_origin.into())).unwrap(), - order_asset, - Orderbook::nonce(), - ); + let outcome_asset = asset(data.fill_order_outcome_asset); - let _ = Orderbook::make_order( - RuntimeOrigin::signed(data.make_fill_order_origin.into()), - order_asset, - orderside(data.make_fill_order_side), - data.make_fill_order_amount, - data.make_fill_order_price, + let _ = Orderbook::place_order( + RuntimeOrigin::signed(data.fill_order_origin.into()), + data.market_id, + outcome_asset, + orderside(data.fill_order_side), + data.fill_order_amount, + data.fill_order_price, ); - let _ = - Orderbook::fill_order(RuntimeOrigin::signed(data.fill_order_origin.into()), order_hash); - - // Make arbitrary order and attempt to cancel - let order_asset = asset(data.make_cancel_order_asset); - let order_hash = Orderbook::order_hash( - &ensure_signed(RuntimeOrigin::signed(data.make_cancel_order_origin.into())).unwrap(), - order_asset, - Orderbook::nonce(), + let _ = Orderbook::fill_order( + RuntimeOrigin::signed(data.fill_order_origin.into()), + data.order_id, + None, ); - let _ = Orderbook::make_order( - RuntimeOrigin::signed(data.make_cancel_order_origin.into()), - order_asset, - orderside(data.make_cancel_order_side), - data.make_cancel_order_amount, - data.make_cancel_order_price, + // Make arbitrary order and attempt to cancel + let _ = Orderbook::place_order( + RuntimeOrigin::signed(data.cancel_order_origin.into()), + data.market_id, + outcome_asset, + orderside(data.cancel_order_side), + data.cancel_order_amount, + data.cancel_order_price, ); - let _ = Orderbook::cancel_order( - RuntimeOrigin::signed(data.make_cancel_order_origin.into()), - order_asset, - order_hash, + let _ = Orderbook::remove_order( + RuntimeOrigin::signed(data.cancel_order_origin.into()), + data.order_id, ); }); let _ = ext.commit_all(); @@ -77,19 +69,19 @@ fuzz_target!(|data: Data| { #[derive(Debug, arbitrary::Arbitrary)] struct Data { - make_fill_order_amount: u128, - make_fill_order_asset: (u128, u16), - make_fill_order_price: u128, - make_fill_order_origin: u8, - make_fill_order_side: u8, + market_id: u128, + order_id: u128, + fill_order_amount: u128, + fill_order_outcome_asset: (u128, u16), + fill_order_price: u128, fill_order_origin: u8, + fill_order_side: u8, - make_cancel_order_amount: u128, - make_cancel_order_asset: (u128, u16), - make_cancel_order_price: u128, - make_cancel_order_origin: u8, - make_cancel_order_side: u8, + cancel_order_amount: u128, + cancel_order_price: u128, + cancel_order_origin: u8, + cancel_order_side: u8, } fn asset(seed: (u128, u16)) -> Asset { diff --git a/zrml/orderbook-v1/src/benchmarks.rs b/zrml/orderbook-v1/src/benchmarks.rs index e60585e88..8f51b5d23 100644 --- a/zrml/orderbook-v1/src/benchmarks.rs +++ b/zrml/orderbook-v1/src/benchmarks.rs @@ -24,10 +24,11 @@ #![allow(clippy::type_complexity)] use super::*; +use crate::utils::market_mock; #[cfg(test)] use crate::Pallet as OrderBook; use frame_benchmarking::{account, benchmarks, whitelisted_caller}; -use frame_support::{dispatch::UnfilteredDispatchable, traits::Currency}; +use frame_support::dispatch::UnfilteredDispatchable; use frame_system::RawOrigin; use orml_traits::MultiCurrency; use sp_runtime::SaturatedConversion; @@ -37,71 +38,80 @@ use zeitgeist_primitives::{constants::BASE, types::Asset}; fn generate_funded_account(seed: Option) -> Result { let acc = if let Some(s) = seed { account("AssetHolder", 0, s) } else { whitelisted_caller() }; - let asset = Asset::CategoricalOutcome::(0u32.into(), 0); - T::Shares::deposit(asset, &acc, BASE.saturating_mul(1_000).saturated_into())?; - let _ = T::Currency::deposit_creating(&acc, BASE.saturating_mul(1_000).saturated_into()); + let outcome_asset = Asset::CategoricalOutcome::>(0u32.into(), 0); + T::AssetManager::deposit(outcome_asset, &acc, BASE.saturating_mul(1_000).saturated_into())?; + let _ = T::AssetManager::deposit(Asset::Ztg, &acc, BASE.saturating_mul(1_000).saturated_into()); Ok(acc) } // Creates an account and gives it asset and currency. `seed` specifies the account seed, // None will return a whitelisted account -// Returns `account`, `asset`, `amount` and `price` +// Returns `account`, `asset`, `outcome_asset_amount` and `base_asset_amount` fn order_common_parameters( seed: Option, -) -> Result<(T::AccountId, Asset, BalanceOf, BalanceOf), &'static str> { +) -> Result< + (T::AccountId, Asset>, BalanceOf, BalanceOf, MarketIdOf), + &'static str, +> { let acc = generate_funded_account::(seed)?; - let asset = Asset::CategoricalOutcome::(0u32.into(), 0); - let amt: BalanceOf = BASE.saturated_into(); - let prc: BalanceOf = 1u32.into(); - Ok((acc, asset, amt, prc)) + let outcome_asset = Asset::CategoricalOutcome::>(0u32.into(), 0); + let outcome_asset_amount: BalanceOf = BASE.saturated_into(); + let base_asset_amount: BalanceOf = 100u32.into(); + let market_id: MarketIdOf = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + Ok((acc, outcome_asset, outcome_asset_amount, base_asset_amount, market_id)) } // Creates an order of type `order_type`. `seed` specifies the account seed, // None will return a whitelisted account -// Returns `account`, `asset` and `order_hash` -fn create_order( +// Returns `account`, `asset`, `order_id` +fn place_order( order_type: OrderSide, seed: Option, -) -> Result<(T::AccountId, Asset, T::Hash), &'static str> { - let (acc, asset, amount, price) = order_common_parameters::(seed)?; - let _ = Call::::make_order { asset, side: order_type.clone(), amount, price } - .dispatch_bypass_filter(RawOrigin::Signed(acc.clone()).into())?; +) -> Result<(T::AccountId, MarketIdOf, OrderId), &'static str> { + let (acc, outcome_asset, outcome_asset_amount, base_asset_amount, market_id) = + order_common_parameters::(seed)?; - if order_type == OrderSide::Bid { - let hash = Pallet::::bids(asset).last().copied().ok_or("No bids found")?; - Ok((acc, asset, hash)) - } else { - let hash = Pallet::::asks(asset).last().copied().ok_or("No asks found")?; - Ok((acc, asset, hash)) + let order_id = >::get(); + let _ = Call::::place_order { + market_id, + outcome_asset, + side: order_type.clone(), + outcome_asset_amount, + base_asset_amount, } + .dispatch_bypass_filter(RawOrigin::Signed(acc.clone()).into())?; + + Ok((acc, market_id, order_id)) } benchmarks! { - cancel_order_ask { - let (caller, asset, order_hash) = create_order::(OrderSide::Ask, None)?; - }: cancel_order(RawOrigin::Signed(caller), asset, order_hash) + remove_order_ask { + let (caller, _, order_id) = place_order::(OrderSide::Ask, None)?; + }: remove_order(RawOrigin::Signed(caller), order_id) - cancel_order_bid { - let (caller, asset, order_hash) = create_order::(OrderSide::Bid, None)?; - }: cancel_order(RawOrigin::Signed(caller), asset, order_hash) + remove_order_bid { + let (caller, _, order_id) = place_order::(OrderSide::Bid, None)?; + }: remove_order(RawOrigin::Signed(caller), order_id) fill_order_ask { let caller = generate_funded_account::(None)?; - let (_, _, order_hash) = create_order::(OrderSide::Ask, Some(0))?; - }: fill_order(RawOrigin::Signed(caller), order_hash) + let (_, _, order_id) = place_order::(OrderSide::Ask, Some(0))?; + }: fill_order(RawOrigin::Signed(caller), order_id, None) fill_order_bid { let caller = generate_funded_account::(None)?; - let (_, _, order_hash) = create_order::(OrderSide::Bid, Some(0))?; - }: fill_order(RawOrigin::Signed(caller), order_hash) + let (_, _, order_id) = place_order::(OrderSide::Bid, Some(0))?; + }: fill_order(RawOrigin::Signed(caller), order_id, None) - make_order_ask { - let (caller, asset, amt, prc) = order_common_parameters::(None)?; - }: make_order(RawOrigin::Signed(caller), asset, OrderSide::Ask, amt, prc) + place_order_ask { + let (caller, outcome_asset, outcome_asset_amount, base_asset_amount, market_id) = order_common_parameters::(None)?; + }: place_order(RawOrigin::Signed(caller), market_id, outcome_asset, OrderSide::Ask, outcome_asset_amount, base_asset_amount) - make_order_bid { - let (caller, asset, amt, prc) = order_common_parameters::(None)?; - }: make_order(RawOrigin::Signed(caller), asset, OrderSide::Bid, amt, prc) + place_order_bid { + let (caller, outcome_asset, outcome_asset_amount, base_asset_amount, market_id) = order_common_parameters::(None)?; + }: place_order(RawOrigin::Signed(caller), market_id, outcome_asset, OrderSide::Bid, outcome_asset_amount, base_asset_amount) impl_benchmark_test_suite!( OrderBook, diff --git a/zrml/orderbook-v1/src/lib.rs b/zrml/orderbook-v1/src/lib.rs index ba80157c7..946896e37 100644 --- a/zrml/orderbook-v1/src/lib.rs +++ b/zrml/orderbook-v1/src/lib.rs @@ -21,373 +21,437 @@ extern crate alloc; +use crate::{types::*, weights::*}; +use alloc::{vec, vec::Vec}; +use core::marker::PhantomData; +use frame_support::{ + dispatch::DispatchResultWithPostInfo, + ensure, + pallet_prelude::{OptionQuery, StorageMap, StorageValue, ValueQuery}, + traits::{Currency, IsType, StorageVersion}, + transactional, PalletId, Twox64Concat, +}; +use frame_system::{ensure_signed, pallet_prelude::OriginFor}; +use orml_traits::{BalanceStatus, MultiCurrency, NamedMultiReservableCurrency}; pub use pallet::*; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; use sp_runtime::{ - traits::{CheckedMul, CheckedSub}, - ArithmeticError, DispatchError, RuntimeDebug, + traits::{CheckedSub, Get, Zero}, + ArithmeticError, Perquintill, SaturatedConversion, Saturating, +}; +use zeitgeist_primitives::{ + traits::MarketCommonsPalletApi, + types::{Asset, Market, MarketStatus, MarketType, ScalarPosition, ScoringRule}, }; -use zeitgeist_primitives::types::Asset; #[cfg(feature = "runtime-benchmarks")] mod benchmarks; pub mod mock; #[cfg(test)] mod tests; +pub mod types; +mod utils; pub mod weights; #[frame_support::pallet] mod pallet { - use crate::{weights::*, Order, OrderSide}; - use core::{cmp, marker::PhantomData}; - use frame_support::{ - dispatch::DispatchResultWithPostInfo, - ensure, - pallet_prelude::{ConstU32, StorageMap, StorageValue, ValueQuery}, - traits::{ - Currency, ExistenceRequirement, Hooks, IsType, ReservableCurrency, StorageVersion, - WithdrawReasons, - }, - transactional, Blake2_128Concat, BoundedVec, Identity, - }; - use frame_system::{ensure_signed, pallet_prelude::OriginFor}; - use orml_traits::{MultiCurrency, MultiReservableCurrency}; - use parity_scale_codec::Encode; - use sp_runtime::{ - traits::{Hash, Zero}, - ArithmeticError, DispatchError, - }; - use zeitgeist_primitives::{traits::MarketId, types::Asset}; + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Shares of outcome assets and native currency + type AssetManager: NamedMultiReservableCurrency< + Self::AccountId, + CurrencyId = Asset>, + ReserveIdentifier = [u8; 8], + >; + + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type MarketCommons: MarketCommonsPalletApi; + + #[pallet::constant] + type PalletId: Get; + + type WeightInfo: WeightInfoZeitgeist; + } /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); - pub(crate) type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; + pub(crate) type BalanceOf = <::AssetManager as MultiCurrency< + ::AccountId, + >>::Balance; + pub(crate) type MarketIdOf = + <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub(crate) type AccountIdOf = ::AccountId; + pub(crate) type OrderOf = Order, BalanceOf, MarketIdOf>; + pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; + pub(crate) type MarketCommonsBalanceOf = + <<::MarketCommons as MarketCommonsPalletApi>::Currency as Currency< + AccountIdOf, + >>::Balance; + pub(crate) type MarketOf = Market< + AccountIdOf, + MarketCommonsBalanceOf, + ::BlockNumber, + MomentOf, + Asset>, + >; + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData); + + #[pallet::storage] + pub type NextOrderId = StorageValue<_, OrderId, ValueQuery>; + + #[pallet::storage] + pub type Orders = StorageMap<_, Twox64Concat, OrderId, OrderOf, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event + where + T: Config, + { + OrderFilled { + order_id: OrderId, + maker: AccountIdOf, + taker: AccountIdOf, + filled: BalanceOf, + unfilled_outcome_asset_amount: BalanceOf, + unfilled_base_asset_amount: BalanceOf, + }, + OrderPlaced { + order_id: OrderId, + order: OrderOf, + }, + OrderRemoved { + order_id: OrderId, + maker: T::AccountId, + }, + } + + #[pallet::error] + pub enum Error { + /// The sender is not the order creator. + NotOrderCreator, + /// The order does not exist. + OrderDoesNotExist, + /// The market is not active. + MarketIsNotActive, + /// The scoring rule is not orderbook. + InvalidScoringRule, + /// The specified amount parameter is too high for the order. + AmountTooHighForOrder, + /// The specified amount parameter is zero. + AmountIsZero, + /// The specified outcome asset is not part of the market. + InvalidOutcomeAsset, + /// The maker partial fill leads to a too low quotient for the next order execution. + MakerPartialFillTooLow, + } #[pallet::call] impl Pallet { + /// Removes an order. + /// + /// # Weight + /// + /// Complexity: `O(1)` #[pallet::call_index(0)] #[pallet::weight( - T::WeightInfo::cancel_order_ask().max(T::WeightInfo::cancel_order_bid()) + T::WeightInfo::remove_order_ask().max(T::WeightInfo::remove_order_bid()) )] #[transactional] - pub fn cancel_order( - origin: OriginFor, - asset: Asset, - order_hash: T::Hash, - ) -> DispatchResultWithPostInfo { + pub fn remove_order(origin: OriginFor, order_id: OrderId) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; - let mut bid = true; - - if let Some(order_data) = Self::order_data(order_hash) { - let maker = &order_data.maker; - ensure!(sender == *maker, Error::::NotOrderCreator); - - match order_data.side { - OrderSide::Bid => { - let cost = order_data.cost()?; - T::Currency::unreserve(maker, cost); - let mut bids = Self::bids(asset); - remove_item::(&mut bids, order_hash); - >::insert(asset, bids); - } - OrderSide::Ask => { - T::Shares::unreserve(order_data.asset, maker, order_data.total); - let mut asks = Self::asks(asset); - remove_item::(&mut asks, order_hash); - >::insert(asset, asks); - bid = false; - } - } - >::remove(order_hash); - } else { - return Err(Error::::OrderDoesNotExist.into()); + let order_data = >::get(order_id).ok_or(Error::::OrderDoesNotExist)?; + + let maker = &order_data.maker; + ensure!(sender == *maker, Error::::NotOrderCreator); + + match order_data.side { + OrderSide::Bid => { + T::AssetManager::unreserve_named( + &Self::reserve_id(), + order_data.base_asset, + maker, + order_data.base_asset_amount, + ); + } + OrderSide::Ask => { + T::AssetManager::unreserve_named( + &Self::reserve_id(), + order_data.outcome_asset, + maker, + order_data.outcome_asset_amount, + ); + } } - if bid { - Ok(Some(T::WeightInfo::cancel_order_bid()).into()) - } else { - Ok(Some(T::WeightInfo::cancel_order_ask()).into()) + >::remove(order_id); + + Self::deposit_event(Event::OrderRemoved { order_id, maker: maker.clone() }); + + match order_data.side { + OrderSide::Bid => Ok(Some(T::WeightInfo::remove_order_bid()).into()), + OrderSide::Ask => Ok(Some(T::WeightInfo::remove_order_ask()).into()), } } + /// Fill an existing order entirely (`maker_partial_fill` = None) + /// or partially (`maker_partial_fill` = Some(partial_amount)). + /// + /// NOTE: The `maker_partial_fill` is the partial amount of what the maker wants to fill. + /// + /// # Weight + /// + /// Complexity: `O(1)` #[pallet::call_index(1)] #[pallet::weight( T::WeightInfo::fill_order_ask().max(T::WeightInfo::fill_order_bid()) )] #[transactional] - pub fn fill_order(origin: OriginFor, order_hash: T::Hash) -> DispatchResultWithPostInfo { - let sender = ensure_signed(origin)?; - let mut bid = true; - - if let Some(order_data) = Self::order_data(order_hash) { - ensure!(order_data.taker.is_none(), Error::::OrderAlreadyTaken); + pub fn fill_order( + origin: OriginFor, + order_id: OrderId, + maker_partial_fill: Option>, + ) -> DispatchResultWithPostInfo { + let taker = ensure_signed(origin)?; - let cost = order_data.cost()?; + let mut order_data = >::get(order_id).ok_or(Error::::OrderDoesNotExist)?; + let market = T::MarketCommons::market(&order_data.market_id)?; + ensure!(market.scoring_rule == ScoringRule::Orderbook, Error::::InvalidScoringRule); + ensure!(market.status == MarketStatus::Active, Error::::MarketIsNotActive); + let base_asset = market.base_asset; - let maker = order_data.maker; + let makers_requested_total = match order_data.side { + OrderSide::Bid => order_data.outcome_asset_amount, + OrderSide::Ask => order_data.base_asset_amount, + }; + let maker_fill = maker_partial_fill.unwrap_or(makers_requested_total); + ensure!(!maker_fill.is_zero(), Error::::AmountIsZero); + ensure!(maker_fill <= makers_requested_total, Error::::AmountTooHighForOrder); - match order_data.side { - OrderSide::Bid => { - T::Shares::ensure_can_withdraw( - order_data.asset, - &sender, - order_data.total, - )?; + let maker = order_data.maker.clone(); - T::Currency::unreserve(&maker, cost); - T::Currency::transfer( - &maker, - &sender, - cost, - ExistenceRequirement::AllowDeath, - )?; + // the reserve of the maker should always be enough to repatriate successfully, e.g. taker gets a little bit less + // it should always ensure that the maker's request (maker_fill) is fully filled + match order_data.side { + OrderSide::Bid => { + T::AssetManager::ensure_can_withdraw( + order_data.outcome_asset, + &taker, + maker_fill, + )?; + + // Note that this always rounds down, i.e. the taker will always get a little bit less than what they asked for. + // This ensures that the reserve of the maker is always enough to repatriate successfully! + let ratio = Perquintill::from_rational( + maker_fill.saturated_into::(), + order_data.outcome_asset_amount.saturated_into::(), + ); + let taker_fill = ratio + .mul_floor(order_data.base_asset_amount.saturated_into::()) + .saturated_into::>(); + + T::AssetManager::repatriate_reserved_named( + &Self::reserve_id(), + base_asset, + &maker, + &taker, + taker_fill, + BalanceStatus::Free, + )?; + + T::AssetManager::transfer( + order_data.outcome_asset, + &taker, + &maker, + maker_fill, + )?; + + order_data.base_asset_amount = order_data + .base_asset_amount + .checked_sub(&taker_fill) + .ok_or(ArithmeticError::Underflow)?; + order_data.outcome_asset_amount = order_data + .outcome_asset_amount + .checked_sub(&maker_fill) + .ok_or(ArithmeticError::Underflow)?; + // this ensures that partial fills, which fill nearly the whole order, are not executed + // this protects the last fill happening without a division by zero for `Perquintill::from_rational` + let is_ratio_quotient_valid = order_data.outcome_asset_amount.is_zero() + || order_data.outcome_asset_amount.saturated_into::() >= 100u128; + ensure!(is_ratio_quotient_valid, Error::::MakerPartialFillTooLow); + } + OrderSide::Ask => { + T::AssetManager::ensure_can_withdraw(base_asset, &taker, maker_fill)?; - T::Shares::transfer(order_data.asset, &sender, &maker, order_data.total)?; - } - OrderSide::Ask => { - T::Currency::ensure_can_withdraw( - &sender, - cost, - WithdrawReasons::all(), - Zero::zero(), - )?; - - T::Shares::unreserve(order_data.asset, &maker, order_data.total); - T::Shares::transfer(order_data.asset, &maker, &sender, order_data.total)?; - - T::Currency::transfer( - &sender, - &maker, - cost, - ExistenceRequirement::AllowDeath, - )?; - bid = false; - } + // Note that this always rounds down, i.e. the taker will always get a little bit less than what they asked for. + // This ensures that the reserve of the maker is always enough to repatriate successfully! + let ratio = Perquintill::from_rational( + maker_fill.saturated_into::(), + order_data.base_asset_amount.saturated_into::(), + ); + let taker_fill = ratio + .mul_floor(order_data.outcome_asset_amount.saturated_into::()) + .saturated_into::>(); + + T::AssetManager::repatriate_reserved_named( + &Self::reserve_id(), + order_data.outcome_asset, + &maker, + &taker, + taker_fill, + BalanceStatus::Free, + )?; + + T::AssetManager::transfer(base_asset, &taker, &maker, maker_fill)?; + + order_data.outcome_asset_amount = order_data + .outcome_asset_amount + .checked_sub(&taker_fill) + .ok_or(ArithmeticError::Underflow)?; + order_data.base_asset_amount = order_data + .base_asset_amount + .checked_sub(&maker_fill) + .ok_or(ArithmeticError::Underflow)?; + // this ensures that partial fills, which fill nearly the whole order, are not executed + // this protects the last fill happening without a division by zero for `Perquintill::from_rational` + let is_ratio_quotient_valid = order_data.base_asset_amount.is_zero() + || order_data.base_asset_amount.saturated_into::() >= 100u128; + ensure!(is_ratio_quotient_valid, Error::::MakerPartialFillTooLow); } + }; + + let unfilled_outcome_asset_amount = order_data.outcome_asset_amount; + let unfilled_base_asset_amount = order_data.base_asset_amount; + let total_unfilled = + unfilled_outcome_asset_amount.saturating_add(unfilled_base_asset_amount); - Self::deposit_event(Event::OrderFilled(sender, order_hash)); + if total_unfilled.is_zero() { + >::remove(order_id); } else { - return Err(Error::::OrderDoesNotExist.into()); + >::insert(order_id, order_data.clone()); } - if bid { - Ok(Some(T::WeightInfo::fill_order_bid()).into()) - } else { - Ok(Some(T::WeightInfo::fill_order_ask()).into()) + Self::deposit_event(Event::OrderFilled { + order_id, + maker, + taker: taker.clone(), + filled: maker_fill, + unfilled_outcome_asset_amount, + unfilled_base_asset_amount, + }); + + match order_data.side { + OrderSide::Bid => Ok(Some(T::WeightInfo::fill_order_bid()).into()), + OrderSide::Ask => Ok(Some(T::WeightInfo::fill_order_ask()).into()), } } + /// Place a new order. + /// + /// # Weight + /// + /// Complexity: `O(1)` #[pallet::call_index(2)] #[pallet::weight( - T::WeightInfo::make_order_ask().max(T::WeightInfo::make_order_bid()) + T::WeightInfo::place_order_ask().max(T::WeightInfo::place_order_bid()) )] #[transactional] - pub fn make_order( + pub fn place_order( origin: OriginFor, - asset: Asset, + #[pallet::compact] market_id: MarketIdOf, + outcome_asset: Asset>, side: OrderSide, - #[pallet::compact] amount: BalanceOf, - #[pallet::compact] price: BalanceOf, + #[pallet::compact] outcome_asset_amount: BalanceOf, + #[pallet::compact] base_asset_amount: BalanceOf, ) -> DispatchResultWithPostInfo { - let sender = ensure_signed(origin)?; + let who = ensure_signed(origin)?; - // Only store nonce in memory for now. - let nonce = >::get(); - let hash = Self::order_hash(&sender, asset, nonce); - let mut bid = true; + let market = T::MarketCommons::market(&market_id)?; + ensure!(market.status == MarketStatus::Active, Error::::MarketIsNotActive); + ensure!(market.scoring_rule == ScoringRule::Orderbook, Error::::InvalidScoringRule); + let market_assets = Self::outcome_assets(market_id, &market); + ensure!( + market_assets.binary_search(&outcome_asset).is_ok(), + Error::::InvalidOutcomeAsset + ); + let base_asset = market.base_asset; + + let order_id = >::get(); + let next_order_id = order_id.checked_add(1).ok_or(ArithmeticError::Overflow)?; - // Love the smell of fresh orders in the morning. let order = Order { + market_id, side: side.clone(), - maker: sender.clone(), - taker: None, - asset, - total: amount, - price, - filled: Zero::zero(), + maker: who.clone(), + outcome_asset, + base_asset, + outcome_asset_amount, + base_asset_amount, }; - let cost = order.cost()?; - match side { OrderSide::Bid => { - ensure!( - T::Currency::can_reserve(&sender, cost), - Error::::InsufficientBalance, - ); - - >::try_mutate(asset, |b: &mut BoundedVec| { - b.try_push(hash).map_err(|_| >::StorageOverflow) - })?; - - T::Currency::reserve(&sender, cost)?; + T::AssetManager::reserve_named( + &Self::reserve_id(), + base_asset, + &who, + base_asset_amount, + )?; } OrderSide::Ask => { - ensure!( - T::Shares::can_reserve(asset, &sender, amount), - Error::::InsufficientBalance, - ); - - >::try_mutate(asset, |a| { - a.try_push(hash).map_err(|_| >::StorageOverflow) - })?; - - T::Shares::reserve(asset, &sender, amount)?; - bid = false; + T::AssetManager::reserve_named( + &Self::reserve_id(), + outcome_asset, + &who, + outcome_asset_amount, + )?; } } - >::insert(hash, Some(order.clone())); - >::try_mutate(|n| { - *n = n.checked_add(1).ok_or(ArithmeticError::Overflow)?; - Ok::<_, DispatchError>(()) - })?; - Self::deposit_event(Event::OrderMade(sender, hash, order)); + >::insert(order_id, order.clone()); + >::put(next_order_id); + Self::deposit_event(Event::OrderPlaced { order_id, order }); - if bid { - Ok(Some(T::WeightInfo::make_order_bid()).into()) - } else { - Ok(Some(T::WeightInfo::make_order_ask()).into()) + match side { + OrderSide::Bid => Ok(Some(T::WeightInfo::place_order_bid()).into()), + OrderSide::Ask => Ok(Some(T::WeightInfo::place_order_ask()).into()), } } } - #[pallet::config] - pub trait Config: frame_system::Config { - type Currency: ReservableCurrency; - - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - type MarketId: MarketId; - - type Shares: MultiReservableCurrency< - Self::AccountId, - Balance = BalanceOf, - CurrencyId = Asset, - >; - - type WeightInfo: WeightInfoZeitgeist; - } - - #[pallet::error] - pub enum Error { - /// Insufficient balance. - InsufficientBalance, - NotOrderCreator, - /// The order was already taken. - OrderAlreadyTaken, - /// The order does not exist. - OrderDoesNotExist, - /// It was tried to append an item to storage beyond the boundaries. - StorageOverflow, - } - - #[pallet::event] - #[pallet::generate_deposit(fn deposit_event)] - pub enum Event - where - T: Config, - { - /// [taker, order_hash] - OrderFilled(::AccountId, ::Hash), - /// [maker, order_hash, order_data] - OrderMade( - ::AccountId, - ::Hash, - Order, T::MarketId>, - ), - } - - #[pallet::hooks] - impl Hooks for Pallet {} - - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(PhantomData); - - #[pallet::storage] - #[pallet::getter(fn asks)] - pub type Asks = StorageMap< - _, - Blake2_128Concat, - Asset, - BoundedVec>, - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn bids)] - pub type Bids = StorageMap< - _, - Blake2_128Concat, - Asset, - BoundedVec>, - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn nonce)] - pub type Nonce = StorageValue<_, u64, ValueQuery>; - - #[pallet::storage] - #[pallet::getter(fn order_data)] - pub type OrderData = StorageMap< - _, - Identity, - T::Hash, - Option, T::MarketId>>, - ValueQuery, - >; - impl Pallet { - pub fn order_hash( - creator: &T::AccountId, - asset: Asset, - nonce: u64, - ) -> T::Hash { - (&creator, asset, nonce).using_encoded(T::Hashing::hash) + /// The reserve ID of the orderbook pallet. + #[inline] + pub fn reserve_id() -> [u8; 8] { + T::PalletId::get().0 } - } - - fn remove_item(items: &mut BoundedVec, item: I) { - let pos = items.iter().position(|&i| i == item).unwrap(); - items.swap_remove(pos); - } -} -#[derive(Clone, Encode, Eq, Decode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] -pub enum OrderSide { - Bid, - Ask, -} - -#[derive(Clone, Encode, Eq, Decode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] -pub struct Order { - side: OrderSide, - maker: AccountId, - taker: Option, - asset: Asset, - total: Balance, - price: Balance, - filled: Balance, -} - -impl Order -where - Balance: CheckedSub + CheckedMul, - MarketId: MaxEncodedLen, -{ - pub fn cost(&self) -> Result { - match self.total.checked_sub(&self.filled) { - Some(subtotal) => match subtotal.checked_mul(&self.price) { - Some(cost) => Ok(cost), - _ => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), - }, - _ => Err(DispatchError::Arithmetic(ArithmeticError::Overflow)), + pub fn outcome_assets( + market_id: MarketIdOf, + market: &MarketOf, + ) -> Vec>> { + match market.market_type { + MarketType::Categorical(categories) => { + let mut assets = Vec::new(); + for i in 0..categories { + assets.push(Asset::CategoricalOutcome(market_id, i)); + } + assets + } + MarketType::Scalar(_) => { + vec![ + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + Asset::ScalarOutcome(market_id, ScalarPosition::Short), + ] + } + } } } } diff --git a/zrml/orderbook-v1/src/mock.rs b/zrml/orderbook-v1/src/mock.rs index d7570fc83..4b42d12c7 100644 --- a/zrml/orderbook-v1/src/mock.rs +++ b/zrml/orderbook-v1/src/mock.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Forecasting Technologies LTD. +// Copyright 2022-2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -26,11 +26,12 @@ use sp_runtime::{ }; use zeitgeist_primitives::{ constants::mock::{ - BlockHashCount, ExistentialDeposit, ExistentialDeposits, MaxLocks, MaxReserves, BASE, + BlockHashCount, ExistentialDeposit, ExistentialDeposits, GetNativeCurrencyId, MaxLocks, + MaxReserves, MinimumPeriod, OrderbookPalletId, PmPalletId, BASE, }, types::{ - AccountIdTest, Amount, Balance, BlockNumber, BlockTest, CurrencyId, Hash, Index, MarketId, - UncheckedExtrinsicTest, + AccountIdTest, Amount, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, CurrencyId, + Hash, Index, MarketId, Moment, UncheckedExtrinsicTest, }, }; @@ -45,17 +46,20 @@ construct_runtime!( UncheckedExtrinsic = UncheckedExtrinsicTest, { Balances: pallet_balances::{Call, Config, Event, Pallet, Storage}, + MarketCommons: zrml_market_commons::{Pallet, Storage}, Orderbook: orderbook_v1::{Call, Event, Pallet}, System: frame_system::{Call, Config, Event, Pallet, Storage}, - Tokens: orml_tokens::{Config, Pallet, Storage}, + Tokens: orml_tokens::{Config, Event, Pallet, Storage}, + AssetManager: orml_currencies::{Call, Pallet, Storage}, + Timestamp: pallet_timestamp::{Pallet}, } ); impl crate::Config for Runtime { - type Currency = Balances; - type RuntimeEvent = (); - type MarketId = MarketId; - type Shares = Tokens; + type AssetManager = AssetManager; + type RuntimeEvent = RuntimeEvent; + type MarketCommons = MarketCommons; + type PalletId = OrderbookPalletId; type WeightInfo = orderbook_v1::weights::WeightInfo; } @@ -69,7 +73,7 @@ impl frame_system::Config for Runtime { type BlockWeights = (); type RuntimeCall = RuntimeCall; type DbWeight = (); - type RuntimeEvent = (); + type RuntimeEvent = RuntimeEvent; type Hash = Hash; type Hashing = BlakeTwo256; type Header = Header; @@ -86,12 +90,19 @@ impl frame_system::Config for Runtime { type OnSetCode = (); } +impl orml_currencies::Config for Runtime { + type GetNativeCurrencyId = GetNativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type WeightInfo = (); +} + impl orml_tokens::Config for Runtime { type Amount = Amount; type Balance = Balance; type CurrencyId = CurrencyId; type DustRemovalWhitelist = Everything; - type RuntimeEvent = (); + type RuntimeEvent = RuntimeEvent; type ExistentialDeposits = ExistentialDeposits; type MaxLocks = (); type MaxReserves = MaxReserves; @@ -104,7 +115,7 @@ impl pallet_balances::Config for Runtime { type AccountStore = System; type Balance = Balance; type DustRemoval = (); - type RuntimeEvent = (); + type RuntimeEvent = RuntimeEvent; type ExistentialDeposit = ExistentialDeposit; type MaxLocks = MaxLocks; type MaxReserves = MaxReserves; @@ -112,6 +123,20 @@ impl pallet_balances::Config for Runtime { type WeightInfo = (); } +impl pallet_timestamp::Config for Runtime { + type MinimumPeriod = MinimumPeriod; + type Moment = Moment; + type OnTimestampSet = (); + type WeightInfo = (); +} + +impl zrml_market_commons::Config for Runtime { + type Currency = Balances; + type MarketId = MarketId; + type PredictionMarketsPalletId = PmPalletId; + type Timestamp = Timestamp; +} + pub struct ExtBuilder { balances: Vec<(AccountIdTest, Balance)>, } @@ -129,6 +154,10 @@ impl ExtBuilder { .assimilate_storage(&mut t) .unwrap(); - t.into() + let mut t: sp_io::TestExternalities = t.into(); + + t.execute_with(|| System::set_block_number(1)); + + t } } diff --git a/zrml/orderbook-v1/src/tests.rs b/zrml/orderbook-v1/src/tests.rs index 4bbbd31af..3c5ae098d 100644 --- a/zrml/orderbook-v1/src/tests.rs +++ b/zrml/orderbook-v1/src/tests.rs @@ -1,3 +1,4 @@ +// Copyright 2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -15,30 +16,110 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -use crate::{mock::*, Error, OrderSide}; +use crate::{mock::*, utils::market_mock, Error, Event, Order, OrderSide, Orders}; use frame_support::{ assert_noop, assert_ok, traits::{Currency, ReservableCurrency}, }; use orml_traits::{MultiCurrency, MultiReservableCurrency}; +use test_case::test_case; use zeitgeist_primitives::{ constants::BASE, - types::{AccountIdTest, Asset}, + types::{AccountIdTest, Asset, ScoringRule}, }; +use zrml_market_commons::{MarketCommonsPalletApi, Markets}; + +#[test_case(ScoringRule::CPMM; "CPMM")] +#[test_case(ScoringRule::RikiddoSigmoidFeeMarketEma; "Rikiddo")] +fn place_order_fails_with_wrong_scoring_rule(scoring_rule: ScoringRule) { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + assert_ok!(MarketCommons::mutate_market(&market_id, |market| { + market.scoring_rule = scoring_rule; + Ok(()) + })); + assert_noop!( + Orderbook::place_order( + RuntimeOrigin::signed(ALICE), + market_id, + Asset::CategoricalOutcome(0, 2), + OrderSide::Bid, + 100, + 250, + ), + Error::::InvalidScoringRule + ); + }); +} + +#[test_case(ScoringRule::CPMM; "CPMM")] +#[test_case(ScoringRule::RikiddoSigmoidFeeMarketEma; "Rikiddo")] +fn fill_order_fails_with_wrong_scoring_rule(scoring_rule: ScoringRule) { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let order_id = 0u128; + + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(ALICE), + market_id, + Asset::CategoricalOutcome(0, 2), + OrderSide::Bid, + 10, + 250, + )); + + assert_ok!(MarketCommons::mutate_market(&market_id, |market| { + market.scoring_rule = scoring_rule; + Ok(()) + })); + + assert_noop!( + Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_id, None), + Error::::InvalidScoringRule + ); + }); +} #[test] -fn it_makes_orders() { +fn it_fails_order_does_not_exist() { ExtBuilder::default().build().execute_with(|| { + let order_id = 0u128; + assert_noop!( + Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_id, None), + Error::::OrderDoesNotExist, + ); + + assert_noop!( + Orderbook::remove_order(RuntimeOrigin::signed(ALICE), order_id), + Error::::OrderDoesNotExist, + ); + }); +} + +#[test] +fn it_places_orders() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + // Give some shares for Bob. - assert_ok!(Tokens::deposit(Asset::CategoricalOutcome(0, 1), &BOB, 100)); + assert_ok!(AssetManager::deposit(Asset::CategoricalOutcome(0, 1), &BOB, 10)); // Make an order from Alice to buy shares. - assert_ok!(Orderbook::make_order( + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(ALICE), + market_id, Asset::CategoricalOutcome(0, 2), OrderSide::Bid, - 25, 10, + 250, )); let reserved_funds = @@ -46,8 +127,9 @@ fn it_makes_orders() { assert_eq!(reserved_funds, 250); // Make an order from Bob to sell shares. - assert_ok!(Orderbook::make_order( + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(BOB), + market_id, Asset::CategoricalOutcome(0, 1), OrderSide::Ask, 10, @@ -60,55 +142,332 @@ fn it_makes_orders() { } #[test] -fn it_takes_orders() { +fn it_fills_ask_orders_fully() { ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let outcome_asset = Asset::CategoricalOutcome(0, 1); // Give some shares for Bob. - assert_ok!(Tokens::deposit(Asset::CategoricalOutcome(0, 1), &BOB, 100)); + assert_ok!(Tokens::deposit(outcome_asset, &BOB, 100)); // Make an order from Bob to sell shares. - assert_ok!(Orderbook::make_order( + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(BOB), - Asset::CategoricalOutcome(0, 1), + market_id, + outcome_asset, OrderSide::Ask, 10, - 5, + 50, )); - let order_hash = Orderbook::order_hash(&BOB, Asset::CategoricalOutcome(0, 1), 0); - assert_ok!(Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_hash)); + let reserved_bob = Tokens::reserved_balance(outcome_asset, &BOB); + assert_eq!(reserved_bob, 10); + + let order_id = 0u128; + assert_ok!(Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_id, None)); + + let reserved_bob = Tokens::reserved_balance(outcome_asset, &BOB); + assert_eq!(reserved_bob, 0); + + System::assert_last_event( + Event::::OrderFilled { + order_id, + maker: BOB, + taker: ALICE, + filled: 50, + unfilled_outcome_asset_amount: 0, + unfilled_base_asset_amount: 0, + } + .into(), + ); let alice_bal = >::free_balance(&ALICE); - let alice_shares = Tokens::free_balance(Asset::CategoricalOutcome(0, 1), &ALICE); + let alice_shares = Tokens::free_balance(outcome_asset, &ALICE); assert_eq!(alice_bal, BASE - 50); assert_eq!(alice_shares, 10); let bob_bal = >::free_balance(&BOB); - let bob_shares = Tokens::free_balance(Asset::CategoricalOutcome(0, 1), &BOB); + let bob_shares = Tokens::free_balance(outcome_asset, &BOB); assert_eq!(bob_bal, BASE + 50); assert_eq!(bob_shares, 90); }); } #[test] -fn it_cancels_orders() { +fn it_fills_bid_orders_fully() { ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let outcome_asset = Asset::CategoricalOutcome(0, 1); + + // Make an order from Bob to sell shares. + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(BOB), + market_id, + outcome_asset, + OrderSide::Bid, + 10, + 50, + )); + + let reserved_bob = Balances::reserved_balance(BOB); + assert_eq!(reserved_bob, 50); + + let order_id = 0u128; + assert_ok!(Tokens::deposit(outcome_asset, &ALICE, 10)); + assert_ok!(Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_id, None)); + + let reserved_bob = Tokens::reserved_balance(outcome_asset, &BOB); + assert_eq!(reserved_bob, 0); + + System::assert_last_event( + Event::::OrderFilled { + order_id, + maker: BOB, + taker: ALICE, + filled: 10, + unfilled_outcome_asset_amount: 0, + unfilled_base_asset_amount: 0, + } + .into(), + ); + + let alice_bal = >::free_balance(&ALICE); + let alice_shares = Tokens::free_balance(outcome_asset, &ALICE); + assert_eq!(alice_bal, BASE + 50); + assert_eq!(alice_shares, 0); + + let bob_bal = >::free_balance(&BOB); + let bob_shares = Tokens::free_balance(outcome_asset, &BOB); + assert_eq!(bob_bal, BASE - 50); + assert_eq!(bob_shares, 10); + }); +} + +#[test] +fn it_fills_bid_orders_partially() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let outcome_asset = Asset::CategoricalOutcome(0, 1); + + // Make an order from Bob to buy outcome tokens. + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(BOB), + market_id, + outcome_asset, + OrderSide::Bid, + 1000, + 5000, + )); + + let reserved_bob = Balances::reserved_balance(BOB); + assert_eq!(reserved_bob, 5000); + + let order_id = 0u128; + assert_ok!(Tokens::deposit(outcome_asset, &ALICE, 1000)); + + // instead of selling 1000 shares, Alice sells 700 shares + let portion = Some(700); + assert_ok!(Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_id, portion,)); + + let order = >::get(order_id).unwrap(); + assert_eq!( + order, + Order { + market_id, + side: OrderSide::Bid, + maker: BOB, + outcome_asset, + base_asset: Asset::Ztg, + // from 1000 to 300 changed (partially filled) + outcome_asset_amount: 300, + base_asset_amount: 1500, + } + ); + + let reserved_bob = Balances::reserved_balance(BOB); + // 5000 - (700 shares * 500 price) = 1500 + assert_eq!(reserved_bob, 1500); + + System::assert_last_event( + Event::::OrderFilled { + order_id, + maker: BOB, + taker: ALICE, + filled: 700, + unfilled_outcome_asset_amount: 300, + unfilled_base_asset_amount: 1500, + } + .into(), + ); + + let alice_bal = >::free_balance(&ALICE); + let alice_shares = Tokens::free_balance(outcome_asset, &ALICE); + assert_eq!(alice_bal, BASE + 3500); + assert_eq!(alice_shares, 300); + + let bob_bal = >::free_balance(&BOB); + let bob_shares = Tokens::free_balance(outcome_asset, &BOB); + // 3500 of base_asset lost, 1500 of base_asset reserved + assert_eq!(bob_bal, BASE - 5000); + assert_eq!(bob_shares, 700); + + let reserved_bob = Balances::reserved_balance(BOB); + assert_eq!(reserved_bob, 1500); + }); +} + +#[test] +fn it_fills_ask_orders_partially() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market.clone()); + + let outcome_asset = Asset::CategoricalOutcome(0, 1); + + assert_ok!(Tokens::deposit(outcome_asset, &BOB, 2000)); + + // Make an order from Bob to sell outcome tokens. + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(BOB), + market_id, + outcome_asset, + OrderSide::Ask, + 1000, + 5000, + )); + + let reserved_bob = Tokens::reserved_balance(outcome_asset, &BOB); + assert_eq!(reserved_bob, 1000); + + let order_id = 0u128; + assert_ok!(Tokens::deposit(market.base_asset, &ALICE, 5000)); + + // instead of buying 5000 of the base asset, Alice buys 700 shares + let portion = Some(700); + assert_ok!(Orderbook::fill_order(RuntimeOrigin::signed(ALICE), order_id, portion,)); + + let order = >::get(order_id).unwrap(); + assert_eq!( + order, + Order { + market_id, + side: OrderSide::Ask, + maker: BOB, + outcome_asset, + base_asset: Asset::Ztg, + // from 1000 to 860 changed (partially filled) + outcome_asset_amount: 860, + // from 5000 to 4300 changed (partially filled) + base_asset_amount: 4300, + } + ); + + let reserved_bob = Tokens::reserved_balance(outcome_asset, &BOB); + assert_eq!(reserved_bob, 860); + + System::assert_last_event( + Event::::OrderFilled { + order_id, + maker: BOB, + taker: ALICE, + filled: 700, + unfilled_outcome_asset_amount: 860, + unfilled_base_asset_amount: 4300, + } + .into(), + ); + + let alice_bal = >::free_balance(&ALICE); + let alice_shares = Tokens::free_balance(outcome_asset, &ALICE); + assert_eq!(alice_bal, BASE - 700); + assert_eq!(alice_shares, 140); + + let bob_bal = >::free_balance(&BOB); + let bob_shares = Tokens::free_balance(outcome_asset, &BOB); + assert_eq!(bob_bal, BASE + 700); + // ask order was adjusted from 1000 to 860, and bob had 2000 shares at start + assert_eq!(bob_shares, 1000); + + let reserved_bob = Tokens::reserved_balance(outcome_asset, &BOB); + assert_eq!(reserved_bob, 860); + }); +} + +#[test] +fn it_removes_orders() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + // Make an order from Alice to buy shares. let share_id = Asset::CategoricalOutcome(0, 2); - assert_ok!(Orderbook::make_order( + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(ALICE), + market_id, share_id, OrderSide::Bid, 25, 10 )); - let order_hash = Orderbook::order_hash(&ALICE, share_id, 0); + let order_id = 0u128; + System::assert_last_event( + Event::::OrderPlaced { + order_id: 0, + order: Order { + market_id, + side: OrderSide::Bid, + maker: ALICE, + outcome_asset: share_id, + base_asset: Asset::Ztg, + outcome_asset_amount: 25, + base_asset_amount: 10, + }, + } + .into(), + ); + + let order = >::get(order_id).unwrap(); + assert_eq!( + order, + Order { + market_id, + side: OrderSide::Bid, + maker: ALICE, + outcome_asset: share_id, + base_asset: Asset::Ztg, + outcome_asset_amount: 25, + base_asset_amount: 10, + } + ); assert_noop!( - Orderbook::cancel_order(RuntimeOrigin::signed(BOB), share_id, order_hash), + Orderbook::remove_order(RuntimeOrigin::signed(BOB), order_id), Error::::NotOrderCreator, ); - assert_ok!(Orderbook::cancel_order(RuntimeOrigin::signed(ALICE), share_id, order_hash)); + let reserved_funds = + >::reserved_balance(&ALICE); + assert_eq!(reserved_funds, 10); + + assert_ok!(Orderbook::remove_order(RuntimeOrigin::signed(ALICE), order_id)); + + let reserved_funds = + >::reserved_balance(&ALICE); + assert_eq!(reserved_funds, 0); + + assert!(>::get(order_id).is_none()); + + System::assert_last_event(Event::::OrderRemoved { order_id, maker: ALICE }.into()); }); } diff --git a/zrml/orderbook-v1/src/types.rs b/zrml/orderbook-v1/src/types.rs new file mode 100644 index 000000000..b0e1d2b73 --- /dev/null +++ b/zrml/orderbook-v1/src/types.rs @@ -0,0 +1,40 @@ +// Copyright 2023 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; +use zeitgeist_primitives::types::Asset; + +pub type OrderId = u128; + +#[derive(Clone, Encode, Eq, Decode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +pub enum OrderSide { + Bid, + Ask, +} + +#[derive(Clone, Encode, Eq, Decode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +pub struct Order { + pub market_id: MarketId, + pub side: OrderSide, + pub maker: AccountId, + pub outcome_asset: Asset, + pub base_asset: Asset, + pub outcome_asset_amount: Balance, + pub base_asset_amount: Balance, +} diff --git a/zrml/orderbook-v1/src/utils.rs b/zrml/orderbook-v1/src/utils.rs new file mode 100644 index 000000000..bac412f2a --- /dev/null +++ b/zrml/orderbook-v1/src/utils.rs @@ -0,0 +1,66 @@ +// Copyright 2023 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(any(feature = "runtime-benchmarks", test))] + +use crate::*; +use frame_support::traits::Currency; +use sp_runtime::traits::AccountIdConversion; +use zeitgeist_primitives::{ + traits::MarketCommonsPalletApi, + types::{ + Asset, Deadlines, Market, MarketCreation, MarketDisputeMechanism, MarketPeriod, + MarketStatus, MarketType, ScoringRule, + }, +}; + +type CurrencyOf = <::MarketCommons as MarketCommonsPalletApi>::Currency; +type BalanceOf = as Currency<::AccountId>>::Balance; +type MarketOf = Market< + ::AccountId, + BalanceOf, + ::BlockNumber, + MomentOf, + Asset>, +>; + +pub(crate) fn market_mock() -> MarketOf +where + T: crate::Config, +{ + Market { + base_asset: Asset::Ztg, + creation: MarketCreation::Permissionless, + creator_fee: sp_runtime::Perbill::zero(), + creator: T::PalletId::get().into_account_truncating(), + market_type: MarketType::Categorical(64u16), + dispute_mechanism: Some(MarketDisputeMechanism::Authorized), + metadata: Default::default(), + oracle: T::PalletId::get().into_account_truncating(), + period: MarketPeriod::Block(Default::default()), + deadlines: Deadlines { + grace_period: 1_u32.into(), + oracle_duration: 1_u32.into(), + dispute_duration: 1_u32.into(), + }, + report: None, + resolved_outcome: None, + scoring_rule: ScoringRule::Orderbook, + status: MarketStatus::Active, + bonds: Default::default(), + } +} diff --git a/zrml/orderbook-v1/src/weights.rs b/zrml/orderbook-v1/src/weights.rs index 4d02a09ba..9b173638b 100644 --- a/zrml/orderbook-v1/src/weights.rs +++ b/zrml/orderbook-v1/src/weights.rs @@ -1,5 +1,4 @@ -// Copyright 2022-2023 Forecasting Technologies LTD. -// Copyright 2021-2022 Zeitgeist PM LLC. +// Copyright 2023 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -15,38 +14,31 @@ // // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . - //! Autogenerated weights for zrml_orderbook_v1 //! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 -//! DATE: 2021-08-20, STEPS: `[0, ]`, REPEAT: 30000, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: `2023-09-14`, STEPS: `10`, REPEAT: `1000`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `chralt`, CPU: `` +//! EXECUTION: `Some(Wasm)`, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` // Executed Command: -// target/release/zeitgeist +// ./target/release/zeitgeist // benchmark -// --chain -// dev -// --execution -// wasm -// --extrinsic -// * -// --output -// ./zrml/orderbook-v1/src/weights.rs -// --pallet -// zrml-orderbook-v1 -// --repeat -// 30000 -// --steps -// 0 -// --template -// ./misc/weight_template.hbs -// --wasm-execution -// compiled +// pallet +// --chain=dev +// --steps=10 +// --repeat=1000 +// --pallet=zrml_orderbook_v1 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./zrml/orderbook-v1/src/weights.rs +// --template=./misc/weight_template.hbs #![allow(unused_parens)] #![allow(unused_imports)] -#![allow(clippy::unnecessary_cast)] use core::marker::PhantomData; use frame_support::{traits::Get, weights::Weight}; @@ -54,45 +46,117 @@ use frame_support::{traits::Get, weights::Weight}; /// Trait containing the required functions for weight retrival within /// zrml_orderbook_v1 (automatically generated) pub trait WeightInfoZeitgeist { - fn cancel_order_ask() -> Weight; - fn cancel_order_bid() -> Weight; + fn remove_order_ask() -> Weight; + fn remove_order_bid() -> Weight; fn fill_order_ask() -> Weight; fn fill_order_bid() -> Weight; - fn make_order_ask() -> Weight; - fn make_order_bid() -> Weight; + fn place_order_ask() -> Weight; + fn place_order_bid() -> Weight; } /// Weight functions for zrml_orderbook_v1 (automatically generated) pub struct WeightInfo(PhantomData); impl WeightInfoZeitgeist for WeightInfo { - fn cancel_order_ask() -> Weight { - Weight::from_ref_time(53_301_000) - .saturating_add(T::DbWeight::get().reads(3 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + /// Storage: Orderbook Orders (r:1 w:1) + /// Proof: Orderbook Orders (max_values: None, max_size: Some(175), added: 2650, mode: MaxEncodedLen) + /// Storage: Tokens Reserves (r:1 w:1) + /// Proof: Tokens Reserves (max_values: None, max_size: Some(1276), added: 3751, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:1 w:1) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + fn remove_order_ask() -> Weight { + // Proof Size summary in bytes: + // Measured: `635` + // Estimated: `8999` + // Minimum execution time: 28_000 nanoseconds. + Weight::from_parts(31_000_000, 8999) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } - fn cancel_order_bid() -> Weight { - Weight::from_ref_time(49_023_000) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(2 as u64)) + /// Storage: Orderbook Orders (r:1 w:1) + /// Proof: Orderbook Orders (max_values: None, max_size: Some(175), added: 2650, mode: MaxEncodedLen) + /// Storage: Balances Reserves (r:1 w:1) + /// Proof: Balances Reserves (max_values: None, max_size: Some(1249), added: 3724, mode: MaxEncodedLen) + fn remove_order_bid() -> Weight { + // Proof Size summary in bytes: + // Measured: `316` + // Estimated: `6374` + // Minimum execution time: 27_000 nanoseconds. + Weight::from_parts(29_000_000, 6374) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } + /// Storage: Orderbook Orders (r:1 w:1) + /// Proof: Orderbook Orders (max_values: None, max_size: Some(175), added: 2650, mode: MaxEncodedLen) + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: Tokens Reserves (r:1 w:1) + /// Proof: Tokens Reserves (max_values: None, max_size: Some(1276), added: 3751, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:2 w:2) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) fn fill_order_ask() -> Weight { - Weight::from_ref_time(119_376_000) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + // Proof Size summary in bytes: + // Measured: `1304` + // Estimated: `17220` + // Minimum execution time: 70_000 nanoseconds. + Weight::from_parts(73_000_000, 17220) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) } + /// Storage: Orderbook Orders (r:1 w:1) + /// Proof: Orderbook Orders (max_values: None, max_size: Some(175), added: 2650, mode: MaxEncodedLen) + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:2 w:2) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: Balances Reserves (r:1 w:1) + /// Proof: Balances Reserves (max_values: None, max_size: Some(1249), added: 3724, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) fn fill_order_bid() -> Weight { - Weight::from_ref_time(119_917_000) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + // Proof Size summary in bytes: + // Measured: `1243` + // Estimated: `17193` + // Minimum execution time: 62_000 nanoseconds. + Weight::from_parts(64_000_000, 17193) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) } - fn make_order_ask() -> Weight { - Weight::from_ref_time(80_092_000) - .saturating_add(T::DbWeight::get().reads(3 as u64)) - .saturating_add(T::DbWeight::get().writes(4 as u64)) + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: Orderbook NextOrderId (r:1 w:1) + /// Proof: Orderbook NextOrderId (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Tokens Reserves (r:1 w:1) + /// Proof: Tokens Reserves (max_values: None, max_size: Some(1276), added: 3751, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:1 w:1) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: Orderbook Orders (r:0 w:1) + /// Proof: Orderbook Orders (max_values: None, max_size: Some(175), added: 2650, mode: MaxEncodedLen) + fn place_order_ask() -> Weight { + // Proof Size summary in bytes: + // Measured: `588` + // Estimated: `9876` + // Minimum execution time: 35_000 nanoseconds. + Weight::from_parts(38_000_000, 9876) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) } - fn make_order_bid() -> Weight { - Weight::from_ref_time(62_027_000) - .saturating_add(T::DbWeight::get().reads(2 as u64)) - .saturating_add(T::DbWeight::get().writes(3 as u64)) + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: Orderbook NextOrderId (r:1 w:1) + /// Proof: Orderbook NextOrderId (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: Balances Reserves (r:1 w:1) + /// Proof: Balances Reserves (max_values: None, max_size: Some(1249), added: 3724, mode: MaxEncodedLen) + /// Storage: Orderbook Orders (r:0 w:1) + /// Proof: Orderbook Orders (max_values: None, max_size: Some(175), added: 2650, mode: MaxEncodedLen) + fn place_order_bid() -> Weight { + // Proof Size summary in bytes: + // Measured: `330` + // Estimated: `7251` + // Minimum execution time: 31_000 nanoseconds. + Weight::from_parts(32_000_000, 7251) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } } diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index a8906a493..959f4e39a 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -332,7 +332,10 @@ mod pallet { T::DestroyOrigin::ensure_origin(origin)?; let market = >::market(&market_id)?; - ensure!(market.scoring_rule == ScoringRule::CPMM, Error::::InvalidScoringRule); + ensure!( + matches!(market.scoring_rule, ScoringRule::CPMM | ScoringRule::Orderbook), + Error::::InvalidScoringRule + ); let market_status = market.status; let market_account = >::market_account(market_id); @@ -532,7 +535,7 @@ mod pallet { ); match m.scoring_rule { - ScoringRule::CPMM => { + ScoringRule::CPMM | ScoringRule::Orderbook => { m.status = MarketStatus::Active; } ScoringRule::RikiddoSigmoidFeeMarketEma => { @@ -1341,7 +1344,10 @@ mod pallet { ensure!(amount != BalanceOf::::zero(), Error::::ZeroAmount); let market = >::market(&market_id)?; - ensure!(market.scoring_rule == ScoringRule::CPMM, Error::::InvalidScoringRule); + ensure!( + matches!(market.scoring_rule, ScoringRule::CPMM | ScoringRule::Orderbook), + Error::::InvalidScoringRule + ); Self::ensure_market_is_active(&market)?; let market_account = >::market_account(market_id); @@ -2243,7 +2249,10 @@ mod pallet { T::AssetManager::free_balance(market.base_asset, &who) >= amount, Error::::NotEnoughBalance ); - ensure!(market.scoring_rule == ScoringRule::CPMM, Error::::InvalidScoringRule); + ensure!( + matches!(market.scoring_rule, ScoringRule::CPMM | ScoringRule::Orderbook), + Error::::InvalidScoringRule + ); Self::ensure_market_is_active(&market)?; let market_account = >::market_account(market_id); @@ -3064,7 +3073,7 @@ mod pallet { } let status: MarketStatus = match creation { MarketCreation::Permissionless => match scoring_rule { - ScoringRule::CPMM => MarketStatus::Active, + ScoringRule::CPMM | ScoringRule::Orderbook => MarketStatus::Active, ScoringRule::RikiddoSigmoidFeeMarketEma => MarketStatus::CollectingSubsidy, }, MarketCreation::Advised => MarketStatus::Proposed, diff --git a/zrml/swaps/src/lib.rs b/zrml/swaps/src/lib.rs index d3e5cf5a6..69b2b74a5 100644 --- a/zrml/swaps/src/lib.rs +++ b/zrml/swaps/src/lib.rs @@ -1938,6 +1938,9 @@ mod pallet { let pool_amount = >::zero(); (pool_status, total_subsidy, total_weight, weights, pool_amount) } + ScoringRule::Orderbook => { + return Err(Error::::InvalidScoringRule.into()); + } }; let pool = Pool { assets: sorted_assets, @@ -2496,6 +2499,9 @@ mod pallet { T::RikiddoSigmoidFeeMarketEma::cost(pool_id, &outstanding_after)?; cost_before.checked_sub(&cost_after).ok_or(ArithmeticError::Overflow)? } + ScoringRule::Orderbook => { + return Err(Error::::InvalidScoringRule.into()); + } }; if let Some(maao) = min_asset_amount_out { @@ -2545,6 +2551,7 @@ mod pallet { ScoringRule::RikiddoSigmoidFeeMarketEma => Ok( T::WeightInfo::swap_exact_amount_in_rikiddo(pool.assets.len().saturated_into()), ), + ScoringRule::Orderbook => Err(Error::::InvalidScoringRule.into()), } } @@ -2652,6 +2659,9 @@ mod pallet { T::RikiddoSigmoidFeeMarketEma::cost(pool_id, &outstanding_after)?; cost_after.checked_sub(&cost_before).ok_or(ArithmeticError::Overflow)? } + ScoringRule::Orderbook => { + return Err(Error::::InvalidScoringRule.into()); + } }; if asset_in == pool.base_asset && !handle_fees && to_adjust_in_value { @@ -2713,6 +2723,7 @@ mod pallet { pool.assets.len().saturated_into(), )) } + ScoringRule::Orderbook => Err(Error::::InvalidScoringRule.into()), } } } diff --git a/zrml/swaps/src/utils.rs b/zrml/swaps/src/utils.rs index 615611697..a34e2e719 100644 --- a/zrml/swaps/src/utils.rs +++ b/zrml/swaps/src/utils.rs @@ -216,6 +216,9 @@ where return Err(Error::::UnsupportedTrade.into()); } } + ScoringRule::Orderbook => { + return Err(Error::::InvalidScoringRule.into()); + } } let spot_price_after = @@ -230,6 +233,9 @@ where spot_price_before.saturating_sub(spot_price_after) < 20u8.into(), Error::::MathApproximation ), + ScoringRule::Orderbook => { + return Err(Error::::InvalidScoringRule.into()); + } } if let Some(max_price) = p.max_price { @@ -250,6 +256,9 @@ where let volume = if p.asset_in == base_asset { asset_amount_in } else { asset_amount_out }; T::RikiddoSigmoidFeeMarketEma::update_volume(p.pool_id, volume)?; } + ScoringRule::Orderbook => { + return Err(Error::::InvalidScoringRule.into()); + } } (p.event)(SwapEvent {