From 30ea9d9a06365955bbfd3fb8437e768934f80cf8 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 30 Nov 2025 21:27:04 -0500 Subject: [PATCH] Tidy the DEX pallet --- Cargo.lock | 62 +- substrate/abi/src/dex.rs | 27 +- substrate/coins/Cargo.toml | 2 +- substrate/coins/src/lib.rs | 10 +- substrate/coins/src/mock.rs | 2 +- substrate/dex/Cargo.toml | 60 +- substrate/dex/{LICENSE-AGPL3 => LICENSE} | 0 substrate/dex/LICENSE-APACHE2 | 211 --- substrate/dex/README.md | 3 + substrate/dex/src/benchmarking.rs | 230 ---- substrate/dex/src/lib.rs | 1539 +++++----------------- substrate/dex/src/mock.rs | 132 +- substrate/dex/src/tests.rs | 1389 ------------------- substrate/dex/src/types.rs | 54 - substrate/dex/src/weights.rs | 259 ---- substrate/median/src/lexicographic.rs | 1 + substrate/median/src/lib.rs | 6 +- substrate/primitives/src/address.rs | 4 +- substrate/primitives/src/dex.rs | 184 +++ substrate/primitives/src/lib.rs | 3 + substrate/runtime/Cargo.toml | 4 + substrate/runtime/src/wasm/mod.rs | 37 +- 22 files changed, 664 insertions(+), 3555 deletions(-) rename substrate/dex/{LICENSE-AGPL3 => LICENSE} (100%) delete mode 100644 substrate/dex/LICENSE-APACHE2 create mode 100644 substrate/dex/README.md delete mode 100644 substrate/dex/src/benchmarking.rs delete mode 100644 substrate/dex/src/tests.rs delete mode 100644 substrate/dex/src/types.rs delete mode 100644 substrate/dex/src/weights.rs create mode 100644 substrate/primitives/src/dex.rs diff --git a/Cargo.lock b/Cargo.lock index cac727c4..853022f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,9 +1478,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.47" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -2203,7 +2203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -4030,9 +4030,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -7022,9 +7022,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -8394,18 +8394,17 @@ dependencies = [ name = "serai-dex-pallet" version = "0.1.0" dependencies = [ - "frame-benchmarking", + "borsh", "frame-support", "frame-system", + "pallet-timestamp", "parity-scale-codec", - "rand_core 0.6.4", + "serai-abi", "serai-coins-pallet", - "serai-primitives", - "sp-api", + "serai-core-pallet", "sp-core", "sp-io", - "sp-runtime", - "sp-std", + "substrate-median", ] [[package]] @@ -9049,6 +9048,7 @@ dependencies = [ "serai-abi", "serai-coins-pallet", "serai-core-pallet", + "serai-dex-pallet", "serai-signals-pallet", "serai-validator-sets-pallet", "sp-api", @@ -10592,9 +10592,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -10646,9 +10646,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -10940,9 +10940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -10953,9 +10953,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10963,9 +10963,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -10976,9 +10976,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -11233,9 +11233,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -11764,18 +11764,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/substrate/abi/src/dex.rs b/substrate/abi/src/dex.rs index 23adf1ee..80a55164 100644 --- a/substrate/abi/src/dex.rs +++ b/substrate/abi/src/dex.rs @@ -8,21 +8,26 @@ use serai_primitives::{ balance::{Amount, ExternalBalance, Balance}, }; +/// The address used for a liquidity pool by the DEX. +pub fn address(coin: ExternalCoin) -> SeraiAddress { + SeraiAddress::system(borsh::to_vec(&(b"DEX", coin)).unwrap()) +} + /// A call to the DEX. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { /// Add liquidity. add_liquidity { - /// The coin to add liquidity for. - coin: ExternalCoin, + /// The pool to add liquidity to, specified by its external coin. + external_coin: ExternalCoin, /// The intended amount of SRI to add as liquidity. sri_intended: Amount, /// The intended amount of the coin to add as liquidity. - coin_intended: Amount, + external_coin_intended: Amount, /// The minimum amount of SRI to add as liquidity. sri_minimum: Amount, /// The minimum amount of the coin to add as liquidity. - coin_minimum: Amount, + external_coin_minimum: Amount, }, /// Transfer these liquidity tokens to the specified address. transfer_liquidity { @@ -40,17 +45,17 @@ pub enum Call { /// The minimum amount of SRI to receive. sri_minimum: Amount, /// The minimum amount of the coin to receive. - coin_minimum: Amount, + external_coin_minimum: Amount, }, /// Swap an exact amount of coins. - swap_exact { + swap { /// The coins to swap. coins_to_swap: Balance, /// The minimum balance to receive. minimum_to_receive: Balance, }, /// Swap for an exact amount of coins. - swap_for_exact { + swap_for { /// The coins to receive. coins_to_receive: Balance, /// The maximum amount to swap. @@ -64,8 +69,8 @@ impl Call { Call::add_liquidity { .. } | Call::transfer_liquidity { .. } | Call::remove_liquidity { .. } | - Call::swap_exact { .. } | - Call::swap_for_exact { .. } => true, + Call::swap { .. } | + Call::swap_for { .. } => true, } } } @@ -84,7 +89,7 @@ pub enum Event { /// The amount of liquidity tokens which were minted. liquidity_tokens_minted: Amount, /// The amount of the coin which was added to the pool's liquidity. - coin_amount: Amount, + external_coin_amount: Amount, /// The amount of SRI which was added to the pool's liquidity. sri_amount: Amount, }, @@ -98,7 +103,7 @@ pub enum Event { /// The mount of liquidity tokens which were burnt. liquidity_tokens_burnt: Amount, /// The amount of the coin which was removed from the pool's liquidity. - coin_amount: Amount, + external_coin_amount: Amount, /// The amount of SRI which was removed from the pool's liquidity. sri_amount: Amount, }, diff --git a/substrate/coins/Cargo.toml b/substrate/coins/Cargo.toml index 27d6651f..8bd04d57 100644 --- a/substrate/coins/Cargo.toml +++ b/substrate/coins/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Coins pallet for Serai" license = "AGPL-3.0-only" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/coins" -authors = ["Akil Demir "] +authors = ["Luke Parker "] edition = "2021" rust-version = "1.85" diff --git a/substrate/coins/src/lib.rs b/substrate/coins/src/lib.rs index 191af067..f6f64b7e 100644 --- a/substrate/coins/src/lib.rs +++ b/substrate/coins/src/lib.rs @@ -227,6 +227,13 @@ mod pallet { Self::emit_event(Event::Transfer { from: from.into(), to: to.into(), coins }); Ok(()) } + + /// Burn `coins` from `from`. + pub fn burn_fn(from: Public, coins: Balance) -> Result<(), Error> { + Self::burn_internal(from, coins)?; + Self::emit_event(Event::Burn { from: from.into(), coins }); + Ok(()) + } } #[pallet::call] @@ -245,8 +252,7 @@ mod pallet { #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn burn(origin: OriginFor, coins: Balance) -> DispatchResult { let from = ensure_signed(origin)?; - Self::burn_internal(from, coins)?; - Self::emit_event(Event::Burn { from: from.into(), coins }); + Self::burn_fn(from, coins)?; Ok(()) } diff --git a/substrate/coins/src/mock.rs b/substrate/coins/src/mock.rs index 7cd1c705..fa8e35ec 100644 --- a/substrate/coins/src/mock.rs +++ b/substrate/coins/src/mock.rs @@ -1,4 +1,4 @@ -//! Test environment for Coins pallet. +//! Test environment for the Coins pallet. use borsh::BorshDeserialize; diff --git a/substrate/dex/Cargo.toml b/substrate/dex/Cargo.toml index 9dd7e465..7d356fc8 100644 --- a/substrate/dex/Cargo.toml +++ b/substrate/dex/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "DEX pallet for Serai" license = "AGPL-3.0-only" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/dex" -authors = ["Parity Technologies , Akil Demir "] +authors = ["Luke Parker "] edition = "2021" rust-version = "1.85" @@ -12,61 +12,61 @@ rust-version = "1.85" all-features = true rustdoc-args = ["--cfg", "docsrs"] -[package.metadata.cargo-machete] -ignored = ["scale"] - [lints] workspace = true [dependencies] -scale = { package = "parity-scale-codec", version = "3.6.1", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } -sp-std = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } -sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } -sp-api = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } -sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false } -frame-benchmarking = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false, optional = true } -coins-pallet = { package = "serai-coins-pallet", path = "../coins", default-features = false } +substrate-median = { path = "../median", default-features = false } -serai-primitives = { path = "../primitives", default-features = false } +serai-abi = { path = "../abi", default-features = false, features = ["substrate"] } +serai-core-pallet = { path = "../core", default-features = false } +serai-coins-pallet = { path = "../coins", default-features = false } [dev-dependencies] -rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } + +sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false, features = ["std"] } + +pallet-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc", default-features = false, features = ["std"] } [features] -default = ["std"] std = [ "scale/std", - "sp-std/std", - "sp-io/std", - "sp-api/std", - "sp-runtime/std", "sp-core/std", - "serai-primitives/std", - "frame-system/std", "frame-support/std", - "frame-benchmarking?/std", - "coins-pallet/std", -] -runtime-benchmarks = [ - "sp-runtime/runtime-benchmarks", + "substrate-median/std", - "frame-system/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-benchmarking/runtime-benchmarks", + "serai-abi/std", + "serai-core-pallet/std", + "serai-coins-pallet/std", ] + try-runtime = [ - "sp-runtime/try-runtime", - "frame-system/try-runtime", "frame-support/try-runtime", + + "serai-abi/try-runtime", + "serai-core-pallet/try-runtime", + "serai-coins-pallet/try-runtime", ] + +runtime-benchmarks = [ + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", + + "serai-core-pallet/runtime-benchmarks", + "serai-coins-pallet/runtime-benchmarks", +] + +default = ["std"] diff --git a/substrate/dex/LICENSE-AGPL3 b/substrate/dex/LICENSE similarity index 100% rename from substrate/dex/LICENSE-AGPL3 rename to substrate/dex/LICENSE diff --git a/substrate/dex/LICENSE-APACHE2 b/substrate/dex/LICENSE-APACHE2 deleted file mode 100644 index fbb0616d..00000000 --- a/substrate/dex/LICENSE-APACHE2 +++ /dev/null @@ -1,211 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - NOTE - -Individual files contain the following tag instead of the full license -text. - - SPDX-License-Identifier: Apache-2.0 - -This enables machine processing of license information based on the SPDX -License Identifiers that are here available: http://spdx.org/licenses/ \ No newline at end of file diff --git a/substrate/dex/README.md b/substrate/dex/README.md new file mode 100644 index 00000000..3ee5c9bc --- /dev/null +++ b/substrate/dex/README.md @@ -0,0 +1,3 @@ +# DEX Pallet + +Pallet implementing the necessary DEX logic for the Serai protocol. diff --git a/substrate/dex/src/benchmarking.rs b/substrate/dex/src/benchmarking.rs deleted file mode 100644 index 866bcf15..00000000 --- a/substrate/dex/src/benchmarking.rs +++ /dev/null @@ -1,230 +0,0 @@ -// This file was originally: - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// It has been forked into a crate distributed under the AGPL 3.0. -// Please check the current distribution for up-to-date copyright and licensing information. - -//! Dex pallet benchmarking. - -use super::*; -use frame_benchmarking::{benchmarks, whitelisted_caller}; -use frame_support::{assert_ok, storage::bounded::BoundedVec}; -use frame_system::RawOrigin as SystemOrigin; - -use sp_runtime::traits::StaticLookup; -use sp_std::{ops::Div, prelude::*}; - -use serai_primitives::{Amount, Balance}; - -use crate::Pallet as Dex; -use coins_pallet::Pallet as Coins; - -const INITIAL_COIN_BALANCE: u64 = 1_000_000_000; -type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; - -type LiquidityTokens = coins_pallet::Pallet; - -fn create_coin(coin: &ExternalCoin) -> (T::AccountId, AccountIdLookupOf) { - let caller: T::AccountId = whitelisted_caller(); - let caller_lookup = T::Lookup::unlookup(caller); - assert_ok!(Coins::::mint( - caller, - Balance { coin: Coin::native(), amount: Amount(SubstrateAmount::MAX.div(1000u64)) } - )); - assert_ok!(Coins::::mint( - caller, - Balance { coin: (*coin).into(), amount: Amount(INITIAL_COIN_BALANCE) } - )); - (caller, caller_lookup) -} - -fn create_coin_and_pool( - coin: &ExternalCoin, -) -> (ExternalCoin, T::AccountId, AccountIdLookupOf) { - let (caller, caller_lookup) = create_coin::(coin); - assert_ok!(Dex::::create_pool(*coin)); - - (*coin, caller, caller_lookup) -} - -benchmarks! { - add_liquidity { - let coin1 = Coin::native(); - let coin2 = ExternalCoin::Bitcoin; - let (lp_token, caller, _) = create_coin_and_pool::(&coin2); - let add_amount: u64 = 1000; - }: _( - SystemOrigin::Signed(caller), - coin2, - 1000u64, - add_amount, - 0u64, - 0u64, - caller - ) - verify { - let pool_id = Dex::::get_pool_id(coin1, coin2.into()).unwrap(); - let lp_minted = Dex::::calc_lp_amount_for_zero_supply( - add_amount, - 1000u64, - ).unwrap(); - assert_eq!( - LiquidityTokens::::balance(caller, lp_token.into()).0, - lp_minted - ); - assert_eq!( - Coins::::balance(Dex::::get_pool_account(pool_id), Coin::native()).0, - add_amount - ); - assert_eq!( - Coins::::balance( - Dex::::get_pool_account(pool_id), - ExternalCoin::Bitcoin.into(), - ).0, - 1000 - ); - } - - remove_liquidity { - let coin1 = Coin::native(); - let coin2 = ExternalCoin::Monero; - let (lp_token, caller, _) = create_coin_and_pool::(&coin2); - let add_amount: u64 = 100; - let lp_minted = Dex::::calc_lp_amount_for_zero_supply( - add_amount, - 1000u64 - ).unwrap(); - let remove_lp_amount: u64 = lp_minted.checked_div(10).unwrap(); - - Dex::::add_liquidity( - SystemOrigin::Signed(caller).into(), - coin2, - 1000u64, - add_amount, - 0u64, - 0u64, - caller, - )?; - let total_supply = LiquidityTokens::::supply(Coin::from(lp_token)); - }: _( - SystemOrigin::Signed(caller), - coin2, - remove_lp_amount, - 0u64, - 0u64, - caller - ) - verify { - let new_total_supply = LiquidityTokens::::supply(Coin::from(lp_token)); - assert_eq!( - new_total_supply, - total_supply - remove_lp_amount - ); - } - - swap_exact_tokens_for_tokens { - let native = Coin::native(); - let coin1 = ExternalCoin::Bitcoin; - let coin2 = ExternalCoin::Ether; - let (_, caller, _) = create_coin_and_pool::(&coin1); - let (_, _) = create_coin::(&coin2); - - Dex::::add_liquidity( - SystemOrigin::Signed(caller).into(), - coin1, - 200u64, - // TODO: this call otherwise fails with `InsufficientLiquidityMinted` if we don't multiply - // with 3. Might be again related to their expectance on ed being > 1. - 100 * 3, - 0u64, - 0u64, - caller, - )?; - - let swap_amount = 100u64; - - // since we only allow the native-coin pools, then the worst case scenario would be to swap - // coin1-native-coin2 - Dex::::create_pool(coin2)?; - Dex::::add_liquidity( - SystemOrigin::Signed(caller).into(), - coin2, - 1000u64, - 500, - 0u64, - 0u64, - caller, - )?; - - let path = vec![Coin::from(coin1), native, Coin::from(coin2)]; - let path = BoundedVec::<_, T::MaxSwapPathLength>::try_from(path).unwrap(); - let native_balance = Coins::::balance(caller, native).0; - let coin1_balance = Coins::::balance(caller, ExternalCoin::Bitcoin.into()).0; - }: _(SystemOrigin::Signed(caller), path, swap_amount, 1u64, caller) - verify { - let ed_bump = 2u64; - let new_coin1_balance = Coins::::balance(caller, ExternalCoin::Bitcoin.into()).0; - assert_eq!(new_coin1_balance, coin1_balance - 100u64); - } - - swap_tokens_for_exact_tokens { - let native = Coin::native(); - let coin1 = ExternalCoin::Bitcoin; - let coin2 = ExternalCoin::Ether; - let (_, caller, _) = create_coin_and_pool::(&coin1); - let (_, _) = create_coin::(&coin2); - - Dex::::add_liquidity( - SystemOrigin::Signed(caller).into(), - coin1, - 500u64, - 1000, - 0u64, - 0u64, - caller, - )?; - - // since we only allow the native-coin pools, then the worst case scenario would be to swap - // coin1-native-coin2 - Dex::::create_pool(coin2)?; - Dex::::add_liquidity( - SystemOrigin::Signed(caller).into(), - coin2, - 1000u64, - 500, - 0u64, - 0u64, - caller, - )?; - let path = vec![Coin::from(coin1), native, Coin::from(coin2)]; - - let path: BoundedVec<_, T::MaxSwapPathLength> = BoundedVec::try_from(path).unwrap(); - let coin2_balance = Coins::::balance(caller, ExternalCoin::Ether.into()).0; - }: _( - SystemOrigin::Signed(caller), - path.clone(), - 100u64, - 1000, - caller - ) - verify { - let new_coin2_balance = Coins::::balance(caller, ExternalCoin::Ether.into()).0; - assert_eq!(new_coin2_balance, coin2_balance + 100u64); - } - - impl_benchmark_test_suite!(Dex, crate::mock::new_test_ext(), crate::mock::Test); -} diff --git a/substrate/dex/src/lib.rs b/substrate/dex/src/lib.rs index 71d10549..8e438962 100644 --- a/substrate/dex/src/lib.rs +++ b/substrate/dex/src/lib.rs @@ -1,1293 +1,376 @@ -// This file was originally: - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// It has been forked into a crate distributed under the AGPL 3.0. -// Please check the current distribution for up-to-date copyright and licensing information. - -//! # Serai Dex pallet -//! -//! Serai Dex pallet based on the [Uniswap V2](https://github.com/Uniswap/v2-core) logic. -//! -//! ## Overview -//! -//! This pallet allows you to: -//! -//! - [create a liquidity pool](`Pallet::create_pool()`) for 2 coins -//! - [provide the liquidity](`Pallet::add_liquidity()`) and receive back an LP token -//! - [exchange the LP token back to coins](`Pallet::remove_liquidity()`) -//! - [swap a specific amount of coins for another](`Pallet::swap_exact_tokens_for_tokens()`) if -//! there is a pool created, or -//! - [swap some coins for a specific amount of -//! another](`Pallet::swap_tokens_for_exact_tokens()`). -//! - [query for an exchange price](`DexApi::quote_price_exact_tokens_for_tokens`) via -//! a runtime call endpoint -//! - [query the size of a liquidity pool](`DexApi::get_reserves`) via a runtime api -//! endpoint. -//! -//! The `quote_price_exact_tokens_for_tokens` and `quote_price_tokens_for_exact_tokens` functions -//! both take a path parameter of the route to take. If you want to swap from native coin to -//! non-native coin 1, you would pass in a path of `[DOT, 1]` or `[1, DOT]`. If you want to swap -//! from non-native coin 1 to non-native coin 2, you would pass in a path of `[1, DOT, 2]`. -//! -//! (For an example of configuring this pallet to use `MultiLocation` as an coin id, see the -//! cumulus repo). -//! -//! Here is an example `state_call` that asks for a quote of a pool of native versus coin 1: -//! -//! ```text -//! curl -sS -H "Content-Type: application/json" -d \ -//! '{ -//! "id": 1, -//! "jsonrpc": "2.0", -//! "method": "state_call", -//! "params": [ -//! "DexApi_quote_price_tokens_for_exact_tokens", -//! "0x0101000000000000000000000011000000000000000000" -//! ] -//! }' \ -//! http://localhost:9933/ -//! ``` -//! (This can be run against the kitchen sync node in the `node` folder of this repo.) +#![doc = include_str!("../README.md")] #![deny(missing_docs)] -#![cfg_attr(not(feature = "std"), no_std)] -use frame_support::traits::DefensiveOption; +#![cfg_attr(not(any(feature = "std", test)), no_std)] -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - -mod types; -pub mod weights; - -#[cfg(test)] -mod tests; +extern crate alloc; #[cfg(test)] mod mock; -use frame_support::{ensure, pallet_prelude::*, BoundedBTreeSet}; -use frame_system::{ - pallet_prelude::{BlockNumberFor, OriginFor}, - ensure_signed, -}; - -pub use pallet::*; - -use sp_runtime::{ - traits::{TrailingZeroInput, IntegerSquareRoot}, - DispatchError, -}; - -use serai_primitives::*; - -use sp_std::prelude::*; -pub use types::*; -pub use weights::WeightInfo; - -// TODO: Investigate why Substrate generates these -#[allow( - unreachable_patterns, - clippy::cast_possible_truncation, - clippy::no_effect_underscore_binding -)] +#[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] -pub mod pallet { +mod pallet { + use frame_system::pallet_prelude::*; + use frame_support::pallet_prelude::*; + + use serai_abi::{ + primitives::{ + prelude::*, + dex::{Error as PrimitivesError, Reserves, Premise}, + }, + Event, + }; + + use serai_core_pallet::Pallet as Core; + type Coins = serai_coins_pallet::Pallet; + type LiquidityTokens = + serai_coins_pallet::Pallet; + use super::*; - use sp_core::sr25519::Public; + /// The configuration of this pallet. + #[pallet::config] + pub trait Config: + frame_system::Config + + serai_core_pallet::Config + + serai_coins_pallet::Config + + serai_coins_pallet::Config + { + } - use coins_pallet::{Pallet as CoinsPallet, Config as CoinsConfig}; + /// An error incurred. + #[pallet::error] + pub enum Error { + /// The effected/would-be-used liquidity is invalid. + InvalidLiquidity, + /// An arithmetic overflow occurred. + Overflow, + /// An arithmetic underflow occured. + Underflow, + /// The requested swap wasn't satisfied. + Unsatisfied, + /// The swap was from the coin it's to. + FromToSelf, + } - /// Pool ID. - /// - /// The pool's `AccountId` is derived from this type. Any changes to the type may necessitate a - /// migration. - pub type PoolId = ExternalCoin; + impl From for Error { + fn from(error: PrimitivesError) -> Error { + match error { + PrimitivesError::Overflow => Error::Overflow, + PrimitivesError::Underflow => Error::Underflow, + PrimitivesError::KInvariant => Error::Unsatisfied, + } + } + } - /// LiquidityTokens Pallet as an instance of coins pallet. - pub type LiquidityTokens = coins_pallet::Pallet; - - /// A type used for amount conversions. - pub type HigherPrecisionBalance = u128; + #[pallet::storage] + type LpFeeInThousandths = StorageMap<_, Identity, ExternalCoin, u8, OptionQuery>; + /// The Pallet struct. #[pallet::pallet] pub struct Pallet(_); - #[pallet::config] - pub trait Config: - frame_system::Config - + CoinsConfig - + coins_pallet::Config - { - /// Overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - /// A % the liquidity providers will take of every swap. Represents 10ths of a percent. - #[pallet::constant] - type LPFee: Get; - - /// The minimum LP token amount that could be minted. Ameliorates rounding errors. - #[pallet::constant] - type MintMinLiquidity: Get; - - /// The max number of hops in a swap. - #[pallet::constant] - type MaxSwapPathLength: Get; - - /// Last N number of blocks that oracle keeps track of the prices. - #[pallet::constant] - type MedianPriceWindowLength: Get; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - } - - /// Map from `PoolId` to `()`. This establishes whether a pool has been officially - /// created rather than people sending tokens directly to a pool's public account. - #[pallet::storage] - pub type Pools = StorageMap<_, Blake2_128Concat, PoolId, (), OptionQuery>; - - #[pallet::storage] - #[pallet::getter(fn spot_price_for_block)] - pub type SpotPriceForBlock = - StorageDoubleMap<_, Identity, BlockNumberFor, Identity, ExternalCoin, Amount, OptionQuery>; - - /// Moving window of prices from each block. - /// - /// The [u8; 8] key is the amount's big endian bytes, and u16 is the amount of inclusions in this - /// multi-set. Since the underlying map is lexicographically sorted, this map stores amounts from - /// low to high. - #[pallet::storage] - pub type SpotPrices = - StorageDoubleMap<_, Identity, ExternalCoin, Identity, [u8; 8], u16, OptionQuery>; - - // SpotPrices, yet with keys stored in reverse lexicographic order. - #[pallet::storage] - pub type ReverseSpotPrices = - StorageDoubleMap<_, Identity, ExternalCoin, Identity, [u8; 8], (), OptionQuery>; - - /// Current length of the `SpotPrices` map. - #[pallet::storage] - pub type SpotPricesLength = StorageMap<_, Identity, ExternalCoin, u16, OptionQuery>; - - /// Current position of the median within the `SpotPrices` map; - #[pallet::storage] - pub type CurrentMedianPosition = - StorageMap<_, Identity, ExternalCoin, u16, OptionQuery>; - - /// Current median price of the prices in the `SpotPrices` map at any given time. - #[pallet::storage] - #[pallet::getter(fn median_price)] - pub type MedianPrice = StorageMap<_, Identity, ExternalCoin, Amount, OptionQuery>; - - /// The price used for evaluating economic security, which is the highest observed median price. - #[pallet::storage] - #[pallet::getter(fn security_oracle_value)] - pub type SecurityOracleValue = - StorageMap<_, Identity, ExternalCoin, Amount, OptionQuery>; - - /// Total swap volume of a given pool in terms of SRI. - #[pallet::storage] - #[pallet::getter(fn swap_volume)] - pub type SwapVolume = StorageMap<_, Identity, PoolId, u64, OptionQuery>; - impl Pallet { - fn restore_median( - coin: ExternalCoin, - mut current_median_pos: u16, - mut current_median: Amount, - length: u16, - ) { - // 1 -> 0 (the only value) - // 2 -> 1 (the higher element), 4 -> 2 (the higher element) - // 3 -> 1 (the true median) - let target_median_pos = length / 2; - while current_median_pos < target_median_pos { - // Get the amount of presences for the current element - let key = current_median.0.to_be_bytes(); - let presences = SpotPrices::::get(coin, key).unwrap(); - // > is correct, not >=. - // Consider: - // - length = 1, current_median_pos = 0, presences = 1, target_median_pos = 0 - // - length = 2, current_median_pos = 0, presences = 2, target_median_pos = 1 - // - length = 2, current_median_pos = 0, presences = 1, target_median_pos = 1 - if (current_median_pos + presences) > target_median_pos { - break; - } - current_median_pos += presences; - - let key = SpotPrices::::hashed_key_for(coin, key); - let next_price = SpotPrices::::iter_key_prefix_from(coin, key).next().unwrap(); - current_median = Amount(u64::from_be_bytes(next_price)); - } - - while current_median_pos > target_median_pos { - // Get the next element - let key = reverse_lexicographic_order(current_median.0.to_be_bytes()); - let key = ReverseSpotPrices::::hashed_key_for(coin, key); - let next_price = ReverseSpotPrices::::iter_key_prefix_from(coin, key).next().unwrap(); - let next_price = reverse_lexicographic_order(next_price); - current_median = Amount(u64::from_be_bytes(next_price)); - - // Get its amount of presences - let presences = SpotPrices::::get(coin, current_median.0.to_be_bytes()).unwrap(); - // Adjust from next_value_first_pos to this_value_first_pos by substracting this value's - // amount of times present - current_median_pos -= presences; - - if current_median_pos <= target_median_pos { - break; - } - } - - CurrentMedianPosition::::set(coin, Some(current_median_pos)); - MedianPrice::::set(coin, Some(current_median)); - } - - pub(crate) fn insert_into_median(coin: ExternalCoin, amount: Amount) { - let new_quantity_of_presences = - SpotPrices::::get(coin, amount.0.to_be_bytes()).unwrap_or(0) + 1; - SpotPrices::::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences)); - if new_quantity_of_presences == 1 { - ReverseSpotPrices::::set( - coin, - reverse_lexicographic_order(amount.0.to_be_bytes()), - Some(()), - ); - } - - let new_length = SpotPricesLength::::get(coin).unwrap_or(0) + 1; - SpotPricesLength::::set(coin, Some(new_length)); - - let Some(current_median) = MedianPrice::::get(coin) else { - MedianPrice::::set(coin, Some(amount)); - CurrentMedianPosition::::set(coin, Some(0)); - return; - }; - - let mut current_median_pos = CurrentMedianPosition::::get(coin).unwrap(); - // If this is being inserted before the current median, the current median's position has - // increased - if amount < current_median { - current_median_pos += 1; - } - Self::restore_median(coin, current_median_pos, current_median, new_length); - } - - pub(crate) fn remove_from_median(coin: ExternalCoin, amount: Amount) { - let mut current_median = MedianPrice::::get(coin).unwrap(); - - let mut current_median_pos = CurrentMedianPosition::::get(coin).unwrap(); - if amount < current_median { - current_median_pos -= 1; - } - - let new_quantity_of_presences = - SpotPrices::::get(coin, amount.0.to_be_bytes()).unwrap() - 1; - if new_quantity_of_presences == 0 { - let normal_key = amount.0.to_be_bytes(); - SpotPrices::::remove(coin, normal_key); - ReverseSpotPrices::::remove(coin, reverse_lexicographic_order(amount.0.to_be_bytes())); - - // If we've removed the current item at this position, update to the item now at this - // position - if amount == current_median { - let key = SpotPrices::::hashed_key_for(coin, normal_key); - current_median = Amount(u64::from_be_bytes( - SpotPrices::::iter_key_prefix_from(coin, key).next().unwrap(), - )); - } - } else { - SpotPrices::::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences)); - } - - let new_length = SpotPricesLength::::get(coin).unwrap() - 1; - SpotPricesLength::::set(coin, Some(new_length)); - - Self::restore_median(coin, current_median_pos, current_median, new_length); + fn emit_event(event: Event) { + Core::::emit_event(event) } } - // Pallet's events. - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// A successful call of the `CreatePool` extrinsic will create this event. - PoolCreated { - /// The pool id associated with the pool. Note that the order of the coins may not be - /// the same as the order specified in the create pool extrinsic. - pool_id: PoolId, - /// The account ID of the pool. - pool_account: T::AccountId, - }, + const MINIMUM_LIQUIDITY: u64 = 1 << 16; - /// A successful call of the `AddLiquidity` extrinsic will create this event. - LiquidityAdded { - /// The account that the liquidity was taken from. - who: T::AccountId, - /// The account that the liquidity tokens were minted to. - mint_to: T::AccountId, - /// The pool id of the pool that the liquidity was added to. - pool_id: PoolId, - /// The amount of the coin that was added to the pool. - coin_amount: SubstrateAmount, - /// The amount of the SRI that was added to the pool. - sri_amount: SubstrateAmount, - /// The amount of lp tokens that were minted of that id. - lp_token_minted: SubstrateAmount, - }, - - /// A successful call of the `RemoveLiquidity` extrinsic will create this event. - LiquidityRemoved { - /// The account that the liquidity tokens were burned from. - who: T::AccountId, - /// The account that the coins were transferred to. - withdraw_to: T::AccountId, - /// The pool id that the liquidity was removed from. - pool_id: PoolId, - /// The amount of the first coin that was removed from the pool. - coin_amount: SubstrateAmount, - /// The amount of the second coin that was removed from the pool. - sri_amount: SubstrateAmount, - /// The amount of lp tokens that were burned of that id. - lp_token_burned: SubstrateAmount, - }, - - /// Coins have been converted from one to another. Both `SwapExactTokenForToken` - /// and `SwapTokenForExactToken` will generate this event. - SwapExecuted { - /// Which account was the instigator of the swap. - who: T::AccountId, - /// The account that the coins were transferred to. - send_to: T::AccountId, - /// The route of coin ids that the swap went through. - /// E.g. A -> SRI -> B - path: BoundedVec, - /// The amount of the first coin that was swapped. - amount_in: SubstrateAmount, - /// The amount of the second coin that was received. - amount_out: SubstrateAmount, - }, - } - - #[pallet::error] - pub enum Error { - /// Provided coins are equal. - EqualCoins, - /// Pool already exists. - PoolExists, - /// Desired amount can't be zero. - WrongDesiredAmount, - /// Provided amount should be greater than or equal to the existential deposit/coin's - /// minimum amount. - CoinAmountLessThanMinimum, - /// Provided amount should be greater than or equal to the existential deposit/coin's - /// minimum amount. - SriAmountLessThanMinimum, - /// Reserve needs to always be greater than or equal to the existential deposit/coin's - /// minimum amount. - ReserveLeftLessThanMinimum, - /// Desired amount can't be equal to the pool reserve. - AmountOutTooHigh, - /// The pool doesn't exist. - PoolNotFound, - /// An overflow happened. - Overflow, - /// The minimum amount requirement for the first token in the pair wasn't met. - CoinOneDepositDidNotMeetMinimum, - /// The minimum amount requirement for the second token in the pair wasn't met. - CoinTwoDepositDidNotMeetMinimum, - /// The minimum amount requirement for the first token in the pair wasn't met. - CoinOneWithdrawalDidNotMeetMinimum, - /// The minimum amount requirement for the second token in the pair wasn't met. - CoinTwoWithdrawalDidNotMeetMinimum, - /// Optimal calculated amount is less than desired. - OptimalAmountLessThanDesired, - /// Insufficient liquidity minted. - InsufficientLiquidityMinted, - /// Requested liquidity can't be zero. - ZeroLiquidity, - /// Amount can't be zero. - ZeroAmount, - /// Calculated amount out is less than provided minimum amount. - ProvidedMinimumNotSufficientForSwap, - /// Provided maximum amount is not sufficient for swap. - ProvidedMaximumNotSufficientForSwap, - /// The provided path must consists of 2 coins at least. - InvalidPath, - /// It was not possible to calculate path data. - PathError, - /// The provided path must consists of unique coins. - NonUniquePath, - /// Unable to find an element in an array/vec that should have one-to-one correspondence - /// with another. For example, an array of coins constituting a `path` should have a - /// corresponding array of `amounts` along the path. - CorrespondenceError, - } - - #[pallet::hooks] - impl Hooks> for Pallet { - fn on_finalize(n: BlockNumberFor) { - // we run this on on_finalize because we want to use the last price of the block for a coin. - // This prevents the exploit where a malicious block proposer spikes the price in either - // direction, then includes a swap in the other direction (ensuring they don't get arbitraged - // against) - // Since they'll have to leave the spike present at the end of the block, making the next - // block the one to include any arbitrage transactions (which there's no guarantee they'll - // produce), this cannot be done in a way without significant risk - for coin in Pools::::iter_keys() { - // insert the new price to our oracle window - // The spot price for 1 coin, in atomic units, to SRI is used - let sri_per_coin = - if let Ok((sri_balance, coin_balance)) = Self::get_reserves(&Coin::Serai, &coin.into()) { - // We use 1 coin to handle rounding errors which may occur with atomic units - // If we used atomic units, any coin whose atomic unit is worth less than SRI's atomic - // unit would cause a 'price' of 0 - // If the decimals aren't large enough to provide sufficient buffer, use 10,000 - let coin_decimals = coin.decimals().max(5); - let accuracy_increase = - HigherPrecisionBalance::from(SubstrateAmount::pow(10, coin_decimals)); - u64::try_from( - accuracy_increase * HigherPrecisionBalance::from(sri_balance) / - HigherPrecisionBalance::from(coin_balance), - ) - .unwrap_or(u64::MAX) - } else { - 0 - }; - - let sri_per_coin = Amount(sri_per_coin); - SpotPriceForBlock::::set(n, coin, Some(sri_per_coin)); - Self::insert_into_median(coin, sri_per_coin); - if SpotPricesLength::::get(coin).unwrap() > T::MedianPriceWindowLength::get() { - let old = n - T::MedianPriceWindowLength::get().into(); - let old_price = SpotPriceForBlock::::get(old, coin).unwrap(); - SpotPriceForBlock::::remove(old, coin); - Self::remove_from_median(coin, old_price); - } - - // update the oracle value - let median = Self::median_price(coin).unwrap_or(Amount(0)); - let oracle_value = Self::security_oracle_value(coin).unwrap_or(Amount(0)); - if median > oracle_value { - SecurityOracleValue::::set(coin, Some(median)); - } - } - } - } - - impl Pallet { - /// Creates an empty liquidity pool and an associated new `lp_token` coin - /// (the id of which is returned in the `Event::PoolCreated` event). - /// - /// Once a pool is created, someone may [`Pallet::add_liquidity`] to it. - pub(crate) fn create_pool(coin: ExternalCoin) -> DispatchResult { - // get pool_id - let pool_id = Self::get_pool_id(coin.into(), Coin::native())?; - ensure!(!Pools::::contains_key(pool_id), Error::::PoolExists); - - let pool_account = Self::get_pool_account(pool_id); - frame_system::Pallet::::inc_providers(&pool_account); - - Pools::::insert(pool_id, ()); - Self::deposit_event(Event::PoolCreated { pool_id, pool_account }); - Ok(()) - } - - /// A hook to be called whenever a network's session is rotated. - pub fn on_new_session(network: NetworkId) { - // Only track the price for non-SRI coins as this is SRI denominated - if let NetworkId::External(n) = network { - for coin in n.coins() { - SecurityOracleValue::::set(coin, Self::median_price(coin)); - } - } - } - } - - /// Pallet's callable functions. #[pallet::call] impl Pallet { - /// Provide liquidity into the pool of `coin1` and `coin2`. - /// NOTE: an optimal amount of coin1 and coin2 will be calculated and - /// might be different than the provided `amount1_desired`/`amount2_desired` - /// thus you should provide the min amount you're happy to provide. - /// Params `amount1_min`/`amount2_min` represent that. - /// `mint_to` will be sent the liquidity tokens that represent this share of the pool. - /// - /// Once liquidity is added, someone may successfully call - /// [`Pallet::swap_exact_tokens_for_tokens`] successfully. + /// Add liquidity. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::add_liquidity())] - #[allow(clippy::too_many_arguments)] + #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn add_liquidity( origin: OriginFor, - coin: ExternalCoin, - coin_desired: SubstrateAmount, - sri_desired: SubstrateAmount, - coin_min: SubstrateAmount, - sri_min: SubstrateAmount, - mint_to: T::AccountId, + external_coin: ExternalCoin, + sri_intended: Amount, + external_coin_intended: Amount, + sri_minimum: Amount, + external_coin_minimum: Amount, ) -> DispatchResult { - let sender = ensure_signed(origin)?; - ensure!((sri_desired > 0) && (coin_desired > 0), Error::::WrongDesiredAmount); + let from = ensure_signed(origin)?; - let pool_id = Self::get_pool_id(coin.into(), Coin::native())?; + let pool = serai_abi::dex::address(external_coin); + let supply = LiquidityTokens::::supply(Coin::from(external_coin)).0; - // create the pool if it doesn't exist. We can just attempt to do that because our checks - // far enough to allow that. - if Pools::::get(pool_id).is_none() { - Self::create_pool(coin)?; - } - let pool_account = Self::get_pool_account(pool_id); - - let sri_reserve = Self::get_balance(&pool_account, Coin::Serai); - let coin_reserve = Self::get_balance(&pool_account, coin.into()); - - let sri_amount: SubstrateAmount; - let coin_amount: SubstrateAmount; - if (sri_reserve == 0) || (coin_reserve == 0) { - sri_amount = sri_desired; - coin_amount = coin_desired; - } else { - let coin_optimal = Self::quote(sri_desired, sri_reserve, coin_reserve)?; - - if coin_optimal <= coin_desired { - ensure!(coin_optimal >= coin_min, Error::::CoinTwoDepositDidNotMeetMinimum); - sri_amount = sri_desired; - coin_amount = coin_optimal; - } else { - let sri_optimal = Self::quote(coin_desired, coin_reserve, sri_reserve)?; - ensure!(sri_optimal <= sri_desired, Error::::OptimalAmountLessThanDesired); - ensure!(sri_optimal >= sri_min, Error::::CoinOneDepositDidNotMeetMinimum); - sri_amount = sri_optimal; - coin_amount = coin_desired; + let (sri_actual, external_coin_actual, liquidity) = if supply == 0 { + let sri_actual = sri_intended; + let external_coin_actual = external_coin_intended; + let liquidity = Amount( + u64::try_from((u128::from(sri_actual.0) * u128::from(external_coin_actual.0)).isqrt()) + .map_err(|_| Error::::Overflow)?, + ); + if liquidity.0 < MINIMUM_LIQUIDITY { + Err(Error::::InvalidLiquidity)?; } - } - - ensure!(sri_amount.saturating_add(sri_reserve) >= 1, Error::::SriAmountLessThanMinimum); - ensure!(coin_amount.saturating_add(coin_reserve) >= 1, Error::::CoinAmountLessThanMinimum); - - Self::transfer( - &sender, - &pool_account, - Balance { coin: Coin::Serai, amount: Amount(sri_amount) }, - )?; - Self::transfer( - &sender, - &pool_account, - Balance { coin: coin.into(), amount: Amount(coin_amount) }, - )?; - - let total_supply = LiquidityTokens::::supply(Coin::from(coin)); - - let lp_token_amount: SubstrateAmount; - if total_supply == 0 { - lp_token_amount = Self::calc_lp_amount_for_zero_supply(sri_amount, coin_amount)?; - LiquidityTokens::::mint( - pool_account, - Balance { coin: coin.into(), amount: Amount(T::MintMinLiquidity::get()) }, - )?; + (sri_intended, external_coin_intended, liquidity) } else { - let side1 = Self::mul_div(sri_amount, total_supply, sri_reserve)?; - let side2 = Self::mul_div(coin_amount, total_supply, coin_reserve)?; - lp_token_amount = side1.min(side2); - } + let reserves = Reserves { + sri: Coins::::balance(pool, Coin::Serai), + external_coin: Coins::::balance(pool, Coin::from(external_coin)), + }; - ensure!( - lp_token_amount > T::MintMinLiquidity::get(), - Error::::InsufficientLiquidityMinted - ); - - LiquidityTokens::::mint( - mint_to, - Balance { coin: coin.into(), amount: Amount(lp_token_amount) }, - )?; - - Self::deposit_event(Event::LiquidityAdded { - who: sender, - mint_to, - pool_id, - coin_amount, - sri_amount, - lp_token_minted: lp_token_amount, - }); - - Ok(()) - } - - /// Allows you to remove liquidity by providing the `lp_token_burn` tokens that will be - /// burned in the process. With the usage of `amount1_min_receive`/`amount2_min_receive` - /// it's possible to control the min amount of returned tokens you're happy with. - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::remove_liquidity())] - pub fn remove_liquidity( - origin: OriginFor, - coin: ExternalCoin, - lp_token_burn: SubstrateAmount, - coin_min_receive: SubstrateAmount, - sri_min_receive: SubstrateAmount, - withdraw_to: T::AccountId, - ) -> DispatchResult { - let sender = ensure_signed(origin.clone())?; - - let pool_id = Self::get_pool_id(coin.into(), Coin::native()).unwrap(); - ensure!(lp_token_burn > 0, Error::::ZeroLiquidity); - - Pools::::get(pool_id).as_ref().ok_or(Error::::PoolNotFound)?; - - let pool_account = Self::get_pool_account(pool_id); - let sri_reserve = Self::get_balance(&pool_account, Coin::Serai); - let coin_reserve = Self::get_balance(&pool_account, coin.into()); - - let total_supply = LiquidityTokens::::supply(Coin::from(coin)); - let lp_redeem_amount = lp_token_burn; - - let sri_amount = Self::mul_div(lp_redeem_amount, sri_reserve, total_supply)?; - let coin_amount = Self::mul_div(lp_redeem_amount, coin_reserve, total_supply)?; - - ensure!( - (sri_amount != 0) && (sri_amount >= sri_min_receive), - Error::::CoinOneWithdrawalDidNotMeetMinimum - ); - ensure!( - (coin_amount != 0) && (coin_amount >= coin_min_receive), - Error::::CoinTwoWithdrawalDidNotMeetMinimum - ); - let sri_reserve_left = sri_reserve.saturating_sub(sri_amount); - let coin_reserve_left = coin_reserve.saturating_sub(coin_amount); - - ensure!(sri_reserve_left >= 1, Error::::ReserveLeftLessThanMinimum); - ensure!(coin_reserve_left >= 1, Error::::ReserveLeftLessThanMinimum); - - // burn the provided lp token amount that includes the fee - LiquidityTokens::::burn( - origin, - Balance { coin: coin.into(), amount: Amount(lp_token_burn) }, - )?; - - Self::transfer( - &pool_account, - &withdraw_to, - Balance { coin: Coin::Serai, amount: Amount(sri_amount) }, - )?; - Self::transfer( - &pool_account, - &withdraw_to, - Balance { coin: coin.into(), amount: Amount(coin_amount) }, - )?; - - Self::deposit_event(Event::LiquidityRemoved { - who: sender, - withdraw_to, - pool_id, - coin_amount, - sri_amount, - lp_token_burned: lp_token_burn, - }); - - Ok(()) - } - - /// Swap the exact amount of `coin1` into `coin2`. - /// `amount_out_min` param allows you to specify the min amount of the `coin2` - /// you're happy to receive. - /// - /// [`DexApi::quote_price_exact_tokens_for_tokens`] runtime call can be called - /// for a quote. - #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::swap_exact_tokens_for_tokens())] - pub fn swap_exact_tokens_for_tokens( - origin: OriginFor, - path: BoundedVec, - amount_in: SubstrateAmount, - amount_out_min: SubstrateAmount, - send_to: T::AccountId, - ) -> DispatchResult { - let sender = ensure_signed(origin)?; - Self::do_swap_exact_tokens_for_tokens( - sender, - path, - amount_in, - Some(amount_out_min), - send_to, - )?; - Ok(()) - } - - /// Swap any amount of `coin1` to get the exact amount of `coin2`. - /// `amount_in_max` param allows to specify the max amount of the `coin1` - /// you're happy to provide. - /// - /// [`DexApi::quote_price_tokens_for_exact_tokens`] runtime call can be called - /// for a quote. - #[pallet::call_index(3)] - #[pallet::weight(T::WeightInfo::swap_tokens_for_exact_tokens())] - pub fn swap_tokens_for_exact_tokens( - origin: OriginFor, - path: BoundedVec, - amount_out: SubstrateAmount, - amount_in_max: SubstrateAmount, - send_to: T::AccountId, - ) -> DispatchResult { - let sender = ensure_signed(origin)?; - Self::do_swap_tokens_for_exact_tokens( - sender, - path, - amount_out, - Some(amount_in_max), - send_to, - )?; - Ok(()) - } - } - - impl Pallet { - /// Swap exactly `amount_in` of coin `path[0]` for coin `path[1]`. - /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire - /// the amount desired. - /// - /// Withdraws the `path[0]` coin from `sender`, deposits the `path[1]` coin to `send_to`. - /// - /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. - pub fn do_swap_exact_tokens_for_tokens( - sender: T::AccountId, - path: BoundedVec, - amount_in: SubstrateAmount, - amount_out_min: Option, - send_to: T::AccountId, - ) -> Result { - ensure!(amount_in > 0, Error::::ZeroAmount); - if let Some(amount_out_min) = amount_out_min { - ensure!(amount_out_min > 0, Error::::ZeroAmount); - } - - Self::validate_swap_path(&path)?; - - let amounts = Self::get_amounts_out(amount_in, &path)?; - let amount_out = - *amounts.last().defensive_ok_or("get_amounts_out() returned an empty result")?; - - if let Some(amount_out_min) = amount_out_min { - ensure!(amount_out >= amount_out_min, Error::::ProvidedMinimumNotSufficientForSwap); - } - - Self::do_swap(sender, &amounts, path, send_to)?; - Ok(amount_out) - } - - /// Take the `path[0]` coin and swap some amount for `amount_out` of the `path[1]`. If an - /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be - /// too costly. - /// - /// Withdraws `path[0]` coin from `sender`, deposits the `path[1]` coin to `send_to`, - /// - /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. - pub fn do_swap_tokens_for_exact_tokens( - sender: T::AccountId, - path: BoundedVec, - amount_out: SubstrateAmount, - amount_in_max: Option, - send_to: T::AccountId, - ) -> Result { - ensure!(amount_out > 0, Error::::ZeroAmount); - if let Some(amount_in_max) = amount_in_max { - ensure!(amount_in_max > 0, Error::::ZeroAmount); - } - - Self::validate_swap_path(&path)?; - - let amounts = Self::get_amounts_in(amount_out, &path)?; - let amount_in = - *amounts.first().defensive_ok_or("get_amounts_in() returned an empty result")?; - - if let Some(amount_in_max) = amount_in_max { - ensure!(amount_in <= amount_in_max, Error::::ProvidedMaximumNotSufficientForSwap); - } - - Self::do_swap(sender, &amounts, path, send_to)?; - Ok(amount_in) - } - - /// Transfer an `amount` of `coin_id`. - fn transfer( - from: &T::AccountId, - to: &T::AccountId, - balance: Balance, - ) -> Result { - CoinsPallet::::transfer_internal(*from, *to, balance)?; - Ok(balance.amount) - } - - /// Convert a `HigherPrecisionBalance` type to an `SubstrateAmount`. - pub(crate) fn convert_hpb_to_coin_balance( - amount: HigherPrecisionBalance, - ) -> Result> { - amount.try_into().map_err(|_| Error::::Overflow) - } - - /// Swap coins along a `path`, depositing in `send_to`. - pub(crate) fn do_swap( - sender: T::AccountId, - amounts: &[SubstrateAmount], - path: BoundedVec, - send_to: T::AccountId, - ) -> Result<(), DispatchError> { - ensure!(amounts.len() > 1, Error::::CorrespondenceError); - if let Some([coin1, coin2]) = &path.get(0 .. 2) { - let pool_id = Self::get_pool_id(*coin1, *coin2)?; - let pool_account = Self::get_pool_account(pool_id); - // amounts should always contain a corresponding element to path. - let first_amount = amounts.first().ok_or(Error::::CorrespondenceError)?; - - Self::transfer( - &sender, - &pool_account, - Balance { coin: *coin1, amount: Amount(*first_amount) }, - )?; - - let mut i = 0; - let path_len = u32::try_from(path.len()).unwrap(); - #[allow(clippy::explicit_counter_loop)] - for coins_pair in path.windows(2) { - if let [coin1, coin2] = coins_pair { - let pool_id = Self::get_pool_id(*coin1, *coin2)?; - let pool_account = Self::get_pool_account(pool_id); - - let amount_out = - amounts.get((i + 1) as usize).ok_or(Error::::CorrespondenceError)?; - - let to = if i < path_len - 2 { - let coin3 = path.get((i + 2) as usize).ok_or(Error::::PathError)?; - Self::get_pool_account(Self::get_pool_id(*coin2, *coin3)?) - } else { - send_to - }; - - let reserve = Self::get_balance(&pool_account, *coin2); - let reserve_left = reserve.saturating_sub(*amount_out); - ensure!(reserve_left >= 1, Error::::ReserveLeftLessThanMinimum); - - Self::transfer( - &pool_account, - &to, - Balance { coin: *coin2, amount: Amount(*amount_out) }, - )?; - - // update the volume - let swap_volume = if *coin1 == Coin::Serai { - amounts.get(i as usize).ok_or(Error::::CorrespondenceError)? - } else { - amount_out - }; - let existing = SwapVolume::::get(pool_id).unwrap_or(0); - let new_volume = existing.saturating_add(*swap_volume); - SwapVolume::::set(pool_id, Some(new_volume)); + let (sri_actual, external_coin_actual) = { + let (sri_optimal, external_coin_optimal) = ( + Premise::establish(Coin::from(external_coin), Coin::Serai) + .expect("ext, sri satisfies sri ^ sri") + .quote_for_in(reserves, external_coin_intended) + .map_err(Error::::from)?, + Premise::establish(Coin::Serai, Coin::from(external_coin)) + .expect("sri, ext satisfies sri ^ sri") + .quote_for_in(reserves, sri_intended) + .map_err(Error::::from)?, + ); + if sri_optimal < sri_intended { + if sri_optimal < sri_minimum { + Err(Error::::Unsatisfied)?; + } + (sri_optimal, external_coin_intended) + } else { + if external_coin_optimal < external_coin_minimum { + Err(Error::::Unsatisfied)?; + } + (sri_intended, external_coin_optimal) } - i += 1; - } + }; - Self::deposit_event(Event::SwapExecuted { - who: sender, - send_to, - path, - amount_in: *first_amount, - amount_out: *amounts.last().expect("Always has more than 1 element"), - }); - } else { - return Err(Error::::InvalidPath.into()); - } - Ok(()) - } + let liquidity = { + let supply = u128::from(supply); + let sri_liquidity = + u64::try_from((u128::from(sri_actual.0) * supply) / u128::from(reserves.sri.0)) + .map_err(|_| Error::::Overflow)?; + let external_coin_liquidity = u64::try_from( + (u128::from(external_coin_actual.0) * supply) / u128::from(reserves.external_coin.0), + ) + .map_err(|_| Error::::Overflow)?; + Amount(sri_liquidity.min(external_coin_liquidity)) + }; - /// The account ID of the pool. - /// - /// This actually does computation. If you need to keep using it, then make sure you cache - /// the value and only call this once. - pub fn get_pool_account(pool_id: PoolId) -> T::AccountId { - let encoded_pool_id = sp_io::hashing::blake2_256(&Encode::encode(&pool_id)[..]); - - Decode::decode(&mut TrailingZeroInput::new(encoded_pool_id.as_ref())) - .expect("infinite length input; no invalid inputs for type; qed") - } - - /// Get the `owner`'s balance of `coin`, which could be the chain's native coin or another - /// fungible. Returns a value in the form of an `Amount`. - fn get_balance(owner: &T::AccountId, coin: Coin) -> SubstrateAmount { - CoinsPallet::::balance(*owner, coin).0 - } - - /// Returns a pool id constructed from 2 coins. - /// We expect deterministic order, so (coin1, coin2) or (coin2, coin1) returns the same - /// result. Coins have to be different and one of them should be Coin::Serai. - pub fn get_pool_id(coin1: Coin, coin2: Coin) -> Result> { - ensure!((coin1 == Coin::Serai) || (coin2 == Coin::Serai), Error::::PoolNotFound); - ensure!(coin1 != coin2, Error::::EqualCoins); - ExternalCoin::try_from(coin1) - .or_else(|()| ExternalCoin::try_from(coin2)) - .map_err(|()| Error::::PoolNotFound) - } - - /// Returns the balance of each coin in the pool. - /// The tuple result is in the order requested (not necessarily the same as pool order). - pub fn get_reserves( - coin1: &Coin, - coin2: &Coin, - ) -> Result<(SubstrateAmount, SubstrateAmount), Error> { - let pool_id = Self::get_pool_id(*coin1, *coin2)?; - let pool_account = Self::get_pool_account(pool_id); - - let balance1 = Self::get_balance(&pool_account, *coin1); - let balance2 = Self::get_balance(&pool_account, *coin2); - - if (balance1 == 0) || (balance2 == 0) { - Err(Error::::PoolNotFound)?; - } - - Ok((balance1, balance2)) - } - - /// Leading to an amount at the end of a `path`, get the required amounts in. - pub(crate) fn get_amounts_in( - amount_out: SubstrateAmount, - path: &BoundedVec, - ) -> Result, DispatchError> { - let mut amounts: Vec = vec![amount_out]; - - for coins_pair in path.windows(2).rev() { - if let [coin1, coin2] = coins_pair { - let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2)?; - let prev_amount = amounts.last().expect("Always has at least one element"); - let amount_in = Self::get_amount_in(*prev_amount, reserve_in, reserve_out)?; - amounts.push(amount_in); - } - } - - amounts.reverse(); - Ok(amounts) - } - - /// Following an amount into a `path`, get the corresponding amounts out. - pub(crate) fn get_amounts_out( - amount_in: SubstrateAmount, - path: &BoundedVec, - ) -> Result, DispatchError> { - let mut amounts: Vec = vec![amount_in]; - - for coins_pair in path.windows(2) { - if let [coin1, coin2] = coins_pair { - let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2)?; - let prev_amount = amounts.last().expect("Always has at least one element"); - let amount_out = Self::get_amount_out(*prev_amount, reserve_in, reserve_out)?; - amounts.push(amount_out); - } - } - - Ok(amounts) - } - - fn get_swap_path_from_coins( - coin1: Coin, - coin2: Coin, - ) -> Option> { - if coin1 == coin2 { - return None; - } - - let path = if (coin1 == Coin::native() && coin2 != Coin::native()) || - (coin2 == Coin::native() && coin1 != Coin::native()) - { - vec![coin1, coin2] - } else { - vec![coin1, Coin::native(), coin2] + (sri_actual, external_coin_actual, liquidity) }; - Some(path.try_into().unwrap()) + Coins::::transfer_fn( + from, + pool.into(), + Balance { coin: Coin::Serai, amount: sri_actual }, + )?; + Coins::::transfer_fn( + from, + pool.into(), + Balance { coin: Coin::from(external_coin), amount: external_coin_actual }, + )?; + LiquidityTokens::::mint( + from, + Balance { coin: Coin::from(external_coin), amount: liquidity }, + )?; + + // TODO: Event + + Ok(()) } - /// Used by the RPC service to provide current prices. - pub fn quote_price_exact_tokens_for_tokens( - coin1: Coin, - coin2: Coin, - amount: SubstrateAmount, - include_fee: bool, - ) -> Option { - let path = Self::get_swap_path_from_coins(coin1, coin2)?; + /// Transfer these liquidity tokens to the specified address. + #[pallet::call_index(1)] + #[pallet::weight((0, DispatchClass::Normal))] // TODO + pub fn transfer_liquidity( + origin: OriginFor, + to: SeraiAddress, + liquidity_tokens: ExternalBalance, + ) -> DispatchResult { + let from = ensure_signed(origin)?; + LiquidityTokens::::transfer_fn(from, to.into(), liquidity_tokens.into())?; - let mut amounts: Vec = vec![amount]; - for coins_pair in path.windows(2) { - if let [coin1, coin2] = coins_pair { - let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2).ok()?; - let prev_amount = amounts.last().expect("Always has at least one element"); - let amount_out = if include_fee { - Self::get_amount_out(*prev_amount, reserve_in, reserve_out).ok()? - } else { - Self::quote(*prev_amount, reserve_in, reserve_out).ok()? - }; + // TODO: Event - amounts.push(amount_out); + Ok(()) + } + + /// Remove liquidity. + #[pallet::call_index(2)] + #[pallet::weight((0, DispatchClass::Normal))] // TODO + pub fn remove_liquidity( + origin: OriginFor, + liquidity_tokens: ExternalBalance, + sri_minimum: Amount, + external_coin_minimum: Amount, + ) -> DispatchResult { + let from = ensure_signed(origin)?; + + let external_coin = liquidity_tokens.coin; + let pool = serai_abi::dex::address(external_coin); + let supply = LiquidityTokens::::supply(Coin::from(external_coin)).0; + if supply.saturating_sub(liquidity_tokens.amount.0) < MINIMUM_LIQUIDITY { + Err(Error::::InvalidLiquidity)?; + } + let supply = u128::from(supply); + + let reserves = Reserves { + sri: Coins::::balance(pool, Coin::Serai), + external_coin: Coins::::balance(pool, Coin::from(external_coin)), + }; + let sri_amount = + (u128::from(liquidity_tokens.amount.0) * u128::from(reserves.sri.0)) / supply; + let sri_amount = Amount(u64::try_from(sri_amount).map_err(|_| Error::::Overflow)?); + let external_coin_amount = + (u128::from(liquidity_tokens.amount.0) * u128::from(reserves.external_coin.0)) / supply; + let external_coin_amount = + Amount(u64::try_from(external_coin_amount).map_err(|_| Error::::Overflow)?); + if (sri_amount < sri_minimum) || (external_coin_amount < external_coin_minimum) { + Err(Error::::Unsatisfied)?; + } + + LiquidityTokens::::burn_fn(from, liquidity_tokens.into())?; + Coins::::transfer_fn( + from, + pool.into(), + Balance { coin: Coin::Serai, amount: sri_amount }, + )?; + Coins::::transfer_fn( + from, + pool.into(), + Balance { coin: Coin::from(external_coin), amount: external_coin_amount }, + )?; + + // TODO: Event + + Ok(()) + } + + /// Swap an exact amount of coins. + #[pallet::call_index(3)] + #[pallet::weight((0, DispatchClass::Normal))] // TODO + pub fn swap( + origin: OriginFor, + coins_to_swap: Balance, + minimum_to_receive: Balance, + ) -> DispatchResult { + let origin = SeraiAddress::from(ensure_signed(origin)?); + + let mut transfer_from = origin; + let mut next_amount = coins_to_swap.amount; + + let swaps = Premise::route(coins_to_swap.coin, minimum_to_receive.coin) + .ok_or(Error::::FromToSelf)?; + for swap in &swaps { + let external_coin = swap.external_coin(); + let pool = serai_abi::dex::address(external_coin); + + // Fetch the pool's reserves + let reserves = Reserves { + sri: Coins::::balance(pool, Coin::Serai), + external_coin: Coins::::balance(pool, Coin::from(external_coin)), + }; + if (reserves.sri == Amount(0)) || (reserves.external_coin == Amount(0)) { + Err(Error::::InvalidLiquidity)?; } + + // Transfer from the prior (the originating account or pool) to the current pool + /* + This is impossible to reach, yet would cause the amount _yet to be transferred out_ to + be credited both as part of the reserves _and_ the amount in if violated. + */ + assert!(transfer_from != pool, "swap routed from a coin to itself"); + Coins::::transfer_fn( + transfer_from.into(), + pool.into(), + Balance { coin: swap.r#in(), amount: next_amount }, + )?; + + // Update the current status + transfer_from = pool; + next_amount = swap.quote_for_in(reserves, next_amount).map_err(Error::::from)?; } - Some(*amounts.last().unwrap()) - } - - /// Used by the RPC service to provide current prices. - pub fn quote_price_tokens_for_exact_tokens( - coin1: Coin, - coin2: Coin, - amount: SubstrateAmount, - include_fee: bool, - ) -> Option { - let path = Self::get_swap_path_from_coins(coin1, coin2)?; - - let mut amounts: Vec = vec![amount]; - for coins_pair in path.windows(2).rev() { - if let [coin1, coin2] = coins_pair { - let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2).ok()?; - let prev_amount = amounts.last().expect("Always has at least one element"); - let amount_in = if include_fee { - Self::get_amount_in(*prev_amount, reserve_in, reserve_out).ok()? - } else { - Self::quote(*prev_amount, reserve_out, reserve_in).ok()? - }; - amounts.push(amount_in); - } + // Check the amount meets the expectation + if next_amount.0 < minimum_to_receive.amount.0 { + Err(Error::::Unsatisfied)?; } - Some(*amounts.last().unwrap()) + // Transfer the resulting coins to the origin + Coins::::transfer_fn( + transfer_from.into(), + origin.into(), + Balance { coin: minimum_to_receive.coin, amount: next_amount }, + )?; + + // TODO: Event + + Ok(()) } - /// Calculates the optimal amount from the reserves. - pub fn quote( - amount: SubstrateAmount, - reserve1: SubstrateAmount, - reserve2: SubstrateAmount, - ) -> Result> { - // amount * reserve2 / reserve1 - Self::mul_div(amount, reserve2, reserve1) - } + /// Swap for an exact amount of coins. + #[pallet::call_index(4)] + #[pallet::weight((0, DispatchClass::Normal))] // TODO + pub fn swap_for( + origin: OriginFor, + coins_to_receive: Balance, + maximum_to_swap: Balance, + ) -> DispatchResult { + let origin = SeraiAddress::from(ensure_signed(origin)?); - pub(super) fn calc_lp_amount_for_zero_supply( - amount1: SubstrateAmount, - amount2: SubstrateAmount, - ) -> Result> { - let amount1 = HigherPrecisionBalance::from(amount1); - let amount2 = HigherPrecisionBalance::from(amount2); + let mut transfer_to = origin; + let mut next_amount = coins_to_receive.amount; - let result = amount1 - .checked_mul(amount2) - .ok_or(Error::::Overflow)? - .integer_sqrt() - .checked_sub(T::MintMinLiquidity::get().into()) - .ok_or(Error::::InsufficientLiquidityMinted)?; + let swaps = Premise::route(maximum_to_swap.coin, coins_to_receive.coin) + .ok_or(Error::::FromToSelf)?; + let mut i = swaps.len(); + while { + i -= 1; + let swap = swaps[i]; - result.try_into().map_err(|_| Error::::Overflow) - } + let external_coin = swap.external_coin(); + let pool = serai_abi::dex::address(external_coin); - fn mul_div( - a: SubstrateAmount, - b: SubstrateAmount, - c: SubstrateAmount, - ) -> Result> { - let a = HigherPrecisionBalance::from(a); - let b = HigherPrecisionBalance::from(b); - let c = HigherPrecisionBalance::from(c); + // Fetch the pool's reserves + let reserves = Reserves { + sri: Coins::::balance(pool, Coin::Serai), + external_coin: Coins::::balance(pool, Coin::from(external_coin)), + }; - let result = - a.checked_mul(b).ok_or(Error::::Overflow)?.checked_div(c).ok_or(Error::::Overflow)?; + /* + Transfer the output requested. - result.try_into().map_err(|_| Error::::Overflow) - } + While this violates the traditional Checks-Effects-Interactions pattern, in a way + favoring the caller, this function is within a `transactional` layer + (due to being a call) making all its DB operations only persisted upon success. + */ + /* + This is impossible to reach, yet ensures the amount not yet transferred in isn't + excluded when determining the reserves on the next iteration. + */ + assert!(transfer_to != pool, "swap routed to a coin from itself"); + Coins::::transfer_fn( + pool.into(), + transfer_to.into(), + Balance { coin: swap.out(), amount: next_amount }, + )?; - /// Calculates amount out. - /// - /// Given an input amount of an coin and pair reserves, returns the maximum output amount - /// of the other coin. - pub fn get_amount_out( - amount_in: SubstrateAmount, - reserve_in: SubstrateAmount, - reserve_out: SubstrateAmount, - ) -> Result> { - let amount_in = HigherPrecisionBalance::from(amount_in); - let reserve_in = HigherPrecisionBalance::from(reserve_in); - let reserve_out = HigherPrecisionBalance::from(reserve_out); + transfer_to = pool; + next_amount = swap.quote_for_out(reserves, next_amount).map_err(Error::::from)?; - if (reserve_in == 0) || (reserve_out == 0) { - return Err(Error::::ZeroLiquidity); + i != 0 + } {} + + // Check the amount meets the expectation + if next_amount.0 > maximum_to_swap.amount.0 { + Err(Error::::Unsatisfied)?; } - let amount_in_with_fee = amount_in - .checked_mul( - HigherPrecisionBalance::from(1000u32) - HigherPrecisionBalance::from(T::LPFee::get()), - ) - .ok_or(Error::::Overflow)?; + // Transfer the necessary coins from the origin + Coins::::transfer_fn( + origin.into(), + transfer_to.into(), + Balance { coin: maximum_to_swap.coin, amount: next_amount }, + )?; - let numerator = amount_in_with_fee.checked_mul(reserve_out).ok_or(Error::::Overflow)?; + // TODO: Event - let denominator = reserve_in - .checked_mul(1000u32.into()) - .ok_or(Error::::Overflow)? - .checked_add(amount_in_with_fee) - .ok_or(Error::::Overflow)?; - - let result = numerator.checked_div(denominator).ok_or(Error::::Overflow)?; - - result.try_into().map_err(|_| Error::::Overflow) - } - - /// Calculates amount in. - /// - /// Given an output amount of an coin and pair reserves, returns a required input amount - /// of the other coin. - pub fn get_amount_in( - amount_out: SubstrateAmount, - reserve_in: SubstrateAmount, - reserve_out: SubstrateAmount, - ) -> Result> { - let amount_out = HigherPrecisionBalance::from(amount_out); - let reserve_in = HigherPrecisionBalance::from(reserve_in); - let reserve_out = HigherPrecisionBalance::from(reserve_out); - - if (reserve_in == 0) || (reserve_out == 0) { - Err(Error::::ZeroLiquidity)? - } - - if amount_out >= reserve_out { - Err(Error::::AmountOutTooHigh)? - } - - let numerator = reserve_in - .checked_mul(amount_out) - .ok_or(Error::::Overflow)? - .checked_mul(1000u32.into()) - .ok_or(Error::::Overflow)?; - - let denominator = reserve_out - .checked_sub(amount_out) - .ok_or(Error::::Overflow)? - .checked_mul( - HigherPrecisionBalance::from(1000u32) - HigherPrecisionBalance::from(T::LPFee::get()), - ) - .ok_or(Error::::Overflow)?; - - let result = numerator - .checked_div(denominator) - .ok_or(Error::::Overflow)? - .checked_add(1) - .ok_or(Error::::Overflow)?; - - result.try_into().map_err(|_| Error::::Overflow) - } - - /// Ensure that a path is valid. - fn validate_swap_path( - path: &BoundedVec, - ) -> Result<(), DispatchError> { - ensure!(path.len() >= 2, Error::::InvalidPath); - - // validate all the pools in the path are unique - let mut pools = BoundedBTreeSet::::new(); - for coins_pair in path.windows(2) { - if let [coin1, coin2] = coins_pair { - let pool_id = Self::get_pool_id(*coin1, *coin2)?; - let new_element = pools.try_insert(pool_id).map_err(|_| Error::::Overflow)?; - if !new_element { - return Err(Error::::NonUniquePath.into()); - } - } - } Ok(()) } } } -impl Swap for Pallet { - fn swap_exact_tokens_for_tokens( - sender: T::AccountId, - path: Vec, - amount_in: HigherPrecisionBalance, - amount_out_min: Option, - send_to: T::AccountId, - ) -> Result { - let path = path.try_into().map_err(|_| Error::::PathError)?; - let amount_out_min = amount_out_min.map(Self::convert_hpb_to_coin_balance).transpose()?; - let amount_out = Self::do_swap_exact_tokens_for_tokens( - sender, - path, - Self::convert_hpb_to_coin_balance(amount_in)?, - amount_out_min, - send_to, - )?; - Ok(amount_out.into()) - } - - fn swap_tokens_for_exact_tokens( - sender: T::AccountId, - path: Vec, - amount_out: HigherPrecisionBalance, - amount_in_max: Option, - send_to: T::AccountId, - ) -> Result { - let path = path.try_into().map_err(|_| Error::::PathError)?; - let amount_in_max = amount_in_max.map(Self::convert_hpb_to_coin_balance).transpose()?; - let amount_in = Self::do_swap_tokens_for_exact_tokens( - sender, - path, - Self::convert_hpb_to_coin_balance(amount_out)?, - amount_in_max, - send_to, - )?; - Ok(amount_in.into()) - } -} - -sp_api::decl_runtime_apis! { - /// This runtime api allows people to query the size of the liquidity pools - /// and quote prices for swaps. - pub trait DexApi { - /// Provides a quote for [`Pallet::swap_tokens_for_exact_tokens`]. - /// - /// Note that the price may have changed by the time the transaction is executed. - /// (Use `amount_in_max` to control slippage.) - fn quote_price_tokens_for_exact_tokens( - coin1: Coin, - coin2: Coin, - amount: SubstrateAmount, - include_fee: bool - ) -> Option; - - /// Provides a quote for [`Pallet::swap_exact_tokens_for_tokens`]. - /// - /// Note that the price may have changed by the time the transaction is executed. - /// (Use `amount_out_min` to control slippage.) - fn quote_price_exact_tokens_for_tokens( - coin1: Coin, - coin2: Coin, - amount: SubstrateAmount, - include_fee: bool - ) -> Option; - - /// Returns the size of the liquidity pool for the given coin pair. - fn get_reserves(coin1: Coin, coin2: Coin) -> Option<(SubstrateAmount, SubstrateAmount)>; - } -} - -sp_core::generate_feature_enabled_macro!( - runtime_benchmarks_enabled, - feature = "runtime-benchmarks", - $ -); +pub use pallet::*; diff --git a/substrate/dex/src/mock.rs b/substrate/dex/src/mock.rs index 666c0324..03a76144 100644 --- a/substrate/dex/src/mock.rs +++ b/substrate/dex/src/mock.rs @@ -1,126 +1,62 @@ -// This file was originally: +//! Test environment for the DEX pallet. -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 +use borsh::BorshDeserialize; -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +use frame_support::{sp_runtime::BuildStorage, derive_impl, construct_runtime}; -// It has been forked into a crate distributed under the AGPL 3.0. -// Please check the current distribution for up-to-date copyright and licensing information. +use serai_coins_pallet::{CoinsInstance, LiquidityTokensInstance}; -//! Test environment for Dex pallet. - -use super::*; use crate as dex; -use frame_support::{ - construct_runtime, - traits::{ConstU16, ConstU32, ConstU64}, -}; - -use sp_core::{H256, sr25519::Public}; -use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, -}; - -use serai_primitives::{Coin, Balance, Amount, system_address}; - -pub use coins_pallet as coins; - -type Block = frame_system::mocking::MockBlock; - -pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10; - construct_runtime!( pub enum Test { System: frame_system, - CoinsPallet: coins, - LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp, + Core: serai_core_pallet, + Coins: serai_coins_pallet::, + LiquidityTokens: serai_coins_pallet::, Dex: dex, } ); +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = Public; - type Lookup = IdentityLookup; - type Block = Block; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type DbWeight = (); - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; + type AccountId = sp_core::sr25519::Public; + type Lookup = frame_support::sp_runtime::traits::IdentityLookup; + type Block = frame_system::mocking::MockBlock; } -impl coins::Config for Test { - type RuntimeEvent = RuntimeEvent; - type AllowMint = (); +#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] +impl pallet_timestamp::Config for Test {} + +impl serai_core_pallet::Config for Test {} + +impl serai_coins_pallet::Config for Test { + type AllowMint = serai_coins_pallet::AlwaysAllowMint; } -impl coins::Config for Test { - type RuntimeEvent = RuntimeEvent; - type AllowMint = (); +impl serai_coins_pallet::Config for Test { + type AllowMint = serai_coins_pallet::AlwaysAllowMint; } -impl Config for Test { - type RuntimeEvent = RuntimeEvent; - - type WeightInfo = (); - type LPFee = ConstU32<3>; // means 0.3% - type MaxSwapPathLength = ConstU32<4>; - - type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; - - // 100 is good enough when the main currency has 12 decimals. - type MintMinLiquidity = ConstU64<100>; -} +impl crate::Config for Test {} pub(crate) fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); - let accounts: Vec = vec![ - system_address(b"account1").into(), - system_address(b"account2").into(), - system_address(b"account3").into(), - system_address(b"account4").into(), - ]; - coins::GenesisConfig:: { - accounts: accounts - .into_iter() - .map(|a| (a, Balance { coin: Coin::Serai, amount: Amount(1 << 60) })) - .collect(), - _ignore: Default::default(), + serai_coins_pallet::GenesisConfig:: { + accounts: vec![], + _instance: Default::default(), } - .assimilate_storage(&mut t) + .assimilate_storage(&mut storage) + .unwrap(); + serai_coins_pallet::GenesisConfig:: { + accounts: vec![], + _instance: Default::default(), + } + .assimilate_storage(&mut storage) .unwrap(); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext + storage.into() } diff --git a/substrate/dex/src/tests.rs b/substrate/dex/src/tests.rs deleted file mode 100644 index 1a23a7a2..00000000 --- a/substrate/dex/src/tests.rs +++ /dev/null @@ -1,1389 +0,0 @@ -// This file was originally: - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// It has been forked into a crate distributed under the AGPL 3.0. -// Please check the current distribution for up-to-date copyright and licensing information. - -use crate::{ - mock::{*, MEDIAN_PRICE_WINDOW_LENGTH}, - *, -}; -use frame_support::{assert_noop, assert_ok}; - -pub use coins_pallet as coins; - -use coins::Pallet as CoinsPallet; - -use serai_primitives::{Balance, COINS, PublicKey, system_address, Amount}; - -type LiquidityTokens = coins_pallet::Pallet; -type LiquidityTokensError = coins_pallet::Error; - -fn events() -> Vec> { - let result = System::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| if let mock::RuntimeEvent::Dex(inner) = e { Some(inner) } else { None }) - .collect(); - - System::reset_events(); - - result -} - -fn pools() -> Vec { - let mut s: Vec<_> = Pools::::iter().map(|x| x.0).collect(); - s.sort(); - s -} - -fn coins() -> Vec { - COINS.to_vec() -} - -fn balance(owner: PublicKey, coin: Coin) -> u64 { - CoinsPallet::::balance(owner, coin).0 -} - -fn pool_balance(owner: PublicKey, token_id: Coin) -> u64 { - LiquidityTokens::::balance(owner, token_id).0 -} - -macro_rules! bvec { - ($( $x:tt )*) => { - vec![$( $x )*].try_into().unwrap() - } -} - -#[test] -fn check_pool_accounts_dont_collide() { - use std::collections::HashSet; - let mut map = HashSet::new(); - - for coin in coins() { - if let Coin::External(c) = coin { - let account = Dex::get_pool_account(c); - if map.contains(&account) { - panic!("Collision at {c:?}"); - } - map.insert(account); - } - } -} - -#[test] -fn check_max_numbers() { - new_test_ext().execute_with(|| { - assert_eq!(Dex::quote(3u64, u64::MAX, u64::MAX).ok().unwrap(), 3); - assert!(Dex::quote(u64::MAX, 3u64, u64::MAX).is_err()); - assert_eq!(Dex::quote(u64::MAX, u64::MAX, 1u64).ok().unwrap(), 1); - - assert_eq!(Dex::get_amount_out(100u64, u64::MAX, u64::MAX).ok().unwrap(), 99); - assert_eq!(Dex::get_amount_in(100u64, u64::MAX, u64::MAX).ok().unwrap(), 101); - }); -} - -#[test] -fn can_create_pool() { - new_test_ext().execute_with(|| { - let coin_account_deposit: u64 = 0; - let user: PublicKey = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Monero); - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(1000) })); - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_eq!(balance(user, coin1), 1000 - coin_account_deposit); - - assert_eq!( - events(), - [Event::::PoolCreated { pool_id, pool_account: Dex::get_pool_account(pool_id) }] - ); - assert_eq!(pools(), vec![pool_id]); - }); -} - -#[test] -fn create_same_pool_twice_should_fail() { - new_test_ext().execute_with(|| { - let coin = ExternalCoin::Dai; - assert_ok!(Dex::create_pool(coin)); - assert_noop!(Dex::create_pool(coin), Error::::PoolExists); - }); -} - -#[test] -fn different_pools_should_have_different_lp_tokens() { - new_test_ext().execute_with(|| { - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Bitcoin); - let coin3 = Coin::External(ExternalCoin::Ether); - let pool_id_1_2 = Dex::get_pool_id(coin1, coin2).unwrap(); - let pool_id_1_3 = Dex::get_pool_id(coin1, coin3).unwrap(); - - let lp_token2_1 = coin2; - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - let lp_token3_1 = coin3; - - assert_eq!( - events(), - [Event::::PoolCreated { - pool_id: pool_id_1_2, - pool_account: Dex::get_pool_account(pool_id_1_2), - }] - ); - - assert_ok!(Dex::create_pool(coin3.try_into().unwrap())); - assert_eq!( - events(), - [Event::::PoolCreated { - pool_id: pool_id_1_3, - pool_account: Dex::get_pool_account(pool_id_1_3), - }] - ); - - assert_ne!(lp_token2_1, lp_token3_1); - }); -} - -#[test] -fn can_add_liquidity() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Dai); - let coin3 = Coin::External(ExternalCoin::Monero); - - let lp_token1 = coin2; - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - let lp_token2 = coin3; - assert_ok!(Dex::create_pool(coin3.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint( - user, - Balance { coin: coin1, amount: Amount(10000 * 2 + 1) } - )); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin3, amount: Amount(1000) })); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 10, - 10000, - 10, - 10000, - user, - )); - - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - assert!(events().contains(&Event::::LiquidityAdded { - who: user, - mint_to: user, - pool_id, - sri_amount: 10000, - coin_amount: 10, - lp_token_minted: 216, - })); - let pallet_account = Dex::get_pool_account(pool_id); - assert_eq!(balance(pallet_account, coin1), 10000); - assert_eq!(balance(pallet_account, coin2), 10); - assert_eq!(balance(user, coin1), 10000 + 1); - assert_eq!(balance(user, coin2), 1000 - 10); - assert_eq!(pool_balance(user, lp_token1), 216); - - // try to pass the non-native - native coins, the result should be the same - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin3.try_into().unwrap(), - 10, - 10000, - 10, - 10000, - user, - )); - - let pool_id = Dex::get_pool_id(coin1, coin3).unwrap(); - assert!(events().contains(&Event::::LiquidityAdded { - who: user, - mint_to: user, - pool_id, - sri_amount: 10000, - coin_amount: 10, - lp_token_minted: 216, - })); - let pallet_account = Dex::get_pool_account(pool_id); - assert_eq!(balance(pallet_account, coin1), 10000); - assert_eq!(balance(pallet_account, coin3), 10); - assert_eq!(balance(user, coin1), 1); - assert_eq!(balance(user, coin3), 1000 - 10); - assert_eq!(pool_balance(user, lp_token2), 216); - }); -} - -#[test] -fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = ExternalCoin::Bitcoin; - - assert_ok!(Dex::create_pool(coin2)); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(1000) })); - assert_ok!(CoinsPallet::::mint( - user, - Balance { coin: coin2.into(), amount: Amount(1000) } - )); - - assert_noop!( - Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 1, 1, 1, 1, user), - Error::::InsufficientLiquidityMinted - ); - }); -} - -#[test] -fn add_tiny_liquidity_directly_to_pool_address() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Ether); - let coin3 = Coin::External(ExternalCoin::Dai); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - assert_ok!(Dex::create_pool(coin3.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(10000 * 2) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin3, amount: Amount(10000) })); - - // check we're still able to add the liquidity even when the pool already has some coin1 - let pallet_account = Dex::get_pool_account(Dex::get_pool_id(coin1, coin2).unwrap()); - assert_ok!(CoinsPallet::::mint( - pallet_account, - Balance { coin: coin1, amount: Amount(1000) } - )); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 10, - 10000, - 10, - 10000, - user, - )); - - // check the same but for coin3 (non-native token) - let pallet_account = Dex::get_pool_account(Dex::get_pool_id(coin1, coin3).unwrap()); - assert_ok!(CoinsPallet::::mint( - pallet_account, - Balance { coin: coin2, amount: Amount(1) } - )); - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin3.try_into().unwrap(), - 10, - 10000, - 10, - 10000, - user, - )); - }); -} - -#[test] -fn can_remove_liquidity() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Monero); - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - - let lp_token = coin2; - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint( - user, - Balance { coin: coin1, amount: Amount(10000000000) } - )); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(100000) })); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 100000, - 1000000000, - 100000, - 1000000000, - user, - )); - - let total_lp_received = pool_balance(user, lp_token); - - assert_ok!(Dex::remove_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - total_lp_received, - 0, - 0, - user, - )); - - assert!(events().contains(&Event::::LiquidityRemoved { - who: user, - withdraw_to: user, - pool_id, - sri_amount: 999990000, - coin_amount: 99999, - lp_token_burned: total_lp_received, - })); - - let pool_account = Dex::get_pool_account(pool_id); - assert_eq!(balance(pool_account, coin1), 10000); - assert_eq!(balance(pool_account, coin2), 1); - assert_eq!(pool_balance(pool_account, lp_token), 100); - - assert_eq!(balance(user, coin1), 10000000000 - 1000000000 + 999990000); - assert_eq!(balance(user, coin2), 99999); - assert_eq!(pool_balance(user, lp_token), 0); - }); -} - -#[test] -fn can_not_redeem_more_lp_tokens_than_were_minted() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Dai); - let lp_token = coin2; - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 10, - 10000, - 10, - 10000, - user, - )); - - // Only 216 lp_tokens_minted - assert_eq!(pool_balance(user, lp_token), 216); - - assert_noop!( - Dex::remove_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 216 + 1, // Try and redeem 10 lp tokens while only 9 minted. - 0, - 0, - user, - ), - LiquidityTokensError::::NotEnoughCoins - ); - }); -} - -#[test] -fn can_quote_price() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Ether); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(100000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 200, - 10000, - 1, - 1, - user, - )); - - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, 3000, false,), - Some(60) - ); - // including fee so should get less out... - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, 3000, true,), - Some(46) - ); - // Check it still gives same price: - // (if the above accidentally exchanged then it would not give same quote as before) - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, 3000, false,), - Some(60) - ); - // including fee so should get less out... - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, 3000, true,), - Some(46) - ); - - // Check inverse: - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(coin2, Coin::native(), 60, false,), - Some(3000) - ); - // including fee so should get less out... - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(coin2, Coin::native(), 60, true,), - Some(2302) - ); - - // - // same tests as above but for quote_price_tokens_for_exact_tokens: - // - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(Coin::native(), coin2, 60, false,), - Some(3000) - ); - // including fee so should need to put more in... - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(Coin::native(), coin2, 60, true,), - Some(4299) - ); - // Check it still gives same price: - // (if the above accidentally exchanged then it would not give same quote as before) - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(Coin::native(), coin2, 60, false,), - Some(3000) - ); - // including fee so should need to put more in... - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(Coin::native(), coin2, 60, true,), - Some(4299) - ); - - // Check inverse: - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(coin2, Coin::native(), 3000, false,), - Some(60) - ); - // including fee so should need to put more in... - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(coin2, Coin::native(), 3000, true,), - Some(86) - ); - - // - // roundtrip: Without fees one should get the original number - // - let amount_in = 100; - - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(coin2, Coin::native(), amount_in, false,).and_then( - |amount| Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, amount, false,) - ), - Some(amount_in) - ); - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, amount_in, false,).and_then( - |amount| Dex::quote_price_exact_tokens_for_tokens(coin2, Coin::native(), amount, false,) - ), - Some(amount_in) - ); - - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(coin2, Coin::native(), amount_in, false,).and_then( - |amount| Dex::quote_price_tokens_for_exact_tokens(Coin::native(), coin2, amount, false,) - ), - Some(amount_in) - ); - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(Coin::native(), coin2, amount_in, false,).and_then( - |amount| Dex::quote_price_tokens_for_exact_tokens(coin2, Coin::native(), amount, false,) - ), - Some(amount_in) - ); - }); -} - -#[test] -fn quote_price_exact_tokens_for_tokens_matches_execution() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let user2 = system_address(b"user2").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Bitcoin); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(100000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 200, - 10000, - 1, - 1, - user, - )); - - let amount = 1; - let quoted_price = 49; - assert_eq!( - Dex::quote_price_exact_tokens_for_tokens(coin2, coin1, amount, true,), - Some(quoted_price) - ); - - assert_ok!(CoinsPallet::::mint(user2, Balance { coin: coin2, amount: Amount(amount) })); - let prior_sri_balance = 0; - assert_eq!(prior_sri_balance, balance(user2, coin1)); - assert_ok!(Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user2), - bvec![coin2, coin1], - amount, - 1, - user2, - )); - - assert_eq!(prior_sri_balance + quoted_price, balance(user2, coin1)); - }); -} - -#[test] -fn quote_price_tokens_for_exact_tokens_matches_execution() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let user2 = system_address(b"user2").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Monero); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(100000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 200, - 10000, - 1, - 1, - user, - )); - - let amount = 49; - let quoted_price = 1; - assert_eq!( - Dex::quote_price_tokens_for_exact_tokens(coin2, coin1, amount, true,), - Some(quoted_price) - ); - - assert_ok!(CoinsPallet::::mint(user2, Balance { coin: coin2, amount: Amount(amount) })); - let prior_sri_balance = 0; - assert_eq!(prior_sri_balance, balance(user2, coin1)); - let prior_coin_balance = 49; - assert_eq!(prior_coin_balance, balance(user2, coin2)); - assert_ok!(Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user2), - bvec![coin2, coin1], - amount, - 1, - user2, - )); - - assert_eq!(prior_sri_balance + amount, balance(user2, coin1)); - assert_eq!(prior_coin_balance - quoted_price, balance(user2, coin2)); - }); -} - -#[test] -fn can_swap_with_native() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Ether); - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - let liquidity1 = 10000; - let liquidity2 = 200; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - - let input_amount = 100; - let expect_receive = Dex::get_amount_out(input_amount, liquidity2, liquidity1).ok().unwrap(); - - assert_ok!(Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1], - input_amount, - 1, - user, - )); - - let pallet_account = Dex::get_pool_account(pool_id); - assert_eq!(balance(user, coin1), expect_receive); - assert_eq!(balance(user, coin2), 1000 - liquidity2 - input_amount); - assert_eq!(balance(pallet_account, coin1), liquidity1 - expect_receive); - assert_eq!(balance(pallet_account, coin2), liquidity2 + input_amount); - }); -} - -#[test] -fn can_swap_with_realistic_values() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let sri = Coin::native(); - let dai = Coin::External(ExternalCoin::Dai); - assert_ok!(Dex::create_pool(dai.try_into().unwrap())); - - const UNIT: u64 = 1_000_000_000; - - assert_ok!(CoinsPallet::::mint( - user, - Balance { coin: sri, amount: Amount(300_000 * UNIT) } - )); - assert_ok!(CoinsPallet::::mint( - user, - Balance { coin: dai, amount: Amount(1_100_000 * UNIT) } - )); - - let liquidity_sri = 200_000 * UNIT; // ratio for a 5$ price - let liquidity_dai = 1_000_000 * UNIT; - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - dai.try_into().unwrap(), - liquidity_dai, - liquidity_sri, - 1, - 1, - user, - )); - - let input_amount = 10 * UNIT; // dai - - assert_ok!(Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![dai, sri], - input_amount, - 1, - user, - )); - - assert!(events().contains(&Event::::SwapExecuted { - who: user, - send_to: user, - path: bvec![dai, sri], - amount_in: 10 * UNIT, // usd - amount_out: 1_993_980_120, // About 2 dot after div by UNIT. - })); - }); -} - -#[test] -fn can_not_swap_in_pool_with_no_liquidity_added_yet() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Monero); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - // Check can't swap an empty pool - assert_noop!( - Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1], - 10, - 1, - user, - ), - Error::::PoolNotFound - ); - }); -} - -#[test] -fn check_no_panic_when_try_swap_close_to_empty_pool() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Bitcoin); - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - let lp_token = coin2; - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - let liquidity1 = 10000; - let liquidity2 = 200; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - - let lp_token_minted = pool_balance(user, lp_token); - assert!(events().contains(&Event::::LiquidityAdded { - who: user, - mint_to: user, - pool_id, - sri_amount: liquidity1, - coin_amount: liquidity2, - lp_token_minted, - })); - - let pallet_account = Dex::get_pool_account(pool_id); - assert_eq!(balance(pallet_account, coin1), liquidity1); - assert_eq!(balance(pallet_account, coin2), liquidity2); - - assert_ok!(Dex::remove_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - lp_token_minted, - 1, - 1, - user, - )); - - // Now, the pool should exist but be almost empty. - // Let's try and drain it. - assert_eq!(balance(pallet_account, coin1), 708); - assert_eq!(balance(pallet_account, coin2), 15); - - // validate the reserve should always stay above the ED - // Following test fail again due to the force on ED being > 1. - // assert_noop!( - // Dex::swap_tokens_for_exact_tokens( - // RuntimeOrigin::signed(user), - // bvec![coin2, coin1], - // 708 - ed + 1, // amount_out - // 500, // amount_in_max - // user, - // ), - // Error::::ReserveLeftLessThanMinimum - // ); - - assert_ok!(Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1], - 608, // amount_out - 500, // amount_in_max - user, - )); - - let token_1_left = balance(pallet_account, coin1); - let token_2_left = balance(pallet_account, coin2); - assert_eq!(token_1_left, 708 - 608); - - // The price for the last tokens should be very high - assert_eq!( - Dex::get_amount_in(token_1_left - 1, token_2_left, token_1_left).ok().unwrap(), - 10625 - ); - - assert_noop!( - Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1], - token_1_left - 1, // amount_out - 1000, // amount_in_max - user, - ), - Error::::ProvidedMaximumNotSufficientForSwap - ); - - // Try to swap what's left in the pool - assert_noop!( - Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1], - token_1_left, // amount_out - 1000, // amount_in_max - user, - ), - Error::::AmountOutTooHigh - ); - }); -} - -#[test] -fn swap_should_not_work_if_too_much_slippage() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Ether); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - let liquidity1 = 10000; - let liquidity2 = 200; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - - let exchange_amount = 100; - - assert_noop!( - Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1], - exchange_amount, // amount_in - 4000, // amount_out_min - user, - ), - Error::::ProvidedMinimumNotSufficientForSwap - ); - }); -} - -#[test] -fn can_swap_tokens_for_exact_tokens() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Dai); - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(20000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - let pallet_account = Dex::get_pool_account(pool_id); - let before1 = balance(pallet_account, coin1) + balance(user, coin1); - let before2 = balance(pallet_account, coin2) + balance(user, coin2); - - let liquidity1 = 10000; - let liquidity2 = 200; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - - let exchange_out = 50; - let expect_in = Dex::get_amount_in(exchange_out, liquidity1, liquidity2).ok().unwrap(); - - assert_ok!(Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin1, coin2], - exchange_out, // amount_out - 3500, // amount_in_max - user, - )); - - assert_eq!(balance(user, coin1), 10000 - expect_in); - assert_eq!(balance(user, coin2), 1000 - liquidity2 + exchange_out); - assert_eq!(balance(pallet_account, coin1), liquidity1 + expect_in); - assert_eq!(balance(pallet_account, coin2), liquidity2 - exchange_out); - - // check invariants: - - // native and coin totals should be preserved. - assert_eq!(before1, balance(pallet_account, coin1) + balance(user, coin1)); - assert_eq!(before2, balance(pallet_account, coin2) + balance(user, coin2)); - }); -} - -#[test] -fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let user2 = system_address(b"user2").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Monero); - let pool_id = Dex::get_pool_id(coin1, coin2).unwrap(); - let lp_token = coin2; - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - let base1 = 10000; - let base2 = 1000; - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(base1) })); - assert_ok!(CoinsPallet::::mint(user2, Balance { coin: coin1, amount: Amount(base1) })); - assert_ok!(CoinsPallet::::mint(user2, Balance { coin: coin2, amount: Amount(base2) })); - - let pallet_account = Dex::get_pool_account(pool_id); - let before1 = balance(pallet_account, coin1) + balance(user, coin1) + balance(user2, coin1); - let before2 = balance(pallet_account, coin2) + balance(user, coin2) + balance(user2, coin2); - - let liquidity1 = 10000; - let liquidity2 = 200; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user2), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user2, - )); - - assert_eq!(balance(user, coin1), base1); - assert_eq!(balance(user, coin2), 0); - - let exchange_out = 50; - let expect_in = Dex::get_amount_in(exchange_out, liquidity1, liquidity2).ok().unwrap(); - - assert_ok!(Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin1, coin2], - exchange_out, // amount_out - 3500, // amount_in_max - user, - )); - - assert_eq!(balance(user, coin1), base1 - expect_in); - assert_eq!(balance(pallet_account, coin1), liquidity1 + expect_in); - assert_eq!(balance(user, coin2), exchange_out); - assert_eq!(balance(pallet_account, coin2), liquidity2 - exchange_out); - - // check invariants: - - // native and coin totals should be preserved. - assert_eq!( - before1, - balance(pallet_account, coin1) + balance(user, coin1) + balance(user2, coin1) - ); - assert_eq!( - before2, - balance(pallet_account, coin2) + balance(user, coin2) + balance(user2, coin2) - ); - - let lp_token_minted = pool_balance(user2, lp_token); - assert_eq!(lp_token_minted, 1314); - - assert_ok!(Dex::remove_liquidity( - RuntimeOrigin::signed(user2), - coin2.try_into().unwrap(), - lp_token_minted, - 0, - 0, - user2, - )); - }); -} - -#[test] -fn swap_tokens_for_exact_tokens_should_not_work_if_too_much_slippage() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Ether); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(20000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(1000) })); - - let liquidity1 = 10000; - let liquidity2 = 200; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - - let exchange_out = 1; - - assert_noop!( - Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin1, coin2], - exchange_out, // amount_out - 50, // amount_in_max just greater than slippage. - user, - ), - Error::::ProvidedMaximumNotSufficientForSwap - ); - }); -} - -#[test] -fn swap_exact_tokens_for_tokens_in_multi_hops() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Dai); - let coin3 = Coin::External(ExternalCoin::Monero); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - assert_ok!(Dex::create_pool(coin3.try_into().unwrap())); - - let base1 = 10000; - let base2 = 10000; - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(base1 * 2) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(base2) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin3, amount: Amount(base2) })); - - let liquidity1 = 10000; - let liquidity2 = 200; - let liquidity3 = 2000; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin3.try_into().unwrap(), - liquidity3, - liquidity1, - 1, - 1, - user, - )); - - let input_amount = 500; - let expect_out2 = Dex::get_amount_out(input_amount, liquidity2, liquidity1).ok().unwrap(); - let expect_out3 = Dex::get_amount_out(expect_out2, liquidity1, liquidity3).ok().unwrap(); - - assert_noop!( - Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin1], - input_amount, - 80, - user, - ), - Error::::InvalidPath - ); - - assert_noop!( - Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1, coin2], - input_amount, - 80, - user, - ), - Error::::NonUniquePath - ); - - assert_ok!(Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1, coin3], - input_amount, // amount_in - 80, // amount_out_min - user, - )); - - let pool_id1 = Dex::get_pool_id(coin1, coin2).unwrap(); - let pool_id2 = Dex::get_pool_id(coin1, coin3).unwrap(); - let pallet_account1 = Dex::get_pool_account(pool_id1); - let pallet_account2 = Dex::get_pool_account(pool_id2); - - assert_eq!(balance(user, coin2), base2 - liquidity2 - input_amount); - assert_eq!(balance(pallet_account1, coin2), liquidity2 + input_amount); - assert_eq!(balance(pallet_account1, coin1), liquidity1 - expect_out2); - assert_eq!(balance(pallet_account2, coin1), liquidity1 + expect_out2); - assert_eq!(balance(pallet_account2, coin3), liquidity3 - expect_out3); - assert_eq!(balance(user, coin3), 10000 - liquidity3 + expect_out3); - }); -} - -#[test] -fn swap_tokens_for_exact_tokens_in_multi_hops() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Bitcoin); - let coin3 = Coin::External(ExternalCoin::Ether); - - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - assert_ok!(Dex::create_pool(coin3.try_into().unwrap())); - - let base1 = 10000; - let base2 = 10000; - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(base1 * 2) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(base2) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin3, amount: Amount(base2) })); - - let liquidity1 = 10000; - let liquidity2 = 200; - let liquidity3 = 2000; - - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - liquidity2, - liquidity1, - 1, - 1, - user, - )); - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin3.try_into().unwrap(), - liquidity3, - liquidity1, - 1, - 1, - user, - )); - - let exchange_out3 = 100; - let expect_in2 = Dex::get_amount_in(exchange_out3, liquidity1, liquidity3).ok().unwrap(); - let expect_in1 = Dex::get_amount_in(expect_in2, liquidity2, liquidity1).ok().unwrap(); - - assert_ok!(Dex::swap_tokens_for_exact_tokens( - RuntimeOrigin::signed(user), - bvec![coin2, coin1, coin3], - exchange_out3, // amount_out - 1000, // amount_in_max - user, - )); - - let pool_id1 = Dex::get_pool_id(coin1, coin2).unwrap(); - let pool_id2 = Dex::get_pool_id(coin1, coin3).unwrap(); - let pallet_account1 = Dex::get_pool_account(pool_id1); - let pallet_account2 = Dex::get_pool_account(pool_id2); - - assert_eq!(balance(user, coin2), base2 - liquidity2 - expect_in1); - assert_eq!(balance(pallet_account1, coin1), liquidity1 - expect_in2); - assert_eq!(balance(pallet_account1, coin2), liquidity2 + expect_in1); - assert_eq!(balance(pallet_account2, coin1), liquidity1 + expect_in2); - assert_eq!(balance(pallet_account2, coin3), liquidity3 - exchange_out3); - assert_eq!(balance(user, coin3), 10000 - liquidity3 + exchange_out3); - }); -} - -#[test] -fn can_not_swap_same_coin() { - new_test_ext().execute_with(|| { - let user = system_address(b"user1").into(); - let coin1 = Coin::External(ExternalCoin::Dai); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(1000) })); - - let exchange_amount = 10; - assert_noop!( - Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![coin1, coin1], - exchange_amount, - 1, - user, - ), - Error::::PoolNotFound - ); - - assert_noop!( - Dex::swap_exact_tokens_for_tokens( - RuntimeOrigin::signed(user), - bvec![Coin::native(), Coin::native()], - exchange_amount, - 1, - user, - ), - Error::::EqualCoins - ); - }); -} - -#[test] -fn validate_pool_id_sorting() { - new_test_ext().execute_with(|| { - // Serai < Bitcoin < Ether < Dai < Monero. - // coin1 <= coin2 for this test to pass. - let native = Coin::native(); - let coin1 = Coin::External(ExternalCoin::Bitcoin); - let coin2 = Coin::External(ExternalCoin::Monero); - assert_eq!(Dex::get_pool_id(native, coin2).unwrap(), coin2.try_into().unwrap()); - assert_eq!(Dex::get_pool_id(coin2, native).unwrap(), coin2.try_into().unwrap()); - assert!(matches!(Dex::get_pool_id(native, native), Err(Error::::EqualCoins))); - assert!(matches!(Dex::get_pool_id(coin2, coin1), Err(Error::::PoolNotFound))); - assert!(coin2 > coin1); - assert!(coin1 <= coin1); - assert_eq!(coin1, coin1); - assert!(native < coin1); - }); -} - -#[test] -fn cannot_block_pool_creation() { - new_test_ext().execute_with(|| { - // User 1 is the pool creator - let user = system_address(b"user1").into(); - // User 2 is the attacker - let attacker = system_address(b"attacker").into(); - - assert_ok!(CoinsPallet::::mint( - attacker, - Balance { coin: Coin::native(), amount: Amount(10000) } - )); - - // The target pool the user wants to create is Native <=> Coin(2) - let coin1 = Coin::native(); - let coin2 = Coin::External(ExternalCoin::Ether); - - // Attacker computes the still non-existing pool account for the target pair - let pool_account = Dex::get_pool_account(Dex::get_pool_id(coin2, coin1).unwrap()); - // And transfers 1 to that pool account - assert_ok!(CoinsPallet::::transfer_internal( - attacker, - pool_account, - Balance { coin: Coin::native(), amount: Amount(1) } - )); - // Then, the attacker creates 14 tokens and sends one of each to the pool account - // skip the coin1 and coin2 coins. - for coin in coins().into_iter().filter(|c| (*c != coin1) && (*c != coin2)) { - assert_ok!(CoinsPallet::::mint(attacker, Balance { coin, amount: Amount(1000) })); - assert_ok!(CoinsPallet::::transfer_internal( - attacker, - pool_account, - Balance { coin, amount: Amount(1) } - )); - } - - // User can still create the pool - assert_ok!(Dex::create_pool(coin2.try_into().unwrap())); - - // User has to transfer one Coin(2) token to the pool account (otherwise add_liquidity will - // fail with `CoinTwoDepositDidNotMeetMinimum`), also transfer native token for the same error. - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin1, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::mint(user, Balance { coin: coin2, amount: Amount(10000) })); - assert_ok!(CoinsPallet::::transfer_internal( - user, - pool_account, - Balance { coin: coin2, amount: Amount(1) } - )); - assert_ok!(CoinsPallet::::transfer_internal( - user, - pool_account, - Balance { coin: coin1, amount: Amount(100) } - )); - - // add_liquidity shouldn't fail because of the number of consumers - assert_ok!(Dex::add_liquidity( - RuntimeOrigin::signed(user), - coin2.try_into().unwrap(), - 100, - 9900, - 10, - 9900, - user, - )); - }); -} - -#[test] -fn test_median_price() { - new_test_ext().execute_with(|| { - use rand_core::{RngCore, OsRng}; - - let mut prices = vec![]; - for i in 0 .. 100 { - // Randomly use an active number - if (i != 0) && (OsRng.next_u64() % u64::from(MEDIAN_PRICE_WINDOW_LENGTH / 3) == 0) { - let old_index = usize::try_from( - OsRng.next_u64() % - u64::from(MEDIAN_PRICE_WINDOW_LENGTH) % - u64::try_from(prices.len()).unwrap(), - ) - .unwrap(); - let window_base = prices.len().saturating_sub(MEDIAN_PRICE_WINDOW_LENGTH.into()); - prices.push(prices[window_base + old_index]); - } else { - prices.push(OsRng.next_u64()); - } - } - let coin = ExternalCoin::Bitcoin; - - assert!(prices.len() >= (2 * usize::from(MEDIAN_PRICE_WINDOW_LENGTH))); - for i in 0 .. prices.len() { - let price = Amount(prices[i]); - - let n = BlockNumberFor::::from(u32::try_from(i).unwrap()); - SpotPriceForBlock::::set(n, coin, Some(price)); - Dex::insert_into_median(coin, price); - if SpotPricesLength::::get(coin).unwrap() > MEDIAN_PRICE_WINDOW_LENGTH { - let old = n - u64::from(MEDIAN_PRICE_WINDOW_LENGTH); - let old_price = SpotPriceForBlock::::get(old, coin).unwrap(); - SpotPriceForBlock::::remove(old, coin); - Dex::remove_from_median(coin, old_price); - } - - // get the current window (cloning so our sort doesn't affect the original array) - let window_base = (i + 1).saturating_sub(MEDIAN_PRICE_WINDOW_LENGTH.into()); - let mut window = Vec::from(&prices[window_base ..= i]); - assert!(window.len() <= MEDIAN_PRICE_WINDOW_LENGTH.into()); - - // get the median - window.sort(); - let median_index = window.len() / 2; - assert_eq!(Dex::median_price(coin).unwrap(), Amount(window[median_index])); - } - }); -} diff --git a/substrate/dex/src/types.rs b/substrate/dex/src/types.rs deleted file mode 100644 index 818f7567..00000000 --- a/substrate/dex/src/types.rs +++ /dev/null @@ -1,54 +0,0 @@ -// This file was originally: - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// It has been forked into a crate distributed under the AGPL 3.0. -// Please check the current distribution for up-to-date copyright and licensing information. - -use super::*; - -/// Trait for providing methods to swap between the various coin classes. -pub trait Swap { - /// Swap exactly `amount_in` of coin `path[0]` for coin `path[1]`. - /// If an `amount_out_min` is specified, it will return an error if it is unable to acquire - /// the amount desired. - /// - /// Withdraws the `path[0]` coin from `sender`, deposits the `path[1]` coin to `send_to`, - /// - /// If successful, returns the amount of `path[1]` acquired for the `amount_in`. - fn swap_exact_tokens_for_tokens( - sender: AccountId, - path: Vec, - amount_in: Balance, - amount_out_min: Option, - send_to: AccountId, - ) -> Result; - - /// Take the `path[0]` coin and swap some amount for `amount_out` of the `path[1]`. If an - /// `amount_in_max` is specified, it will return an error if acquiring `amount_out` would be - /// too costly. - /// - /// Withdraws `path[0]` coin from `sender`, deposits `path[1]` coin to `send_to`, - /// - /// If successful returns the amount of the `path[0]` taken to provide `path[1]`. - fn swap_tokens_for_exact_tokens( - sender: AccountId, - path: Vec, - amount_out: Balance, - amount_in_max: Option, - send_to: AccountId, - ) -> Result; -} diff --git a/substrate/dex/src/weights.rs b/substrate/dex/src/weights.rs deleted file mode 100644 index e32f3069..00000000 --- a/substrate/dex/src/weights.rs +++ /dev/null @@ -1,259 +0,0 @@ -// This file was originally: - -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// It has been forked into a crate distributed under the AGPL 3.0. -// Please check the current distribution for up-to-date copyright and licensing information. - -//! Autogenerated weights for Dex Pallet. -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-07-18, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `runner-gghbxkbs-project-145-concurrent-0`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` -//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` - -// Executed Command: -// target/production/substrate -// benchmark -// pallet -// --steps=50 -// --repeat=20 -// --extrinsic=* -// --wasm-execution=compiled -// --heap-pages=4096 -// --json-file=/builds/parity/mirrors/substrate/.git/.artifacts/bench.json -// --pallet=serai_dex_pallet -// --chain=dev -// --header=./HEADER-APACHE2 -// --output=./substrate/dex/pallet/src/weights.rs -// --template=./.maintain/frame-weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for Dex Pallet. -pub trait WeightInfo { - fn create_pool() -> Weight; - fn add_liquidity() -> Weight; - fn remove_liquidity() -> Weight; - fn swap_exact_tokens_for_tokens() -> Weight; - fn swap_tokens_for_exact_tokens() -> Weight; -} - -/// Weights for Dex Pallet using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `DexPallet::Pools` (r:1 w:1) - /// Proof: `DexPallet::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:1 w:1) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:1 w:1) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `DexPallet::NextPoolCoinId` (r:1 w:1) - /// Proof: `DexPallet::NextPoolCoinId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Coin` (r:1 w:1) - /// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Account` (r:1 w:1) - /// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn create_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `729` - // Estimated: `6196` - // Minimum execution time: 131_688_000 picoseconds. - Weight::from_parts(134_092_000, 6196) - .saturating_add(T::DbWeight::get().reads(8_u64)) - .saturating_add(T::DbWeight::get().writes(8_u64)) - } - /// Storage: `DexPallet::Pools` (r:1 w:0) - /// Proof: `DexPallet::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:1 w:1) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:2 w:2) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Coin` (r:1 w:1) - /// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Account` (r:2 w:2) - /// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn add_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `1382` - // Estimated: `6208` - // Minimum execution time: 157_310_000 picoseconds. - Weight::from_parts(161_547_000, 6208) - .saturating_add(T::DbWeight::get().reads(8_u64)) - .saturating_add(T::DbWeight::get().writes(7_u64)) - } - /// Storage: `DexPallet::Pools` (r:1 w:0) - /// Proof: `DexPallet::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:1 w:1) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:2 w:2) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Coin` (r:1 w:1) - /// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Account` (r:1 w:1) - /// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn remove_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `1371` - // Estimated: `6208` - // Minimum execution time: 142_769_000 picoseconds. - Weight::from_parts(145_139_000, 6208) - .saturating_add(T::DbWeight::get().reads(7_u64)) - .saturating_add(T::DbWeight::get().writes(6_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:3 w:3) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:6 w:6) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn swap_exact_tokens_for_tokens() -> Weight { - // Proof Size summary in bytes: - // Measured: `1738` - // Estimated: `16644` - // Minimum execution time: 213_186_000 picoseconds. - Weight::from_parts(217_471_000, 16644) - .saturating_add(T::DbWeight::get().reads(10_u64)) - .saturating_add(T::DbWeight::get().writes(10_u64)) - } - /// Storage: `Coins::Coin` (r:3 w:3) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:6 w:6) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn swap_tokens_for_exact_tokens() -> Weight { - // Proof Size summary in bytes: - // Measured: `1738` - // Estimated: `16644` - // Minimum execution time: 213_793_000 picoseconds. - Weight::from_parts(218_584_000, 16644) - .saturating_add(T::DbWeight::get().reads(10_u64)) - .saturating_add(T::DbWeight::get().writes(10_u64)) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `DexPallet::Pools` (r:1 w:1) - /// Proof: `DexPallet::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:2 w:2) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:1 w:1) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:1 w:1) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `DexPallet::NextPoolCoinId` (r:1 w:1) - /// Proof: `DexPallet::NextPoolCoinId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Coin` (r:1 w:1) - /// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Account` (r:1 w:1) - /// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn create_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `729` - // Estimated: `6196` - // Minimum execution time: 131_688_000 picoseconds. - Weight::from_parts(134_092_000, 6196) - .saturating_add(RocksDbWeight::get().reads(8_u64)) - .saturating_add(RocksDbWeight::get().writes(8_u64)) - } - /// Storage: `DexPallet::Pools` (r:1 w:0) - /// Proof: `DexPallet::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:1 w:1) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:2 w:2) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Coin` (r:1 w:1) - /// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Account` (r:2 w:2) - /// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn add_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `1382` - // Estimated: `6208` - // Minimum execution time: 157_310_000 picoseconds. - Weight::from_parts(161_547_000, 6208) - .saturating_add(RocksDbWeight::get().reads(8_u64)) - .saturating_add(RocksDbWeight::get().writes(7_u64)) - } - /// Storage: `DexPallet::Pools` (r:1 w:0) - /// Proof: `DexPallet::Pools` (`max_values`: None, `max_size`: Some(30), added: 2505, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:1 w:1) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:2 w:2) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Coin` (r:1 w:1) - /// Proof: `PoolCoins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `PoolCoins::Account` (r:1 w:1) - /// Proof: `PoolCoins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn remove_liquidity() -> Weight { - // Proof Size summary in bytes: - // Measured: `1371` - // Estimated: `6208` - // Minimum execution time: 142_769_000 picoseconds. - Weight::from_parts(145_139_000, 6208) - .saturating_add(RocksDbWeight::get().reads(7_u64)) - .saturating_add(RocksDbWeight::get().writes(6_u64)) - } - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `Coins::Coin` (r:3 w:3) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:6 w:6) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - fn swap_exact_tokens_for_tokens() -> Weight { - // Proof Size summary in bytes: - // Measured: `1738` - // Estimated: `16644` - // Minimum execution time: 213_186_000 picoseconds. - Weight::from_parts(217_471_000, 16644) - .saturating_add(RocksDbWeight::get().reads(10_u64)) - .saturating_add(RocksDbWeight::get().writes(10_u64)) - } - /// Storage: `Coins::Coin` (r:3 w:3) - /// Proof: `Coins::Coin` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) - /// Storage: `Coins::Account` (r:6 w:6) - /// Proof: `Coins::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn swap_tokens_for_exact_tokens() -> Weight { - // Proof Size summary in bytes: - // Measured: `1738` - // Estimated: `16644` - // Minimum execution time: 213_793_000 picoseconds. - Weight::from_parts(218_584_000, 16644) - .saturating_add(RocksDbWeight::get().reads(10_u64)) - .saturating_add(RocksDbWeight::get().writes(10_u64)) - } -} diff --git a/substrate/median/src/lexicographic.rs b/substrate/median/src/lexicographic.rs index 228ba0a1..3eb37749 100644 --- a/substrate/median/src/lexicographic.rs +++ b/substrate/median/src/lexicographic.rs @@ -1,4 +1,5 @@ use core::cmp::Ord; +use alloc::vec::Vec; use scale::FullCodec; diff --git a/substrate/median/src/lib.rs b/substrate/median/src/lib.rs index ae026a78..4d041f6d 100644 --- a/substrate/median/src/lib.rs +++ b/substrate/median/src/lib.rs @@ -1,10 +1,14 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] #![deny(missing_docs)] use core::cmp::Ordering; +extern crate alloc; +#[cfg(feature = "std")] +extern crate std; + use scale::{EncodeLike, FullCodec}; use frame_support::storage::*; diff --git a/substrate/primitives/src/address.rs b/substrate/primitives/src/address.rs index f54f9925..3a58dd75 100644 --- a/substrate/primitives/src/address.rs +++ b/substrate/primitives/src/address.rs @@ -43,8 +43,8 @@ impl SeraiAddress { /// logarithm for a point whose representation has a known Blake2b-256 preimage. // The alternative would be to massage this until its not a valid point, which isn't worth the // computational expense as this should be a hard problem for outputs which happen to be points. - pub fn system(label: &[u8]) -> Self { - Self(sp_core::blake2_256(label)) + pub fn system(label: impl AsRef<[u8]>) -> Self { + Self(sp_core::blake2_256(label.as_ref())) } } diff --git a/substrate/primitives/src/dex.rs b/substrate/primitives/src/dex.rs new file mode 100644 index 00000000..adbfcf1c --- /dev/null +++ b/substrate/primitives/src/dex.rs @@ -0,0 +1,184 @@ +use alloc::{vec, vec::Vec}; + +use crate::{ + coin::{ExternalCoin, Coin}, + balance::Amount, +}; + +/// An error incurred with the DEX. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Error { + /// An arithmetic overflow occurred. + Overflow, + /// An arithmetic underflow occured. + Underflow, + /// The `x * y = k` invariant was violated. + KInvariant, +} + +/// The premise of a swap. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Premise { + /// A swap to SRI. + ToSerai { + /// The coin swapped from. + from: ExternalCoin, + }, + /// A swap from SRI. + FromSerai { + /// The coin swapped to. + to: ExternalCoin, + }, +} + +impl Premise { + /// Establish the premise of a swap. + /// + /// This will return `None` if not exactly one coin is `Coin::Serai`. + pub fn establish(coin_in: Coin, coin_out: Coin) -> Option { + if !((coin_in == Coin::Serai) ^ (coin_out == Coin::Serai)) { + None?; + } + + Some(match coin_in { + Coin::Serai => match coin_out { + Coin::Serai => unreachable!("prior checked exactly one was `Coin::Serai`"), + Coin::External(coin) => Premise::FromSerai { to: coin }, + }, + Coin::External(coin) => Premise::ToSerai { from: coin }, + }) + } + + /// Establish the route for a swap. + /// + /// This will return `None` if the coin from is the coin to. + pub fn route(coin_in: Coin, coin_out: Coin) -> Option> { + if coin_in == coin_out { + None?; + } + Some(if (coin_in == Coin::Serai) ^ (coin_out == Coin::Serai) { + vec![Self::establish(coin_in, coin_out).expect("sri ^ sri")] + } else { + // Since they aren't both `Coin::Serai`, and not just one is, neither are + vec![ + Self::establish(coin_in, Coin::Serai).expect("sri ^ sri #1"), + Self::establish(Coin::Serai, coin_out).expect("sri ^ sri #2"), + ] + }) + } + + /// Fetch the coin _in_ for the swap. + pub fn r#in(self) -> Coin { + match self { + Premise::ToSerai { from } => from.into(), + Premise::FromSerai { .. } => Coin::Serai, + } + } + + /// Fetch the coin _out_ from the swap. + pub fn out(self) -> Coin { + match self { + Premise::ToSerai { .. } => Coin::Serai, + Premise::FromSerai { to } => to.into(), + } + } + + /// Fetch the external coin present within the swap. + pub fn external_coin(self) -> ExternalCoin { + match self { + Premise::ToSerai { from: external_coin } | Premise::FromSerai { to: external_coin } => { + external_coin + } + } + } +} + +/// The reserves for a liquidity pool. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Reserves { + /// The amount of SRI already present. + pub sri: Amount, + /// The amount of the external coin already present. + pub external_coin: Amount, +} + +impl Reserves { + /// The product of two amounts. + /// + /// This is intended to be used as the famous `x * y = k` formula's implementation. + fn product(x: u64, y: u64) -> u128 { + u128::from(x) * u128::from(y) + } + + /// Decompose this into `(in_reserve, out_reserve)` given the direction for a swap. + fn as_in_out(self, premise: Premise) -> (u64, u64) { + match premise { + Premise::ToSerai { .. } => (self.external_coin.0, self.sri.0), + Premise::FromSerai { .. } => (self.sri.0, self.external_coin.0), + } + } +} + +impl Premise { + /// Validate the following swap may be performed. + /// + /// Validation of the amounts occur via the `x * y = k` formula popularized with Uniswap V2, + /// itself licensed under the GPL. + /// + /// For more information, please see the following links: + /// + /// + /// + /// + fn validate( + self, + reserves: Reserves, + amount_in: Amount, + amount_out: Amount, + ) -> Result<(), Error> { + let (in_reserve, out_reserve) = reserves.as_in_out(self); + let current_k = Reserves::product(in_reserve, out_reserve); + let proposed_k = Reserves::product( + in_reserve.checked_add(amount_in.0).ok_or(Error::Overflow)?, + out_reserve.checked_sub(amount_out.0).ok_or(Error::Underflow)?, + ); + if proposed_k < current_k { + Err(Error::KInvariant)?; + } + Ok(()) + } + + /// Quote a swap for an amount in. + pub fn quote_for_in(self, reserves: Reserves, amount_in: Amount) -> Result { + let (in_reserve, out_reserve) = reserves.as_in_out(self); + let current_k = Reserves::product(in_reserve, out_reserve); + let proposed_in_reserve = u128::from(in_reserve) + u128::from(amount_in.0); + let required_proposed_out_reserve = current_k.div_ceil(proposed_in_reserve); + // If this does not fit in a `u64`, the following substraction would have underflowed + let required_proposed_out_reserve = + u64::try_from(required_proposed_out_reserve).map_err(|_| Error::Underflow)?; + let amount_out = + Amount(out_reserve.checked_sub(required_proposed_out_reserve).ok_or(Error::Underflow)?); + + // Ensure this passes validation using a consistent, traditionally presented function + self.validate(reserves, amount_in, amount_out)?; + + Ok(amount_out) + } + + /// Quote a swap for an amount out. + pub fn quote_for_out(self, reserves: Reserves, amount_out: Amount) -> Result { + let (in_reserve, out_reserve) = reserves.as_in_out(self); + let current_k = Reserves::product(in_reserve, out_reserve); + let proposed_out_reserve = out_reserve.checked_sub(amount_out.0).ok_or(Error::Underflow)?; + let required_proposed_in_reserve = current_k.div_ceil(u128::from(proposed_out_reserve)); + let required_proposed_in_reserve = + u64::try_from(required_proposed_in_reserve).map_err(|_| Error::Overflow)?; + let amount_in = + Amount(required_proposed_in_reserve.checked_sub(in_reserve).ok_or(Error::Overflow)?); + + self.validate(reserves, amount_in, amount_out)?; + + Ok(amount_in) + } +} diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index a792c7ea..30b1b87f 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -38,6 +38,9 @@ pub mod network_id; /// Types for identifying and working with validator sets. pub mod validator_sets; +/// Types for the DEX. +pub mod dex; + /// Types for signaling. pub mod signals; diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index bf6eacea..6c2097b7 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -53,6 +53,7 @@ serai-core-pallet = { path = "../core", default-features = false } serai-coins-pallet = { path = "../coins", default-features = false } serai-validator-sets-pallet = { path = "../validator-sets", default-features = false } serai-signals-pallet = { path = "../signals", default-features = false } +serai-dex-pallet = { path = "../dex", default-features = false } [build-dependencies] substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "8c36534bb0bd5a02979f94bb913d11d55fe7eadc" } @@ -88,6 +89,7 @@ std = [ "serai-coins-pallet/std", "serai-validator-sets-pallet/std", "serai-signals-pallet/std", + "serai-dex-pallet/std", ] try-runtime = [ @@ -107,6 +109,7 @@ try-runtime = [ "serai-coins-pallet/try-runtime", "serai-validator-sets-pallet/try-runtime", "serai-signals-pallet/try-runtime", + "serai-dex-pallet/try-runtime", ] runtime-benchmarks = [ @@ -123,6 +126,7 @@ runtime-benchmarks = [ "serai-coins-pallet/runtime-benchmarks", "serai-validator-sets-pallet/runtime-benchmarks", "serai-signals-pallet/runtime-benchmarks", + "serai-dex-pallet/runtime-benchmarks", ] default = ["std"] diff --git a/substrate/runtime/src/wasm/mod.rs b/substrate/runtime/src/wasm/mod.rs index 9d5bcf4b..ff86b4fc 100644 --- a/substrate/runtime/src/wasm/mod.rs +++ b/substrate/runtime/src/wasm/mod.rs @@ -93,6 +93,9 @@ mod runtime { #[runtime::pallet_index(5)] pub type LiquidityTokens = serai_coins_pallet::Pallet; + #[runtime::pallet_index(6)] + pub type Dex = serai_dex_pallet::Pallet; + #[runtime::pallet_index(0xfd)] #[runtime::disable_inherent] pub type Timestamp = pallet_timestamp::Pallet; @@ -171,6 +174,7 @@ impl serai_signals_pallet::Config for Runtime { impl serai_coins_pallet::Config for Runtime { type AllowMint = serai_coins_pallet::AlwaysAllowMint; } +impl serai_dex_pallet::Config for Runtime {} impl pallet_timestamp::Config for Runtime { type Moment = u64; @@ -292,16 +296,35 @@ impl From for RuntimeCall { serai_abi::Call::Dex(call) => { use serai_abi::dex::Call; match call { + Call::add_liquidity { + external_coin, + sri_intended, + external_coin_intended, + sri_minimum, + external_coin_minimum, + } => RuntimeCall::Dex(serai_dex_pallet::Call::add_liquidity { + external_coin, + sri_intended, + external_coin_intended, + sri_minimum, + external_coin_minimum, + }), Call::transfer_liquidity { to, liquidity_tokens } => { - RuntimeCall::LiquidityTokens(serai_coins_pallet::Call::transfer { - to: to.into(), - coins: liquidity_tokens.into(), + RuntimeCall::Dex(serai_dex_pallet::Call::transfer_liquidity { to, liquidity_tokens }) + } + Call::remove_liquidity { liquidity_tokens, sri_minimum, external_coin_minimum } => { + RuntimeCall::Dex(serai_dex_pallet::Call::remove_liquidity { + liquidity_tokens, + sri_minimum, + external_coin_minimum, }) } - Call::add_liquidity { .. } | - Call::remove_liquidity { .. } | - Call::swap_exact { .. } | - Call::swap_for_exact { .. } => todo!("TODO"), + Call::swap { coins_to_swap, minimum_to_receive } => { + RuntimeCall::Dex(serai_dex_pallet::Call::swap { coins_to_swap, minimum_to_receive }) + } + Call::swap_for { coins_to_receive, maximum_to_swap } => { + RuntimeCall::Dex(serai_dex_pallet::Call::swap_for { coins_to_receive, maximum_to_swap }) + } } } serai_abi::Call::GenesisLiquidity(call) => {