From 3d9bbff91219bb324f047427224ee318061a6d43 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Wed, 11 Oct 2023 18:34:50 +0200 Subject: [PATCH] Implement AMM 2.0-light (#1092) * Implement neo-swaps * Fix compile issues * Implement `DeployPoolsApi` as noop * Fix formatting * Add missing copyright notices * Fix clippy issues * Fix more clippy issues and rename `balances` to `reserves` * Remove `println!` * Add missing copyright notice * . * Add benchmarking and include `NeoSwaps` in Runtime * Add neo-swaps benchmarks; remove `split` (for now) * Add benchmarks for `create_market_and_deploy_pool` * Properly implement `DeployPool` * Fix benchmarks, remove unnecessary parameter * Fix formatting * Fix dependencies * Add math docs * Fix typo * Make me codeowner of neo-swaps * Add neo-swaps to main README and fix link to PDF * Fix link * Update versions to v0.4.0 (#1098) * Update weights (#1101) * Remove unnecessary comment from toml * Use default features * Bump version * Use checked math in `SoloLp` * Make match expression explicit * Add comment about saturation * Use `MultiCurrency` instead of `ZeitgeistAssetManager` * Apply suggestions from code review Co-authored-by: Harald Heckmann * Remove superfluous inserts * Fix error documentation * Abstract reserve updates into `Pool` * Make complete set operations require transactional * Make test name not coffee-based * Update zrml/neo-swaps/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/neo-swaps/src/mock.rs Co-authored-by: Harald Heckmann * Reorganize tests * Format * Fix comment * Remove `macros.rs` * Fix benchmarks * Fix formatting * Update benchmark script and add preliminary benchmarks * Update ED buffer handling * Fix documentation * Implement market creation fees for neo-swaps * Use `MarketCreatorFee` in runtime * Add missing files * Add weights * Reduce length of `MarketsCollectingSubsidy` (#1118) * Add bad block of the proof size fiasko to Battery Station chain spec (#1119) Add bad block to Battery Station chain spec * Update weights v0.4.0 (#1121) * Update moonbeam dependencies (bench fix) * Update weights * Fix docs * Rename `IndexType` and change its type value * Remove commented code * Fix order of config parameters * Specify order of assets * Remove commented code * Apply suggestions from code review Co-authored-by: Chralt * Fix `u16`/`u32` casts * Update zrml/prediction-markets/src/benchmarks.rs Co-authored-by: Chralt * Update zrml/prediction-markets/src/benchmarks.rs Co-authored-by: Chralt * Update zrml/neo-swaps/src/math.rs Co-authored-by: Chralt * Update primitives/src/math/fixed.rs Co-authored-by: Chralt * Update primitives/src/math/fixed.rs Co-authored-by: Chralt * Fix formatting * Fix conflicts * Fix dispute period * Format code --------- Co-authored-by: Harald Heckmann Co-authored-by: Chralt --- CODEOWNERS | 3 +- Cargo.lock | 95 ++ Cargo.toml | 7 + README.md | 12 +- docs/changelog_for_devs.md | 32 + primitives/Cargo.toml | 3 + primitives/src/constants.rs | 6 +- primitives/src/constants/mock.rs | 10 +- primitives/src/lib.rs | 2 + primitives/src/math/check_arithm_rslt.rs | 81 ++ primitives/src/math/consts.rs | 37 + primitives/src/math/fixed.rs | 824 ++++++++++++++++ primitives/src/math/mod.rs | 20 + primitives/src/pool.rs | 1 + primitives/src/traits.rs | 4 + .../src/traits/complete_set_operations_api.rs | 35 + primitives/src/traits/deploy_pool_api.rs | 33 + runtime/battery-station/Cargo.toml | 7 +- runtime/battery-station/src/parameters.rs | 4 + runtime/common/Cargo.toml | 2 + runtime/common/src/lib.rs | 29 +- runtime/zeitgeist/Cargo.toml | 6 +- runtime/zeitgeist/src/parameters.rs | 4 + scripts/benchmarks/configuration.sh | 4 +- zrml/neo-swaps/Cargo.toml | 109 +++ zrml/neo-swaps/README.md | 47 + zrml/neo-swaps/docs/docs.pdf | Bin 0 -> 204370 bytes zrml/neo-swaps/docs/docs.tex | 260 ++++++ zrml/neo-swaps/src/benchmarking.rs | 239 +++++ zrml/neo-swaps/src/consts.rs | 47 + zrml/neo-swaps/src/lib.rs | 878 ++++++++++++++++++ zrml/neo-swaps/src/math.rs | 282 ++++++ zrml/neo-swaps/src/mock.rs | 506 ++++++++++ zrml/neo-swaps/src/tests/buy.rs | 355 +++++++ zrml/neo-swaps/src/tests/deploy_pool.rs | 481 ++++++++++ zrml/neo-swaps/src/tests/exit.rs | 317 +++++++ zrml/neo-swaps/src/tests/join.rs | 249 +++++ zrml/neo-swaps/src/tests/mod.rs | 120 +++ zrml/neo-swaps/src/tests/sell.rs | 336 +++++++ zrml/neo-swaps/src/tests/withdraw_fees.rs | 67 ++ zrml/neo-swaps/src/traits/distribute_fees.rs | 43 + .../src/traits/liquidity_shares_manager.rs | 49 + zrml/neo-swaps/src/traits/mod.rs | 24 + zrml/neo-swaps/src/traits/pool_operations.rs | 83 ++ zrml/neo-swaps/src/types/fee_distribution.rs | 24 + .../neo-swaps/src/types/market_creator_fee.rs | 59 ++ zrml/neo-swaps/src/types/mod.rs | 26 + zrml/neo-swaps/src/types/pool.rs | 135 +++ zrml/neo-swaps/src/types/solo_lp.rs | 88 ++ zrml/neo-swaps/src/weights.rs | 163 ++++ zrml/prediction-markets/src/benchmarks.rs | 41 +- zrml/prediction-markets/src/lib.rs | 299 ++++-- zrml/prediction-markets/src/mock.rs | 106 ++- zrml/prediction-markets/src/tests.rs | 108 +++ zrml/prediction-markets/src/weights.rs | 28 + zrml/swaps/src/lib.rs | 14 +- zrml/swaps/src/utils.rs | 6 +- 57 files changed, 6720 insertions(+), 130 deletions(-) create mode 100644 primitives/src/math/check_arithm_rslt.rs create mode 100644 primitives/src/math/consts.rs create mode 100644 primitives/src/math/fixed.rs create mode 100644 primitives/src/math/mod.rs create mode 100644 primitives/src/traits/complete_set_operations_api.rs create mode 100644 primitives/src/traits/deploy_pool_api.rs create mode 100644 zrml/neo-swaps/Cargo.toml create mode 100644 zrml/neo-swaps/README.md create mode 100644 zrml/neo-swaps/docs/docs.pdf create mode 100644 zrml/neo-swaps/docs/docs.tex create mode 100644 zrml/neo-swaps/src/benchmarking.rs create mode 100644 zrml/neo-swaps/src/consts.rs create mode 100644 zrml/neo-swaps/src/lib.rs create mode 100644 zrml/neo-swaps/src/math.rs create mode 100644 zrml/neo-swaps/src/mock.rs create mode 100644 zrml/neo-swaps/src/tests/buy.rs create mode 100644 zrml/neo-swaps/src/tests/deploy_pool.rs create mode 100644 zrml/neo-swaps/src/tests/exit.rs create mode 100644 zrml/neo-swaps/src/tests/join.rs create mode 100644 zrml/neo-swaps/src/tests/mod.rs create mode 100644 zrml/neo-swaps/src/tests/sell.rs create mode 100644 zrml/neo-swaps/src/tests/withdraw_fees.rs create mode 100644 zrml/neo-swaps/src/traits/distribute_fees.rs create mode 100644 zrml/neo-swaps/src/traits/liquidity_shares_manager.rs create mode 100644 zrml/neo-swaps/src/traits/mod.rs create mode 100644 zrml/neo-swaps/src/traits/pool_operations.rs create mode 100644 zrml/neo-swaps/src/types/fee_distribution.rs create mode 100644 zrml/neo-swaps/src/types/market_creator_fee.rs create mode 100644 zrml/neo-swaps/src/types/mod.rs create mode 100644 zrml/neo-swaps/src/types/pool.rs create mode 100644 zrml/neo-swaps/src/types/solo_lp.rs create mode 100644 zrml/neo-swaps/src/weights.rs 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 0000000000000000000000000000000000000000..f98e4c2bfa37a5691c36ecc673d53b7cb94130c1 GIT binary patch literal 204370 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 WQ~*2rTt)`QrWVFrs;aL3Zd?Fxz0o58 literal 0 HcmV?d00001 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>, DispatchError> { + let market = T::MarketCommons::market(&market_id)?; + Ok(match market.market_type { + MarketType::Categorical(categories) => { + let mut assets = Vec::new(); + for i in 0..categories { + assets.push(Asset::CategoricalOutcome(market_id, i)); + } + assets + } + MarketType::Scalar(_) => { + vec![ + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + Asset::ScalarOutcome(market_id, ScalarPosition::Short), + ] + } + }) + } + + fn try_mutate_pool(market_id: &MarketIdOf, mutator: F) -> DispatchResult + where + F: FnMut(&mut PoolOf) -> DispatchResult, + { + Pools::::try_mutate(market_id, |maybe_pool| { + maybe_pool.as_mut().ok_or(Error::::PoolNotFound.into()).and_then(mutator) + }) + } + } + + impl DeployPoolApi for Pallet { + type AccountId = T::AccountId; + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + fn deploy_pool( + who: Self::AccountId, + market_id: Self::MarketId, + amount: Self::Balance, + spot_prices: Vec, + swap_fee: Self::Balance, + ) -> DispatchResult { + Self::do_deploy_pool(who, market_id, amount, spot_prices, swap_fee) + } + } +} diff --git a/zrml/neo-swaps/src/math.rs b/zrml/neo-swaps/src/math.rs new file mode 100644 index 000000000..f0978deb6 --- /dev/null +++ b/zrml/neo-swaps/src/math.rs @@ -0,0 +1,282 @@ +// 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 crate::{BalanceOf, Config, Error}; +use alloc::vec::Vec; +use core::marker::PhantomData; +use fixed::FixedU128; +use hydra_dx_math::transcendental::{exp, ln}; +use sp_runtime::{DispatchError, SaturatedConversion}; +use typenum::U80; + +type Fractional = U80; +type Fixed = FixedU128; + +pub(crate) trait MathOps { + fn calculate_swap_amount_out_for_buy( + reserve: BalanceOf, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + fn calculate_swap_amount_out_for_sell( + reserve: BalanceOf, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + fn calculate_spot_price( + reserve: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + fn calculate_reserves_from_spot_prices( + amount: BalanceOf, + spot_prices: Vec>, + ) -> Result<(BalanceOf, Vec>), DispatchError>; +} + +pub(crate) struct Math(PhantomData); + +impl MathOps for Math { + fn calculate_swap_amount_out_for_buy( + reserve: BalanceOf, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + let reserve = reserve.saturated_into(); + let amount_in = amount_in.saturated_into(); + let liquidity = liquidity.saturated_into(); + detail::calculate_swap_amount_out_for_buy(reserve, amount_in, liquidity) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } + + fn calculate_swap_amount_out_for_sell( + reserve: BalanceOf, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + let reserve = reserve.saturated_into(); + let amount_in = amount_in.saturated_into(); + let liquidity = liquidity.saturated_into(); + detail::calculate_swap_amount_out_for_sell(reserve, amount_in, liquidity) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } + + fn calculate_spot_price( + reserve: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + let reserve = reserve.saturated_into(); + let liquidity = liquidity.saturated_into(); + detail::calculate_spot_price(reserve, liquidity) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } + + fn calculate_reserves_from_spot_prices( + amount: BalanceOf, + spot_prices: Vec>, + ) -> Result<(BalanceOf, Vec>), DispatchError> { + let amount = amount.saturated_into(); + let spot_prices = spot_prices.into_iter().map(|p| p.saturated_into()).collect(); + let (liquidity, spot_prices) = + detail::calculate_reserves_from_spot_prices(amount, spot_prices) + .ok_or_else(|| -> DispatchError { Error::::MathError.into() })?; + let liquidity = liquidity.saturated_into(); + let spot_prices = spot_prices.into_iter().map(|p| p.saturated_into()).collect(); + Ok((liquidity, spot_prices)) + } +} + +mod detail { + use super::*; + use zeitgeist_primitives::{ + constants::DECIMALS, + math::fixed::{IntoFixedDecimal, IntoFixedFromDecimal}, + }; + + /// Calculate b * ln( e^(x/b) − 1 + e^(−r_i/b) ) + r_i − x + pub(super) fn calculate_swap_amount_out_for_buy( + reserve: u128, + amount_in: u128, + liquidity: u128, + ) -> Option { + let result_fixed = calculate_swap_amount_out_for_buy_fixed( + to_fixed(reserve)?, + to_fixed(amount_in)?, + to_fixed(liquidity)?, + )?; + from_fixed(result_fixed) + } + + /// Calculate –1 * b * ln( e^(-x/b) − 1 + e^(r_i/b) ) + r_i + pub(super) fn calculate_swap_amount_out_for_sell( + reserve: u128, + amount_in: u128, + liquidity: u128, + ) -> Option { + let result_fixed = calculate_swap_amount_out_for_sell_fixed( + to_fixed(reserve)?, + to_fixed(amount_in)?, + to_fixed(liquidity)?, + )?; + from_fixed(result_fixed) + } + + /// Calculate e^(-r_i/b). + pub(super) fn calculate_spot_price(reserve: u128, liquidity: u128) -> Option { + let result_fixed = calculate_spot_price_fixed(to_fixed(reserve)?, to_fixed(liquidity)?)?; + from_fixed(result_fixed) + } + + pub(super) fn calculate_reserves_from_spot_prices( + amount: u128, + spot_prices: Vec, + ) -> Option<(u128, Vec)> { + let (liquidity_fixed, reserve_fixed) = calculate_reserve_from_spot_prices_fixed( + to_fixed(amount)?, + spot_prices.into_iter().map(to_fixed).collect::>>()?, + )?; + let liquidity = from_fixed(liquidity_fixed)?; + let reserve = reserve_fixed.into_iter().map(from_fixed).collect::>>()?; + Some((liquidity, reserve)) + } + + fn to_fixed(value: B) -> Option + where + B: Into + From, + { + value.to_fixed_from_fixed_decimal(DECIMALS).ok() + } + + fn from_fixed(value: Fixed) -> Option + where + B: Into + From, + { + value.to_fixed_decimal(DECIMALS).ok() + } + + fn calculate_swap_amount_out_for_buy_fixed( + reserve: Fixed, + amount_in: Fixed, + liquidity: Fixed, + ) -> Option { + // FIXME Defensive programming: Check for underflow in x/b and r_i/b. + let exp_x_over_b: Fixed = exp(amount_in.checked_div(liquidity)?, false).ok()?; + let exp_neg_r_over_b = exp(reserve.checked_div(liquidity)?, true).ok()?; + // FIXME Defensive programming: Check for underflow in the exponential expressions. + let inside_ln = + exp_x_over_b.checked_add(exp_neg_r_over_b)?.checked_sub(Fixed::checked_from_num(1)?)?; + let (ln_result, ln_neg) = ln(inside_ln).ok()?; + let blob = liquidity.checked_mul(ln_result)?; + let reserve_plus_blob = + if ln_neg { reserve.checked_sub(blob)? } else { reserve.checked_add(blob)? }; + reserve_plus_blob.checked_sub(amount_in) + } + + fn calculate_swap_amount_out_for_sell_fixed( + reserve: Fixed, + amount_in: Fixed, + liquidity: Fixed, + ) -> Option { + // FIXME Defensive programming: Check for underflow in x/b and r_i/b. + let exp_neg_x_over_b: Fixed = exp(amount_in.checked_div(liquidity)?, true).ok()?; + let exp_r_over_b = exp(reserve.checked_div(liquidity)?, false).ok()?; + // FIXME Defensive programming: Check for underflow in the exponential expressions. + let inside_ln = + exp_neg_x_over_b.checked_add(exp_r_over_b)?.checked_sub(Fixed::checked_from_num(1)?)?; + let (ln_result, ln_neg) = ln(inside_ln).ok()?; + let blob = liquidity.checked_mul(ln_result)?; + if ln_neg { reserve.checked_add(blob) } else { reserve.checked_sub(blob) } + } + + pub(crate) fn calculate_spot_price_fixed(reserve: Fixed, liquidity: Fixed) -> Option { + exp(reserve.checked_div(liquidity)?, true).ok() + } + + fn calculate_reserve_from_spot_prices_fixed( + amount: Fixed, + spot_prices: Vec, + ) -> Option<(Fixed, Vec)> { + // FIXME Defensive programming - ensure against underflows + let tmp_reserves = spot_prices + .iter() + // Drop the bool (second tuple component) as ln(p) is always negative. + .map(|&price| ln(price).map(|(value, _)| value)) + .collect::, _>>() + .ok()?; + let max_value = *tmp_reserves.iter().max()?; + let liquidity = amount.checked_div(max_value)?; + let reserves: Vec = + tmp_reserves.iter().map(|&r| r.checked_mul(liquidity)).collect::>>()?; + Some((liquidity, reserves)) + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::{assert_approx, consts::*}; + use std::str::FromStr; + use test_case::test_case; + + // Example taken from + // https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-lmsr + #[test] + fn calculate_swap_amount_out_for_buy_works() { + let liquidity = 144269504088; + assert_eq!( + calculate_swap_amount_out_for_buy(_10, _10, liquidity).unwrap(), + 58496250072 + ); + } + + #[test] + fn calculate_swap_amount_out_for_sell_works() { + let liquidity = 144269504088; + assert_eq!( + calculate_swap_amount_out_for_sell(_10, _10, liquidity).unwrap(), + 41503749928 + ); + } + + #[test] + fn calcuate_spot_price_works() { + let liquidity = 144269504088; + assert_eq!(calculate_spot_price(_10, liquidity).unwrap(), _1_2); + assert_eq!(calculate_spot_price(_10 - 58496250072, liquidity).unwrap(), _3_4); + assert_eq!(calculate_spot_price(_20, liquidity).unwrap(), _1_4); + } + + #[test] + fn calculate_reserves_from_spot_prices_works() { + let expected_liquidity = 144269504088; + let (liquidity, reserves) = + calculate_reserves_from_spot_prices(_10, vec![_1_2, _1_2]).unwrap(); + assert_approx!(liquidity, expected_liquidity, 1); + assert_eq!(reserves, vec![_10, _10]); + } + + // This test ensures that we don't mess anything up when we change precision. + #[test_case(false, Fixed::from_str("10686474581524.462146990468650739308072").unwrap())] + #[test_case(true, Fixed::from_str("0.000000000000093576229688").unwrap())] + fn exp_does_not_overflow_or_underflow(neg: bool, expected: Fixed) { + let value = 30; + let result: Fixed = exp(Fixed::checked_from_num(value).unwrap(), neg).unwrap(); + assert_eq!(result, expected); + } + } +} diff --git a/zrml/neo-swaps/src/mock.rs b/zrml/neo-swaps/src/mock.rs new file mode 100644 index 000000000..5939c574e --- /dev/null +++ b/zrml/neo-swaps/src/mock.rs @@ -0,0 +1,506 @@ +// Copyright 2022-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 . + +#![cfg(feature = "mock")] +#![allow( + // Mocks are only used for fuzzing and unit tests + clippy::arithmetic_side_effects, + clippy::too_many_arguments, +)] + +use crate as zrml_neo_swaps; +use crate::{consts::*, AssetOf, MarketIdOf}; +use core::marker::PhantomData; +use frame_support::{ + construct_runtime, ord_parameter_types, parameter_types, + traits::{Contains, Everything, NeverEnsureOrigin}, +}; +use frame_system::{EnsureRoot, EnsureSignedBy}; +#[cfg(feature = "parachain")] +use orml_asset_registry::AssetMetadata; +use orml_traits::MultiCurrency; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Get, IdentityLookup}, + DispatchResult, Percent, SaturatedConversion, +}; +use substrate_fixed::{types::extra::U33, FixedI128, FixedU128}; +#[cfg(feature = "parachain")] +use zeitgeist_primitives::types::Asset; +use zeitgeist_primitives::{ + constants::mock::{ + AddOutcomePeriod, AggregationPeriod, AppealBond, AppealPeriod, AuthorizedPalletId, + BalanceFractionalDecimals, BlockHashCount, BlocksPerYear, CorrectionPeriod, CourtPalletId, + ExistentialDeposit, ExistentialDeposits, ExitFee, GdVotingPeriod, GetNativeCurrencyId, + GlobalDisputeLockId, GlobalDisputesPalletId, InflationPeriod, LiquidityMiningPalletId, + LockId, MaxAppeals, MaxApprovals, MaxAssets, MaxCourtParticipants, MaxCreatorFee, + MaxDelegations, MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, MaxGlobalDisputeVotes, + MaxGracePeriod, MaxInRatio, MaxLocks, MaxMarketLifetime, MaxOracleDuration, MaxOutRatio, + MaxOwners, MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, MaxSubsidyPeriod, MaxSwapFee, + MaxTotalWeight, MaxWeight, MinAssets, MinCategories, MinDisputeDuration, MinJurorStake, + MinOracleDuration, MinOutcomeVoteAmount, MinSubsidy, MinSubsidyPeriod, MinWeight, + MinimumPeriod, NeoMaxSwapFee, NeoSwapsPalletId, OutcomeBond, OutcomeFactor, OutsiderBond, + PmPalletId, RemoveKeysLimit, RequestInterval, SimpleDisputesPalletId, SwapsPalletId, + TreasuryPalletId, VotePeriod, VotingOutcomeFee, BASE, CENT, + }, + traits::DeployPoolApi, + types::{ + AccountIdTest, Amount, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, CurrencyId, + Hash, Index, MarketId, Moment, PoolId, UncheckedExtrinsicTest, + }, +}; +use zrml_neo_swaps::{traits::DistributeFees, BalanceOf}; +use zrml_rikiddo::types::{EmaMarketVolume, FeeSigmoid, RikiddoSigmoidMV}; + +pub const ALICE: AccountIdTest = 0; +#[allow(unused)] +pub const BOB: AccountIdTest = 1; +pub const CHARLIE: AccountIdTest = 2; +pub const DAVE: AccountIdTest = 3; +pub const EVE: AccountIdTest = 4; +pub const FEE_ACCOUNT: AccountIdTest = 5; +pub const SUDO: AccountIdTest = 123456; +pub const EXTERNAL_FEES: Balance = CENT; + +#[cfg(feature = "parachain")] +pub const FOREIGN_ASSET: Asset = Asset::ForeignAsset(1); + +parameter_types! { + pub const FeeAccount: AccountIdTest = FEE_ACCOUNT; +} +ord_parameter_types! { + pub const AuthorizedDisputeResolutionUser: AccountIdTest = ALICE; +} +ord_parameter_types! { + pub const Sudo: AccountIdTest = SUDO; +} +parameter_types! { + pub storage NeoMinSwapFee: Balance = 0; +} +parameter_types! { + pub const MinSubsidyPerAccount: Balance = BASE; + pub const AdvisoryBond: Balance = 0; + pub const AdvisoryBondSlashPercentage: Percent = Percent::from_percent(10); + pub const OracleBond: Balance = 0; + pub const ValidityBond: Balance = 0; + pub const DisputeBond: Balance = 0; + pub const MaxCategories: u16 = MAX_ASSETS + 1; +} + +pub struct DeployPoolNoop; + +impl DeployPoolApi for DeployPoolNoop { + type AccountId = AccountIdTest; + type Balance = Balance; + type MarketId = MarketId; + + fn deploy_pool( + _who: Self::AccountId, + _market_id: Self::MarketId, + _amount: Self::Balance, + _swap_prices: Vec, + _swap_fee: Self::Balance, + ) -> DispatchResult { + Ok(()) + } +} + +pub struct ExternalFees(PhantomData, PhantomData); + +impl DistributeFees for ExternalFees +where + F: Get, +{ + type Asset = AssetOf; + type AccountId = T::AccountId; + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + fn distribute( + _market_id: Self::MarketId, + asset: Self::Asset, + account: Self::AccountId, + amount: Self::Balance, + ) -> Self::Balance { + let fees = zeitgeist_primitives::math::fixed::bmul( + amount.saturated_into(), + EXTERNAL_FEES.saturated_into(), + ) + .unwrap() + .saturated_into(); + let _ = T::MultiCurrency::transfer(asset, &account, &F::get(), fees); + fees + } +} + +pub type UncheckedExtrinsic = UncheckedExtrinsicTest; + +pub struct DustRemovalWhitelist; + +impl Contains for DustRemovalWhitelist { + fn contains(account_id: &AccountIdTest) -> bool { + *account_id == FEE_ACCOUNT + } +} + +construct_runtime!( + pub enum Runtime + where + Block = BlockTest, + NodeBlock = BlockTest, + UncheckedExtrinsic = UncheckedExtrinsic, + { + NeoSwaps: zrml_neo_swaps::{Call, Event, Pallet}, + Authorized: zrml_authorized::{Event, Pallet, Storage}, + Balances: pallet_balances::{Call, Config, Event, Pallet, Storage}, + Court: zrml_court::{Event, Pallet, Storage}, + AssetManager: orml_currencies::{Call, Pallet, Storage}, + LiquidityMining: zrml_liquidity_mining::{Config, Event, Pallet}, + MarketCommons: zrml_market_commons::{Pallet, Storage}, + PredictionMarkets: zrml_prediction_markets::{Event, Pallet, Storage}, + RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Pallet, Storage}, + RikiddoSigmoidFeeMarketEma: zrml_rikiddo::{Pallet, Storage}, + SimpleDisputes: zrml_simple_disputes::{Event, Pallet, Storage}, + GlobalDisputes: zrml_global_disputes::{Event, Pallet, Storage}, + Swaps: zrml_swaps::{Call, Event, Pallet}, + System: frame_system::{Call, Config, Event, Pallet, Storage}, + Timestamp: pallet_timestamp::{Pallet}, + Tokens: orml_tokens::{Config, Event, Pallet, Storage}, + Treasury: pallet_treasury::{Call, Event, Pallet, Storage}, + } +); + +impl crate::Config for Runtime { + type MultiCurrency = AssetManager; + type CompleteSetOperations = PredictionMarkets; + type ExternalFees = ExternalFees; + type MarketCommons = MarketCommons; + type RuntimeEvent = RuntimeEvent; + type MaxSwapFee = NeoMaxSwapFee; + type PalletId = NeoSwapsPalletId; + type WeightInfo = zrml_neo_swaps::weights::WeightInfo; +} + +impl pallet_randomness_collective_flip::Config for Runtime {} + +impl zrml_rikiddo::Config for Runtime { + type Timestamp = Timestamp; + type Balance = Balance; + type FixedTypeU = FixedU128; + type FixedTypeS = FixedI128; + type BalanceFractionalDecimals = BalanceFractionalDecimals; + type PoolId = PoolId; + type Rikiddo = RikiddoSigmoidMV< + Self::FixedTypeU, + Self::FixedTypeS, + FeeSigmoid, + EmaMarketVolume, + >; +} + +impl zrml_prediction_markets::Config for Runtime { + type AdvisoryBond = AdvisoryBond; + type AdvisoryBondSlashPercentage = AdvisoryBondSlashPercentage; + type ApproveOrigin = EnsureSignedBy; + #[cfg(feature = "parachain")] + type AssetRegistry = MockRegistry; + type Authorized = Authorized; + type CloseOrigin = EnsureSignedBy; + type Court = Court; + type DeployPool = DeployPoolNoop; + type DestroyOrigin = EnsureSignedBy; + type DisputeBond = DisputeBond; + type RuntimeEvent = RuntimeEvent; + type GlobalDisputes = GlobalDisputes; + type LiquidityMining = LiquidityMining; + type MaxCategories = MaxCategories; + type MaxDisputes = MaxDisputes; + type MinDisputeDuration = MinDisputeDuration; + type MinOracleDuration = MinOracleDuration; + type MaxCreatorFee = MaxCreatorFee; + type MaxDisputeDuration = MaxDisputeDuration; + type MaxGracePeriod = MaxGracePeriod; + type MaxOracleDuration = MaxOracleDuration; + type MaxSubsidyPeriod = MaxSubsidyPeriod; + type MaxMarketLifetime = MaxMarketLifetime; + type MinCategories = MinCategories; + type MinSubsidyPeriod = MinSubsidyPeriod; + type MaxEditReasonLen = MaxEditReasonLen; + type MaxRejectReasonLen = MaxRejectReasonLen; + type OracleBond = OracleBond; + type OutsiderBond = OutsiderBond; + type PalletId = PmPalletId; + type RejectOrigin = EnsureSignedBy; + type RequestEditOrigin = EnsureSignedBy; + type ResolveOrigin = EnsureSignedBy; + type AssetManager = AssetManager; + type SimpleDisputes = SimpleDisputes; + type Slash = Treasury; + type Swaps = Swaps; + type ValidityBond = ValidityBond; + type WeightInfo = zrml_prediction_markets::weights::WeightInfo; +} + +impl zrml_authorized::Config for Runtime { + type AuthorizedDisputeResolutionOrigin = + EnsureSignedBy; + type CorrectionPeriod = CorrectionPeriod; + type RuntimeEvent = RuntimeEvent; + type DisputeResolution = zrml_prediction_markets::Pallet; + type MarketCommons = MarketCommons; + type PalletId = AuthorizedPalletId; + type WeightInfo = zrml_authorized::weights::WeightInfo; +} + +impl zrml_court::Config for Runtime { + type AppealBond = AppealBond; + type BlocksPerYear = BlocksPerYear; + type DisputeResolution = zrml_prediction_markets::Pallet; + type VotePeriod = VotePeriod; + type AggregationPeriod = AggregationPeriod; + type AppealPeriod = AppealPeriod; + type LockId = LockId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type InflationPeriod = InflationPeriod; + type MarketCommons = MarketCommons; + type MaxAppeals = MaxAppeals; + type MaxDelegations = MaxDelegations; + type MaxSelectedDraws = MaxSelectedDraws; + type MaxCourtParticipants = MaxCourtParticipants; + type MinJurorStake = MinJurorStake; + type MonetaryGovernanceOrigin = EnsureRoot; + type PalletId = CourtPalletId; + type Random = RandomnessCollectiveFlip; + type RequestInterval = RequestInterval; + type Slash = Treasury; + type TreasuryPalletId = TreasuryPalletId; + type WeightInfo = zrml_court::weights::WeightInfo; +} + +impl zrml_liquidity_mining::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MarketCommons = MarketCommons; + type MarketId = MarketId; + type PalletId = LiquidityMiningPalletId; + type WeightInfo = zrml_liquidity_mining::weights::WeightInfo; +} + +impl frame_system::Config for Runtime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountIdTest; + type BaseCallFilter = Everything; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockNumber = BlockNumber; + type BlockWeights = (); + type RuntimeCall = RuntimeCall; + type DbWeight = (); + type RuntimeEvent = RuntimeEvent; + type Hash = Hash; + type Hashing = BlakeTwo256; + type Header = Header; + type Index = Index; + type Lookup = IdentityLookup; + type MaxConsumers = frame_support::traits::ConstU32<16>; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type RuntimeOrigin = RuntimeOrigin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +impl orml_currencies::Config for Runtime { + type GetNativeCurrencyId = GetNativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type WeightInfo = (); +} + +impl orml_tokens::Config for Runtime { + type Amount = Amount; + type Balance = Balance; + type CurrencyId = CurrencyId; + type DustRemovalWhitelist = DustRemovalWhitelist; + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type CurrencyHooks = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl zrml_market_commons::Config for Runtime { + type Currency = Balances; + type MarketId = MarketId; + type PredictionMarketsPalletId = PmPalletId; + type Timestamp = Timestamp; +} + +impl pallet_timestamp::Config for Runtime { + type MinimumPeriod = MinimumPeriod; + type Moment = Moment; + type OnTimestampSet = (); + type WeightInfo = (); +} + +impl zrml_simple_disputes::Config for Runtime { + type AssetManager = AssetManager; + type RuntimeEvent = RuntimeEvent; + type OutcomeBond = OutcomeBond; + type OutcomeFactor = OutcomeFactor; + type DisputeResolution = zrml_prediction_markets::Pallet; + type MarketCommons = MarketCommons; + type MaxDisputes = MaxDisputes; + type PalletId = SimpleDisputesPalletId; + type WeightInfo = zrml_simple_disputes::weights::WeightInfo; +} + +impl zrml_global_disputes::Config for Runtime { + type AddOutcomePeriod = AddOutcomePeriod; + type RuntimeEvent = RuntimeEvent; + type DisputeResolution = zrml_prediction_markets::Pallet; + type MarketCommons = MarketCommons; + type Currency = Balances; + type GlobalDisputeLockId = GlobalDisputeLockId; + type GlobalDisputesPalletId = GlobalDisputesPalletId; + type MaxGlobalDisputeVotes = MaxGlobalDisputeVotes; + type MaxOwners = MaxOwners; + type MinOutcomeVoteAmount = MinOutcomeVoteAmount; + type RemoveKeysLimit = RemoveKeysLimit; + type GdVotingPeriod = GdVotingPeriod; + type VotingOutcomeFee = VotingOutcomeFee; + type WeightInfo = zrml_global_disputes::weights::WeightInfo; +} + +impl zrml_swaps::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ExitFee = ExitFee; + type FixedTypeU = ::FixedTypeU; + type FixedTypeS = ::FixedTypeS; + type LiquidityMining = LiquidityMining; + type MarketCommons = MarketCommons; + type MaxAssets = MaxAssets; + type MaxInRatio = MaxInRatio; + type MaxOutRatio = MaxOutRatio; + type MaxSwapFee = MaxSwapFee; + type MaxTotalWeight = MaxTotalWeight; + type MaxWeight = MaxWeight; + type MinAssets = MinAssets; + type MinSubsidy = MinSubsidy; + type MinSubsidyPerAccount = MinSubsidyPerAccount; + type MinWeight = MinWeight; + type PalletId = SwapsPalletId; + type RikiddoSigmoidFeeMarketEma = RikiddoSigmoidFeeMarketEma; + type AssetManager = AssetManager; + type WeightInfo = zrml_swaps::weights::WeightInfo; +} + +impl pallet_treasury::Config for Runtime { + type ApproveOrigin = EnsureSignedBy; + type Burn = (); + type BurnDestination = (); + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type MaxApprovals = MaxApprovals; + type OnSlash = (); + type PalletId = TreasuryPalletId; + type ProposalBond = (); + type ProposalBondMinimum = (); + type ProposalBondMaximum = (); + type RejectOrigin = EnsureSignedBy; + type SpendFunds = (); + type SpendOrigin = NeverEnsureOrigin; + type SpendPeriod = (); + type WeightInfo = (); +} + +#[cfg(feature = "parachain")] +zrml_prediction_markets::impl_mock_registry! { + MockRegistry, + CurrencyId, + Balance, + zeitgeist_primitives::types::CustomMetadata +} + +#[allow(unused)] +pub struct ExtBuilder { + balances: Vec<(AccountIdTest, Balance)>, +} + +// FIXME Remove this in favor of adding whatever the account need in the individual tests. +#[allow(unused)] +impl Default for ExtBuilder { + fn default() -> Self { + Self { balances: vec![(ALICE, _101), (CHARLIE, _1), (DAVE, _1), (EVE, _1)] } + } +} + +#[allow(unused)] +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { balances: self.balances } + .assimilate_storage(&mut t) + .unwrap(); + #[cfg(feature = "parachain")] + use frame_support::traits::GenesisBuild; + #[cfg(feature = "parachain")] + orml_tokens::GenesisConfig:: { balances: vec![(ALICE, FOREIGN_ASSET, _101)] } + .assimilate_storage(&mut t) + .unwrap(); + #[cfg(feature = "parachain")] + let custom_metadata = zeitgeist_primitives::types::CustomMetadata { + allow_as_base_asset: true, + ..Default::default() + }; + #[cfg(feature = "parachain")] + orml_asset_registry_mock::GenesisConfig { + metadata: vec![( + FOREIGN_ASSET, + AssetMetadata { + decimals: 18, + name: "MKL".as_bytes().to_vec(), + symbol: "MKL".as_bytes().to_vec(), + existential_deposit: 0, + location: None, + additional: custom_metadata, + }, + )], + } + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } +} diff --git a/zrml/neo-swaps/src/tests/buy.rs b/zrml/neo-swaps/src/tests/buy.rs new file mode 100644 index 000000000..d93aa5ce8 --- /dev/null +++ b/zrml/neo-swaps/src/tests/buy.rs @@ -0,0 +1,355 @@ +// 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 super::*; +use test_case::test_case; + +// Example taken from +// https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-lmsr +#[test] +fn buy_works() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let liquidity = _10; + let spot_prices = vec![_1_2, _1_2]; + let swap_fee = CENT; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Categorical(2), + liquidity, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(market_id).unwrap(); + let total_fee_percentage = swap_fee + EXTERNAL_FEES; + let amount_in_minus_fees = _10; + let amount_in = bdiv(amount_in_minus_fees, _1 - total_fee_percentage).unwrap(); // This is exactly _10 after deducting fees. + let expected_fees = amount_in - amount_in_minus_fees; + let expected_swap_fee_amount = expected_fees / 2; + let expected_external_fee_amount = expected_fees / 2; + let pool_outcomes_before: Vec<_> = + pool.assets().iter().map(|a| pool.reserve_of(a).unwrap()).collect(); + let pool_liquidity_before = pool.liquidity_parameter; + let asset_out = pool.assets()[0]; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + // Deposit some stuff in the pool account to check that the pools `reserves` fields tracks + // the reserve correctly. + assert_ok!(AssetManager::deposit(asset_out, &pool.account_id, _100)); + assert_ok!(NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + asset_out, + amount_in, + 0, + )); + let pool = Pools::::get(market_id).unwrap(); + assert_eq!(pool.liquidity_parameter, pool_liquidity_before); + assert_eq!(pool.liquidity_shares_manager.owner, ALICE); + assert_eq!(pool.liquidity_shares_manager.total_shares, liquidity); + assert_eq!(pool.liquidity_shares_manager.fees, expected_swap_fee_amount); + let pool_outcomes_after: Vec<_> = pool + .assets() + .iter() + .map(|a| pool.reserve_of(a).unwrap()) + .collect(); + let expected_swap_amount_out = 58496250072; + let expected_amount_in_minus_fees = _10 + 1; // Note: This is 1 Pennock of the correct result. + let expected_amount_out = expected_swap_amount_out + expected_amount_in_minus_fees; + assert_eq!(AssetManager::free_balance(BASE_ASSET, &BOB), 0); + assert_eq!(AssetManager::free_balance(asset_out, &BOB), expected_amount_out); + assert_eq!(pool_outcomes_after[0], pool_outcomes_before[0] - expected_swap_amount_out); + assert_eq!( + pool_outcomes_after[1], + pool_outcomes_before[0] + expected_amount_in_minus_fees, + ); + let expected_pool_account_balance = + expected_swap_fee_amount + AssetManager::minimum_balance(pool.collateral); + assert_eq!( + AssetManager::free_balance(BASE_ASSET, &pool.account_id), + expected_pool_account_balance + ); + assert_eq!( + AssetManager::free_balance(BASE_ASSET, &FEE_ACCOUNT), + expected_external_fee_amount + ); + let price_sum = pool + .assets() + .iter() + .map(|&a| pool.calculate_spot_price(a).unwrap()) + .sum::(); + assert_eq!(price_sum, _1); + System::assert_last_event( + Event::BuyExecuted { + who: BOB, + market_id, + asset_out, + amount_in, + amount_out: expected_amount_out, + swap_fee_amount: expected_swap_fee_amount, + external_fee_amount: expected_external_fee_amount, + } + .into(), + ); + }); +} + +#[test] +fn buy_fails_on_incorrect_asset_count() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 1, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + Error::::IncorrectAssetCount + ); + }); +} + +#[test] +fn buy_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + Markets::::remove(market_id); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Suspended)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::CollectingSubsidy)] +#[test_case(MarketStatus::InsufficientSubsidy)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn buy_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + MarketCommons::mutate_market(&market_id, |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn buy_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + Error::::PoolNotFound, + ); + }); +} + +#[test_case(MarketType::Categorical(2))] +#[test_case(MarketType::Scalar(0..=1))] +fn buy_fails_on_asset_not_found(market_type: MarketType) { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + market_type, + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::CategoricalOutcome(market_id, 2), + _1, + 0 + ), + Error::::AssetNotFound, + ); + }); +} + +#[test] +fn buy_fails_on_numerical_limits() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + let pool = Pools::::get(market_id).unwrap(); + let amount_in = 100 * pool.liquidity_parameter; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + amount_in, + 0, + ), + Error::::NumericalLimits, + ); + }); +} + +#[test] +fn buy_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + let amount_in = _10; + #[cfg(not(feature = "parachain"))] + let expected_error = pallet_balances::Error::::InsufficientBalance; + #[cfg(feature = "parachain")] + let expected_error = orml_tokens::Error::::BalanceTooLow; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in - 1)); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + amount_in, + 0, + ), + expected_error, + ); + }); +} + +#[test] +fn buy_fails_on_amount_out_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + let amount_in = _1; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + // Buying 1 at price of .5 will return less than 2 outcomes due to slippage. + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + amount_in, + _2, + ), + Error::::AmountOutBelowMin, + ); + }); +} + +#[test] +fn buy_fails_on_spot_price_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Categorical(2), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::buy( + RuntimeOrigin::signed(ALICE), + market_id, + 2, + Asset::CategoricalOutcome(market_id, 0), + _70, + 0, + ), + Error::::SpotPriceAboveMax + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/deploy_pool.rs b/zrml/neo-swaps/src/tests/deploy_pool.rs new file mode 100644 index 000000000..59a98522d --- /dev/null +++ b/zrml/neo-swaps/src/tests/deploy_pool.rs @@ -0,0 +1,481 @@ +// 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 super::*; +use test_case::test_case; + +#[test] +fn deploy_pool_works_with_binary_markets() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _10; + let spot_prices = vec![_1_2, _1_2]; + let swap_fee = CENT; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Categorical(2), + amount, + spot_prices.clone(), + swap_fee, + ); + let assets = + vec![Asset::CategoricalOutcome(market_id, 0), Asset::CategoricalOutcome(market_id, 1)]; + let pool = Pools::::get(market_id).unwrap(); + let expected_liquidity = 144269504088; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_eq!(pool.assets(), assets); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1); + assert_eq!(pool.collateral, BASE_ASSET); + assert_eq!(pool.liquidity_shares_manager.owner, ALICE); + assert_eq!(pool.liquidity_shares_manager.total_shares, amount); + assert_eq!(pool.liquidity_shares_manager.fees, 0); + assert_eq!(pool.swap_fee, swap_fee); + assert_eq!(AssetManager::free_balance(pool.collateral, &pool.account_id), buffer); + assert_eq!(AssetManager::free_balance(assets[0], &pool.account_id), amount); + assert_eq!(AssetManager::free_balance(assets[1], &pool.account_id), amount); + assert_eq!(pool.reserve_of(&assets[0]).unwrap(), amount); + assert_eq!(pool.reserve_of(&assets[1]).unwrap(), amount); + assert_eq!(pool.calculate_spot_price(assets[0]).unwrap(), spot_prices[0]); + assert_eq!(pool.calculate_spot_price(assets[1]).unwrap(), spot_prices[1]); + assert_eq!(AssetManager::free_balance(BASE_ASSET, &ALICE), alice_before - amount - buffer); + assert_eq!(AssetManager::free_balance(assets[0], &ALICE), 0); + assert_eq!(AssetManager::free_balance(assets[1], &ALICE), 0); + System::assert_last_event( + Event::PoolDeployed { + who: ALICE, + market_id, + pool_shares_amount: amount, + amounts_in: vec![amount, amount], + liquidity_parameter: pool.liquidity_parameter, + } + .into(), + ); + }); +} + +#[test] +fn deploy_pool_works_with_scalar_marktes() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _100; + let spot_prices = vec![_1_6, _5_6 + 1]; + let swap_fee = CENT; + let market_id: MarketId = 0; + let assets = vec![ + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + Asset::ScalarOutcome(market_id, ScalarPosition::Short), + ]; + // Deploy some funds in the pool account to ensure that rogue funds don't screw up price + // calculatings. + let rogue_funds = _100; + assert_ok!(AssetManager::deposit( + assets[0], + &Pallet::::pool_account_id(&market_id), + rogue_funds + )); + let _ = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + amount, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(market_id).unwrap(); + let expected_liquidity = 558110626551; + let expected_amounts = vec![amount, 101755598229]; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_eq!(pool.assets(), assets); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1_000); + assert_eq!(pool.collateral, BASE_ASSET); + assert_eq!(pool.liquidity_shares_manager.owner, ALICE); + assert_eq!(pool.liquidity_shares_manager.total_shares, amount); + assert_eq!(pool.liquidity_shares_manager.fees, 0); + assert_eq!(pool.swap_fee, swap_fee); + assert_eq!( + AssetManager::free_balance(assets[0], &pool.account_id), + expected_amounts[0] + rogue_funds + ); + assert_eq!(AssetManager::free_balance(assets[1], &pool.account_id), expected_amounts[1]); + assert_eq!(pool.reserve_of(&assets[0]).unwrap(), expected_amounts[0]); + assert_eq!(pool.reserve_of(&assets[1]).unwrap(), expected_amounts[1]); + assert_eq!(pool.calculate_spot_price(assets[0]).unwrap(), spot_prices[0]); + assert_eq!(pool.calculate_spot_price(assets[1]).unwrap(), spot_prices[1]); + assert_eq!(AssetManager::free_balance(BASE_ASSET, &ALICE), alice_before - amount - buffer); + assert_eq!(AssetManager::free_balance(assets[0], &ALICE), 0); + assert_eq!(AssetManager::free_balance(assets[1], &ALICE), amount - expected_amounts[1]); + let price_sum = + pool.assets().iter().map(|&a| pool.calculate_spot_price(a).unwrap()).sum::(); + assert_eq!(price_sum, _1); + System::assert_last_event( + Event::PoolDeployed { + who: ALICE, + market_id, + pool_shares_amount: amount, + amounts_in: expected_amounts, + liquidity_parameter: pool.liquidity_parameter, + } + .into(), + ); + }); +} + +#[test] +fn deploy_pool_fails_on_incorrect_vec_len() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::deploy_pool(RuntimeOrigin::signed(ALICE), market_id, _10, vec![_1_3], CENT), + Error::::IncorrectVecLen + ); + }); +} + +#[test] +fn deploy_pool_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + NeoSwaps::deploy_pool(RuntimeOrigin::signed(ALICE), 0, _10, vec![_1_4, _3_4], CENT), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Suspended)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::CollectingSubsidy)] +#[test_case(MarketStatus::InsufficientSubsidy)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn deploy_pool_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + MarketCommons::mutate_market(&market_id, |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + _1, + vec![_1_2, _1_2], + CENT, + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn deploy_pool_fails_on_duplicate_pool() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + _2, + vec![_1_2, _1_2], + CENT, + ), + Error::::DuplicatePool, + ); + }); +} + +#[test] +fn deploy_pool_fails_on_not_allowed() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(BOB), + market_id, + _10, + vec![_1_4, _3_4], + CENT + ), + Error::::NotAllowed + ); + }); +} + +#[test] +fn deploy_pool_fails_on_invalid_trading_mechanism() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::CPMM); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + _10, + vec![_1_4, _3_4], + CENT + ), + Error::::InvalidTradingMechanism + ); + }); +} + +#[test] +fn deploy_pool_fails_on_market_is_not_binary_or_scalar() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(3), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + vec![_1_3, _1_3, _1_3], + CENT + ), + Error::::MarketNotBinaryOrScalar + ); + }); +} + +// FIXME This test currently fails because the `ensure!` throwing `AssetCountAboveMax` is +// currently unreachable if the market is not binary/scalar. +#[test] +#[should_panic] +fn deploy_pool_fails_on_asset_count_above_max() { + ExtBuilder::default().build().execute_with(|| { + let category_count = MAX_ASSETS + 1; + let market_id = create_market( + ALICE, + BASE_ASSET, + MarketType::Categorical(category_count), + ScoringRule::Lmsr, + ); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + // Depending on the value of MAX_ASSETS and PRICE_BARRIER_*, this `spot_prices` vector + // might violate some other rules for deploying pools. + let mut spot_prices = vec![_1 / category_count as u128; category_count as usize - 1]; + spot_prices.push(_1 - spot_prices.iter().sum::()); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + spot_prices, + CENT + ), + Error::::AssetCountAboveMax + ); + }); +} + +#[test] +fn deploy_pool_fails_on_swap_fee_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + vec![_1_4, _3_4], + MIN_SWAP_FEE - 1, + ), + Error::::SwapFeeBelowMin + ); + }); +} + +#[test] +fn deploy_pool_fails_on_swap_fee_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + vec![_1_4, _3_4], + ::MaxSwapFee::get() + 1, + ), + Error::::SwapFeeAboveMax + ); + }); +} + +#[test_case(vec![_1_4, _3_4 - 1])] +#[test_case(vec![_1_4 + 1, _3_4])] +fn deploy_pool_fails_on_invalid_spot_prices(spot_prices: Vec>) { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + spot_prices, + CENT + ), + Error::::InvalidSpotPrices + ); + }); +} + +#[test] +fn deploy_pool_fails_on_spot_price_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + let spot_price = MIN_SPOT_PRICE - 1; + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + vec![spot_price, _1 - spot_price], + CENT + ), + Error::::SpotPriceBelowMin + ); + }); +} + +#[test] +fn deploy_pool_fails_on_spot_price_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + let spot_price = MAX_SPOT_PRICE + 1; + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + vec![spot_price, _1 - spot_price], + CENT + ), + Error::::SpotPriceAboveMax + ); + }); +} + +#[test] +fn deploy_pool_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::Lmsr); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity - 1, + )); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + vec![_3_4, _1_4], + CENT + ), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn deploy_pool_fails_on_liquidity_too_low() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + let amount = _1_2; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + amount, + )); + assert_noop!( + NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + amount, + vec![_1_2, _1_2], + CENT + ), + Error::::LiquidityTooLow + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/exit.rs b/zrml/neo-swaps/src/tests/exit.rs new file mode 100644 index 000000000..0bfe15412 --- /dev/null +++ b/zrml/neo-swaps/src/tests/exit.rs @@ -0,0 +1,317 @@ +// 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 super::*; + +#[test] +fn exit_works() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let liquidity = _10; + let spot_prices = vec![_1_6, _5_6 + 1]; + let swap_fee = CENT; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + liquidity, + spot_prices.clone(), + swap_fee, + ); + let pool_shares_amount = _4; // Remove 40% to the pool. + let pool_before = Pools::::get(market_id).unwrap(); + let alice_outcomes_before = [ + AssetManager::free_balance(pool_before.assets()[0], &ALICE), + AssetManager::free_balance(pool_before.assets()[1], &ALICE), + ]; + let pool_outcomes_before: Vec<_> = + pool_before.assets().iter().map(|a| pool_before.reserve_of(a).unwrap()).collect(); + assert_ok!(NeoSwaps::exit( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + vec![0, 0], + )); + let pool_after = Pools::::get(market_id).unwrap(); + let ratio = bdiv(pool_shares_amount, liquidity).unwrap(); + let pool_outcomes_after: Vec<_> = + pool_after.assets().iter().map(|a| pool_after.reserve_of(a).unwrap()).collect(); + let expected_pool_diff = vec![ + bmul(ratio, pool_outcomes_before[0]).unwrap(), + bmul(ratio, pool_outcomes_before[1]).unwrap(), + ]; + let alice_outcomes_after = [ + AssetManager::free_balance(pool_after.assets()[0], &ALICE), + AssetManager::free_balance(pool_after.assets()[1], &ALICE), + ]; + assert_eq!(pool_outcomes_after[0], pool_outcomes_before[0] - expected_pool_diff[0]); + assert_eq!(pool_outcomes_after[1], pool_outcomes_before[1] - expected_pool_diff[1]); + assert_eq!(alice_outcomes_after[0], alice_outcomes_before[0] + expected_pool_diff[0]); + assert_eq!(alice_outcomes_after[1], alice_outcomes_before[1] + expected_pool_diff[1]); + assert_eq!( + pool_after.liquidity_parameter, + bmul(_1 - ratio, pool_before.liquidity_parameter).unwrap() + ); + assert_eq!( + pool_after.liquidity_shares_manager.shares_of(&ALICE).unwrap(), + liquidity - pool_shares_amount + ); + System::assert_last_event( + Event::ExitExecuted { + who: ALICE, + market_id, + pool_shares_amount, + amounts_out: expected_pool_diff, + new_liquidity_parameter: pool_after.liquidity_parameter, + } + .into(), + ); + }); +} + +#[test] +fn exit_destroys_pool() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let liquidity = _10; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + liquidity, + vec![_1_6, _5_6 + 1], + CENT, + ); + let pool = Pools::::get(market_id).unwrap(); + let amounts_out = vec![ + pool.reserve_of(&pool.assets()[0]).unwrap(), + pool.reserve_of(&pool.assets()[1]).unwrap(), + ]; + let alice_before = [ + AssetManager::free_balance(pool.assets()[0], &ALICE), + AssetManager::free_balance(pool.assets()[1], &ALICE), + ]; + assert_ok!(NeoSwaps::exit(RuntimeOrigin::signed(ALICE), market_id, liquidity, vec![0, 0])); + assert!(!Pools::::contains_key(market_id)); + assert_eq!(AssetManager::free_balance(pool.collateral, &pool.account_id), 0); + assert_eq!(AssetManager::free_balance(pool.assets()[0], &pool.account_id), 0); + assert_eq!(AssetManager::free_balance(pool.assets()[1], &pool.account_id), 0); + assert_eq!( + AssetManager::free_balance(pool.assets()[0], &ALICE), + alice_before[0] + amounts_out[0] + ); + assert_eq!( + AssetManager::free_balance(pool.assets()[1], &ALICE), + alice_before[1] + amounts_out[1] + ); + System::assert_last_event( + Event::PoolDestroyed { + who: ALICE, + market_id, + pool_shares_amount: liquidity, + amounts_out, + } + .into(), + ); + }); +} + +#[test] +fn exit_fails_on_incorrect_vec_len() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::exit(RuntimeOrigin::signed(ALICE), market_id, _1, vec![0]), + Error::::IncorrectVecLen + ); + }); +} + +#[test] +fn exit_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + Markets::::remove(market_id); + assert_noop!( + NeoSwaps::exit(RuntimeOrigin::signed(ALICE), market_id, _1, vec![0, 0]), + zrml_market_commons::Error::::MarketDoesNotExist + ); + }); +} + +#[test] +fn exit_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::exit(RuntimeOrigin::signed(ALICE), market_id, _1, vec![0, 0]), + Error::::PoolNotFound, + ); + }); +} + +#[test] +fn exit_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let liquidity = _10; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + liquidity, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::exit( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity + 1, // One more than Alice has. + vec![0, 0] + ), + Error::::InsufficientPoolShares, + ); + }); +} + +#[test] +fn exit_fails_on_amount_out_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _20, + vec![_1_2, _1_2], + CENT, + ); + let pool_shares_amount = _5; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + )); + assert_noop!( + NeoSwaps::exit( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + vec![pool_shares_amount + 1, pool_shares_amount] + ), + Error::::AmountOutBelowMin + ); + }); +} + +#[test] +fn exit_fails_if_not_allowed() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _20, + vec![_1_2, _1_2], + CENT, + ); + let pool_shares_amount = _5; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, pool_shares_amount)); + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(BOB), + market_id, + pool_shares_amount, + )); + assert_noop!( + NeoSwaps::exit( + RuntimeOrigin::signed(BOB), + market_id, + pool_shares_amount, + vec![pool_shares_amount, pool_shares_amount] + ), + Error::::NotAllowed + ); + }); +} + +#[test] +fn exit_fails_on_outstanding_fees() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _20, + vec![_1_2, _1_2], + CENT, + ); + let pool_shares_amount = _20; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, pool_shares_amount)); + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(BOB), + market_id, + pool_shares_amount, + )); + assert_ok!(Pools::::try_mutate(market_id, |pool| pool + .as_mut() + .unwrap() + .liquidity_shares_manager + .deposit_fees(1))); + assert_noop!( + NeoSwaps::exit( + RuntimeOrigin::signed(BOB), + market_id, + pool_shares_amount, + vec![pool_shares_amount, pool_shares_amount] + ), + Error::::OutstandingFees + ); + }); +} + +#[test] +fn exit_pool_fails_on_liquidity_too_low() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + // Will result in liquidity of about 0.7213475204444817. + assert_noop!( + NeoSwaps::exit(RuntimeOrigin::signed(ALICE), market_id, _10 - _1_2, vec![0, 0]), + Error::::LiquidityTooLow + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/join.rs b/zrml/neo-swaps/src/tests/join.rs new file mode 100644 index 000000000..ff07328af --- /dev/null +++ b/zrml/neo-swaps/src/tests/join.rs @@ -0,0 +1,249 @@ +// 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 super::*; +use test_case::test_case; + +#[test] +fn join_works() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let liquidity = _10; + let spot_prices = vec![_1_6, _5_6 + 1]; + let swap_fee = CENT; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + liquidity, + spot_prices.clone(), + swap_fee, + ); + let pool_shares_amount = _4; // Add 40% to the pool. + assert_ok!(AssetManager::deposit(BASE_ASSET, &ALICE, pool_shares_amount)); + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + )); + let pool_before = Pools::::get(market_id).unwrap(); + let alice_long_before = AssetManager::free_balance(pool_before.assets()[1], &ALICE); + let pool_outcomes_before: Vec<_> = + pool_before.assets().iter().map(|a| pool_before.reserve_of(a).unwrap()).collect(); + assert_ok!(NeoSwaps::join( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + vec![u128::MAX, u128::MAX], + )); + let pool_after = Pools::::get(market_id).unwrap(); + let ratio = bdiv(liquidity + pool_shares_amount, liquidity).unwrap(); + let pool_outcomes_after: Vec<_> = + pool_after.assets().iter().map(|a| pool_after.reserve_of(a).unwrap()).collect(); + assert_eq!(pool_outcomes_after[0], bmul(ratio, pool_outcomes_before[0]).unwrap()); + assert_eq!(pool_outcomes_after[1], bmul(ratio, pool_outcomes_before[1]).unwrap()); + let long_diff = pool_outcomes_after[1] - pool_outcomes_before[1]; + assert_eq!(AssetManager::free_balance(pool_after.assets()[0], &ALICE), 0); + assert_eq!( + AssetManager::free_balance(pool_after.assets()[1], &ALICE), + alice_long_before - long_diff + ); + assert_eq!( + pool_after.liquidity_parameter, + bmul(ratio, pool_before.liquidity_parameter).unwrap() + ); + assert_eq!( + pool_after.liquidity_shares_manager.shares_of(&ALICE).unwrap(), + liquidity + pool_shares_amount + ); + System::assert_last_event( + Event::JoinExecuted { + who: ALICE, + market_id, + pool_shares_amount, + amounts_in: vec![pool_shares_amount, long_diff], + new_liquidity_parameter: pool_after.liquidity_parameter, + } + .into(), + ); + }); +} + +#[test] +fn join_fails_on_incorrect_vec_len() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::join(RuntimeOrigin::signed(ALICE), market_id, _1, vec![0]), + Error::::IncorrectVecLen + ); + }); +} + +#[test] +fn join_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + Markets::::remove(market_id); + assert_noop!( + NeoSwaps::join(RuntimeOrigin::signed(ALICE), market_id, _1, vec![u128::MAX, u128::MAX]), + zrml_market_commons::Error::::MarketDoesNotExist + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Suspended)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::CollectingSubsidy)] +#[test_case(MarketStatus::InsufficientSubsidy)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn join_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + MarketCommons::mutate_market(&market_id, |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::join(RuntimeOrigin::signed(BOB), market_id, _1, vec![u128::MAX, u128::MAX]), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn join_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::join( + RuntimeOrigin::signed(ALICE), + market_id, + _1, + vec![u128::MAX, u128::MAX], + ), + Error::::PoolNotFound, + ); + }); +} + +#[test] +fn join_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::join( + RuntimeOrigin::signed(ALICE), + market_id, + _100, + vec![u128::MAX, u128::MAX] + ), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn join_fails_on_amount_in_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _20, + vec![_1_2, _1_2], + CENT, + ); + let pool_shares_amount = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + )); + assert_noop!( + NeoSwaps::join( + RuntimeOrigin::signed(ALICE), + market_id, + pool_shares_amount, + vec![pool_shares_amount - 1, pool_shares_amount] + ), + Error::::AmountInAboveMax + ); + }); +} + +#[test] +fn join_fails_if_not_allowed() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _20, + vec![_1_2, _1_2], + CENT, + ); + let pool_shares_amount = _5; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, pool_shares_amount)); + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(BOB), + market_id, + pool_shares_amount, + )); + assert_noop!( + NeoSwaps::join( + RuntimeOrigin::signed(BOB), + market_id, + pool_shares_amount, + vec![pool_shares_amount, pool_shares_amount] + ), + Error::::NotAllowed + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/mod.rs b/zrml/neo-swaps/src/tests/mod.rs new file mode 100644 index 000000000..0e2633f84 --- /dev/null +++ b/zrml/neo-swaps/src/tests/mod.rs @@ -0,0 +1,120 @@ +// 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(all(feature = "mock", test))] + +mod buy; +mod deploy_pool; +mod exit; +mod join; +mod sell; +mod withdraw_fees; + +use crate::{consts::*, mock::*, traits::*, *}; +use frame_support::{assert_noop, assert_ok}; +use orml_traits::MultiCurrency; +use sp_runtime::Perbill; +use zeitgeist_primitives::{ + constants::CENT, + math::fixed::{bdiv, bmul}, + types::{ + AccountIdTest, Asset, Deadlines, MarketCreation, MarketId, MarketPeriod, MarketStatus, + MarketType, MultiHash, ScalarPosition, ScoringRule, + }, +}; +use zrml_market_commons::{MarketCommonsPalletApi, Markets}; + +#[cfg(not(feature = "parachain"))] +const BASE_ASSET: Asset = Asset::Ztg; +#[cfg(feature = "parachain")] +const BASE_ASSET: Asset = FOREIGN_ASSET; + +fn create_market( + creator: AccountIdTest, + base_asset: Asset, + market_type: MarketType, + scoring_rule: ScoringRule, +) -> MarketId { + let mut metadata = [2u8; 50]; + metadata[0] = 0x15; + metadata[1] = 0x30; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(creator), + base_asset, + Perbill::zero(), + EVE, + MarketPeriod::Block(0..2), + Deadlines { + grace_period: 0_u32.into(), + oracle_duration: ::MinOracleDuration::get(), + dispute_duration: 0_u32.into(), + }, + MultiHash::Sha3_384(metadata), + MarketCreation::Permissionless, + market_type, + None, + scoring_rule, + )); + MarketCommons::latest_market_id().unwrap() +} + +fn create_market_and_deploy_pool( + creator: AccountIdTest, + base_asset: Asset, + market_type: MarketType, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, +) -> MarketId { + let market_id = create_market(creator, base_asset, market_type, ScoringRule::Lmsr); + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + amount, + )); + println!("{:?}", AssetManager::free_balance(base_asset, &ALICE)); + assert_ok!(NeoSwaps::deploy_pool( + RuntimeOrigin::signed(ALICE), + market_id, + amount, + spot_prices.clone(), + swap_fee, + )); + market_id +} + +#[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/zrml/neo-swaps/src/tests/sell.rs b/zrml/neo-swaps/src/tests/sell.rs new file mode 100644 index 000000000..58cdd4ba5 --- /dev/null +++ b/zrml/neo-swaps/src/tests/sell.rs @@ -0,0 +1,336 @@ +// 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 super::*; +use test_case::test_case; + +#[test] +fn sell_works() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let liquidity = _10; + let spot_prices = vec![_1_4, _3_4]; + let swap_fee = CENT; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + liquidity, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(market_id).unwrap(); + let amount_in = _10; + let pool_outcomes_before: Vec<_> = + pool.assets().iter().map(|a| pool.reserve_of(a).unwrap()).collect(); + let pool_liquidity_before = pool.liquidity_parameter; + AssetManager::deposit(BASE_ASSET, &BOB, amount_in).unwrap(); + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(BOB), + market_id, + amount_in, + )); + let asset_in = pool.assets()[1]; + assert_ok!(NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 2, + asset_in, + amount_in, + 0, + )); + let total_fee_percentage = swap_fee + EXTERNAL_FEES; + let expected_amount_out = 59632253897u128; + let expected_fees = bmul(total_fee_percentage, expected_amount_out).unwrap(); + let expected_swap_fee_amount = expected_fees / 2; + let expected_external_fee_amount = expected_fees - expected_swap_fee_amount; + let expected_amount_out_minus_fees = expected_amount_out - expected_fees; + assert_eq!(AssetManager::free_balance(BASE_ASSET, &BOB), expected_amount_out_minus_fees); + assert_eq!(AssetManager::free_balance(asset_in, &BOB), 0); + let pool = Pools::::get(market_id).unwrap(); + assert_eq!(pool.liquidity_parameter, pool_liquidity_before); + assert_eq!(pool.liquidity_shares_manager.owner, ALICE); + assert_eq!(pool.liquidity_shares_manager.total_shares, liquidity); + assert_eq!(pool.liquidity_shares_manager.fees, expected_swap_fee_amount); + let pool_outcomes_after: Vec<_> = + pool.assets().iter().map(|a| pool.reserve_of(a).unwrap()).collect(); + assert_eq!(pool_outcomes_after[0], pool_outcomes_before[0] - expected_amount_out); + assert_eq!( + pool_outcomes_after[1], + pool_outcomes_before[1] + (amount_in - expected_amount_out) + ); + let expected_pool_account_balance = + expected_swap_fee_amount + AssetManager::minimum_balance(pool.collateral); + assert_eq!( + AssetManager::free_balance(BASE_ASSET, &pool.account_id), + expected_pool_account_balance + ); + assert_eq!( + AssetManager::free_balance(BASE_ASSET, &FEE_ACCOUNT), + expected_external_fee_amount + ); + assert_eq!( + AssetManager::total_issuance(pool.assets()[0]), + liquidity + amount_in - expected_amount_out + ); + assert_eq!( + AssetManager::total_issuance(pool.assets()[1]), + liquidity + amount_in - expected_amount_out + ); + let price_sum = + pool.assets().iter().map(|&a| pool.calculate_spot_price(a).unwrap()).sum::(); + assert_eq!(price_sum, _1); + System::assert_last_event( + Event::SellExecuted { + who: BOB, + market_id, + asset_in, + amount_in, + amount_out: expected_amount_out, + swap_fee_amount: expected_swap_fee_amount, + external_fee_amount: expected_external_fee_amount, + } + .into(), + ); + }); +} + +#[test] +fn sell_fails_on_incorrect_asset_count() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 1, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + Error::::IncorrectAssetCount + ); + }); +} + +#[test] +fn sell_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + Markets::::remove(market_id); + assert_noop!( + NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Suspended)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::CollectingSubsidy)] +#[test_case(MarketStatus::InsufficientSubsidy)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn sell_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + MarketCommons::mutate_market(&market_id, |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn sell_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::ScalarOutcome(market_id, ScalarPosition::Long), + _1, + 0 + ), + Error::::PoolNotFound, + ); + }); +} + +#[test_case(MarketType::Categorical(2))] +#[test_case(MarketType::Scalar(0..=1))] +fn sell_fails_on_asset_not_found(market_type: MarketType) { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + market_type, + _10, + vec![_1_2, _1_2], + CENT, + ); + assert_noop!( + NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 2, + Asset::CategoricalOutcome(market_id, 2), + _1, + u128::MAX, + ), + Error::::AssetNotFound, + ); + }); +} + +#[test] +fn sell_fails_on_numerical_limits() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + let pool = Pools::::get(market_id).unwrap(); + let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); + let amount_in = 100 * pool.liquidity_parameter; + assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in)); + assert_noop!( + NeoSwaps::buy(RuntimeOrigin::signed(BOB), market_id, 2, asset_in, amount_in, 0), + Error::::NumericalLimits, + ); + }); +} + +#[test] +fn sell_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _10, + vec![_1_2, _1_2], + CENT, + ); + let amount_in = _10; + let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); + assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in - 1)); + assert_noop!( + NeoSwaps::sell( + RuntimeOrigin::signed(BOB), + market_id, + 2, + asset_in, + amount_in, + u128::MAX, + ), + orml_tokens::Error::::BalanceTooLow, + ); + }); +} + +#[test] +fn sell_fails_on_amount_out_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + _100, + vec![_1_2, _1_2], + CENT, + ); + let amount_in = _20; + let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); + assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in)); + // Selling 20 at price of .5 will return less than 10 dollars due to slippage. + assert_noop!( + NeoSwaps::sell(RuntimeOrigin::signed(BOB), market_id, 2, asset_in, amount_in, _10), + Error::::AmountOutBelowMin, + ); + }); +} + +#[test] +fn sell_fails_on_spot_price_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Categorical(2), + _10, + vec![_1_2, _1_2], + CENT, + ); + let asset_in = Asset::CategoricalOutcome(market_id, 0); + let amount_in = _80; + assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in)); + assert_noop!( + NeoSwaps::sell(RuntimeOrigin::signed(BOB), market_id, 2, asset_in, amount_in, 0), + Error::::SpotPriceBelowMin + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/withdraw_fees.rs b/zrml/neo-swaps/src/tests/withdraw_fees.rs new file mode 100644 index 000000000..3fc71d6d2 --- /dev/null +++ b/zrml/neo-swaps/src/tests/withdraw_fees.rs @@ -0,0 +1,67 @@ +// 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 super::*; + +#[test] +fn withdraw_fees_works() { + ExtBuilder::default().build().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + let liquidity = _10; + let spot_prices = vec![_1_6, _5_6 + 1]; + let swap_fee = CENT; + let market_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Scalar(0..=1), + liquidity, + spot_prices.clone(), + swap_fee, + ); + // Mock up some fees for Alice to withdraw. + let mut pool = Pools::::get(market_id).unwrap(); + let fees = 123456789; + assert_ok!(AssetManager::deposit(pool.collateral, &pool.account_id, fees)); + pool.liquidity_shares_manager.fees = fees; + Pools::::insert(market_id, pool.clone()); + let alice_before = AssetManager::free_balance(pool.collateral, &ALICE); + assert_ok!(NeoSwaps::withdraw_fees(RuntimeOrigin::signed(ALICE), market_id)); + let expected_pool_account_balance = AssetManager::minimum_balance(pool.collateral); + assert_eq!( + AssetManager::free_balance(pool.collateral, &pool.account_id), + expected_pool_account_balance + ); + assert_eq!(AssetManager::free_balance(pool.collateral, &ALICE), alice_before + fees); + let pool_after = Pools::::get(market_id).unwrap(); + assert_eq!(pool_after.liquidity_shares_manager.fees, 0); + System::assert_last_event( + Event::FeesWithdrawn { who: ALICE, market_id, amount: fees }.into(), + ); + }); +} + +#[test] +fn withdraw_fees_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::Lmsr); + assert_noop!( + NeoSwaps::withdraw_fees(RuntimeOrigin::signed(ALICE), market_id), + Error::::PoolNotFound + ); + }); +} diff --git a/zrml/neo-swaps/src/traits/distribute_fees.rs b/zrml/neo-swaps/src/traits/distribute_fees.rs new file mode 100644 index 000000000..a6b67ad50 --- /dev/null +++ b/zrml/neo-swaps/src/traits/distribute_fees.rs @@ -0,0 +1,43 @@ +// 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 . + +/// Trait for distributing fees collected from trading to external recipients like the treasury. +pub trait DistributeFees { + type Asset; + type AccountId; + type Balance; + type MarketId; + + /// Deduct and distribute the swap fees of the pool from the specified amount and returns the + /// deducted fees. + /// + /// # Arguments + /// + /// - `market_id`: The market on which the fees are taken. + /// - `asset`: The asset the fee is paid in. + /// - `account`: The account which pays the fees. + /// - `amount`: The gross amount from which fees are deducted. + /// + /// Note that this function is infallible. If distribution is impossible or fails midway, it + /// should return the balance of the already successfully deducted fees. + fn distribute( + market_id: Self::MarketId, + asset: Self::Asset, + account: Self::AccountId, + amount: Self::Balance, + ) -> Self::Balance; +} diff --git a/zrml/neo-swaps/src/traits/liquidity_shares_manager.rs b/zrml/neo-swaps/src/traits/liquidity_shares_manager.rs new file mode 100644 index 000000000..97ad3f333 --- /dev/null +++ b/zrml/neo-swaps/src/traits/liquidity_shares_manager.rs @@ -0,0 +1,49 @@ +// 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 crate::{BalanceOf, Config}; +use sp_runtime::{DispatchError, DispatchResult}; + +/// Trait for managing pool share tokens and distributing fees to LPs according to their share of +/// the total issuance of pool share tokens. +pub trait LiquiditySharesManager { + /// Add `amount` units of pool shares to the account of `who`. + fn join(&mut self, who: &T::AccountId, amount: BalanceOf) -> DispatchResult; + + /// Remove `amount` units of pool shares from the account of `who`. + fn exit(&mut self, who: &T::AccountId, amount: BalanceOf) -> DispatchResult; + + /// Transfer `amount` units of pool shares from `sender` to `receiver`. + fn split( + &mut self, + sender: &T::AccountId, + receiver: &T::AccountId, + amount: BalanceOf, + ) -> DispatchResult; + + /// Deposit a lump sum of fees `amount` to the pool share holders. + fn deposit_fees(&mut self, amount: BalanceOf) -> DispatchResult; + + /// Withdraw and return the share of the fees belonging to `who`. + fn withdraw_fees(&mut self, who: &T::AccountId) -> Result, DispatchError>; + + /// Return the pool shares balance of `who`. + fn shares_of(&self, who: &T::AccountId) -> Result, DispatchError>; + + /// Return the total issuance of pool shares. + fn total_shares(&self) -> Result, DispatchError>; +} diff --git a/zrml/neo-swaps/src/traits/mod.rs b/zrml/neo-swaps/src/traits/mod.rs new file mode 100644 index 000000000..061239e9f --- /dev/null +++ b/zrml/neo-swaps/src/traits/mod.rs @@ -0,0 +1,24 @@ +// 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 distribute_fees; +pub(crate) mod liquidity_shares_manager; +pub(crate) mod pool_operations; + +pub use distribute_fees::DistributeFees; +pub(crate) use liquidity_shares_manager::LiquiditySharesManager; +pub(crate) use pool_operations::PoolOperations; diff --git a/zrml/neo-swaps/src/traits/pool_operations.rs b/zrml/neo-swaps/src/traits/pool_operations.rs new file mode 100644 index 000000000..e187361c8 --- /dev/null +++ b/zrml/neo-swaps/src/traits/pool_operations.rs @@ -0,0 +1,83 @@ +// 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 crate::pallet::{AssetOf, BalanceOf, Config}; +use alloc::vec::Vec; +use sp_runtime::{DispatchError, DispatchResult}; + +/// Trait for LMSR calculations and access to pool data. +pub(crate) trait PoolOperations { + /// Return an ordered vector containing the assets held in the pool. + fn assets(&self) -> Vec>; + + /// Return `true` if the pool holds `asset`. + fn contains(&self, asset: &AssetOf) -> bool; + + /// Return the reserve of `asset` held in the pool. + /// + /// Beware! The reserve need not coincide with the balance in the pool account. + fn reserve_of(&self, asset: &AssetOf) -> Result, DispatchError>; + + /// Perform a checked addition to the balance of `asset`. + fn increase_reserve( + &mut self, + asset: &AssetOf, + increase_amount: &BalanceOf, + ) -> DispatchResult; + + /// Perform a checked subtraction from the balance of `asset`. + fn decrease_reserve( + &mut self, + asset: &AssetOf, + decrease_amount: &BalanceOf, + ) -> DispatchResult; + + /// Calculate the amount received from the swap that is executed when buying (the function + /// `y(x)` from the documentation). + /// + /// Note that `y(x)` does not include the amount of `asset_out` received from buying complete + /// sets and is therefore _not_ the total amount received from the buy. + /// + /// # Parameters + /// + /// - `asset_out`: The outcome being bought. + /// - `amount_in`: The amount of collateral paid. + fn calculate_swap_amount_out_for_buy( + &self, + asset_out: AssetOf, + amount_in: BalanceOf, + ) -> Result, DispatchError>; + + /// Calculate the amount receives from selling an outcome to the pool. + /// + /// # Parameters + /// + /// - `asset_in`: The outcome being sold. + /// - `amount_in`: The amount of `asset_in` sold. + fn calculate_swap_amount_out_for_sell( + &self, + asset_in: AssetOf, + amount_in: BalanceOf, + ) -> Result, DispatchError>; + + /// Calculate the spot price of `asset`. + fn calculate_spot_price(&self, asset: AssetOf) -> Result, DispatchError>; + + /// Calculate the maximum number of units of outcomes anyone is allowed to swap in or out of the + /// pool. + fn calculate_max_amount_in(&self) -> BalanceOf; +} diff --git a/zrml/neo-swaps/src/types/fee_distribution.rs b/zrml/neo-swaps/src/types/fee_distribution.rs new file mode 100644 index 000000000..25ff4a09e --- /dev/null +++ b/zrml/neo-swaps/src/types/fee_distribution.rs @@ -0,0 +1,24 @@ +// 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 crate::{BalanceOf, Config}; + +pub(crate) struct FeeDistribution { + pub(crate) remaining: BalanceOf, + pub(crate) swap_fees: BalanceOf, + pub(crate) external_fees: BalanceOf, +} diff --git a/zrml/neo-swaps/src/types/market_creator_fee.rs b/zrml/neo-swaps/src/types/market_creator_fee.rs new file mode 100644 index 000000000..087627267 --- /dev/null +++ b/zrml/neo-swaps/src/types/market_creator_fee.rs @@ -0,0 +1,59 @@ +// 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 crate::{traits::DistributeFees, AssetOf, BalanceOf, Config, MarketIdOf}; +use core::marker::PhantomData; +use orml_traits::MultiCurrency; +use sp_runtime::{DispatchError, SaturatedConversion}; +use zrml_market_commons::MarketCommonsPalletApi; + +pub struct MarketCreatorFee(PhantomData); + +/// Uses the `creator_fee` field defined by the specified market to deduct a fee for the market's +/// creator. Calling `distribute` is noop if the market doesn't exist or the transfer fails for any +/// reason. +impl DistributeFees for MarketCreatorFee { + type Asset = AssetOf; + type AccountId = T::AccountId; + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + fn distribute( + market_id: Self::MarketId, + asset: Self::Asset, + account: Self::AccountId, + amount: Self::Balance, + ) -> Self::Balance { + Self::impl_distribute(market_id, asset, account, amount) + .unwrap_or_else(|_| 0u8.saturated_into()) + } +} + +impl MarketCreatorFee { + fn impl_distribute( + market_id: MarketIdOf, + asset: AssetOf, + account: T::AccountId, + amount: BalanceOf, + ) -> Result, DispatchError> { + let market = T::MarketCommons::market(&market_id)?; // Should never fail + let fee_amount = market.creator_fee.mul_floor(amount); + // Might fail if the transaction is too small + T::MultiCurrency::transfer(asset, &account, &market.creator, fee_amount)?; + Ok(fee_amount) + } +} diff --git a/zrml/neo-swaps/src/types/mod.rs b/zrml/neo-swaps/src/types/mod.rs new file mode 100644 index 000000000..dc0c4aaf9 --- /dev/null +++ b/zrml/neo-swaps/src/types/mod.rs @@ -0,0 +1,26 @@ +// 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 . + +mod fee_distribution; +mod market_creator_fee; +mod pool; +mod solo_lp; + +pub(crate) use fee_distribution::*; +pub use market_creator_fee::*; +pub(crate) use pool::*; +pub(crate) use solo_lp::*; diff --git a/zrml/neo-swaps/src/types/pool.rs b/zrml/neo-swaps/src/types/pool.rs new file mode 100644 index 000000000..c9cec18d8 --- /dev/null +++ b/zrml/neo-swaps/src/types/pool.rs @@ -0,0 +1,135 @@ +// 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 crate::{ + consts::{EXP_NUMERICAL_LIMIT, MAX_ASSETS}, + math::{Math, MathOps}, + pallet::{AssetOf, BalanceOf, Config}, + traits::{LiquiditySharesManager, PoolOperations}, + Error, +}; +use alloc::{collections::BTreeMap, vec::Vec}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{CheckedAdd, CheckedSub}, + DispatchError, DispatchResult, RuntimeDebug, SaturatedConversion, Saturating, +}; + +#[derive(TypeInfo, Clone, Encode, Eq, Decode, PartialEq, RuntimeDebug)] +#[scale_info(skip_type_params(T))] +pub struct Pool +where + LSM: LiquiditySharesManager, +{ + pub account_id: T::AccountId, + pub reserves: BTreeMap, BalanceOf>, + pub collateral: AssetOf, + pub liquidity_parameter: BalanceOf, + pub liquidity_shares_manager: LSM, + pub swap_fee: BalanceOf, +} + +impl + TypeInfo> PoolOperations for Pool +where + BalanceOf: SaturatedConversion, +{ + fn assets(&self) -> Vec> { + self.reserves.keys().cloned().collect() + } + + fn contains(&self, asset: &AssetOf) -> bool { + self.reserves.contains_key(asset) + } + + fn reserve_of(&self, asset: &AssetOf) -> Result, DispatchError> { + Ok(*self.reserves.get(asset).ok_or(Error::::AssetNotFound)?) + } + + fn increase_reserve( + &mut self, + asset: &AssetOf, + increase_amount: &BalanceOf, + ) -> DispatchResult { + let value = self.reserves.get_mut(asset).ok_or(Error::::AssetNotFound)?; + *value = value.checked_add(increase_amount).ok_or(Error::::MathError)?; + Ok(()) + } + + fn decrease_reserve( + &mut self, + asset: &AssetOf, + decrease_amount: &BalanceOf, + ) -> DispatchResult { + let value = self.reserves.get_mut(asset).ok_or(Error::::AssetNotFound)?; + *value = value.checked_sub(decrease_amount).ok_or(Error::::MathError)?; + Ok(()) + } + + fn calculate_swap_amount_out_for_buy( + &self, + asset_out: AssetOf, + amount_in: BalanceOf, + ) -> Result, DispatchError> { + let reserve = self.reserve_of(&asset_out)?; + Math::::calculate_swap_amount_out_for_buy(reserve, amount_in, self.liquidity_parameter) + } + + fn calculate_swap_amount_out_for_sell( + &self, + asset_in: AssetOf, + amount_in: BalanceOf, + ) -> Result, DispatchError> { + let reserve = self.reserve_of(&asset_in)?; + Math::::calculate_swap_amount_out_for_sell(reserve, amount_in, self.liquidity_parameter) + } + + fn calculate_spot_price(&self, asset: AssetOf) -> Result, DispatchError> { + let reserve = self.reserve_of(&asset)?; + Math::::calculate_spot_price(reserve, self.liquidity_parameter) + } + + fn calculate_max_amount_in(&self) -> BalanceOf { + // Saturation is OK. If this saturates, the maximum amount in is just the numerical limit. + self.liquidity_parameter.saturating_mul(EXP_NUMERICAL_LIMIT.saturated_into()) + } +} + +impl> MaxEncodedLen for Pool +where + T::AccountId: MaxEncodedLen, + AssetOf: MaxEncodedLen, + BalanceOf: MaxEncodedLen, + LSM: MaxEncodedLen, +{ + fn max_encoded_len() -> usize { + let len_account_id = T::AccountId::max_encoded_len(); + let len_reserves = 1usize.saturating_add((MAX_ASSETS as usize).saturating_mul( + >::max_encoded_len().saturating_add(BalanceOf::::max_encoded_len()), + )); + let len_collateral = AssetOf::::max_encoded_len(); + let len_liquidity_parameter = BalanceOf::::max_encoded_len(); + let len_liquidity_shares_manager = LSM::max_encoded_len(); + let len_swap_fee = BalanceOf::::max_encoded_len(); + len_account_id + .saturating_add(len_reserves) + .saturating_add(len_collateral) + .saturating_add(len_liquidity_parameter) + .saturating_add(len_liquidity_shares_manager) + .saturating_add(len_swap_fee) + } +} diff --git a/zrml/neo-swaps/src/types/solo_lp.rs b/zrml/neo-swaps/src/types/solo_lp.rs new file mode 100644 index 000000000..02cbed618 --- /dev/null +++ b/zrml/neo-swaps/src/types/solo_lp.rs @@ -0,0 +1,88 @@ +// 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 crate::{traits::LiquiditySharesManager, BalanceOf, Config, Error}; +use frame_support::ensure; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Zero}, + DispatchError, DispatchResult, RuntimeDebug, +}; + +#[derive(TypeInfo, MaxEncodedLen, Clone, Encode, Eq, Decode, PartialEq, RuntimeDebug)] +#[scale_info(skip_type_params(T))] +pub struct SoloLp { + pub owner: T::AccountId, + pub total_shares: BalanceOf, + pub fees: BalanceOf, +} + +impl SoloLp { + pub(crate) fn new(owner: T::AccountId, total_shares: BalanceOf) -> SoloLp { + SoloLp { owner, total_shares, fees: Zero::zero() } + } +} + +impl LiquiditySharesManager for SoloLp +where + T::AccountId: PartialEq, + BalanceOf: AtLeast32BitUnsigned + Copy + Zero, +{ + fn join(&mut self, who: &T::AccountId, shares: BalanceOf) -> DispatchResult { + ensure!(*who == self.owner, Error::::NotAllowed); + self.total_shares = self.total_shares.checked_add(&shares).ok_or(Error::::MathError)?; + Ok(()) + } + + fn exit(&mut self, who: &T::AccountId, shares: BalanceOf) -> DispatchResult { + ensure!(*who == self.owner, Error::::NotAllowed); + ensure!(shares <= self.total_shares, Error::::InsufficientPoolShares); + self.total_shares = self.total_shares.checked_sub(&shares).ok_or(Error::::MathError)?; + Ok(()) + } + + fn split( + &mut self, + _sender: &T::AccountId, + _receiver: &T::AccountId, + _amount: BalanceOf, + ) -> DispatchResult { + Err(Error::::NotImplemented.into()) + } + + fn deposit_fees(&mut self, amount: BalanceOf) -> DispatchResult { + self.fees = self.fees.checked_add(&amount).ok_or(Error::::MathError)?; + Ok(()) + } + + fn withdraw_fees(&mut self, who: &T::AccountId) -> Result, DispatchError> { + ensure!(*who == self.owner, Error::::NotAllowed); + let result = self.fees; + self.fees = Zero::zero(); + Ok(result) + } + + fn shares_of(&self, who: &T::AccountId) -> Result, DispatchError> { + ensure!(*who == self.owner, Error::::NotAllowed); + Ok(self.total_shares) + } + + fn total_shares(&self) -> Result, DispatchError> { + Ok(self.total_shares) + } +} diff --git a/zrml/neo-swaps/src/weights.rs b/zrml/neo-swaps/src/weights.rs new file mode 100644 index 000000000..618636182 --- /dev/null +++ b/zrml/neo-swaps/src/weights.rs @@ -0,0 +1,163 @@ +// Copyright 2022-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 . + +//! Autogenerated weights for zrml_neo_swaps +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: `2023-09-15`, STEPS: `2`, REPEAT: `0`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `mkl-mac`, CPU: `` +//! EXECUTION: `Some(Native)`, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/zeitgeist +// benchmark +// pallet +// --chain=dev +// --steps=2 +// --repeat=0 +// --pallet=zrml_neo_swaps +// --extrinsic=* +// --execution=native +// --wasm-execution=compiled +// --heap-pages=4096 +// --template=./misc/weight_template.hbs +// --header=./HEADER_GPL3 +// --output=./zrml/neo-swaps/src/weights.rs + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use core::marker::PhantomData; +use frame_support::{traits::Get, weights::Weight}; + +/// Trait containing the required functions for weight retrival within +/// zrml_neo_swaps (automatically generated) +pub trait WeightInfoZeitgeist { + fn buy() -> Weight; + fn sell() -> Weight; + fn join() -> Weight; + fn exit() -> Weight; + fn withdraw_fees() -> Weight; + fn deploy_pool() -> Weight; +} + +/// Weight functions for zrml_neo_swaps (automatically generated) +pub struct WeightInfo(PhantomData); +impl WeightInfoZeitgeist for WeightInfo { + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:3 w:3) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:2 w:2) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(43), added: 2518, mode: MaxEncodedLen) + fn buy() -> Weight { + // Proof Size summary in bytes: + // Measured: `2868` + // Estimated: `28187` + // Minimum execution time: 234_000 nanoseconds. + Weight::from_parts(234_000_000, 28187) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:3 w:3) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:2 w:2) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(43), added: 2518, mode: MaxEncodedLen) + fn sell() -> Weight { + // Proof Size summary in bytes: + // Measured: `3034` + // Estimated: `28187` + // Minimum execution time: 296_000 nanoseconds. + Weight::from_parts(296_000_000, 28187) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(8_u64)) + } + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:4 w:4) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + fn join() -> Weight { + // Proof Size summary in bytes: + // Measured: `2756` + // Estimated: `20535` + // Minimum execution time: 118_000 nanoseconds. + Weight::from_parts(118_000_000, 20535) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:4 w:4) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) + fn exit() -> Weight { + // Proof Size summary in bytes: + // Measured: `2524` + // Estimated: `23142` + // Minimum execution time: 116_000 nanoseconds. + Weight::from_parts(116_000_000, 23142) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) + fn withdraw_fees() -> Weight { + // Proof Size summary in bytes: + // Measured: `1819` + // Estimated: `9734` + // Minimum execution time: 73_000 nanoseconds. + Weight::from_parts(73_000_000, 9734) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: MarketCommons Markets (r:1 w:0) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:4 w:4) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) + fn deploy_pool() -> Weight { + // Proof Size summary in bytes: + // Measured: `2241` + // Estimated: `23142` + // Minimum execution time: 149_000 nanoseconds. + Weight::from_parts(149_000_000, 23142) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } +} diff --git a/zrml/prediction-markets/src/benchmarks.rs b/zrml/prediction-markets/src/benchmarks.rs index 777bff022..b914ce9a3 100644 --- a/zrml/prediction-markets/src/benchmarks.rs +++ b/zrml/prediction-markets/src/benchmarks.rs @@ -154,7 +154,8 @@ fn setup_redeem_shares_common( panic!("setup_redeem_shares_common: Unsupported market type: {market_type:?}"); } - Pallet::::do_buy_complete_set(caller.clone(), market_id, LIQUIDITY.saturated_into())?; + Call::::buy_complete_set { market_id, amount: LIQUIDITY.saturated_into() } + .dispatch_bypass_filter(RawOrigin::Signed(caller.clone()).into())?; let close_origin = T::CloseOrigin::try_successful_origin().unwrap(); let resolve_origin = T::ResolveOrigin::try_successful_origin().unwrap(); Call::::admin_move_market_to_closed { market_id }.dispatch_bypass_filter(close_origin)?; @@ -1286,6 +1287,44 @@ benchmarks! { let _ = >::process_subsidy_collecting_markets(current_block, current_time); } + create_market_and_deploy_pool { + let m in 0..63; // Number of markets closing on the same block. + + let base_asset = Asset::Ztg; + let range_start = (5 * MILLISECS_PER_BLOCK) as u64; + let range_end = (100 * MILLISECS_PER_BLOCK) as u64; + let period = MarketPeriod::Timestamp(range_start..range_end); + let market_type = MarketType::Categorical(2); + let (caller, oracle, deadlines, metadata) = create_market_common_parameters::()?; + let price = (BASE / 2).saturated_into(); + let amount = (10u128 * BASE).saturated_into(); + + ::AssetManager::deposit( + base_asset, + &caller, + amount, + )?; + for i in 0..m { + MarketIdsPerCloseTimeFrame::::try_mutate( + Pallet::::calculate_time_frame_of_moment(range_end), + |ids| ids.try_push(i.into()), + ).unwrap(); + } + }: _( + RawOrigin::Signed(caller), + base_asset, + Perbill::zero(), + oracle, + period, + deadlines, + metadata, + MarketType::Categorical(2), + Some(MarketDisputeMechanism::Court), + amount, + vec![price, price], + (BASE / 100).saturated_into() + ) + impl_benchmark_test_suite!( PredictionMarket, crate::mock::ExtBuilder::default().build(), diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index 8e0fbf90c..d798345d9 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -40,6 +40,7 @@ mod pallet { dispatch::{DispatchResultWithPostInfo, Pays, Weight}, ensure, log, pallet_prelude::{ConstU32, StorageMap, StorageValue, ValueQuery}, + require_transactional, storage::{with_transaction, TransactionOutcome}, traits::{ tokens::BalanceStatus, Currency, EnsureOrigin, Get, Hooks, Imbalance, IsType, @@ -61,7 +62,8 @@ mod pallet { use zeitgeist_primitives::{ constants::MILLISECS_PER_BLOCK, traits::{ - DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi, Swaps, ZeitgeistAssetManager, + CompleteSetOperationsApi, DeployPoolApi, DisputeApi, DisputeMaxWeightApi, + DisputeResolutionApi, Swaps, ZeitgeistAssetManager, }, types::{ Asset, Bond, Deadlines, GlobalDisputeItem, Market, MarketBonds, MarketCreation, @@ -535,7 +537,7 @@ mod pallet { ); match m.scoring_rule { - ScoringRule::CPMM | ScoringRule::Orderbook => { + ScoringRule::CPMM | ScoringRule::Lmsr | ScoringRule::Orderbook => { m.status = MarketStatus::Active; } ScoringRule::RikiddoSigmoidFeeMarketEma => { @@ -623,7 +625,11 @@ mod pallet { #[pallet::compact] amount: BalanceOf, ) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; - Self::do_buy_complete_set(sender, market_id, amount) + Self::do_buy_complete_set(sender, market_id, amount)?; + let market = >::market(&market_id)?; + let assets = Self::outcome_assets(market_id, &market); + let assets_len: u32 = assets.len().saturated_into(); + Ok(Some(T::WeightInfo::buy_complete_set(assets_len)).into()) } /// Dispute on a market that has been reported or already disputed. @@ -793,57 +799,20 @@ mod pallet { ) -> DispatchResultWithPostInfo { // TODO(#787): Handle Rikiddo benchmarks! let sender = ensure_signed(origin)?; - - let bonds = match creation { - MarketCreation::Advised => MarketBonds { - creation: Some(Bond::new(sender.clone(), T::AdvisoryBond::get())), - oracle: Some(Bond::new(sender.clone(), T::OracleBond::get())), - ..Default::default() - }, - MarketCreation::Permissionless => MarketBonds { - creation: Some(Bond::new(sender.clone(), T::ValidityBond::get())), - oracle: Some(Bond::new(sender.clone(), T::OracleBond::get())), - ..Default::default() - }, - }; - - let market = Self::construct_market( + let (ids_len, _) = Self::do_create_market( + sender, base_asset, - sender.clone(), creator_fee, oracle, period, deadlines, metadata, - creation.clone(), + creation, market_type, dispute_mechanism, scoring_rule, - None, - None, - bonds.clone(), )?; - - T::AssetManager::reserve_named( - &Self::reserve_id(), - Asset::Ztg, - &sender, - bonds.total_amount_bonded(&sender), - )?; - - let market_id = >::push_market(market.clone())?; - let market_account = >::market_account(market_id); - let mut extra_weight = Weight::zero(); - - if market.status == MarketStatus::CollectingSubsidy { - extra_weight = Self::start_subsidy(&market, market_id)?; - } - - let ids_amount: u32 = Self::insert_auto_close(&market_id)?; - - Self::deposit_event(Event::MarketCreated(market_id, market_account, market)); - - Ok(Some(T::WeightInfo::create_market(ids_amount).saturating_add(extra_weight)).into()) + Ok(Some(T::WeightInfo::create_market(ids_len)).into()) } /// Edit a proposed market for which request is made. @@ -1331,9 +1300,7 @@ mod pallet { /// /// Complexity: `O(n)`, where `n` is the number of assets for a categorical market. #[pallet::call_index(15)] - #[pallet::weight( - T::WeightInfo::sell_complete_set(T::MaxCategories::get().into()) - )] + #[pallet::weight(T::WeightInfo::sell_complete_set(T::MaxCategories::get().into()))] #[transactional] pub fn sell_complete_set( origin: OriginFor, @@ -1341,41 +1308,9 @@ mod pallet { #[pallet::compact] amount: BalanceOf, ) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; - ensure!(amount != BalanceOf::::zero(), Error::::ZeroAmount); - + Self::do_sell_complete_set(sender, market_id, amount)?; let market = >::market(&market_id)?; - ensure!( - matches!(market.scoring_rule, ScoringRule::CPMM | ScoringRule::Orderbook), - Error::::InvalidScoringRule - ); - Self::ensure_market_is_active(&market)?; - - let market_account = >::market_account(market_id); - ensure!( - T::AssetManager::free_balance(market.base_asset, &market_account) >= amount, - "Market account does not have sufficient reserves.", - ); - let assets = Self::outcome_assets(market_id, &market); - - // verify first. - for asset in assets.iter() { - // Ensures that the sender has sufficient amount of each - // share in the set. - ensure!( - T::AssetManager::free_balance(*asset, &sender) >= amount, - Error::::InsufficientShareBalance, - ); - } - - // write last. - for asset in assets.iter() { - T::AssetManager::slash(*asset, &sender, amount); - } - - T::AssetManager::transfer(market.base_asset, &market_account, &sender, amount)?; - - Self::deposit_event(Event::SoldCompleteSet(market_id, amount, sender)); let assets_len: u32 = assets.len().saturated_into(); Ok(Some(T::WeightInfo::sell_complete_set(assets_len)).into()) } @@ -1483,6 +1418,50 @@ mod pallet { Ok(Some(T::WeightInfo::start_global_dispute(ids_len_1, ids_len_2)).into()) } + + /// Create a market, deploy a LMSR pool, and buy outcome tokens and provide liquidity to the + /// market. + /// + /// # Weight + /// + /// `O(n)` where `n` is the number of markets which close on the same block, plus the + /// resources consumed by `DeployPool::create_pool`. In the standard implementation using + /// neo-swaps, this is `O(m)` where `m` is the number of assets in the market. + #[pallet::weight(T::WeightInfo::create_market_and_deploy_pool(CacheSize::get()))] + #[transactional] + #[pallet::call_index(17)] + pub fn create_market_and_deploy_pool( + origin: OriginFor, + base_asset: Asset>, + creator_fee: Perbill, + oracle: T::AccountId, + period: MarketPeriod>, + deadlines: Deadlines, + metadata: MultiHash, + market_type: MarketType, + dispute_mechanism: Option, + #[pallet::compact] amount: BalanceOf, + spot_prices: Vec>, + #[pallet::compact] swap_fee: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let (ids_len, market_id) = Self::do_create_market( + who.clone(), + base_asset, + creator_fee, + oracle, + period, + deadlines, + metadata, + MarketCreation::Permissionless, + market_type, + dispute_mechanism, + ScoringRule::Lmsr, + )?; + Self::do_buy_complete_set(who.clone(), market_id, amount)?; + T::DeployPool::deploy_pool(who, market_id, amount, spot_prices, swap_fee)?; + Ok(Some(T::WeightInfo::create_market_and_deploy_pool(ids_len)).into()) + } } #[pallet::config] @@ -1539,6 +1518,13 @@ mod pallet { Origin = Self::RuntimeOrigin, >; + /// Used to deploy neo-swaps pools. + type DeployPool: DeployPoolApi< + AccountId = Self::AccountId, + Balance = BalanceOf, + MarketId = MarketIdOf, + >; + /// The origin that is allowed to destroy markets. type DestroyOrigin: EnsureOrigin; @@ -2069,6 +2055,71 @@ mod pallet { Ok(()) } + #[require_transactional] + fn do_create_market( + who: T::AccountId, + base_asset: Asset>, + creator_fee: Perbill, + oracle: T::AccountId, + period: MarketPeriod>, + deadlines: Deadlines, + metadata: MultiHash, + creation: MarketCreation, + market_type: MarketType, + dispute_mechanism: Option, + scoring_rule: ScoringRule, + ) -> Result<(u32, MarketIdOf), DispatchError> { + let bonds = match creation { + MarketCreation::Advised => MarketBonds { + creation: Some(Bond::new(who.clone(), T::AdvisoryBond::get())), + oracle: Some(Bond::new(who.clone(), T::OracleBond::get())), + ..Default::default() + }, + MarketCreation::Permissionless => MarketBonds { + creation: Some(Bond::new(who.clone(), T::ValidityBond::get())), + oracle: Some(Bond::new(who.clone(), T::OracleBond::get())), + ..Default::default() + }, + }; + + let market = Self::construct_market( + base_asset, + who.clone(), + creator_fee, + oracle, + period, + deadlines, + metadata, + creation.clone(), + market_type, + dispute_mechanism, + scoring_rule, + None, + None, + bonds.clone(), + )?; + + T::AssetManager::reserve_named( + &Self::reserve_id(), + Asset::Ztg, + &who, + bonds.total_amount_bonded(&who), + )?; + + let market_id = >::push_market(market.clone())?; + let market_account = >::market_account(market_id); + + if market.status == MarketStatus::CollectingSubsidy { + let _ = Self::start_subsidy(&market, market_id)?; + } + + let ids_amount: u32 = Self::insert_auto_close(&market_id)?; + + Self::deposit_event(Event::MarketCreated(market_id, market_account, market)); + + Ok((ids_amount, market_id)) + } + pub fn outcome_assets( market_id: MarketIdOf, market: &MarketOf, @@ -2238,11 +2289,60 @@ mod pallet { Ok(()) } + #[require_transactional] + pub(crate) fn do_sell_complete_set( + who: T::AccountId, + market_id: MarketIdOf, + amount: BalanceOf, + ) -> DispatchResult { + ensure!(amount != BalanceOf::::zero(), Error::::ZeroAmount); + + let market = >::market(&market_id)?; + ensure!( + matches!( + market.scoring_rule, + ScoringRule::CPMM | ScoringRule::Lmsr | ScoringRule::Orderbook + ), + Error::::InvalidScoringRule + ); + Self::ensure_market_is_active(&market)?; + + let market_account = >::market_account(market_id); + ensure!( + T::AssetManager::free_balance(market.base_asset, &market_account) >= amount, + "Market account does not have sufficient reserves.", + ); + + let assets = Self::outcome_assets(market_id, &market); + + // verify first. + for asset in assets.iter() { + // Ensures that the sender has sufficient amount of each + // share in the set. + ensure!( + T::AssetManager::free_balance(*asset, &who) >= amount, + Error::::InsufficientShareBalance, + ); + } + + // write last. + for asset in assets.iter() { + T::AssetManager::slash(*asset, &who, amount); + } + + T::AssetManager::transfer(market.base_asset, &market_account, &who, amount)?; + + Self::deposit_event(Event::SoldCompleteSet(market_id, amount, who)); + + Ok(()) + } + + #[require_transactional] pub(crate) fn do_buy_complete_set( who: T::AccountId, market_id: MarketIdOf, amount: BalanceOf, - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { ensure!(amount != BalanceOf::::zero(), Error::::ZeroAmount); let market = >::market(&market_id)?; ensure!( @@ -2250,7 +2350,10 @@ mod pallet { Error::::NotEnoughBalance ); ensure!( - matches!(market.scoring_rule, ScoringRule::CPMM | ScoringRule::Orderbook), + matches!( + market.scoring_rule, + ScoringRule::CPMM | ScoringRule::Lmsr | ScoringRule::Orderbook + ), Error::::InvalidScoringRule ); Self::ensure_market_is_active(&market)?; @@ -2265,8 +2368,7 @@ mod pallet { Self::deposit_event(Event::BoughtCompleteSet(market_id, amount, who)); - let assets_len: u32 = assets.len().saturated_into(); - Ok(Some(T::WeightInfo::buy_complete_set(assets_len)).into()) + Ok(()) } pub(crate) fn do_reject_market( @@ -3073,7 +3175,9 @@ mod pallet { } let status: MarketStatus = match creation { MarketCreation::Permissionless => match scoring_rule { - ScoringRule::CPMM | ScoringRule::Orderbook => MarketStatus::Active, + ScoringRule::CPMM | ScoringRule::Lmsr | ScoringRule::Orderbook => { + MarketStatus::Active + } ScoringRule::RikiddoSigmoidFeeMarketEma => MarketStatus::CollectingSubsidy, }, MarketCreation::Advised => MarketStatus::Proposed, @@ -3256,4 +3360,29 @@ mod pallet { remove_auto_resolve::(market_id, resolve_at) } } + + impl CompleteSetOperationsApi for Pallet + where + T: Config, + { + type AccountId = T::AccountId; + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + fn buy_complete_set( + who: Self::AccountId, + market_id: Self::MarketId, + amount: Self::Balance, + ) -> DispatchResult { + Self::do_buy_complete_set(who, market_id, amount) + } + + fn sell_complete_set( + who: Self::AccountId, + market_id: Self::MarketId, + amount: Self::Balance, + ) -> DispatchResult { + Self::do_sell_complete_set(who, market_id, amount) + } + } } diff --git a/zrml/prediction-markets/src/mock.rs b/zrml/prediction-markets/src/mock.rs index 016ab3e76..0abb8b8dd 100644 --- a/zrml/prediction-markets/src/mock.rs +++ b/zrml/prediction-markets/src/mock.rs @@ -34,34 +34,32 @@ use sp_arithmetic::per_things::Percent; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, + DispatchError, DispatchResult, }; +use std::cell::RefCell; use substrate_fixed::{types::extra::U33, FixedI128, FixedU128}; use zeitgeist_primitives::{ constants::mock::{ - AggregationPeriod, AppealBond, AppealPeriod, AuthorizedPalletId, BalanceFractionalDecimals, - BlockHashCount, BlocksPerYear, CorrectionPeriod, CourtPalletId, ExistentialDeposit, - ExistentialDeposits, ExitFee, GetNativeCurrencyId, InflationPeriod, - LiquidityMiningPalletId, LockId, MaxAppeals, MaxApprovals, MaxAssets, MaxCategories, - MaxCourtParticipants, MaxCreatorFee, MaxDelegations, MaxDisputeDuration, MaxDisputes, - MaxEditReasonLen, MaxGracePeriod, MaxInRatio, MaxMarketLifetime, MaxOracleDuration, - MaxOutRatio, MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, MaxSubsidyPeriod, - MaxSwapFee, MaxTotalWeight, MaxWeight, MinAssets, MinCategories, MinDisputeDuration, - MinJurorStake, MinOracleDuration, MinSubsidy, MinSubsidyPeriod, MinWeight, MinimumPeriod, - OutcomeBond, OutcomeFactor, OutsiderBond, PmPalletId, RequestInterval, - SimpleDisputesPalletId, SwapsPalletId, TreasuryPalletId, VotePeriod, BASE, CENT, - MILLISECS_PER_BLOCK, + AddOutcomePeriod, AggregationPeriod, AppealBond, AppealPeriod, AuthorizedPalletId, + BalanceFractionalDecimals, BlockHashCount, BlocksPerYear, CorrectionPeriod, CourtPalletId, + ExistentialDeposit, ExistentialDeposits, ExitFee, GdVotingPeriod, GetNativeCurrencyId, + GlobalDisputeLockId, GlobalDisputesPalletId, InflationPeriod, LiquidityMiningPalletId, + LockId, MaxAppeals, MaxApprovals, MaxAssets, MaxCategories, MaxCourtParticipants, + MaxCreatorFee, MaxDelegations, MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, + MaxGlobalDisputeVotes, MaxGracePeriod, MaxInRatio, MaxMarketLifetime, MaxOracleDuration, + MaxOutRatio, MaxOwners, MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, + MaxSubsidyPeriod, MaxSwapFee, MaxTotalWeight, MaxWeight, MinAssets, MinCategories, + MinDisputeDuration, MinJurorStake, MinOracleDuration, MinOutcomeVoteAmount, MinSubsidy, + MinSubsidyPeriod, MinWeight, MinimumPeriod, OutcomeBond, OutcomeFactor, OutsiderBond, + PmPalletId, RemoveKeysLimit, RequestInterval, SimpleDisputesPalletId, SwapsPalletId, + TreasuryPalletId, VotePeriod, VotingOutcomeFee, BASE, CENT, MILLISECS_PER_BLOCK, }, + traits::DeployPoolApi, types::{ AccountIdTest, Amount, Asset, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, CurrencyId, Hash, Index, MarketId, Moment, PoolId, SerdeWrapper, UncheckedExtrinsicTest, }, }; - -use zeitgeist_primitives::constants::mock::{ - AddOutcomePeriod, GdVotingPeriod, GlobalDisputeLockId, GlobalDisputesPalletId, - MaxGlobalDisputeVotes, MaxOwners, MinOutcomeVoteAmount, RemoveKeysLimit, VotingOutcomeFee, -}; - use zrml_rikiddo::types::{EmaMarketVolume, FeeSigmoid, RikiddoSigmoidMV}; pub const ALICE: AccountIdTest = 0; @@ -74,6 +72,76 @@ pub const SUDO: AccountIdTest = 69; pub const INITIAL_BALANCE: u128 = 1_000 * BASE; +#[allow(unused)] +pub struct DeployPoolMock; + +#[allow(unused)] +#[derive(Clone)] +pub struct DeployPoolArgs { + who: AccountIdTest, + market_id: MarketId, + amount: Balance, + swap_prices: Vec, + swap_fee: Balance, +} + +thread_local! { + pub static DEPLOY_POOL_CALL_DATA: RefCell> = RefCell::new(vec![]); + pub static DEPLOY_POOL_RETURN_VALUE: RefCell = RefCell::new(Ok(())); +} + +#[allow(unused)] +impl DeployPoolApi for DeployPoolMock { + type AccountId = AccountIdTest; + type Balance = Balance; + type MarketId = MarketId; + + fn deploy_pool( + who: Self::AccountId, + market_id: Self::MarketId, + amount: Self::Balance, + swap_prices: Vec, + swap_fee: Self::Balance, + ) -> DispatchResult { + DEPLOY_POOL_CALL_DATA.with(|value| { + value.borrow_mut().push(DeployPoolArgs { + who, + market_id, + amount, + swap_prices, + swap_fee, + }) + }); + DEPLOY_POOL_RETURN_VALUE.with(|v| *v.borrow()) + } +} + +#[allow(unused)] +impl DeployPoolMock { + pub fn called_once_with( + who: AccountIdTest, + market_id: MarketId, + amount: Balance, + swap_prices: Vec, + swap_fee: Balance, + ) -> bool { + if DEPLOY_POOL_CALL_DATA.with(|value| value.borrow().len()) != 1 { + return false; + } + let args = DEPLOY_POOL_CALL_DATA.with(|value| value.borrow()[0].clone()); + args.who == who + && args.market_id == market_id + && args.amount == amount + && args.swap_prices == swap_prices + && args.swap_fee == swap_fee + } + + pub fn return_error() { + DEPLOY_POOL_RETURN_VALUE + .with(|value| *value.borrow_mut() = Err(DispatchError::Other("neo-swaps"))); + } +} + ord_parameter_types! { pub const Sudo: AccountIdTest = SUDO; } @@ -123,6 +191,7 @@ impl crate::Config for Runtime { type MaxCreatorFee = MaxCreatorFee; type Court = Court; type DestroyOrigin = EnsureSignedBy; + type DeployPool = DeployPoolMock; type DisputeBond = DisputeBond; type RuntimeEvent = RuntimeEvent; type GlobalDisputes = GlobalDisputes; @@ -379,6 +448,7 @@ pub struct ExtBuilder { impl Default for ExtBuilder { fn default() -> Self { + DEPLOY_POOL_CALL_DATA.with(|value| value.borrow_mut().clear()); Self { balances: vec![ (ALICE, INITIAL_BALANCE), diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index ab152fb8d..9dc7b9085 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -5668,6 +5668,76 @@ fn create_market_sets_the_correct_market_parameters_and_reserves_the_correct_amo }); } +#[test] +fn create_market_and_deploy_pool_works() { + ExtBuilder::default().build().execute_with(|| { + let creator = ALICE; + let creator_fee = Perbill::from_parts(1); + let oracle = BOB; + let period = MarketPeriod::Block(1..2); + let deadlines = Deadlines { + grace_period: 1, + oracle_duration: ::MinOracleDuration::get() + 2, + dispute_duration: ::MinDisputeDuration::get() + 3, + }; + let metadata = gen_metadata(0x99); + let MultiHash::Sha3_384(multihash) = metadata; + let market_type = MarketType::Categorical(7); + let dispute_mechanism = Some(MarketDisputeMechanism::Authorized); + let amount = 1234567890; + let swap_prices = vec![50 * CENT, 50 * CENT]; + let swap_fee = CENT; + let market_id = 0; + assert_ok!(PredictionMarkets::create_market_and_deploy_pool( + RuntimeOrigin::signed(creator), + Asset::Ztg, + creator_fee, + oracle, + period.clone(), + deadlines, + metadata, + market_type.clone(), + dispute_mechanism.clone(), + amount, + swap_prices.clone(), + swap_fee, + )); + let market = MarketCommons::market(&0).unwrap(); + let bonds = MarketBonds { + creation: Some(Bond::new(ALICE, ::ValidityBond::get())), + oracle: Some(Bond::new(ALICE, ::OracleBond::get())), + outsider: None, + dispute: None, + }; + assert_eq!(market.creator, creator); + assert_eq!(market.creation, MarketCreation::Permissionless); + assert_eq!(market.creator_fee, creator_fee); + assert_eq!(market.oracle, oracle); + assert_eq!(market.metadata, multihash); + assert_eq!(market.market_type, market_type); + assert_eq!(market.period, period); + assert_eq!(market.deadlines, deadlines); + assert_eq!(market.scoring_rule, ScoringRule::Lmsr); + assert_eq!(market.status, MarketStatus::Active); + assert_eq!(market.report, None); + assert_eq!(market.resolved_outcome, None); + assert_eq!(market.dispute_mechanism, dispute_mechanism); + assert_eq!(market.bonds, bonds); + // Check that the correct amount of full sets were bought. + assert_eq!( + AssetManager::free_balance(Asset::CategoricalOutcome(market_id, 0), &ALICE), + amount + ); + assert!(DeployPoolMock::called_once_with( + creator, + market_id, + amount, + swap_prices, + swap_fee + )); + }); +} + #[test] fn create_cpmm_market_and_deploy_assets_sets_the_correct_market_parameters_and_reserves_the_correct_amount() { @@ -5727,6 +5797,44 @@ fn create_cpmm_market_and_deploy_assets_sets_the_correct_market_parameters_and_r }); } +#[test] +fn create_market_and_deploy_pool_errors() { + ExtBuilder::default().build().execute_with(|| { + let creator = ALICE; + let oracle = BOB; + let period = MarketPeriod::Block(1..2); + let deadlines = Deadlines { + grace_period: 1, + oracle_duration: ::MinOracleDuration::get() + 2, + dispute_duration: ::MinDisputeDuration::get() + 3, + }; + let metadata = gen_metadata(0x99); + let market_type = MarketType::Categorical(7); + let dispute_mechanism = Some(MarketDisputeMechanism::Authorized); + let amount = 1234567890; + let swap_prices = vec![50 * CENT, 50 * CENT]; + let swap_fee = CENT; + DeployPoolMock::return_error(); + assert_noop!( + PredictionMarkets::create_market_and_deploy_pool( + RuntimeOrigin::signed(creator), + Asset::Ztg, + Perbill::zero(), + oracle, + period.clone(), + deadlines, + metadata, + market_type.clone(), + dispute_mechanism.clone(), + amount, + swap_prices.clone(), + swap_fee, + ), + DispatchError::Other("neo-swaps"), + ); + }); +} + #[test] fn create_market_functions_respect_fee_boundaries() { ExtBuilder::default().build().execute_with(|| { diff --git a/zrml/prediction-markets/src/weights.rs b/zrml/prediction-markets/src/weights.rs index 3ef74599b..a2ea80eed 100644 --- a/zrml/prediction-markets/src/weights.rs +++ b/zrml/prediction-markets/src/weights.rs @@ -82,6 +82,7 @@ pub trait WeightInfoZeitgeist { fn market_status_manager(b: u32, f: u32) -> Weight; fn market_resolution_manager(r: u32, d: u32) -> Weight; fn process_subsidy_collecting_markets_dummy() -> Weight; + fn create_market_and_deploy_pool(m: u32) -> Weight; } /// Weight functions for zrml_prediction_markets (automatically generated) @@ -755,4 +756,31 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: Timestamp Now (r:1 w:0) + /// Proof: Timestamp Now (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + /// Storage: Balances Reserves (r:1 w:1) + /// Proof: Balances Reserves (max_values: None, max_size: Some(1249), added: 3724, mode: MaxEncodedLen) + /// Storage: MarketCommons MarketCounter (r:1 w:1) + /// Proof: MarketCommons MarketCounter (max_values: Some(1), max_size: Some(16), added: 511, mode: MaxEncodedLen) + /// Storage: PredictionMarkets MarketIdsPerCloseTimeFrame (r:1 w:1) + /// Proof: PredictionMarkets MarketIdsPerCloseTimeFrame (max_values: None, max_size: Some(1050), added: 3525, mode: MaxEncodedLen) + /// Storage: System Account (r:2 w:2) + /// Proof: System Account (max_values: None, max_size: Some(132), added: 2607, mode: MaxEncodedLen) + /// Storage: Tokens Accounts (r:4 w:4) + /// Proof: Tokens Accounts (max_values: None, max_size: Some(123), added: 2598, mode: MaxEncodedLen) + /// Storage: Tokens TotalIssuance (r:2 w:2) + /// Proof: Tokens TotalIssuance (max_values: None, max_size: Some(43), added: 2518, mode: MaxEncodedLen) + /// Storage: NeoSwaps Pools (r:1 w:1) + /// Proof: NeoSwaps Pools (max_values: None, max_size: Some(4652), added: 7127, mode: MaxEncodedLen) + /// Storage: MarketCommons Markets (r:0 w:1) + /// Proof: MarketCommons Markets (max_values: None, max_size: Some(541), added: 3016, mode: MaxEncodedLen) + fn create_market_and_deploy_pool(_m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `291 + m * (17 ±0)` + // Estimated: `36032` + // Minimum execution time: 166_000 nanoseconds. + Weight::from_parts(172_000_000, 36032) + .saturating_add(T::DbWeight::get().reads(13_u64)) + .saturating_add(T::DbWeight::get().writes(13_u64)) + } } diff --git a/zrml/swaps/src/lib.rs b/zrml/swaps/src/lib.rs index aaaf31a4f..e4e02fc38 100644 --- a/zrml/swaps/src/lib.rs +++ b/zrml/swaps/src/lib.rs @@ -1952,7 +1952,7 @@ mod pallet { let pool_amount = >::zero(); (pool_status, total_subsidy, total_weight, weights, pool_amount) } - ScoringRule::Orderbook => { + ScoringRule::Lmsr | ScoringRule::Orderbook => { return Err(Error::::InvalidScoringRule.into()); } }; @@ -2513,7 +2513,7 @@ mod pallet { T::RikiddoSigmoidFeeMarketEma::cost(pool_id, &outstanding_after)?; cost_before.checked_sub(&cost_after).ok_or(ArithmeticError::Overflow)? } - ScoringRule::Orderbook => { + ScoringRule::Lmsr | ScoringRule::Orderbook => { return Err(Error::::InvalidScoringRule.into()); } }; @@ -2565,7 +2565,9 @@ mod pallet { ScoringRule::RikiddoSigmoidFeeMarketEma => Ok( T::WeightInfo::swap_exact_amount_in_rikiddo(pool.assets.len().saturated_into()), ), - ScoringRule::Orderbook => Err(Error::::InvalidScoringRule.into()), + ScoringRule::Lmsr | ScoringRule::Orderbook => { + Err(Error::::InvalidScoringRule.into()) + } } } @@ -2673,7 +2675,7 @@ mod pallet { T::RikiddoSigmoidFeeMarketEma::cost(pool_id, &outstanding_after)?; cost_after.checked_sub(&cost_before).ok_or(ArithmeticError::Overflow)? } - ScoringRule::Orderbook => { + ScoringRule::Lmsr | ScoringRule::Orderbook => { return Err(Error::::InvalidScoringRule.into()); } }; @@ -2737,7 +2739,9 @@ mod pallet { pool.assets.len().saturated_into(), )) } - ScoringRule::Orderbook => Err(Error::::InvalidScoringRule.into()), + ScoringRule::Lmsr | ScoringRule::Orderbook => { + Err(Error::::InvalidScoringRule.into()) + } } } } diff --git a/zrml/swaps/src/utils.rs b/zrml/swaps/src/utils.rs index a34e2e719..9006b5ee6 100644 --- a/zrml/swaps/src/utils.rs +++ b/zrml/swaps/src/utils.rs @@ -216,7 +216,7 @@ where return Err(Error::::UnsupportedTrade.into()); } } - ScoringRule::Orderbook => { + ScoringRule::Lmsr | ScoringRule::Orderbook => { return Err(Error::::InvalidScoringRule.into()); } } @@ -233,7 +233,7 @@ where spot_price_before.saturating_sub(spot_price_after) < 20u8.into(), Error::::MathApproximation ), - ScoringRule::Orderbook => { + ScoringRule::Lmsr | ScoringRule::Orderbook => { return Err(Error::::InvalidScoringRule.into()); } } @@ -256,7 +256,7 @@ where let volume = if p.asset_in == base_asset { asset_amount_in } else { asset_amount_out }; T::RikiddoSigmoidFeeMarketEma::update_volume(p.pool_id, volume)?; } - ScoringRule::Orderbook => { + ScoringRule::Lmsr | ScoringRule::Orderbook => { return Err(Error::::InvalidScoringRule.into()); } }