diff --git a/CODEOWNERS b/CODEOWNERS
index 74c117914..00e47700b 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -11,6 +11,7 @@
/zrml/authorized/ @Chralt98
/zrml/court/ @Chralt98
/zrml/global-disputes/ @Chralt98
+/zrml/neo-swaps/ @maltekliemann
/zrml/prediction-markets/ @maltekliemann
/zrml/rikiddo/ @sea212
/zrml/simple-disputes/ @Chralt98
@@ -19,4 +20,4 @@
# Skip weight and Cargo.toml files
**/weights.rs
-**/Cargo.toml
\ No newline at end of file
+**/Cargo.toml
diff --git a/Cargo.lock b/Cargo.lock
index 0fdc7e3d1..d97dc2c9c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -473,6 +473,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+[[package]]
+name = "az"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973"
+
[[package]]
name = "backtrace"
version = "0.3.68"
@@ -617,6 +623,7 @@ dependencies = [
"zrml-global-disputes",
"zrml-liquidity-mining",
"zrml-market-commons",
+ "zrml-neo-swaps",
"zrml-orderbook-v1",
"zrml-prediction-markets",
"zrml-rikiddo",
@@ -903,6 +910,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+[[package]]
+name = "bytemuck"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
+
[[package]]
name = "byteorder"
version = "1.4.3"
@@ -1223,6 +1236,7 @@ dependencies = [
"pallet-treasury",
"pallet-utility",
"pallet-vesting",
+ "zeitgeist-primitives",
]
[[package]]
@@ -2745,6 +2759,19 @@ dependencies = [
"scale-info",
]
+[[package]]
+name = "fixed"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36a65312835c1097a0c926ff3702df965285fadc33d948b87397ff8961bad881"
+dependencies = [
+ "az",
+ "bytemuck",
+ "half",
+ "num-traits",
+ "typenum 1.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "fixed-hash"
version = "0.8.0"
@@ -3377,6 +3404,12 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
[[package]]
name = "handlebars"
version = "4.3.7"
@@ -3569,6 +3602,20 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+[[package]]
+name = "hydra-dx-math"
+version = "7.4.3"
+source = "git+https://github.com/galacticcouncil/HydraDX-node?tag=v18.0.0#6173a8b0661582247eed774330aa8fa6d99d524d"
+dependencies = [
+ "fixed",
+ "num-traits",
+ "parity-scale-codec",
+ "primitive-types",
+ "scale-info",
+ "sp-arithmetic",
+ "sp-std",
+]
+
[[package]]
name = "hyper"
version = "0.14.27"
@@ -14325,8 +14372,10 @@ name = "zeitgeist-primitives"
version = "0.4.0"
dependencies = [
"arbitrary",
+ "fixed",
"frame-support",
"frame-system",
+ "more-asserts",
"orml-currencies",
"orml-tokens",
"orml-traits",
@@ -14337,6 +14386,7 @@ dependencies = [
"sp-core",
"sp-runtime",
"test-case",
+ "typenum 1.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -14431,6 +14481,7 @@ dependencies = [
"zrml-global-disputes",
"zrml-liquidity-mining",
"zrml-market-commons",
+ "zrml-neo-swaps",
"zrml-orderbook-v1",
"zrml-prediction-markets",
"zrml-rikiddo",
@@ -14555,6 +14606,50 @@ dependencies = [
"zeitgeist-primitives",
]
+[[package]]
+name = "zrml-neo-swaps"
+version = "0.4.0"
+dependencies = [
+ "fixed",
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "hydra-dx-math",
+ "more-asserts",
+ "orml-asset-registry",
+ "orml-currencies",
+ "orml-tokens",
+ "orml-traits",
+ "pallet-balances",
+ "pallet-randomness-collective-flip",
+ "pallet-timestamp",
+ "pallet-treasury",
+ "pallet-xcm",
+ "parity-scale-codec",
+ "scale-info",
+ "serde",
+ "sp-api",
+ "sp-io",
+ "sp-runtime",
+ "substrate-fixed",
+ "test-case",
+ "typenum 1.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "xcm",
+ "xcm-builder",
+ "zeitgeist-primitives",
+ "zrml-authorized",
+ "zrml-court",
+ "zrml-global-disputes",
+ "zrml-liquidity-mining",
+ "zrml-market-commons",
+ "zrml-neo-swaps",
+ "zrml-prediction-markets",
+ "zrml-prediction-markets-runtime-api",
+ "zrml-rikiddo",
+ "zrml-simple-disputes",
+ "zrml-swaps",
+]
+
[[package]]
name = "zrml-orderbook-v1"
version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index 899a30cb3..93e07a943 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ default-members = [
"zrml/global-disputes",
"zrml/liquidity-mining",
"zrml/market-commons",
+ "zrml/neo-swaps",
"zrml/orderbook-v1",
"zrml/prediction-markets",
"zrml/prediction-markets/runtime-api",
@@ -31,6 +32,7 @@ members = [
"zrml/global-disputes",
"zrml/liquidity-mining",
"zrml/market-commons",
+ "zrml/neo-swaps",
"zrml/orderbook-v1",
"zrml/orderbook-v1/fuzz",
"zrml/prediction-markets",
@@ -229,6 +231,7 @@ zrml-court = { path = "zrml/court", default-features = false }
zrml-global-disputes = { path = "zrml/global-disputes", default-features = false }
zrml-liquidity-mining = { path = "zrml/liquidity-mining", default-features = false }
zrml-market-commons = { path = "zrml/market-commons", default-features = false }
+zrml-neo-swaps = { path = "zrml/neo-swaps", default-features = false }
zrml-orderbook-v1 = { path = "zrml/orderbook-v1", default-features = false }
zrml-prediction-markets = { path = "zrml/prediction-markets", default-features = false }
zrml-prediction-markets-runtime-api = { path = "zrml/prediction-markets/runtime-api", default-features = false }
@@ -250,6 +253,9 @@ url = "2.2.2"
arbitrary = { version = "1.3.0", default-features = false }
arrayvec = { version = "0.7.4", default-features = false }
cfg-if = { version = "1.0.0" }
+fixed = { version = "=1.15.0", default-features = false, features = ["num-traits"] }
+# Using math code directly from the HydraDX node repository as https://github.com/galacticcouncil/hydradx-math is outdated and has been archived in May 2023.
+hydra-dx-math = { git = "https://github.com/galacticcouncil/HydraDX-node", package = "hydra-dx-math", tag = "v18.0.0", default-features = false }
# Hashbrown works in no_std by default and default features are used in Rikiddo
hashbrown = { version = "0.12.3", default-features = true }
hex-literal = { version = "0.3.4", default-features = false }
@@ -258,6 +264,7 @@ num-traits = { version = "0.2.15", default-features = false }
rand = { version = "0.8.5", default-features = false }
rand_chacha = { version = "0.3.1", default-features = false }
serde = { version = "1.0.152", default-features = false }
+typenum = { version = "1.15.0", default-features = false }
[profile.dev.package]
blake2 = { opt-level = 3 }
diff --git a/README.md b/README.md
index b31e630fb..62263e619 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,18 @@
# Zeitgeist: An Evolving Blockchain for Prediction Markets and Futarchy
-![Rust](https://github.com/zeitgeistpm/zeitgeist/workflows/Rust/badge.svg) [![Codecov](https://codecov.io/gh/zeitgeistpm/zeitgeist/branch/main/graph/badge.svg)](https://codecov.io/gh/zeitgeistpm/zeitgeist) [![Discord](https://img.shields.io/badge/discord-https%3A%2F%2Fdiscord.gg%2FMD3TbH3ctv-purple)](https://discord.gg/MD3TbH3ctv) [![Telegram](https://img.shields.io/badge/telegram-https%3A%2F%2Ft.me%2Fzeitgeist__official-blue)](https://t.me/zeitgeist_official)
+![Rust](https://github.com/zeitgeistpm/zeitgeist/workflows/Rust/badge.svg)
+[![Codecov](https://codecov.io/gh/zeitgeistpm/zeitgeist/branch/main/graph/badge.svg)](https://codecov.io/gh/zeitgeistpm/zeitgeist)
+[![Discord](https://img.shields.io/badge/discord-https%3A%2F%2Fdiscord.gg%2FMD3TbH3ctv-purple)](https://discord.gg/MD3TbH3ctv)
+[![Telegram](https://img.shields.io/badge/telegram-https%3A%2F%2Ft.me%2Fzeitgeist__official-blue)](https://t.me/zeitgeist_official)
Zeitgeist is a decentralized network for creating, betting on, and resolving
prediction markets. The platform's native currency, the ZTG, is used to sway the
direction of the network, and as a means of last-call dispute resolution.
Additionally, Zeitgeist is a protocol for efficient trading of prediction market
shares and will one day become the backbone of the decentralized finance
-ecosystem by allowing traders to create complex financial contracts on
-virtually _anything_.
+ecosystem by allowing traders to create complex financial contracts on virtually
+_anything_.
## Modules
@@ -27,6 +30,9 @@ virtually _anything_.
liquidity to swap pools.
- [market-commons](./zrml/market-commons) - Contains common operations on
markets that are used by multiple pallets.
+- [neo-swaps](./zrml/neo-swaps) - An implementation of the Logarithmic Market
+ Scoring Rule as constant function market maker, tailor-made for decentralized
+ combinatorial markets and Futarchy.
- [orderbook-v1](./zrml/orderbook-v1) - A naive orderbook implementation that's
only part of Zeitgeist's PoC. Will be replaced by a v2 orderbook that uses
0x-style hybrid on-chain and off-chain trading.
diff --git a/docs/changelog_for_devs.md b/docs/changelog_for_devs.md
index fbdf342e4..29f993350 100644
--- a/docs/changelog_for_devs.md
+++ b/docs/changelog_for_devs.md
@@ -12,6 +12,38 @@ As of 0.3.9, the changelog's format is based on
components which query the chain's storage, the extrinsics or the runtime
APIs/RPC interface.
+## v0.4.1
+
+### Added
+
+- Implement AMM-2.0-light in the form of zrml-neo-swaps. The new pallet has the
+ following dispatchables:
+
+ - `buy`: Buy outcome tokens from the specified market.
+ - `sell`: Sell outcome tokens to the specified market.
+ - `join`: Join the liquidity pool for the specified market.
+ - `exit`: Exit the liquidity pool for the specified market.
+ - `withdraw_fees`: Withdraw swap fees from the specified market.
+ - `deploy_pool`: Deploy a pool for the specified market and provide liquidity.
+
+ The new pallet has the following events:
+
+ - `BuyExecuted { who, market_id, asset_out, amount_in, amount_out, swap_fee_amount, external_fee_amount }`:
+ Informant bought a position.
+ - `SellExecuted { who, market_id, asset_in, amount_in, amount_out, swap_fee_amount, external_fee_amount }`:
+ Informants sold a position.
+ - `FeesWithdrawn { who }`: Liquidity provider withdrew fees.
+ - `JoinExecuted { who, market_id, pool_shares_amount, amounts_in, new_liquidity_parameter }`:
+ Liquidity provider joined the pool.
+ - `ExitExecuted { who, market_id, pool_shares_amount, amounts_out, new_liquidity_parameter }`:
+ Liquidity provider left the pool.
+ - `PoolDeployed { who, market_id, pool_shares_amount, amounts_in, liquidity_parameter }`:
+ Pool was created.
+ - `PoolDestroyed { who, market_id, pool_shares_amount, amounts_out }`: Pool
+ was destroyed.
+
+ For details, please refer to the `README.md` and the in-file documentation.
+
## v0.4.0
[#976]: https://github.com/zeitgeistpm/zeitgeist/pull/976
diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml
index 4c2360e79..82e613543 100644
--- a/primitives/Cargo.toml
+++ b/primitives/Cargo.toml
@@ -1,5 +1,6 @@
[dependencies]
arbitrary = { workspace = true, optional = true }
+fixed = { workspace = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
orml-currencies = { workspace = true }
@@ -13,7 +14,9 @@ sp-core = { workspace = true }
sp-runtime = { workspace = true }
[dev-dependencies]
+more-asserts = { workspace = true }
test-case = { workspace = true }
+typenum = { workspace = true }
[features]
default = ["std"]
diff --git a/primitives/src/constants.rs b/primitives/src/constants.rs
index d5e32ef71..3841666a1 100644
--- a/primitives/src/constants.rs
+++ b/primitives/src/constants.rs
@@ -37,7 +37,8 @@ pub const BLOCKS_PER_MINUTE: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as Bloc
pub const BLOCKS_PER_HOUR: BlockNumber = BLOCKS_PER_MINUTE * 60; // 300
// Definitions for currency
-pub const BASE: u128 = 10_000_000_000;
+pub const DECIMALS: u8 = 10;
+pub const BASE: u128 = 10u128.pow(DECIMALS as u32);
pub const CENT: Balance = BASE / 100; // 100_000_000
pub const MILLI: Balance = CENT / 10; // 10_000_000
pub const MICRO: Balance = MILLI / 1000; // 10_000
@@ -82,6 +83,9 @@ pub const GLOBAL_DISPUTES_LOCK_ID: [u8; 8] = *b"zge/gdlk";
/// Pallet identifier, mainly used for named balance reserves.
pub const LM_PALLET_ID: PalletId = PalletId(*b"zge/lymg");
+// NeoSwaps
+pub const NS_PALLET_ID: PalletId = PalletId(*b"zge/neos");
+
// Prediction Markets
/// The maximum allowed market life time, measured in blocks.
pub const MAX_MARKET_LIFETIME: BlockNumber = 4 * BLOCKS_PER_YEAR;
diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs
index 5f1ec243c..487f252d0 100644
--- a/primitives/src/constants/mock.rs
+++ b/primitives/src/constants/mock.rs
@@ -70,6 +70,13 @@ parameter_types! {
pub const LiquidityMiningPalletId: PalletId = PalletId(*b"zge/lymg");
}
+// NeoSwaps
+parameter_types! {
+ pub storage NeoExitFee: Balance = CENT;
+ pub const NeoMaxSwapFee: Balance = 10 * CENT;
+ pub const NeoSwapsPalletId: PalletId = PalletId(*b"zge/neos");
+}
+
// Prediction Market parameters
parameter_types! {
pub const AdvisoryBond: Balance = 25 * CENT;
@@ -146,11 +153,10 @@ parameter_types! {
}
parameter_type_with_key! {
- // Well, not every asset is a currency ¯\_(ツ)_/¯
pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance {
match currency_id {
Asset::Ztg => ExistentialDeposit::get(),
- _ => 0
+ _ => 10
}
};
}
diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs
index 0bb25dee5..5a3d424d8 100644
--- a/primitives/src/lib.rs
+++ b/primitives/src/lib.rs
@@ -1,3 +1,4 @@
+// Copyright 2023 Forecasting Technologies LTD.
// Copyright 2021-2022 Zeitgeist PM LLC.
//
// This file is part of Zeitgeist.
@@ -22,6 +23,7 @@ extern crate alloc;
mod asset;
pub mod constants;
mod market;
+pub mod math;
mod max_runtime_usize;
mod outcome_report;
mod pool;
diff --git a/primitives/src/math/check_arithm_rslt.rs b/primitives/src/math/check_arithm_rslt.rs
new file mode 100644
index 000000000..1bd010f06
--- /dev/null
+++ b/primitives/src/math/check_arithm_rslt.rs
@@ -0,0 +1,81 @@
+// Copyright 2023 Forecasting Technologies LTD.
+// Copyright 2021-2022 Zeitgeist PM LLC.
+//
+// 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 .
+//
+// This file incorporates work covered by the license above but
+// published without copyright notice by Balancer Labs
+// (, contact@balancer.finance) in the
+// balancer-core repository
+// .
+
+use crate::math::consts::ARITHM_OF;
+use frame_support::dispatch::DispatchError;
+use sp_runtime::traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub};
+
+/// Check Arithmetic - Result
+///
+/// Checked arithmetic operations returning `Result<_, DispatchError>`.
+pub trait CheckArithmRslt: CheckedAdd + CheckedDiv + CheckedMul + CheckedSub {
+ /// Check Addition - Result
+ ///
+ /// Same as `sp_runtime::traits::CheckedAdd::checked_add` but returns a
+ /// `Result` instead of `Option`.
+ fn check_add_rslt(&self, n: &Self) -> Result;
+
+ /// Check Division - Result
+ ///
+ /// Same as `sp_runtime::traits::CheckedDiv::checked_div` but returns a
+ /// `Result` instead of `Option`.
+ fn check_div_rslt(&self, n: &Self) -> Result;
+
+ /// Check Multiplication - Result
+ ///
+ /// Same as `sp_runtime::traits::CheckedMul::checked_mul` but returns a
+ /// `Result` instead of `Option`.
+ fn check_mul_rslt(&self, n: &Self) -> Result;
+
+ /// Check Subtraction - Result
+ ///
+ /// Same as `sp_runtime::traits::CheckedSub::checked_sub` but returns a
+ /// `Result` instead of `Option`.
+ fn check_sub_rslt(&self, n: &Self) -> Result;
+}
+
+impl CheckArithmRslt for T
+where
+ T: CheckedAdd + CheckedDiv + CheckedMul + CheckedSub,
+{
+ #[inline]
+ fn check_add_rslt(&self, n: &Self) -> Result {
+ self.checked_add(n).ok_or(ARITHM_OF)
+ }
+
+ #[inline]
+ fn check_div_rslt(&self, n: &Self) -> Result {
+ self.checked_div(n).ok_or(ARITHM_OF)
+ }
+
+ #[inline]
+ fn check_mul_rslt(&self, n: &Self) -> Result {
+ self.checked_mul(n).ok_or(ARITHM_OF)
+ }
+
+ #[inline]
+ fn check_sub_rslt(&self, n: &Self) -> Result {
+ self.checked_sub(n).ok_or(ARITHM_OF)
+ }
+}
diff --git a/primitives/src/math/consts.rs b/primitives/src/math/consts.rs
new file mode 100644
index 000000000..08e8f2aad
--- /dev/null
+++ b/primitives/src/math/consts.rs
@@ -0,0 +1,37 @@
+// Copyright 2023 Forecasting Technologies LTD.
+// Copyright 2021-2022 Zeitgeist PM LLC.
+//
+// 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 .
+//
+// This file incorporates work covered by the license above but
+// published without copyright notice by Balancer Labs
+// (, contact@balancer.finance) in the
+// balancer-core repository
+// .
+
+use crate::constants::BASE;
+use frame_support::dispatch::DispatchError;
+
+pub const ARITHM_OF: DispatchError = DispatchError::Other("Arithmetic overflow");
+
+/// The amount of precision to use in exponentiation.
+pub const BPOW_PRECISION: u128 = 10;
+/// The minimum value of the base parameter in bpow_approx.
+pub const BPOW_APPROX_BASE_MIN: u128 = BASE / 4;
+/// The maximum value of the base parameter in bpow_approx.
+pub const BPOW_APPROX_BASE_MAX: u128 = 7 * BASE / 4;
+/// The maximum number of terms from the binomial series used to calculate bpow_approx.
+pub const BPOW_APPROX_MAX_ITERATIONS: u128 = 100;
diff --git a/primitives/src/math/fixed.rs b/primitives/src/math/fixed.rs
new file mode 100644
index 000000000..f977563a2
--- /dev/null
+++ b/primitives/src/math/fixed.rs
@@ -0,0 +1,824 @@
+// Copyright 2023 Forecasting Technologies LTD.
+// Copyright 2021-2022 Zeitgeist PM LLC.
+//
+// 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 .
+//
+// This file incorporates work covered by the license above but
+// published without copyright notice by Balancer Labs
+// (, contact@balancer.finance) in the
+// balancer-core repository
+// .
+
+use crate::{
+ constants::BASE,
+ math::{
+ check_arithm_rslt::CheckArithmRslt,
+ consts::{
+ BPOW_APPROX_BASE_MAX, BPOW_APPROX_BASE_MIN, BPOW_APPROX_MAX_ITERATIONS, BPOW_PRECISION,
+ },
+ },
+};
+use alloc::{borrow::ToOwned, format, string::ToString, vec::Vec};
+use core::convert::TryFrom;
+use fixed::{traits::Fixed, ParseFixedError};
+use frame_support::dispatch::DispatchError;
+
+pub fn btoi(a: u128) -> Result {
+ a.check_div_rslt(&BASE)
+}
+
+pub fn bfloor(a: u128) -> Result {
+ btoi(a)?.check_mul_rslt(&BASE)
+}
+
+pub fn bsub_sign(a: u128, b: u128) -> Result<(u128, bool), DispatchError> {
+ Ok(if a >= b { (a.check_sub_rslt(&b)?, false) } else { (b.check_sub_rslt(&a)?, true) })
+}
+
+pub fn bmul(a: u128, b: u128) -> Result {
+ let c0 = a.check_mul_rslt(&b)?;
+ let c1 = c0.check_add_rslt(&BASE.check_div_rslt(&2)?)?;
+ c1.check_div_rslt(&BASE)
+}
+
+pub fn bdiv(a: u128, b: u128) -> Result {
+ let c0 = a.check_mul_rslt(&BASE)?;
+ let c1 = c0.check_add_rslt(&b.check_div_rslt(&2)?)?;
+ c1.check_div_rslt(&b)
+}
+
+pub fn bpowi(a: u128, n: u128) -> Result {
+ let mut z = if n % 2 != 0 { a } else { BASE };
+
+ let mut b = a;
+ let mut m = n.check_div_rslt(&2)?;
+
+ while m != 0 {
+ b = bmul(b, b)?;
+
+ if m % 2 != 0 {
+ z = bmul(z, b)?;
+ }
+
+ m = m.check_div_rslt(&2)?;
+ }
+
+ Ok(z)
+}
+
+/// Compute the power `base ** exp`.
+///
+/// # Arguments
+///
+/// * `base`: The base, a number between `BASE / 4` and `7 * BASE / 4`
+/// * `exp`: The exponent
+///
+/// # Errors
+///
+/// If this function encounters an arithmetic over/underflow, or if the numerical limits
+/// for `base` (specified above) are violated, a `DispatchError::Other` is returned.
+pub fn bpow(base: u128, exp: u128) -> Result {
+ let whole = bfloor(exp)?;
+ let remain = exp.check_sub_rslt(&whole)?;
+
+ let whole_pow = bpowi(base, btoi(whole)?)?;
+
+ if remain == 0 {
+ return Ok(whole_pow);
+ }
+
+ let partial_result = bpow_approx(base, remain)?;
+ bmul(whole_pow, partial_result)
+}
+
+/// Compute an estimate of the power `base ** exp`.
+///
+/// # Arguments
+///
+/// * `base`: The base, an element of `[BASE / 4, 7 * BASE / 4]`
+/// * `exp`: The exponent, an element of `[0, BASE]`
+///
+/// # Errors
+///
+/// If this function encounters an arithmetic over/underflow, or if the numerical limits
+/// for `base` or `exp` (specified above) are violated, a `DispatchError::Other` is
+/// returned.
+pub fn bpow_approx(base: u128, exp: u128) -> Result {
+ // We use the binomial power series for this calculation. We stop adding terms to
+ // the result as soon as one term is smaller than `BPOW_PRECISION`. (Thanks to the
+ // limits on `base` and `exp`, this means that the total error should not exceed
+ // 4*BPOW_PRECISION`.)
+ if exp > BASE {
+ return Err(DispatchError::Other("[bpow_approx]: expected exp <= BASE"));
+ }
+ if base < BPOW_APPROX_BASE_MIN {
+ return Err(DispatchError::Other("[bpow_approx]: expected base >= BASE / 4"));
+ }
+ if base > BPOW_APPROX_BASE_MAX {
+ return Err(DispatchError::Other("[bpow_approx]: expected base <= 7 * BASE / 4"));
+ }
+
+ let a = exp;
+ let (x, xneg) = bsub_sign(base, BASE)?;
+ let mut term = BASE;
+ let mut sum = term;
+ let mut negative = false;
+
+ // term(k) = numer / denom
+ // = (product(a - i - 1, i=1-->k) * x^k) / (k!)
+ // each iteration, multiply previous term by (a-(k-1)) * x / k
+ // continue until term is less than precision
+ for i in 1..=BPOW_APPROX_MAX_ITERATIONS {
+ if term < BPOW_PRECISION {
+ break;
+ }
+
+ let big_k = i.check_mul_rslt(&BASE)?;
+ let (c, cneg) = bsub_sign(a, big_k.check_sub_rslt(&BASE)?)?;
+ term = bmul(term, bmul(c, x)?)?;
+ term = bdiv(term, big_k)?;
+ if term == 0 {
+ break;
+ }
+
+ if xneg {
+ negative = !negative;
+ }
+ if cneg {
+ negative = !negative;
+ }
+ if negative {
+ // Never underflows. In fact, the absolute value of the terms is strictly
+ // decreasing thanks to the numerical limits.
+ sum = sum.check_sub_rslt(&term)?;
+ } else {
+ sum = sum.check_add_rslt(&term)?;
+ }
+ }
+
+ // If term is still large, then MAX_ITERATIONS was violated (can't happen with the current
+ // limits).
+ if term >= BPOW_PRECISION {
+ return Err(DispatchError::Other("[bpow_approx] Maximum number of iterations exceeded"));
+ }
+
+ Ok(sum)
+}
+
+/// Converts a fixed point decimal number into another type.
+pub trait FromFixedDecimal>
+where
+ Self: Sized,
+{
+ /// Craft a fixed point decimal number from `N`.
+ fn from_fixed_decimal(decimal: N, places: u8) -> Result;
+}
+
+/// Converts a fixed point decimal number into another type.
+pub trait IntoFixedFromDecimal {
+ /// Converts a fixed point decimal number into another type.
+ fn to_fixed_from_fixed_decimal(self, places: u8) -> Result;
+}
+
+/// Converts a type into a fixed point decimal number.
+pub trait FromFixedToDecimal
+where
+ Self: Sized + TryFrom,
+{
+ /// Craft a fixed point decimal number from another type.
+ fn from_fixed_to_fixed_decimal(fixed: F, places: u8) -> Result;
+}
+
+/// Converts a type into a fixed point decimal number.
+pub trait IntoFixedDecimal> {
+ /// Converts a type into a fixed point decimal number.
+ fn to_fixed_decimal(self, places: u8) -> Result;
+}
+
+impl> FromFixedDecimal for F {
+ /// Craft a `Fixed` type from a fixed point decimal number of type `N`
+ fn from_fixed_decimal(decimal: N, places: u8) -> Result {
+ let decimal_u128 = decimal.into();
+ let mut decimal_string = decimal_u128.to_string();
+
+ if decimal_string.len() <= places as usize {
+ // This can never underflow (places >= len). Saturating subtraction to satisfy clippy.
+ decimal_string = "0.".to_owned()
+ + &"0".repeat((places as usize).saturating_sub(decimal_string.len()))
+ + &decimal_string;
+ } else {
+ // This can never underflow (len > places). Saturating subtraction to satisfy clippy.
+ decimal_string.insert(decimal_string.len().saturating_sub(places as usize), '.');
+ }
+
+ F::from_str(&decimal_string)
+ }
+}
+
+impl IntoFixedFromDecimal for N
+where
+ F: Fixed + FromFixedDecimal,
+ N: Into,
+{
+ /// Converts a fixed point decimal number into `Fixed` type (e.g. `Balance` -> `Fixed`).
+ fn to_fixed_from_fixed_decimal(self, places: u8) -> Result {
+ F::from_fixed_decimal(self, places)
+ }
+}
+
+impl> FromFixedToDecimal for N {
+ fn from_fixed_to_fixed_decimal(fixed: F, decimals: u8) -> Result {
+ let decimals_usize = decimals as usize;
+ let s = fixed.to_string();
+ let mut parts: Vec<&str> = s.split('.').collect();
+ // If there's no fractional part, then `fixed` was an integer.
+ if parts.len() != 2 {
+ parts.push("0");
+ }
+
+ let (int_part, frac_part) = (parts[0], parts[1]);
+ let mut increment = false;
+
+ let new_frac_part = if frac_part.len() < decimals_usize {
+ format!("{}{}", frac_part, "0".repeat(decimals_usize.saturating_sub(frac_part.len())))
+ } else {
+ // Adding rounding behavior
+ let round_digit = frac_part.chars().nth(decimals_usize);
+ match round_digit {
+ Some(d) if d >= '5' => increment = true,
+ _ => {}
+ }
+
+ frac_part.chars().take(decimals_usize).collect()
+ };
+
+ let mut fixed_decimal: u128 = format!("{}{}", int_part, new_frac_part)
+ .parse::()
+ .map_err(|_| "Failed to parse the fixed decimal representation into u128")?;
+
+ if increment {
+ fixed_decimal = fixed_decimal.saturating_add(1);
+ }
+
+ let result: N = fixed_decimal.try_into().map_err(|_| {
+ "The parsed fixed decimal representation does not fit into the target type"
+ })?;
+ Ok(result)
+ }
+}
+
+impl IntoFixedDecimal for F
+where
+ F: Fixed,
+ N: FromFixedToDecimal,
+{
+ /// Converts a `Fixed` type into a fixed point decimal number.
+ fn to_fixed_decimal(self, places: u8) -> Result {
+ N::from_fixed_to_fixed_decimal(self, places)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ assert_approx,
+ constants::BASE,
+ math::{
+ consts::{ARITHM_OF, BPOW_PRECISION},
+ fixed::{bdiv, bmul, bpow, bpow_approx},
+ },
+ };
+ use fixed::{traits::ToFixed, FixedU128};
+ use frame_support::{assert_err, dispatch::DispatchError};
+ use more_asserts::assert_le;
+ use test_case::test_case;
+ use typenum::U80;
+
+ pub const ERR: Result = Err(ARITHM_OF);
+
+ macro_rules! create_tests {
+ (
+ $op:ident;
+
+ 0 => $_0_0:expr, $_0_1:expr, $_0_2:expr, $_0_3:expr;
+ 1 => $_1_0:expr, $_1_1:expr, $_1_2:expr, $_1_3:expr;
+ 2 => $_2_0:expr, $_2_1:expr, $_2_2:expr, $_2_3:expr;
+ 3 => $_3_0:expr, $_3_1:expr, $_3_2:expr, $_3_3:expr;
+ max_n => $max_n_0:expr, $max_n_1:expr, $max_n_2:expr, $max_n_3:expr;
+ n_max => $n_max_0:expr, $n_max_1:expr, $n_max_2:expr, $n_max_3:expr;
+ ) => {
+ assert_eq!($op(0, 0 * BASE), $_0_0);
+ assert_eq!($op(0, 1 * BASE), $_0_1);
+ assert_eq!($op(0, 2 * BASE), $_0_2);
+ assert_eq!($op(0, 3 * BASE), $_0_3);
+
+ assert_eq!($op(1 * BASE, 0 * BASE), $_1_0);
+ assert_eq!($op(1 * BASE, 1 * BASE), $_1_1);
+ assert_eq!($op(1 * BASE, 2 * BASE), $_1_2);
+ assert_eq!($op(1 * BASE, 3 * BASE), $_1_3);
+
+ assert_eq!($op(2 * BASE, 0 * BASE), $_2_0);
+ assert_eq!($op(2 * BASE, 1 * BASE), $_2_1);
+ assert_eq!($op(2 * BASE, 2 * BASE), $_2_2);
+ assert_eq!($op(2 * BASE, 3 * BASE), $_2_3);
+
+ assert_eq!($op(3 * BASE, 0 * BASE), $_3_0);
+ assert_eq!($op(3 * BASE, 1 * BASE), $_3_1);
+ assert_eq!($op(3 * BASE, 2 * BASE), $_3_2);
+ assert_eq!($op(3 * BASE, 3 * BASE), $_3_3);
+
+ assert_eq!($op(u128::MAX, 0 * BASE), $max_n_0);
+ assert_eq!($op(u128::MAX, 1 * BASE), $max_n_1);
+ assert_eq!($op(u128::MAX, 2 * BASE), $max_n_2);
+ assert_eq!($op(u128::MAX, 3 * BASE), $max_n_3);
+
+ assert_eq!($op(0, u128::MAX), $n_max_0);
+ assert_eq!($op(1, u128::MAX), $n_max_1);
+ assert_eq!($op(2, u128::MAX), $n_max_2);
+ assert_eq!($op(3, u128::MAX), $n_max_3);
+ };
+ }
+
+ #[test]
+ fn bdiv_has_minimum_set_of_correct_values() {
+ create_tests!(
+ bdiv;
+ 0 => ERR, Ok(0), Ok(0), Ok(0);
+ 1 => ERR, Ok(BASE), Ok(BASE / 2), Ok(BASE / 3);
+ 2 => ERR, Ok(2 * BASE), Ok(BASE), Ok(6666666667);
+ 3 => ERR, Ok(3 * BASE), Ok(3 * BASE / 2), Ok(BASE);
+ max_n => ERR, ERR, ERR, ERR;
+ n_max => Ok(0), Ok(1 / BASE), Ok(2 / BASE), Ok(3 / BASE);
+ );
+ }
+
+ #[test]
+ fn bmul_has_minimum_set_of_correct_values() {
+ create_tests!(
+ bmul;
+ 0 => Ok(0), Ok(0), Ok(0), Ok(0);
+ 1 => Ok(0), Ok(BASE), Ok(2 * BASE), Ok(3 * BASE);
+ 2 => Ok(0), Ok(2 * BASE), Ok(4 * BASE), Ok(6 * BASE);
+ 3 => Ok(0), Ok(3 * BASE), Ok(6 * BASE), Ok(9 * BASE);
+ max_n => Ok(0), ERR, ERR, ERR;
+ n_max => Ok(0), ERR, ERR, ERR;
+ );
+ }
+
+ #[test]
+ fn bpow_has_minimum_set_of_correct_values() {
+ let test_vector: Vec<(u128, u128, u128)> = vec![
+ (2500000000, 0, 10000000000),
+ (2500000000, 10000000000, 2500000000),
+ (2500000000, 33333333333, 98431332),
+ (2500000000, 200000000, 9726549474),
+ (2500000000, 500000000000, 0),
+ (5000000000, 0, 10000000000),
+ (5000000000, 10000000000, 5000000000),
+ (5000000000, 33333333333, 992125657),
+ (5000000000, 200000000, 9862327044),
+ (5000000000, 500000000000, 0),
+ (7500000000, 0, 10000000000),
+ (7500000000, 10000000000, 7500000000),
+ (7500000000, 33333333333, 3832988750),
+ (7500000000, 200000000, 9942628790),
+ (7500000000, 500000000000, 5663),
+ (10000000000, 0, 10000000000),
+ (10000000000, 10000000000, 10000000000),
+ (10000000000, 33333333333, 10000000000),
+ (10000000000, 200000000, 10000000000),
+ (10000000000, 500000000000, 10000000000),
+ (12500000000, 0, 10000000000),
+ (12500000000, 10000000000, 12500000000),
+ (12500000000, 33333333333, 21039401269),
+ (12500000000, 200000000, 10044728444),
+ (12500000000, 500000000000, 700649232162408),
+ (15000000000, 0, 10000000000),
+ (15000000000, 10000000000, 15000000000),
+ (15000000000, 33333333333, 38634105686),
+ (15000000000, 200000000, 10081422716),
+ (15000000000, 500000000000, 6376215002140495869),
+ (17500000000, 0, 10000000000),
+ (17500000000, 10000000000, 17500000000),
+ (17500000000, 33333333333, 64584280985),
+ (17500000000, 200000000, 10112551840),
+ (17500000000, 500000000000, 14187387615511831479362),
+ ];
+ for (base, exp, expected) in test_vector.iter() {
+ let result = bpow(*base, *exp).unwrap();
+ let precision = *expected / BASE + 4 * BPOW_PRECISION; // relative + absolute error
+ let diff = if result > *expected { result - *expected } else { *expected - result };
+ assert_le!(diff, precision);
+ }
+ }
+
+ #[test]
+ fn bpow_returns_error_when_parameters_are_outside_of_specified_limits() {
+ let test_vector: Vec<(u128, u128)> =
+ vec![(BASE / 10, 3 * BASE / 2), (2 * BASE - BASE / 10, 3 * BASE / 2)];
+ for (base, exp) in test_vector.iter() {
+ assert!(bpow(*base, *exp).is_err());
+ }
+ }
+
+ #[test]
+ fn bpow_approx_has_minimum_set_of_correct_values() {
+ let precision = 4 * BPOW_PRECISION;
+ let test_vector: Vec<(u128, u128, u128)> = vec![
+ (2500000000, 0, 10000000000),
+ (2500000000, 1000000000, 8705505632),
+ (2500000000, 2000000000, 7578582832),
+ (2500000000, 3000000000, 6597539553),
+ (2500000000, 4000000000, 5743491774),
+ (2500000000, 5000000000, 5000000000),
+ (2500000000, 6000000000, 4352752816),
+ (2500000000, 7000000000, 3789291416),
+ (2500000000, 8000000000, 3298769776),
+ (2500000000, 9000000000, 2871745887),
+ (2500000000, 10000000000, 2500000000),
+ (5000000000, 0, 10000000000),
+ (5000000000, 1000000000, 9330329915),
+ (5000000000, 2000000000, 8705505632),
+ (5000000000, 3000000000, 8122523963),
+ (5000000000, 4000000000, 7578582832),
+ (5000000000, 5000000000, 7071067811),
+ (5000000000, 6000000000, 6597539553),
+ (5000000000, 7000000000, 6155722066),
+ (5000000000, 8000000000, 5743491774),
+ (5000000000, 9000000000, 5358867312),
+ (5000000000, 10000000000, 5000000000),
+ (7500000000, 0, 10000000000),
+ (7500000000, 1000000000, 9716416578),
+ (7500000000, 2000000000, 9440875112),
+ (7500000000, 3000000000, 9173147546),
+ (7500000000, 4000000000, 8913012289),
+ (7500000000, 5000000000, 8660254037),
+ (7500000000, 6000000000, 8414663590),
+ (7500000000, 7000000000, 8176037681),
+ (7500000000, 8000000000, 7944178807),
+ (7500000000, 9000000000, 7718895067),
+ (7500000000, 10000000000, 7500000000),
+ (10000000000, 0, 10000000000),
+ (10000000000, 1000000000, 10000000000),
+ (10000000000, 2000000000, 10000000000),
+ (10000000000, 3000000000, 10000000000),
+ (10000000000, 4000000000, 10000000000),
+ (10000000000, 5000000000, 10000000000),
+ (10000000000, 6000000000, 10000000000),
+ (10000000000, 7000000000, 10000000000),
+ (10000000000, 8000000000, 10000000000),
+ (10000000000, 9000000000, 10000000000),
+ (10000000000, 10000000000, 10000000000),
+ (12500000000, 0, 10000000000),
+ (12500000000, 1000000000, 10225651825),
+ (12500000000, 2000000000, 10456395525),
+ (12500000000, 3000000000, 10692345999),
+ (12500000000, 4000000000, 10933620739),
+ (12500000000, 5000000000, 11180339887),
+ (12500000000, 6000000000, 11432626298),
+ (12500000000, 7000000000, 11690605597),
+ (12500000000, 8000000000, 11954406247),
+ (12500000000, 9000000000, 12224159606),
+ (12500000000, 10000000000, 12500000000),
+ (15000000000, 0, 10000000000),
+ (15000000000, 1000000000, 10413797439),
+ (15000000000, 2000000000, 10844717711),
+ (15000000000, 3000000000, 11293469354),
+ (15000000000, 4000000000, 11760790225),
+ (15000000000, 5000000000, 12247448713),
+ (15000000000, 6000000000, 12754245006),
+ (15000000000, 7000000000, 13282012399),
+ (15000000000, 8000000000, 13831618672),
+ (15000000000, 9000000000, 14403967511),
+ (15000000000, 10000000000, 15000000000),
+ (17500000000, 0, 10000000000),
+ (17500000000, 1000000000, 10575570503),
+ (17500000000, 2000000000, 11184269147),
+ (17500000000, 3000000000, 11828002689),
+ (17500000000, 4000000000, 12508787635),
+ (17500000000, 5000000000, 13228756555),
+ (17500000000, 6000000000, 13990164762),
+ (17500000000, 7000000000, 14795397379),
+ (17500000000, 8000000000, 15646976811),
+ (17500000000, 9000000000, 16547570643),
+ (17500000000, 10000000000, 17500000000),
+ ];
+ for (base, exp, expected) in test_vector.iter() {
+ let result = bpow_approx(*base, *exp).unwrap();
+ let diff = if result > *expected { result - *expected } else { *expected - result };
+ assert_le!(diff, precision);
+ }
+ }
+
+ #[test]
+ fn bpow_approx_returns_error_when_parameters_are_outside_of_specified_limits() {
+ let test_vector: Vec<(u128, u128, DispatchError)> = vec![
+ (BASE, BASE + 1, DispatchError::Other("[bpow_approx]: expected exp <= BASE")),
+ (BASE / 10, BASE / 2, DispatchError::Other("[bpow_approx]: expected base >= BASE / 4")),
+ (
+ 2 * BASE - BASE / 10,
+ BASE / 2,
+ DispatchError::Other("[bpow_approx]: expected base <= 7 * BASE / 4"),
+ ),
+ ];
+ for (base, exp, err) in test_vector.iter() {
+ assert_err!(bpow_approx(*base, *exp), *err);
+ }
+ }
+
+ #[test_case(0, 10, 0.0)]
+ #[test_case(1, 10, 0.0000000001)]
+ #[test_case(9, 10, 0.0000000009)]
+ #[test_case(123_456_789, 10, 0.123456789)]
+ #[test_case(999_999_999, 10, 0.999999999)]
+ #[test_case(10_000_000_000, 10, 1.0)]
+ #[test_case(10_000_000_001, 10, 1.00000000001)]
+ #[test_case(20751874964, 10, 2.075_187_496_394_219)]
+ #[test_case(123456789876543210, 10, 12_345_678.987_654_32)]
+ #[test_case(99999999999999999999999, 10, 9999999999999.9999999999)]
+ // Tests taken from Rikiddo pallet
+ #[test_case(1, 10, 0.000_000_000_1)]
+ #[test_case(123_456_789, 10, 0.012_345_678_9)]
+ #[test_case(9_999, 2, 99.99)]
+ #[test_case(736_101, 2, 7_361.01)]
+ #[test_case(133_733_333_333, 8, 1_337.333_333_33)]
+ #[test_case(1, 1, 0.1)]
+ #[test_case(55, 11, 0.000_000_000_6)]
+ #[test_case(34, 11, 0.000_000_000_3)]
+ fn to_fixed_from_fixed_decimal(value: u128, decimals: u8, expected_float: f64) {
+ let result: FixedU128 = value.to_fixed_from_fixed_decimal(decimals).unwrap();
+ assert_approx!(result, >::from_num(expected_float), 1);
+ }
+
+ #[test_case(0.0, 10, 0)]
+ #[test_case(0.00000000004, 10, 0)]
+ #[test_case(0.00000000005, 10, 1)]
+ #[test_case(0.0000000001, 10, 1)]
+ #[test_case(0.00000000099, 10, 10)]
+ #[test_case(0.0123456789, 10, 123_456_789)]
+ #[test_case(0.09999999999, 10, 1_000_000_000)]
+ #[test_case(0.19999999999, 10, 2_000_000_000)]
+ #[test_case(0.99999999999, 10, 10_000_000_000)]
+ #[test_case(1.0, 10, 10_000_000_000)]
+ #[test_case(1.00000000001, 10, 10_000_000_000)]
+ #[test_case(1.67899999995, 10, 16_790_000_000)]
+ #[test_case(1.89999999995, 10, 19_000_000_000)]
+ #[test_case(1.99999999995, 10, 20_000_000_000)]
+ #[test_case(2.075_187_496_394_219, 10, 20751874964)]
+ #[test_case(12_345_678.987_654_32, 10, 123456789876543210)]
+ #[test_case(99.999999999999, 10, 1_000_000_000_000)]
+ #[test_case(9999999999999.9999999999, 10, 99999999999999999999999)]
+ // Tests taken from Rikiddo pallet
+ #[test_case(32.5, 0, 33)]
+ #[test_case(32.25, 0, 32)]
+ #[test_case(200.0, 8, 20_000_000_000)]
+ #[test_case(200.1234, 8, 20_012_340_000)]
+ #[test_case(200.1234, 2, 20_012)]
+ #[test_case(200.1254, 2, 20_013)]
+ #[test_case(123.456, 3, 123_456)]
+ #[test_case(123.0, 0, 123)]
+ // Random values
+ #[test_case(0.1161, 3, 116)]
+ #[test_case(0.2449, 3, 245)]
+ #[test_case(0.29, 3, 290)]
+ #[test_case(0.297, 3, 297)]
+ #[test_case(0.3423, 3, 342)]
+ #[test_case(0.4259, 3, 426)]
+ #[test_case(0.4283, 3, 428)]
+ #[test_case(0.4317, 3, 432)]
+ #[test_case(0.4649, 3, 465)]
+ #[test_case(0.4924, 3, 492)]
+ #[test_case(0.5656, 3, 566)]
+ #[test_case(0.7197, 3, 720)]
+ #[test_case(0.9803, 3, 980)]
+ #[test_case(1.0285, 3, 1029)]
+ #[test_case(1.0661, 3, 1066)]
+ #[test_case(1.0701, 3, 1070)]
+ #[test_case(1.1505, 3, 1151)]
+ #[test_case(1.1814, 3, 1181)]
+ #[test_case(1.2284, 3, 1228)]
+ #[test_case(1.3549, 3, 1355)]
+ #[test_case(1.3781, 3, 1378)]
+ #[test_case(1.3987, 3, 1399)]
+ #[test_case(1.5239, 3, 1524)]
+ #[test_case(1.5279, 3, 1528)]
+ #[test_case(1.5636, 3, 1564)]
+ #[test_case(1.5688, 3, 1569)]
+ #[test_case(1.6275, 3, 1628)]
+ #[test_case(1.6567, 3, 1657)]
+ #[test_case(1.7245, 3, 1725)]
+ #[test_case(1.7264, 3, 1726)]
+ #[test_case(1.7884, 3, 1788)]
+ #[test_case(1.8532, 3, 1853)]
+ #[test_case(2.0569, 3, 2057)]
+ #[test_case(2.0801, 3, 2080)]
+ #[test_case(2.1192, 3, 2119)]
+ #[test_case(2.1724, 3, 2172)]
+ #[test_case(2.2966, 3, 2297)]
+ #[test_case(2.3375, 3, 2338)]
+ #[test_case(2.3673, 3, 2367)]
+ #[test_case(2.4284, 3, 2428)]
+ #[test_case(2.431, 3, 2431)]
+ #[test_case(2.4724, 3, 2472)]
+ #[test_case(2.5036, 3, 2504)]
+ #[test_case(2.5329, 3, 2533)]
+ #[test_case(2.5976, 3, 2598)]
+ #[test_case(2.625, 3, 2625)]
+ #[test_case(2.7198, 3, 2720)]
+ #[test_case(2.7713, 3, 2771)]
+ #[test_case(2.8375, 3, 2838)]
+ #[test_case(2.9222, 3, 2922)]
+ #[test_case(2.9501, 3, 2950)]
+ #[test_case(2.9657, 3, 2966)]
+ #[test_case(3.0959, 3, 3096)]
+ #[test_case(3.182, 3, 3182)]
+ #[test_case(3.216, 3, 3216)]
+ #[test_case(3.2507, 3, 3251)]
+ #[test_case(3.3119, 3, 3312)]
+ #[test_case(3.338, 3, 3338)]
+ #[test_case(3.473, 3, 3473)]
+ #[test_case(3.5163, 3, 3516)]
+ #[test_case(3.5483, 3, 3548)]
+ #[test_case(3.6441, 3, 3644)]
+ #[test_case(3.7228, 3, 3723)]
+ #[test_case(3.7712, 3, 3771)]
+ #[test_case(3.7746, 3, 3775)]
+ #[test_case(3.8729, 3, 3873)]
+ #[test_case(3.8854, 3, 3885)]
+ #[test_case(3.935, 3, 3935)]
+ #[test_case(3.9437, 3, 3944)]
+ #[test_case(3.9872, 3, 3987)]
+ #[test_case(4.0136, 3, 4014)]
+ #[test_case(4.069, 3, 4069)]
+ #[test_case(4.0889, 3, 4089)]
+ #[test_case(4.2128, 3, 4213)]
+ #[test_case(4.2915, 3, 4292)]
+ #[test_case(4.3033, 3, 4303)]
+ #[test_case(4.3513, 3, 4351)]
+ #[test_case(4.3665, 3, 4367)]
+ #[test_case(4.3703, 3, 4370)]
+ #[test_case(4.4216, 3, 4422)]
+ #[test_case(4.4768, 3, 4477)]
+ #[test_case(4.5022, 3, 4502)]
+ #[test_case(4.5236, 3, 4524)]
+ #[test_case(4.5336, 3, 4534)]
+ #[test_case(4.5371, 3, 4537)]
+ #[test_case(4.5871, 3, 4587)]
+ #[test_case(4.696, 3, 4696)]
+ #[test_case(4.6967, 3, 4697)]
+ #[test_case(4.775, 3, 4775)]
+ #[test_case(4.7977, 3, 4798)]
+ #[test_case(4.825, 3, 4825)]
+ #[test_case(4.8334, 3, 4833)]
+ #[test_case(4.8335, 3, 4834)]
+ #[test_case(4.8602, 3, 4860)]
+ #[test_case(4.9123, 3, 4912)]
+ #[test_case(5.0153, 3, 5015)]
+ #[test_case(5.143, 3, 5143)]
+ #[test_case(5.1701, 3, 5170)]
+ #[test_case(5.1721, 3, 5172)]
+ #[test_case(5.1834, 3, 5183)]
+ #[test_case(5.2639, 3, 5264)]
+ #[test_case(5.2667, 3, 5267)]
+ #[test_case(5.2775, 3, 5278)]
+ #[test_case(5.3815, 3, 5382)]
+ #[test_case(5.4786, 3, 5479)]
+ #[test_case(5.4879, 3, 5488)]
+ #[test_case(5.4883, 3, 5488)]
+ #[test_case(5.494, 3, 5494)]
+ #[test_case(5.5098, 3, 5510)]
+ #[test_case(5.5364, 3, 5536)]
+ #[test_case(5.5635, 3, 5564)]
+ #[test_case(5.5847, 3, 5585)]
+ #[test_case(5.6063, 3, 5606)]
+ #[test_case(5.6352, 3, 5635)]
+ #[test_case(5.6438, 3, 5644)]
+ #[test_case(5.7062, 3, 5706)]
+ #[test_case(5.7268, 3, 5727)]
+ #[test_case(5.7535, 3, 5754)]
+ #[test_case(5.8718, 3, 5872)]
+ #[test_case(5.8901, 3, 5890)]
+ #[test_case(5.956, 3, 5956)]
+ #[test_case(5.9962, 3, 5996)]
+ #[test_case(6.1368, 3, 6137)]
+ #[test_case(6.1665, 3, 6167)]
+ #[test_case(6.2001, 3, 6200)]
+ #[test_case(6.286, 3, 6286)]
+ #[test_case(6.2987, 3, 6299)]
+ #[test_case(6.3282, 3, 6328)]
+ #[test_case(6.3284, 3, 6328)]
+ #[test_case(6.3707, 3, 6371)]
+ #[test_case(6.3897, 3, 6390)]
+ #[test_case(6.5623, 3, 6562)]
+ #[test_case(6.5701, 3, 6570)]
+ #[test_case(6.6014, 3, 6601)]
+ #[test_case(6.6157, 3, 6616)]
+ #[test_case(6.6995, 3, 6700)]
+ #[test_case(6.7213, 3, 6721)]
+ #[test_case(6.8694, 3, 6869)]
+ #[test_case(6.932, 3, 6932)]
+ #[test_case(6.9411, 3, 6941)]
+ #[test_case(7.0225, 3, 7023)]
+ #[test_case(7.032, 3, 7032)]
+ #[test_case(7.1557, 3, 7156)]
+ #[test_case(7.1647, 3, 7165)]
+ #[test_case(7.183, 3, 7183)]
+ #[test_case(7.1869, 3, 7187)]
+ #[test_case(7.2222, 3, 7222)]
+ #[test_case(7.2293, 3, 7229)]
+ #[test_case(7.4952, 3, 7495)]
+ #[test_case(7.563, 3, 7563)]
+ #[test_case(7.5905, 3, 7591)]
+ #[test_case(7.7602, 3, 7760)]
+ #[test_case(7.7763, 3, 7776)]
+ #[test_case(7.8228, 3, 7823)]
+ #[test_case(7.8872, 3, 7887)]
+ #[test_case(7.9229, 3, 7923)]
+ #[test_case(7.9928, 3, 7993)]
+ #[test_case(8.0465, 3, 8047)]
+ #[test_case(8.0572, 3, 8057)]
+ #[test_case(8.0623, 3, 8062)]
+ #[test_case(8.0938, 3, 8094)]
+ #[test_case(8.145, 3, 8145)]
+ #[test_case(8.1547, 3, 8155)]
+ #[test_case(8.162, 3, 8162)]
+ #[test_case(8.1711, 3, 8171)]
+ #[test_case(8.2104, 3, 8210)]
+ #[test_case(8.2124, 3, 8212)]
+ #[test_case(8.2336, 3, 8234)]
+ #[test_case(8.2414, 3, 8241)]
+ #[test_case(8.3364, 3, 8336)]
+ #[test_case(8.5011, 3, 8501)]
+ #[test_case(8.5729, 3, 8573)]
+ #[test_case(8.7035, 3, 8704)]
+ #[test_case(8.882, 3, 8882)]
+ #[test_case(8.8834, 3, 8883)]
+ #[test_case(8.8921, 3, 8892)]
+ #[test_case(8.9127, 3, 8913)]
+ #[test_case(8.9691, 3, 8969)]
+ #[test_case(8.9782, 3, 8978)]
+ #[test_case(9.0893, 3, 9089)]
+ #[test_case(9.1449, 3, 9145)]
+ #[test_case(9.1954, 3, 9195)]
+ #[test_case(9.241, 3, 9241)]
+ #[test_case(9.3169, 3, 9317)]
+ #[test_case(9.3172, 3, 9317)]
+ #[test_case(9.406, 3, 9406)]
+ #[test_case(9.4351, 3, 9435)]
+ #[test_case(9.5563, 3, 9556)]
+ #[test_case(9.5958, 3, 9596)]
+ #[test_case(9.6461, 3, 9646)]
+ #[test_case(9.6985, 3, 9699)]
+ #[test_case(9.7331, 3, 9733)]
+ #[test_case(9.7433, 3, 9743)]
+ #[test_case(9.7725, 3, 9773)]
+ #[test_case(9.8178, 3, 9818)]
+ #[test_case(9.8311, 3, 9831)]
+ #[test_case(9.8323, 3, 9832)]
+ #[test_case(9.8414, 3, 9841)]
+ #[test_case(9.88, 3, 9880)]
+ #[test_case(9.9107, 3, 9911)]
+ fn to_fixed_decimal_works(value_float: f64, decimals: u8, expected: u128) {
+ let value_fixed: FixedU128 = value_float.to_fixed();
+ let result: u128 = value_fixed.to_fixed_decimal(decimals).unwrap();
+ // We allow for a small error because some floats like 9.9665 are actually 9.9664999... and
+ // round down instead of up.
+ assert_approx!(result, expected, 1);
+ }
+}
+
+#[macro_export]
+macro_rules! assert_approx {
+ ($left:expr, $right:expr, $precision:expr $(,)?) => {
+ match (&$left, &$right, &$precision) {
+ (left_val, right_val, precision_val) => {
+ let diff = if *left_val > *right_val {
+ *left_val - *right_val
+ } else {
+ *right_val - *left_val
+ };
+ if diff > *precision_val {
+ panic!(
+ "assertion `left approx== right` failed\n left: {}\n right: {}\n \
+ precision: {}\ndifference: {}",
+ *left_val, *right_val, *precision_val, diff
+ );
+ }
+ }
+ }
+ };
+}
diff --git a/primitives/src/math/mod.rs b/primitives/src/math/mod.rs
new file mode 100644
index 000000000..42a27f245
--- /dev/null
+++ b/primitives/src/math/mod.rs
@@ -0,0 +1,20 @@
+// 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 .
+
+pub mod check_arithm_rslt;
+mod consts;
+pub mod fixed;
diff --git a/primitives/src/pool.rs b/primitives/src/pool.rs
index 62ca56d80..c84d64d54 100644
--- a/primitives/src/pool.rs
+++ b/primitives/src/pool.rs
@@ -84,5 +84,6 @@ where
pub enum ScoringRule {
CPMM,
RikiddoSigmoidFeeMarketEma,
+ Lmsr,
Orderbook,
}
diff --git a/primitives/src/traits.rs b/primitives/src/traits.rs
index a1be3cd57..c2742e3a9 100644
--- a/primitives/src/traits.rs
+++ b/primitives/src/traits.rs
@@ -16,12 +16,16 @@
// You should have received a copy of the GNU General Public License
// along with Zeitgeist. If not, see .
+mod complete_set_operations_api;
+mod deploy_pool_api;
mod dispute_api;
mod market_commons_pallet_api;
mod market_id;
mod swaps;
mod zeitgeist_multi_reservable_currency;
+pub use complete_set_operations_api::CompleteSetOperationsApi;
+pub use deploy_pool_api::DeployPoolApi;
pub use dispute_api::{DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi};
pub use market_commons_pallet_api::MarketCommonsPalletApi;
pub use market_id::MarketId;
diff --git a/primitives/src/traits/complete_set_operations_api.rs b/primitives/src/traits/complete_set_operations_api.rs
new file mode 100644
index 000000000..3685da5b4
--- /dev/null
+++ b/primitives/src/traits/complete_set_operations_api.rs
@@ -0,0 +1,35 @@
+// 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 sp_runtime::DispatchResult;
+
+pub trait CompleteSetOperationsApi {
+ type AccountId;
+ type Balance;
+ type MarketId;
+
+ fn buy_complete_set(
+ who: Self::AccountId,
+ market_id: Self::MarketId,
+ amount: Self::Balance,
+ ) -> DispatchResult;
+ fn sell_complete_set(
+ who: Self::AccountId,
+ market_id: Self::MarketId,
+ amount: Self::Balance,
+ ) -> DispatchResult;
+}
diff --git a/primitives/src/traits/deploy_pool_api.rs b/primitives/src/traits/deploy_pool_api.rs
new file mode 100644
index 000000000..92f0bd1df
--- /dev/null
+++ b/primitives/src/traits/deploy_pool_api.rs
@@ -0,0 +1,33 @@
+// 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 alloc::vec::Vec;
+use sp_runtime::DispatchResult;
+
+pub trait DeployPoolApi {
+ type AccountId;
+ type Balance;
+ type MarketId;
+
+ fn deploy_pool(
+ who: Self::AccountId,
+ market_id: Self::MarketId,
+ amount: Self::Balance,
+ swap_prices: Vec,
+ swap_fee: Self::Balance,
+ ) -> DispatchResult;
+}
diff --git a/runtime/battery-station/Cargo.toml b/runtime/battery-station/Cargo.toml
index 4d21ad84e..7df3e156b 100644
--- a/runtime/battery-station/Cargo.toml
+++ b/runtime/battery-station/Cargo.toml
@@ -1,7 +1,6 @@
[build-dependencies]
substrate-wasm-builder = { workspace = true }
-
[dependencies]
frame-executive = { workspace = true }
frame-support = { workspace = true }
@@ -113,6 +112,7 @@ zrml-court = { workspace = true }
zrml-global-disputes = { workspace = true, optional = true }
zrml-liquidity-mining = { workspace = true }
zrml-market-commons = { workspace = true }
+zrml-neo-swaps = { workspace = true }
zrml-orderbook-v1 = { workspace = true }
zrml-prediction-markets = { workspace = true }
zrml-rikiddo = { workspace = true }
@@ -178,8 +178,8 @@ runtime-benchmarks = [
"frame-system/runtime-benchmarks",
"hex-literal",
"orml-asset-registry?/runtime-benchmarks",
- "orml-tokens/runtime-benchmarks",
"orml-benchmarking",
+ "orml-tokens/runtime-benchmarks",
"orml-xtokens?/runtime-benchmarks",
"pallet-author-inherent?/runtime-benchmarks",
"pallet-author-mapping?/runtime-benchmarks",
@@ -210,6 +210,7 @@ runtime-benchmarks = [
"zrml-authorized/runtime-benchmarks",
"zrml-court/runtime-benchmarks",
"zrml-liquidity-mining/runtime-benchmarks",
+ "zrml-neo-swaps/runtime-benchmarks",
"zrml-prediction-markets/runtime-benchmarks",
"zrml-simple-disputes/runtime-benchmarks",
"zrml-global-disputes/runtime-benchmarks",
@@ -323,6 +324,7 @@ std = [
"zrml-court/std",
"zrml-liquidity-mining/std",
"zrml-market-commons/std",
+ "zrml-neo-swaps/std",
"zrml-prediction-markets/std",
"zrml-rikiddo/std",
"zrml-simple-disputes/std",
@@ -377,6 +379,7 @@ try-runtime = [
"zrml-court/try-runtime",
"zrml-liquidity-mining/try-runtime",
"zrml-market-commons/try-runtime",
+ "zrml-neo-swaps/try-runtime",
"zrml-prediction-markets/try-runtime",
"zrml-rikiddo/try-runtime",
"zrml-simple-disputes/try-runtime",
diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs
index 2eb042b1e..2d54e052d 100644
--- a/runtime/battery-station/src/parameters.rs
+++ b/runtime/battery-station/src/parameters.rs
@@ -190,6 +190,10 @@ parameter_types! {
// Additional storage item size of 32 bytes.
pub const DepositFactor: Balance = deposit(0, 32);
+ // NeoSwaps
+ pub const NeoSwapsMaxSwapFee: Balance = 10 * CENT;
+ pub const NeoSwapsPalletId: PalletId = NS_PALLET_ID;
+
// ORML
pub const GetNativeCurrencyId: CurrencyId = Asset::Ztg;
diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml
index 9935b06e0..ed404d80b 100644
--- a/runtime/common/Cargo.toml
+++ b/runtime/common/Cargo.toml
@@ -29,6 +29,7 @@ pallet-transaction-payment-rpc-runtime-api = { workspace = true }
pallet-treasury = { workspace = true }
pallet-utility = { workspace = true }
pallet-vesting = { workspace = true }
+zeitgeist-primitives = { workspace = true }
# Utility
cfg-if = { workspace = true }
@@ -72,6 +73,7 @@ std = [
"pallet-utility/std",
"pallet-vesting/std",
"pallet-parachain-staking?/std",
+ "zeitgeist-primitives/std",
]
[package]
diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs
index f311977a1..c7a299446 100644
--- a/runtime/common/src/lib.rs
+++ b/runtime/common/src/lib.rs
@@ -51,7 +51,9 @@ macro_rules! decl_common_types {
};
#[cfg(feature = "try-runtime")]
use frame_try_runtime::{TryStateSelect, UpgradeCheckSelect};
- use sp_runtime::generic;
+ use sp_runtime::{generic, DispatchResult};
+ use zeitgeist_primitives::traits::DeployPoolApi;
+ use zrml_neo_swaps::types::MarketCreatorFee;
pub type Block = generic::Block;
@@ -219,10 +221,10 @@ macro_rules! decl_common_types {
common_runtime::impl_fee_types!();
pub mod opaque {
- //! Opaque types. These are used by the CLI to instantiate machinery that don't need to know
- //! the specifics of the runtime. They can then be made to be agnostic over specific formats
- //! of data like extrinsics, allowing for them to continue syncing the network through upgrades
- //! to even the core data structures.
+ //! Opaque types. These are used by the CLI to instantiate machinery that don't need to
+ //! know the specifics of the runtime. They can then be made to be agnostic over
+ //! specific formats of data like extrinsics, allowing for them to continue syncing the
+ //! network through upgrades to even the core data structures.
use super::Header;
use alloc::vec::Vec;
@@ -311,7 +313,8 @@ 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,
+ NeoSwaps: zrml_neo_swaps::{Call, Event, Pallet, Storage} = 60,
+ Orderbook: zrml_orderbook_v1::{Call, Event, Pallet, Storage} = 61,
$($additional_pallets)*
}
@@ -1123,6 +1126,7 @@ macro_rules! impl_config_traits {
type Court = Court;
type CloseOrigin = EnsureRoot;
type DestroyOrigin = EnsureRootOrAllAdvisoryCommittee;
+ type DeployPool = NeoSwaps;
type DisputeBond = DisputeBond;
type RuntimeEvent = RuntimeEvent;
type GlobalDisputes = GlobalDisputes;
@@ -1237,6 +1241,17 @@ macro_rules! impl_config_traits {
type WeightInfo = zrml_styx::weights::WeightInfo;
}
+ impl zrml_neo_swaps::Config for Runtime {
+ type CompleteSetOperations = PredictionMarkets;
+ type ExternalFees = MarketCreatorFee;
+ type MarketCommons = MarketCommons;
+ type MultiCurrency = AssetManager;
+ type RuntimeEvent = RuntimeEvent;
+ type WeightInfo = zrml_neo_swaps::weights::WeightInfo;
+ type MaxSwapFee = NeoSwapsMaxSwapFee;
+ type PalletId = NeoSwapsPalletId;
+ }
+
impl zrml_orderbook_v1::Config for Runtime {
type AssetManager = AssetManager;
type RuntimeEvent = RuntimeEvent;
@@ -1357,6 +1372,7 @@ macro_rules! create_runtime_api {
list_benchmark!(list, extra, zrml_prediction_markets, PredictionMarkets);
list_benchmark!(list, extra, zrml_liquidity_mining, LiquidityMining);
list_benchmark!(list, extra, zrml_styx, Styx);
+ list_benchmark!(list, extra, zrml_neo_swaps, NeoSwaps);
cfg_if::cfg_if! {
if #[cfg(feature = "parachain")] {
@@ -1459,6 +1475,7 @@ macro_rules! create_runtime_api {
add_benchmark!(params, batches, zrml_prediction_markets, PredictionMarkets);
add_benchmark!(params, batches, zrml_liquidity_mining, LiquidityMining);
add_benchmark!(params, batches, zrml_styx, Styx);
+ add_benchmark!(params, batches, zrml_neo_swaps, NeoSwaps);
cfg_if::cfg_if! {
diff --git a/runtime/zeitgeist/Cargo.toml b/runtime/zeitgeist/Cargo.toml
index 83514b05d..93b1906bd 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-neo-swaps = { workspace = true }
zrml-orderbook-v1 = { workspace = true }
zrml-prediction-markets = { workspace = true }
zrml-rikiddo = { workspace = true }
@@ -177,9 +178,9 @@ runtime-benchmarks = [
"hex-literal",
"polkadot-runtime?/runtime-benchmarks",
"orml-asset-registry?/runtime-benchmarks",
+ "orml-benchmarking",
"orml-tokens/runtime-benchmarks",
"orml-xtokens?/runtime-benchmarks",
- "orml-benchmarking",
"pallet-author-inherent?/runtime-benchmarks",
"pallet-author-mapping?/runtime-benchmarks",
"pallet-author-slot-filter?/runtime-benchmarks",
@@ -207,6 +208,7 @@ runtime-benchmarks = [
"zrml-authorized/runtime-benchmarks",
"zrml-court/runtime-benchmarks",
"zrml-liquidity-mining/runtime-benchmarks",
+ "zrml-neo-swaps/runtime-benchmarks",
"zrml-prediction-markets/runtime-benchmarks",
"zrml-simple-disputes/runtime-benchmarks",
"zrml-global-disputes/runtime-benchmarks",
@@ -312,6 +314,7 @@ std = [
"zrml-court/std",
"zrml-liquidity-mining/std",
"zrml-market-commons/std",
+ "zrml-neo-swaps/std",
"zrml-prediction-markets/std",
"zrml-rikiddo/std",
"zrml-simple-disputes/std",
@@ -366,6 +369,7 @@ try-runtime = [
"zrml-court/try-runtime",
"zrml-liquidity-mining/try-runtime",
"zrml-market-commons/try-runtime",
+ "zrml-neo-swaps/try-runtime",
"zrml-prediction-markets/try-runtime",
"zrml-rikiddo/try-runtime",
"zrml-simple-disputes/try-runtime",
diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs
index b4a907ec3..4bdf563fa 100644
--- a/runtime/zeitgeist/src/parameters.rs
+++ b/runtime/zeitgeist/src/parameters.rs
@@ -190,6 +190,10 @@ parameter_types! {
// Additional storage item size of 32 bytes.
pub const DepositFactor: Balance = deposit(0, 32);
+ // NeoSwaps
+ pub const NeoSwapsMaxSwapFee: Balance = 10 * CENT;
+ pub const NeoSwapsPalletId: PalletId = NS_PALLET_ID;
+
// ORML
pub const GetNativeCurrencyId: CurrencyId = Asset::Ztg;
diff --git a/scripts/benchmarks/configuration.sh b/scripts/benchmarks/configuration.sh
index c4f71fe2c..f5ce38ec1 100644
--- a/scripts/benchmarks/configuration.sh
+++ b/scripts/benchmarks/configuration.sh
@@ -27,8 +27,8 @@ export ORML_PALLETS_STEPS="${ORML_PALLETS_STEPS:-50}"
export ORML_WEIGHT_TEMPLATE="./misc/orml_weight_template.hbs"
export ZEITGEIST_PALLETS=(
- zrml_authorized zrml_court zrml_global_disputes zrml_liquidity_mining zrml_prediction_markets \
- zrml_swaps zrml_styx \
+ zrml_authorized zrml_court zrml_global_disputes zrml_liquidity_mining zrml_neo_swaps \
+ zrml_prediction_markets zrml_swaps zrml_styx \
)
export ZEITGEIST_PALLETS_RUNS="${ZEITGEIST_PALLETS_RUNS:-1000}"
export ZEITGEIST_PALLETS_STEPS="${ZEITGEIST_PALLETS_STEPS:-10}"
diff --git a/zrml/neo-swaps/Cargo.toml b/zrml/neo-swaps/Cargo.toml
new file mode 100644
index 000000000..62f5f1a3b
--- /dev/null
+++ b/zrml/neo-swaps/Cargo.toml
@@ -0,0 +1,109 @@
+[dependencies]
+fixed = { workspace = true }
+frame-benchmarking = { workspace = true, optional = true }
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+hydra-dx-math = { workspace = true }
+orml-traits = { workspace = true }
+parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] }
+scale-info = { workspace = true, features = ["derive"] }
+sp-runtime = { workspace = true }
+typenum = { workspace = true }
+zeitgeist-primitives = { workspace = true }
+zrml-market-commons = { workspace = true }
+
+# Mock
+
+orml-asset-registry = { workspace = true, optional = true }
+orml-currencies = { workspace = true, optional = true }
+orml-tokens = { workspace = true, optional = true }
+pallet-balances = { workspace = true, optional = true }
+pallet-randomness-collective-flip = { workspace = true, optional = true }
+pallet-timestamp = { workspace = true, optional = true }
+pallet-treasury = { workspace = true, optional = true }
+pallet-xcm = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+sp-api = { workspace = true, optional = true }
+sp-io = { workspace = true, optional = true }
+substrate-fixed = { workspace = true, optional = true }
+xcm = { workspace = true, optional = true }
+xcm-builder = { workspace = true, optional = true }
+zrml-authorized = { workspace = true, optional = true }
+zrml-court = { workspace = true, optional = true }
+zrml-global-disputes = { workspace = true, optional = true }
+zrml-liquidity-mining = { workspace = true, optional = true }
+zrml-prediction-markets = { workspace = true, optional = true }
+zrml-prediction-markets-runtime-api = { workspace = true, optional = true }
+zrml-rikiddo = { workspace = true, optional = true }
+zrml-simple-disputes = { workspace = true, optional = true }
+zrml-swaps = { workspace = true, optional = true }
+
+
+[dev-dependencies]
+more-asserts = { workspace = true }
+test-case = { workspace = true }
+zrml-neo-swaps = { workspace = true, features = ["mock"] }
+
+[features]
+default = ["std"]
+mock = [
+ "orml-currencies/default",
+ "orml-tokens/default",
+ "pallet-balances",
+ "pallet-randomness-collective-flip/default",
+ "pallet-timestamp/default",
+ "pallet-treasury/default",
+ "sp-api/default",
+ "sp-io/default",
+ "substrate-fixed",
+ "zeitgeist-primitives/mock",
+ "zrml-prediction-markets-runtime-api/default",
+ "zrml-rikiddo/default",
+ "zrml-swaps/default",
+ "xcm/default",
+ "orml-asset-registry/default",
+ "orml-currencies/default",
+ "orml-tokens/default",
+ "pallet-balances/default",
+ "pallet-timestamp/default",
+ "sp-api/default",
+ "sp-io/default",
+ "zrml-court/std",
+ "zrml-authorized/std",
+ "zrml-liquidity-mining/std",
+ "zrml-simple-disputes/std",
+ "zrml-global-disputes/std",
+ "zrml-prediction-markets/std",
+ "zrml-prediction-markets/mock",
+ "zrml-prediction-markets/default",
+ "serde/default",
+]
+parachain = ["zrml-prediction-markets/parachain"]
+runtime-benchmarks = [
+ "frame-benchmarking/runtime-benchmarks",
+ "frame-support/runtime-benchmarks",
+ "frame-system/runtime-benchmarks",
+ "xcm-builder/runtime-benchmarks",
+ "pallet-xcm/runtime-benchmarks",
+]
+std = [
+ "frame-benchmarking?/std",
+ "frame-support/std",
+ "frame-system/std",
+ "orml-traits/std",
+ "parity-scale-codec/std",
+ "sp-runtime/std",
+ "xcm-builder/std",
+ "pallet-xcm/std",
+ "zeitgeist-primitives/std",
+ "zrml-market-commons/std",
+]
+try-runtime = [
+ "frame-support/try-runtime",
+]
+
+[package]
+authors = ["Zeitgeist PM "]
+edition = "2021"
+name = "zrml-neo-swaps"
+version = "0.4.0"
diff --git a/zrml/neo-swaps/README.md b/zrml/neo-swaps/README.md
new file mode 100644
index 000000000..e2b057c39
--- /dev/null
+++ b/zrml/neo-swaps/README.md
@@ -0,0 +1,47 @@
+# Neo Swaps Module
+
+The Neo Swaps module implements liquidity pools which use the Logarithmic Market
+Scoring Rule (LMSR) to determine spot prices and swap amounts, and allow users
+to dynamically provide liquidity.
+
+## Overview
+
+For a detailed description of the underlying mathematics see [here][docslink].
+
+### Terminology
+
+- _Collateral_: The currency type that backs the outcomes in the pool. This is
+ also called the _base asset_ in other contexts.
+- _Exit_: Refers to removing (part of) the liquidity from a liquidity pool in
+ exchange for burning pool shares.
+- _External fees_: After taking swap fees, additional fees can be withdrawn
+ from an informant's collateral. They might go to the chain's treasury or the
+ market creator.
+- _Join_: Refers to adding more liquidity to a pool and receiving pool shares
+ in return.
+- _Liquidity provider_: A user who owns pool shares indicating their stake in
+ the liquidity pool.
+- _Pool Shares_: A token indicating the owner's per rate share of the
+ liquidity pool.
+- _Reserve_: The balances in the liquidity pool used for trading.
+- _Swap fees_: Part of the collateral paid or received by informants that is
+ moved to a separate account owned by the liquidity providers. They need to
+ be withdrawn using the `withdraw_fees` extrinsic.
+
+### Notes
+
+- The `Pool` struct tracks the reserve held in the pool account. The reserve
+ changes when trades are executed or the liquidity changes, but the reserve
+ does not take into account funds that are sent to the pool account
+ unsolicitedly. This fixes a griefing vector which allows an attacker to
+ change prices by sending funds to the pool account.
+- Pool shares are not recorded using the `ZeitgeistAssetManager` trait.
+ Instead, they are part of the `Pool` object and can be tracked using events.
+- When the native currency is used as collateral, the pallet deposits the
+ existential deposit to the pool account (which holds the swap fees). This is
+ done to ensure that small amounts of fees don't cause the entire transaction
+ to error with `ExistentialDeposit`. This "buffer" is removed when the pool
+ is destroyed. The pool account is expected to be whitelisted from dusting
+ for all other assets.
+
+[docslink]: ./docs/docs.pdf
diff --git a/zrml/neo-swaps/docs/docs.pdf b/zrml/neo-swaps/docs/docs.pdf
new file mode 100644
index 000000000..f98e4c2bf
Binary files /dev/null and b/zrml/neo-swaps/docs/docs.pdf differ
diff --git a/zrml/neo-swaps/docs/docs.tex b/zrml/neo-swaps/docs/docs.tex
new file mode 100644
index 000000000..a3712d394
--- /dev/null
+++ b/zrml/neo-swaps/docs/docs.tex
@@ -0,0 +1,260 @@
+\documentclass[12pt]{article}
+
+% Packages
+\usepackage{amsmath}
+\usepackage{amsfonts}
+\usepackage{amssymb}
+\usepackage{graphicx}
+\usepackage{hyperref}
+\usepackage{geometry}
+\usepackage{listings}
+\usepackage{xcolor}
+\usepackage{parskip}
+\geometry{a4paper, margin=1in}
+\newtheorem{theorem}{Theorem}
+\lstset{
+ literate={←}{$\leftarrow$}{1}
+ {→}{$\rightarrow$}{1},
+ basicstyle=\ttfamily\small,
+ keywordstyle=\color{blue},
+ commentstyle=\color{olive},
+ numberstyle=\tiny\color{gray},
+ numbers=left,
+ frame=single,
+ backgroundcolor=\color{yellow!10},
+ breaklines=true,
+ captionpos=b,
+ tabsize=4
+}
+
+\title{zrml-neo-swaps Documentation}
+\date{0.4.1 (\today)}
+
+\begin{document}
+
+\maketitle
+
+\section{Introduction}
+
+This document provides the mathematical and technical details for zrml-neo-swaps. The automatic market maker (AMM) implemented by zrml-neo-swaps is a variant of the Logarithmic Market Scoring Rule (LMSR; \cite{hanson_2003}) which was first developed by Gnosis (see \url{https://docs.gnosis.io/conditionaltokens/docs/introduction3/}). We often refer to it as AMM 2.0.
+
+Unlike the typical implementation using a cost function (see \cite{chen_vaughan_2010}), this implementation of LMSR is a \emph{constant-function market maker} (CFMM), similar to the classical constant product market maker, which allows us to implement \emph{dynamic liquidity}. In other words, liquidity providers (LPs) can come and go as they please, allowing the market to self-regulate how much price resistance the AMM should provide.
+
+As of v0.4.1, the AMM is only available for markets with two outcomes. This will be mitigated in a future publication.
+
+\section{The Trading Function}
+
+We consider a prediction market with $n$ outcomes, denoted by $1, \ldots, n$ for simplicity. Every complete set of outcome tokens is backed a unit of collateral, denotes by \$. The AMM operates on a \emph{liquidity pool} (or just \emph{pool}), which consists of a \emph{reserve} $(r_1, \ldots, r_n)$ of outcome tokens and a \emph{liquidity parameter} $b$. The trading function is defined as
+\[
+ \varphi(b, r) = \sum_i e^{-r_i/b}.
+\]
+In fact, $\varphi(b, r)$ must always equal $1$. This means that a trader may change the reserve from $r$ to $r'$ and receive the delta provided that $\varphi(b, r') = 1$. We denote such a trade by $r \rightarrow r'$. We call these outcome-to-outcome (O2O) swaps.
+
+However, we do not allow users to execute these types of trades. Instead, we only allow \emph{buys} (exchange collateral for outcome tokens) and \emph{sells} (exchange outcome tokens for collateral).
+
+\section{Buying and Selling}
+
+\subsection{Buying}
+
+Buying and selling is implemented by combining complete set operations (exchange $x$ dollars for $x$ units of every outcome) and O2O swaps.
+
+Alice wants to swap $x$ dollars for units of outcome $i$. This is done by exchanging $x$ dollars for $x$ complete sets and then selling all outcomes $k \neq i$ for more $i$ using an O2O swap $r \rightarrow r'$, which yields $y(x)$ additional units of $i$. \emph{Ignoring swap fees}, this modifies the reserve to $r'$, where $r_k' = r_k + x$ for $k \neq i$ and $r_i' = r_i - y(x)$. As trades don't change the invariant, we have $1 = \sum_k e^{-r_k'/b}$. Thus, using $1 = \varphi(b, r) = \sum_k e^{-r_k/b}$,
+\begin{align*}
+ 1 &= \sum_k e^{-r_k'/b} \\
+ &= \sum_{k \neq i} e^{-(r_k + x)/b} + e^{-(r_i-y(x))/b} \\
+ &= e^{-x/b} \sum_{k \neq i} e^{-r_k/b} + e^{y(x)/b} e^{-r_i/b} \\
+ &= e^{-x/b} (1 - e^{-r_i/b}) + e^{y(x)/b} e^{-r_i/b}.
+\end{align*}
+Rearranging these terms gives
+\[
+ e^{y(x)/b} = e^{r_i/b} (1 - e^{-x/b}(1 - e^{-r_i/b})),
+\]
+and, thus,
+\begin{align*}
+ y(x) &= b \ln(e^{r_i/b} (1 - e^{-x/b}(1 - e^{-r_i/b}))) \\
+ &= b \ln (1 - e^{-x/b}(1 - e^{-r_i/b})) + r_i \\
+ &= b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i - x.
+\end{align*}
+
+Note that the total amount of outcome $i$ that Alice receives is $y(x)$ from the O2O trade and $x$ from the complete set operation. We denote this by $z(x) = y(x) + x$.
+
+This allows us to calculate the \emph{spot price} of outcome $i$
+\[
+ p_i(b, r) = \lim_{x \rightarrow 0} \frac{x}{z(x)} = \frac{1}{z'(0)} = \frac{1}{y'(0) + 1}.
+\]
+Calculating the derivative of $y$ yields
+\[
+ y'(x) = \frac{e^{x/b}}{e^{x/b} - 1 + e^{-r_i/b}} - 1
+\]
+and thus $y'(0) = e^{r_i/b} - 1$, which yields $p_i(b, r) = e^{-r_i/b}$.
+
+Note that this means
+\[
+ 1 = \varphi(b, r) = \sum_i p_i(b, r).
+\]
+In particular, $(p_1, \ldots, p_n)$ always maps to a probability distribution.
+
+Trading fees are specified as fractional (a fee of $f = .01$ means that $1\%$ are charged) and deducted from the amount of collateral before the complete set operations are executed. In other words, the liquidity providers receive $fx$ dollars (fees are distributed pro rata amongst the liquidity providers) and Alice goes through the entire process described above with $\tilde x = (1-f)x$ in place of $x$. The spot price taking the fees into account is (as expected)
+\[
+ \psi(b, r, f) = (1 - f)^{-1}e^{-r_i/b}.
+\]
+
+\subsection{Selling}
+
+Alice wants to swap $x$ units of $i$ for dollars. This is done by selling $x' < x$ units of $i$ for $v(x) = x - x'$ units of all outcomes $k \neq i$ and then selling $v(x)$ units of complete sets, which yields $v(x)$ dollars. \emph{Ignoring swap fees}, this modifies the reserve from $r$ to $r'$, where $r_k' = r_k - v(x)$ and $r_i = r_i + x'$. Using $1 = \varphi(b, r')$ and $x' = x - v(x)$, we get
+\begin{align*}
+ 1 &= \sum_k e^{-r_k'/b} \\
+ &= \sum_{k \neq i} e^{-(r_k - v(x))/b} + e^{-(r_i + x')/b} \\
+ &= e^{v(x)/b} \sum_{k \neq i} e^{-r_i/b} + e^{-x'/b} e^{-r_i/b} \\
+ &= e^{v(x)/b} (1 - e^{-r_i/b}) + e^{-x/b} e^{v(x)/b} e^{-r_i/b} \\
+ &= e^{v(x)/b} ( 1 - e^{-r_i/b} + e^{-(r_i + x)/b} ).
+\end{align*}
+Thus, we get
+$$
+ e^{-v(x)/b} = 1 - e^{-r_i/b} + e^{-(r_i + x)/b},
+$$
+which in turn yields \begin{align*}
+ v(x) &= - b \ln (1 - e^{-r_i/b} + e^{-(x+r_i)/b} \\
+ &= -b \ln (e^{r_i/b} - 1 + e^{-x/b}) + r_i \\ &= -b \ln (e^{(x + r_i)/b} - e^{x/b} + 1) + r_i + x.
+\end{align*}
+
+Trading fees are deducted from the amount of collateral received from the complete set operation. In other words, the liquidity providers receive $fv(x)$ dollars and Alice receives $(1-f)v(x)$. The selling price (the amount of collateral received for each unit of $x$), is then (as expected)
+\[
+ \lim_{x \rightarrow 0} \frac{(1-f)v(x)}{x} = (1-f) v'(0) = (1-f) e^{-r_i/b} = (1-f)p_i(b, r).
+\]
+This leads to a typical bid-ask spread between buy and sell price.
+
+\section{Dynamic Liquidity}
+
+Liquidity may be added or removed dynamically to regulate the market's price resistance. Each LP's share of the pool is tracked using pool share tokens, which record their \emph{pro rata} share of the pool.
+
+We consider a pool with liquidity parameter $b$, reserve $r$ and a total issuance of $q$ pool shares.
+
+\subsection{Adding Liquidity}
+
+Alice wants to add liquidity to the pool. She's willing to pay $x$ dollars. To implement this, she first spends $x$ dollars to buy $x$ complete sets.
+
+Now let $i$ be so that $r_i = \max_k r_k$. Let $\lambda = x / r_i$ and $\mu = 1 + \lambda$. For each $k$, Alice moves $\lambda r_k$ units of $k$ into the pool and receives $\lambda q$ pool shares. The liquidity parameter changes from $b$ to $b' = \mu b$. Alice's transfers change the reserve from $r$ to $r' = \mu r$.
+
+The new total issuance of pool shares is $\mu q$ and Alice's share of the pool now is $\lambda / \mu$. Note that Alice retains the balance $(x)^n - \lambda r$ of "left-over tokens".
+
+\subsection{Withdrawing Liquidity}
+
+Alice wants to withdraw liquidity from the pool. She's willing to burn $p$ pool shares.
+
+Let $\lambda = p / q$ and $\mu = 1 - \lambda$. For each $k$, Alice receives $\lambda r_k$ units of $k$ from the pool. The liquidity parameter changes from $b$ to $b' = \mu b$. Alice's transfers change the reserve from $r$ to $r' = \mu r$.
+
+Alice could now convert $x = \min_i r_i$ complete sets of her newly received funds into $x$ dollars. The remainder of the funds will remain in her wallet until the market resolves or she opts to sell them.
+
+\subsection{Fee Distribution}
+
+Fees are distributed pro rata amongst the liquidity providers. These funds are completely separate from the reserve used for trading. Transferring the fees into the pool (like the constant product market maker does) wouldn't make any sense here as collateral is not directly traded on the pool.
+
+\section{Creating Pools}
+
+Creating a pool is straightforward. The initial odds are defined by adding different amounts of each outcome to the pool. If Alice wants to deposit liquidity worth $x$ units of collateral with initial probability $p$, then she starts off by buying $x$ complete sets. The following algorithm is used to calculate how many units of each outcome go into the pool. Alice retains the other tokens as "left-overs".
+
+Let $b = 1$, and let $r_i = - b \ln p_i$ for all $i$. Now let $y = x / \max_i r_i$. Then $y r_i \leq x$ for all $i$ and there exists $i_0$ so that $y r_{i_0} = x$. Set $\tilde r_i = y r_i$ and $\tilde b = yb$. Then
+\[
+ p_i(\tilde r) = e^{-\tilde r_i/\tilde b} = e^{-r_i/b} = p_i
+\]
+and $\max_i \tilde r_i = x$ (so Alice uses up at least one of her outcome balances).
+
+In pseudocode:
+
+\begin{lstlisting}[language=Pascal, caption=Procedure to Calculate Balances]
+Procedure CalculateBalances(p[1...n], x)
+ b ← 1 // Initialize b, larger values may be picked for numerical stability
+ For i from 1 to n do
+ r[i] ← -b * log(p[i])
+ End For
+ y ← x / max(r[1...n])
+ For i from 1 to n do
+ r[i] ← y * r[i]
+ End For
+ b ← y * b
+ Return r, b
+End Procedure
+\end{lstlisting}
+
+\section{Additional Formulas}
+
+\subsection{Estimated Price After Execution}
+
+After executing a buy for $x$ units of collateral for outcome $i$, the new reserve of $i$ is
+\[
+ r_i' = r_i - y((1-f)x) = -b \ln (1 - e^{-(1-f)x/b}(1 - e^{-r_i/b})).
+\]
+Thus, the new price is
+\[
+ p_i(f, b, r') = \frac{1}{1-f} (1 - e^{-(1-f)x/b}(1 - e^{-r_i/b})).
+\]
+
+After executing a sell of $x$ units of outcome $i$ for collateral, the new reserve of $i$ is
+\[
+ r_i' = r_i + x' = r_i + x - v(x) = b \ln (e^{(x + r_i)/b} - e^{x/b} + 1).
+\]
+The new price is therefore
+\[
+ p_i(f, b, r') = \frac{1}{1-f} (e^{(x + r_i)/b} - e^{x/b} + 1).
+\]
+
+\section{Numerical Issues}
+
+Special care must be taken to avoid over- and underflows when calculating expressions like
+\begin{align*}
+ y(x) &= b \ln (e^{x/b} - 1 + e^{-r_i/b}) + r_i - x, \\
+ v(x) &= -b \ln (e^{r_i/b} - 1 + e^{-x/b}) + r_i.
+\end{align*}
+The magnitude of $y(x)$ is the same as $x$, but the exponentials $e^{x/b}$ and $e^{-r_i/b}$ over- or underflow easily.
+
+Let $A = 20$. Python calculates $e^A = 485165195.4097903$ and $e^{-A} = 2.061153622438558 \cdot 10^{-9}$. The fixed crate (see \url{https://crates.io/crates/fixed}) can represent these using \texttt{FixedU128} without considerable loss of precision or risk of over- or underflow. Let $M = e^A$.
+
+Note that for any number $a$, the following are equivalent: 1) $M^{-1} \leq e^a \leq M$, 2) $M^{-1} \leq e^{-a} \leq M$. Thus, the following restrictions prevent over- and underflows when calculating the exponential expressions:
+
+\begin{itemize}
+ \item The amount $x$ must satisfy $x \leq Ab$.
+ \item The price of $i$ must satisfy $p_i(b, r) = e^{-r_i/b} \geq e^{-A}$.
+\end{itemize}
+
+How "bad" are these restrictions? The first restriction is completely irrelevant: Suppose Alice executes a trade of $y(x)$ units of outcome $i$ for $x = Ab$ dollars, the maximum allowed value. Let $q = 1 - e^{-r_i/b} \in (0, 1)$. Then
+\begin{align*}
+ \ln(e^A) - \ln(e^A - q) &= \ln\left(\frac{e^A}{e^A - q}\right) \\
+ &\leq \ln\left(\frac{e^A}{e^A - 1}\right) \\
+ &\approx 2.0611536900435727 \cdot 10^{-9} \\
+ &\leq 10^{-10}.
+\end{align*}
+Let $\varepsilon = 10^{-10}$. Then we have
+\begin{align*}
+ y(x) &= b\ln(e^A - 1 + e^{-r_i/b}) + r_i - x \\
+ &\geq b(\ln(e^A) - \varepsilon) + r_i - x \\
+ &= bA - b\varepsilon + r_i - x \\
+ &= r_i - b\varepsilon.
+\end{align*}
+Thus, Alice receives all funds from the pool except $b \varepsilon$, which is very small unless the pool contains an inordinate amount of liquidity.
+
+The second restriction means that no trades of outcome $i$ can be executed if the price of $i$ drops below the threshold $\varepsilon = e^{-A}$. On markets with two outcomes (binary or scalar), this is equivalent to the price of the other outcome rising above $1 - \varepsilon$. Due to risk considerations, these are generally scenarios that won't occur.
+
+For markets with two outcomes (binary or scalar), we therefore make the following restriction: \emph{Any trade that moves the price of an outcome below $\varepsilon = .005$ (or equivalently, moves the price of an outcome above $1 - \varepsilon$) is not allowed.} This will ensure that the pool is always in a valid state where it can execute trades. Note that in the presence of a swap fee of 1\%, this isn't even a restriction.
+
+Markets with more than two outcomes are currently not allowed to use AMM 2.0 pools. The issue in a market with three or more outcomes $A, B, C, \ldots$ is that if $C$ is a clear underdog and most of the trading happens between the favorites $A$ and $B$, then the price of $C$ might drop below the allowed threshold and \emph{brick} the market of $C$ (all trades involving $C$ must be rejected due to numerical issues). While this is most likely to happen if the market is $C$-weakly trivialized (it is common knowledge that $C$ will almost certainly not materialize), which should never happen on a live market, this is unfortunate. A solution for this issue is provided in the near future.
+
+\newpage
+
+\begin{thebibliography}{9}
+ \bibitem{chen_vaughan_2010}
+ Yiling Chen and Jennifer Wortman Vaughan,
+ \emph{A new understanding of prediction markets via no-regret learning},
+ EC '10: Proceedings of the 11th ACM conference on Electronic commerce,
+ June 2010, Pages 189–198.
+ \url{https://doi.org/10.1145/1807342.1807372}
+
+ \bibitem{hanson_2003}
+ Robin Hanson,
+ \emph{Logarithmic Market Scoring Rules for Modular Combinatorial Information Aggregation},
+ The Journal of Prediction Markets, 1(1),
+ May 2003.
+ \url{https://doi.org/10.5750/jpm.v1i1.417}
+\end{thebibliography}
+
+\end{document}
diff --git a/zrml/neo-swaps/src/benchmarking.rs b/zrml/neo-swaps/src/benchmarking.rs
new file mode 100644
index 000000000..a2dc0b1ee
--- /dev/null
+++ b/zrml/neo-swaps/src/benchmarking.rs
@@ -0,0 +1,239 @@
+// 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(feature = "runtime-benchmarks")]
+
+use super::*;
+use crate::{
+ consts::*, traits::liquidity_shares_manager::LiquiditySharesManager, AssetOf, BalanceOf,
+ MarketIdOf, Pallet as NeoSwaps, Pools,
+};
+use frame_benchmarking::v2::*;
+use frame_support::{
+ assert_ok,
+ storage::{with_transaction, TransactionOutcome::*},
+};
+use frame_system::RawOrigin;
+use orml_traits::MultiCurrency;
+use sp_runtime::{Perbill, SaturatedConversion};
+use zeitgeist_primitives::{
+ constants::CENT,
+ traits::CompleteSetOperationsApi,
+ types::{Asset, Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule},
+};
+use zrml_market_commons::MarketCommonsPalletApi;
+
+macro_rules! assert_ok_with_transaction {
+ ($expr:expr) => {{
+ assert_ok!(with_transaction(|| match $expr {
+ Ok(val) => Commit(Ok(val)),
+ Err(err) => Rollback(Err(err)),
+ }));
+ }};
+}
+
+fn create_market(
+ caller: T::AccountId,
+ base_asset: AssetOf,
+ asset_count: AssetIndexType,
+) -> MarketIdOf {
+ let market = Market {
+ base_asset,
+ creation: MarketCreation::Permissionless,
+ creator_fee: Perbill::zero(),
+ creator: caller.clone(),
+ oracle: caller,
+ metadata: vec![0, 50],
+ market_type: MarketType::Categorical(asset_count),
+ period: MarketPeriod::Block(0u32.into()..1u32.into()),
+ deadlines: Default::default(),
+ scoring_rule: ScoringRule::Lmsr,
+ status: MarketStatus::Active,
+ report: None,
+ resolved_outcome: None,
+ dispute_mechanism: None,
+ bonds: Default::default(),
+ };
+ let maybe_market_id = T::MarketCommons::push_market(market);
+ maybe_market_id.unwrap()
+}
+
+fn create_market_and_deploy_pool(
+ caller: T::AccountId,
+ base_asset: AssetOf,
+ asset_count: AssetIndexType,
+ amount: BalanceOf,
+) -> MarketIdOf {
+ let market_id = create_market::(caller.clone(), base_asset, asset_count);
+ let total_cost = amount + T::MultiCurrency::minimum_balance(base_asset);
+ assert_ok!(T::MultiCurrency::deposit(base_asset, &caller, total_cost));
+ assert_ok_with_transaction!(T::CompleteSetOperations::buy_complete_set(
+ caller.clone(),
+ market_id,
+ amount
+ ));
+ assert_ok!(NeoSwaps::::deploy_pool(
+ RawOrigin::Signed(caller).into(),
+ market_id,
+ amount,
+ vec![_1_2.saturated_into(), _1_2.saturated_into()],
+ CENT.saturated_into(),
+ ));
+ market_id
+}
+
+#[benchmarks]
+mod benchmarks {
+ use super::*;
+
+ #[benchmark]
+ fn buy() {
+ let alice: T::AccountId = whitelisted_caller();
+ let base_asset = Asset::Ztg;
+ let asset_count = 2u16;
+ let market_id = create_market_and_deploy_pool::(
+ alice,
+ base_asset,
+ asset_count,
+ _10.saturated_into(),
+ );
+ let asset_out = Asset::CategoricalOutcome(market_id, 0);
+ let amount_in = _1.saturated_into();
+ let min_amount_out = 0u8.saturated_into();
+
+ let bob: T::AccountId = whitelisted_caller();
+ assert_ok!(T::MultiCurrency::deposit(base_asset, &bob, amount_in));
+
+ #[extrinsic_call]
+ _(RawOrigin::Signed(bob), market_id, asset_count, asset_out, amount_in, min_amount_out);
+ }
+
+ #[benchmark]
+ fn sell() {
+ let alice: T::AccountId = whitelisted_caller();
+ let base_asset = Asset::Ztg;
+ let market_id =
+ create_market_and_deploy_pool::(alice, base_asset, 2u16, _10.saturated_into());
+ let asset_in = Asset::CategoricalOutcome(market_id, 0);
+ let amount_in = _1.saturated_into();
+ let min_amount_out = 0u8.saturated_into();
+
+ let bob: T::AccountId = whitelisted_caller();
+ assert_ok!(T::MultiCurrency::deposit(asset_in, &bob, amount_in));
+
+ #[extrinsic_call]
+ _(RawOrigin::Signed(bob), market_id, 2, asset_in, amount_in, min_amount_out);
+ }
+
+ #[benchmark]
+ fn join() {
+ let alice: T::AccountId = whitelisted_caller();
+ let base_asset = Asset::Ztg;
+ let market_id = create_market_and_deploy_pool::(
+ alice.clone(),
+ base_asset,
+ 2u16,
+ _10.saturated_into(),
+ );
+ let pool_shares_amount = _1.saturated_into();
+ let max_amounts_in = vec![u128::MAX.saturated_into(), u128::MAX.saturated_into()];
+
+ assert_ok!(T::MultiCurrency::deposit(base_asset, &alice, pool_shares_amount));
+ assert_ok_with_transaction!(T::CompleteSetOperations::buy_complete_set(
+ alice.clone(),
+ market_id,
+ pool_shares_amount
+ ));
+
+ #[extrinsic_call]
+ _(RawOrigin::Signed(alice), market_id, pool_shares_amount, max_amounts_in);
+ }
+
+ // There are two execution paths in `exit`: 1) Keep pool alive or 2) destroy it. Clearly 1) is
+ // heavier.
+ #[benchmark]
+ fn exit() {
+ let alice: T::AccountId = whitelisted_caller();
+ let base_asset = Asset::Ztg;
+ let market_id = create_market_and_deploy_pool::(
+ alice.clone(),
+ base_asset,
+ 2u16,
+ _10.saturated_into(),
+ );
+ let pool_shares_amount = _1.saturated_into();
+ let min_amounts_out = vec![0u8.saturated_into(), 0u8.saturated_into()];
+
+ #[extrinsic_call]
+ _(RawOrigin::Signed(alice), market_id, pool_shares_amount, min_amounts_out);
+
+ assert!(Pools::::contains_key(market_id)); // Ensure we took the right turn.
+ }
+
+ #[benchmark]
+ fn withdraw_fees() {
+ let alice: T::AccountId = whitelisted_caller();
+ let base_asset = Asset::Ztg;
+ let market_id = create_market_and_deploy_pool::(
+ alice.clone(),
+ base_asset,
+ 2u16,
+ _10.saturated_into(),
+ );
+ let fee_amount = _1.saturated_into();
+
+ // Mock up some fees.
+ let mut pool = Pools::::get(market_id).unwrap();
+ assert_ok!(T::MultiCurrency::deposit(base_asset, &pool.account_id, fee_amount));
+ assert_ok!(pool.liquidity_shares_manager.deposit_fees(fee_amount));
+ Pools::::insert(market_id, pool);
+
+ #[extrinsic_call]
+ _(RawOrigin::Signed(alice), market_id);
+ }
+
+ #[benchmark]
+ fn deploy_pool() {
+ let alice: T::AccountId = whitelisted_caller();
+ let base_asset = Asset::Ztg;
+ let market_id = create_market::(alice.clone(), base_asset, 2);
+ let amount = _10.saturated_into();
+ let total_cost = amount + T::MultiCurrency::minimum_balance(base_asset);
+
+ assert_ok!(T::MultiCurrency::deposit(base_asset, &alice, total_cost));
+ assert_ok_with_transaction!(T::CompleteSetOperations::buy_complete_set(
+ alice.clone(),
+ market_id,
+ amount
+ ));
+
+ #[extrinsic_call]
+ _(
+ RawOrigin::Signed(alice),
+ market_id,
+ amount,
+ vec![_1_2.saturated_into(), _1_2.saturated_into()],
+ CENT.saturated_into(),
+ );
+ }
+
+ impl_benchmark_test_suite!(
+ NeoSwaps,
+ crate::mock::ExtBuilder::default().build(),
+ crate::mock::Runtime
+ );
+}
diff --git a/zrml/neo-swaps/src/consts.rs b/zrml/neo-swaps/src/consts.rs
new file mode 100644
index 000000000..cab971b4b
--- /dev/null
+++ b/zrml/neo-swaps/src/consts.rs
@@ -0,0 +1,47 @@
+// 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 zeitgeist_primitives::constants::BASE;
+
+pub(crate) const EXP_NUMERICAL_LIMIT: u128 = 20; // Numerical limit for exp arguments.
+pub(crate) const MAX_ASSETS: u16 = 128;
+
+pub(crate) const _1: u128 = BASE;
+pub(crate) const _2: u128 = 2 * _1;
+pub(crate) const _3: u128 = 3 * _1;
+pub(crate) const _4: u128 = 4 * _1;
+pub(crate) const _5: u128 = 5 * _1;
+pub(crate) const _9: u128 = 9 * _1;
+pub(crate) const _10: u128 = 10 * _1;
+pub(crate) const _20: u128 = 20 * _1;
+pub(crate) const _70: u128 = 70 * _1;
+pub(crate) const _80: u128 = 80 * _1;
+pub(crate) const _100: u128 = 100 * _1;
+pub(crate) const _101: u128 = 101 * _1;
+
+pub(crate) const _1_2: u128 = _1 / 2;
+
+pub(crate) const _1_3: u128 = _1 / 3;
+pub(crate) const _2_3: u128 = _2 / 3;
+
+pub(crate) const _1_4: u128 = _1 / 4;
+pub(crate) const _3_4: u128 = _3 / 4;
+
+pub(crate) const _1_5: u128 = _1 / 5;
+
+pub(crate) const _1_6: u128 = _1 / 6;
+pub(crate) const _5_6: u128 = _5 / 6;
diff --git a/zrml/neo-swaps/src/lib.rs b/zrml/neo-swaps/src/lib.rs
new file mode 100644
index 000000000..a2bf01d6a
--- /dev/null
+++ b/zrml/neo-swaps/src/lib.rs
@@ -0,0 +1,878 @@
+// 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 .
+
+#![doc = include_str!("../README.md")]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+extern crate alloc;
+
+mod benchmarking;
+mod consts;
+mod math;
+mod mock;
+mod tests;
+pub mod traits;
+pub mod types;
+pub mod weights;
+
+pub use pallet::*;
+
+#[frame_support::pallet]
+mod pallet {
+ use crate::{
+ consts::MAX_ASSETS,
+ math::{Math, MathOps},
+ traits::{pool_operations::PoolOperations, DistributeFees, LiquiditySharesManager},
+ types::{FeeDistribution, Pool, SoloLp},
+ weights::*,
+ };
+ use alloc::{collections::BTreeMap, vec, vec::Vec};
+ use core::marker::PhantomData;
+ use frame_support::{
+ dispatch::DispatchResultWithPostInfo,
+ ensure,
+ pallet_prelude::StorageMap,
+ require_transactional,
+ traits::{Get, IsType, StorageVersion},
+ transactional, PalletId, Twox64Concat,
+ };
+ use frame_system::{ensure_signed, pallet_prelude::OriginFor};
+ use orml_traits::MultiCurrency;
+ use sp_runtime::{
+ traits::{AccountIdConversion, CheckedSub, Saturating, Zero},
+ DispatchError, DispatchResult, SaturatedConversion,
+ };
+ use zeitgeist_primitives::{
+ constants::{BASE, CENT},
+ math::fixed::{bdiv, bmul},
+ traits::{CompleteSetOperationsApi, DeployPoolApi},
+ types::{Asset, MarketStatus, MarketType, ScalarPosition, ScoringRule},
+ };
+ use zrml_market_commons::MarketCommonsPalletApi;
+
+ // These should not be config parameters to avoid misconfigurations.
+ pub(crate) const MIN_SWAP_FEE: u128 = BASE / 1_000; // 0.1%.
+ pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
+ pub(crate) const MAX_SPOT_PRICE: u128 = BASE - CENT / 2;
+ pub(crate) const MIN_SPOT_PRICE: u128 = CENT / 2;
+ pub(crate) const MIN_LIQUIDITY: u128 = BASE;
+
+ pub(crate) type AccountIdOf = ::AccountId;
+ pub(crate) type AssetOf = Asset>;
+ pub(crate) type BalanceOf =
+ <::MultiCurrency as MultiCurrency>>::Balance;
+ pub(crate) type AssetIndexType = u16;
+ pub(crate) type MarketIdOf =
+ <::MarketCommons as MarketCommonsPalletApi>::MarketId;
+ pub(crate) type PoolOf = Pool>;
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ type CompleteSetOperations: CompleteSetOperationsApi<
+ AccountId = Self::AccountId,
+ Balance = BalanceOf,
+ MarketId = MarketIdOf,
+ >;
+
+ /// Distribute external fees. The fees are paid from the pool account, which in turn has
+ /// received the fees from the trader.
+ type ExternalFees: DistributeFees<
+ Asset = AssetOf,
+ AccountId = AccountIdOf,
+ Balance = BalanceOf,
+ MarketId = MarketIdOf,
+ >;
+
+ type MarketCommons: MarketCommonsPalletApi;
+
+ type MultiCurrency: MultiCurrency>;
+
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+
+ type WeightInfo: WeightInfoZeitgeist;
+
+ #[pallet::constant]
+ type MaxSwapFee: Get>;
+
+ #[pallet::constant]
+ type PalletId: Get;
+ }
+
+ #[pallet::pallet]
+ #[pallet::storage_version(STORAGE_VERSION)]
+ #[pallet::generate_store(pub(super) trait Store)]
+ pub struct Pallet(PhantomData);
+
+ #[pallet::storage]
+ #[pallet::getter(fn pools)]
+ pub type Pools = StorageMap<_, Twox64Concat, MarketIdOf, PoolOf>;
+
+ #[pallet::event]
+ #[pallet::generate_deposit(fn deposit_event)]
+ pub enum Event
+ where
+ T: Config,
+ {
+ /// Informant bought a position.
+ BuyExecuted {
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ asset_out: AssetOf,
+ amount_in: BalanceOf,
+ amount_out: BalanceOf,
+ swap_fee_amount: BalanceOf,
+ external_fee_amount: BalanceOf,
+ },
+ /// Informant sold a position.
+ SellExecuted {
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ asset_in: AssetOf,
+ amount_in: BalanceOf,
+ amount_out: BalanceOf,
+ swap_fee_amount: BalanceOf,
+ external_fee_amount: BalanceOf,
+ },
+ /// Liquidity provider withdrew fees.
+ FeesWithdrawn { who: T::AccountId, market_id: MarketIdOf, amount: BalanceOf },
+ /// Liquidity provider joined the pool.
+ JoinExecuted {
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ pool_shares_amount: BalanceOf,
+ amounts_in: Vec>,
+ new_liquidity_parameter: BalanceOf,
+ },
+ /// Liquidity provider left the pool.
+ ExitExecuted {
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ pool_shares_amount: BalanceOf,
+ amounts_out: Vec>,
+ new_liquidity_parameter: BalanceOf,
+ },
+ /// Pool was createed.
+ PoolDeployed {
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ pool_shares_amount: BalanceOf,
+ amounts_in: Vec>,
+ liquidity_parameter: BalanceOf,
+ },
+ /// Pool was destroyed.
+ PoolDestroyed {
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ pool_shares_amount: BalanceOf,
+ amounts_out: Vec>,
+ },
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// The number of assets in the pool is above the allowed maximum.
+ AssetCountAboveMax,
+ /// Amount paid is above the specified maximum.
+ AmountInAboveMax,
+ /// Amount received is below the specified minimum.
+ AmountOutBelowMin,
+ /// Specified asset was not found in this pool.
+ AssetNotFound,
+ /// Market already has an associated pool.
+ DuplicatePool,
+ /// Incorrect asset count.
+ IncorrectAssetCount,
+ // Length of `max_amounts_in`, `max_amounts_out` or `spot_prices` must be equal to the
+ // number of outcomes in the market.
+ IncorrectVecLen,
+ /// User doesn't own enough pool shares.
+ InsufficientPoolShares,
+ /// The liquidity in the pool is too low.
+ LiquidityTooLow,
+ /// Sum of spot prices is not `1`.
+ InvalidSpotPrices,
+ /// Market's trading mechanism is not LMSR.
+ InvalidTradingMechanism,
+ /// Pool can only be traded on if the market is active.
+ MarketNotActive,
+ /// Deploying pools is only supported for scalar or binary markets.
+ MarketNotBinaryOrScalar,
+ /// Some calculation failed. This shouldn't happen.
+ MathError,
+ /// The user is not allowed to execute this command.
+ NotAllowed,
+ /// This feature is not yet implemented.
+ NotImplemented,
+ /// Some value in the operation is too large or small.
+ NumericalLimits,
+ /// Outstanding fees prevent liquidity withdrawal.
+ OutstandingFees,
+ /// Specified market does not have a pool.
+ PoolNotFound,
+ /// Spot price is above the allowed maximum.
+ SpotPriceAboveMax,
+ /// Spot price is below the allowed minimum.
+ SpotPriceBelowMin,
+ /// Pool's swap fee exceeds the allowed upper limit.
+ SwapFeeAboveMax,
+ /// Pool's swap fee is below the allowed lower limit.
+ SwapFeeBelowMin,
+ /// This shouldn't happen.
+ Unexpected,
+ /// Specified monetary amount is zero.
+ ZeroAmount,
+ }
+
+ #[pallet::call]
+ impl Pallet {
+ /// Buy outcome tokens from the specified market.
+ ///
+ /// The `amount_in` is paid in collateral. The transaction fails if the amount of outcome
+ /// tokens received is smaller than `min_amount_out`. The user must correctly specify the
+ /// number of outcomes for benchmarking reasons.
+ ///
+ /// # Parameters
+ ///
+ /// - `origin`: The origin account making the purchase.
+ /// - `market_id`: Identifier for the market related to the trade.
+ /// - `asset_count`: Number of assets in the pool.
+ /// - `asset_out`: Asset to be purchased.
+ /// - `amount_in`: Amount of collateral paid by the user.
+ /// - `min_amount_out`: Minimum number of outcome tokens the user expects to receive.
+ ///
+ /// # Complexity
+ ///
+ /// Depends on the implementation of `CompleteSetOperationsApi` and `ExternalFees`; when
+ /// using the canonical implementations, the runtime complexity is `O(asset_count)`.
+ #[pallet::call_index(0)]
+ #[pallet::weight(T::WeightInfo::buy())]
+ #[transactional]
+ pub fn buy(
+ origin: OriginFor,
+ #[pallet::compact] market_id: MarketIdOf,
+ asset_count: AssetIndexType,
+ asset_out: AssetOf,
+ #[pallet::compact] amount_in: BalanceOf,
+ #[pallet::compact] min_amount_out: BalanceOf,
+ ) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+ let asset_count_real = T::MarketCommons::market(&market_id)?.outcomes();
+ ensure!(asset_count == asset_count_real, Error::::IncorrectAssetCount);
+ Self::do_buy(who, market_id, asset_out, amount_in, min_amount_out)?;
+ Ok(Some(T::WeightInfo::buy()).into())
+ }
+
+ /// Sell outcome tokens to the specified market.
+ ///
+ /// The `amount_in` is paid in outcome tokens. The transaction fails if the amount of outcome
+ /// tokens received is smaller than `min_amount_out`. The user must correctly specify the
+ /// number of outcomes for benchmarking reasons.
+ ///
+ /// # Parameters
+ ///
+ /// - `origin`: The origin account making the sale.
+ /// - `market_id`: Identifier for the market related to the trade.
+ /// - `asset_count`: Number of assets in the pool.
+ /// - `asset_in`: Asset to be sold.
+ /// - `amount_in`: Amount of outcome tokens paid by the user.
+ /// - `min_amount_out`: Minimum amount of collateral the user expects to receive.
+ ///
+ /// # Complexity
+ ///
+ /// Depends on the implementation of `CompleteSetOperationsApi` and `ExternalFees`; when
+ /// using the canonical implementations, the runtime complexity is `O(asset_count)`.
+ #[pallet::call_index(1)]
+ #[pallet::weight(T::WeightInfo::sell())]
+ #[transactional]
+ pub fn sell(
+ origin: OriginFor,
+ #[pallet::compact] market_id: MarketIdOf,
+ asset_count: AssetIndexType,
+ asset_in: AssetOf,
+ #[pallet::compact] amount_in: BalanceOf,
+ #[pallet::compact] min_amount_out: BalanceOf,
+ ) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+ let asset_count_real = T::MarketCommons::market(&market_id)?.outcomes();
+ ensure!(asset_count == asset_count_real, Error::::IncorrectAssetCount);
+ Self::do_sell(who, market_id, asset_in, amount_in, min_amount_out)?;
+ Ok(Some(T::WeightInfo::sell()).into())
+ }
+
+ /// Join the liquidity pool for the specified market.
+ ///
+ /// The LP receives pool shares in exchange for staking outcome tokens into the pool. The
+ /// `max_amounts_in` vector specifies the maximum number of each outcome token that the LP is
+ /// willing to deposit. These amounts are used to adjust the outcome balances in the pool
+ /// according to the new proportion of pool shares owned by the LP.
+ ///
+ /// Note that the user must acquire the outcome tokens in a separate transaction, either by
+ /// buying from the pool or by using complete set operations.
+ ///
+ /// # Parameters
+ ///
+ /// - `market_id`: Identifier for the market related to the pool.
+ /// - `pool_shares_amount`: The number of new pool shares the LP will receive.
+ /// - `max_amounts_in`: Vector of the maximum amounts of each outcome token the LP is
+ /// willing to deposit (with outcomes specified in the order of `MarketCommonsApi`).
+ ///
+ /// # Complexity
+ ///
+ /// `O(n)` where `n` is the number of assets in the pool.
+ #[pallet::call_index(2)]
+ #[pallet::weight(T::WeightInfo::join())]
+ #[transactional]
+ pub fn join(
+ origin: OriginFor,
+ #[pallet::compact] market_id: MarketIdOf,
+ #[pallet::compact] pool_shares_amount: BalanceOf,
+ max_amounts_in: Vec>,
+ ) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+ let asset_count = T::MarketCommons::market(&market_id)?.outcomes();
+ ensure!(max_amounts_in.len() == asset_count as usize, Error::::IncorrectVecLen);
+ Self::do_join(who, market_id, pool_shares_amount, max_amounts_in)?;
+ Ok(Some(T::WeightInfo::join()).into())
+ }
+
+ /// Exit the liquidity pool for the specified market.
+ ///
+ /// The LP relinquishes pool shares in exchange for withdrawing outcome tokens from the
+ /// pool. The `min_amounts_out` vector specifies the minimum number of each outcome token
+ /// that the LP expects to withdraw. These minimum amounts are used to adjust the outcome
+ /// balances in the pool, taking into account the reduction in the LP's pool share
+ /// ownership.
+ ///
+ /// The transaction will fail unless the LP withdraws their fees from the pool beforehand. A
+ /// batch transaction is very useful here.
+ ///
+ /// If the LP withdraws all pool shares that exist, then the pool is afterwards destroyed. A
+ /// new pool can be deployed at any time, provided that the market is still open.
+ ///
+ /// The LP is not allowed to leave a positive but small amount liquidity in the pool. If the
+ /// liquidity parameter drops below a certain threshold, the transaction will fail. The only
+ /// solution is to withdraw _all_ liquidity and let the pool die.
+ ///
+ /// # Parameters
+ ///
+ /// - `market_id`: Identifier for the market related to the pool.
+ /// - `pool_shares_amount_out`: The number of pool shares the LP will relinquish.
+ /// - `min_amounts_out`: Vector of the minimum amounts of each outcome token the LP expects
+ /// to withdraw (with outcomes specified in the order given by `MarketCommonsApi`).
+ ///
+ /// # Complexity
+ ///
+ /// `O(n)` where `n` is the number of assets in the pool.
+ #[pallet::call_index(3)]
+ #[pallet::weight(T::WeightInfo::exit())]
+ #[transactional]
+ pub fn exit(
+ origin: OriginFor,
+ #[pallet::compact] market_id: MarketIdOf,
+ #[pallet::compact] pool_shares_amount_out: BalanceOf,
+ min_amounts_out: Vec>,
+ ) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+ let asset_count = T::MarketCommons::market(&market_id)?.outcomes();
+ ensure!(min_amounts_out.len() == asset_count as usize, Error::::IncorrectVecLen);
+ Self::do_exit(who, market_id, pool_shares_amount_out, min_amounts_out)?;
+ Ok(Some(T::WeightInfo::exit()).into())
+ }
+
+ /// Withdraw swap fees from the specified market.
+ ///
+ /// The transaction will fail if the caller is not a liquidity provider. Should always be
+ /// used before calling `exit`.
+ ///
+ /// # Parameters
+ ///
+ /// - `market_id`: Identifier for the market related to the pool.
+ ///
+ /// # Complexity
+ ///
+ /// `O(1)`.
+ #[pallet::call_index(4)]
+ #[pallet::weight(T::WeightInfo::withdraw_fees())]
+ #[transactional]
+ pub fn withdraw_fees(
+ origin: OriginFor,
+ #[pallet::compact] market_id: MarketIdOf,
+ ) -> DispatchResult {
+ let who = ensure_signed(origin)?;
+ Self::do_withdraw_fees(who, market_id)?;
+ Ok(())
+ }
+
+ /// Deploy a pool for the specified market and provide liquidity.
+ ///
+ /// The sender specifies a vector of `spot_prices` for the market's outcomes in the order
+ /// given by the `MarketCommonsApi`. The transaction will fail if the spot prices don't add
+ /// up to exactly `BASE`.
+ ///
+ /// Depending on the values in the `spot_prices`, the transaction will transfer different
+ /// amounts of each outcome to the pool. The sender specifies a maximum `amount` of outcome
+ /// tokens to spend.
+ ///
+ /// Note that the sender must acquire the outcome tokens in a separate transaction by using
+ /// complete set operations. It's therefore convenient to batch this function together with
+ /// a `buy_complete_set` with `amount` as amount of complete sets to buy.
+ ///
+ /// Deploying the pool will cost the signer an additional fee to the tune of the
+ /// collateral's existential deposit. This fee is placed in the pool account and ensures
+ /// that swap fees can be stored in the pool account without triggering dusting or failed
+ /// transfers.
+ ///
+ /// The operation is currently limited to binary and scalar markets.
+ ///
+ /// # Complexity
+ ///
+ /// `O(n)` where `n` is the number of outcomes in the specified market.
+ #[pallet::call_index(5)]
+ #[pallet::weight(T::WeightInfo::deploy_pool())]
+ #[transactional]
+ pub fn deploy_pool(
+ origin: OriginFor,
+ #[pallet::compact] market_id: MarketIdOf,
+ #[pallet::compact] amount: BalanceOf,
+ spot_prices: Vec>,
+ #[pallet::compact] swap_fee: BalanceOf,
+ ) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+ let asset_count = T::MarketCommons::market(&market_id)?.outcomes() as u32;
+ ensure!(spot_prices.len() == asset_count as usize, Error::::IncorrectVecLen);
+ Self::do_deploy_pool(who, market_id, amount, spot_prices, swap_fee)?;
+ Ok(Some(T::WeightInfo::deploy_pool()).into())
+ }
+ }
+
+ impl Pallet {
+ #[require_transactional]
+ fn do_buy(
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ asset_out: AssetOf,
+ amount_in: BalanceOf,
+ min_amount_out: BalanceOf,
+ ) -> DispatchResult {
+ ensure!(amount_in != Zero::zero(), Error::::ZeroAmount);
+ let market = T::MarketCommons::market(&market_id)?;
+ ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive);
+ Self::try_mutate_pool(&market_id, |pool| {
+ ensure!(pool.contains(&asset_out), Error::::AssetNotFound);
+ // Defensive check (shouldn't ever happen)!
+ ensure!(
+ pool.calculate_spot_price(asset_out)? <= MAX_SPOT_PRICE.saturated_into(),
+ Error::::Unexpected
+ );
+ ensure!(amount_in <= pool.calculate_max_amount_in(), Error::::NumericalLimits);
+ T::MultiCurrency::transfer(pool.collateral, &who, &pool.account_id, amount_in)?;
+ let FeeDistribution {
+ remaining: amount_in_minus_fees,
+ swap_fees: swap_fee_amount,
+ external_fees: external_fee_amount,
+ } = Self::distribute_fees(market_id, pool, amount_in)?;
+ let swap_amount_out =
+ pool.calculate_swap_amount_out_for_buy(asset_out, amount_in_minus_fees)?;
+ let amount_out = swap_amount_out.saturating_add(amount_in_minus_fees);
+ ensure!(amount_out >= min_amount_out, Error::::AmountOutBelowMin);
+ // Instead of letting `who` buy the complete sets and then transfer almost all of
+ // the outcomes to the pool account, we prevent `(n-1)` storage reads by using the
+ // pool account to buy. Note that the fees are already in the pool at this point.
+ T::CompleteSetOperations::buy_complete_set(
+ pool.account_id.clone(),
+ market_id,
+ amount_in_minus_fees,
+ )?;
+ T::MultiCurrency::transfer(asset_out, &pool.account_id, &who, amount_out)?;
+ for asset in pool.assets().iter() {
+ pool.increase_reserve(asset, &amount_in_minus_fees)?;
+ if *asset == asset_out {
+ pool.decrease_reserve(asset, &amount_out)?;
+ }
+ }
+ let new_price = pool.calculate_spot_price(asset_out)?;
+ ensure!(
+ new_price <= MAX_SPOT_PRICE.saturated_into(),
+ Error::::SpotPriceAboveMax
+ );
+ Self::deposit_event(Event::::BuyExecuted {
+ who: who.clone(),
+ market_id,
+ asset_out,
+ amount_in,
+ amount_out,
+ swap_fee_amount,
+ external_fee_amount,
+ });
+ Ok(())
+ })
+ }
+
+ #[require_transactional]
+ fn do_sell(
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ asset_in: AssetOf,
+ amount_in: BalanceOf,
+ min_amount_out: BalanceOf,
+ ) -> DispatchResult {
+ ensure!(amount_in != Zero::zero(), Error::::ZeroAmount);
+ let market = T::MarketCommons::market(&market_id)?;
+ ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive);
+ Self::try_mutate_pool(&market_id, |pool| {
+ ensure!(pool.contains(&asset_in), Error::::AssetNotFound);
+ // Defensive check (shouldn't ever happen)!
+ ensure!(
+ pool.calculate_spot_price(asset_in)? >= MIN_SPOT_PRICE.saturated_into(),
+ Error::::Unexpected
+ );
+ ensure!(amount_in <= pool.calculate_max_amount_in(), Error::::NumericalLimits);
+ // Instead of first executing a swap with `(n-1)` transfers from the pool account to
+ // `who` and then selling complete sets, we prevent `(n-1)` storage reads: 1)
+ // Transfer `amount_in` units of `asset_in` to the pool account, 2) sell
+ // `amount_out` complete sets using the pool account, 3) transfer
+ // `amount_out_minus_fees` units of collateral to `who`. The fees automatically end
+ // up in the pool.
+ let amount_out = pool.calculate_swap_amount_out_for_sell(asset_in, amount_in)?;
+ // Beware! This transfer happen _after_ calculating `amount_out`:
+ T::MultiCurrency::transfer(asset_in, &who, &pool.account_id, amount_in)?;
+ T::CompleteSetOperations::sell_complete_set(
+ pool.account_id.clone(),
+ market_id,
+ amount_out,
+ )?;
+ let FeeDistribution {
+ remaining: amount_out_minus_fees,
+ swap_fees: swap_fee_amount,
+ external_fees: external_fee_amount,
+ } = Self::distribute_fees(market_id, pool, amount_out)?;
+ ensure!(amount_out_minus_fees >= min_amount_out, Error::::AmountOutBelowMin);
+ T::MultiCurrency::transfer(
+ pool.collateral,
+ &pool.account_id,
+ &who,
+ amount_out_minus_fees,
+ )?;
+ for asset in pool.assets().iter() {
+ if *asset == asset_in {
+ pool.increase_reserve(asset, &amount_in)?;
+ }
+ pool.decrease_reserve(asset, &amount_out)?;
+ }
+ let new_price = pool.calculate_spot_price(asset_in)?;
+ ensure!(
+ new_price >= MIN_SPOT_PRICE.saturated_into(),
+ Error::::SpotPriceBelowMin
+ );
+ Self::deposit_event(Event::::SellExecuted {
+ who: who.clone(),
+ market_id,
+ asset_in,
+ amount_in,
+ amount_out,
+ swap_fee_amount,
+ external_fee_amount,
+ });
+ Ok(())
+ })
+ }
+
+ #[require_transactional]
+ fn do_join(
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ pool_shares_amount: BalanceOf,
+ max_amounts_in: Vec>,
+ ) -> DispatchResult {
+ ensure!(pool_shares_amount != Zero::zero(), Error::::ZeroAmount);
+ let market = T::MarketCommons::market(&market_id)?;
+ ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive);
+ Self::try_mutate_pool(&market_id, |pool| {
+ // FIXME Round up to avoid exploits.
+ let ratio = bdiv(
+ pool_shares_amount.saturated_into(),
+ pool.liquidity_shares_manager.total_shares()?.saturated_into(),
+ )?;
+ let mut amounts_in = vec![];
+ for (&asset, &max_amount_in) in pool.assets().iter().zip(max_amounts_in.iter()) {
+ let balance_in_pool = pool.reserve_of(&asset)?;
+ // FIXME Round up to avoid exploits.
+ let amount_in = bmul(ratio, balance_in_pool.saturated_into())?.saturated_into();
+ amounts_in.push(amount_in);
+ ensure!(amount_in <= max_amount_in, Error::::AmountInAboveMax);
+ T::MultiCurrency::transfer(asset, &who, &pool.account_id, amount_in)?;
+ }
+ for ((_, balance), &amount_in) in pool.reserves.iter_mut().zip(amounts_in.iter()) {
+ *balance = balance.saturating_add(amount_in);
+ }
+ pool.liquidity_shares_manager.join(&who, pool_shares_amount)?;
+ let new_liquidity_parameter = pool.liquidity_parameter.saturating_add(
+ bmul(ratio.saturated_into(), pool.liquidity_parameter.saturated_into())?
+ .saturated_into(),
+ );
+ pool.liquidity_parameter = new_liquidity_parameter;
+ Self::deposit_event(Event::::JoinExecuted {
+ who: who.clone(),
+ market_id,
+ pool_shares_amount,
+ amounts_in,
+ new_liquidity_parameter,
+ });
+ Ok(())
+ })
+ }
+
+ #[require_transactional]
+ fn do_exit(
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ pool_shares_amount: BalanceOf,
+ min_amounts_out: Vec>,
+ ) -> DispatchResult {
+ ensure!(pool_shares_amount != Zero::zero(), Error::::ZeroAmount);
+ let _ = T::MarketCommons::market(&market_id)?;
+ Pools::::try_mutate_exists(market_id, |maybe_pool| {
+ let pool =
+ maybe_pool.as_mut().ok_or::(Error::::PoolNotFound.into())?;
+ ensure!(
+ pool.liquidity_shares_manager.fees == Zero::zero(),
+ Error::::OutstandingFees
+ );
+ let ratio = bdiv(
+ pool_shares_amount.saturated_into(),
+ pool.liquidity_shares_manager.total_shares()?.saturated_into(),
+ )?;
+ let mut amounts_out = vec![];
+ for (&asset, &min_amount_out) in pool.assets().iter().zip(min_amounts_out.iter()) {
+ let balance_in_pool = pool.reserve_of(&asset)?;
+ let amount_out: BalanceOf =
+ bmul(ratio, balance_in_pool.saturated_into())?.saturated_into();
+ amounts_out.push(amount_out);
+ ensure!(amount_out >= min_amount_out, Error::::AmountOutBelowMin);
+ T::MultiCurrency::transfer(asset, &pool.account_id, &who, amount_out)?;
+ }
+ for ((_, balance), &amount_out) in pool.reserves.iter_mut().zip(amounts_out.iter())
+ {
+ *balance = balance.saturating_sub(amount_out);
+ }
+ pool.liquidity_shares_manager.exit(&who, pool_shares_amount)?;
+ if pool.liquidity_shares_manager.total_shares()? == Zero::zero() {
+ // FIXME We will withdraw all remaining funds (the "buffer"). This is an ugly
+ // hack and system should offer the option to whitelist accounts.
+ let remaining =
+ T::MultiCurrency::free_balance(pool.collateral, &pool.account_id);
+ T::MultiCurrency::withdraw(pool.collateral, &pool.account_id, remaining)?;
+ *maybe_pool = None; // Delete the storage map entry.
+ Self::deposit_event(Event::::PoolDestroyed {
+ who: who.clone(),
+ market_id,
+ pool_shares_amount,
+ amounts_out,
+ });
+ } else {
+ let liq = pool.liquidity_parameter;
+ let new_liquidity_parameter = liq.saturating_sub(
+ bmul(ratio.saturated_into(), liq.saturated_into())?.saturated_into(),
+ );
+ ensure!(
+ new_liquidity_parameter >= MIN_LIQUIDITY.saturated_into(),
+ Error::::LiquidityTooLow
+ );
+ pool.liquidity_parameter = new_liquidity_parameter;
+ Self::deposit_event(Event::::ExitExecuted {
+ who: who.clone(),
+ market_id,
+ pool_shares_amount,
+ amounts_out,
+ new_liquidity_parameter,
+ });
+ }
+ Ok(())
+ })
+ }
+
+ #[require_transactional]
+ fn do_withdraw_fees(who: T::AccountId, market_id: MarketIdOf) -> DispatchResult {
+ Self::try_mutate_pool(&market_id, |pool| {
+ let amount = pool.liquidity_shares_manager.withdraw_fees(&who)?;
+ T::MultiCurrency::transfer(pool.collateral, &pool.account_id, &who, amount)?; // Should never fail.
+ Self::deposit_event(Event::::FeesWithdrawn {
+ who: who.clone(),
+ market_id,
+ amount,
+ });
+ Ok(())
+ })
+ }
+
+ #[require_transactional]
+ fn do_deploy_pool(
+ who: T::AccountId,
+ market_id: MarketIdOf,
+ amount: BalanceOf,
+ spot_prices: Vec>,
+ swap_fee: BalanceOf,
+ ) -> DispatchResult {
+ ensure!(!Pools::::contains_key(market_id), Error::::DuplicatePool);
+ let market = T::MarketCommons::market(&market_id)?;
+ ensure!(market.creator == who, Error::::NotAllowed);
+ ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive);
+ ensure!(market.scoring_rule == ScoringRule::Lmsr, Error::::InvalidTradingMechanism);
+ let asset_count = spot_prices.len();
+ ensure!(asset_count as u16 == market.outcomes(), Error::::IncorrectVecLen);
+ ensure!(market.outcomes() == 2, Error::::MarketNotBinaryOrScalar);
+ ensure!(market.outcomes() <= MAX_ASSETS, Error::::AssetCountAboveMax);
+ ensure!(swap_fee >= MIN_SWAP_FEE.saturated_into(), Error::::SwapFeeBelowMin);
+ ensure!(swap_fee <= T::MaxSwapFee::get(), Error::::SwapFeeAboveMax);
+ ensure!(
+ spot_prices
+ .iter()
+ .fold(Zero::zero(), |acc: BalanceOf, &val| acc.saturating_add(val))
+ == BASE.saturated_into(),
+ Error::::InvalidSpotPrices
+ );
+ for &p in spot_prices.iter() {
+ ensure!(
+ p.saturated_into::() >= MIN_SPOT_PRICE,
+ Error::::SpotPriceBelowMin
+ );
+ ensure!(
+ p.saturated_into::() <= MAX_SPOT_PRICE,
+ Error::::SpotPriceAboveMax
+ );
+ }
+ let (liquidity_parameter, amounts_in) =
+ Math::::calculate_reserves_from_spot_prices(amount, spot_prices)?;
+ ensure!(
+ liquidity_parameter >= MIN_LIQUIDITY.saturated_into(),
+ Error::::LiquidityTooLow
+ );
+ let pool_account_id = Self::pool_account_id(&market_id);
+ let assets = Self::outcomes(market_id)?;
+ let mut reserves = BTreeMap::new();
+ for (&amount_in, &asset) in amounts_in.iter().zip(assets.iter()) {
+ T::MultiCurrency::transfer(asset, &who, &pool_account_id, amount_in)?;
+ let _ = reserves.insert(asset, amount_in);
+ }
+ let pool = Pool {
+ account_id: pool_account_id,
+ reserves,
+ collateral: market.base_asset,
+ liquidity_parameter,
+ liquidity_shares_manager: SoloLp::new(who.clone(), amount),
+ swap_fee,
+ };
+ // FIXME Ensure that the existential deposit doesn't kill fees. This is an ugly hack and
+ // system should offer the option to whitelist accounts.
+ T::MultiCurrency::transfer(
+ pool.collateral,
+ &who,
+ &pool.account_id,
+ T::MultiCurrency::minimum_balance(pool.collateral),
+ )?;
+ Pools::::insert(market_id, pool);
+ Self::deposit_event(Event::::PoolDeployed {
+ who,
+ market_id,
+ pool_shares_amount: amount,
+ amounts_in,
+ liquidity_parameter,
+ });
+ Ok(())
+ }
+
+ #[inline]
+ pub(crate) fn pool_account_id(market_id: &MarketIdOf) -> T::AccountId {
+ T::PalletId::get().into_sub_account_truncating((*market_id).saturated_into::())
+ }
+
+ /// Distribute swap fees and external fees and returns the remaining amount.
+ ///
+ /// # Arguments
+ ///
+ /// - `market_id`: The ID of the market to which the pool belongs.
+ /// - `pool`: The pool on which the trade was executed.
+ /// - `amount`: The gross amount from which the fee is deduced.
+ ///
+ /// Will fail if the total amount of fees is more than the gross amount. In particular, the
+ /// function will fail if the external fees exceed the gross amount.
+ #[require_transactional]
+ fn distribute_fees(
+ market_id: MarketIdOf,
+ pool: &mut PoolOf,
+ amount: BalanceOf,
+ ) -> Result, DispatchError> {
+ let swap_fees_u128 = bmul(pool.swap_fee.saturated_into(), amount.saturated_into())?;
+ let swap_fees = swap_fees_u128.saturated_into();
+ pool.liquidity_shares_manager.deposit_fees(swap_fees)?; // Should only error unexpectedly!
+ let external_fees = T::ExternalFees::distribute(
+ market_id,
+ pool.collateral,
+ pool.account_id.clone(),
+ amount,
+ );
+ let total_fees = external_fees.saturating_add(swap_fees);
+ let remaining = amount.checked_sub(&total_fees).ok_or(Error::::Unexpected)?;
+ Ok(FeeDistribution { remaining, swap_fees, external_fees })
+ }
+
+ // FIXME Carbon copy of a function in prediction-markets. To be removed later.
+ fn outcomes(market_id: MarketIdOf) -> Result