diff --git a/Cargo.lock b/Cargo.lock index fd7e43308..145e78ef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,10 +232,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c775f0d12169cba7aae4caeb547bb6a50781c7449a8aa53793827c9ec4abf488" dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-r1cs-std", + "ark-std 0.5.0", ] [[package]] @@ -244,10 +256,10 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" dependencies = [ - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "hashbrown 0.13.2", "itertools 0.10.5", @@ -255,6 +267,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash 0.8.11", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.2", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + [[package]] name = "ark-ed-on-bls12-381-bandersnatch" version = "0.4.0" @@ -262,9 +295,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cde0f2aa063a2a5c28d39b47761aa102bda7c13c84fc118a61b87c7b2f785c" dependencies = [ "ark-bls12-381", - "ark-ec", - "ark-ff", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-std 0.4.0", ] [[package]] @@ -273,17 +306,37 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" dependencies = [ - "ark-ff-asm", - "ark-ff-macros", - "ark-serialize", - "ark-std", + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "digest 0.10.7", "itertools 0.10.5", "num-bigint", "num-traits", "paste", - "rustc_version 0.4.0", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec 0.7.4", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", "zeroize", ] @@ -297,6 +350,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.79", +] + [[package]] name = "ark-ff-macros" version = "0.4.2" @@ -310,29 +373,86 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "ark-poly" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" dependencies = [ - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "hashbrown 0.13.2", ] +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash 0.8.11", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.2", +] + +[[package]] +name = "ark-r1cs-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-relations", + "ark-std 0.5.0", + "educe", + "num-bigint", + "num-integer", + "num-traits", + "tracing", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff 0.5.0", + "ark-std 0.5.0", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ark-scale" version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b08346a3e38e2be792ef53ee168623c9244d968ff00cd70fb9932f6fe36393" dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "parity-scale-codec", ] @@ -342,10 +462,10 @@ version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f69c00b3b529be29528a6f2fd5fa7b1790f8bed81b9cdca17e326538545a179" dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "parity-scale-codec", "scale-info", ] @@ -355,10 +475,10 @@ name = "ark-secret-scalar" version = "0.0.2" source = "git+https://github.com/w3f/ring-vrf?rev=3119f51#3119f51b54b69308abfb0671f6176cb125ae1bf1" dependencies = [ - "ark-ec", - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "ark-transcript", "digest 0.10.7", "rand_core 0.6.4", @@ -371,8 +491,21 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ - "ark-serialize-derive", - "ark-std", + "ark-serialize-derive 0.4.2", + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive 0.5.0", + "ark-std 0.5.0", + "arrayvec 0.7.4", "digest 0.10.7", "num-bigint", ] @@ -388,6 +521,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "ark-std" version = "0.4.0" @@ -398,14 +542,24 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "ark-transcript" version = "0.0.2" source = "git+https://github.com/w3f/ring-vrf?rev=3119f51#3119f51b54b69308abfb0671f6176cb125ae1bf1" dependencies = [ - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "digest 0.10.7", "rand_core 0.6.4", "sha3", @@ -633,7 +787,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -668,7 +822,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -740,12 +894,12 @@ version = "0.0.1" source = "git+https://github.com/w3f/ring-vrf?rev=3119f51#3119f51b54b69308abfb0671f6176cb125ae1bf1" dependencies = [ "ark-bls12-381", - "ark-ec", + "ark-ec 0.4.2", "ark-ed-on-bls12-381-bandersnatch", - "ark-ff", + "ark-ff 0.4.2", "ark-scale 0.0.12", - "ark-serialize", - "ark-std", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "dleq_vrf", "fflonk", "merlin 3.0.0", @@ -881,7 +1035,9 @@ dependencies = [ "xcm-emulator", "zeitgeist-primitives", "zrml-authorized", + "zrml-combinatorial-tokens", "zrml-court", + "zrml-futarchy", "zrml-global-disputes", "zrml-hybrid-router", "zrml-market-commons", @@ -939,7 +1095,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -1376,7 +1532,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -1428,11 +1584,11 @@ name = "common" version = "0.1.0" source = "git+https://github.com/w3f/ring-proof?rev=0e948f3#0e948f3c28cbacecdd3020403c4841c0eb339213" dependencies = [ - "ark-ec", - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "fflonk", "merlin 3.0.0", ] @@ -1781,9 +1937,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array 0.14.7", "subtle", @@ -2026,7 +2182,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2304,7 +2460,7 @@ dependencies = [ "digest 0.10.7", "fiat-crypto", "platforms", - "rustc_version 0.4.0", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -2317,7 +2473,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2357,7 +2513,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2374,7 +2530,7 @@ checksum = "ad08a837629ad949b73d032c637653d069e909cffe4ee7870b02301939ce39cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2466,7 +2622,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2477,7 +2633,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2489,7 +2645,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version 0.4.1", "syn 1.0.109", ] @@ -2578,7 +2734,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2586,12 +2742,12 @@ name = "dleq_vrf" version = "0.0.2" source = "git+https://github.com/w3f/ring-vrf?rev=3119f51#3119f51b54b69308abfb0671f6176cb125ae1bf1" dependencies = [ - "ark-ec", - "ark-ff", + "ark-ec 0.4.2", + "ark-ff 0.4.2", "ark-scale 0.0.10", "ark-secret-scalar", - "ark-serialize", - "ark-std", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "ark-transcript", "arrayvec 0.7.4", "rand_core 0.6.4", @@ -2619,7 +2775,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.60", + "syn 2.0.79", "termcolor", "toml 0.8.2", "walkdir", @@ -2738,6 +2894,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "either" version = "1.11.0" @@ -2781,6 +2949,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "enumflags2" version = "0.7.9" @@ -2798,7 +2986,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2809,7 +2997,7 @@ checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -2963,7 +3151,7 @@ dependencies = [ "prettier-please", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -3042,11 +3230,11 @@ name = "fflonk" version = "0.1.0" source = "git+https://github.com/w3f/fflonk#1e854f35e9a65d08b11a86291405cdc95baa0a35" dependencies = [ - "ark-ec", - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "merlin 3.0.0", ] @@ -3255,7 +3443,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -3382,7 +3570,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -3394,7 +3582,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -3404,7 +3592,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -3588,7 +3776,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -3751,8 +3939,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -3778,7 +3966,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.6", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -3849,6 +4037,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", +] + [[package]] name = "heck" version = "0.4.1" @@ -3913,7 +4110,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ - "crypto-mac 0.11.1", + "crypto-mac 0.11.0", "digest 0.9.0", ] @@ -4070,7 +4267,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -4193,9 +4390,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -4326,6 +4523,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -4610,7 +4816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -5225,7 +5431,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -5239,7 +5445,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -5250,7 +5456,7 @@ checksum = "d710e1214dffbab3b5dacb21475dde7d6ed84c69ff722b3a47a782668d44fbac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -5261,7 +5467,7 @@ checksum = "b8fb85ec1620619edf2984a7693497d4ec88a9665d8b87e942856884c92dbf2a" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -6474,7 +6680,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -7062,7 +7268,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -7392,7 +7598,7 @@ version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", "syn 1.0.109", @@ -7482,7 +7688,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" dependencies = [ - "crypto-mac 0.11.1", + "crypto-mac 0.11.0", ] [[package]] @@ -7555,7 +7761,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -7576,7 +7782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.5.0", ] [[package]] @@ -7596,7 +7802,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -9059,7 +9265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -9079,7 +9285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -9123,14 +9329,22 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", "toml_edit 0.20.2", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -9163,14 +9377,14 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -9209,7 +9423,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -9334,9 +9548,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -9533,7 +9747,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -9550,14 +9764,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -9571,13 +9785,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.5", ] [[package]] @@ -9588,9 +9802,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "resolv-conf" @@ -9617,11 +9837,11 @@ name = "ring" version = "0.1.0" source = "git+https://github.com/w3f/ring-proof?rev=0e948f3#0e948f3c28cbacecdd3020403c4841c0eb339213" dependencies = [ - "ark-ec", - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "common", "fflonk", "merlin 3.0.0", @@ -9780,6 +10000,36 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures 0.3.30", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.1", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.79", + "unicode-ident", +] + [[package]] name = "rtnetlink" version = "0.10.1" @@ -9834,9 +10084,9 @@ dependencies = [ [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver 1.0.22", ] @@ -10111,7 +10361,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -11089,7 +11339,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -11382,7 +11632,7 @@ checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -11704,7 +11954,7 @@ dependencies = [ "curve25519-dalek 4.1.2", "rand_core 0.6.4", "ring 0.17.8", - "rustc_version 0.4.0", + "rustc_version 0.4.1", "sha2 0.10.8", "subtle", ] @@ -11778,7 +12028,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -12030,7 +12280,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "quote", "sp-core-hashing", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -12049,7 +12299,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -12266,7 +12516,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -12463,7 +12713,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -12791,7 +13041,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -12939,9 +13189,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "subtle-ng" @@ -12962,9 +13212,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -13061,7 +13311,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -13072,7 +13322,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", "test-case-core", ] @@ -13116,7 +13366,7 @@ checksum = "e4c60d69f36615a077cc7663b9cb8e42275722d23e58a7fa3d2c7f2915d09d04" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -13127,7 +13377,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -13290,7 +13540,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -13376,9 +13626,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -13389,11 +13639,11 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -13402,11 +13652,22 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.5.0", + "toml_datetime", + "winnow 0.6.20", ] [[package]] @@ -13470,7 +13731,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -13514,7 +13775,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -13685,7 +13946,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", - "rand 0.8.5", + "rand 0.7.3", "static_assertions", ] @@ -13721,9 +13982,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -13888,7 +14149,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", "wasm-bindgen-shared", ] @@ -13922,7 +14183,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -14464,7 +14725,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ - "windows-core 0.51.1", + "windows-core", "windows-targets 0.48.5", ] @@ -14477,15 +14738,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.5", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -14700,6 +14952,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -14800,7 +15061,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -15039,7 +15300,9 @@ dependencies = [ "xcm-emulator", "zeitgeist-primitives", "zrml-authorized", + "zrml-combinatorial-tokens", "zrml-court", + "zrml-futarchy", "zrml-global-disputes", "zrml-hybrid-router", "zrml-market-commons", @@ -15069,7 +15332,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -15089,7 +15352,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.79", ] [[package]] @@ -15110,6 +15373,47 @@ dependencies = [ "zrml-market-commons", ] +[[package]] +name = "zrml-combinatorial-tokens" +version = "0.5.5" +dependencies = [ + "ark-bn254", + "ark-ff 0.5.0", + "env_logger 0.10.2", + "frame-benchmarking", + "frame-support", + "frame-system", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "rstest", + "scale-info", + "sp-io", + "sp-runtime", + "test-case", + "zeitgeist-primitives", + "zrml-combinatorial-tokens", + "zrml-market-commons", +] + +[[package]] +name = "zrml-combinatorial-tokens-fuzz" +version = "0.5.5" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "orml-traits", + "rand 0.8.5", + "sp-runtime", + "zeitgeist-primitives", + "zrml-combinatorial-tokens", +] + [[package]] name = "zrml-court" version = "0.5.6" @@ -15137,6 +15441,41 @@ dependencies = [ "zrml-market-commons", ] +[[package]] +name = "zrml-futarchy" +version = "0.5.5" +dependencies = [ + "arbitrary", + "env_logger 0.10.2", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "test-case", + "zeitgeist-primitives", + "zrml-futarchy", +] + +[[package]] +name = "zrml-futarchy-fuzz" +version = "0.5.5" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "orml-traits", + "rand 0.8.5", + "sp-runtime", + "zeitgeist-primitives", + "zrml-futarchy", +] + [[package]] name = "zrml-global-disputes" version = "0.5.6" @@ -15187,6 +15526,7 @@ dependencies = [ "test-case", "zeitgeist-primitives", "zrml-authorized", + "zrml-combinatorial-tokens", "zrml-court", "zrml-global-disputes", "zrml-hybrid-router", @@ -15249,6 +15589,7 @@ dependencies = [ "typenum", "zeitgeist-primitives", "zrml-authorized", + "zrml-combinatorial-tokens", "zrml-court", "zrml-global-disputes", "zrml-market-commons", @@ -15257,6 +15598,21 @@ dependencies = [ "zrml-prediction-markets-runtime-api", ] +[[package]] +name = "zrml-neo-swaps-fuzz" +version = "0.5.5" +dependencies = [ + "arbitrary", + "frame-support", + "frame-system", + "libfuzzer-sys", + "orml-traits", + "rand 0.8.5", + "sp-runtime", + "zeitgeist-primitives", + "zrml-neo-swaps", +] + [[package]] name = "zrml-orderbook" version = "0.5.6" diff --git a/Cargo.toml b/Cargo.toml index d04e185ec..d34f44cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,9 @@ default-members = [ "runtime/battery-station", "runtime/zeitgeist", "zrml/authorized", + "zrml/combinatorial-tokens", "zrml/court", + "zrml/futarchy", "zrml/hybrid-router", "zrml/global-disputes", "zrml/market-commons", @@ -36,11 +38,16 @@ members = [ "runtime/battery-station", "runtime/zeitgeist", "zrml/authorized", + "zrml/combinatorial-tokens", + "zrml/combinatorial-tokens/fuzz", "zrml/court", + "zrml/futarchy", + "zrml/futarchy/fuzz", "zrml/hybrid-router", "zrml/global-disputes", "zrml/market-commons", "zrml/neo-swaps", + "zrml/neo-swaps/fuzz", "zrml/orderbook", "zrml/orderbook/fuzz", "zrml/parimutuel", @@ -244,7 +251,9 @@ common-runtime = { path = "runtime/common", default-features = false } zeitgeist-macros = { path = "macros", default-features = false } zeitgeist-primitives = { path = "primitives", default-features = false } zrml-authorized = { path = "zrml/authorized", default-features = false } +zrml-combinatorial-tokens = { path = "zrml/combinatorial-tokens", default-features = false } zrml-court = { path = "zrml/court", default-features = false } +zrml-futarchy = { path = "zrml/futarchy", default-features = false } zrml-global-disputes = { path = "zrml/global-disputes", default-features = false } zrml-hybrid-router = { path = "zrml/hybrid-router", default-features = false } zrml-market-commons = { path = "zrml/market-commons", default-features = false } @@ -264,11 +273,14 @@ futures = "0.3.30" jsonrpsee = "0.16.3" libfuzzer-sys = "0.4.7" more-asserts = "0.3.1" +rstest = "0.23.0" test-case = "3.3.1" url = "2.5.0" # Other (wasm) arbitrary = { version = "1.3.2", default-features = false } +ark-bn254 = { version = "0.5.0", default-features = false, features = ["curve"] } +ark-ff = { version = "0.5.0", default-features = false } arrayvec = { version = "0.7.4", default-features = false } cfg-if = { version = "1.0.0" } fixed = { version = "=1.15.0", default-features = false, features = ["num-traits"] } @@ -282,6 +294,15 @@ rand_chacha = { version = "0.3.1", default-features = false } serde = { version = "1.0.198", default-features = false } typenum = { version = "1.17.0", default-features = false } +[profile.test] +overflow-checks = true + +[profile.test.package."*"] +overflow-checks = true + +[profile.dev] +overflow-checks = true + [profile.dev.package] blake2 = { opt-level = 3 } blake2b_simd = { opt-level = 3 } @@ -327,17 +348,28 @@ x25519-dalek = { opt-level = 3 } yamux = { opt-level = 3 } zeroize = { opt-level = 3 } +[profile.dev.package."*"] +overflow-checks = true + [profile.production] codegen-units = 1 incremental = false inherits = "release" lto = true +overflow-checks = true + +[profile.production.package."*"] +overflow-checks = true [profile.release] opt-level = 3 +overflow-checks = true # Zeitgeist runtime requires unwinding. panic = "unwind" +[profile.release.package."*"] +overflow-checks = true + # xcm-emulator incompatible block number type fixed # Commits: diff --git a/README.md b/README.md index e2dffb8b8..cf6899b58 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,12 @@ decentralized court. ## Modules - [authorized](./zrml/authorized) - Offers authorized resolution of disputes. +- [combinatorial-tokens](./zrml/combinatorial-tokens) - The module responsible + for generating Zeitgeist 2.0 outcome tokens. - [court](./zrml/court) - An implementation of a court mechanism used to resolve disputes in a decentralized fashion. +- [futarchy](./zrml/futarchy) - A novel on-chain governance mechanism using + prediction markets. - [global-disputes](./zrml-global-disputes) - Global disputes sets one out of multiple outcomes with the most locked ZTG tokens as the canonical outcome. This is the default process if a dispute mechanism fails to resolve. diff --git a/primitives/src/asset.rs b/primitives/src/asset.rs index d5c923537..f432123c8 100644 --- a/primitives/src/asset.rs +++ b/primitives/src/asset.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -20,7 +20,7 @@ use crate::traits::ZeitgeistAssetEnumerator; use crate::{ traits::PoolSharesId, - types::{CategoryIndex, PoolId}, + types::{CategoryIndex, CombinatorialId, PoolId}, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -48,12 +48,13 @@ use serde::{Deserialize, Serialize}; pub enum Asset { CategoricalOutcome(MarketId, CategoryIndex), ScalarOutcome(MarketId, ScalarPosition), - CombinatorialOutcome, + CombinatorialOutcomeLegacy, // Here to avoid having to migrate all holdings on the chain. PoolShare(PoolId), #[default] Ztg, ForeignAsset(u32), ParimutuelShare(MarketId, CategoryIndex), + CombinatorialToken(CombinatorialId), } #[cfg(feature = "runtime-benchmarks")] diff --git a/primitives/src/constants.rs b/primitives/src/constants.rs index bdfc39226..821ab446f 100644 --- a/primitives/src/constants.rs +++ b/primitives/src/constants.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -41,6 +41,7 @@ pub const BLOCKS_PER_HOUR: BlockNumber = BLOCKS_PER_MINUTE * 60; // 300 // Definitions for currency pub const DECIMALS: u8 = 10; pub const BASE: u128 = 10u128.pow(DECIMALS as u32); +pub const DIME: Balance = BASE / 10; // 1_000_000_000 pub const CENT: Balance = BASE / 100; // 100_000_000 pub const MILLI: Balance = CENT / 10; // 10_000_000 pub const MICRO: Balance = MILLI / 1000; // 10_000 @@ -70,6 +71,9 @@ parameter_types! { /// Pallet identifier, mainly used for named balance reserves. pub const AUTHORIZED_PALLET_ID: PalletId = PalletId(*b"zge/atzd"); +// Combinatorial Tokens +pub const COMBINATORIAL_TOKENS_PALLET_ID: PalletId = PalletId(*b"zge/coto"); + // Court /// Pallet identifier, mainly used for named balance reserves. pub const COURT_PALLET_ID: PalletId = PalletId(*b"zge/cout"); diff --git a/primitives/src/constants/base_multiples.rs b/primitives/src/constants/base_multiples.rs index ee7686516..f8e245ab1 100644 --- a/primitives/src/constants/base_multiples.rs +++ b/primitives/src/constants/base_multiples.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Forecasting Technologies LTD. +// Copyright 2024-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -39,9 +39,13 @@ pub const _36: u128 = 36 * _1; pub const _40: u128 = 40 * _1; pub const _70: u128 = 70 * _1; pub const _80: u128 = 80 * _1; +pub const _99: u128 = 99 * _1; pub const _100: u128 = 100 * _1; pub const _200: u128 = 200 * _1; pub const _101: u128 = 101 * _1; +pub const _300: u128 = 300 * _1; +pub const _321: u128 = 321 * _1; +pub const _400: u128 = 400 * _1; pub const _444: u128 = 444 * _1; pub const _500: u128 = 500 * _1; pub const _777: u128 = 777 * _1; @@ -60,6 +64,8 @@ pub const _1_5: u128 = _1 / 5; pub const _1_6: u128 = _1 / 6; pub const _5_6: u128 = _5 / 6; +pub const _1_7: u128 = _1 / 7; + pub const _1_10: u128 = _1 / 10; pub const _2_10: u128 = _2 / 10; pub const _3_10: u128 = _3 / 10; diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs index 215bdeade..e16de65d1 100644 --- a/primitives/src/constants/mock.rs +++ b/primitives/src/constants/mock.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -33,6 +33,11 @@ parameter_types! { pub const CorrectionPeriod: BlockNumber = 4; } +// CombinatorialTokens +parameter_types! { + pub const CombinatorialTokensPalletId: PalletId = PalletId(*b"zge/coto"); +} + // Court parameter_types! { pub const AppealBond: Balance = 5 * BASE; diff --git a/primitives/src/math/checked_ops_res.rs b/primitives/src/math/checked_ops_res.rs index 13cd420a6..66f0a3c67 100644 --- a/primitives/src/math/checked_ops_res.rs +++ b/primitives/src/math/checked_ops_res.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -64,6 +64,13 @@ where fn checked_rem_res(&self, other: &Self) -> Result; } +pub trait CheckedIncRes +where + Self: Sized, +{ + fn checked_inc_res(&self) -> Result; +} + impl CheckedAddRes for T where T: CheckedAdd, @@ -123,3 +130,13 @@ where self.checked_rem(other).ok_or(DispatchError::Arithmetic(ArithmeticError::DivisionByZero)) } } + +impl CheckedIncRes for T +where + T: CheckedAdd + From, +{ + #[inline] + fn checked_inc_res(&self) -> Result { + self.checked_add(&1u8.into()).ok_or(DispatchError::Arithmetic(ArithmeticError::Overflow)) + } +} diff --git a/primitives/src/traits.rs b/primitives/src/traits.rs index 1298baa68..f39fc9dee 100644 --- a/primitives/src/traits.rs +++ b/primitives/src/traits.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -16,26 +16,40 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . +mod combinatorial_tokens_api; +mod combinatorial_tokens_benchmark_helper; +mod combinatorial_tokens_fuel; +mod combinatorial_tokens_unsafe_api; mod complete_set_operations_api; mod deploy_pool_api; mod dispute_api; mod distribute_fees; +mod futarchy_benchmark_helper; +mod futarchy_oracle; mod hybrid_router_amm_api; mod hybrid_router_orderbook_api; mod market_builder; mod market_commons_pallet_api; mod market_id; +mod payout_api; mod swaps; mod zeitgeist_asset; +pub use combinatorial_tokens_api::*; +pub use combinatorial_tokens_benchmark_helper::*; +pub use combinatorial_tokens_fuel::*; +pub use combinatorial_tokens_unsafe_api::*; pub use complete_set_operations_api::*; pub use deploy_pool_api::*; pub use dispute_api::*; pub use distribute_fees::*; +pub use futarchy_benchmark_helper::*; +pub use futarchy_oracle::*; pub use hybrid_router_amm_api::*; pub use hybrid_router_orderbook_api::*; pub use market_builder::*; pub use market_commons_pallet_api::*; pub use market_id::*; +pub use payout_api::*; pub use swaps::*; pub use zeitgeist_asset::*; diff --git a/primitives/src/traits/combinatorial_tokens_api.rs b/primitives/src/traits/combinatorial_tokens_api.rs new file mode 100644 index 000000000..3024ae33b --- /dev/null +++ b/primitives/src/traits/combinatorial_tokens_api.rs @@ -0,0 +1,42 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{traits::CombinatorialTokensFuel, types::SplitPositionDispatchInfo}; +use alloc::vec::Vec; +use core::fmt::Debug; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::DispatchError; + +/// Trait that can be used to expose the internal functionality of zrml-combinatorial-tokens to +/// other pallets. +pub trait CombinatorialTokensApi { + type AccountId; + type Balance; + type CombinatorialId; + type MarketId; + type Fuel: Clone + CombinatorialTokensFuel + Debug + Decode + Encode + Eq + TypeInfo; + + fn split_position( + who: Self::AccountId, + parent_collection_id: Option, + market_id: Self::MarketId, + partition: Vec>, + amount: Self::Balance, + force_max_work: Self::Fuel, + ) -> Result, DispatchError>; +} diff --git a/primitives/src/traits/combinatorial_tokens_benchmark_helper.rs b/primitives/src/traits/combinatorial_tokens_benchmark_helper.rs new file mode 100644 index 000000000..23906395d --- /dev/null +++ b/primitives/src/traits/combinatorial_tokens_benchmark_helper.rs @@ -0,0 +1,32 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use alloc::vec::Vec; +use sp_runtime::DispatchResult; + +/// Trait used for setting up benchmarks of zrml-combinatorial-tokens. Must not be used in +/// production. +pub trait CombinatorialTokensBenchmarkHelper { + type Balance; + type MarketId; + + /// Prepares the market with the specified `market_id` to have a particular `payout`. + fn setup_payout_vector( + market_id: Self::MarketId, + payout: Option>, + ) -> DispatchResult; +} diff --git a/primitives/src/traits/combinatorial_tokens_fuel.rs b/primitives/src/traits/combinatorial_tokens_fuel.rs new file mode 100644 index 000000000..4d939f0af --- /dev/null +++ b/primitives/src/traits/combinatorial_tokens_fuel.rs @@ -0,0 +1,27 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +/// A trait for keeping track of a certain amount of work to be done. +pub trait CombinatorialTokensFuel { + /// Creates a `Fuel` object from a `total` value which indicates the total amount of work to be + /// done. This is usually done for benchmarking purposes. + fn from_total(total: u32) -> Self; + + /// Returns a `u32` which indicates the total amount of work to be done. Must be `O(1)` to avoid + /// excessive calculation if this call is used when calculating extrinsic weight. + fn total(&self) -> u32; +} diff --git a/primitives/src/traits/combinatorial_tokens_unsafe_api.rs b/primitives/src/traits/combinatorial_tokens_unsafe_api.rs new file mode 100644 index 000000000..9893ddd18 --- /dev/null +++ b/primitives/src/traits/combinatorial_tokens_unsafe_api.rs @@ -0,0 +1,48 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::types::Asset; +use alloc::vec::Vec; +use sp_runtime::DispatchResult; + +// Very fast and very unsafe API for splitting and merging combinatorial tokens. Calling the exposed +// functions with a bad `assets` argument can break the reserve. +pub trait CombinatorialTokensUnsafeApi { + type AccountId; + type Balance; + type MarketId; + + /// Transfers `amount` units of collateral from the user to the pallet's reserve and mints + /// `amount` units of each asset in `assets`. Can break the reserve or result in loss of funds + /// if the value of the elements in `assets` don't add up to exactly 1. + fn split_position_unsafe( + who: Self::AccountId, + collateral: Asset, + assets: Vec>, + amount: Self::Balance, + ) -> DispatchResult; + + /// Transfers `amount` units of collateral from the pallet's reserve to the user and burns + /// `amount` units of each asset in `assets`. Can break the reserve or result in loss of funds + /// if the value of the elements in `assets` don't add up to exactly 1. + fn merge_position_unsafe( + who: Self::AccountId, + collateral: Asset, + assets: Vec>, + amount: Self::Balance, + ) -> DispatchResult; +} diff --git a/primitives/src/traits/futarchy_benchmark_helper.rs b/primitives/src/traits/futarchy_benchmark_helper.rs new file mode 100644 index 000000000..e439253a0 --- /dev/null +++ b/primitives/src/traits/futarchy_benchmark_helper.rs @@ -0,0 +1,22 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +pub trait FutarchyBenchmarkHelper { + /// Creates an oracle which returns `value` when evaluated, provided that state is not modified + /// any further. + fn create_oracle(value: bool) -> Oracle; +} diff --git a/primitives/src/traits/futarchy_oracle.rs b/primitives/src/traits/futarchy_oracle.rs new file mode 100644 index 000000000..9309cea6b --- /dev/null +++ b/primitives/src/traits/futarchy_oracle.rs @@ -0,0 +1,29 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use frame_support::pallet_prelude::Weight; + +pub trait FutarchyOracle { + type BlockNumber; + + /// Evaluates the query at the current block and returns the weight consumed and a `bool` + /// indicating whether the query evaluated positively. + fn evaluate(&self) -> (Weight, bool); + + /// Updates the oracle's data and returns the weight consumed. + fn update(&mut self, now: Self::BlockNumber) -> Weight; +} diff --git a/primitives/src/traits/market_commons_pallet_api.rs b/primitives/src/traits/market_commons_pallet_api.rs index a24eecefa..e55b3c825 100644 --- a/primitives/src/traits/market_commons_pallet_api.rs +++ b/primitives/src/traits/market_commons_pallet_api.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -37,7 +37,7 @@ use sp_runtime::{ // Abstraction of the market type, which is not a part of `MarketCommonsPalletApi` because Rust // doesn't support type aliases in traits. -type MarketOf = Market< +pub type MarketOf = Market< ::AccountId, ::Balance, ::BlockNumber, diff --git a/primitives/src/traits/payout_api.rs b/primitives/src/traits/payout_api.rs new file mode 100644 index 000000000..4f03a84da --- /dev/null +++ b/primitives/src/traits/payout_api.rs @@ -0,0 +1,25 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use alloc::vec::Vec; + +pub trait PayoutApi { + type Balance; + type MarketId; + + fn payout_vector(market_id: Self::MarketId) -> Option>; +} diff --git a/primitives/src/types.rs b/primitives/src/types.rs index f361dd568..c30aef1c4 100644 --- a/primitives/src/types.rs +++ b/primitives/src/types.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -16,21 +16,25 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . +use crate::traits::CombinatorialTokensBenchmarkHelper; pub use crate::{ asset::*, market::*, max_runtime_usize::*, outcome_report::OutcomeReport, proxy_type::*, serde_wrapper::*, }; -#[cfg(feature = "arbitrary")] -use arbitrary::{Arbitrary, Result, Unstructured}; -use frame_support::weights::Weight; +use alloc::vec::Vec; +use core::marker::PhantomData; +use frame_support::{dispatch::PostDispatchInfo, weights::Weight}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{ generic, traits::{BlakeTwo256, IdentifyAccount, Verify}, - MultiSignature, OpaqueExtrinsic, + DispatchResult, MultiSignature, OpaqueExtrinsic, }; +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Result, Unstructured}; + /// Signed counter-part of Balance pub type Amount = i128; @@ -54,6 +58,9 @@ pub type BlockNumber = u64; /// The index of the category for a `CategoricalOutcome` asset. pub type CategoryIndex = u16; +/// The type used to identify combinatorial outcomes. +pub type CombinatorialId = [u8; 32]; + /// Multihash for digest sizes up to 384 bit. /// The multicodec encoding the hash algorithm uses only 1 byte, /// effecitvely limiting the number of available hash types. @@ -177,3 +184,27 @@ pub struct XcmMetadata { /// Should be updated regularly. pub fee_factor: Option, } + +pub struct NoopCombinatorialTokensBenchmarkHelper( + PhantomData<(Balance, MarketId)>, +); + +impl CombinatorialTokensBenchmarkHelper + for NoopCombinatorialTokensBenchmarkHelper +{ + type Balance = Balance; + type MarketId = MarketId; + + fn setup_payout_vector( + _market_id: Self::MarketId, + _payout: Option>, + ) -> DispatchResult { + Ok(()) + } +} + +pub struct SplitPositionDispatchInfo { + pub collection_ids: Vec, + pub position_ids: Vec>, + pub post_dispatch_info: PostDispatchInfo, +} diff --git a/runtime/battery-station/Cargo.toml b/runtime/battery-station/Cargo.toml index 99e5b5f18..489717b94 100644 --- a/runtime/battery-station/Cargo.toml +++ b/runtime/battery-station/Cargo.toml @@ -109,7 +109,9 @@ xcm-executor = { workspace = true, optional = true } common-runtime = { workspace = true } zeitgeist-primitives = { workspace = true } zrml-authorized = { workspace = true } +zrml-combinatorial-tokens = { workspace = true } zrml-court = { workspace = true } +zrml-futarchy = { workspace = true } zrml-global-disputes = { workspace = true, optional = true } zrml-hybrid-router = { workspace = true } zrml-market-commons = { workspace = true } @@ -214,7 +216,9 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder?/runtime-benchmarks", "zrml-authorized/runtime-benchmarks", + "zrml-combinatorial-tokens/runtime-benchmarks", "zrml-court/runtime-benchmarks", + "zrml-futarchy/runtime-benchmarks", "zrml-hybrid-router/runtime-benchmarks", "zrml-neo-swaps/runtime-benchmarks", "zrml-parimutuel/runtime-benchmarks", @@ -327,7 +331,9 @@ std = [ "zeitgeist-primitives/std", "zrml-authorized/std", + "zrml-combinatorial-tokens/std", "zrml-court/std", + "zrml-futarchy/std", "zrml-hybrid-router/std", "zrml-market-commons/std", "zrml-neo-swaps/std", @@ -381,7 +387,9 @@ try-runtime = [ # Zeitgeist runtime pallets "zrml-authorized/try-runtime", + "zrml-combinatorial-tokens/try-runtime", "zrml-court/try-runtime", + "zrml-futarchy/try-runtime", "zrml-hybrid-router/try-runtime", "zrml-market-commons/try-runtime", "zrml-neo-swaps/try-runtime", diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs index c52edce56..e3d02f7bb 100644 --- a/runtime/battery-station/src/parameters.rs +++ b/runtime/battery-station/src/parameters.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -89,6 +89,9 @@ parameter_types! { pub const TechnicalCommitteeMaxProposals: u32 = 64; pub const TechnicalCommitteeMotionDuration: BlockNumber = 7 * BLOCKS_PER_DAY; + // CombinatorialTokens + pub const CombinatorialTokensPalletId: PalletId = COMBINATORIAL_TOKENS_PALLET_ID; + // Contracts pub const ContractsMaxDelegateDependencies: u32 = 32; @@ -153,7 +156,11 @@ parameter_types! { /// can lead to extrinsic with very big weight: see delegate for instance. pub const MaxVotes: u32 = 100; /// The maximum number of public proposals that can exist at any time. - pub const MaxProposals: u32 = 100; + pub const DemocracyMaxProposals: u32 = 100; + + // Futarchy + pub const FutarchyMaxProposals: u32 = 4; + pub const MinDuration: BlockNumber = 7 * BLOCKS_PER_DAY; // Hybrid Router parameters pub const HybridRouterPalletId: PalletId = HYBRID_ROUTER_PALLET_ID; @@ -188,6 +195,7 @@ parameter_types! { pub const NeoSwapsMaxSwapFee: Balance = 10 * CENT; pub const NeoSwapsPalletId: PalletId = NS_PALLET_ID; pub const MaxLiquidityTreeDepth: u32 = 9u32; + pub const MaxSplits: u16 = 128u16; // ORML pub const GetNativeCurrencyId: CurrencyId = Asset::Ztg; @@ -439,7 +447,8 @@ parameter_type_with_key! { pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance { match currency_id { Asset::CategoricalOutcome(_,_) => ExistentialDeposit::get(), - Asset::CombinatorialOutcome => ExistentialDeposit::get(), + Asset::CombinatorialToken(_) => ExistentialDeposit::get(), + Asset::CombinatorialOutcomeLegacy => ExistentialDeposit::get(), Asset::PoolShare(_) => ExistentialDeposit::get(), Asset::ScalarOutcome(_,_) => ExistentialDeposit::get(), #[cfg(feature = "parachain")] diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index c638bd59e..5db5669b9 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -72,11 +72,9 @@ macro_rules! decl_common_types { parameter_types, storage::child, traits::{Currency, Get, Imbalance, NeverEnsureOrigin, OnRuntimeUpgrade, OnUnbalanced}, - BoundedVec, Twox64Concat, + Blake2_256, BoundedVec, Twox64Concat, }; use frame_system::EnsureSigned; - #[cfg(feature = "try-runtime")] - use frame_try_runtime::{TryStateSelect, UpgradeCheckSelect}; use orml_traits::MultiCurrency; use pallet_balances::CreditOf; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -86,12 +84,25 @@ macro_rules! decl_common_types { generic, DispatchError, DispatchResult, RuntimeDebug, SaturatedConversion, }; use zeitgeist_primitives::traits::{DeployPoolApi, DistributeFees, MarketCommonsPalletApi}; + use zrml_combinatorial_tokens::types::{CryptographicIdManager, Fuel}; + use zrml_neo_swaps::types::DecisionMarketOracle; + + #[cfg(feature = "try-runtime")] + use frame_try_runtime::{TryStateSelect, UpgradeCheckSelect}; + + #[cfg(feature = "runtime-benchmarks")] + use zrml_neo_swaps::types::DecisionMarketBenchmarkHelper; + + #[cfg(feature = "runtime-benchmarks")] + use zrml_prediction_markets::types::PredictionMarketsCombinatorialTokensBenchmarkHelper; + + use zrml_neo_swaps::migration::MigratePoolStorageItems; pub type Block = generic::Block; type Address = sp_runtime::MultiAddress; - type Migrations = (); + type Migrations = (MigratePoolStorageItems); pub type Executive = frame_executive::Executive< Runtime, @@ -334,6 +345,8 @@ macro_rules! create_runtime { Orderbook: zrml_orderbook::{Call, Event, Pallet, Storage} = 61, Parimutuel: zrml_parimutuel::{Call, Event, Pallet, Storage} = 62, HybridRouter: zrml_hybrid_router::{Call, Event, Pallet, Storage} = 64, + CombinatorialTokens: zrml_combinatorial_tokens::{Call, Event, Pallet, Storage} = 65, + Futarchy: zrml_futarchy::{Call, Event, Pallet, Storage} = 66, $($additional_pallets)* } @@ -772,7 +785,7 @@ macro_rules! impl_config_traits { type PalletsOrigin = OriginCaller; type MaxVotes = MaxVotes; type WeightInfo = weights::pallet_democracy::WeightInfo; - type MaxProposals = MaxProposals; + type MaxProposals = DemocracyMaxProposals; type Preimages = Preimage; type MaxBlacklisted = ConstU32<100>; type MaxDeposits = ConstU32<100>; @@ -1113,6 +1126,19 @@ macro_rules! impl_config_traits { type WeightInfo = zrml_authorized::weights::WeightInfo; } + impl zrml_combinatorial_tokens::Config for Runtime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = PredictionMarketsCombinatorialTokensBenchmarkHelper; + type CombinatorialIdManager = CryptographicIdManager; + type Fuel = Fuel; + type MarketCommons = MarketCommons; + type MultiCurrency = AssetManager; + type Payout = PredictionMarkets; + type RuntimeEvent = RuntimeEvent; + type PalletId = CombinatorialTokensPalletId; + type WeightInfo = zrml_combinatorial_tokens::weights::WeightInfo; + } + impl zrml_court::Config for Runtime { type AppealBond = AppealBond; type BlocksPerYear = BlocksPerYear; @@ -1140,6 +1166,17 @@ macro_rules! impl_config_traits { type WeightInfo = zrml_court::weights::WeightInfo; } + impl zrml_futarchy::Config for Runtime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = DecisionMarketBenchmarkHelper; + type MaxProposals = FutarchyMaxProposals; + type MinDuration = MinDuration; + type Oracle = DecisionMarketOracle; + type RuntimeEvent = RuntimeEvent; + type Scheduler = Scheduler; + type WeightInfo = zrml_futarchy::weights::WeightInfo; + } + impl zrml_market_commons::Config for Runtime { type Balance = Balance; type MarketId = MarketId; @@ -1233,13 +1270,18 @@ macro_rules! impl_config_traits { common_runtime::impl_market_creator_fees!(); impl zrml_neo_swaps::Config for Runtime { + type CombinatorialId = CombinatorialId; + type CombinatorialTokens = CombinatorialTokens; + type CombinatorialTokensUnsafe = CombinatorialTokens; type CompleteSetOperations = PredictionMarkets; type ExternalFees = MarketCreatorFee; type MarketCommons = MarketCommons; type MultiCurrency = AssetManager; + type PoolId = MarketId; type RuntimeEvent = RuntimeEvent; type WeightInfo = zrml_neo_swaps::weights::WeightInfo; type MaxLiquidityTreeDepth = MaxLiquidityTreeDepth; + type MaxSplits = MaxSplits; type MaxSwapFee = NeoSwapsMaxSwapFee; type PalletId = NeoSwapsPalletId; } @@ -1387,7 +1429,9 @@ macro_rules! create_runtime_api { list_benchmark!(list, extra, pallet_vesting, Vesting); list_benchmark!(list, extra, zrml_swaps, Swaps); list_benchmark!(list, extra, zrml_authorized, Authorized); + list_benchmark!(list, extra, zrml_combinatorial_tokens, CombinatorialTokens); list_benchmark!(list, extra, zrml_court, Court); + list_benchmark!(list, extra, zrml_futarchy, Futarchy); list_benchmark!(list, extra, zrml_global_disputes, GlobalDisputes); list_benchmark!(list, extra, zrml_orderbook, Orderbook); list_benchmark!(list, extra, zrml_parimutuel, Parimutuel); @@ -1475,7 +1519,9 @@ macro_rules! create_runtime_api { add_benchmark!(params, batches, pallet_vesting, Vesting); add_benchmark!(params, batches, zrml_swaps, Swaps); add_benchmark!(params, batches, zrml_authorized, Authorized); + add_benchmark!(params, batches, zrml_combinatorial_tokens, CombinatorialTokens); add_benchmark!(params, batches, zrml_court, Court); + add_benchmark!(params, batches, zrml_futarchy, Futarchy); add_benchmark!(params, batches, zrml_global_disputes, GlobalDisputes); add_benchmark!(params, batches, zrml_orderbook, Orderbook); add_benchmark!(params, batches, zrml_parimutuel, Parimutuel); @@ -2046,6 +2092,33 @@ macro_rules! create_common_tests { mod common_tests { common_runtime::fee_tests!(); + mod utility { + use crate::{Balances, BlockNumber, Futarchy, Preimage, Scheduler, System}; + use frame_support::traits::Hooks; + + // Beware! This only advances certain pallets. + pub(crate) fn run_to_block(to: BlockNumber) { + while System::block_number() < to { + let now = System::block_number(); + + Futarchy::on_finalize(now); + Balances::on_finalize(now); + Preimage::on_finalize(now); + Scheduler::on_finalize(now); + System::on_finalize(now); + + let next = now + 1; + System::set_block_number(next); + + System::on_initialize(next); + Scheduler::on_initialize(next); + Preimage::on_initialize(next); + Balances::on_initialize(next); + Futarchy::on_initialize(next); + } + } + } + mod dust_removal { use crate::*; use frame_support::PalletId; @@ -2085,6 +2158,136 @@ macro_rules! create_common_tests { }); } } + + mod futarchy { + use crate::{ + common_tests::utility, AccountId, Asset, AssetManager, Balance, Balances, + Futarchy, MarketId, NeoSwaps, PredictionMarkets, Preimage, Runtime, + RuntimeCall, RuntimeOrigin, Scheduler, System, + }; + use frame_support::{assert_ok, dispatch::RawOrigin, traits::StorePreimage}; + use orml_traits::MultiCurrency; + use sp_runtime::{ + traits::{Hash, Zero}, + BuildStorage, Perbill, + }; + use zeitgeist_primitives::{ + math::fixed::{BaseProvider, ZeitgeistBase}, + traits::MarketBuilderTrait, + types::{ + Deadlines, MarketCreation, MarketPeriod, MarketType, MultiHash, ScoringRule, + }, + }; + use zrml_futarchy::types::Proposal; + use zrml_market_commons::types::MarketBuilder; + use zrml_neo_swaps::types::{DecisionMarketOracle, DecisionMarketOracleScoreboard}; + + #[test] + fn futarchy_schedules_and_executes_call() { + let mut t: sp_io::TestExternalities = + frame_system::GenesisConfig::::default() + .build_storage() + .unwrap() + .into(); + t.execute_with(|| { + let alice = AccountId::from([0u8; 32]); + + let collateral: Asset = Asset::Ztg; + let one: Balance = ZeitgeistBase::get().unwrap(); + let total_cost: Balance = one.saturating_mul(100_000u128); + assert_ok!(AssetManager::deposit(collateral, &alice, total_cost)); + + let mut metadata = [0x01; 50]; + metadata[0] = 0x15; + metadata[1] = 0x30; + let multihash = MultiHash::Sha3_384(metadata); + + let oracle_duration = + ::MinOracleDuration::get(); + let deadlines = Deadlines { + grace_period: Default::default(), + oracle_duration, + dispute_duration: Zero::zero(), + }; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(alice.clone()), + collateral, + Perbill::zero(), + alice.clone(), + MarketPeriod::Block(0..999), + deadlines, + multihash, + MarketCreation::Permissionless, + MarketType::Categorical(2), + None, + ScoringRule::AmmCdaHybrid, + )); + + let market_id = 0; + let amount = one * 100u128; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(alice.clone()), + market_id, + amount, + )); + + assert_ok!(NeoSwaps::deploy_pool( + RuntimeOrigin::signed(alice.clone()), + market_id, + amount, + vec![one / 10u128 * 9u128, one / 10u128], + one / 100, + )); + + let duration = ::MinDuration::get(); + + // Wrap `remark_with_event` call in `dispatch_as` so that it doesn't error + // with `BadOrigin`. + let bob = AccountId::from([0x01; 32]); + let remark = b"hullo".to_vec(); + let remark_dispatched_as = pallet_utility::Call::::dispatch_as { + as_origin: Box::new(RawOrigin::Signed(bob.clone()).into()), + call: Box::new( + frame_system::Call::remark_with_event { remark: remark.clone() } + .into(), + ), + }; + let call = + Preimage::bound(RuntimeCall::from(remark_dispatched_as)).unwrap(); + let scoreboard = + DecisionMarketOracleScoreboard::new(40_000, 10_000, one / 7, one); + let oracle = DecisionMarketOracle::new( + market_id, + Asset::CategoricalOutcome(market_id, 0), + Asset::CategoricalOutcome(market_id, 1), + scoreboard, + ); + let when = duration + 10; + let proposal = Proposal { when, call, oracle }; + + assert_ok!(Futarchy::submit_proposal( + RawOrigin::Root.into(), + duration, + proposal.clone() + )); + + utility::run_to_block(when); + + let hash = ::Hashing::hash(&remark); + System::assert_has_event( + frame_system::Event::::Remarked { sender: bob, hash }.into(), + ); + System::assert_has_event( + pallet_scheduler::Event::::Dispatched { + task: (when, 0), + id: None, + result: Ok(()), + } + .into(), + ); + }); + } + } } }; } diff --git a/runtime/zeitgeist/Cargo.toml b/runtime/zeitgeist/Cargo.toml index 1bccea943..3d7ff3488 100644 --- a/runtime/zeitgeist/Cargo.toml +++ b/runtime/zeitgeist/Cargo.toml @@ -108,7 +108,9 @@ xcm-executor = { workspace = true, optional = true } common-runtime = { workspace = true } zeitgeist-primitives = { workspace = true } zrml-authorized = { workspace = true } +zrml-combinatorial-tokens = { workspace = true } zrml-court = { workspace = true } +zrml-futarchy = { workspace = true } zrml-global-disputes = { workspace = true, optional = true } zrml-hybrid-router = { workspace = true } zrml-market-commons = { workspace = true } @@ -211,7 +213,9 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", "xcm-builder?/runtime-benchmarks", "zrml-authorized/runtime-benchmarks", + "zrml-combinatorial-tokens/runtime-benchmarks", "zrml-court/runtime-benchmarks", + "zrml-futarchy/runtime-benchmarks", "zrml-hybrid-router/runtime-benchmarks", "zrml-neo-swaps/runtime-benchmarks", "zrml-parimutuel/runtime-benchmarks", @@ -316,7 +320,9 @@ std = [ "zeitgeist-primitives/std", "zrml-authorized/std", + "zrml-combinatorial-tokens/std", "zrml-court/std", + "zrml-futarchy/std", "zrml-hybrid-router/std", "zrml-market-commons/std", "zrml-neo-swaps/std", @@ -369,7 +375,9 @@ try-runtime = [ # Zeitgeist runtime pallets "zrml-authorized/try-runtime", + "zrml-combinatorial-tokens/try-runtime", "zrml-court/try-runtime", + "zrml-futarchy/try-runtime", "zrml-hybrid-router/try-runtime", "zrml-market-commons/try-runtime", "zrml-neo-swaps/try-runtime", diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs index 7a69c5f85..adc3ded53 100644 --- a/runtime/zeitgeist/src/parameters.rs +++ b/runtime/zeitgeist/src/parameters.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -89,6 +89,9 @@ parameter_types! { pub const TechnicalCommitteeMaxProposals: u32 = 64; pub const TechnicalCommitteeMotionDuration: BlockNumber = 7 * BLOCKS_PER_DAY; + // CombinatorialTokens + pub const CombinatorialTokensPalletId: PalletId = COMBINATORIAL_TOKENS_PALLET_ID; + // Contracts pub const ContractsMaxDelegateDependencies: u32 = 32; @@ -153,7 +156,11 @@ parameter_types! { /// can lead to extrinsic with very big weight: see delegate for instance. pub const MaxVotes: u32 = 100; /// The maximum number of public proposals that can exist at any time. - pub const MaxProposals: u32 = 100; + pub const DemocracyMaxProposals: u32 = 100; + + // Futarchy + pub const FutarchyMaxProposals: u32 = 4; + pub const MinDuration: BlockNumber = 7 * BLOCKS_PER_DAY; // Hybrid Router parameters pub const HybridRouterPalletId: PalletId = HYBRID_ROUTER_PALLET_ID; @@ -188,6 +195,7 @@ parameter_types! { pub const NeoSwapsMaxSwapFee: Balance = 10 * CENT; pub const NeoSwapsPalletId: PalletId = NS_PALLET_ID; pub const MaxLiquidityTreeDepth: u32 = 9u32; + pub const MaxSplits: u16 = 128u16; // ORML pub const GetNativeCurrencyId: CurrencyId = Asset::Ztg; @@ -439,7 +447,8 @@ parameter_type_with_key! { pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance { match currency_id { Asset::CategoricalOutcome(_,_) => ExistentialDeposit::get(), - Asset::CombinatorialOutcome => ExistentialDeposit::get(), + Asset::CombinatorialToken(_) => ExistentialDeposit::get(), + Asset::CombinatorialOutcomeLegacy => ExistentialDeposit::get(), Asset::PoolShare(_) => ExistentialDeposit::get(), Asset::ScalarOutcome(_,_) => ExistentialDeposit::get(), #[cfg(feature = "parachain")] diff --git a/scripts/benchmarks/configuration.sh b/scripts/benchmarks/configuration.sh index 1633936f4..41409c06a 100644 --- a/scripts/benchmarks/configuration.sh +++ b/scripts/benchmarks/configuration.sh @@ -4,10 +4,9 @@ EXTERNAL_WEIGHTS_PATH="./runtime/common/src/weights/" # This script contains the configuration for other benchmarking scripts. export FRAME_PALLETS=( - frame_system pallet_balances pallet_bounties pallet_collective \ - pallet_democracy pallet_identity pallet_membership pallet_multisig pallet_preimage \ - pallet_proxy pallet_scheduler pallet_timestamp pallet_treasury pallet_utility \ - pallet_vesting \ + frame_system pallet_balances pallet_bounties pallet_collective pallet_democracy \ + pallet_identity pallet_membership pallet_multisig pallet_preimage pallet_proxy \ + pallet_scheduler pallet_timestamp pallet_treasury pallet_utility pallet_vesting ) export FRAME_PALLETS_RUNS="${FRAME_PALLETS_RUNS:-20}" export FRAME_PALLETS_STEPS="${FRAME_PALLETS_STEPS:-50}" @@ -27,8 +26,9 @@ export ORML_PALLETS_STEPS="${ORML_PALLETS_STEPS:-50}" export ORML_WEIGHT_TEMPLATE="./misc/orml_weight_template.hbs" export ZEITGEIST_PALLETS=( - zrml_authorized zrml_court zrml_global_disputes zrml_hybrid_router zrml_neo_swaps \ - zrml_orderbook zrml_parimutuel zrml_prediction_markets zrml_styx \ + zrml_authorized zrml_combinatorial_tokens zrml_court zrml_futarchy zrml_global_disputes \ + zrml_hybrid_router zrml_neo_swaps zrml_orderbook zrml_parimutuel zrml_prediction_markets \ + zrml_swaps zrml_styx \ ) export ZEITGEIST_PALLETS_RUNS="${ZEITGEIST_PALLETS_RUNS:-20}" export ZEITGEIST_PALLETS_STEPS="${ZEITGEIST_PALLETS_STEPS:-50}" diff --git a/scripts/tests/coverage.sh b/scripts/tests/coverage.sh index aeed8dbfd..71b59fa16 100755 --- a/scripts/tests/coverage.sh +++ b/scripts/tests/coverage.sh @@ -11,7 +11,7 @@ export LLVM_PROFILE_FILE="cargo-test-%p-%m.profraw" rustflags="-Cinstrument-coverage" test_package_with_feature "primitives" "std" "$rustflags" -no_runtime_benchmarks=('court' 'market-commons' 'rikiddo') +no_runtime_benchmarks=('court' 'market-commons') for package in zrml/*; do if [[ " ${no_runtime_benchmarks[*]} " != *" ${package##*/} "* ]]; then @@ -25,4 +25,4 @@ done unset CARGO_INCREMENTAL LLVM_PROFILE_FILE -grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --llvm --ignore '../*' --ignore "/*" -o $RUNNER_TEMP/zeitgeist-test-coverage.lcov \ No newline at end of file +grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --llvm --ignore '../*' --ignore "/*" -o $RUNNER_TEMP/zeitgeist-test-coverage.lcov diff --git a/scripts/tests/fuzz.sh b/scripts/tests/fuzz.sh index beff95f26..c70909ac1 100755 --- a/scripts/tests/fuzz.sh +++ b/scripts/tests/fuzz.sh @@ -57,3 +57,13 @@ cargo fuzz run --release --fuzz-dir zrml/swaps/fuzz pool_exit -- -runs=$(($(($RU # --- Orderbook-v1 Pallet fuzz tests --- cargo fuzz run --release --fuzz-dir zrml/orderbook/fuzz orderbook_v1_full_workflow -- -runs=$RUNS + +cargo fuzz run --release --fuzz-dir zrml/futarchy/fuzz submit_proposal -- -runs=$RUNS + +cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz split_position -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz merge_position -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/combinatorial-tokens/fuzz redeem_position -- -runs=$RUNS + +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz deploy_combinatorial_pool -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz combo_buy -- -runs=$RUNS +cargo fuzz run --release --fuzz-dir zrml/neo-swaps/fuzz combo_sell -- -runs=$RUNS diff --git a/zrml/combinatorial-tokens/Cargo.toml b/zrml/combinatorial-tokens/Cargo.toml new file mode 100644 index 000000000..fca934403 --- /dev/null +++ b/zrml/combinatorial-tokens/Cargo.toml @@ -0,0 +1,64 @@ +[dependencies] +ark-bn254 = { workspace = true } +ark-ff = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +orml-traits = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true, features = ["derive"] } +sp-runtime = { workspace = true } +zeitgeist-primitives = { workspace = true } + +# mock + +env_logger = { workspace = true, optional = true } +orml-currencies = { workspace = true, optional = true } +orml-tokens = { workspace = true, optional = true } +pallet-balances = { workspace = true, optional = true } +pallet-timestamp = { workspace = true, optional = true } +sp-io = { workspace = true, optional = true } +zrml-market-commons = { workspace = true, optional = true } + +[dev-dependencies] +rstest = { workspace = true } +test-case = { workspace = true } +zrml-combinatorial-tokens = { workspace = true, features = ["default", "mock"] } + +[features] +default = ["std"] +mock = [ + "env_logger/default", + "orml-currencies/default", + "orml-tokens/default", + "sp-io/default", + "pallet-balances/default", + "pallet-timestamp/default", + "zrml-market-commons/default", + "zeitgeist-primitives/mock", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "ark-bn254/std", + "ark-ff/std", + "orml-traits/std", + "parity-scale-codec/std", + "sp-runtime/std", + "zeitgeist-primitives/std", +] +try-runtime = [ + "frame-support/try-runtime", +] + +[package] +authors = ["Zeitgeist PM "] +edition.workspace = true +name = "zrml-combinatorial-tokens" +version = "0.5.5" diff --git a/zrml/combinatorial-tokens/README.md b/zrml/combinatorial-tokens/README.md new file mode 100644 index 000000000..43e88d9b0 --- /dev/null +++ b/zrml/combinatorial-tokens/README.md @@ -0,0 +1,38 @@ +# Combinatorial Tokens Module + +The combinatorial-tokens module implements modern Zeitgeist's method of +creating and destroying outcome tokens. + +## Overview + +In a categorical or scalar prediction market, one unit of a complete set (i.e. one unit of each outcome token of the market) always redeems for one unit of collateral. + +In a Yes/No market, for instance, holding `x` units of Yes and `x` units of No means that, when the market resolves, you will always receive `x` units of collateral. In a scalar market, on the other hand, `x` units of Long and `x` units of Short will always redeem to a total of `x` units of collateral, as well. + +This means that buying and selling collateral for complete sets should be allowed. For example, `x` units of collateral should fetch `x` units of complete set, and vice versa. Buying complete sets can be thought of as splitting collateral into outcome tokens, while selling complete sets can be thought of as merging outcome tokens back into collateral. + +The combinatorial-tokens module generalizes this approach to not only allow splitting and merging into collateral, but also splitting and merging into outcome tokens of multiple different markets. This allows us to create outcome tokens that combine multiple events. They are called _combinatorial tokens_. + +For example, splitting an `A` token from one categorical market using another categorical market with two outcomes `X` and `Y` yields `A & X` and `A & Y` tokens. They represent the event that `A` and `X` (resp. `Y`) occur. Splitting a Yes token from a binary market using a scalar market will give `Yes & Long` and `Yes & Short` tokens. They represent Long/Short tokens contingent on `Yes` occurring. + +In addition to splitting and merging, combinatorial tokens can be redeemed if one of the markets involved in creating them has been resolved. For example, if the `XY` market above resolves to `X`, then every unit of `X & A` redeems for a unit of `A` and `Y & A` is worthless. If the scalar market above resolves so that `Long` is valued at `.4` and `Short` at `.6`, then every unit of `Yes & Long` redeems for `.4` units of `Yes` and every unit of `Yes & Short` redeems for `.6`. + +An important distinction which we've so far neglected to make is the distinction between an abstract _collection_ like `X & A` or `Yes & Short` and a concrete _position_, which is a collection together with a collateral token against which it is valued. Collections are purely abstract and used in the implementation. Positions are actual tokens on the chain. + +Collections and positions are identified using their IDs. When using the standard combinatorial ID Manager, this ID is a 256 bit value. The position ID of a certain token can be calculated using the collection ID and the collateral. + +### Terminology + +- _Combinatorial token_: Any instance of `zeitgeist_primitives::Asset::CombinatorialToken`. +- _Complete set (of a prediction market)_: An abstract set containing every outcome of a particular prediction market. One unit of a complete set is one unit of each outcome token from the market in question. After the market resolves, a complete set always redeems for exactly one unit of collateral. +- _Merge_: The process of exchanging multiple tokens for a single token of equal value. +- _Split_: The process of exchanging a token for more complicated tokens of equal value. + +### Combinatorial ID Manager + +Calculating + +alt_bn128 + +combinatorial tokens, as [defined by +Gnosis](https://gnosis-conditional-tokens.readthedocs.io/en/latest/developer-guide.html#) in Substrate. diff --git a/zrml/combinatorial-tokens/fuzz/Cargo.toml b/zrml/combinatorial-tokens/fuzz/Cargo.toml new file mode 100644 index 000000000..32d76bf87 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/Cargo.toml @@ -0,0 +1,38 @@ +[[bin]] +doc = false +name = "split_position" +path = "split_position.rs" +test = false + +[[bin]] +doc = false +name = "merge_position" +path = "merge_position.rs" +test = false + +[[bin]] +doc = false +name = "redeem_position" +path = "redeem_position.rs" +test = false + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +frame-support = { workspace = true, features = ["default"] } +frame-system = { workspace = true } +libfuzzer-sys = { workspace = true } +orml-traits = { workspace = true, features = ["default"] } +rand = { workspace = true, features = ["default"] } +sp-runtime = { workspace = true, features = ["default"] } +zeitgeist-primitives = { workspace = true, features = ["default", "mock"] } +zrml-combinatorial-tokens = { workspace = true, features = ["default", "mock"] } + +[package] +authors = ["Forecasting Technologies Ltd"] +edition.workspace = true +name = "zrml-combinatorial-tokens-fuzz" +publish = false +version = "0.5.5" + +[package.metadata] +cargo-fuzz = true diff --git a/zrml/combinatorial-tokens/fuzz/common.rs b/zrml/combinatorial-tokens/fuzz/common.rs new file mode 100644 index 000000000..a1a5cd138 --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/common.rs @@ -0,0 +1,52 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use zeitgeist_primitives::{ + traits::MarketOf, + types::{Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, +}; +use zrml_combinatorial_tokens::{AssetOf, Config, MarketIdOf}; + +pub(crate) fn market( + market_id: MarketIdOf, + base_asset: AssetOf, + market_type: MarketType, +) -> MarketOf<::MarketCommons> +where + T: Config, + ::AccountId: Default, +{ + Market { + market_id, + base_asset, + creator: Default::default(), + creation: MarketCreation::Permissionless, + creator_fee: Default::default(), + oracle: Default::default(), + metadata: Default::default(), + market_type, + period: MarketPeriod::Block(0u8.into()..10u8.into()), + deadlines: Default::default(), + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Active, + report: None, + resolved_outcome: None, + dispute_mechanism: None, + bonds: Default::default(), + early_close: None, + } +} diff --git a/zrml/combinatorial-tokens/fuzz/merge_position.rs b/zrml/combinatorial-tokens/fuzz/merge_position.rs new file mode 100644 index 000000000..97e9696de --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/merge_position.rs @@ -0,0 +1,137 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + traits::{CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, MarketType}, +}; +use zrml_combinatorial_tokens::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Runtime, RuntimeOrigin}, + }, + AccountIdOf, BalanceOf, CombinatorialIdOf, Config, FuelOf, MarketIdOf, +}; + +#[derive(Debug)] +struct MergePositionFuzzParams { + account_id: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + fuel: FuelOf, +} + +impl<'a> Arbitrary<'a> for MergePositionFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let parent_collection_id = Arbitrary::arbitrary(u)?; + let market_id = 0u8.into(); + let amount = Arbitrary::arbitrary(u)?; + let fuel = FuelOf::::from_total(u.int_in_range(1..=100)?); + + // Note: This might result in members of unequal length, but that's OK. + let min_len = 0; + let max_len = 10; + let len = u.int_in_range(0..=max_len)?; + let partition = + (min_len..len).map(|_| Arbitrary::arbitrary(u)).collect::>>()?; + + let params = MergePositionFuzzParams { + account_id, + parent_collection_id, + market_id, + partition, + amount, + fuel, + }; + + Ok(params) + } +} + +fuzz_target!(|params: MergePositionFuzzParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + // We create a market and equip the user with the tokens they require to make the + // `merge_position` call meaningful, and deposit collateral in the pallet account. + let collateral = Asset::Ztg; + let asset_count = if let Some(member) = params.partition.first() { + member.len().max(2) as u16 + } else { + 2u16 // In this case the index set doesn't fit the market. + }; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + + let positions = params + .partition + .iter() + .cloned() + .map(|index_set| { + CombinatorialTokens::position_from_parent_collection( + params.parent_collection_id, + params.market_id, + index_set, + FuelOf::::from_total(16), + ) + }) + .collect::, _>>() + .unwrap(); + for &position in positions.iter() { + <::MultiCurrency>::deposit( + position, + ¶ms.account_id, + params.amount, + ) + .unwrap(); + } + + // Is not required if `parent_collection_id.is_some()`, but we're doing it anyways. + <::MultiCurrency>::deposit( + collateral, + &CombinatorialTokens::account_id(), + params.amount, + ) + .unwrap(); + + let _ = CombinatorialTokens::merge_position( + RuntimeOrigin::signed(params.account_id), + params.parent_collection_id, + params.market_id, + params.partition, + params.amount, + params.fuel, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/combinatorial-tokens/fuzz/redeem_position.rs b/zrml/combinatorial-tokens/fuzz/redeem_position.rs new file mode 100644 index 000000000..67f2c199e --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/redeem_position.rs @@ -0,0 +1,136 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::{CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, MarketType}, +}; +use zrml_combinatorial_tokens::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Runtime, RuntimeOrigin}, + types::MockPayout, + }, + traits::CombinatorialIdManager, + AccountIdOf, BalanceOf, CombinatorialIdOf, Config, FuelOf, MarketIdOf, +}; + +#[derive(Debug)] +struct RedeemPositionFuzzParams { + account_id: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + index_set: Vec, + fuel: FuelOf, + payout_vector: Option>>, + amount: BalanceOf, +} + +impl<'a> Arbitrary<'a> for RedeemPositionFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let parent_collection_id = Arbitrary::arbitrary(u)?; + let market_id = 0u8.into(); + let amount = Arbitrary::arbitrary(u)?; + let fuel = FuelOf::::from_total(u.int_in_range(1..=100)?); + + let min_len = 2; + let max_len = 1000; + let len = u.int_in_range(0..=max_len)?; + let index_set = + (min_len..len).map(|_| bool::arbitrary(u)).collect::>>()?; + + // Clamp every value of the payout vector to [0..1]. That doesn't ensure that the payout + // vector is valid, but it's valid enough to avoid most overflows. + let payout_vector = Some( + (min_len..len) + .map(|_| Ok(u128::arbitrary(u)? % _1)) + .collect::>>()?, + ); + + let params = RedeemPositionFuzzParams { + account_id, + parent_collection_id, + market_id, + index_set, + fuel, + payout_vector, + amount, + }; + + Ok(params) + } +} + +fuzz_target!(|params: RedeemPositionFuzzParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + // We create a market and equip the user with the tokens they require to make the + // `redeem_position` call meaningful. We also provide the pallet account with collateral in + // case it's required. + let collateral = Asset::Ztg; + let asset_count = params.index_set.len() as u16; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + + let position = if let Some(pci) = params.parent_collection_id { + let position_id = + ::CombinatorialIdManager::get_position_id(collateral, pci); + + Asset::CombinatorialToken(position_id) + } else { + Asset::Ztg + }; + <::MultiCurrency>::deposit(position, ¶ms.account_id, params.amount) + .unwrap(); + + // Is not required if `parent_collection_id.is_some()`, but we're doing it anyways. + <::MultiCurrency>::deposit( + collateral, + &CombinatorialTokens::account_id(), + params.amount * asset_count as u128, + ) + .unwrap(); + + // Mock up the payout vector. + MockPayout::set_return_value(params.payout_vector); + + let _ = CombinatorialTokens::redeem_position( + RuntimeOrigin::signed(params.account_id), + params.parent_collection_id, + params.market_id, + params.index_set, + params.fuel, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/combinatorial-tokens/fuzz/split_position.rs b/zrml/combinatorial-tokens/fuzz/split_position.rs new file mode 100644 index 000000000..9cf2e1f8f --- /dev/null +++ b/zrml/combinatorial-tokens/fuzz/split_position.rs @@ -0,0 +1,118 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + traits::{CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, MarketType}, +}; +use zrml_combinatorial_tokens::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Runtime, RuntimeOrigin}, + }, + traits::CombinatorialIdManager, + AccountIdOf, BalanceOf, CombinatorialIdOf, Config, FuelOf, MarketIdOf, +}; + +#[derive(Debug)] +struct SplitPositionFuzzParams { + account_id: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + fuel: FuelOf, +} + +impl<'a> Arbitrary<'a> for SplitPositionFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let parent_collection_id = Arbitrary::arbitrary(u)?; + let market_id = 0u8.into(); + let amount = Arbitrary::arbitrary(u)?; + let fuel = FuelOf::::from_total(u.int_in_range(1..=100)?); + + // Note: This might result in members of unequal length, but that's OK. + let min_len = 0; + let max_len = 10; + let len = u.int_in_range(0..=max_len)?; + let partition = + (min_len..len).map(|_| Arbitrary::arbitrary(u)).collect::>>()?; + + let params = SplitPositionFuzzParams { + account_id, + parent_collection_id, + market_id, + partition, + amount, + fuel, + }; + + Ok(params) + } +} + +fuzz_target!(|params: SplitPositionFuzzParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + // We create a market and equip the user with the tokens they require to make the + // `split_position` call meaningful. + let collateral = Asset::Ztg; + let asset_count = if let Some(member) = params.partition.first() { + member.len().max(2) as u16 + } else { + 2u16 // In this case the index set doesn't fit the market. + }; + let market = common::market::( + params.market_id, + collateral, + MarketType::Categorical(asset_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + + let position = if let Some(pci) = params.parent_collection_id { + let position_id = + ::CombinatorialIdManager::get_position_id(collateral, pci); + + Asset::CombinatorialToken(position_id) + } else { + Asset::Ztg + }; + <::MultiCurrency>::deposit(position, ¶ms.account_id, params.amount) + .unwrap(); + + let _ = CombinatorialTokens::split_position( + RuntimeOrigin::signed(params.account_id), + params.parent_collection_id, + params.market_id, + params.partition, + params.amount, + params.fuel, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/combinatorial-tokens/src/benchmarking.rs b/zrml/combinatorial-tokens/src/benchmarking.rs new file mode 100644 index 000000000..68fe22a97 --- /dev/null +++ b/zrml/combinatorial-tokens/src/benchmarking.rs @@ -0,0 +1,602 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use crate::{BalanceOf, Call, Config, Event, MarketIdOf, Pallet}; +use alloc::{vec, vec::Vec}; +use frame_benchmarking::v2::*; +use frame_support::dispatch::RawOrigin; +use frame_system::Pallet as System; +use orml_traits::MultiCurrency; +use sp_runtime::{traits::Zero, Perbill}; +use zeitgeist_primitives::{ + math::fixed::{BaseProvider, ZeitgeistBase}, + traits::{CombinatorialTokensBenchmarkHelper, CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, +}; + +fn create_market(caller: T::AccountId, asset_count: u16) -> MarketIdOf { + let market = Market { + market_id: Default::default(), + base_asset: Asset::Ztg, + creation: MarketCreation::Permissionless, + creator_fee: Perbill::zero(), + creator: caller.clone(), + oracle: caller, + metadata: Default::default(), + market_type: MarketType::Categorical(asset_count), + period: MarketPeriod::Block(0u32.into()..1u32.into()), + deadlines: Default::default(), + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Active, + report: None, + resolved_outcome: None, + dispute_mechanism: None, + bonds: Default::default(), + early_close: None, + }; + T::MarketCommons::push_market(market).unwrap() +} + +fn create_payout_vector(asset_count: u16) -> Vec> { + let mut result = vec![Zero::zero(); asset_count as usize]; + result[0] = ZeitgeistBase::get().unwrap(); + + result +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn split_position_vertical_sans_parent(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let position_count: usize = n.try_into().unwrap(); + let total = m; + + let parent_collection_id = None; + let market_id = create_market::(alice.clone(), position_count.try_into().unwrap()); + // Partition is 10...0, 010...0, ..., 0...01. + let partition: Vec<_> = (0..position_count) + .map(|index| { + let mut index_set = vec![false; position_count]; + index_set[index] = true; + + index_set + }) + .collect(); + let amount = ZeitgeistBase::get().unwrap(); + + T::MultiCurrency::deposit(Asset::Ztg, &alice, amount).unwrap(); + + #[extrinsic_call] + split_position( + RawOrigin::Signed(alice.clone()), + parent_collection_id, + market_id, + partition.clone(), + amount, + T::Fuel::from_total(total), + ); + + let collection_ids: Vec<_> = partition + .iter() + .cloned() + .map(|index_set| { + Pallet::::collection_id_from_parent_collection( + parent_collection_id, + market_id, + index_set, + T::Fuel::from_total(total), + ) + .unwrap() + }) + .collect(); + let assets_out: Vec<_> = collection_ids + .iter() + .cloned() + .map(|collection_id| { + Pallet::::position_from_collection_id(market_id, collection_id).unwrap() + }) + .collect(); + let expected_event = ::RuntimeEvent::from(Event::::TokenSplit { + who: alice, + parent_collection_id, + market_id, + partition, + asset_in: Asset::Ztg, + assets_out, + collection_ids, + amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn split_position_vertical_with_parent(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let position_count: usize = n.try_into().unwrap(); + let total = m; + + let parent_collection_id = None; + let parent_market_id = create_market::(alice.clone(), 2); + + // The collection/position that we're merging into. + let cid_01 = Pallet::::collection_id_from_parent_collection( + parent_collection_id, + parent_market_id, + vec![false, true], + T::Fuel::from_total(total), + ) + .unwrap(); + let pos_01 = Pallet::::position_from_collection_id(parent_market_id, cid_01).unwrap(); + + let child_market_id = create_market::(alice.clone(), position_count.try_into().unwrap()); + let partition: Vec<_> = (0..position_count) + .map(|index| { + let mut index_set = vec![false; position_count]; + index_set[index] = true; + + index_set + }) + .collect(); + let amount = ZeitgeistBase::get().unwrap(); + + T::MultiCurrency::deposit(pos_01, &alice, amount).unwrap(); + + #[extrinsic_call] + split_position( + RawOrigin::Signed(alice.clone()), + Some(cid_01), + child_market_id, + partition.clone(), + amount, + T::Fuel::from_total(total), + ); + + let collection_ids: Vec<_> = partition + .iter() + .cloned() + .map(|index_set| { + Pallet::::collection_id_from_parent_collection( + Some(cid_01), + child_market_id, + index_set, + T::Fuel::from_total(total), + ) + .unwrap() + }) + .collect(); + let assets_out: Vec<_> = collection_ids + .iter() + .cloned() + .map(|collection_id| { + Pallet::::position_from_collection_id(child_market_id, collection_id).unwrap() + }) + .collect(); + let expected_event = ::RuntimeEvent::from(Event::::TokenSplit { + who: alice, + parent_collection_id: Some(cid_01), + market_id: child_market_id, + partition, + asset_in: pos_01, + assets_out, + collection_ids, + amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn split_position_horizontal(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let position_count: usize = n.try_into().unwrap(); + let asset_count = position_count + 1; + let total = m; + + let parent_collection_id = None; + let market_id = create_market::(alice.clone(), asset_count.try_into().unwrap()); + // Partition is 10...0, 010...0, ..., 0...010. Doesn't contain 0...01. + let partition: Vec<_> = (0..position_count) + .map(|index| { + let mut index_set = vec![false; asset_count]; + index_set[index] = true; + + index_set + }) + .collect(); + let amount = ZeitgeistBase::get().unwrap(); + + // Add 1...10 to Alice's account. + let mut asset_in_index_set = vec![true; asset_count]; + *asset_in_index_set.last_mut().unwrap() = false; + let asset_in = Pallet::::position_from_parent_collection( + parent_collection_id, + market_id, + asset_in_index_set, + T::Fuel::from_total(total), + ) + .unwrap(); + T::MultiCurrency::deposit(asset_in, &alice, amount).unwrap(); + + #[extrinsic_call] + split_position( + RawOrigin::Signed(alice.clone()), + parent_collection_id, + market_id, + partition.clone(), + amount, + T::Fuel::from_total(total), + ); + + let collection_ids: Vec<_> = partition + .iter() + .cloned() + .map(|index_set| { + Pallet::::collection_id_from_parent_collection( + parent_collection_id, + market_id, + index_set, + T::Fuel::from_total(total), + ) + .unwrap() + }) + .collect(); + let assets_out: Vec<_> = collection_ids + .iter() + .cloned() + .map(|collection_id| { + Pallet::::position_from_collection_id(market_id, collection_id).unwrap() + }) + .collect(); + let expected_event = ::RuntimeEvent::from(Event::::TokenSplit { + who: alice, + parent_collection_id, + market_id, + partition, + asset_in, + assets_out, + collection_ids, + amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn merge_position_vertical_sans_parent(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let position_count: usize = n.try_into().unwrap(); + let total = m; + + let parent_collection_id = None; + let market_id = create_market::(alice.clone(), position_count.try_into().unwrap()); + let partition: Vec<_> = (0..position_count) + .map(|index| { + let mut index_set = vec![false; position_count]; + index_set[index] = true; + + index_set + }) + .collect(); + let amount = ZeitgeistBase::get().unwrap(); + + let assets_in: Vec<_> = partition + .iter() + .cloned() + .map(|index_set| { + Pallet::::position_from_parent_collection( + parent_collection_id, + market_id, + index_set, + T::Fuel::from_total(total), + ) + .unwrap() + }) + .collect(); + + for &asset in assets_in.iter() { + T::MultiCurrency::deposit(asset, &alice, amount).unwrap(); + } + T::MultiCurrency::deposit(Asset::Ztg, &Pallet::::account_id(), amount).unwrap(); + + #[extrinsic_call] + merge_position( + RawOrigin::Signed(alice.clone()), + parent_collection_id, + market_id, + partition.clone(), + amount, + T::Fuel::from_total(total), + ); + + let expected_event = ::RuntimeEvent::from(Event::::TokenMerged { + who: alice, + parent_collection_id, + market_id, + partition, + asset_out: Asset::Ztg, + assets_in, + amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn merge_position_vertical_with_parent(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let position_count: usize = n.try_into().unwrap(); + let total = m; + + let parent_collection_id = None; + let parent_market_id = create_market::(alice.clone(), 2); + + // The collection/position that we're merging into. + let cid_01 = Pallet::::collection_id_from_parent_collection( + parent_collection_id, + parent_market_id, + vec![false, true], + T::Fuel::from_total(total), + ) + .unwrap(); + let pos_01 = Pallet::::position_from_collection_id(parent_market_id, cid_01).unwrap(); + + let child_market_id = create_market::(alice.clone(), position_count.try_into().unwrap()); + let partition: Vec<_> = (0..position_count) + .map(|index| { + let mut index_set = vec![false; position_count]; + index_set[index] = true; + + index_set + }) + .collect(); + let amount = ZeitgeistBase::get().unwrap(); + + let assets_in: Vec<_> = partition + .iter() + .cloned() + .map(|index_set| { + Pallet::::position_from_parent_collection( + Some(cid_01), + child_market_id, + index_set, + T::Fuel::from_total(total), + ) + .unwrap() + }) + .collect(); + + for &asset in assets_in.iter() { + T::MultiCurrency::deposit(asset, &alice, amount).unwrap(); + } + + #[extrinsic_call] + merge_position( + RawOrigin::Signed(alice.clone()), + Some(cid_01), + child_market_id, + partition.clone(), + amount, + T::Fuel::from_total(total), + ); + + let expected_event = ::RuntimeEvent::from(Event::::TokenMerged { + who: alice, + parent_collection_id: Some(cid_01), + market_id: child_market_id, + partition, + asset_out: pos_01, + assets_in, + amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn merge_position_horizontal(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let position_count: usize = n.try_into().unwrap(); + let asset_count = position_count + 1; + let total = m; + + let parent_collection_id = None; + let market_id = create_market::(alice.clone(), asset_count.try_into().unwrap()); + // Partition is 10...0, 010...0, ..., 0...010. Doesn't contain 0...01. + let partition: Vec<_> = (0..position_count) + .map(|index| { + let mut index_set = vec![false; asset_count]; + index_set[index] = true; + + index_set + }) + .collect(); + let amount = ZeitgeistBase::get().unwrap(); + + let assets_in: Vec<_> = partition + .iter() + .cloned() + .map(|index_set| { + Pallet::::position_from_parent_collection( + parent_collection_id, + market_id, + index_set, + T::Fuel::from_total(total), + ) + .unwrap() + }) + .collect(); + + for &asset in assets_in.iter() { + T::MultiCurrency::deposit(asset, &alice, amount).unwrap(); + } + + #[extrinsic_call] + merge_position( + RawOrigin::Signed(alice.clone()), + parent_collection_id, + market_id, + partition.clone(), + amount, + T::Fuel::from_total(total), + ); + + let mut asset_out_index_set = vec![true; asset_count]; + *asset_out_index_set.last_mut().unwrap() = false; + let asset_out = Pallet::::position_from_parent_collection( + parent_collection_id, + market_id, + asset_out_index_set, + T::Fuel::from_total(total), + ) + .unwrap(); + let expected_event = ::RuntimeEvent::from(Event::::TokenMerged { + who: alice, + parent_collection_id, + market_id, + partition, + asset_out, + assets_in, + amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn redeem_position_sans_parent(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let n_u16: u16 = n.try_into().unwrap(); + let asset_count = n_u16 + 1; + let total = m; + + // `index_set` has `n` entries that are `true`, which results in `n` iterations in the `for` + // loop in `redeem_position`. + let mut index_set = vec![true; asset_count as usize]; + *index_set.last_mut().unwrap() = false; + + let parent_collection_id = None; + let market_id = create_market::(alice.clone(), asset_count); + + let payout_vector = create_payout_vector::(asset_count); + T::BenchmarkHelper::setup_payout_vector(market_id, Some(payout_vector)).unwrap(); + + // Deposit tokens for Alice and the pallet account. + let position = Pallet::::position_from_parent_collection( + parent_collection_id, + market_id, + index_set.clone(), + T::Fuel::from_total(total), + ) + .unwrap(); + let amount = ZeitgeistBase::get().unwrap(); + T::MultiCurrency::deposit(position, &alice, amount).unwrap(); + T::MultiCurrency::deposit(Asset::Ztg, &Pallet::::account_id(), amount).unwrap(); + + #[extrinsic_call] + redeem_position( + RawOrigin::Signed(alice.clone()), + parent_collection_id, + market_id, + index_set.clone(), + T::Fuel::from_total(total), + ); + + let expected_event = ::RuntimeEvent::from(Event::::TokenRedeemed { + who: alice, + parent_collection_id, + market_id, + index_set, + asset_in: position, + amount_in: amount, + asset_out: Asset::Ztg, + amount_out: amount, + }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn redeem_position_with_parent(n: Linear<2, 32>, m: Linear<32, 64>) { + let alice: T::AccountId = whitelisted_caller(); + + let n_u16: u16 = n.try_into().unwrap(); + let asset_count = n_u16 + 1; + let total = m; + + // `index_set` has `n` entries that are `true`, which results in `n` iterations in the `for` + // loop in `redeem_position`. + let mut index_set = vec![true; asset_count as usize]; + *index_set.last_mut().unwrap() = false; + + let parent_market_id = create_market::(alice.clone(), 2); + let cid_01 = Pallet::::collection_id_from_parent_collection( + None, + parent_market_id, + vec![false, true], + T::Fuel::from_total(total), + ) + .unwrap(); + let pos_01 = Pallet::::position_from_collection_id(parent_market_id, cid_01).unwrap(); + + let child_market_id = create_market::(alice.clone(), asset_count); + let pos_01_10 = Pallet::::position_from_parent_collection( + Some(cid_01), + child_market_id, + index_set.clone(), + T::Fuel::from_total(total), + ) + .unwrap(); + let amount = ZeitgeistBase::get().unwrap(); + T::MultiCurrency::deposit(pos_01_10, &alice, amount).unwrap(); + + let payout_vector = create_payout_vector::(asset_count); + T::BenchmarkHelper::setup_payout_vector(child_market_id, Some(payout_vector)).unwrap(); + + #[extrinsic_call] + redeem_position( + RawOrigin::Signed(alice.clone()), + Some(cid_01), + child_market_id, + index_set.clone(), + T::Fuel::from_total(total), + ); + + let expected_event = ::RuntimeEvent::from(Event::::TokenRedeemed { + who: alice, + parent_collection_id: Some(cid_01), + market_id: child_market_id, + index_set, + asset_in: pos_01_10, + amount_in: amount, + asset_out: pos_01, + amount_out: amount, + }); + System::::assert_last_event(expected_event.into()); + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::ext_builder::ExtBuilder::build(), + crate::mock::runtime::Runtime + ); +} diff --git a/zrml/combinatorial-tokens/src/lib.rs b/zrml/combinatorial-tokens/src/lib.rs new file mode 100644 index 000000000..56283f2ea --- /dev/null +++ b/zrml/combinatorial-tokens/src/lib.rs @@ -0,0 +1,810 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work licensed under the GNU Lesser General +// Public License 3.0 but published without copyright notice by Gnosis +// (, info@gnosis.io) in the +// conditional-tokens-contracts repository +// , +// and has been relicensed under GPL-3.0-or-later in this repository. + +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod benchmarking; +pub mod mock; +mod tests; +pub mod traits; +pub mod types; +pub mod weights; + +pub use pallet::*; + +#[frame_support::pallet] +mod pallet { + use crate::{ + traits::CombinatorialIdManager, + types::{CollectionIdError, TransmutationType}, + weights::WeightInfoZeitgeist, + }; + use alloc::{vec, vec::Vec}; + use core::{fmt::Debug, marker::PhantomData}; + use frame_support::{ + ensure, + pallet_prelude::{DispatchResultWithPostInfo, IsType, StorageVersion}, + require_transactional, transactional, PalletId, + }; + use frame_system::{ + ensure_signed, + pallet_prelude::{BlockNumberFor, OriginFor}, + }; + use orml_traits::MultiCurrency; + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + use sp_runtime::{ + traits::{AccountIdConversion, Get, Zero}, + DispatchError, DispatchResult, SaturatedConversion, + }; + use zeitgeist_primitives::{ + math::{checked_ops_res::CheckedAddRes, fixed::FixedMul}, + traits::{ + CombinatorialTokensApi, CombinatorialTokensFuel, CombinatorialTokensUnsafeApi, + MarketCommonsPalletApi, PayoutApi, + }, + types::{Asset, CombinatorialId, SplitPositionDispatchInfo}, + }; + + #[cfg(feature = "runtime-benchmarks")] + use zeitgeist_primitives::traits::CombinatorialTokensBenchmarkHelper; + + #[pallet::config] + pub trait Config: frame_system::Config { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: CombinatorialTokensBenchmarkHelper< + Balance = BalanceOf, + MarketId = MarketIdOf, + >; + + /// Interface for calculating collection and position IDs. + type CombinatorialIdManager: CombinatorialIdManager< + Asset = AssetOf, + MarketId = MarketIdOf, + CombinatorialId = CombinatorialId, + Fuel = Self::Fuel, + >; + + type Fuel: Clone + + CombinatorialTokensFuel + + Debug + + Decode + + Encode + + Eq + + MaxEncodedLen + + PartialEq + + TypeInfo; + + type MarketCommons: MarketCommonsPalletApi>; + + type MultiCurrency: MultiCurrency>; + + /// Interface for acquiring the payout vector by market ID. + type Payout: PayoutApi, MarketId = MarketIdOf>; + + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + #[pallet::constant] + type PalletId: Get; + + type WeightInfo: WeightInfoZeitgeist; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData); + + pub type AccountIdOf = ::AccountId; + pub type AssetOf = Asset>; + pub type BalanceOf = + <::MultiCurrency as MultiCurrency>>::Balance; + pub type CombinatorialIdOf = + <::CombinatorialIdManager as CombinatorialIdManager>::CombinatorialId; + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub type FuelOf = <::CombinatorialIdManager as CombinatorialIdManager>::Fuel; + pub(crate) type SplitPositionDispatchInfoOf = + SplitPositionDispatchInfo, MarketIdOf>; + + pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event + where + T: Config, + { + /// User `who` has split `amount` units of token `asset_in` into the same amount of each + /// token in `assets_out` using `partition`. The ith element of `partition` matches the ith + /// element of `assets_out`, so `assets_out[i]` is the outcome represented by the specified + /// `parent_collection_id` when split using `partition[i]` in `market_id`. The same goes for + /// the `collection_ids` vector, the ith element of which specifies the collection ID of + /// `assets_out[i]`. + TokenSplit { + who: AccountIdOf, + parent_collection_id: Option, + market_id: MarketIdOf, + partition: Vec>, + asset_in: AssetOf, + assets_out: Vec>, + collection_ids: Vec, + amount: BalanceOf, + }, + + /// User `who` has merged `amount` units of each of the tokens in `assets_in` into the same + /// amount of `asset_out`. The ith element of the `partition` matches the ith element of + /// `assets_in`, so `assets_in[i]` is the outcome represented by the specified + /// `parent_collection_id` when split using `partition[i]` in `market_id`. Note that the + /// `parent_collection_id` is equal to the collection ID of the position `asset_out`; if + /// `asset_out` is the collateral token, then `parent_collection_id` is `None`. + TokenMerged { + who: AccountIdOf, + parent_collection_id: Option, + market_id: MarketIdOf, + partition: Vec>, + asset_out: AssetOf, + assets_in: Vec>, + amount: BalanceOf, + }, + + /// User `who` has redeemed `amount_in` units of `asset_in` for `amount_out` units of + /// `asset_out` using the report for the market specified by `market_id`. The + /// `parent_collection_id` specifies the collection ID of the `asset_out`; it is `None` if + /// the `asset_out` is the collateral token. + TokenRedeemed { + who: AccountIdOf, + parent_collection_id: Option, + market_id: MarketIdOf, + index_set: Vec, + asset_in: AssetOf, + amount_in: BalanceOf, + asset_out: AssetOf, + amount_out: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// An error for the collection ID retrieval occured. + CollectionIdRetrievalFailed(CollectionIdError), + + /// Specified index set is trival, empty, or doesn't match the market's number of outcomes. + InvalidIndexSet, + + /// Specified partition is empty, contains overlaps, is too long or doesn't match the + /// market's number of outcomes. + InvalidPartition, + + /// Specified market is not resolved. + PayoutVectorNotFound, + + /// Account holds no tokens of this type. + NoTokensFound, + + /// Specified token holds no redeemable value. + TokenHasNoValue, + + /// Something unexpected happened. You shouldn't see this. + UnexpectedError, + } + + #[pallet::call] + impl Pallet { + /// Split `amount` units of the position specified by `parent_collection_id` over the market + /// with ID `market_id` according to the given `partition`. + /// + /// The `partition` is specified as a vector whose elements are equal-length `Vec`. A + /// `true` entry at the `i`th index of a partition element means that the `i`th outcome + /// token of the market is contained in this element of the partition. + /// + /// For each element `b` of the partition, the split mints a new outcome token which is made + /// up of the position to be split and the conjunction `(x|...|z)` where `x, ..., z` are the + /// items of `b`. The position to be split, in turn, is burned or transferred into the + /// pallet account, depending on whether or not it is a true combinatorial token or + /// collateral. + /// + /// If the `parent_collection_id` is `None`, then the position split is the collateral of the + /// market given by `market_id`. + /// + /// If the `parent_collection_id` is `Some(pid)`, then there are two cases: vertical and + /// horizontal split. If `partition` is complete (i.e. there is no index `i` so that `b[i]` + /// is `false` for all `b` in `partition`), the position split is the position obtained by + /// combining `pid` with the collateral of the market given by `market_id`. If `partition` + /// is not complete, the position split is the position made up of the + /// `parent_collection_id` and the conjunction `(x|...|z)` where `x, ..., z` are the items + /// covered by `partition`. + /// + /// The `fuel` parameter specifies how much work the cryptographic id manager will do + /// and can be used for benchmarking purposes. + #[pallet::call_index(0)] + #[pallet::weight( + T::WeightInfo::split_position_vertical_sans_parent( + partition.len().saturated_into(), + fuel.total(), + ) + .max(T::WeightInfo::split_position_vertical_with_parent( + partition.len().saturated_into(), + fuel.total(), + )) + .max(T::WeightInfo::split_position_horizontal( + partition.len().saturated_into(), + fuel.total(), + )) + )] + #[transactional] + pub fn split_position( + origin: OriginFor, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + fuel: FuelOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let SplitPositionDispatchInfo { post_dispatch_info, .. } = Self::do_split_position( + who, + parent_collection_id, + market_id, + partition, + amount, + fuel, + )?; + + DispatchResultWithPostInfo::Ok(post_dispatch_info) + } + + /// Merge `amount` units of the tokens obtained by splitting `parent_collection_id` using + /// `partition` into the position specified by `parent_collection_id` (vertical split) or + /// the position obtained by splitting `parent_collection_id` according to `partiton` over + /// the market with ID `market_id` (horizontal; see below for details). + /// + /// The `partition` is specified as a vector whose elements are equal-length `Vec`. A + /// `true` entry at the `i`th index of a partition element means that the `i`th outcome + /// token of the market is contained in this element of the partition. + /// + /// For each element `b` of the partition, the split burns the outcome tokens which are made + /// up of the position to be split and the conjunction `(x|...|z)` where `x, ..., z` are the + /// items of `b`. The position given by `parent_collection_id` is + /// + /// If the `parent_collection_id` is `None`, then the position split is the collateral of the + /// market given by `market_id`. + /// + /// If the `parent_collection_id` is `Some(pid)`, then there are two cases: vertical and + /// horizontal merge. If `partition` is complete (i.e. there is no index `i` so that `b[i]` + /// is `false` for all `b` in `partition`), the the result of the merge is the position + /// defined by `parent_collection_id`. If `partition` is not complete, the result of the + /// merge is the position made up of the `parent_collection_id` and the conjunction + /// `(x|...|z)` where `x, ..., z` are the items covered by `partition`. + /// + /// The `fuel` parameter specifies how much work the cryptographic id manager will do + /// and can be used for benchmarking purposes. + #[pallet::call_index(1)] + #[pallet::weight( + T::WeightInfo::merge_position_vertical_sans_parent( + partition.len().saturated_into(), + fuel.total(), + ) + .max(T::WeightInfo::merge_position_vertical_with_parent( + partition.len().saturated_into(), + fuel.total(), + )) + .max(T::WeightInfo::merge_position_horizontal( + partition.len().saturated_into(), + fuel.total(), + )) + )] + #[transactional] + pub fn merge_position( + origin: OriginFor, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + fuel: FuelOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_merge_position(who, parent_collection_id, market_id, partition, amount, fuel) + } + + /// (Partially) redeems a position if part of it belongs to a resolved market given by + /// `market_id`. + /// + /// The position to be redeemed is the position obtained by combining the position given by + /// `parent_collection_id` and `collateral` with the conjunction `(x|...|z)` where `x, ... + /// z` are the outcome tokens of the market `market_id` given by `partition`. + /// + /// The position to be redeemed is completely removed from the origin's wallet. According to + /// how much the conjunction `(x|...|z)` is valued, the user is paid in the position defined + /// by `parent_collection_id` and `collateral`. + /// + /// The `fuel` parameter specifies how much work the cryptographic id manager will do + /// and can be used for benchmarking purposes. + #[pallet::call_index(2)] + #[pallet::weight( + T::WeightInfo::redeem_position_with_parent( + index_set.len().saturated_into(), + fuel.total(), + ) + .max(T::WeightInfo::redeem_position_sans_parent( + index_set.len().saturated_into(), + fuel.total() + )) + )] + #[transactional] + pub fn redeem_position( + origin: OriginFor, + parent_collection_id: Option>, + market_id: MarketIdOf, + index_set: Vec, + fuel: FuelOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_redeem_position(who, parent_collection_id, market_id, index_set, fuel) + } + } + + impl Pallet { + #[require_transactional] + fn do_split_position( + who: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + fuel: FuelOf, + ) -> Result, DispatchError> { + let (transmutation_type, position) = Self::transmutation_asset( + parent_collection_id, + market_id, + partition.clone(), + fuel.clone(), + )?; + + // Destroy the token to be split. + let weight = match transmutation_type { + TransmutationType::VerticalWithParent => { + // Split combinatorial token into higher level position. + // This will fail if the market has a different collateral than the previous + // markets. + T::MultiCurrency::ensure_can_withdraw(position, &who, amount)?; + T::MultiCurrency::withdraw(position, &who, amount)?; + + T::WeightInfo::split_position_vertical_with_parent( + partition.len().saturated_into(), + fuel.total(), + ) + } + TransmutationType::VerticalSansParent => { + // Split collateral into first level position. Store the collateral in the + // pallet account. This is the legacy `buy_complete_set`. + T::MultiCurrency::ensure_can_withdraw(position, &who, amount)?; + T::MultiCurrency::transfer(position, &who, &Self::account_id(), amount)?; + + T::WeightInfo::split_position_vertical_sans_parent( + partition.len().saturated_into(), + fuel.total(), + ) + } + TransmutationType::Horizontal => { + // Horizontal split. + T::MultiCurrency::ensure_can_withdraw(position, &who, amount)?; + T::MultiCurrency::withdraw(position, &who, amount)?; + + T::WeightInfo::split_position_horizontal( + partition.len().saturated_into(), + fuel.total(), + ) + } + }; + + // Deposit the new tokens. + let collection_ids = partition + .iter() + .cloned() + .map(|index_set| { + Self::collection_id_from_parent_collection( + parent_collection_id, + market_id, + index_set, + fuel.clone(), + ) + }) + .collect::, _>>()?; + let positions = collection_ids + .iter() + .cloned() + .map(|collection_id| Self::position_from_collection_id(market_id, collection_id)) + .collect::, _>>()?; + // Security note: Safe as iterations are limited to the number of assets in the market + // thanks to the `ensure!` invocations in `Self::free_index_set`. + for &position in positions.iter() { + T::MultiCurrency::deposit(position, &who, amount)?; + } + + Self::deposit_event(Event::::TokenSplit { + who, + parent_collection_id, + market_id, + partition, + asset_in: position, + assets_out: positions.clone(), + collection_ids: collection_ids.clone(), + amount, + }); + + let dispatch_info = SplitPositionDispatchInfo { + collection_ids, + position_ids: positions, + post_dispatch_info: Some(weight).into(), + }; + + Ok(dispatch_info) + } + + #[require_transactional] + fn do_merge_position( + who: AccountIdOf, + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + amount: BalanceOf, + fuel: FuelOf, + ) -> DispatchResultWithPostInfo { + let (transmutation_type, position) = Self::transmutation_asset( + parent_collection_id, + market_id, + partition.clone(), + fuel.clone(), + )?; + + // Destroy the old tokens. + let positions = partition + .iter() + .cloned() + .map(|index_set| { + Self::position_from_parent_collection( + parent_collection_id, + market_id, + index_set, + fuel.clone(), + ) + }) + .collect::, _>>()?; + // Security note: Safe as iterations are limited to the number of assets in the market + // thanks to the `ensure!` invocations in `Self::free_index_set`. + for &position in positions.iter() { + T::MultiCurrency::withdraw(position, &who, amount)?; + } + + let weight = match transmutation_type { + TransmutationType::VerticalWithParent => { + // Merge combinatorial token into higher level position. + T::MultiCurrency::deposit(position, &who, amount)?; + + T::WeightInfo::merge_position_vertical_with_parent( + partition.len().saturated_into(), + fuel.total(), + ) + } + TransmutationType::VerticalSansParent => { + // Merge first-level tokens into collateral. Move collateral from the pallet + // account to the user's wallet. This is the legacy `sell_complete_set`. + T::MultiCurrency::transfer(position, &Self::account_id(), &who, amount)?; + + T::WeightInfo::merge_position_vertical_sans_parent( + partition.len().saturated_into(), + fuel.total(), + ) + } + TransmutationType::Horizontal => { + // Horizontal merge. + T::MultiCurrency::deposit(position, &who, amount)?; + + T::WeightInfo::merge_position_horizontal( + partition.len().saturated_into(), + fuel.total(), + ) + } + }; + + Self::deposit_event(Event::::TokenMerged { + who, + parent_collection_id, + market_id, + partition, + asset_out: position, + assets_in: positions, + amount, + }); + + Ok(Some(weight).into()) + } + + fn do_redeem_position( + who: T::AccountId, + parent_collection_id: Option>, + market_id: MarketIdOf, + index_set: Vec, + fuel: FuelOf, + ) -> DispatchResultWithPostInfo { + let payout_vector = + T::Payout::payout_vector(market_id).ok_or(Error::::PayoutVectorNotFound)?; + + let market = T::MarketCommons::market(&market_id)?; + let asset_count = market.outcomes() as usize; + let collateral_token = market.base_asset; + + ensure!(index_set.len() == asset_count, Error::::InvalidIndexSet); + ensure!(index_set.iter().any(|&b| b), Error::::InvalidIndexSet); + ensure!(!index_set.iter().all(|&b| b), Error::::InvalidIndexSet); + + // Add up values of each outcome. + let mut total_stake: BalanceOf = Zero::zero(); + // Security note: Safe because `zip` will limit this loop to `payout_vector.len()` + // iterations. + for (&index, value) in index_set.iter().zip(payout_vector.iter()) { + if index { + total_stake = total_stake.checked_add_res(value)?; + } + } + + ensure!(!total_stake.is_zero(), Error::::TokenHasNoValue); + + let position = Self::position_from_parent_collection( + parent_collection_id, + market_id, + index_set.clone(), + fuel.clone(), + )?; + let amount = T::MultiCurrency::free_balance(position, &who); + ensure!(!amount.is_zero(), Error::::NoTokensFound); + T::MultiCurrency::withdraw(position, &who, amount)?; + + let total_payout = total_stake.bmul(amount)?; + + let (weight, asset_out) = if let Some(pci) = parent_collection_id { + // Merge combinatorial token into higher level position. Destroy the tokens. + let position_id = T::CombinatorialIdManager::get_position_id(collateral_token, pci); + let position = Asset::CombinatorialToken(position_id); + T::MultiCurrency::deposit(position, &who, total_payout)?; + + let weight = T::WeightInfo::redeem_position_with_parent( + index_set.len().saturated_into(), + fuel.total(), + ); + + (weight, position) + } else { + T::MultiCurrency::transfer( + collateral_token, + &Self::account_id(), + &who, + total_payout, + )?; + + let weight = T::WeightInfo::redeem_position_sans_parent( + index_set.len().saturated_into(), + fuel.total(), + ); + + (weight, collateral_token) + }; + + Self::deposit_event(Event::::TokenRedeemed { + who, + parent_collection_id, + market_id, + index_set, + asset_in: position, + amount_in: amount, + asset_out, + amount_out: total_payout, + }); + + Ok(Some(weight).into()) + } + + pub fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + pub(crate) fn free_index_set( + market_id: MarketIdOf, + partition: &[Vec], + ) -> Result, DispatchError> { + let market = T::MarketCommons::market(&market_id)?; + let asset_count = market.outcomes() as usize; + let mut free_index_set = vec![true; asset_count]; + + for index_set in partition.iter() { + // Ensure that the partition is not trivial and matches the market's outcomes. + ensure!(index_set.iter().any(|&i| i), Error::::InvalidPartition); + ensure!(index_set.len() == asset_count, Error::::InvalidPartition); + ensure!(!index_set.iter().all(|&i| i), Error::::InvalidPartition); + + // Ensure that `index_set` is disjoint from the previously iterated elements of the + // partition. + ensure!( + free_index_set.iter().zip(index_set.iter()).all(|(i, j)| *i || !*j), + Error::::InvalidPartition + ); + + // Remove indices of `index_set` from `free_index_set`. + free_index_set = + free_index_set.iter().zip(index_set.iter()).map(|(i, j)| *i && !*j).collect(); + } + + Ok(free_index_set) + } + + pub(crate) fn transmutation_asset( + parent_collection_id: Option>, + market_id: MarketIdOf, + partition: Vec>, + fuel: FuelOf, + ) -> Result<(TransmutationType, AssetOf), DispatchError> { + let market = T::MarketCommons::market(&market_id)?; + let collateral_token = market.base_asset; + let free_index_set = Self::free_index_set(market_id, &partition)?; + + let result = if !free_index_set.iter().any(|&i| i) { + // Vertical merge. + if let Some(pci) = parent_collection_id { + let position_id = + T::CombinatorialIdManager::get_position_id(collateral_token, pci); + let position = Asset::CombinatorialToken(position_id); + + (TransmutationType::VerticalWithParent, position) + } else { + (TransmutationType::VerticalSansParent, collateral_token) + } + } else { + let remaining_index_set = free_index_set.into_iter().map(|i| !i).collect(); + let position = Self::position_from_parent_collection( + parent_collection_id, + market_id, + remaining_index_set, + fuel, + )?; + + (TransmutationType::Horizontal, position) + }; + + Ok(result) + } + + pub(crate) fn collection_id_from_parent_collection( + parent_collection_id: Option>, + market_id: MarketIdOf, + index_set: Vec, + fuel: FuelOf, + ) -> Result, DispatchError> { + T::CombinatorialIdManager::get_collection_id( + parent_collection_id, + market_id, + index_set, + fuel, + ) + .map_err(|collection_id_error| { + Error::::CollectionIdRetrievalFailed(collection_id_error).into() + }) + } + + pub(crate) fn position_from_collection_id( + market_id: MarketIdOf, + collection_id: CombinatorialIdOf, + ) -> Result, DispatchError> { + let market = T::MarketCommons::market(&market_id)?; + let collateral_token = market.base_asset; + + let position_id = + T::CombinatorialIdManager::get_position_id(collateral_token, collection_id); + let asset = Asset::CombinatorialToken(position_id); + + Ok(asset) + } + + pub fn position_from_parent_collection( + parent_collection_id: Option>, + market_id: MarketIdOf, + index_set: Vec, + fuel: FuelOf, + ) -> Result, DispatchError> { + let collection_id = Self::collection_id_from_parent_collection( + parent_collection_id, + market_id, + index_set, + fuel, + )?; + + Self::position_from_collection_id(market_id, collection_id) + } + } + + impl CombinatorialTokensApi for Pallet + where + T: Config, + { + type AccountId = T::AccountId; + type Balance = BalanceOf; + type CombinatorialId = CombinatorialIdOf; + type MarketId = MarketIdOf; + type Fuel = <::CombinatorialIdManager as CombinatorialIdManager>::Fuel; + + fn split_position( + who: Self::AccountId, + parent_collection_id: Option, + market_id: Self::MarketId, + partition: Vec>, + amount: Self::Balance, + fuel: Self::Fuel, + ) -> Result, DispatchError> { + Self::do_split_position(who, parent_collection_id, market_id, partition, amount, fuel) + } + } + + impl CombinatorialTokensUnsafeApi for Pallet + where + T: Config, + { + type AccountId = T::AccountId; + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + fn split_position_unsafe( + who: Self::AccountId, + collateral: Asset, + assets: Vec>, + amount: Self::Balance, + ) -> DispatchResult { + T::MultiCurrency::ensure_can_withdraw(collateral, &who, amount)?; + T::MultiCurrency::transfer(collateral, &who, &Pallet::::account_id(), amount)?; + + for &asset in assets.iter() { + T::MultiCurrency::deposit(asset, &who, amount)?; + } + + Ok(()) + } + + fn merge_position_unsafe( + who: Self::AccountId, + collateral: Asset, + assets: Vec>, + amount: Self::Balance, + ) -> DispatchResult { + T::MultiCurrency::transfer(collateral, &Pallet::::account_id(), &who, amount)?; + + for &asset in assets.iter() { + T::MultiCurrency::ensure_can_withdraw(asset, &who, amount)?; + T::MultiCurrency::withdraw(asset, &who, amount)?; + } + + Ok(()) + } + } +} diff --git a/zrml/combinatorial-tokens/src/mock/consts.rs b/zrml/combinatorial-tokens/src/mock/consts.rs new file mode 100644 index 000000000..58eb0a3d1 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/consts.rs @@ -0,0 +1,22 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#[cfg(feature = "parachain")] +use zeitgeist_primitives::types::{Asset, MarketId}; + +#[cfg(feature = "parachain")] +pub(crate) const FOREIGN_ASSET: Asset = Asset::ForeignAsset(1); diff --git a/zrml/combinatorial-tokens/src/mock/ext_builder.rs b/zrml/combinatorial-tokens/src/mock/ext_builder.rs new file mode 100644 index 000000000..d9340607b --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/ext_builder.rs @@ -0,0 +1,72 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::mock::runtime::{Runtime, System}; +use sp_io::TestExternalities; +use sp_runtime::BuildStorage; + +#[cfg(feature = "parachain")] +use {crate::mock::consts::FOREIGN_ASSET, zeitgeist_primitives::types::CustomMetadata}; + +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build() -> TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + // See the logs in tests when using `RUST_LOG=debug cargo test -- --nocapture` + let _ = env_logger::builder().is_test(true).try_init(); + + pallet_balances::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + #[cfg(feature = "parachain")] + { + orml_tokens::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + let custom_metadata = + CustomMetadata { allow_as_base_asset: true, ..Default::default() }; + + orml_asset_registry::GenesisConfig:: { + assets: vec![( + FOREIGN_ASSET, + AssetMetadata { + decimals: 18, + name: "MKL".as_bytes().to_vec().try_into().unwrap(), + symbol: "MKL".as_bytes().to_vec().try_into().unwrap(), + existential_deposit: 0, + location: None, + additional: custom_metadata, + } + .encode(), + )], + last_asset_id: FOREIGN_ASSET, + } + .assimilate_storage(&mut t) + .unwrap(); + } + + let mut test_ext: sp_io::TestExternalities = t.into(); + + test_ext.execute_with(|| System::set_block_number(1)); + + test_ext + } +} diff --git a/zrml/combinatorial-tokens/src/mock/mod.rs b/zrml/combinatorial-tokens/src/mock/mod.rs new file mode 100644 index 000000000..5c7e91fc5 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(feature = "mock")] + +pub(crate) mod consts; +pub mod ext_builder; +pub mod runtime; +pub mod types; diff --git a/zrml/combinatorial-tokens/src/mock/runtime.rs b/zrml/combinatorial-tokens/src/mock/runtime.rs new file mode 100644 index 000000000..518c3e5d9 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/runtime.rs @@ -0,0 +1,139 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate as zrml_combinatorial_tokens; +use crate::{ + mock::types::MockPayout, + types::{cryptographic_id_manager::Fuel, CryptographicIdManager}, + weights::WeightInfo, +}; +use frame_support::{construct_runtime, traits::Everything, Blake2_256}; +use frame_system::mocking::MockBlock; +use sp_runtime::traits::{BlakeTwo256, ConstU32, IdentityLookup}; +use zeitgeist_primitives::{ + constants::mock::{ + BlockHashCount, CombinatorialTokensPalletId, ExistentialDeposit, ExistentialDeposits, + GetNativeCurrencyId, MaxLocks, MaxReserves, MinimumPeriod, + }, + types::{ + AccountIdTest, Amount, Balance, BasicCurrencyAdapter, CurrencyId, Hash, MarketId, Moment, + }, +}; + +#[cfg(feature = "runtime-benchmarks")] +use crate::mock::types::BenchmarkHelper; + +construct_runtime! { + pub enum Runtime { + CombinatorialTokens: zrml_combinatorial_tokens, + Balances: pallet_balances, + Currencies: orml_currencies, + MarketCommons: zrml_market_commons, + System: frame_system, + Timestamp: pallet_timestamp, + Tokens: orml_tokens, + } +} + +impl zrml_combinatorial_tokens::Config for Runtime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper; + type CombinatorialIdManager = CryptographicIdManager; + type Fuel = Fuel; + type MarketCommons = MarketCommons; + type MultiCurrency = Currencies; + type Payout = MockPayout; + type RuntimeEvent = RuntimeEvent; + type PalletId = CombinatorialTokensPalletId; + type WeightInfo = WeightInfo; +} + +impl orml_currencies::Config for Runtime { + type GetNativeCurrencyId = GetNativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type WeightInfo = (); +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type FreezeIdentifier = (); + type RuntimeHoldReason = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type MaxHolds = (); + type MaxFreezes = (); + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl zrml_market_commons::Config for Runtime { + type Balance = Balance; + type MarketId = MarketId; + type Timestamp = Timestamp; +} + +impl frame_system::Config for Runtime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountIdTest; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type RuntimeCall = RuntimeCall; + type DbWeight = (); + type RuntimeEvent = RuntimeEvent; + type Hash = Hash; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type Nonce = u64; + type MaxConsumers = ConstU32<16>; + type OnKilledAccount = (); + type OnNewAccount = (); + type RuntimeOrigin = RuntimeOrigin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); + type OnSetCode = (); +} + +impl pallet_timestamp::Config for Runtime { + type MinimumPeriod = MinimumPeriod; + type Moment = Moment; + type OnTimestampSet = (); + type WeightInfo = (); +} + +impl orml_tokens::Config for Runtime { + type Amount = Amount; + type Balance = Balance; + type CurrencyId = CurrencyId; + type DustRemovalWhitelist = Everything; + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type CurrencyHooks = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} diff --git a/zrml/combinatorial-tokens/src/mock/types/benchmark_helper.rs b/zrml/combinatorial-tokens/src/mock/types/benchmark_helper.rs new file mode 100644 index 000000000..0a42c3a0b --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/types/benchmark_helper.rs @@ -0,0 +1,42 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{ + mock::{runtime::Runtime, types::MockPayout}, + BalanceOf, MarketIdOf, +}; +use alloc::vec::Vec; +use sp_runtime::DispatchResult; +use zeitgeist_primitives::traits::CombinatorialTokensBenchmarkHelper; + +pub struct BenchmarkHelper; + +impl CombinatorialTokensBenchmarkHelper for BenchmarkHelper { + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + /// A bit of a messy implementation as this sets the return value of the next `payout_vector` + /// call, regardless of what `_market_id` is. + fn setup_payout_vector( + _market_id: Self::MarketId, + payout: Option>, + ) -> DispatchResult { + MockPayout::set_return_value(payout); + + Ok(()) + } +} diff --git a/zrml/combinatorial-tokens/src/mock/types/mod.rs b/zrml/combinatorial-tokens/src/mock/types/mod.rs new file mode 100644 index 000000000..40663b2a0 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/types/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#[cfg(feature = "runtime-benchmarks")] +mod benchmark_helper; +mod payout; + +#[cfg(feature = "runtime-benchmarks")] +pub(crate) use benchmark_helper::BenchmarkHelper; +pub use payout::MockPayout; diff --git a/zrml/combinatorial-tokens/src/mock/types/payout.rs b/zrml/combinatorial-tokens/src/mock/types/payout.rs new file mode 100644 index 000000000..447bb445d --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/types/payout.rs @@ -0,0 +1,64 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use alloc::vec; +use core::cell::RefCell; +use zeitgeist_primitives::{ + traits::PayoutApi, + types::{Balance, MarketId}, +}; + +pub struct MockPayout; + +impl MockPayout { + pub fn set_return_value(value: Option>) { + PAYOUT_VECTOR_RETURN_VALUE.with(|v| *v.borrow_mut() = Some(value)); + } + + pub fn not_called() -> bool { + PAYOUT_VECTOR_CALL_DATA.with(|values| values.borrow().is_empty()) + } + + pub fn called_once_with(expected: MarketId) -> bool { + if PAYOUT_VECTOR_CALL_DATA.with(|values| values.borrow().len()) != 1 { + return false; + } + + let actual = + PAYOUT_VECTOR_CALL_DATA.with(|value| *value.borrow().first().expect("can't be empty")); + + actual == expected + } +} + +impl PayoutApi for MockPayout { + type Balance = Balance; + type MarketId = MarketId; + + fn payout_vector(market_id: Self::MarketId) -> Option> { + PAYOUT_VECTOR_CALL_DATA.with(|values| values.borrow_mut().push(market_id)); + + PAYOUT_VECTOR_RETURN_VALUE + .with(|value| value.borrow().clone()) + .expect("MockPayout: No return value configured") + } +} + +thread_local! { + pub static PAYOUT_VECTOR_CALL_DATA: RefCell> = const { RefCell::new(vec![]) }; + pub static PAYOUT_VECTOR_RETURN_VALUE: RefCell>>> = const { RefCell::new(None) }; +} diff --git a/zrml/combinatorial-tokens/src/tests/integration.rs b/zrml/combinatorial-tokens/src/tests/integration.rs new file mode 100644 index 000000000..0b7012c60 --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/integration.rs @@ -0,0 +1,534 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; + +#[test] +fn split_followed_by_merge_vertical_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + let amount = _1; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + partition.clone(), + amount, + Fuel::new(16, false), + )); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + assert_eq!(alice.free_balance(ct_001), _1); + assert_eq!(alice.free_balance(ct_110), _1); + assert_eq!(pallet.free_balance(Asset::Ztg), _1); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + partition, + amount, + Fuel::new(16, false), + )); + assert_eq!(alice.free_balance(Asset::Ztg), _100); + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(pallet.free_balance(Asset::Ztg), 0); + }); +} + +#[test] +fn split_followed_by_merge_vertical_with_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + let ct_001_1010 = CombinatorialToken([ + 107, 142, 3, 38, 49, 137, 237, 239, 1, 131, 197, 221, 236, 46, 246, 93, 185, 197, 228, + 184, 75, 79, 107, 73, 89, 19, 22, 124, 15, 58, 110, 100, + ]); + + let parent_market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let parent_amount = _3; + let parent_partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + parent_market_id, + parent_partition.clone(), + parent_amount, + Fuel::new(16, false), + )); + + let child_market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let child_amount = _1; + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + let child_partition = vec![vec![B0, B1, B0, B1], vec![B1, B0, B1, B0]]; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + child_partition.clone(), + child_amount, + Fuel::new(16, false), + )); + assert_eq!(alice.free_balance(ct_001), parent_amount - child_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(Asset::Ztg), _100 - parent_amount); + assert_eq!(alice.free_balance(ct_001_0101), child_amount); + assert_eq!(alice.free_balance(ct_001_1010), child_amount); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + child_partition, + child_amount, + Fuel::new(16, false), + )); + assert_eq!(alice.free_balance(ct_001), parent_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(Asset::Ztg), _100 - parent_amount); + assert_eq!(alice.free_balance(ct_001_0101), 0); + assert_eq!(alice.free_balance(ct_001_1010), 0); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + parent_market_id, + parent_partition, + parent_amount, + Fuel::new(16, false), + )); + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(Asset::Ztg), _100); + assert_eq!(alice.free_balance(ct_001_0101), 0); + assert_eq!(alice.free_balance(ct_001_1010), 0); + assert_eq!(pallet.free_balance(Asset::Ztg), 0); + }); +} + +#[test] +fn split_followed_by_merge_vertical_with_parent_in_opposite_order() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_0 = create_market(Asset::Ztg, MarketType::Categorical(3)); + let market_1 = create_market(Asset::Ztg, MarketType::Categorical(4)); + + let partition_0 = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + let partition_1 = vec![vec![B0, B0, B1, B1], vec![B1, B1, B0, B0]]; + + let amount = _1; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let id_001 = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + let id_110 = [ + 1, 189, 94, 224, 153, 162, 145, 214, 33, 231, 230, 19, 122, 179, 122, 117, 193, 123, + 73, 220, 240, 131, 180, 180, 137, 14, 179, 148, 188, 13, 107, 65, + ]; + + let ct_0011 = CombinatorialToken([ + 32, 70, 65, 46, 183, 161, 122, 58, 80, 224, 102, 106, 63, 89, 191, 19, 235, 137, 64, + 182, 25, 222, 198, 172, 230, 42, 120, 101, 100, 150, 172, 125, + ]); + let ct_1100 = CombinatorialToken([ + 28, 158, 82, 180, 87, 230, 168, 233, 74, 123, 50, 76, 131, 203, 82, 194, 214, 165, 87, + 200, 58, 244, 23, 184, 79, 127, 201, 39, 82, 243, 186, 1, + ]); + let id_0011 = [ + 77, 83, 228, 134, 221, 156, 53, 34, 133, 83, 120, 8, 232, 53, 54, 200, 181, 110, 13, + 145, 238, 130, 69, 147, 108, 167, 41, 217, 105, 22, 126, 136, + ]; + let id_1100 = [ + 10, 211, 115, 219, 24, 177, 205, 243, 234, 68, 234, 119, 21, 211, 103, 229, 185, 23, + 63, 75, 206, 10, 196, 75, 10, 110, 147, 40, 90, 61, 145, 90, + ]; + + let ct_001_0011 = CombinatorialToken([ + 156, 47, 254, 154, 29, 5, 149, 94, 214, 135, 92, 36, 188, 120, 42, 144, 136, 151, 255, + 91, 232, 152, 91, 236, 177, 66, 36, 72, 134, 234, 212, 177, + ]); + let ct_001_1100 = CombinatorialToken([ + 224, 47, 73, 22, 156, 226, 199, 74, 28, 251, 44, 108, 73, 125, 192, 151, 193, 60, 156, + 240, 215, 23, 138, 168, 181, 175, 241, 70, 71, 126, 48, 45, + ]); + let ct_110_0011 = CombinatorialToken([ + 191, 106, 159, 227, 136, 131, 143, 101, 127, 7, 109, 82, 45, 169, 246, 45, 250, 217, + 33, 147, 166, 174, 232, 35, 58, 20, 111, 167, 6, 6, 73, 67, + ]); + let ct_110_1100 = CombinatorialToken([ + 184, 155, 104, 90, 231, 10, 30, 1, 213, 7, 1, 58, 117, 172, 118, 72, 118, 89, 219, 216, + 140, 27, 228, 2, 87, 26, 169, 150, 172, 154, 49, 219, + ]); + + // Split ZTG into A|B and C. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_0, + partition_0.clone(), + amount, + Fuel::new(16, false), + )); + + // Split C into C&(U|V) and C&(W|X). + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(id_001), + market_1, + partition_1.clone(), + amount, + Fuel::new(16, false), + )); + + // Split A|B into into (A|B)&(U|V) and (A|B)&(W|X). + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(id_110), + market_1, + partition_1.clone(), + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), _1); + assert_eq!(alice.free_balance(ct_001_1100), _1); + assert_eq!(alice.free_balance(ct_110_0011), _1); + assert_eq!(alice.free_balance(ct_110_1100), _1); + assert_eq!(alice.free_balance(ct_0011), 0); + assert_eq!(alice.free_balance(ct_1100), 0); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + + // Merge C&(U|V) and (A|B)&(U|V) into U|V. + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(id_1100), + market_0, + partition_0.clone(), + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), _1); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_110_0011), _1); + assert_eq!(alice.free_balance(ct_110_1100), 0); + assert_eq!(alice.free_balance(ct_0011), 0); + assert_eq!(alice.free_balance(ct_1100), _1); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + + // Merge C&(W|X) and (A|B)&(W|X) into W|X. + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(id_0011), + market_0, + partition_0, + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), 0); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_110_0011), 0); + assert_eq!(alice.free_balance(ct_110_1100), 0); + assert_eq!(alice.free_balance(ct_0011), _1); + assert_eq!(alice.free_balance(ct_1100), _1); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + + // Merge U|V and W|X into ZTG. + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_1, + partition_1, + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), 0); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_110_0011), 0); + assert_eq!(alice.free_balance(ct_110_1100), 0); + assert_eq!(alice.free_balance(ct_0011), 0); + assert_eq!(alice.free_balance(ct_1100), 0); + assert_eq!(alice.free_balance(Asset::Ztg), _100); + }); +} + +// This test shows that splitting a token horizontally can be accomplished by splitting the parent +// token vertically with a finer partition. +#[test] +fn split_vertical_followed_by_horizontal_split_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let amount = _1; + + // Split vertically and then horizontally. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + amount, + Fuel::new(16, false), + )); + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + amount, + Fuel::new(16, false), + )); + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_010 = CombinatorialToken([ + 23, 108, 101, 109, 145, 51, 201, 192, 240, 28, 43, 57, 53, 4, 75, 101, 116, 20, 184, + 25, 227, 71, 149, 136, 59, 82, 81, 105, 41, 160, 39, 142, + ]); + let ct_100 = CombinatorialToken([ + 63, 95, 93, 48, 199, 160, 113, 178, 33, 24, 52, 193, 247, 121, 229, 30, 231, 100, 209, + 14, 57, 98, 193, 214, 34, 251, 53, 51, 136, 146, 93, 26, + ]); + + assert_eq!(alice.free_balance(ct_001), amount); + assert_eq!(alice.free_balance(ct_010), amount); + assert_eq!(alice.free_balance(ct_100), amount); + + // Split vertically. This should yield the same amount as the two splits above. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0], vec![B0, B0, B1]], + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), 2 * amount); + assert_eq!(alice.free_balance(ct_010), 2 * amount); + assert_eq!(alice.free_balance(ct_100), 2 * amount); + }); +} + +// This test shows that splitting a token horizontally can be accomplished by splitting a the parent +// token vertically with a finer partition. +#[test] +fn split_vertical_followed_by_horizontal_split_with_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + // Prepare level 1 token. + let parent_market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let parent_amount = _6; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + parent_market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + parent_amount, + Fuel::new(16, false), + )); + + let child_market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let child_amount_first_pass = _3; + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let ct_001_0011 = CombinatorialToken([ + 156, 47, 254, 154, 29, 5, 149, 94, 214, 135, 92, 36, 188, 120, 42, 144, 136, 151, 255, + 91, 232, 152, 91, 236, 177, 66, 36, 72, 134, 234, 212, 177, + ]); + let ct_001_1100 = CombinatorialToken([ + 224, 47, 73, 22, 156, 226, 199, 74, 28, 251, 44, 108, 73, 125, 192, 151, 193, 60, 156, + 240, 215, 23, 138, 168, 181, 175, 241, 70, 71, 126, 48, 45, + ]); + let ct_001_1000 = CombinatorialToken([ + 9, 208, 130, 141, 130, 87, 234, 29, 150, 109, 181, 68, 138, 137, 66, 8, 251, 157, 224, + 152, 176, 104, 231, 193, 178, 99, 184, 123, 78, 213, 63, 150, + ]); + let ct_001_0100 = CombinatorialToken([ + 220, 137, 106, 212, 207, 90, 155, 125, 22, 15, 184, 90, 227, 159, 173, 59, 33, 73, 50, + 245, 183, 245, 46, 56, 66, 199, 94, 129, 154, 18, 48, 73, + ]); + + // Split vertically and then horizontally. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + vec![vec![B0, B0, B1, B1], vec![B1, B1, B0, B0]], + child_amount_first_pass, + Fuel::new(16, false), + )); + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + vec![vec![B1, B0, B0, B0], vec![B0, B1, B0, B0]], + child_amount_first_pass, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), parent_amount - child_amount_first_pass); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(ct_001_0011), child_amount_first_pass); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_001_1000), child_amount_first_pass); + assert_eq!(alice.free_balance(ct_001_0100), child_amount_first_pass); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + assert_eq!(pallet.free_balance(ct_001_1100), 0); + + // Split vertically. This should yield the same amount as the two splits above. + let child_amount_second_pass = _2; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + vec![vec![B1, B0, B0, B0], vec![B0, B1, B0, B0], vec![B0, B0, B1, B1]], + child_amount_second_pass, + Fuel::new(16, false), + )); + + let total_child_amount = child_amount_first_pass + child_amount_second_pass; + assert_eq!(alice.free_balance(ct_001), parent_amount - total_child_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(ct_001_0011), total_child_amount); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_001_1000), total_child_amount); + assert_eq!(alice.free_balance(ct_001_0100), total_child_amount); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + assert_eq!(pallet.free_balance(ct_001_1100), 0); + }); +} + +#[test] +fn split_horizontal_followed_by_merge_horizontal() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let amount = _1; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + amount, + Fuel::new(16, false), + )); + + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + amount, + Fuel::new(16, false), + )); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), _1); + assert_eq!(alice.free_balance(ct_110), _1); + }); +} diff --git a/zrml/combinatorial-tokens/src/tests/merge_position.rs b/zrml/combinatorial-tokens/src/tests/merge_position.rs new file mode 100644 index 000000000..d124776e8 --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/merge_position.rs @@ -0,0 +1,298 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +#[test_case( + Asset::Ztg, + CombinatorialToken([207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139]), + CombinatorialToken([101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131]) +)] +#[test_case( + Asset::ForeignAsset(1), + CombinatorialToken([97, 71, 129, 186, 219, 73, 163, 242, 183, 111, 224, 26, 45, 104, 11, 229, 241, 31, 154, 126, 118, 218, 142, 191, 3, 255, 156, 77, 32, 1, 66, 227]), + CombinatorialToken([156, 42, 42, 43, 18, 242, 8, 247, 100, 196, 173, 111, 167, 225, 207, 149, 166, 194, 255, 1, 238, 128, 72, 199, 188, 57, 236, 168, 26, 58, 104, 156]) +)] +fn merge_position_works_no_parent( + collateral: Asset, + ct_001: Asset, + ct_110: Asset, +) { + ExtBuilder::build().execute_with(|| { + let amount = _100; + let alice = + Account::new(0).deposit(ct_001, amount).unwrap().deposit(ct_110, amount).unwrap(); + // Mock a deposit into the pallet's account. + let pallet = + Account::new(Pallet::::account_id()).deposit(collateral, amount).unwrap(); + + let parent_collection_id = None; + let market_id = create_market(collateral, MarketType::Categorical(3)); + let partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + parent_collection_id, + market_id, + partition.clone(), + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(collateral), _100); + assert_eq!(pallet.free_balance(collateral), 0); + + System::assert_last_event( + Event::::TokenMerged { + who: alice.id, + parent_collection_id, + market_id, + partition, + assets_in: vec![ct_001, ct_110], + asset_out: collateral, + amount, + } + .into(), + ); + }); +} + +#[test] +fn merge_position_works_parent() { + ExtBuilder::build().execute_with(|| { + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + let ct_001_1010 = CombinatorialToken([ + 107, 142, 3, 38, 49, 137, 237, 239, 1, 131, 197, 221, 236, 46, 246, 93, 185, 197, 228, + 184, 75, 79, 107, 73, 89, 19, 22, 124, 15, 58, 110, 100, + ]); + + let amount = _100; + let alice = Account::new(0) + .deposit(ct_001_0101, amount) + .unwrap() + .deposit(ct_001_1010, amount) + .unwrap(); + + let _ = create_market(Asset::Ztg, MarketType::Categorical(3)); + + // Collection ID of [0, 0, 1]. + let parent_collection_id = Some([ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let partition = vec![vec![B0, B1, B0, B1], vec![B1, B0, B1, B0]]; + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + parent_collection_id, + market_id, + partition.clone(), + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001), amount); + assert_eq!(alice.free_balance(ct_001_0101), 0); + assert_eq!(alice.free_balance(ct_001_1010), 0); + + System::assert_last_event( + Event::::TokenMerged { + who: alice.id, + parent_collection_id, + market_id, + partition, + assets_in: vec![ct_001_0101, ct_001_1010], + asset_out: ct_001, + amount, + } + .into(), + ); + }); +} + +#[test] +fn merge_position_horizontal_works() { + ExtBuilder::build().execute_with(|| { + let ct_100 = CombinatorialToken([ + 63, 95, 93, 48, 199, 160, 113, 178, 33, 24, 52, 193, 247, 121, 229, 30, 231, 100, 209, + 14, 57, 98, 193, 214, 34, 251, 53, 51, 136, 146, 93, 26, + ]); + let ct_010 = CombinatorialToken([ + 23, 108, 101, 109, 145, 51, 201, 192, 240, 28, 43, 57, 53, 4, 75, 101, 116, 20, 184, + 25, 227, 71, 149, 136, 59, 82, 81, 105, 41, 160, 39, 142, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + let amount = _100; + let alice = Account::new(0).deposit(ct_100, _100).unwrap().deposit(ct_010, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B1, B0], vec![B1, B0, B0]], + amount, + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_110), amount); + assert_eq!(alice.free_balance(ct_100), 0); + assert_eq!(alice.free_balance(ct_010), 0); + }); +} + +#[test] +fn merge_position_fails_if_market_not_found() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + 0, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + 1, + Fuel::new(16, false), + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test] +fn merge_position_fails_on_invalid_partition_length() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B1]]; + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false) + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn merge_position_fails_on_trivial_partition_member() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B0]]; + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false) + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn merge_position_fails_on_overlapping_partition_members() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B1]]; + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false) + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn merge_position_fails_on_insufficient_funds() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _99).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + Fuel::new(16, false), + ), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn merge_position_fails_on_insufficient_funds_foreign_token() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::ForeignAsset(1), _99).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + Fuel::new(16, false), + ), + orml_tokens::Error::::BalanceTooLow + ); + }); +} diff --git a/zrml/combinatorial-tokens/src/tests/mod.rs b/zrml/combinatorial-tokens/src/tests/mod.rs new file mode 100644 index 000000000..2e3ef1b01 --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/mod.rs @@ -0,0 +1,101 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(all(feature = "mock", test))] + +mod integration; +mod merge_position; +mod redeem_position; +mod split_position; + +use crate::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Currencies, MarketCommons, Runtime, RuntimeOrigin, System}, + types::MockPayout, + }, + types::cryptographic_id_manager::Fuel, + Error, Event, Pallet, +}; +use frame_support::{assert_noop, assert_ok}; +use orml_traits::MultiCurrency; +use sp_runtime::{DispatchError, Perbill}; +use zeitgeist_primitives::{ + constants::base_multiples::*, + types::{ + AccountIdTest, Asset, Asset::CombinatorialToken, Balance, Market, MarketBonds, + MarketCreation, MarketId, MarketPeriod, MarketStatus, MarketType, ScoringRule, + }, +}; +use zrml_market_commons::MarketCommonsPalletApi; + +// For better readability of index sets. +pub(crate) const B0: bool = false; +pub(crate) const B1: bool = true; + +fn create_market(base_asset: Asset, market_type: MarketType) -> MarketId { + let market = Market { + base_asset, + market_id: Default::default(), + creation: MarketCreation::Permissionless, + creator_fee: Perbill::zero(), + creator: Default::default(), + market_type, + dispute_mechanism: None, + metadata: Default::default(), + oracle: Default::default(), + period: MarketPeriod::Block(Default::default()), + deadlines: Default::default(), + report: None, + resolved_outcome: None, + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Disputed, + bonds: MarketBonds::default(), + early_close: None, + }; + MarketCommons::push_market(market).unwrap(); + MarketCommons::latest_market_id().unwrap() +} + +/// Utility struct for managing test accounts. +pub(crate) struct Account { + id: AccountIdTest, +} + +impl Account { + // TODO Not a pressing issue, but double booking accounts should be illegal. + pub(crate) fn new(id: AccountIdTest) -> Account { + Account { id } + } + + /// Deposits `amount` of `asset` and returns the account to allow call chains. + pub(crate) fn deposit( + self, + asset: Asset, + amount: Balance, + ) -> Result { + Currencies::deposit(asset, &self.id, amount).map(|_| self) + } + + pub(crate) fn signed(&self) -> RuntimeOrigin { + RuntimeOrigin::signed(self.id) + } + + pub(crate) fn free_balance(&self, asset: Asset) -> Balance { + Currencies::free_balance(asset, &self.id) + } +} diff --git a/zrml/combinatorial-tokens/src/tests/redeem_position.rs b/zrml/combinatorial-tokens/src/tests/redeem_position.rs new file mode 100644 index 000000000..2f191b999 --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/redeem_position.rs @@ -0,0 +1,222 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +#[test] +fn redeem_position_fails_on_no_payout_vector() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let market_id = 0; + MockPayout::set_return_value(None); + assert_noop!( + CombinatorialTokens::redeem_position( + alice.signed(), + None, + market_id, + vec![], + Fuel::new(16, false) + ), + Error::::PayoutVectorNotFound + ); + assert!(MockPayout::called_once_with(market_id)); + }); +} + +#[test] +fn redeem_position_fails_on_market_not_found() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + MockPayout::set_return_value(Some(vec![_1_2, _1_2])); + assert_noop!( + CombinatorialTokens::redeem_position( + alice.signed(), + None, + 0, + vec![], + Fuel::new(16, false) + ), + zrml_market_commons::Error::::MarketDoesNotExist + ); + }); +} + +#[test_case(vec![B0, B1, B0, B1]; "incorrect_len")] +#[test_case(vec![B0, B0, B0]; "all_zero")] +#[test_case(vec![B1, B1, B1]; "all_one")] +fn redeem_position_fails_on_incorrect_index_set(index_set: Vec) { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + MockPayout::set_return_value(Some(vec![_1_3, _1_3, _1_3])); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + assert_noop!( + CombinatorialTokens::redeem_position( + alice.signed(), + None, + market_id, + index_set, + Fuel::new(16, false) + ), + Error::::InvalidIndexSet + ); + }); +} + +#[test] +fn redeem_position_fails_if_tokens_have_no_value() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + MockPayout::set_return_value(Some(vec![0, _1_2, _1_2, 0])); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let index_set = vec![B1, B0, B0, B1]; + assert_noop!( + CombinatorialTokens::redeem_position( + alice.signed(), + None, + market_id, + index_set, + Fuel::new(16, false) + ), + Error::::TokenHasNoValue + ); + }); +} + +#[test] +fn redeem_position_fails_if_user_holds_no_winning_tokens() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + MockPayout::set_return_value(Some(vec![0, _1_2, _1_2, 0])); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let index_set = vec![B0, B1, B0, B1]; + assert_noop!( + CombinatorialTokens::redeem_position( + alice.signed(), + None, + market_id, + index_set, + Fuel::new(16, false) + ), + Error::::NoTokensFound, + ); + }); +} + +#[test] +fn redeem_position_works_sans_parent() { + ExtBuilder::build().execute_with(|| { + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let alice = Account::new(0).deposit(ct_110, _3).unwrap(); + let amount_in = _3; + let pallet = + Account::new(Pallet::::account_id()).deposit(Asset::Ztg, amount_in).unwrap(); + + MockPayout::set_return_value(Some(vec![_1_4, _1_2, _1_4])); + + let parent_collection_id = None; + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let index_set = vec![B1, B1, B0]; + assert_ok!(CombinatorialTokens::redeem_position( + alice.signed(), + parent_collection_id, + market_id, + index_set.clone(), + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_110), 0); + let amount_out = _2 + _1_4; + assert_eq!(alice.free_balance(Asset::Ztg), amount_out); + assert_eq!(pallet.free_balance(Asset::Ztg), _3_4); + + System::assert_last_event( + Event::::TokenRedeemed { + who: alice.id, + parent_collection_id, + market_id, + index_set, + asset_in: ct_110, + amount_in, + asset_out: Asset::Ztg, + amount_out, + } + .into(), + ); + + assert!(MockPayout::called_once_with(market_id)); + }); +} + +#[test] +fn redeem_position_works_with_parent() { + ExtBuilder::build().execute_with(|| { + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + + let amount_in = _7; + let alice = Account::new(0).deposit(ct_001_0101, amount_in).unwrap(); + + MockPayout::set_return_value(Some(vec![_1_4, 0, _1_2, _1_4])); + + let _ = create_market(Asset::Ztg, MarketType::Categorical(3)); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + + // Collection ID of [0, 0, 1]. + let parent_collection_id = Some([ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]); + let index_set = vec![B0, B1, B0, B1]; + assert_ok!(CombinatorialTokens::redeem_position( + alice.signed(), + parent_collection_id, + market_id, + index_set.clone(), + Fuel::new(16, false), + )); + + assert_eq!(alice.free_balance(ct_001_0101), 0); + let amount_out = _1 + _3_4; + assert_eq!(alice.free_balance(ct_001), amount_out); + + System::assert_last_event( + Event::::TokenRedeemed { + who: alice.id, + parent_collection_id, + market_id, + index_set, + asset_in: ct_001_0101, + amount_in, + asset_out: ct_001, + amount_out, + } + .into(), + ); + + assert!(MockPayout::called_once_with(market_id)); + }); +} diff --git a/zrml/combinatorial-tokens/src/tests/split_position.rs b/zrml/combinatorial-tokens/src/tests/split_position.rs new file mode 100644 index 000000000..331acda6e --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/split_position.rs @@ -0,0 +1,404 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; + +#[test] +fn split_position_works_vertical_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let parent_collection_id = None; + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + + let amount = _1; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + parent_collection_id, + market_id, + partition.clone(), + amount, + Fuel::new(16, false), + )); + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + assert_eq!(alice.free_balance(ct_001), amount); + assert_eq!(alice.free_balance(ct_110), amount); + assert_eq!(alice.free_balance(Asset::Ztg), _100 - amount); + assert_eq!(pallet.free_balance(Asset::Ztg), amount); + + System::assert_last_event( + Event::::TokenSplit { + who: alice.id, + parent_collection_id, + market_id, + partition, + asset_in: Asset::Ztg, + assets_out: vec![ct_001, ct_110], + collection_ids: vec![ + [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, + 196, 112, 45, 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ], + [ + 1, 189, 94, 224, 153, 162, 145, 214, 33, 231, 230, 19, 122, 179, 122, 117, + 193, 123, 73, 220, 240, 131, 180, 180, 137, 14, 179, 148, 188, 13, 107, 65, + ], + ], + amount, + } + .into(), + ); + }); +} + +#[test] +fn split_position_works_vertical_with_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let parent_market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let parent_amount = _3; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + parent_market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + parent_amount, + Fuel::new(16, false), + )); + + let child_market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let child_amount = _1; + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + let partition = vec![vec![B0, B1, B0, B1], vec![B1, B0, B1, B0]]; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + partition.clone(), + child_amount, + Fuel::new(16, false), + )); + + // Alice is left with 2 units of [0, 0, 1], 3 units of [1, 1, 0] and one unit of each of the + // two new tokens. + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + let ct_001_1010 = CombinatorialToken([ + 107, 142, 3, 38, 49, 137, 237, 239, 1, 131, 197, 221, 236, 46, 246, 93, 185, 197, 228, + 184, 75, 79, 107, 73, 89, 19, 22, 124, 15, 58, 110, 100, + ]); + + assert_eq!(alice.free_balance(Asset::Ztg), _100 - parent_amount); + assert_eq!(alice.free_balance(ct_001), parent_amount - child_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(ct_001_0101), child_amount); + assert_eq!(alice.free_balance(ct_001_1010), child_amount); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + assert_eq!(pallet.free_balance(ct_001), 0); // Combinatorial tokens are destroyed when split. + + System::assert_last_event( + Event::::TokenSplit { + who: alice.id, + parent_collection_id: Some(parent_collection_id), + market_id: child_market_id, + partition, + asset_in: ct_001, + assets_out: vec![ct_001_0101, ct_001_1010], + collection_ids: vec![ + [ + 93, 24, 254, 39, 137, 146, 204, 128, 95, 226, 32, 110, 212, 68, 65, 13, + 128, 86, 96, 119, 117, 240, 144, 57, 224, 160, 106, 176, 250, 172, 157, 47, + ], + [ + 98, 123, 162, 148, 54, 175, 126, 250, 173, 76, 229, 156, 108, 125, 245, 68, + 132, 230, 48, 72, 247, 45, 233, 27, 100, 225, 243, 113, 21, 69, 45, 113, + ], + ], + amount: child_amount, + } + .into(), + ); + }); +} + +// Intentionally left out as it is covered by +// `integration::vertical_split_followed_by_horizontal_split_no_parent`. +// #[test] +// fn split_position_works_horizontal_no_parent() {} + +// Intentionally left out as it is covered by +// `integration::vertical_split_followed_by_horizontal_split_with_parent`. +// #[test] +// fn split_position_works_horizontal_with_parent() {} + +#[test] +fn split_position_fails_if_market_not_found() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + 0, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + 1, + Fuel::new(16, false), + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test] +fn split_position_fails_on_invalid_partition_length() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B1]]; + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false), + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_empty_partition_member() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Second element is empty. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B0]]; + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false) + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_overlapping_partition_members() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Last elements overlap. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B1]]; + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false), + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_trivial_partition() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B1, B1]]; + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + partition, + _1, + Fuel::new(16, false) + ), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_insufficient_funds_native_token_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _99).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + Fuel::new(16, false), + ), + orml_currencies::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn split_position_fails_on_insufficient_funds_foreign_token_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::ForeignAsset(1), _99).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + Fuel::new(16, false), + ), + orml_currencies::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn split_position_vertical_fails_on_insufficient_funds_combinatorial_token() { + ExtBuilder::build().execute_with(|| { + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + + let alice = Account::new(0).deposit(ct_001, _99).unwrap(); + + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + + let _ = create_market(Asset::Ztg, MarketType::Categorical(3)); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + market_id, + vec![vec![B1, B0, B1, B0], vec![B0, B1, B0, B1]], + _100, + Fuel::new(16, false), + ), + orml_tokens::Error::::BalanceTooLow + ); + + // Make sure that we're testing for the right balance. This call should work! + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + market_id, + vec![vec![B1, B0, B1, B0], vec![B0, B1, B0, B1]], + _99, + Fuel::new(16, false), + )); + }); +} + +#[test] +fn split_position_horizontal_fails_on_insufficient_funds_combinatorial_token() { + ExtBuilder::build().execute_with(|| { + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + let alice = Account::new(0).deposit(ct_110, _99).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + _100, + Fuel::new(16, false), + ), + orml_tokens::Error::::BalanceTooLow + ); + + // Make sure that we're testing for the right balance. This call should work! + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + _99, + Fuel::new(16, false), + )); + }); +} diff --git a/zrml/combinatorial-tokens/src/traits/combinatorial_id_manager.rs b/zrml/combinatorial-tokens/src/traits/combinatorial_id_manager.rs new file mode 100644 index 000000000..dcee9a12d --- /dev/null +++ b/zrml/combinatorial-tokens/src/traits/combinatorial_id_manager.rs @@ -0,0 +1,53 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work licensed under the GNU Lesser General +// Public License 3.0 but published without copyright notice by Gnosis +// (, info@gnosis.io) in the +// conditional-tokens-contracts repository +// , +// and has been relicensed under GPL-3.0-or-later in this repository. + +use crate::types::CollectionIdError; +use alloc::vec::Vec; + +/// Handles calculations of combinatorial IDs. +pub trait CombinatorialIdManager { + type Asset; + type MarketId; + type CombinatorialId; + type Fuel; + + /// Calculate the collection ID obtained when splitting `parent_collection_id` over the market + /// given by `market_id` and the `index_set`. + /// + /// The `fuel` parameter specifies how much work the function will do and can be used for + /// benchmarking purposes. + fn get_collection_id( + parent_collection_id: Option, + market_id: Self::MarketId, + index_set: Vec, + fuel: Self::Fuel, + ) -> Result; + + /// Calculate the position ID belonging to the `collection_id` combined with `collateral` as + /// collateral. + fn get_position_id( + collateral: Self::Asset, + collection_id: Self::CombinatorialId, + ) -> Self::CombinatorialId; +} diff --git a/zrml/combinatorial-tokens/src/traits/mod.rs b/zrml/combinatorial-tokens/src/traits/mod.rs new file mode 100644 index 000000000..2b9aa08a3 --- /dev/null +++ b/zrml/combinatorial-tokens/src/traits/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod combinatorial_id_manager; + +pub use combinatorial_id_manager::CombinatorialIdManager; diff --git a/zrml/combinatorial-tokens/src/types/collection_id_error.rs b/zrml/combinatorial-tokens/src/types/collection_id_error.rs new file mode 100644 index 000000000..2de7ce936 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/collection_id_error.rs @@ -0,0 +1,35 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work licensed under the GNU Lesser General +// Public License 3.0 but published without copyright notice by Gnosis +// (, info@gnosis.io) in the +// conditional-tokens-contracts repository +// , +// and has been relicensed under GPL-3.0-or-later in this repository. + +use frame_support::PalletError; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; + +#[derive(Decode, Encode, Eq, PartialEq, PalletError, RuntimeDebug, TypeInfo)] +pub enum CollectionIdError { + InvalidParentCollectionId, + EllipticCurvePointNotFoundWithFuel, + EllipticCurvePointXToBytesConversionFailed, +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/mod.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/mod.rs new file mode 100644 index 000000000..b1947d3b0 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/mod.rs @@ -0,0 +1,593 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work licensed under the GNU Lesser General +// Public License 3.0 but published without copyright notice by Gnosis +// (, info@gnosis.io) in the +// conditional-tokens-contracts repository +// , +// and has been relicensed under GPL-3.0-or-later in this repository. + +//! Highest/lowest bit always refers to the big endian representation of each bit sequence. + +mod tests; + +use crate::types::{cryptographic_id_manager::Fuel, CollectionIdError}; +use ark_bn254::{g1::G1Affine, Fq}; +use ark_ff::{BigInteger, PrimeField}; +use core::ops::Neg; +use sp_runtime::traits::{One, Zero}; +use zeitgeist_primitives::{traits::CombinatorialTokensFuel, types::CombinatorialId}; + +/// Returns a valid collection ID from an `hash` and an optional `parent_collection_id`. +/// +/// Will return `None` if `parent_collection_id` is not a valid collection ID or +/// the decompression of the hash doesn't return a valid point of `alt_bn128` +/// (maybe insufficient `fuel` parameter) or because of a failing bytes conversion. +pub(crate) fn get_collection_id( + hash: CombinatorialId, + parent_collection_id: Option, + fuel: Fuel, +) -> Result { + let mut u = decompress_hash(hash, fuel)?; + + if let Some(pci) = parent_collection_id { + let v = decompress_collection_id(pci)?; + let w = u + v; // Projective coordinates. + u = w.into(); // Affine coordinates. + } + + // Convert back to bytes _before_ flipping, as flipping will sometimes result in numbers larger + // than the base field modulus. + let bytes_y_even: CombinatorialId = + u.x.into_bigint() + .to_bytes_be() + .try_into() + .map_err(|_| CollectionIdError::EllipticCurvePointXToBytesConversionFailed)?; + + let bytes = if u.y.into_bigint().is_odd() { + flip_second_highest_bit(&bytes_y_even) + } else { + bytes_y_even + }; + + Ok(bytes) +} + +/// Decompresses a collection ID `hash` to a point of `alt_bn128`. The amount of work done can be +/// controlled using the `fuel` parameter. +/// +/// We don't have mathematical proof that the points of `alt_bn128` are distributed so that the +/// required number of iterations is below the specified limit of iterations, but there's good +/// evidence that input hash requires more than `log_2(P) = 507.19338271000436` iterations. With a +/// `fuel.total` value of `32`, statistical evidence suggests a 1 in 500_000_000 chance that the +/// number of iterations will not be enough. +fn decompress_hash(hash: CombinatorialId, fuel: Fuel) -> Result { + // Calculate `odd` first, then get congruent point `x` in `Fq`. As `hash` might represent a + // larger big endian number than `field_modulus()`, the MSB of `x` might be different from the + // MSB of `x_u256`. + let odd = is_msb_set(&hash); + + let mut x = Fq::from_be_bytes_mod_order(&hash); + let mut y_opt = None; + let mut dummy_x = Fq::zero(); // Used to prevent rustc from optimizing dummy work away. + let mut dummy_y = None; + for _ in 0..fuel.total() { + // If `y_opt.is_some()` and we're still in the loop, then `force_max_work` is set and we're + // jus here to spin our wheels for the benchmarks. + if y_opt.is_some() { + // Perform the same calculations as below, but store them in the dummy variables to + // avoid setting off rustc optimizations. + dummy_x = x + Fq::one(); + + let matching_y = matching_y_coordinate(dummy_x); + + if matching_y.is_some() { + dummy_y = matching_y; + } + } else { + x += Fq::one(); + + let matching_y = matching_y_coordinate(x); + + if matching_y.is_some() { + y_opt = matching_y; + + if !fuel.consume_all() { + break; + } + } + } + } + // Ensure that the dummies are considered "read" by rustc. + core::hint::black_box(dummy_x); + core::hint::black_box(dummy_y); + // This **should** be infallible if `fuel.total()` is large. + let mut y = y_opt.ok_or(CollectionIdError::EllipticCurvePointNotFoundWithFuel)?; + + // We have two options for the y-coordinate of the corresponding point: `y` and `P - y`. If + // `odd` is set but `y` isn't odd, we switch to the other option. + if (odd && y.into_bigint().is_even()) || (!odd && y.into_bigint().is_odd()) { + y = y.neg(); + } + + Ok(G1Affine::new(x, y)) +} + +fn decompress_collection_id(collection_id: CombinatorialId) -> Result { + let odd = is_second_msb_set(&collection_id); + let chopped_collection_id = chop_off_two_highest_bits(&collection_id); + let x = Fq::from_be_bytes_mod_order(&chopped_collection_id); + + // Ensure that the big-endian integer represented by `collection_id` was less than the field + // modulus. Otherwise, we consider `collection_id` an invalid ID. + if x.into_bigint().to_bytes_be() != chopped_collection_id { + return Err(CollectionIdError::InvalidParentCollectionId); + } + + // Fails if `collection_id` is not a collection ID. + let mut y = matching_y_coordinate(x).ok_or(CollectionIdError::InvalidParentCollectionId)?; + + // We have two options for the y-coordinate of the corresponding point: `y` and `P - y`. If + // `odd` is set but `y` isn't odd, we switch to the other option. + if (odd && y.into_bigint().is_even()) || (!odd && y.into_bigint().is_odd()) { + y = y.neg(); + } + + Ok(G1Affine::new(x, y)) +} + +/// Flips the second highest bit of big-endian `bytes`. +fn flip_second_highest_bit(bytes: &CombinatorialId) -> CombinatorialId { + let mut bytes = *bytes; + bytes[0] ^= 0b01000000; + bytes +} + +/// Checks if the most significant bit of the big-endian `bytes` is set. +fn is_msb_set(bytes: &CombinatorialId) -> bool { + (bytes[0] & 0b10000000) != 0 +} + +/// Checks if the second most significant bit of the big-endian `bytes` is set. +fn is_second_msb_set(bytes: &CombinatorialId) -> bool { + (bytes[0] & 0b01000000) != 0 +} + +/// Zeroes out the two most significant bits off the big-endian `bytes`. +fn chop_off_two_highest_bits(bytes: &CombinatorialId) -> CombinatorialId { + let mut bytes = *bytes; + bytes[0] &= 0b00111111; + bytes +} + +/// Returns a value `y` of `Fq` so that `(x, y)` is a point on `alt_bn128` or `None` if there is no +/// such value. +fn matching_y_coordinate(x: Fq) -> Option { + let xx = x * x; + let xxx = x * xx; + let yy = xxx + Fq::from(3); + let y = pow_magic_number(yy); + + if y * y == yy { Some(y) } else { None } +} + +/// Returns `x` to the power of `(P + 1) / 4` where `P` is the base field modulus of `alt_bn128`. +fn pow_magic_number(mut x: Fq) -> Fq { + let x_1 = x; + x *= x; + let x_2 = x; + x *= x; + x *= x; + x *= x_2; + let x_10 = x; + x *= x_1; + let x_11 = x; + x *= x_10; + let x_21 = x; + x *= x; + let x_42 = x; + x *= x; + x *= x_42; + x *= x; + x *= x; + x *= x_42; + x *= x_11; + let x_557 = x; + x *= x; + x *= x; + x *= x_21; + let x_2249 = x; + x *= x; + x *= x; + x *= x; + x *= x_2249; + x *= x_557; + let x_20798 = x; + x *= x; + x *= x; + x *= x; + x *= x_20798; + x *= x_2249; + let x_189431 = x; + x *= x_20798; + let x_210229 = x; + x *= x; + x *= x; + x *= x_189431; + let x_1030347 = x; + x *= x; + let x_2060694 = x; + x *= x; + x *= x; + x *= x; + x *= x_2060694; + x *= x_210229; + let x_18756475 = x; + x *= x_1030347; + let x_19786822 = x; + x *= x; + x *= x; + x *= x; + x *= x_18756475; + let x_177051051 = x; + x *= x; + x *= x; + x *= x_177051051; + x *= x; + x *= x; + x *= x_177051051; + x *= x_19786822; + let x_3737858893 = x; + x *= x; + let x_7475717786 = x; + x *= x; + x *= x; + x *= x_7475717786; + x *= x_3737858893; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x_7475717786; + x *= x_177051051; + let x_665515934005 = x; + x *= x; + x *= x_665515934005; + x *= x_3737858893; + let x_2000285660908 = x; + x *= x; + x *= x_2000285660908; + x *= x; + let x_12001713965448 = x; + x *= x; + x *= x_12001713965448; + let x_36005141896344 = x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x_36005141896344; + x *= x_12001713965448; + x *= x_665515934005; + let x_1200836912478805 = x; + x *= x_2000285660908; + let x_1202837198139713 = x; + x *= x; + x *= x_1200836912478805; + let x_3606511308758231 = x; + x *= x_1202837198139713; + let x_4809348506897944 = x; + x *= x_3606511308758231; + let x_8415859815656175 = x; + x *= x_4809348506897944; + let x_13225208322554119 = x; + x *= x_8415859815656175; + let x_21641068138210294 = x; + x *= x; + x *= x_21641068138210294; + x *= x; + x *= x_13225208322554119; + let x_143071617151815883 = x; + x *= x; + x *= x; + x *= x_21641068138210294; + let x_593927536745473826 = x; + x *= x_143071617151815883; + let x_736999153897289709 = x; + x *= x; + x *= x_736999153897289709; + x *= x_593927536745473826; + let x_2804924998437342953 = x; + x *= x_736999153897289709; + let x_3541924152334632662 = x; + x *= x_2804924998437342953; + let x_6346849150771975615 = x; + x *= x_3541924152334632662; + let x_9888773303106608277 = x; + x *= x; + x *= x; + x *= x_9888773303106608277; + x *= x_6346849150771975615; + let x_55790715666305017000 = x; + x *= x; + x *= x_55790715666305017000; + x *= x_9888773303106608277; + let x_177260920302021659277 = x; + x *= x_55790715666305017000; + let x_233051635968326676277 = x; + x *= x_177260920302021659277; + let x_410312556270348335554 = x; + x *= x_233051635968326676277; + let x_643364192238675011831 = x; + x *= x_410312556270348335554; + let x_1053676748509023347385 = x; + x *= x; + x *= x_1053676748509023347385; + x *= x; + x *= x_643364192238675011831; + let x_6965424683292815096141 = x; + x *= x_1053676748509023347385; + let x_8019101431801838443526 = x; + x *= x; + x *= x_8019101431801838443526; + x *= x; + x *= x_6965424683292815096141; + let x_55080033274103845757297 = x; + x *= x; + let x_110160066548207691514594 = x; + x *= x; + x *= x; + x *= x_110160066548207691514594; + x *= x_55080033274103845757297; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x_110160066548207691514594; + x *= x_8019101431801838443526; + let x_9812265024222286383242392 = x; + x *= x_55080033274103845757297; + let x_9867345057496390228999689 = x; + x *= x_9812265024222286383242392; + let x_19679610081718676612242081 = x; + x *= x_9867345057496390228999689; + let x_29546955139215066841241770 = x; + x *= x; + x *= x_29546955139215066841241770; + x *= x; + x *= x; + x *= x; + x *= x_29546955139215066841241770; + x *= x_19679610081718676612242081; + let x_758353488562095347643286331 = x; + x *= x; + x *= x_758353488562095347643286331; + x *= x; + x *= x_29546955139215066841241770; + let x_4579667886511787152700959756 = x; + x *= x; + x *= x_4579667886511787152700959756; + x *= x_758353488562095347643286331; + let x_14497357148097456805746165599 = x; + x *= x_4579667886511787152700959756; + let x_19077025034609243958447125355 = x; + x *= x; + x *= x; + x *= x_14497357148097456805746165599; + let x_90805457286534432639534667019 = x; + x *= x_19077025034609243958447125355; + let x_109882482321143676597981792374 = x; + x *= x; + x *= x_90805457286534432639534667019; + let x_310570421928821785835498251767 = x; + x *= x_109882482321143676597981792374; + let x_420452904249965462433480044141 = x; + x *= x_310570421928821785835498251767; + let x_731023326178787248268978295908 = x; + x *= x; + x *= x_731023326178787248268978295908; + x *= x_420452904249965462433480044141; + let x_2613522882786327207240414931865 = x; + x *= x_731023326178787248268978295908; + let x_3344546208965114455509393227773 = x; + x *= x; + x *= x_3344546208965114455509393227773; + x *= x; + x *= x; + x *= x_2613522882786327207240414931865; + let x_42748077390367700673353133665141 = x; + x *= x; + x *= x; + x *= x; + x *= x_42748077390367700673353133665141; + x *= x_3344546208965114455509393227773; + let x_388077242722274420515687596214042 = x; + x *= x_42748077390367700673353133665141; + let x_430825320112642121189040729879183 = x; + x *= x; + let x_861650640225284242378081459758366 = x; + x *= x_430825320112642121189040729879183; + x *= x; + x *= x; + x *= x_861650640225284242378081459758366; + x *= x_388077242722274420515687596214042; + let x_6419631724299264117162257814522604 = x; + x *= x; + x *= x_430825320112642121189040729879183; + let x_13270088768711170355513556358924391 = x; + x *= x_6419631724299264117162257814522604; + let x_19689720493010434472675814173446995 = x; + x *= x_13270088768711170355513556358924391; + let x_32959809261721604828189370532371386 = x; + x *= x_19689720493010434472675814173446995; + let x_52649529754732039300865184705818381 = x; + x *= x_32959809261721604828189370532371386; + let x_85609339016453644129054555238189767 = x; + x *= x_52649529754732039300865184705818381; + let x_138258868771185683429919739944008148 = x; + x *= x; + x *= x_138258868771185683429919739944008148; + let x_414776606313557050289759219832024444 = x; + x *= x_138258868771185683429919739944008148; + x *= x; + x *= x; + x *= x_414776606313557050289759219832024444; + x *= x_85609339016453644129054555238189767; + let x_2712527845668981629297529614174344579 = x; + x *= x_138258868771185683429919739944008148; + let x_2850786714440167312727449354118352727 = x; + x *= x_2712527845668981629297529614174344579; + let x_5563314560109148942024978968292697306 = x; + x *= x_2850786714440167312727449354118352727; + let x_8414101274549316254752428322411050033 = x; + x *= x_5563314560109148942024978968292697306; + let x_13977415834658465196777407290703747339 = x; + x *= x; + x *= x_13977415834658465196777407290703747339; + x *= x_8414101274549316254752428322411050033; + let x_50346348778524711845084650194522292050 = x; + x *= x_13977415834658465196777407290703747339; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x * x_50346348778524711845084650194522292050 +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/decompress_collection_id.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/decompress_collection_id.rs new file mode 100644 index 000000000..926ff1cfc --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/decompress_collection_id.rs @@ -0,0 +1,578 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +#[test_case( + [0x16, 0x74, 0xab, 0x10, 0xed, 0xf8, 0xc4, 0xe2, 0x25, 0x72, 0x9e, 0x20, 0x9a, 0x58, 0x75, 0xa1, 0x9f, 0x14, 0x46, 0xba, 0xec, 0x3b, 0x30, 0xdf, 0x9b, 0xa8, 0x65, 0x75, 0xd5, 0x2d, 0xe3, 0xd3], + ( + "0x1674ab10edf8c4e225729e209a5875a19f1446baec3b30df9ba86575d52de3d3", + "0x1919edaf92ff08c3c5a2a5dafef1a0c01376dab9681be7fbe3895a18b96af98e", + ) +)] +#[test_case( + [0x02, 0xfd, 0xc0, 0xbc, 0xde, 0x3b, 0x3d, 0xa1, 0xb4, 0xd6, 0x0d, 0x2f, 0x3f, 0x2c, 0xe7, 0x51, 0xd5, 0x20, 0xce, 0x53, 0xe0, 0x10, 0xb1, 0x16, 0x85, 0x9a, 0x8a, 0x9d, 0xe4, 0x6d, 0x45, 0x5e], + ( + "0x2fdc0bcde3b3da1b4d60d2f3f2ce751d520ce53e010b116859a8a9de46d455e", + "0x23a3ae9baa8ed04165a7ebfdb1ffb683d494fc3bf6e402c23b7de5b8ca3b41f6", + ) +)] +#[test_case( + [0x18, 0x0a, 0x0e, 0x6e, 0x26, 0x13, 0xbc, 0x6e, 0x78, 0x1b, 0x9d, 0x8d, 0x9f, 0xc4, 0x16, 0xe0, 0x51, 0xbb, 0xa5, 0xe3, 0x83, 0x63, 0x93, 0xb2, 0x3e, 0x4f, 0x1d, 0x37, 0x7e, 0x2d, 0x52, 0x2d], + ( + "0x180a0e6e2613bc6e781b9d8d9fc416e051bba5e3836393b23e4f1d377e2d522d", + "0x1bc28847ccf82c7345687caba07a45b130e26e3f4489ceb47524e08a37d28d26", + ) +)] +#[test_case( + [0x1d, 0x9b, 0x46, 0x27, 0xaa, 0x60, 0x6d, 0x7d, 0xda, 0xd6, 0xfe, 0xe6, 0x5d, 0xd9, 0x52, 0x5d, 0x75, 0xac, 0x9b, 0x00, 0x14, 0x42, 0xfa, 0xe6, 0x5a, 0x6e, 0xfd, 0x8f, 0xd4, 0x36, 0x9e, 0xb7], + ( + "0x1d9b4627aa606d7ddad6fee65dd9525d75ac9b001442fae65a6efd8fd4369eb7", + "0x20181bfe3b6cfce9bebb4b3870ddbd9f0cc1bdfff9f15f9bc85debc254ab4b9c", + ) +)] +#[test_case( + [0x1d, 0xbf, 0x33, 0x21, 0x4f, 0x0a, 0xbe, 0xab, 0x8e, 0x39, 0x97, 0xf7, 0x6c, 0x79, 0x62, 0x90, 0x79, 0x5a, 0xc5, 0xc5, 0x1f, 0x51, 0xa7, 0xfb, 0x66, 0x25, 0x8c, 0x5a, 0x72, 0x07, 0x78, 0x9d], + ( + "0x1dbf33214f0abeab8e3997f76c796290795ac5c51f51a7fb66258c5a7207789d", + "0x241860400ae39bd169011fc88731985403051a72222cc82db0f443cbd2f9b886", + ) +)] +#[test_case( + [0x22, 0x3c, 0x15, 0x71, 0x4d, 0x71, 0x7d, 0x70, 0x08, 0x31, 0x72, 0xa2, 0x60, 0xa9, 0x6e, 0xb9, 0xe0, 0x13, 0x40, 0x1b, 0xdd, 0x3e, 0xbe, 0x00, 0xd4, 0x71, 0x49, 0x8c, 0xac, 0x52, 0x45, 0x1a], + ( + "0x223c15714d717d70083172a260a96eb9e013401bdd3ebe00d471498cac52451a", + "0xe05de1bd5c32a55bbf45ebbb59bd406ea680b54f81bcba4a0ba2ec481d1d78e", + ) +)] +#[test_case( + [0x23, 0xb5, 0x47, 0xd2, 0x96, 0xbe, 0x38, 0x65, 0x86, 0x60, 0x33, 0xfe, 0xbb, 0xe9, 0x4a, 0x12, 0x49, 0x2c, 0x81, 0x94, 0x38, 0xfa, 0x7b, 0x5a, 0xd6, 0xc2, 0x0c, 0xbf, 0xb2, 0x82, 0x43, 0x0d], + ( + "0x23b547d296be3865866033febbe94a12492c819438fa7b5ad6c20cbfb282430d", + "0x12cdd3c3f897dbc6237d317b1cc607916fafe3775d2971ee40c5248ee18e2b2a", + ) +)] +#[test_case( + [0x0c, 0x05, 0x3e, 0xb3, 0x09, 0xe1, 0x48, 0xe1, 0xe9, 0xde, 0xb2, 0x46, 0xe4, 0xee, 0x89, 0x74, 0x90, 0xed, 0xd5, 0x4e, 0x26, 0xae, 0x27, 0x56, 0xf1, 0xc4, 0x94, 0x0f, 0x28, 0x84, 0x16, 0xe1], + ( + "0xc053eb309e148e1e9deb246e4ee897490edd54e26ae2756f1c4940f288416e1", + "0x23d95d4f591b78e62a38e3d9c4cd40642303f54ad08e94139be77316ea58a16a", + ) +)] +#[test_case( + [0x0b, 0x59, 0x17, 0x6a, 0xba, 0xb9, 0x17, 0xe7, 0x72, 0xfe, 0x94, 0x50, 0xa8, 0x69, 0xcf, 0x62, 0xc8, 0x32, 0x88, 0x4a, 0x0f, 0xfd, 0xb6, 0x06, 0xbb, 0x6b, 0x3f, 0xa7, 0x0c, 0x1d, 0xb9, 0x1a], + ( + "0xb59176abab917e772fe9450a869cf62c832884a0ffdb606bb6b3fa70c1db91a", + "0x1420a6cb7d026a2c9cb2c05f7ff86afc73a11a6dfd90ce540cfe02d19372524e", + ) +)] +#[test_case( + [0x1e, 0x08, 0x14, 0xc5, 0xe1, 0x64, 0x5b, 0x62, 0x5b, 0x9d, 0xfc, 0xff, 0xe9, 0x7e, 0x49, 0x77, 0x32, 0xf5, 0xfc, 0x65, 0x06, 0x1c, 0x75, 0xf0, 0x06, 0x06, 0x92, 0xb7, 0xa2, 0xea, 0x39, 0x83], + ( + "0x1e0814c5e1645b625b9dfcffe97e497732f5fc65061c75f0060692b7a2ea3983", + "0x1506fac00eed4e27b88c2d07811b0e1586a42494e9f5acf95dac46d25ab93b00", + ) +)] +#[test_case( + [0x07, 0x50, 0x54, 0x78, 0x15, 0x5f, 0xcf, 0x43, 0x5d, 0x96, 0x77, 0xcc, 0x58, 0x7c, 0x85, 0x1e, 0x47, 0x02, 0xb7, 0x3d, 0xc2, 0xd8, 0xc6, 0xf5, 0x16, 0x7d, 0xbb, 0x84, 0x6c, 0x72, 0xbd, 0xb3], + ( + "0x7505478155fcf435d9677cc587c851e4702b73dc2d8c6f5167dbb846c72bdb3", + "0xce98d3afa473f8a47d734aa55dd74f3f6d18faf46689346e9930dfd2689877a", + ) +)] +#[test_case( + [0x28, 0x0a, 0x7e, 0xbf, 0xaf, 0xce, 0x97, 0x43, 0x55, 0x0f, 0x42, 0x8f, 0xc2, 0xd4, 0xdd, 0x28, 0x8f, 0xa8, 0x13, 0x24, 0xcb, 0x6e, 0x10, 0xa6, 0x7b, 0x42, 0x34, 0x5f, 0x1b, 0x76, 0xc6, 0x5d], + ( + "0x280a7ebfafce9743550f428fc2d4dd288fa81324cb6e10a67b42345f1b76c65d", + "0x26ca96ef76f1c517684cfeb53b22157e340990317c020fcf8b631e7157e4ce92", + ) +)] +#[test_case( + [0x08, 0x46, 0xe2, 0x53, 0x97, 0x46, 0xca, 0x06, 0xad, 0xa1, 0x8b, 0x22, 0xba, 0x2f, 0x66, 0xda, 0xcc, 0xaf, 0x0e, 0x9a, 0x99, 0x5c, 0x29, 0x35, 0xce, 0x8d, 0xbc, 0x55, 0x20, 0x8d, 0xcc, 0xbb], + ( + "0x846e2539746ca06ada18b22ba2f66daccaf0e9a995c2935ce8dbc55208dccbb", + "0x1336db44313546d7e740d3b1ed924eb8dbbd73dbb4399b7e37549562ad789930", + ) +)] +#[test_case( + [0x1b, 0xe5, 0xd3, 0x49, 0x51, 0x06, 0x3a, 0x47, 0x6a, 0x3c, 0x78, 0xa7, 0xdb, 0x40, 0x85, 0x5c, 0x49, 0xf2, 0xc5, 0x70, 0xc5, 0x06, 0xb8, 0x5e, 0x3b, 0xef, 0x44, 0x05, 0x68, 0xab, 0x02, 0xab], + ( + "0x1be5d34951063a476a3c78a7db40855c49f2c570c506b85e3bef440568ab02ab", + "0x5d5ebaef6981b87cde5ed99565a36d4eafbc6fbacab1f0cd15e509fba71d8c2", + ) +)] +#[test_case( + [0x01, 0x37, 0xe1, 0x21, 0xfc, 0x1b, 0xc8, 0x0c, 0x5b, 0x30, 0xf2, 0xad, 0x1a, 0x08, 0xe9, 0x26, 0x53, 0x30, 0xfb, 0x33, 0x07, 0x84, 0xa5, 0x63, 0x43, 0xc2, 0x9a, 0xc0, 0x46, 0x20, 0x1d, 0x1b], + ( + "0x137e121fc1bc80c5b30f2ad1a08e9265330fb330784a56343c29ac046201d1b", + "0x18d3facbbf735827083ff4e12a625ba2218edffef5c8815afdefea6528801fec", + ) +)] +#[test_case( + [0x2d, 0xf7, 0x8d, 0xdd, 0x64, 0xbc, 0xb7, 0x53, 0x60, 0xa7, 0x88, 0x16, 0x35, 0x29, 0xe2, 0x84, 0x95, 0x04, 0x08, 0x4a, 0x4b, 0x79, 0x91, 0x17, 0x28, 0xee, 0x33, 0x03, 0x4c, 0x7f, 0x6c, 0xd3], + ( + "0x2df78ddd64bcb75360a788163529e2849504084a4b79911728ee33034c7f6cd3", + "0x1703b907563d006f1df3fe65c0ffff73d469b58b831827d2b68e12278bf9f0e", + ) +)] +#[test_case( + [0x11, 0x05, 0x42, 0x04, 0x50, 0x3c, 0x67, 0x3f, 0x29, 0x4d, 0xf5, 0x82, 0xe7, 0x19, 0xe9, 0x5c, 0x40, 0x50, 0x2b, 0x65, 0xda, 0x36, 0xae, 0x0b, 0x05, 0x1c, 0xea, 0x5b, 0xa3, 0x80, 0x24, 0x7f], + ( + "0x11054204503c673f294df582e719e95c40502b65da36ae0b051cea5ba380247f", + "0xe9c934bcf52dfdefbcd8b9b1d669e41924ae89fb758d1bfacdc33c6ab98b6fc", + ) +)] +#[test_case( + [0x09, 0x1d, 0x74, 0x16, 0xbb, 0xf0, 0x48, 0x03, 0x92, 0x0a, 0x1f, 0x84, 0xd1, 0xd5, 0x63, 0x28, 0x0d, 0xbb, 0x5a, 0x6e, 0x3b, 0x03, 0x3f, 0xce, 0x74, 0xaf, 0x4a, 0x6d, 0x63, 0xf9, 0x02, 0xe3], + ( + "0x91d7416bbf04803920a1f84d1d563280dbb5a6e3b033fce74af4a6d63f902e3", + "0x2976e2bde1f1b94ba5a82a3323351f48148a1e518ac2ed0aa0070fcbd12a9784", + ) +)] +#[test_case( + [0x1b, 0x09, 0x08, 0xb9, 0xc1, 0xf2, 0x25, 0xa8, 0xfe, 0xf8, 0xf1, 0xff, 0x1d, 0x89, 0xf8, 0x65, 0x44, 0x06, 0x5a, 0xb2, 0xf5, 0x28, 0xed, 0x8a, 0x88, 0x47, 0x39, 0x04, 0x4b, 0x9b, 0x5d, 0x0d], + ( + "0x1b0908b9c1f225a8fef8f1ff1d89f86544065ab2f528ed8a884739044b9b5d0d", + "0x13bd194272a6615dfbfae1f888ace71eb2c035973b841c47cabbd5f617b55e5c", + ) +)] +#[test_case( + [0x1e, 0xb7, 0xf8, 0x6e, 0x71, 0x58, 0x7f, 0x50, 0x91, 0x19, 0xec, 0xe1, 0xb1, 0x90, 0x01, 0xfa, 0xc7, 0xbd, 0x45, 0x79, 0xa1, 0xe1, 0xae, 0xdf, 0xca, 0x4e, 0x11, 0x9a, 0x77, 0x78, 0xc0, 0xe0], + ( + "0x1eb7f86e71587f509119ece1b19001fac7bd4579a1e1aedfca4e119a7778c0e0", + "0x14a7107c10f3b98153f4750d35b7dc5713defeab87d7ce4712ed84b7657efb84", + ) +)] +#[test_case( + [0x18, 0x2f, 0x02, 0x1a, 0xcc, 0x92, 0x56, 0x45, 0x5e, 0x36, 0xf8, 0x2a, 0xca, 0xec, 0x16, 0xbd, 0xd4, 0x53, 0x6d, 0x1d, 0xca, 0xa1, 0xcd, 0x4b, 0xa0, 0x66, 0xb7, 0xba, 0xb4, 0x06, 0x7d, 0x72], + ( + "0x182f021acc9256455e36f82acaec16bdd4536d1dcaa1cd4ba066b7bab4067d72", + "0x1f326b433282b1e92c7012bf026405427d2490d9e28e6c01273f988255b9983c", + ) +)] +#[test_case( + [0x13, 0xbc, 0xaa, 0xb8, 0x01, 0xe0, 0x94, 0x26, 0xf8, 0xda, 0xa9, 0x2b, 0xf2, 0xca, 0x83, 0x28, 0x2a, 0xe3, 0xed, 0x70, 0xcc, 0x8c, 0x27, 0x7a, 0xa3, 0x44, 0xb8, 0xfe, 0xb9, 0x72, 0x81, 0x8c], + ( + "0x13bcaab801e09426f8daa92bf2ca83282ae3ed70cc8c277aa344b8feb972818c", + "0xc1abae2e25a39dcc9054e7122a7403439b3a0f0ebbddfe3c1ac26c8023f2cc", + ) +)] +#[test_case( + [0x1d, 0x87, 0x67, 0x17, 0x6e, 0xc6, 0xd9, 0x75, 0x96, 0xd0, 0x4e, 0x6b, 0xd7, 0x02, 0x4a, 0xa1, 0xcf, 0x32, 0x59, 0x50, 0x89, 0xb6, 0x45, 0x17, 0xa4, 0x3c, 0xd1, 0x0c, 0x1f, 0x99, 0x01, 0xbf], + ( + "0x1d8767176ec6d97596d04e6bd7024aa1cf32595089b64517a43cd10c1f9901bf", + "0x2302a48e833e3c702ed1eb82eebf8edb8b2cd48e03516061723ee87430a8d06a", + ) +)] +#[test_case( + [0x06, 0xfd, 0x22, 0x53, 0x3c, 0xcd, 0x75, 0x7c, 0xb6, 0xdb, 0xfd, 0x1d, 0x32, 0xbe, 0xbb, 0x29, 0x30, 0x3d, 0xa7, 0x1b, 0xc3, 0x34, 0x6b, 0x96, 0xf8, 0x76, 0x6e, 0x7e, 0xbd, 0xbf, 0x04, 0x61], + ( + "0x6fd22533ccd757cb6dbfd1d32bebb29303da71bc3346b96f8766e7ebdbf0461", + "0x246972fcae26d8b93dc62ffc2462c50cdfe22af01ca802f4072349ea4823b34e", + ) +)] +#[test_case( + [0x2d, 0x8f, 0x77, 0x8c, 0xfe, 0xbd, 0x03, 0x80, 0x95, 0xf3, 0x03, 0x8d, 0xdf, 0x86, 0x25, 0x44, 0xf8, 0x79, 0x8a, 0x64, 0xfe, 0x42, 0x33, 0x04, 0x7a, 0x2d, 0x29, 0x0e, 0xef, 0x4f, 0xf5, 0xd6], + ( + "0x2d8f778cfebd038095f3038ddf862544f8798a64fe4233047a2d290eef4ff5d6", + "0x27d0fab4ecf2e55a22af1f3f4d44e79ab133a2b9baecc4a59ae044d1c4ca4d52", + ) +)] +#[test_case( + [0x01, 0x55, 0x8a, 0xd8, 0xdd, 0xf9, 0x24, 0xe5, 0x75, 0x8f, 0x4c, 0x29, 0x4b, 0x56, 0xfc, 0x27, 0x1f, 0xb2, 0xa8, 0x38, 0x2a, 0xaa, 0x86, 0x0d, 0x94, 0x3f, 0x52, 0x49, 0xa5, 0xe7, 0x63, 0x34], + ( + "0x1558ad8ddf924e5758f4c294b56fc271fb2a8382aaa860d943f5249a5e76334", + "0x155645307359f5a7a6c304a353715fd8023809355a255daf4854f026d92693dc", + ) +)] +#[test_case( + [0x1e, 0x3c, 0xd8, 0xb4, 0xa8, 0x9e, 0x71, 0x63, 0x8a, 0xbf, 0xb0, 0x10, 0xe7, 0xfc, 0x77, 0x9c, 0xe9, 0x59, 0xe6, 0x39, 0x66, 0x73, 0x26, 0x37, 0x12, 0x83, 0x7c, 0xf0, 0xed, 0x76, 0x63, 0x91], + ( + "0x1e3cd8b4a89e71638abfb010e7fc779ce959e6396673263712837cf0ed766391", + "0x11d80dd5dd67cc67fcfe9bed95cfc2c4101526932088c041239885d723f0bc86", + ) +)] +#[test_case( + [0x22, 0x60, 0x1d, 0x97, 0x77, 0x53, 0x1c, 0x8c, 0x68, 0x75, 0x89, 0x8e, 0x47, 0xff, 0xd8, 0x04, 0xb2, 0x21, 0x4e, 0xc2, 0x41, 0x1f, 0x40, 0x43, 0x25, 0x9c, 0x45, 0x76, 0xc7, 0x4e, 0x75, 0xfb], + ( + "0x22601d9777531c8c6875898e47ffd804b2214ec2411f4043259c4576c74e75fb", + "0xe61684d4e5ffd436cc8cc5cf7de02d8a3c373b396b72f9692e6694b998164c2", + ) +)] +#[test_case( + [0x27, 0x01, 0xa9, 0xee, 0x7c, 0x63, 0x65, 0xce, 0x47, 0x75, 0x21, 0x31, 0xe9, 0xb9, 0xdd, 0x46, 0xb4, 0xc0, 0x48, 0xa9, 0x0d, 0x92, 0xf2, 0xe2, 0xf6, 0xae, 0xbb, 0x22, 0x6e, 0x58, 0xf0, 0x40], + ( + "0x2701a9ee7c6365ce47752131e9b9dd46b4c048a90d92f2e2f6aebb226e58f040", + "0x1875a82988dfe307569b5171ba09cc1707e1b03ecd3084f3251395a7894f0c22", + ) +)] +#[test_case( + [0x08, 0xbb, 0x8c, 0x18, 0x75, 0x35, 0x03, 0x40, 0x36, 0x3e, 0xe4, 0x35, 0x02, 0xba, 0x73, 0xf6, 0x77, 0x73, 0x3f, 0x29, 0xb2, 0x25, 0x2d, 0xf1, 0x89, 0x72, 0xcc, 0x96, 0x4b, 0xd4, 0x62, 0xc5], + ( + "0x8bb8c1875350340363ee43502ba73f677733f29b2252df18972cc964bd462c5", + "0x299bdb13d2514ee0677704821b2d2748def74762a22fd2bf649ceb17b91f9996", + ) +)] +#[test_case( + [0x01, 0xd7, 0x14, 0x30, 0x44, 0xba, 0x51, 0x3f, 0x92, 0x9f, 0xe7, 0x38, 0xd8, 0x0b, 0xd8, 0x4a, 0x45, 0x5e, 0x2b, 0xa9, 0x93, 0x59, 0x87, 0xaa, 0x7f, 0x8c, 0xf7, 0x7e, 0xc1, 0x8c, 0xf2, 0x0b], + ( + "0x1d7143044ba513f929fe738d80bd84a455e2ba9935987aa7f8cf77ec18cf20b", + "0x1a4e9dedf0f06f7c20e2c324c84e74f58b2c4845c3642ec20b3eb683e75b6edc", + ) +)] +#[test_case( + [0x07, 0x8a, 0x2a, 0x4c, 0x2e, 0xea, 0x7a, 0x70, 0x4a, 0x12, 0x05, 0xd4, 0x96, 0xdb, 0x94, 0x62, 0xe1, 0xee, 0xb1, 0xcb, 0x30, 0xcd, 0xd8, 0xf9, 0xc3, 0xc6, 0xca, 0x42, 0x3a, 0xdc, 0x61, 0xca], + ( + "0x78a2a4c2eea7a704a1205d496db9462e1eeb1cb30cdd8f9c3c6ca423adc61ca", + "0x5e7dd7f679c96cdf9509fb3d4db66af20f28a8dcc2622f1c90419110b465828", + ) +)] +#[test_case( + [0x1d, 0xbe, 0x17, 0xe6, 0x50, 0x79, 0x12, 0x6c, 0xaf, 0x00, 0x09, 0x7b, 0xdf, 0x54, 0x7c, 0x44, 0x57, 0xc6, 0x15, 0x1f, 0x4c, 0x6b, 0x90, 0xf4, 0xdc, 0x54, 0x24, 0xf5, 0x66, 0xdf, 0x0e, 0xe7], + ( + "0x1dbe17e65079126caf00097bdf547c4457c6151f4c6b90f4dc5424f566df0ee7", + "0x1f2fcbc14e2148084446caa954a2e0765dcf8af48e69dc186de83efa94fc806e", + ) +)] +#[test_case( + [0x2d, 0xec, 0xa9, 0x23, 0x55, 0x5c, 0x5c, 0xfc, 0xa7, 0x97, 0x2d, 0xb2, 0xb8, 0x38, 0xb5, 0x68, 0xef, 0xef, 0x51, 0xfa, 0x44, 0x72, 0x4c, 0x66, 0x4c, 0xc0, 0x45, 0x2a, 0xb9, 0xff, 0x7d, 0x63], + ( + "0x2deca923555c5cfca7972db2b838b568efef51fa44724c664cc0452ab9ff7d63", + "0x2d47b4ffdebf3532efc6490bab958d393fd785dd02bbc2b03fc24a9678ee304", + ) +)] +#[test_case( + [0x01, 0x98, 0x31, 0xeb, 0xf6, 0xa1, 0x58, 0x81, 0x45, 0x57, 0xfe, 0x02, 0x9a, 0x45, 0x37, 0xd5, 0xbf, 0x0f, 0xa3, 0xee, 0x84, 0xba, 0x43, 0x56, 0xe0, 0xe5, 0x98, 0x5f, 0x11, 0x29, 0x4a, 0xa4], + ( + "0x19831ebf6a158814557fe029a4537d5bf0fa3ee84ba4356e0e5985f11294aa4", + "0x2c8cad155eebb55bfd8492efade248ccee03202f5efb361904f74bbba5a17410", + ) +)] +#[test_case( + [0x2c, 0x72, 0xbe, 0xfc, 0x77, 0xa2, 0x88, 0xdb, 0xc8, 0x9f, 0xd6, 0x11, 0xcf, 0x22, 0x73, 0x5c, 0x64, 0x4a, 0x34, 0xad, 0x2b, 0xb0, 0x49, 0x20, 0xd1, 0x62, 0x96, 0xc8, 0x77, 0x0e, 0x81, 0x7b], + ( + "0x2c72befc77a288dbc89fd611cf22735c644a34ad2bb04920d16296c8770e817b", + "0x68230ca4c35321ef952a1336e48b5b99f74baeb5b4ff2ad6481e652274eb984", + ) +)] +#[test_case( + [0x28, 0x26, 0x3b, 0xc4, 0xaa, 0xed, 0xbc, 0xcc, 0xd2, 0xf6, 0x87, 0xa2, 0x05, 0x60, 0x48, 0x3c, 0x6a, 0x0a, 0xee, 0x2c, 0x89, 0x49, 0x74, 0x45, 0x75, 0xad, 0xc1, 0xf8, 0x1d, 0x4f, 0x72, 0xda], + ( + "0x28263bc4aaedbcccd2f687a20560483c6a0aee2c8949744575adc1f81d4f72da", + "0x1bfd7ddceed64b57a5b069ae92b2e7d56706c7a9dc56f4bbd82d986ee95603c6", + ) +)] +#[test_case( + [0x26, 0x38, 0x46, 0x0e, 0x75, 0x6c, 0x86, 0x2b, 0x10, 0xbe, 0x8b, 0xda, 0x35, 0x13, 0x9a, 0xa7, 0xa6, 0x80, 0xcb, 0xf8, 0xab, 0x74, 0x0a, 0x1a, 0xdc, 0xa9, 0x6e, 0xd2, 0x2e, 0xf0, 0xb1, 0x6f], + ( + "0x2638460e756c862b10be8bda35139aa7a680cbf8ab740a1adca96ed22ef0b16f", + "0xde2e2c86e45de9a933cc052dd17707b8b5309e39914a0f9c79c134d76147b04", + ) +)] +#[test_case( + [0x05, 0x52, 0x6b, 0xc9, 0xa0, 0xd7, 0x16, 0xe6, 0x66, 0x70, 0xd2, 0x31, 0x9e, 0x04, 0x1e, 0x46, 0xc9, 0x41, 0x64, 0xc4, 0x1c, 0x0c, 0xa7, 0x12, 0xe8, 0x11, 0x7b, 0x1a, 0xf6, 0x46, 0x76, 0xf8], + ( + "0x5526bc9a0d716e66670d2319e041e46c94164c41c0ca712e8117b1af64676f8", + "0x25e29f0a37ed937d2c226650fc78037790ea9c6d80baef29fa4539438bf853d8", + ) +)] +#[test_case( + [0x2b, 0x70, 0xd4, 0xe9, 0x4d, 0x8e, 0x35, 0x49, 0xd6, 0x09, 0x53, 0xf5, 0x18, 0x65, 0x9b, 0xb8, 0x54, 0xf2, 0x22, 0x7c, 0x5a, 0x88, 0xde, 0x27, 0xdb, 0x77, 0x70, 0x51, 0x8e, 0xd3, 0xe9, 0x31], + ( + "0x2b70d4e94d8e3549d60953f518659bb854f2227c5a88de27db7770518ed3e931", + "0x1c844b23b907c8244677b9bd1b3c64edaa47ac04537c19762d7b38d43cd1f148", + ) +)] +#[test_case( + [0x2e, 0x53, 0xd1, 0xcd, 0xd2, 0x8c, 0x7f, 0x6c, 0x2e, 0xa0, 0xc3, 0xc3, 0x2e, 0xea, 0x02, 0x28, 0x51, 0x10, 0xeb, 0xb9, 0xcc, 0x50, 0x7a, 0xa0, 0xc1, 0x2f, 0x1e, 0x33, 0xf7, 0x6e, 0xdf, 0x74], + ( + "0x2e53d1cdd28c7f6c2ea0c3c32eea02285110ebb9cc507aa0c12f1e33f76edf74", + "0x1a87019d19b0108728fa9e79c9083ad6e8688989f09920354380ac2721827282", + ) +)] +#[test_case( + [0x15, 0x69, 0x8b, 0x00, 0x9f, 0x37, 0xf8, 0xa4, 0x64, 0x62, 0xdb, 0xc2, 0x68, 0x8f, 0xff, 0xee, 0xb6, 0x78, 0x71, 0x30, 0xd8, 0xc1, 0xd6, 0xb2, 0x6d, 0x5b, 0xf8, 0xa1, 0x8d, 0x56, 0x27, 0xbf], + ( + "0x15698b009f37f8a46462dbc2688fffeeb6787130d8c1d6b26d5bf8a18d5627bf", + "0x189c4535196083e9a50d66efef26e4d599879d0244ddc08978315df04b41533a", + ) +)] +#[test_case( + [0x23, 0xa4, 0x90, 0x60, 0xff, 0xf2, 0xef, 0x43, 0x29, 0xbc, 0xe4, 0x7d, 0x12, 0x7a, 0x0c, 0x11, 0x3d, 0x66, 0xf4, 0xf8, 0xc1, 0x4f, 0x23, 0x94, 0xc7, 0x97, 0x6c, 0xff, 0x59, 0x5b, 0xd5, 0xaa], + ( + "0x23a49060fff2ef4329bce47d127a0c113d66f4f8c14f2394c7976cff595bd5aa", + "0x11cce022cb4450c53e53839a3940567784be6fb67d6f561966b9dee65f987a3e", + ) +)] +#[test_case( + [0x23, 0x32, 0x9b, 0x82, 0x1e, 0x5f, 0xbb, 0xa3, 0x02, 0x01, 0x27, 0xe3, 0x39, 0x28, 0xe4, 0xf2, 0x50, 0x48, 0x06, 0x17, 0xf9, 0x17, 0x39, 0x96, 0x60, 0xbd, 0x7b, 0x08, 0xb9, 0x28, 0x8b, 0x48], + ( + "0x23329b821e5fbba3020127e33928e4f250480617f917399660bd7b08b9288b48", + "0x197578bd1194ee1d207e9b0d2e4f2e55e4512a1e818dcf3556176ff017192dc2", + ) +)] +#[test_case( + [0x1f, 0x15, 0xd8, 0xcc, 0x8d, 0x7e, 0x53, 0x64, 0xd5, 0xac, 0x6e, 0xa0, 0xd3, 0x23, 0x12, 0x12, 0x76, 0xb1, 0x45, 0xbd, 0xdf, 0x05, 0x68, 0x1d, 0x2f, 0xcc, 0x3d, 0x2f, 0xe5, 0x77, 0x23, 0xf4], + ( + "0x1f15d8cc8d7e5364d5ac6ea0d323121276b145bddf05681d2fcc3d2fe57723f4", + "0x17ddaa99c0dbdfcc568d7b05a25a7f16a64f16eb2e86b6c8f7ec2b9998887792", + ) +)] +#[test_case( + [0x07, 0x76, 0x7c, 0x7e, 0x57, 0x22, 0xba, 0x85, 0x87, 0x91, 0x20, 0x15, 0x4f, 0x58, 0xaa, 0x16, 0xe2, 0xdb, 0x21, 0x75, 0x79, 0xea, 0x1d, 0x3d, 0xf7, 0x66, 0xbc, 0x4c, 0xad, 0xea, 0xc5, 0x4c], + ( + "0x7767c7e5722ba85879120154f58aa16e2db217579ea1d3df766bc4cadeac54c", + "0x871640dd8539208c68524db2071d814214b0c5c6e7de73aaacd2afcddb6ffc8", + ) +)] +#[test_case( + [0x29, 0x88, 0x22, 0x5b, 0x7c, 0x44, 0xf9, 0xe5, 0x06, 0xbb, 0xfe, 0x85, 0x39, 0xcd, 0x26, 0xc8, 0xb9, 0xb8, 0xec, 0xf3, 0xec, 0xab, 0x33, 0x1d, 0x86, 0x95, 0xad, 0xf3, 0x5e, 0x3f, 0xda, 0x07], + ( + "0x2988225b7c44f9e506bbfe8539cd26c8b9b8ecf3ecab331d8695adf35e3fda07", + "0x2f44627a25fb04aa6b44300e7421bf3ee3196d0538e5ae2b210546f783f2cd32", + ) +)] +#[test_case( + [0x2b, 0x5e, 0x49, 0x45, 0xc0, 0x74, 0x9b, 0xf9, 0xe1, 0x7e, 0x4d, 0x1d, 0xea, 0xc5, 0xe4, 0x89, 0x21, 0xb4, 0xc2, 0x82, 0xee, 0x45, 0x08, 0x8a, 0x7b, 0xf9, 0x6a, 0x1b, 0xc5, 0x42, 0xb5, 0x71], + ( + "0x2b5e4945c0749bf9e17e4d1deac5e48921b4c282ee45088a7bf96a1bc542b571", + "0x28b3a31a05ada890146c5e0227de13e27baa96bc781d5e5bcd6109b713d5f758", + ) +)] +#[test_case( + [0x23, 0x55, 0x02, 0x2c, 0x52, 0x57, 0x66, 0x1b, 0xfd, 0xce, 0xbe, 0xd1, 0xc7, 0xad, 0x0e, 0x22, 0x96, 0xa3, 0x3d, 0x4b, 0xe3, 0x05, 0x4d, 0x73, 0x85, 0x3c, 0xf6, 0x3d, 0x60, 0xec, 0x45, 0x70], + ( + "0x2355022c5257661bfdcebed1c7ad0e2296a33d4be3054d73853cf63d60ec4570", + "0xfcfaf1ef9839f6f5598a916ae7c28fc28bc0c987c5c3f9e170159e322c0ec58", + ) +)] +#[test_case( + [0x26, 0x05, 0x25, 0x5c, 0x41, 0x1a, 0x24, 0x88, 0x26, 0x14, 0xa9, 0x47, 0x8e, 0xd2, 0x66, 0x76, 0x22, 0xed, 0xa5, 0xd8, 0xb1, 0xc8, 0x12, 0xc8, 0x2b, 0xd3, 0x8a, 0xac, 0xf9, 0x7b, 0x46, 0xff], + ( + "0x2605255c411a24882614a9478ed2667622eda5d8b1c812c82bd38aacf97b46ff", + "0x20df728f992c0dbd084c479155b1b8cc4b0ace3bc7b7d31d671ed49d568b2d1a", + ) +)] +#[test_case( + [0x15, 0x84, 0x15, 0x4f, 0xf4, 0xec, 0xd3, 0x96, 0x7f, 0x84, 0x94, 0xfa, 0x33, 0xe9, 0x4f, 0x0e, 0x69, 0xb6, 0x9e, 0xb7, 0x50, 0xec, 0xe2, 0x14, 0x83, 0x78, 0xda, 0xbc, 0xba, 0xd3, 0x8c, 0x05], + ( + "0x1584154ff4ecd3967f8494fa33e94f0e69b69eb750ece2148378dabcbad38c05", + "0x51ad519b5257340edff33212434e68c5b43486ceed9211469506621abd5ece", + ) +)] +#[test_case( + [0x1b, 0x73, 0x05, 0x71, 0x5a, 0xdb, 0xc0, 0x99, 0xeb, 0xeb, 0xf9, 0x2d, 0x7d, 0xa0, 0x9e, 0x04, 0xfd, 0x2f, 0x85, 0x9f, 0xd7, 0x61, 0x3d, 0xc2, 0x60, 0xb0, 0x8d, 0x76, 0x73, 0x7c, 0x65, 0x4d], + ( + "0x1b7305715adbc099ebebf92d7da09e04fd2f859fd7613dc260b08d76737c654d", + "0x14a4f76f9fbcc89c48a1f88f7603d551d1fb5e46c539d17cf11b18a73265a4ec", + ) +)] +#[test_case( + [0x15, 0xa6, 0xf1, 0x77, 0xcb, 0xa6, 0x73, 0x5b, 0x75, 0x5b, 0xdd, 0x33, 0xd4, 0x93, 0xe8, 0xa2, 0xe1, 0xd7, 0xe4, 0x16, 0x05, 0x40, 0xb1, 0x57, 0xca, 0x70, 0x37, 0x82, 0xeb, 0x72, 0x16, 0xf5], + ( + "0x15a6f177cba6735b755bdd33d493e8a2e1d7e4160540b157ca703782eb7216f5", + "0x143b33d4f6e8b2ec4b4438e76b334e959a2094ba76693e10fb6e0bbbe1b56a52", + ) +)] +#[test_case( + [0x1c, 0x79, 0x7f, 0xe1, 0x35, 0x4c, 0x09, 0x27, 0xcf, 0x6b, 0x44, 0xe7, 0xa0, 0x3e, 0xe9, 0x51, 0xb5, 0x89, 0xf7, 0x3d, 0x53, 0x7f, 0xa7, 0x93, 0xcb, 0xf6, 0xc4, 0xdd, 0x36, 0x6a, 0x1c, 0x9a], + ( + "0x1c797fe1354c0927cf6b44e7a03ee951b589f73d537fa793cbf6c4dd366a1c9a", + "0x2ac21244ff5ef8ce3981c171cad186d69533485c3e58e5dec28814a6a528491c", + ) +)] +#[test_case( + [0x2c, 0x97, 0x85, 0xc9, 0x60, 0x78, 0xde, 0x3b, 0xdc, 0x3d, 0x95, 0x34, 0x75, 0x81, 0x32, 0x94, 0x45, 0x57, 0x0e, 0x46, 0xc9, 0x7f, 0xc5, 0x42, 0xad, 0x8b, 0xcd, 0xf1, 0xd7, 0x27, 0x42, 0xd8], + ( + "0x2c9785c96078de3bdc3d95347581329445570e46c97fc542ad8bcdf1d72742d8", + "0x14fc6cf9920b68bca02cd2223108a03fb4c8aa5fbb9335de3d7b74be069700e6", + ) +)] +#[test_case( + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], + ("0x1", "0x2") +)] +fn decompress_collection_id_works(collection_id: CombinatorialId, expected: (&str, &str)) { + let x = Fq::from_hex_str(expected.0); + let y = Fq::from_hex_str(expected.1); + let expected = G1Affine::new(x, y); + + let actual = decompress_collection_id(collection_id).unwrap(); + assert_eq!(actual, expected); +} + +#[test_case( + [0x64, 0xf3, 0x41, 0x72, 0x9b, 0xea, 0x43, 0x90, 0x10, 0x1d, 0x0b, 0x1a, 0xcc, 0x67, 0x47, 0xbc, 0x0d, 0x8d, 0x1a, 0xc5, 0x9f, 0xf0, 0xb3, 0x2f, 0xe9, 0x91, 0x94, 0x93, 0x8b, 0x70, 0xa4, 0xda] +)] +#[test_case( + [0x71, 0xfb, 0x5a, 0x00, 0x74, 0xc4, 0xfd, 0xf8, 0xff, 0x2a, 0x59, 0x75, 0x0c, 0xb7, 0x25, 0x7c, 0x60, 0x6b, 0x5d, 0x09, 0x93, 0xf0, 0xe7, 0x9c, 0x33, 0x08, 0x84, 0x72, 0xbc, 0x98, 0xb0, 0xf2] +)] +#[test_case( + [0xed, 0x84, 0xb8, 0xdd, 0xca, 0x0c, 0x1b, 0x21, 0x42, 0x48, 0x4f, 0x42, 0x0e, 0x05, 0xba, 0x7b, 0x19, 0xa7, 0x91, 0xc4, 0xd9, 0x17, 0x5f, 0x4f, 0x49, 0xf7, 0x83, 0x6f, 0xf1, 0xfa, 0xba, 0xae] +)] +#[test_case( + [0x93, 0x0c, 0xf8, 0x88, 0x63, 0xc7, 0x0c, 0x66, 0x28, 0x5f, 0x4b, 0x96, 0x17, 0x10, 0x32, 0x65, 0x3d, 0x91, 0xc5, 0x0e, 0xcf, 0xda, 0x23, 0xf1, 0x82, 0x7e, 0x6a, 0x9b, 0x16, 0xb1, 0x50, 0x95] +)] +#[test_case( + [0x72, 0x7a, 0xf5, 0x5e, 0x17, 0x46, 0xa7, 0x00, 0xd1, 0xde, 0x3e, 0x03, 0x99, 0x92, 0x91, 0x20, 0xdd, 0xf7, 0xae, 0xff, 0xb3, 0x2d, 0xd9, 0x53, 0x18, 0xdc, 0xf5, 0x4d, 0x39, 0x44, 0xa3, 0xd8] +)] +#[test_case( + [0xd5, 0x7f, 0xa0, 0x9a, 0x3f, 0xa7, 0xaf, 0xb2, 0x1c, 0x94, 0xb0, 0x3b, 0x06, 0x65, 0xd3, 0x59, 0x5f, 0xa8, 0x48, 0x18, 0x7e, 0x68, 0xe2, 0xbc, 0x01, 0x0b, 0xfc, 0x16, 0xb1, 0x65, 0x55, 0x63] +)] +#[test_case( + [0x1a, 0x04, 0x7d, 0x43, 0x00, 0xd3, 0x6f, 0xb3, 0xea, 0xef, 0x0b, 0x27, 0x71, 0xf4, 0x54, 0x02, 0xf4, 0x05, 0xd9, 0x90, 0x84, 0x08, 0x7a, 0xd3, 0xd9, 0x59, 0xfb, 0x0d, 0x3f, 0x4d, 0x7d, 0xf4] +)] +#[test_case( + [0xbc, 0x15, 0xb3, 0x40, 0x0d, 0xe4, 0x0a, 0xd4, 0x96, 0x68, 0x98, 0x6a, 0xca, 0xb4, 0xf2, 0xa6, 0x2b, 0x5c, 0x8e, 0x18, 0x3e, 0x22, 0xd1, 0xa1, 0xe3, 0x52, 0xa8, 0x86, 0xc6, 0x56, 0xc2, 0xa9] +)] +#[test_case( + [0x59, 0x87, 0x2c, 0xc4, 0x34, 0x24, 0x80, 0x20, 0x47, 0xf5, 0xc6, 0xda, 0x00, 0x9d, 0xad, 0xc6, 0x48, 0x74, 0x74, 0x10, 0xf0, 0xc7, 0x70, 0x92, 0x7b, 0xe3, 0x9a, 0x1e, 0x47, 0x29, 0x76, 0xe1] +)] +#[test_case( + [0xa6, 0xf3, 0x83, 0x53, 0x08, 0x5f, 0x48, 0xaa, 0x67, 0x65, 0x24, 0xdc, 0x50, 0x50, 0x20, 0x76, 0x2c, 0x14, 0xc6, 0x11, 0x2e, 0xd2, 0x94, 0x87, 0xcf, 0x0e, 0x23, 0x3b, 0x32, 0xc5, 0xc2, 0x88] +)] +#[test_case( + [0x66, 0x61, 0x64, 0x78, 0xd5, 0xa0, 0xad, 0xeb, 0x87, 0x0a, 0x9e, 0x88, 0xb9, 0x1e, 0xe4, 0x77, 0xb1, 0x76, 0x81, 0x63, 0xd8, 0xea, 0x8d, 0x4c, 0x7e, 0x54, 0x33, 0xd4, 0x07, 0xf8, 0x78, 0x50] +)] +#[test_case( + [0x70, 0x8e, 0x06, 0xc5, 0xdf, 0xbf, 0x31, 0x86, 0xf1, 0x25, 0xa4, 0xb2, 0x78, 0x8a, 0x96, 0x61, 0x6f, 0x76, 0xa6, 0x1f, 0xa7, 0x92, 0x5b, 0xec, 0xd0, 0xab, 0xa7, 0xd1, 0xde, 0x77, 0xe0, 0xd7] +)] +#[test_case( + [0x63, 0x76, 0x07, 0xf0, 0xe1, 0x22, 0xde, 0xca, 0x26, 0x3d, 0x6a, 0xba, 0x24, 0xd2, 0x5d, 0x72, 0xc0, 0x1c, 0x52, 0x1b, 0x52, 0x2c, 0x2b, 0xfb, 0x38, 0x9a, 0x7c, 0xac, 0xd6, 0x47, 0xcd, 0x30] +)] +#[test_case( + [0xb3, 0xd6, 0x11, 0x5a, 0x46, 0xcd, 0x0b, 0x52, 0xd8, 0xac, 0xe6, 0xb4, 0x21, 0x3f, 0x1a, 0x1b, 0x52, 0x38, 0xfd, 0x51, 0x01, 0x0a, 0x11, 0x84, 0x5e, 0xb2, 0x03, 0xdb, 0xf4, 0xad, 0x2c, 0x47] +)] +#[test_case( + [0xcd, 0x46, 0xba, 0x18, 0x6b, 0x98, 0xe0, 0x47, 0xff, 0x70, 0x8c, 0xf3, 0xf4, 0x3e, 0x55, 0x25, 0x63, 0x2b, 0x62, 0x47, 0x76, 0xd5, 0xdb, 0xe6, 0xf2, 0xa0, 0x01, 0x13, 0x15, 0x5e, 0x3b, 0xb3] +)] +#[test_case( + [0x98, 0xa9, 0x27, 0x46, 0xc5, 0x6e, 0x65, 0x9b, 0x12, 0x0b, 0x0a, 0x23, 0xed, 0x39, 0x59, 0x33, 0x70, 0x8e, 0x12, 0xd5, 0x89, 0x8f, 0x10, 0x25, 0xb3, 0x8e, 0xb5, 0xfb, 0x03, 0xf2, 0x2d, 0x8e] +)] +#[test_case( + [0x03, 0x9c, 0xe1, 0x34, 0x24, 0x43, 0x6f, 0xd6, 0xf1, 0xe5, 0xb8, 0x98, 0x2a, 0x8a, 0xea, 0xb0, 0x74, 0xf4, 0xeb, 0x5e, 0xfa, 0x05, 0x5f, 0x8c, 0x1f, 0x1b, 0xf9, 0xef, 0x20, 0xe9, 0x90, 0xbd] +)] +#[test_case( + [0x64, 0xd3, 0x87, 0xc1, 0x6e, 0x52, 0x10, 0xe3, 0xe4, 0x8a, 0x7f, 0x07, 0x5d, 0x70, 0xd9, 0x2d, 0x19, 0xe9, 0xcc, 0x94, 0x66, 0x7a, 0x7f, 0x6a, 0x95, 0x36, 0xd0, 0xd9, 0x4c, 0x5b, 0xc4, 0xd7] +)] +#[test_case( + [0x96, 0x74, 0x61, 0x31, 0xcf, 0xcc, 0x2d, 0xcc, 0x27, 0xd0, 0x46, 0xc2, 0x46, 0x2d, 0x10, 0xa7, 0xa4, 0xb8, 0x1f, 0x5b, 0xe6, 0xc9, 0xd5, 0xc7, 0x69, 0x1f, 0xad, 0x1f, 0x34, 0x89, 0x05, 0xee] +)] +#[test_case( + [0x7f, 0x23, 0x8a, 0x24, 0x2f, 0xf8, 0xbe, 0x73, 0xfb, 0xd4, 0x68, 0x5e, 0x36, 0xe7, 0x64, 0xd4, 0xf0, 0x25, 0x7a, 0xb8, 0x47, 0x6e, 0x51, 0x13, 0x18, 0xa5, 0x07, 0xc9, 0x21, 0x2c, 0xb1, 0x73] +)] +#[test_case( + [0x34, 0x67, 0x08, 0xf1, 0x00, 0x8c, 0xe1, 0x71, 0x7f, 0x00, 0x88, 0x08, 0xb8, 0xd3, 0xff, 0xb2, 0x15, 0x1d, 0xf3, 0xc2, 0xbb, 0x45, 0x2d, 0x63, 0x34, 0xda, 0x21, 0x90, 0xdd, 0xd6, 0xfb, 0x91] +)] +#[test_case( + [0xe2, 0x70, 0xb9, 0x4f, 0xfc, 0xda, 0x54, 0x73, 0xd3, 0x9b, 0xf7, 0x23, 0xb1, 0xc3, 0x83, 0xf1, 0xe8, 0x01, 0xe8, 0xf7, 0x57, 0xb0, 0x9d, 0xf3, 0x27, 0xb5, 0x8b, 0xb6, 0x95, 0x3d, 0x78, 0xa8] +)] +#[test_case( + [0x25, 0x79, 0x24, 0x89, 0x2e, 0x15, 0x34, 0x5c, 0xe7, 0xfa, 0x78, 0x15, 0x68, 0xf8, 0x23, 0x3d, 0x1d, 0x4e, 0xb8, 0x7c, 0xaf, 0xa8, 0x75, 0x04, 0x49, 0xaf, 0xd0, 0x39, 0x77, 0x7b, 0xbe, 0xac] +)] +#[test_case( + [0x6b, 0x3b, 0x0a, 0x09, 0x43, 0x4d, 0x23, 0x0d, 0x4c, 0x6f, 0x93, 0xba, 0xe3, 0x02, 0xd7, 0x1b, 0xcc, 0xa5, 0x9e, 0xbb, 0x27, 0xb6, 0xa9, 0x66, 0xb3, 0x8f, 0x49, 0x06, 0x73, 0xbe, 0x79, 0xf1] +)] +#[test_case( + [0x49, 0xaf, 0x83, 0x00, 0x60, 0x19, 0x13, 0x24, 0xea, 0x98, 0x1b, 0x1a, 0xf5, 0x84, 0x72, 0x02, 0xd3, 0x0f, 0x28, 0x80, 0xbd, 0xa0, 0x9d, 0x33, 0xc4, 0x49, 0xa2, 0xf5, 0x7b, 0xca, 0xe1, 0xfe] +)] +#[test_case( + [0xd0, 0x38, 0x1e, 0xd6, 0xae, 0xd8, 0x85, 0xe2, 0x2d, 0x22, 0xdc, 0x10, 0x5e, 0x89, 0xc9, 0xc7, 0xc7, 0xba, 0x91, 0x7f, 0x98, 0xfe, 0x05, 0x59, 0xf0, 0xb6, 0x2e, 0xed, 0x24, 0xc7, 0xf5, 0x58] +)] +#[test_case( + [0x6b, 0xe4, 0xe5, 0x7d, 0x54, 0xf0, 0x48, 0xe0, 0x3f, 0x7e, 0xe5, 0x16, 0x91, 0x5d, 0x1c, 0xa2, 0x04, 0x4c, 0x08, 0x85, 0xf3, 0xe2, 0x50, 0x02, 0x73, 0x85, 0x65, 0x79, 0xde, 0x86, 0x5c, 0x75] +)] +#[test_case( + [0x20, 0x24, 0x92, 0x76, 0xe9, 0x41, 0x79, 0x08, 0x75, 0x82, 0xcd, 0xe9, 0x15, 0x76, 0xa0, 0xba, 0x2a, 0x8d, 0x69, 0x9f, 0xca, 0xa3, 0xc5, 0xa6, 0x8a, 0xf6, 0xcd, 0xdb, 0xbe, 0x90, 0x6b, 0x17] +)] +#[test_case( + [0x98, 0x1f, 0x5b, 0x9d, 0x34, 0x7e, 0x79, 0xe6, 0x71, 0xe6, 0x25, 0xe8, 0xb1, 0xe2, 0xdc, 0x27, 0xa3, 0x90, 0x43, 0x14, 0xe3, 0xe5, 0x5e, 0x58, 0x7b, 0x8f, 0xab, 0x9f, 0x9c, 0x94, 0x03, 0x1f] +)] +#[test_case( + [0x8e, 0x63, 0x3a, 0x31, 0xc5, 0x6d, 0x22, 0x8b, 0x4d, 0x55, 0xda, 0xbd, 0x4e, 0x2a, 0x9b, 0xae, 0xf6, 0x12, 0x4c, 0xf6, 0x56, 0x3d, 0xc8, 0x76, 0xb6, 0x33, 0x72, 0x48, 0x9a, 0x30, 0xfc, 0x3f] +)] +#[test_case( + [0x41, 0xe0, 0x5f, 0x70, 0xa2, 0x15, 0x83, 0x7a, 0x69, 0x2a, 0x8e, 0x18, 0x5f, 0x7a, 0x99, 0xe5, 0x86, 0x21, 0x51, 0xbd, 0xe7, 0xe4, 0xf4, 0x72, 0xfa, 0x8b, 0xf8, 0x54, 0x5e, 0xf5, 0x85, 0xd7] +)] +#[test_case( + [0x76, 0x67, 0x10, 0xb5, 0x92, 0xe8, 0x2f, 0xd1, 0xa8, 0x96, 0x8b, 0xb9, 0x13, 0x0f, 0x50, 0xe3, 0xda, 0xfa, 0xeb, 0x12, 0xce, 0xa4, 0x13, 0xe4, 0x5e, 0x31, 0xcd, 0x0c, 0x55, 0x08, 0xd4, 0x4e] +)] +#[test_case( + [0x94, 0xb4, 0xbd, 0xbb, 0xcd, 0x3d, 0x9d, 0x7e, 0x3b, 0x90, 0x2c, 0x9d, 0x02, 0x73, 0xf8, 0x7a, 0x84, 0x51, 0x0e, 0x52, 0xa5, 0x8c, 0x75, 0xfe, 0xce, 0xf5, 0x00, 0x0d, 0xf5, 0x4c, 0x91, 0x85] +)] +#[test_case( + [0x45, 0xe8, 0xf2, 0x5a, 0xfe, 0xf6, 0xfd, 0x7a, 0x2f, 0xf7, 0xcf, 0x6b, 0x05, 0x8b, 0x2d, 0xf9, 0x03, 0x5c, 0x76, 0x7a, 0x16, 0x1b, 0x55, 0x06, 0x39, 0x22, 0xdd, 0xc7, 0xa9, 0x55, 0xf7, 0x24] +)] +#[test_case( + [0xa2, 0xc0, 0xdd, 0xe2, 0x1a, 0x63, 0xd8, 0xe7, 0x57, 0xa9, 0x98, 0x51, 0xd8, 0x79, 0xf6, 0xe2, 0xe5, 0x82, 0x60, 0x7b, 0xd2, 0x08, 0x80, 0xef, 0x64, 0xc8, 0x31, 0xc9, 0xa7, 0xce, 0x88, 0x00] +)] +#[test_case( + [0xaa, 0x0a, 0x4e, 0xa0, 0xab, 0x4c, 0x0e, 0xbf, 0x66, 0x39, 0x9b, 0x36, 0xdc, 0xc1, 0x75, 0x9c, 0x0f, 0x00, 0x31, 0xb5, 0x45, 0xc5, 0x1d, 0xdc, 0x38, 0x45, 0x76, 0x53, 0x31, 0x07, 0x99, 0xa4] +)] +#[test_case( + [0x4d, 0x0f, 0x2c, 0xf2, 0xd1, 0xfc, 0x52, 0x49, 0xc5, 0x21, 0xaa, 0xbf, 0xbf, 0x91, 0xb9, 0x13, 0xd1, 0xfb, 0x42, 0x19, 0x86, 0x0a, 0x35, 0x5e, 0x3a, 0x3a, 0xee, 0x76, 0xd9, 0x2d, 0x6d, 0xf9] +)] +#[test_case( + [0xa2, 0xc3, 0xb5, 0xac, 0x07, 0xbd, 0x2f, 0x74, 0xf7, 0x98, 0x7e, 0x00, 0xe2, 0xaf, 0x52, 0x4f, 0x6a, 0x95, 0x07, 0xd4, 0x14, 0x93, 0x16, 0x87, 0xf6, 0xca, 0x42, 0x34, 0xe3, 0x7d, 0xf3, 0x2c] +)] +#[test_case( + [0x99, 0x32, 0x74, 0x19, 0x77, 0x0f, 0x9b, 0x3d, 0x5d, 0x19, 0xce, 0xad, 0xcc, 0x06, 0xa5, 0x1d, 0x08, 0xe2, 0x86, 0x30, 0x4b, 0x61, 0xd5, 0x08, 0xcc, 0x36, 0xbc, 0x2e, 0x23, 0x5b, 0xf3, 0x05] +)] +#[test_case( + [0x34, 0x7b, 0x86, 0xec, 0xe0, 0x00, 0x89, 0x2a, 0x2d, 0x84, 0x5b, 0x2b, 0x36, 0x82, 0x21, 0x63, 0x8a, 0x2a, 0x04, 0x82, 0x1d, 0x03, 0x2c, 0xe3, 0xef, 0xbc, 0xf7, 0xbe, 0x57, 0x44, 0x79, 0x59] +)] +#[test_case( + [0x3a, 0xb4, 0x53, 0xb4, 0xf9, 0x53, 0xe1, 0x50, 0xaa, 0xb1, 0x57, 0xdd, 0x64, 0xd7, 0x85, 0x77, 0x9e, 0xeb, 0xe6, 0x00, 0xb8, 0x7f, 0xb6, 0xf8, 0xe4, 0x62, 0x1f, 0x41, 0x94, 0x41, 0x73, 0x57] +)] +#[test_case( + [0xfc, 0x0c, 0xe9, 0xb2, 0xec, 0xb2, 0x50, 0xfb, 0xb9, 0x34, 0xbc, 0x5a, 0x74, 0x98, 0xe4, 0xc7, 0x62, 0x8d, 0x6f, 0x1f, 0x3a, 0x56, 0x69, 0x45, 0x47, 0x96, 0xbe, 0x15, 0xf8, 0x56, 0x31, 0x4e] +)] +#[test_case( + [0xab, 0x5e, 0x49, 0x8f, 0xd0, 0xf7, 0x2c, 0xd8, 0x54, 0xed, 0xe6, 0x27, 0x20, 0x4c, 0x23, 0xd6, 0x39, 0xa1, 0x4a, 0x71, 0x4b, 0x35, 0xe7, 0xa8, 0x09, 0x1e, 0x0f, 0x04, 0xa4, 0xd7, 0x49, 0xb2] +)] +#[test_case( + [0x62, 0x52, 0xa3, 0xde, 0xa6, 0x05, 0x54, 0x85, 0x65, 0xb6, 0x83, 0x8f, 0x85, 0x38, 0xee, 0xab, 0x9c, 0x8b, 0x66, 0x64, 0x90, 0x05, 0xc0, 0x17, 0x95, 0x9d, 0x0d, 0x2d, 0x20, 0xec, 0x2a, 0xa0] +)] +#[test_case( + [0xbd, 0xd9, 0x27, 0xb4, 0x6b, 0x96, 0x7b, 0xc9, 0x3a, 0xc4, 0x61, 0x41, 0x8d, 0x5e, 0x66, 0xad, 0xf2, 0xde, 0x77, 0x58, 0x95, 0x42, 0x57, 0x45, 0x5b, 0x16, 0x5e, 0x40, 0x5f, 0x40, 0x25, 0xc9] +)] +#[test_case( + [0x7f, 0x6f, 0x8b, 0xde, 0xf8, 0x24, 0x5a, 0x28, 0x10, 0x50, 0x4d, 0xa9, 0x8c, 0xe0, 0x59, 0x64, 0x5c, 0xa3, 0xa6, 0x27, 0x17, 0x79, 0x8e, 0x5c, 0x13, 0x5f, 0xbb, 0x5c, 0x13, 0x9a, 0x55, 0x59] +)] +#[test_case( + [0x30, 0x65, 0x95, 0xba, 0xf3, 0xbc, 0x3a, 0x34, 0x1a, 0xb9, 0x42, 0xf6, 0x00, 0x94, 0xd9, 0x1e, 0xc5, 0x51, 0x4b, 0x1c, 0x53, 0x5a, 0x33, 0xca, 0x77, 0x03, 0x93, 0x12, 0x39, 0x99, 0x3c, 0x45] +)] +#[test_case( + [0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] +)] +#[test_case( + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +)] +#[test_case( + [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +)] +#[test_case( + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] +)] +#[test_case( + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe] +)] +fn decompress_collection_id_fails_on_invalid_collection_id(collection_id: CombinatorialId) { + let actual = decompress_collection_id(collection_id); + assert_eq!(actual, Err(CollectionIdError::InvalidParentCollectionId)); +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/decompress_hash.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/decompress_hash.rs new file mode 100644 index 000000000..ed5c4220f --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/decompress_hash.rs @@ -0,0 +1,774 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use rstest::rstest; + +#[rstest] +#[case( + [0x83, 0x15, 0x48, 0x88, 0xe8, 0x2c, 0xe4, 0xfc, 0x32, 0xc2, 0xd5, 0xcd, 0x76, 0x6f, 0xfd, 0xc1, 0x8a, 0x8b, 0x00, 0xd9, 0xb7, 0x18, 0x15, 0xc7, 0x2c, 0x52, 0x38, 0x91, 0x11, 0x4e, 0x19, 0xca], + ( + "0x224caba325c9a4a8c2224a60736d4d065b882bb6e63480acb411206360541f3f", + "0x2f69f0116b9c27402783c23a83890dd2e8d11598c875b4562ac61f8716674497", + ) +)] +#[case( + [0x64, 0xf3, 0x41, 0x72, 0x9b, 0xea, 0x43, 0x90, 0x10, 0x1d, 0x0b, 0x1a, 0xcc, 0x67, 0x47, 0xbc, 0x0d, 0x8d, 0x1a, 0xc5, 0x9f, 0xf0, 0xb3, 0x2f, 0xe9, 0x91, 0x94, 0x93, 0x8b, 0x70, 0xa4, 0xda], + ( + "0x42aa48cd987033c9f7c7fadc9649700de8a45a2cf0d1e1571507c65da76aa4d", + "0x17b8beb8dff7a68a74f5d5a59be03a403b01e1b52181bc317b2f85ecf288c160", + ) +)] +#[case( + [0x1f, 0x79, 0x5c, 0xdd, 0x77, 0xab, 0x5e, 0xe4, 0x48, 0x50, 0x24, 0x5e, 0x72, 0xbc, 0x94, 0x80, 0xe0, 0x0c, 0xca, 0x47, 0x6f, 0x83, 0xd4, 0x2b, 0x0f, 0xf0, 0xce, 0x60, 0x28, 0xdf, 0x4b, 0x73], + ( + "0x1f795cdd77ab5ee44850245e72bc9480e00cca476f83d42b0ff0ce6028df4b74", + "0x1611f17dcc316d337dbadfdd9ff708a1574dcafa7464d8192cb83f7dc345f630", + ) +)] +#[case( + [0x11, 0x7b, 0x7c, 0x6c, 0xab, 0x0d, 0x78, 0x37, 0xfe, 0x68, 0x5c, 0x3d, 0x1c, 0x83, 0x83, 0x31, 0x26, 0xb8, 0xe3, 0xa5, 0xcf, 0x2f, 0x10, 0x2d, 0x4f, 0x63, 0xf0, 0x2c, 0x34, 0x68, 0x9d, 0xa4], + ( + "0x117b7c6cab0d7837fe685c3d1c83833126b8e3a5cf2f102d4f63f02c34689da5", + "0x1d20ad42a133eb646a7785bf576b2fdc687c04c63a20de5e70c29207e27dd6e0", + ) +)] +#[case( + [0x71, 0xfb, 0x5a, 0x00, 0x74, 0xc4, 0xfd, 0xf8, 0xff, 0x2a, 0x59, 0x75, 0x0c, 0xb7, 0x25, 0x7c, 0x60, 0x6b, 0x5d, 0x09, 0x93, 0xf0, 0xe7, 0x9c, 0x33, 0x08, 0x84, 0x72, 0xbc, 0x98, 0xb0, 0xf2], + ( + "0x1132bd1ab261bda58e89ce0809b474c1316887e6c30d5281bac76c450b9eb666", + "0x678e222f0d626c3e4298406b8bfbc8fd3f41d6afe467704eb1ac07664fe262", + ) +)] +#[case( + [0x81, 0x8d, 0xe3, 0xde, 0x50, 0x1f, 0xff, 0xa2, 0xe2, 0x1c, 0x26, 0xcf, 0xc1, 0xb1, 0x02, 0x1d, 0x79, 0x13, 0xf7, 0x02, 0x91, 0x0e, 0xcf, 0xfb, 0xcd, 0x5d, 0x2b, 0x06, 0x55, 0xff, 0x62, 0x45], + ( + "0x20c546f88dbcbf4f717b9b62beae51624a1121dfc02b3ae1551c12d8a50567b9", + "0xcdda89c6b78b99bfba909bf8862cde968465cef38fda4e7df6ab617680adfaf", + ) +)] +#[case( + [0xed, 0x84, 0xb8, 0xdd, 0xca, 0x0c, 0x1b, 0x21, 0x42, 0x48, 0x4f, 0x42, 0x0e, 0x05, 0xba, 0x7b, 0x19, 0xa7, 0x91, 0xc4, 0xd9, 0x17, 0x5f, 0x4f, 0x49, 0xf7, 0x83, 0x6f, 0xf1, 0xfa, 0xba, 0xae], + ( + "0x2bf37f1245459a7a6107386808005904bba1e77f3750351a597553149006c593", + "0x19386875ee1078b2ede3be6a02f6482507bed0058b655bce614bc1abb408ddf1", + ) +)] +#[case( + [0xef, 0xa9, 0x42, 0xf4, 0xd0, 0x1f, 0x3a, 0x4d, 0xdd, 0x1e, 0x5c, 0xb6, 0xe8, 0x2a, 0xd8, 0xe7, 0x04, 0xa8, 0x59, 0x2c, 0xf0, 0xe0, 0xb8, 0x46, 0xc3, 0x6d, 0xc9, 0xbd, 0x05, 0x36, 0x9c, 0x46], + ( + "0x2e1809294b58b9a6fbdd45dce2257770a6a2aee74f198e11d2eb9961a342a72e", + "0xb5f465aedf5ad33337e665db38c12018472fa89c7e112bfe874d25f9fb9c13d", + ) +)] +#[case( + [0x19, 0x89, 0xc5, 0xce, 0xac, 0x1a, 0x65, 0xfa, 0x79, 0x22, 0x96, 0xed, 0x44, 0x9a, 0xe7, 0x9e, 0x54, 0x0b, 0x00, 0xd6, 0xa0, 0x0a, 0xd4, 0x46, 0x03, 0x5c, 0x87, 0xc7, 0x5e, 0xa6, 0x63, 0x36], + ( + "0x1989c5ceac1a65fa792296ed449ae79e540b00d6a00ad446035c87c75ea6633a", + "0x6589b2b453dbaed452e90d4a5ec85fbd3641e707f42e0ac3c8d5d88ffa62bea", + ) +)] +#[case( + [0xa5, 0xd8, 0x66, 0xaf, 0xcc, 0xbb, 0x43, 0xcc, 0xe8, 0xe0, 0x18, 0x76, 0xf3, 0x76, 0x2e, 0x4b, 0x32, 0xee, 0xf0, 0xee, 0xf1, 0xf0, 0x85, 0xaf, 0xed, 0xbc, 0xf8, 0x75, 0x88, 0x06, 0x12, 0x93], + ( + "0x14ab7b572926634fbfef47536ef225326c6ab13ab89b2608395b5430fe8f1ac0", + "0x2d760276c0c075b65b46f092b3ee5f0ea63a44bdda2d606acd903d62f4cd3379", + ) +)] +#[case( + [0x7b, 0xa3, 0x02, 0xd4, 0xc7, 0x01, 0xaa, 0xb6, 0xb4, 0x85, 0x28, 0x0b, 0x25, 0x03, 0x73, 0x55, 0x1a, 0xf2, 0xeb, 0x83, 0xa9, 0x8b, 0xe0, 0x04, 0x8f, 0xae, 0xb2, 0xdc, 0x20, 0x23, 0x50, 0xe4], + ( + "0x1ada65ef049e6a6343e49c9e2200c299ebf01660d8a84aea176d9aae6f295657", + "0x2d4d79805f35ec44f628b7cc6030017d6b3f238eae07612b074e42e32369ed34", + ) +)] +#[case( + [0xfd, 0xc6, 0x8c, 0xe3, 0x40, 0xf3, 0x04, 0x4b, 0x52, 0x6b, 0x8b, 0x6b, 0x1d, 0xff, 0x6f, 0x14, 0x3d, 0x4c, 0x04, 0x68, 0x65, 0x53, 0x3a, 0x59, 0x59, 0xbc, 0xb8, 0x28, 0xe9, 0x66, 0x07, 0xad], + ( + "0xbd104a4dafae37ab8da2eda9678b54047c4ef915b1a45972d19fbb6aef5154c", + "0xdffe17159cc7adba911c9aa514b86792832f6fa69d062371154ead6b152375b", + ) +)] +#[case( + [0x93, 0x0c, 0xf8, 0x88, 0x63, 0xc7, 0x0c, 0x66, 0x28, 0x5f, 0x4b, 0x96, 0x17, 0x10, 0x32, 0x65, 0x3d, 0x91, 0xc5, 0x0e, 0xcf, 0xda, 0x23, 0xf1, 0x82, 0x7e, 0x6a, 0x9b, 0x16, 0xb1, 0x50, 0x95], + ( + "0x1e00d2fc0322be8ff6e7a72928c294c770d855a9684c449ce1cc6568d3a58c1", + "0x241d3fe18e6b4bea53f4c1dd19254f35c2591d5e4a076305ccd1327289745b7b", + ) +)] +#[case( + [0x72, 0x7a, 0xf5, 0x5e, 0x17, 0x46, 0xa7, 0x00, 0xd1, 0xde, 0x3e, 0x03, 0x99, 0x92, 0x91, 0x20, 0xdd, 0xf7, 0xae, 0xff, 0xb3, 0x2d, 0xd9, 0x53, 0x18, 0xdc, 0xf5, 0x4d, 0x39, 0x44, 0xa3, 0xd8], + ( + "0x11b2587854e366ad613db296968fe065aef4d9dce24a4438a09bdd1f884aa94b", + "0x89d696be6968733b6012365a3f1823dc24ee9c391b2ec04cea60fa8f30be578", + ) +)] +#[case( + [0xd5, 0x7f, 0xa0, 0x9a, 0x3f, 0xa7, 0xaf, 0xb2, 0x1c, 0x94, 0xb0, 0x3b, 0x06, 0x65, 0xd3, 0x59, 0x5f, 0xa8, 0x48, 0x18, 0x7e, 0x68, 0xe2, 0xbc, 0x01, 0x0b, 0xfc, 0x16, 0xb1, 0x65, 0x55, 0x63], + ( + "0x13ee66cebae12f0b3b539961006071e301a29dd2dca1b8871089cbbb4f716048", + "0x84cb34653fc5497967aedb393171a776bc6ddbd1b92a36c08ff190841b3bc97", + ) +)] +#[case( + [0x1a, 0x04, 0x7d, 0x43, 0x00, 0xd3, 0x6f, 0xb3, 0xea, 0xef, 0x0b, 0x27, 0x71, 0xf4, 0x54, 0x02, 0xf4, 0x05, 0xd9, 0x90, 0x84, 0x08, 0x7a, 0xd3, 0xd9, 0x59, 0xfb, 0x0d, 0x3f, 0x4d, 0x7d, 0xf4], + ( + "0x1a047d4300d36fb3eaef0b2771f45402f405d99084087ad3d959fb0d3f4d7df5", + "0x11b24a741b48731ad5dbda58c09d662b1fc1335952c5b72b9c70f14e26399980", + ) +)] +#[case( + [0xbc, 0x15, 0xb3, 0x40, 0x0d, 0xe4, 0x0a, 0xd4, 0x96, 0x68, 0x98, 0x6a, 0xca, 0xb4, 0xf2, 0xa6, 0x2b, 0x5c, 0x8e, 0x18, 0x3e, 0x22, 0xd1, 0xa1, 0xe3, 0x52, 0xa8, 0x86, 0xc6, 0x56, 0xc2, 0xa9], + ( + "0x2ae8c7e76a4f2a576d77c7474630e98d64d84e6404cd71fa2ef104423cdfcad6", + "0x46d69a8b490311d5970dfeea57c56280cfa9d1c01cc65e309d5d7818e895a2d", + ) +)] +#[case( + [0xaf, 0xff, 0x49, 0xfa, 0xa4, 0xfb, 0x00, 0x56, 0x31, 0xab, 0x38, 0xf3, 0x9d, 0x57, 0x07, 0xd2, 0xba, 0xe3, 0x5c, 0x2a, 0xb9, 0xb5, 0xa5, 0x06, 0x51, 0x32, 0x08, 0x73, 0xf7, 0x99, 0xa4, 0xa9], + ( + "0x1ed25ea201661fd908ba67d018d2feb9f45f1c768060455e9cd0642f6e22acd5", + "0x2cfa7234f94faf07f1369e27ebda4de11e4ea091e6a8f7773fb0ad205394bcb5", + ) +)] +#[case( + [0xf8, 0x01, 0x51, 0x24, 0xd3, 0x4f, 0x6f, 0xc4, 0x7f, 0xbb, 0x2b, 0xd4, 0x48, 0x8b, 0x20, 0x16, 0x48, 0xdd, 0xbf, 0x0a, 0x75, 0xa2, 0x4e, 0x38, 0xa3, 0x26, 0xba, 0x74, 0xff, 0x76, 0x45, 0xab], + ( + "0x60bc8e66d574ef3e629cf43c10466425356aa336b6959767683fe02c5055349", + "0x279d88dc4e3488f950e3d12ed82d45a585d1ab9310a23549d3a9525b78494813", + ) +)] +#[case( + [0xc3, 0x2f, 0xfb, 0xd6, 0x6a, 0xa2, 0x28, 0x56, 0x30, 0x80, 0x73, 0x41, 0x2e, 0xc5, 0xd1, 0x54, 0xa8, 0xfa, 0x86, 0x82, 0x5d, 0x77, 0x71, 0xd2, 0xbf, 0xad, 0x37, 0x37, 0x27, 0x75, 0xa6, 0x97], + ( + "0x19ec20ae5dba7af4f3f5c6728c06fde4af4dc3cbbb0479dcf2b06dbc581b17c", + "0x1d42f564a2450a7e3616618d3550f0b00262a2d3b28187f27c8ef55e9dda5f7d", + ) +)] +#[case( + [0x2d, 0x45, 0xd3, 0x9e, 0x93, 0x38, 0xb1, 0xfb, 0xc4, 0x49, 0x03, 0xa5, 0x2b, 0x3a, 0x17, 0x7a, 0x2a, 0xad, 0x6b, 0xac, 0x0b, 0x68, 0x2c, 0x8f, 0x64, 0xf3, 0x2e, 0xba, 0xd4, 0x04, 0x4d, 0x0b], + ( + "0x2d45d39e9338b1fbc44903a52b3a177a2aad6bac0b682c8f64f32ebad4044d0c", + "0x27c51c1b8f126d3b987027f2a04d8cb26f73db03c8cabe314e3c11c5091945da", + ) +)] +#[case( + [0x59, 0x87, 0x2c, 0xc4, 0x34, 0x24, 0x80, 0x20, 0x47, 0xf5, 0xc6, 0xda, 0x00, 0x9d, 0xad, 0xc6, 0x48, 0x74, 0x74, 0x10, 0xf0, 0xc7, 0x70, 0x92, 0x7b, 0xe3, 0x9a, 0x1e, 0x47, 0x29, 0x76, 0xe1], + ( + "0x2922de5152f2dff68fa581237f1c5568b0f3097f8855a6053fc30e076eac799b", + "0x2aad24a3280997c33422d9081f12ebde0440122f17060ae655336c52c13228e8", + ) +)] +#[case( + [0xa6, 0xf3, 0x83, 0x53, 0x08, 0x5f, 0x48, 0xaa, 0x67, 0x65, 0x24, 0xdc, 0x50, 0x50, 0x20, 0x76, 0x2c, 0x14, 0xc6, 0x11, 0x2e, 0xd2, 0x94, 0x87, 0xcf, 0x0e, 0x23, 0x3b, 0x32, 0xc5, 0xc2, 0x88], + ( + "0x15c697fa64ca682d3e7453b8cbcc175d6590865cf57d34e01aac7ef6a94ecab5", + "0xece650efa6538a1b3d8c1e2a6ccc7e220934121758861c8d62bb48bdf5733e3", + ) +)] +#[case( + [0x66, 0x61, 0x64, 0x78, 0xd5, 0xa0, 0xad, 0xeb, 0x87, 0x0a, 0x9e, 0x88, 0xb9, 0x1e, 0xe4, 0x77, 0xb1, 0x76, 0x81, 0x63, 0xd8, 0xea, 0x8d, 0x4c, 0x7e, 0x54, 0x33, 0xd4, 0x07, 0xf8, 0x78, 0x50], + ( + "0x598c793133d6d98166a131bb61c33bc8273ac410806f83206131ba656fe7dcb", + "0x1790e75e16e2a035609d5344f412fad57aa07013913a5dc784c83ea863f3178", + ) +)] +#[case( + [0x70, 0x8e, 0x06, 0xc5, 0xdf, 0xbf, 0x31, 0x86, 0xf1, 0x25, 0xa4, 0xb2, 0x78, 0x8a, 0x96, 0x61, 0x6f, 0x76, 0xa6, 0x1f, 0xa7, 0x92, 0x5b, 0xec, 0xd0, 0xab, 0xa7, 0xd1, 0xde, 0x77, 0xe0, 0xd7], + ( + "0xfc569e01d5bf133808519457587e5a64073d0fcd6aec6d2586a8fa42d7de64c", + "0x139ed7620d16f3eced5b0b19edfdc8a77d1978caf2a208013b0f5dc0066b8c8e", + ) +)] +#[case( + [0x63, 0x76, 0x07, 0xf0, 0xe1, 0x22, 0xde, 0xca, 0x26, 0x3d, 0x6a, 0xba, 0x24, 0xd2, 0x5d, 0x72, 0xc0, 0x1c, 0x52, 0x1b, 0x52, 0x2c, 0x2b, 0xfb, 0x38, 0x9a, 0x7c, 0xac, 0xd6, 0x47, 0xcd, 0x30], + ( + "0x2ad6b0b1ebf9e76b59cdf4d21cfacb791197cf8814896e0c059647f254dd2a6", + "0xdc8061c2098b8c7f33477d0f9d79689ced5cdd55bd4c3bb55e90541bbcc0b48", + ) +)] +#[case( + [0x9f, 0x49, 0x14, 0x31, 0xc7, 0xdf, 0x9a, 0xbc, 0x2a, 0x1a, 0xf6, 0xd1, 0x22, 0x5f, 0x48, 0x78, 0x91, 0xd0, 0xad, 0xd2, 0x93, 0xf7, 0x2f, 0xb5, 0x3c, 0xc5, 0x89, 0x92, 0xb6, 0xb6, 0x85, 0x9f], + ( + "0xe1c28d9244aba3f012a25ad9ddb3f5fcb4c6e1e5aa1d00d8863e54e2d3f8dcb", + "0x243a9cce52bfaa26c0ff6861e3476c69e0c8eae62510c17e66b6090ac515b1e3", + ) +)] +#[case( + [0xb3, 0xd6, 0x11, 0x5a, 0x46, 0xcd, 0x0b, 0x52, 0xd8, 0xac, 0xe6, 0xb4, 0x21, 0x3f, 0x1a, 0x1b, 0x52, 0x38, 0xfd, 0x51, 0x01, 0x0a, 0x11, 0x84, 0x5e, 0xb2, 0x03, 0xdb, 0xf4, 0xad, 0x2c, 0x47], + ( + "0x22a92601a3382ad5afbc15909cbb11028bb4bd9cc7b4b1dcaa505f976b363473", + "0x2468cc28326c4102230355ae75b7ad9b51a8ef8ae897caf77ebe0629c18492b", + ) +)] +#[case( + [0xe8, 0x66, 0x29, 0x25, 0x84, 0x95, 0x25, 0x7c, 0xaa, 0x29, 0x57, 0x87, 0xf2, 0x50, 0xff, 0x30, 0x14, 0xf9, 0xa8, 0x61, 0x91, 0xd2, 0xfa, 0xe7, 0x4f, 0x57, 0x03, 0xd9, 0xaf, 0x25, 0xb8, 0x87], + ( + "0x26d4ef59ffcea4d5c8e840adec4b9db9b6f3fe1bf00bd0b25ed4d37e4d31c36d", + "0x4da2d54b9dc4f07956051d20389062d5e9b25ed56bd3a68b9066f9543dec88d", + ) +)] +#[case( + [0x63, 0xc1, 0x52, 0x80, 0x1c, 0xf9, 0x6e, 0x49, 0x82, 0x91, 0xb5, 0x86, 0x7e, 0x10, 0xe2, 0x98, 0x2a, 0xe8, 0x40, 0x83, 0x19, 0xfe, 0xee, 0xbe, 0x94, 0x26, 0x64, 0x8f, 0x36, 0xf1, 0xd5, 0x07], + ( + "0x2f8b59a5a962df611f12a197b0e31dcfbe56b60491b59a41be54c6185f7da7b", + "0x22d0275a80ca95052995a482a0c80b5e75b648f4d2db98360e06ede66ae8668e", + ) +)] +#[case( + [0xcd, 0x46, 0xba, 0x18, 0x6b, 0x98, 0xe0, 0x47, 0xff, 0x70, 0x8c, 0xf3, 0xf4, 0x3e, 0x55, 0x25, 0x63, 0x2b, 0x62, 0x47, 0x76, 0xd5, 0xdb, 0xe6, 0xf2, 0xa0, 0x01, 0x13, 0x15, 0x5e, 0x3b, 0xb3], + ( + "0xbb5804ce6d25fa11e2f7619ee38f3af0525b801d50eb1b2021dd0b7b36a4699", + "0x1d2ea0c75485040860d39d5014e8619dd802bd217bb32e16c175dad3800c930f", + ) +)] +#[case( + [0xb9, 0xc0, 0x7c, 0xd7, 0x80, 0xdd, 0xb5, 0x70, 0xcb, 0x2c, 0xb9, 0xe7, 0xa0, 0x6f, 0x50, 0xcf, 0xe3, 0x65, 0x17, 0xd9, 0xb6, 0x9c, 0xa1, 0xa1, 0xba, 0x7c, 0x5a, 0x90, 0x9b, 0xc7, 0x2c, 0x39], + ( + "0x2893917edd48d4f3a23be8c41beb47b71ce0d8257d4741fa061ab64c12503468", + "0x1c4760ed802d7b6a007dab8a489042056a83965f508333bfc3638fad27b9dad", + ) +)] +#[case( + [0x98, 0xa9, 0x27, 0x46, 0xc5, 0x6e, 0x65, 0x9b, 0x12, 0x0b, 0x0a, 0x23, 0xed, 0x39, 0x59, 0x33, 0x70, 0x8e, 0x12, 0xd5, 0x89, 0x8f, 0x10, 0x25, 0xb3, 0x8e, 0xb5, 0xfb, 0x03, 0xf2, 0x2d, 0x8e], + ( + "0x77c3bee21d9851de91a390068b5501aaa09d3215039b07dff2d11b67a7b35bb", + "0x2ea33101a896b978333948a111bfc23fa25f9a128c070f81299f411917192e6d", + ) +)] +#[case( + [0x03, 0x9c, 0xe1, 0x34, 0x24, 0x43, 0x6f, 0xd6, 0xf1, 0xe5, 0xb8, 0x98, 0x2a, 0x8a, 0xea, 0xb0, 0x74, 0xf4, 0xeb, 0x5e, 0xfa, 0x05, 0x5f, 0x8c, 0x1f, 0x1b, 0xf9, 0xef, 0x20, 0xe9, 0x90, 0xbd], + ( + "0x39ce13424436fd6f1e5b8982a8aeab074f4eb5efa055f8c1f1bf9ef20e990be", + "0x1880f40315bb2b5b7663b4591c3d29fc30c8f5201204537c70ecda7171a802dc", + ) +)] +#[case( + [0xde, 0x68, 0xcb, 0xb1, 0x48, 0xd7, 0x00, 0xb3, 0x08, 0xf4, 0x46, 0xb7, 0x26, 0xe9, 0x5b, 0xcf, 0x47, 0xa2, 0x95, 0x02, 0xa6, 0x0b, 0xdf, 0x66, 0x1a, 0x61, 0x99, 0xf0, 0x4b, 0x99, 0x30, 0xaf], + ( + "0x1cd791e5c410800c27b32fdd20e3fa58e99ceabd0444b53129df6994e9a53b94", + "0x812622ba3a01ce0f2d9eff7c20cb39b3f07582bf99101b3a2451ab19c30a7f1", + ) +)] +#[case( + [0x68, 0x70, 0x19, 0x94, 0x4d, 0x49, 0x22, 0x32, 0x2b, 0x04, 0xeb, 0xea, 0x55, 0xc5, 0x18, 0xcf, 0x11, 0x87, 0xfc, 0x5c, 0x41, 0x46, 0x2b, 0x4a, 0xee, 0x57, 0x81, 0x7a, 0x5f, 0xd7, 0xdb, 0x33], + ( + "0x7a77cae8ae5e1deba64607d52c26813e2852739706296307616694caedde0a7", + "0x189aa09a36b8c758731136f5d8f4d528e28aa071563d0e7ef5f6f868381e25d4", + ) +)] +#[case( + [0x81, 0x4d, 0x86, 0x37, 0xc7, 0xd2, 0xcb, 0x96, 0x7b, 0x8f, 0x65, 0x68, 0xbe, 0xcd, 0xb1, 0x42, 0x1e, 0x39, 0xfc, 0x2d, 0xff, 0x24, 0xd2, 0x69, 0xab, 0x86, 0x31, 0x59, 0x5a, 0x08, 0x9c, 0x97], + ( + "0x2084e952056f8b430aeed9fbbbcb0086ef37270b2e413d4f3345192ba90ea20a", + "0x273934c401ac2014771d9cf5e12d344a300c26d6e9a5d82a32c0b10ac3fe245d", + ) +)] +#[case( + [0x12, 0x49, 0x82, 0xde, 0x03, 0x4f, 0x41, 0x29, 0xc5, 0x3c, 0x4a, 0x52, 0xa0, 0x0c, 0xb0, 0xd2, 0x92, 0x3e, 0xc7, 0x4f, 0x0c, 0x9d, 0xf7, 0x78, 0xaf, 0x7e, 0x34, 0xf7, 0x8f, 0x9e, 0xb8, 0x46], + ( + "0x124982de034f4129c53c4a52a00cb0d2923ec74f0c9df778af7e34f78f9eb848", + "0x1c51c87febe5042e553631880fd35de6a395cf4f39521df7d66adfdcd597876a", + ) +)] +#[case( + [0x64, 0xd3, 0x87, 0xc1, 0x6e, 0x52, 0x10, 0xe3, 0xe4, 0x8a, 0x7f, 0x07, 0x5d, 0x70, 0xd9, 0x2d, 0x19, 0xe9, 0xcc, 0x94, 0x66, 0x7a, 0x7f, 0x6a, 0x95, 0x36, 0xd0, 0xd9, 0x4c, 0x5b, 0xc4, 0xd7], + ( + "0x40aeadbabeed09073e9f39a5a6e2871eae6f7719596ea501cf5b8ab9b61ca4a", + "0x1a874ccf79f771a2caae57d317055589b74215479657d1e4dd505a2c062e878c", + ) +)] +#[case( + [0xf8, 0xac, 0x13, 0xc4, 0xc2, 0xc5, 0x50, 0x3d, 0x7f, 0x7c, 0xa8, 0xe3, 0x8a, 0x53, 0x8f, 0x05, 0xa6, 0x06, 0x04, 0x6a, 0xb7, 0x55, 0xd2, 0x3c, 0x86, 0x26, 0x69, 0x72, 0xaf, 0xbf, 0x74, 0x91], + ( + "0x6b68b865ccd2f6ce5eb4c5302ccd531b07eef93ad1cdd7a5983ad00754e822f", + "0x3527ef056760fc1644805149e0697a75dbd7d309d5017a6ed5484277dbe8dcd", + ) +)] +#[case( + [0x96, 0x74, 0x61, 0x31, 0xcf, 0xcc, 0x2d, 0xcc, 0x27, 0xd0, 0x46, 0xc2, 0x46, 0x2d, 0x10, 0xa7, 0xa4, 0xb8, 0x1f, 0x5b, 0xe6, 0xc9, 0xd5, 0xc7, 0x69, 0x1f, 0xad, 0x1f, 0x34, 0x89, 0x05, 0xee], + ( + "0x54775d92c374d4efedf759ec1a9078ede33dfa7ad74761fb4be08daab120e1a", + "0x3e2c1cab56f549baf1a09d07e5951bda28671c9aa0f1ddb676cb5ba831cb4b", + ) +)] +#[case( + [0x7f, 0x23, 0x8a, 0x24, 0x2f, 0xf8, 0xbe, 0x73, 0xfb, 0xd4, 0x68, 0x5e, 0x36, 0xe7, 0x64, 0xd4, 0xf0, 0x25, 0x7a, 0xb8, 0x47, 0x6e, 0x51, 0x13, 0x18, 0xa5, 0x07, 0xc9, 0x21, 0x2c, 0xb1, 0x73], + ( + "0x1e5aed3e6d957e208b33dcf133e4b419c122a595768abbf8a063ef9b7032b6e6", + "0x1ad8d5bb5ae0e4e7f0d30e9016d20767c050e4f1a496a4c0c1cf081234c197d2", + ) +)] +#[case( + [0x1a, 0xa1, 0x15, 0xdf, 0x35, 0x31, 0xf7, 0x8d, 0x8d, 0x42, 0x26, 0xf3, 0xcd, 0xbd, 0x05, 0x02, 0x74, 0x61, 0xbb, 0xd6, 0xbc, 0x69, 0x1a, 0x5d, 0x17, 0x34, 0x3d, 0xb8, 0x2d, 0x29, 0xee, 0x80], + ( + "0x1aa115df3531f78d8d4226f3cdbd05027461bbd6bc691a5d17343db82d29ee81", + "0x7e0e7f3eb35691c2b36cccf35cf944127a51a4fd8dfc4056078ee9690808bc", + ) +)] +#[case( + [0x1e, 0xb4, 0xe8, 0x6d, 0x29, 0x4b, 0x86, 0x7c, 0x59, 0x03, 0x8b, 0x3f, 0x1e, 0x3d, 0x79, 0xc8, 0x5c, 0xdd, 0x59, 0x3a, 0xe2, 0x8c, 0x16, 0x76, 0x3c, 0x5e, 0x68, 0x15, 0x8f, 0x43, 0xbc, 0xd2], + ( + "0x1eb4e86d294b867c59038b3f1e3d79c85cdd593ae28c16763c5e68158f43bcd3", + "0x3f431fae97d3e02471fb81169367862ce60e92728c08e8b5d89c7099567e31a", + ) +)] +#[case( + [0x34, 0x67, 0x08, 0xf1, 0x00, 0x8c, 0xe1, 0x71, 0x7f, 0x00, 0x88, 0x08, 0xb8, 0xd3, 0xff, 0xb2, 0x15, 0x1d, 0xf3, 0xc2, 0xbb, 0x45, 0x2d, 0x63, 0x34, 0xda, 0x21, 0x90, 0xdd, 0xd6, 0xfb, 0x91], + ( + "0x402ba7e1f5b4147c6b042523752a7547d9c893152d362d5f8b9957a0559fe4c", + "0x19aa41a5f2c38b1002eb3bef7e17d62e63b1ebc0554a395b00730f53b8524eda", + ) +)] +#[case( + [0xe2, 0x70, 0xb9, 0x4f, 0xfc, 0xda, 0x54, 0x73, 0xd3, 0x9b, 0xf7, 0x23, 0xb1, 0xc3, 0x83, 0xf1, 0xe8, 0x01, 0xe8, 0xf7, 0x57, 0xb0, 0x9d, 0xf3, 0x27, 0xb5, 0x8b, 0xb6, 0x95, 0x3d, 0x78, 0xa8], + ( + "0x20df7f847813d3ccf25ae049abbe227b89fc3eb1b5e973be37335b5b3349838e", + "0xe8fc480f055cc91887432500a8ab752fa470ec49288701b52d8c658e32f3d3f", + ) +)] +#[case( + [0x25, 0x79, 0x24, 0x89, 0x2e, 0x15, 0x34, 0x5c, 0xe7, 0xfa, 0x78, 0x15, 0x68, 0xf8, 0x23, 0x3d, 0x1d, 0x4e, 0xb8, 0x7c, 0xaf, 0xa8, 0x75, 0x04, 0x49, 0xaf, 0xd0, 0x39, 0x77, 0x7b, 0xbe, 0xac], + ( + "0x257924892e15345ce7fa781568f8233d1d4eb87cafa8750449afd039777bbeae", + "0x1f989e2033b6f96ecae6143bacfa926c432075f6b10afdbde7f0aef1bd516eee", + ) +)] +#[case( + [0xc1, 0x4c, 0xcb, 0x92, 0xa0, 0xfd, 0x6e, 0x44, 0xaf, 0x59, 0xf6, 0x4e, 0x71, 0xfd, 0x3b, 0x85, 0xd6, 0x45, 0xe4, 0x68, 0xb3, 0xa3, 0x7a, 0x88, 0xab, 0x02, 0xd7, 0x7f, 0x3b, 0x9b, 0x17, 0x29], + ( + "0x301fe039fd688dc78669252aed79326d0fc1a4b47a4e1ae0f6a1333ab2241f56", + "0x2435ae0acce98fc60bce565d1b6139d82e7871cc5bf7456b39ff63c057f85473", + ) +)] +#[case( + [0x6b, 0x3b, 0x0a, 0x09, 0x43, 0x4d, 0x23, 0x0d, 0x4c, 0x6f, 0x93, 0xba, 0xe3, 0x02, 0xd7, 0x1b, 0xcc, 0xa5, 0x9e, 0xbb, 0x27, 0xb6, 0xa9, 0x66, 0xb3, 0x8f, 0x49, 0x06, 0x73, 0xbe, 0x79, 0xf1], + ( + "0xa726d2380e9e2b9dbcf084de00026609da2c99856d3144c3b4e30d8c2c47f66", + "0x46ab7fdff5fef556240462c54f44e9529a58881530501d36c32648320e24e56", + ) +)] +#[case( + [0xef, 0xc1, 0x7b, 0x7d, 0xc5, 0xf6, 0x09, 0x60, 0xc7, 0x7e, 0x9a, 0xda, 0x78, 0xee, 0xa6, 0x86, 0x55, 0xf4, 0xd4, 0x66, 0xdc, 0xbe, 0x89, 0x86, 0x71, 0x46, 0xd3, 0x47, 0xcf, 0xb3, 0xe0, 0x78], + ( + "0x2e3041b2412f88b9e63d840072e9450ff7ef2a213af75f5180c4a2ec6dbfeb5d", + "0x18504023c8e5785ed9fac7ed426576a4069e3607c0503328efca5a7abaa2aaa5", + ) +)] +#[case( + [0xc2, 0x40, 0x70, 0xce, 0x86, 0x15, 0xe5, 0x11, 0x95, 0x51, 0xac, 0xe6, 0xb4, 0xcc, 0xc8, 0xf5, 0xb8, 0x8a, 0xf6, 0x8e, 0x02, 0x36, 0x7f, 0xce, 0x8f, 0x74, 0xde, 0xac, 0xa8, 0x9d, 0xf8, 0xaa], + ( + "0xaf3703014f646ab410960caec7677f5a854c48606f55999ef2ae5146aa038f", + "0x2ef525f1ce2b9687b56a4d655374dd2c46332aaad6f0ee71db0af12a962ff371", + ) +)] +#[case( + [0x3a, 0x10, 0xb2, 0x1b, 0xc8, 0x59, 0xf0, 0x86, 0xeb, 0xd2, 0x64, 0x2f, 0xb5, 0x22, 0xfc, 0xea, 0xf7, 0x07, 0xdc, 0x5e, 0xeb, 0x46, 0x0c, 0x12, 0x27, 0xfa, 0xf3, 0xf6, 0x36, 0x74, 0xf1, 0xb1], + ( + "0x9ac63a8e728505d33821e7933a1a48d5f8671cd82d44184ebda67df5df7f46f", + "0xb0804c0f989a16041f75b8b8728838f365f476aabc19b145d0620b53386451c", + ) +)] +#[case( + [0xcf, 0x3d, 0xc4, 0xaf, 0x86, 0x79, 0xba, 0x0c, 0x0e, 0x55, 0x72, 0x1f, 0x28, 0x0e, 0x5d, 0x6e, 0xd7, 0x0c, 0xc3, 0x98, 0x64, 0xd1, 0x31, 0x1c, 0x05, 0x15, 0xa2, 0x5f, 0x4f, 0xa0, 0xc2, 0xed], + ( + "0xdac8ae401b339652d145b452208fbf879071952c30a06e714937203edaccdd4", + "0x2f178c75c7f3681cc8417a2efa8a5cfe9e9505c6346e8cf77c7c97108d1707ff", + ) +)] +#[case( + [0x49, 0xaf, 0x83, 0x00, 0x60, 0x19, 0x13, 0x24, 0xea, 0x98, 0x1b, 0x1a, 0xf5, 0x84, 0x72, 0x02, 0xd3, 0x0f, 0x28, 0x80, 0xbd, 0xa0, 0x9d, 0x33, 0xc4, 0x49, 0xa2, 0xf5, 0x7b, 0xca, 0xe1, 0xfe], + ( + "0x194b348d7ee772fb3247d564740319a53b8dbdef552ed2a6882916dea34de4b8", + "0x2792fb2742e19963369b48f358a321f35138bd5cec49cb47181f9b315bfbd9c8", + ) +)] +#[case( + [0xe7, 0xb2, 0x22, 0xb4, 0xae, 0xea, 0xfe, 0xc6, 0x2b, 0xa4, 0xb2, 0x04, 0xe0, 0x8a, 0xa2, 0x85, 0xca, 0x74, 0x0a, 0x75, 0xa3, 0x12, 0x4d, 0xc7, 0xf3, 0x22, 0xc2, 0x9f, 0x18, 0x95, 0x56, 0x13], + ( + "0x2620e8e92a247e1f4a639b2ada85410f6c6e6030014b239302a09243b6a160f9", + "0x30229ec3bc668ca9965bd8c24c64af0026f4c8c7359d6c566775e1bd55514219", + ) +)] +#[case( + [0xd0, 0x38, 0x1e, 0xd6, 0xae, 0xd8, 0x85, 0xe2, 0x2d, 0x22, 0xdc, 0x10, 0x5e, 0x89, 0xc9, 0xc7, 0xc7, 0xba, 0x91, 0x7f, 0x98, 0xfe, 0x05, 0x59, 0xf0, 0xb6, 0x2e, 0xed, 0x24, 0xc7, 0xf5, 0x58], + ( + "0xea6e50b2a12053b4be1c5365884685169b4e739f736db250033fe91c2d4003f", + "0x12cec8eef83574d643c0602fcba9e51cbb4cb1315dfd36ddc442c46df948301d", + ) +)] +#[case( + [0x6b, 0xe4, 0xe5, 0x7d, 0x54, 0xf0, 0x48, 0xe0, 0x3f, 0x7e, 0xe5, 0x16, 0x91, 0x5d, 0x1c, 0xa2, 0x04, 0x4c, 0x08, 0x85, 0xf3, 0xe2, 0x50, 0x02, 0x73, 0x85, 0x65, 0x79, 0xde, 0x86, 0x5c, 0x75], + ( + "0xb1c4897928d088ccede59a98e5a6be6d549336322febae7fb444d4c2d8c61e8", + "0x2e7ed32a4ac6444abee30ba89c4e9e5937bde6898c52bc0e8cac29a4b2c53a78", + ) +)] +#[case( + [0x20, 0x24, 0x92, 0x76, 0xe9, 0x41, 0x79, 0x08, 0x75, 0x82, 0xcd, 0xe9, 0x15, 0x76, 0xa0, 0xba, 0x2a, 0x8d, 0x69, 0x9f, 0xca, 0xa3, 0xc5, 0xa6, 0x8a, 0xf6, 0xcd, 0xdb, 0xbe, 0x90, 0x6b, 0x17], + ( + "0x20249276e94179087582cde91576a0ba2a8d699fcaa3c5a68af6cddbbe906b1a", + "0x7bf62a4abe2e61ca5c6fbfa714b557425f9dbf4c334a36ea96f46a1e74b4364", + ) +)] +#[case( + [0x98, 0x1f, 0x5b, 0x9d, 0x34, 0x7e, 0x79, 0xe6, 0x71, 0xe6, 0x25, 0xe8, 0xb1, 0xe2, 0xdc, 0x27, 0xa3, 0x90, 0x43, 0x14, 0xe3, 0xe5, 0x5e, 0x58, 0x7b, 0x8f, 0xab, 0x9f, 0x9c, 0x94, 0x03, 0x1f], + ( + "0x6f2704490e9996948f554c52d5ed30edd0c0360aa8ffeb0c72e075b131d0b4b", + "0x2d5bf423778fedc032bc999d5662e0e17873b38ce38acb17ac534dde1c52496d", + ) +)] +#[case( + [0x8e, 0x63, 0x3a, 0x31, 0xc5, 0x6d, 0x22, 0x8b, 0x4d, 0x55, 0xda, 0xbd, 0x4e, 0x2a, 0x9b, 0xae, 0xf6, 0x12, 0x4c, 0xf6, 0x56, 0x3d, 0xc8, 0x76, 0xb6, 0x33, 0x72, 0x48, 0x9a, 0x30, 0xfc, 0x3f], + ( + "0x2d9a9d4c0309e237dcb54f504b27eaf3c70f77d3855a335c3df25a1ae93701b2", + "0x1a1c7b0840b30bdaef6016acdd9396772e2da3639cc2b7c1c79ed30fdf94bce3", + ) +)] +#[case( + [0x41, 0xe0, 0x5f, 0x70, 0xa2, 0x15, 0x83, 0x7a, 0x69, 0x2a, 0x8e, 0x18, 0x5f, 0x7a, 0x99, 0xe5, 0x86, 0x21, 0x51, 0xbd, 0xe7, 0xe4, 0xf4, 0x72, 0xfa, 0x8b, 0xf8, 0x54, 0x5e, 0xf5, 0x85, 0xd7], + ( + "0x117c10fdc0e3e350b0da4861ddf94187ee9fe72c7f7329e5be6b6c3d86788891", + "0x106b5ce44a3c2c05625ab0f76a6857a10db19b30554817a261dea85e384266d8", + ) +)] +#[case( + [0x76, 0x67, 0x10, 0xb5, 0x92, 0xe8, 0x2f, 0xd1, 0xa8, 0x96, 0x8b, 0xb9, 0x13, 0x0f, 0x50, 0xe3, 0xda, 0xfa, 0xeb, 0x12, 0xce, 0xa4, 0x13, 0xe4, 0x5e, 0x31, 0xcd, 0x0c, 0x55, 0x08, 0xd4, 0x4e], + ( + "0x159e73cfd084ef7e37f6004c100ca028abf815effdc07ec9e5f0b4dea40ed9c1", + "0x13f78fde0e9596fad99a998b345d5269a58053753f3a7ec60c9fcd07f0e3b9a4", + ) +)] +#[case( + [0x6a, 0x8f, 0xdb, 0x6f, 0xdf, 0xa0, 0xf2, 0xd3, 0x5a, 0xba, 0x0e, 0x9d, 0x7e, 0xfe, 0x47, 0x36, 0x45, 0x9c, 0xe1, 0xa0, 0x5c, 0x78, 0x25, 0x25, 0x60, 0x0f, 0x48, 0x82, 0x92, 0xfc, 0x1a, 0x66], + ( + "0x9c73e8a1d3db27fea1983307bfb967b169a0c7d8b94900ae7ce3054e2021fd9", + "0x1835feae059b1bbdf10a9b8146d3c9e533dcd3c0f47ade837fa46f16906080b6", + ) +)] +#[case( + [0x45, 0x52, 0xa1, 0x43, 0x6a, 0xe7, 0xa1, 0x69, 0x61, 0x91, 0x3d, 0xfb, 0x71, 0x48, 0xe6, 0x18, 0x30, 0x11, 0x13, 0x09, 0xaf, 0xcc, 0xe3, 0x8a, 0xfa, 0x82, 0x33, 0x6a, 0x7c, 0x14, 0xda, 0x07], + ( + "0x14ee52d089b6013fa940f844efc78dba988fa878475b18fdbe61a753a397dcc2", + "0x166ed0d935faa57333f3d7b5563bf82573b89fd7874573d8df0984cc05cbd752", + ) +)] +#[case( + [0x94, 0xb4, 0xbd, 0xbb, 0xcd, 0x3d, 0x9d, 0x7e, 0x3b, 0x90, 0x2c, 0x9d, 0x02, 0x73, 0xf8, 0x7a, 0x84, 0x51, 0x0e, 0x52, 0xa5, 0x8c, 0x75, 0xfe, 0xce, 0xf5, 0x00, 0x0d, 0xf5, 0x4c, 0x91, 0x85], + ( + "0x387d26329a8bd01129f5b797defef61bdccce9e6c3716571a935bc96bd599b3", + "0x269d410ba9ea2bc9883b21071c96ce684d2d3f8751ec5b202d2108351f1e6e91", + ) +)] +#[case( + [0x45, 0xe8, 0xf2, 0x5a, 0xfe, 0xf6, 0xfd, 0x7a, 0x2f, 0xf7, 0xcf, 0x6b, 0x05, 0x8b, 0x2d, 0xf9, 0x03, 0x5c, 0x76, 0x7a, 0x16, 0x1b, 0x55, 0x06, 0x39, 0x22, 0xdd, 0xc7, 0xa9, 0x55, 0xf7, 0x24], + ( + "0x1584a3e81dc55d5077a789b48409d59b6bdb0be8ada98a78fd0251b0d0d8f9de", + "0x1a9e98c09961eeb7ecda08b384f55b4bee996f5c5d3447979575bbe91b8e68b2", + ) +)] +#[case( + [0xa2, 0xc0, 0xdd, 0xe2, 0x1a, 0x63, 0xd8, 0xe7, 0x57, 0xa9, 0x98, 0x51, 0xd8, 0x79, 0xf6, 0xe2, 0xe5, 0x82, 0x60, 0x7b, 0xd2, 0x08, 0x80, 0xef, 0x64, 0xc8, 0x31, 0xc9, 0xa7, 0xce, 0x88, 0x00], + ( + "0x1193f28976cef86a2eb8c72e53f5edca1efe20c798b32147b0668d851e57902d", + "0x241199f07ac58ba64709d646771084f30197bb59ff56a608d6e3dc3e726fdebb", + ) +)] +#[case( + [0xf7, 0x03, 0x68, 0x24, 0x29, 0x60, 0x72, 0x91, 0x63, 0x35, 0xa4, 0x35, 0x19, 0xa7, 0x09, 0xd4, 0x1e, 0x8e, 0x24, 0x4a, 0x34, 0xd0, 0x76, 0x71, 0x61, 0xe3, 0x76, 0xea, 0x41, 0x96, 0x5d, 0x8e], + ( + "0x50ddfe5c36851c0c9a447a49220500029070f732a9781af3540ba7807256b31", + "0xbc3e5ca0c6840889a0dda586ae1134ae54d0a55b9455559b9eb05fee189ce79", + ) +)] +#[case( + [0x62, 0xc1, 0xaf, 0x24, 0xad, 0x33, 0xc5, 0x62, 0x35, 0x66, 0x76, 0x40, 0x1c, 0x86, 0x66, 0xad, 0x22, 0x05, 0x21, 0x82, 0xc9, 0xa2, 0xaf, 0xdd, 0x7a, 0xbc, 0x13, 0x8f, 0xaa, 0x0d, 0x15, 0x50], + ( + "0x1f9123eead0850ec4c5ead31983b5f1f3024c5ff8bf1ac3027afb61f9131ac3", + "0x7293610b80e44120b6d7760348f8fce52682cb22937352d5b3027fbe6c25b54", + ) +)] +#[case( + [0xaa, 0x0a, 0x4e, 0xa0, 0xab, 0x4c, 0x0e, 0xbf, 0x66, 0x39, 0x9b, 0x36, 0xdc, 0xc1, 0x75, 0x9c, 0x0f, 0x00, 0x31, 0xb5, 0x45, 0xc5, 0x1d, 0xdc, 0x38, 0x45, 0x76, 0x53, 0x31, 0x07, 0x99, 0xa4], + ( + "0x18dd634807b72e423d48ca13583d6c83487bf2010c6fbe3483e3d20ea790a1d1", + "0x17c137a4ae3a4b273db4d0a58881f63fb785eb13200df81571f135eb7872b539", + ) +)] +#[case( + [0xa9, 0x4f, 0xd0, 0x74, 0x47, 0x80, 0x07, 0xc2, 0xe9, 0x54, 0x55, 0x14, 0x6a, 0x76, 0xe4, 0xff, 0x6c, 0x33, 0xa8, 0x54, 0xd1, 0x51, 0xf9, 0x92, 0xc0, 0x54, 0xbd, 0x62, 0xc3, 0xab, 0xc4, 0xd7], + ( + "0x1822e51ba3eb2745c06383f0e5f2dbe6a5af68a097fc99eb0bf3191e3a34cd04", + "0x255727a6ad0bf34a99c7ea8d7c824f6076521640f7e6ad459e67e1e2d290412b", + ) +)] +#[case( + [0xa3, 0x25, 0xbc, 0x68, 0xe7, 0x21, 0x01, 0xcd, 0xfd, 0x27, 0x23, 0xba, 0xb8, 0x2c, 0x44, 0xec, 0x83, 0x71, 0x00, 0x6f, 0xb1, 0xf3, 0x7a, 0x7d, 0x3b, 0xcd, 0x38, 0x72, 0xe2, 0x32, 0x77, 0x41], + ( + "0x11f8d110438c2150d436529733a83bd3bcecc0bb789e1ad5876b942e58bb7f6d", + "0x1efe429d05856da71c87b24c67834e96758d9a56fed30bf0ecedc6e23fc6e86f", + ) +)] +#[case( + [0x79, 0x3d, 0xd1, 0xab, 0xa5, 0x46, 0x98, 0xfd, 0x37, 0x56, 0x61, 0x3b, 0x80, 0x53, 0x18, 0xd9, 0xce, 0x22, 0xe0, 0x57, 0x92, 0xca, 0x62, 0x56, 0x11, 0xac, 0x6d, 0xa2, 0xee, 0xf0, 0x6b, 0xcf], + ( + "0x187534c5e2e358a9c6b5d5ce7d50681e9f200b34c1e6cd3b996b55753df67142", + "0x238bf89fc4013d62d7b3b59d62fd7e80f5579a230cbf259b9f2afd52fc0d05d8", + ) +)] +#[case( + [0x48, 0xa2, 0xf5, 0x9a, 0x3d, 0x15, 0x48, 0x70, 0xf2, 0x1b, 0x98, 0x8d, 0x2e, 0x61, 0x75, 0x4c, 0xa2, 0x19, 0x4f, 0xc9, 0xcd, 0x9e, 0xad, 0x8f, 0xff, 0x14, 0x30, 0xc1, 0x1a, 0xf5, 0x0c, 0xfe], + ( + "0x183ea7275be3a84739cb52d6ace01cef0a97e538652ce302c2f3a4aa42780fb9", + "0x107f79839eb4ef78cd3bf14ea4f31af765d750fcec55a05bdea0e1988799d770", + ) +)] +#[case( + [0x4d, 0x0f, 0x2c, 0xf2, 0xd1, 0xfc, 0x52, 0x49, 0xc5, 0x21, 0xaa, 0xbf, 0xbf, 0x91, 0xb9, 0x13, 0xd1, 0xfb, 0x42, 0x19, 0x86, 0x0a, 0x35, 0x5e, 0x3a, 0x3a, 0xee, 0x76, 0xd9, 0x2d, 0x6d, 0xf9], + ( + "0x1caade7ff0cab2200cd165093e1060b63a79d7881d986ad0fe1a626000b070b3", + "0x1af1ed981c6638f540e8213d8dd86effc94aae6a246033e6763fe22da4b52f8", + ) +)] +#[case( + [0xa2, 0xc3, 0xb5, 0xac, 0x07, 0xbd, 0x2f, 0x74, 0xf7, 0x98, 0x7e, 0x00, 0xe2, 0xaf, 0x52, 0x4f, 0x6a, 0x95, 0x07, 0xd4, 0x14, 0x93, 0x16, 0x87, 0xf6, 0xca, 0x42, 0x34, 0xe3, 0x7d, 0xf3, 0x2c], + ( + "0x1196ca5364284ef7cea7acdd5e2b4936a410c81fdb3db6e042689df05a06fb58", + "0x1c650bdd7a09178066e43e0ffe80de22481d67f45eafbb322b91b7527a5bf5ef", + ) +)] +#[case( + [0x9f, 0x07, 0xc9, 0x80, 0xf0, 0xb8, 0xb1, 0x26, 0x03, 0xd8, 0x8e, 0xfe, 0xd7, 0x7f, 0x1a, 0x75, 0x01, 0xbc, 0x3b, 0xa1, 0xe4, 0x1e, 0x10, 0x2c, 0xf3, 0xf1, 0x62, 0xf1, 0xf8, 0x1b, 0x74, 0xef], + ( + "0xddade284d23d0a8dae7bddb52fb115c3b37fbedaac8b0853f8fbead6ea47d1d", + "0x1747a74235fb00662a67bc573471a5f7c6514b4e82f06388b938ed62f1900fbf", + ) +)] +#[case( + [0x43, 0xef, 0x47, 0xbe, 0x77, 0xec, 0xba, 0x40, 0xa5, 0x5f, 0x78, 0x5b, 0x05, 0x26, 0x21, 0x73, 0x32, 0x6d, 0x9c, 0x10, 0xae, 0x1a, 0xf1, 0xaa, 0xba, 0xde, 0x95, 0x61, 0x71, 0x86, 0x00, 0x08], + ( + "0x138af94b96bb1a16ed0f32a483a4c9159aec317f45a9271d7ebe094a990902c3", + "0x27800587109ddaa3d2398457e8eff9d08594f2ef80ff6c168be9c17bfc67bb86", + ) +)] +#[case( + [0x99, 0x32, 0x74, 0x19, 0x77, 0x0f, 0x9b, 0x3d, 0x5d, 0x19, 0xce, 0xad, 0xcc, 0x06, 0xa5, 0x1d, 0x08, 0xe2, 0x86, 0x30, 0x4b, 0x61, 0xd5, 0x08, 0xcc, 0x36, 0xbc, 0x2e, 0x23, 0x5b, 0xf3, 0x05], + ( + "0x80588c0d37abac03428fd8a47829c04425e467c120c756117d517e999e4fb32", + "0x2b2b597df75fa810ed717dfe13439b741757b38c9334b122f4be3c506af6256d", + ) +)] +#[case( + [0x34, 0x7b, 0x86, 0xec, 0xe0, 0x00, 0x89, 0x2a, 0x2d, 0x84, 0x5b, 0x2b, 0x36, 0x82, 0x21, 0x63, 0x8a, 0x2a, 0x04, 0x82, 0x1d, 0x03, 0x2c, 0xe3, 0xef, 0xbc, 0xf7, 0xbe, 0x57, 0x44, 0x79, 0x59], + ( + "0x4173879fecee90075341574b500c905f2a899f0b4916256b39c6ba77ec77c13", + "0x245945b21c5f4c904ca92ae4841b7e3cf68b478b4b57188067544a70cf641e92", + ) +)] +#[case( + [0xb2, 0x32, 0xe7, 0x37, 0x69, 0xe5, 0xec, 0x75, 0xd8, 0x50, 0xeb, 0xac, 0xa4, 0x05, 0xd5, 0x7b, 0x59, 0x8d, 0x3b, 0xe7, 0x74, 0x7f, 0x00, 0xb6, 0x28, 0x11, 0x65, 0xe9, 0x27, 0xa8, 0x36, 0xb8], + ( + "0x2105fbdec6510bf8af601a891f81cc629308fc333b29a10e73afc1a49e313ee8", + "0x252caf4b3112785e5f912c17d052c4c14fa676800fcd37876dadde3a28d7dd81", + ) +)] +#[case( + [0x66, 0xa8, 0xd3, 0xe8, 0x9d, 0xc0, 0x41, 0x16, 0xe4, 0x64, 0xc4, 0xfe, 0x87, 0xcf, 0x7c, 0x83, 0x93, 0x12, 0x86, 0xef, 0xf4, 0x6a, 0xa2, 0x5a, 0x17, 0xf2, 0x62, 0xcd, 0x83, 0x1e, 0xbd, 0x1e], + ( + "0x5e03702db5d00c373c4399184cccbc8640fb1cd23870d3f9fb14a9fd224c292", + "0x26fdc724c7abf7481d3156c1d3d11b477c75099b1ae5aa683e8db7cfb8e94c9e", + ) +)] +#[case( + [0xb6, 0x95, 0xf0, 0x38, 0xf9, 0x32, 0x4f, 0x62, 0x8c, 0x2d, 0x39, 0x60, 0x83, 0xbe, 0x9f, 0xa5, 0xa9, 0xe6, 0xf7, 0xbf, 0xe7, 0x1f, 0x64, 0xc9, 0x5b, 0x8d, 0xc7, 0x23, 0xde, 0x94, 0x0e, 0xa9], + ( + "0x256904e0559d6ee5633c683cff3a968ce362b80badca0521a72c22df551d16d5", + "0x8a31cf2520f6f457641e85200ce2926b6869746fe11b7c3371d3b99658a4b67", + ) +)] +#[case( + [0x3a, 0xb4, 0x53, 0xb4, 0xf9, 0x53, 0xe1, 0x50, 0xaa, 0xb1, 0x57, 0xdd, 0x64, 0xd7, 0x85, 0x77, 0x9e, 0xeb, 0xe6, 0x00, 0xb8, 0x7f, 0xb6, 0xf8, 0xe4, 0x62, 0x1f, 0x41, 0x94, 0x41, 0x73, 0x57], + ( + "0xa50054218224126f2611226e3562d1a076a7b6f500dec6ba841932abbc47611", + "0x6077211dfc9be65367a46b06d7e680e80be6ea6b89b37942739bedbef074a42", + ) +)] +#[case( + [0xfc, 0x0c, 0xe9, 0xb2, 0xec, 0xb2, 0x50, 0xfb, 0xb9, 0x34, 0xbc, 0x5a, 0x74, 0x98, 0xe4, 0xc7, 0x62, 0x8d, 0x6f, 0x1f, 0x3a, 0x56, 0x69, 0x45, 0x47, 0x96, 0xbe, 0x15, 0xf8, 0x56, 0x31, 0x4e], + ( + "0xa17617486ba302b1fa35fc9ed122af36d065a48301d74831af401a3bde53eec", + "0x963de747d8d2b579ae26cf9112dd73fb1698112221245375a8915df8cd5c68f", + ) +)] +#[case( + [0x7c, 0x79, 0x95, 0x2e, 0x47, 0x38, 0xd5, 0x5f, 0xd8, 0xe3, 0x8f, 0x00, 0x89, 0x21, 0xb9, 0xba, 0x3a, 0xda, 0x1b, 0x74, 0xc2, 0xd1, 0x7c, 0x80, 0xe4, 0x47, 0x50, 0x0a, 0xb5, 0x40, 0xcf, 0xd7], + ( + "0x1bb0f84884d5950c68430393861f08ff0bd74651f1ede7666c0637dd0446d54b", + "0x1f6ba94c51523e4ea8e86c22cc91d2b41800cb6b82f55dff5c0e29eb7af447a4", + ) +)] +#[case( + [0xab, 0x5e, 0x49, 0x8f, 0xd0, 0xf7, 0x2c, 0xd8, 0x54, 0xed, 0xe6, 0x27, 0x20, 0x4c, 0x23, 0xd6, 0x39, 0xa1, 0x4a, 0x71, 0x4b, 0x35, 0xe7, 0xa8, 0x09, 0x1e, 0x0f, 0x04, 0xa4, 0xd7, 0x49, 0xb2], + ( + "0x1a315e372d624c5b2bfd15039bc81abd731d0abd11e0880054bc6ac01b6051de", + "0x2f3da23bb87b5172ca0a1fda1eca300280f31aa27bd7fb97880cc68cf44dd8e7", + ) +)] +#[case( + [0xaf, 0x4c, 0x6f, 0xf5, 0xb4, 0x84, 0x7e, 0x3c, 0x08, 0x7c, 0xf6, 0x1f, 0x23, 0x14, 0x1c, 0x60, 0xd4, 0x61, 0xf2, 0x52, 0x3a, 0x5f, 0x11, 0xa5, 0x5c, 0x26, 0x24, 0xdf, 0x02, 0xaf, 0x85, 0x46], + ( + "0x1e1f849d10ef9dbedf8c24fb9e9013480dddb29e0109b1fda7c4809a79388d72", + "0x1769bb9eef351384b387d37efa6c5e350f5f5aa92828fb66a7ce1e19a1160c07", + ) +)] +#[case( + [0x99, 0xd5, 0x5e, 0xd5, 0x2a, 0xf3, 0xed, 0x12, 0xb0, 0x4d, 0x7f, 0x6d, 0x19, 0xcc, 0xaf, 0x69, 0x38, 0x85, 0xdd, 0xdb, 0xcd, 0x8c, 0xbe, 0xd9, 0xd9, 0x01, 0x20, 0x28, 0x34, 0x18, 0x4f, 0xf0], + ( + "0x8a8737c875f0c95875cae499548a65072019e2794375f32249f7be3aaa1581c", + "0x18291fd161cc0167ccdc7ace93209769f4c2b598e5d4e18471bbb9dd030ec411", + ) +)] +#[case( + [0xe2, 0x7d, 0x5b, 0xf5, 0x9a, 0x10, 0x33, 0x4b, 0x41, 0x18, 0x88, 0x70, 0x78, 0x86, 0x2b, 0xbd, 0xd1, 0x62, 0x21, 0xfb, 0xd0, 0xaa, 0xb9, 0x99, 0x50, 0x10, 0xaf, 0x19, 0xdf, 0x1c, 0x2b, 0x3a], + ( + "0x20ec222a1549b2a45fd771967280ca47735c77b62ee38f645f8e7ebe7d28361f", + "0x17e26eba42b8a51b5364e0d117e68d46ea8d7c52f3885ed075bc441d62daf4e9", + ) +)] +#[case( + [0x62, 0x52, 0xa3, 0xde, 0xa6, 0x05, 0x54, 0x85, 0x65, 0xb6, 0x83, 0x8f, 0x85, 0x38, 0xee, 0xab, 0x9c, 0x8b, 0x66, 0x64, 0x90, 0x05, 0xc0, 0x17, 0x95, 0x9d, 0x0d, 0x2d, 0x20, 0xec, 0x2a, 0xa0], + ( + "0x18a06f8e3a21431f515f82282363df06d889141bf222afd1d5bf4ff6ff23019", + "0x1b7100007cbb0527c1fac9c535384306437fee51c246f5aad7398f6b147135e", + ) +)] +#[case( + [0x63, 0x13, 0x43, 0x12, 0x88, 0x35, 0x21, 0x36, 0x56, 0xaa, 0x9f, 0x4d, 0xfd, 0x7d, 0x6e, 0xca, 0xa5, 0xa8, 0x25, 0xd0, 0x9c, 0xb4, 0xa6, 0xa4, 0x24, 0xcf, 0x53, 0x90, 0x92, 0x17, 0xb8, 0xdd], + ( + "0x24aa62cc5d1e0e2e60a13e0fa7abe0f76a550adcbd11189ac8e3b62e11dbe50", + "0x2b9d2ba81761e8cbdef99433b49216b1d0a7dbcb19b5b000de82bd497febce90", + ) +)] +#[case( + [0xbd, 0xd9, 0x27, 0xb4, 0x6b, 0x96, 0x7b, 0xc9, 0x3a, 0xc4, 0x61, 0x41, 0x8d, 0x5e, 0x66, 0xad, 0xf2, 0xde, 0x77, 0x58, 0x95, 0x42, 0x57, 0x45, 0x5b, 0x16, 0x5e, 0x40, 0x5f, 0x40, 0x25, 0xc9], + ( + "0x2cac3c5bc8019b4c11d3901e08da5d952c5a37a45becf79da6b4b9fbd5c92df6", + "0xed67a945faf7b28eea82c4367889b22772a45a5b3552afcf47499a1f776b749", + ) +)] +#[case( + [0x7f, 0x6f, 0x8b, 0xde, 0xf8, 0x24, 0x5a, 0x28, 0x10, 0x50, 0x4d, 0xa9, 0x8c, 0xe0, 0x59, 0x64, 0x5c, 0xa3, 0xa6, 0x27, 0x17, 0x79, 0x8e, 0x5c, 0x13, 0x5f, 0xbb, 0x5c, 0x13, 0x9a, 0x55, 0x59], + ( + "0x1ea6eef935c119d49fafc23c89dda8a92da0d1044695f9419b1ea32e62a05acc", + "0x5998789b8df4a10b776915ba84d5a2bd92eacbe8839fd9467e58df7deb54918", + ) +)] +#[case( + [0x36, 0xa5, 0x2b, 0x01, 0xb7, 0x76, 0x93, 0x9b, 0x6a, 0x6c, 0xec, 0xca, 0x0e, 0x11, 0x54, 0xc8, 0xb7, 0x91, 0x88, 0xa0, 0x9d, 0x0e, 0xf4, 0x67, 0x9b, 0x84, 0xa3, 0xe9, 0x05, 0x38, 0xf1, 0x99], + ( + "0x640dc8ed644f371b21ca7138c8ffc6b20101e0f349d29da5f6417d22cbbf455", + "0xcd695eb60268449ff5bc196b963104ece0e0a25ea40fe70cccbf7f513b25e32", + ) +)] +#[case( + [0x0b, 0xac, 0x65, 0xce, 0x37, 0x67, 0x55, 0x9a, 0x34, 0x61, 0xbc, 0xc0, 0x78, 0x39, 0xfb, 0x41, 0x93, 0xf0, 0xb3, 0x5d, 0xad, 0x91, 0x90, 0xbe, 0xe5, 0xa4, 0x5c, 0xce, 0x12, 0x9f, 0x49, 0x2f], + ( + "0xbac65ce3767559a3461bcc07839fb4193f0b35dad9190bee5a45cce129f4931", + "0x16f8bfc17896276daad9fc8042bd1cc23102691f1bf6d02d8a98af3506479b42", + ) +)] +#[case( + [0x4b, 0x73, 0x62, 0x43, 0x62, 0x23, 0xef, 0xbd, 0xda, 0xe8, 0x27, 0xc7, 0x47, 0x05, 0x88, 0x51, 0x94, 0xdc, 0x53, 0x8e, 0x82, 0x7e, 0xb1, 0x9e, 0xc0, 0x63, 0xe2, 0x61, 0x2b, 0x54, 0x37, 0x58], + ( + "0x1b0f13d080f24f942297e210c5842ff3fd5ae8fd1a0ce7118443564a52d73a13", + "0x2a63fcc846be71e41180eb5555af10c3d800c4f2182bcfa70ccb38c1ca031b3e", + ) +)] +#[case( + [0x30, 0x65, 0x95, 0xba, 0xf3, 0xbc, 0x3a, 0x34, 0x1a, 0xb9, 0x42, 0xf6, 0x00, 0x94, 0xd9, 0x1e, 0xc5, 0x51, 0x4b, 0x1c, 0x53, 0x5a, 0x33, 0xca, 0x77, 0x03, 0x93, 0x12, 0x39, 0x99, 0x3c, 0x45], + ( + "0x14748128a9a0a6268fd3f7f1380c12dcfe08aeae8693d3ae306fb611c3f00", + "0x1b97709f6f73cc1de91f093c39f1676b3f9df710e918d44f413c577b75f6a2d4", + ) +)] +#[case( + [0xa4, 0x60, 0x9a, 0x18, 0xf4, 0x91, 0x1a, 0x4c, 0x2e, 0xcf, 0x7d, 0xe1, 0x21, 0x17, 0x13, 0xe3, 0x51, 0xe6, 0x4d, 0xcf, 0xa2, 0x67, 0xe3, 0xe1, 0x63, 0x5a, 0x05, 0x07, 0xb7, 0xd9, 0xf6, 0x4c], + ( + "0x1333aec050fc39cf05deacbd9c930aca8b620e1b69128439aef860c32e62fe78", + "0x7d90bb5159c95cb06870125aa576afa08c51d1e036af08c62ab5f6bff57ac95", + ) +)] +#[case( + [0x7a, 0x19, 0xf1, 0xbc, 0xc8, 0xc0, 0x82, 0xc3, 0x6f, 0x15, 0x7a, 0x6a, 0x53, 0x8a, 0xd7, 0x57, 0x89, 0xe6, 0xb5, 0x86, 0xe3, 0x97, 0x7e, 0xdd, 0x99, 0xc4, 0xad, 0x9b, 0xde, 0xc9, 0x98, 0x32], + ( + "0x195154d7065d426ffe74eefd5088269c5ae3e06412b3e9c32183956e2dcf9da7", + "0x1afabeb6cc7b74080ace244b6b464335b4b068f661f72b969e9badc0f4d2508e", + ) +)] +#[case( + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ( + "0xe0a77c19a07df2f666ea36f7879462c0a78eb28f5c70b3dd35d438dc58f0d9d", + "0x7e1e236cdff80b192b235c513456f009e6c4be16a4bd0cba8b1c036c07d676f", + ) +)] +#[case( + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe], + ( + "0xe0a77c19a07df2f666ea36f7879462c0a78eb28f5c70b3dd35d438dc58f0d9c", + "0x1ba60ab9532bc4edca3bebc05b5b1895dab411a9ecf0485a9c95c4d19bea3c7d", + ) +)] +#[case( + [0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ( + "0x2e6ec6347b397f591ebee925f9fa9e89a1fa55ba5e38d5cb0f7dcfa49e0c0ae5", + "0x195e851647e4861db215653fd73aaa4c4238f70ad4132e1d83af907f3b50e3a7", + ) +)] +#[case( + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], + ( + "0x2", + "0xce2c194b86251806451ec04be095d60517130cff61fcb49c4ef4e708dac7f34", + ) +)] +#[case( + [0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + ( + "0x1000000000000000000000000000000000000000000000000000000000000003", + "0x818b9939a932ca6f6f95b35223bfd401a66e8bfad7101b8d1cc6df85c4f5fdc", + ) +)] +#[case( + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + ( + "0x1", + "0x2", + ) +)] +fn decompress_hash_works( + #[case] hash: CombinatorialId, + #[values(false, true)] consume_all: bool, + #[case] expected: (&str, &str), +) { + let x = Fq::from_hex_str(expected.0); + let y = Fq::from_hex_str(expected.1); + let expected = G1Affine::new(x, y); + let actual = decompress_hash(hash, Fuel { total: 16, consume_all }).unwrap(); + assert_eq!(actual, expected); +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/get_collection_id.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/get_collection_id.rs new file mode 100644 index 000000000..b19615caa --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/get_collection_id.rs @@ -0,0 +1,96 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use rstest::rstest; + +// Gnosis test cases using mocked keccak256 results, found here: https://gnosis-conditional-tokens.readthedocs.io/en/latest/developer-guide.html# +#[rstest] +#[case( + // 0x52ff54f0f5616e34a2d4f56fb68ab4cc636bf0d92111de74d1ec99040a8da118 + [ + 0x52, 0xFF, 0x54, 0xF0, 0xF5, 0x61, 0x6E, 0x34, 0xA2, 0xD4, 0xF5, 0x6F, 0xB6, 0x8A, 0xB4, + 0xCC, 0x63, 0x6B, 0xF0, 0xD9, 0x21, 0x11, 0xDE, 0x74, 0xD1, 0xEC, 0x99, 0x04, 0x0A, 0x8D, + 0xA1, 0x18, + ], + None, + // 0x229b067e142fce0aea84afb935095c6ecbea8647b8a013e795cc0ced3210a3d5 + Some([ + 0x22, 0x9B, 0x06, 0x7E, 0x14, 0x2F, 0xCE, 0x0A, 0xEA, 0x84, 0xAF, 0xB9, 0x35, 0x09, 0x5C, + 0x6E, 0xCB, 0xEA, 0x86, 0x47, 0xB8, 0xA0, 0x13, 0xE7, 0x95, 0xCC, 0x0C, 0xED, 0x32, 0x10, + 0xA3, 0xD5, + ]) +)] +#[case( + [ + 0xD7, 0x9C, 0x1D, 0x3F, 0x71, 0xF6, 0xC9, 0xD9, 0x98, 0x35, 0x3B, 0xA2, 0xA8, 0x48, 0xE5, + 0x96, 0xF0, 0xC6, 0xC1, 0xA9, 0xF6, 0xFA, 0x63, 0x3F, 0x2C, 0x9E, 0xC6, 0x5A, 0xAA, 0x09, + 0x7C, 0xDC, + ], + None, + // 0x560ae373ed304932b6f424c8a243842092c117645533390a3c1c95ff481587c2 + Some([ + 0x56, 0x0A, 0xE3, 0x73, 0xED, 0x30, 0x49, 0x32, 0xB6, 0xF4, 0x24, 0xC8, 0xA2, 0x43, 0x84, + 0x20, 0x92, 0xC1, 0x17, 0x64, 0x55, 0x33, 0x39, 0x0A, 0x3C, 0x1C, 0x95, 0xFF, 0x48, 0x15, + 0x87, 0xC2, + ]) +)] +#[case( + [ + 0xD7, 0x9C, 0x1D, 0x3F, 0x71, 0xF6, 0xC9, 0xD9, 0x98, 0x35, 0x3B, 0xA2, 0xA8, 0x48, 0xE5, + 0x96, 0xF0, 0xC6, 0xC1, 0xA9, 0xF6, 0xFA, 0x63, 0x3F, 0x2C, 0x9E, 0xC6, 0x5A, 0xAA, 0x09, + 0x7C, 0xDC, + ], + Some([ + 0x22, 0x9B, 0x06, 0x7E, 0x14, 0x2F, 0xCE, 0x0A, 0xEA, 0x84, 0xAF, 0xB9, 0x35, 0x09, 0x5C, + 0x6E, 0xCB, 0xEA, 0x86, 0x47, 0xB8, 0xA0, 0x13, 0xE7, 0x95, 0xCC, 0x0C, 0xED, 0x32, 0x10, + 0xA3, 0xD5, + ]), + // 0x6f722aa250221af2eba9868fc9d7d43994794177dd6fa7766e3e72ba3c111909 + Some([ + 0x6F, 0x72, 0x2A, 0xA2, 0x50, 0x22, 0x1A, 0xF2, 0xEB, 0xA9, 0x86, 0x8F, 0xC9, 0xD7, 0xD4, + 0x39, 0x94, 0x79, 0x41, 0x77, 0xDD, 0x6F, 0xA7, 0x76, 0x6E, 0x3E, 0x72, 0xBA, 0x3C, 0x11, + 0x19, 0x09, + ]) +)] +#[case( + [ + 0x52, 0xFF, 0x54, 0xF0, 0xF5, 0x61, 0x6E, 0x34, 0xA2, 0xD4, 0xF5, 0x6F, 0xB6, 0x8A, 0xB4, + 0xCC, 0x63, 0x6B, 0xF0, 0xD9, 0x21, 0x11, 0xDE, 0x74, 0xD1, 0xEC, 0x99, 0x04, 0x0A, 0x8D, + 0xA1, 0x18, + ], + Some([ + 0x56, 0x0A, 0xE3, 0x73, 0xED, 0x30, 0x49, 0x32, 0xB6, 0xF4, 0x24, 0xC8, 0xA2, 0x43, 0x84, + 0x20, 0x92, 0xC1, 0x17, 0x64, 0x55, 0x33, 0x39, 0x0A, 0x3C, 0x1C, 0x95, 0xFF, 0x48, 0x15, + 0x87, 0xC2, + ]), + Some([ + 0x6F, 0x72, 0x2A, 0xA2, 0x50, 0x22, 0x1A, 0xF2, 0xEB, 0xA9, 0x86, 0x8F, 0xC9, 0xD7, 0xD4, + 0x39, 0x94, 0x79, 0x41, 0x77, 0xDD, 0x6F, 0xA7, 0x76, 0x6E, 0x3E, 0x72, 0xBA, 0x3C, 0x11, + 0x19, 0x09, + ]) +)] +fn get_collection_id_works( + #[case] hash: CombinatorialId, + #[case] parent_collection_id: Option, + #[values(false, true)] consume_all: bool, + #[case] expected: Option, +) { + let actual = + get_collection_id(hash, parent_collection_id, Fuel { total: 16, consume_all }).ok(); + assert_eq!(actual, expected); +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/matching_y_coordinate.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/matching_y_coordinate.rs new file mode 100644 index 000000000..3176126d0 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/matching_y_coordinate.rs @@ -0,0 +1,281 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +// Empty string in the `expected` argument signals `None`. +#[test_case("0x00", "")] +#[test_case("0x01", "0x02")] +// Python-generated: +#[test_case( + "0x20e949416f9b53d227472744dcc6e807311aa8cf1f3de6e23d9f146759d5afe2", + "0x14142aae4ac3081fc594fde12028d9b329e610472146bdfe7ec3ed4492894b90" +)] +#[test_case( + "0x26df0c83801d57dab45b0e36bbf322a7fecf072e2542b77de0b8eb450165bd1b", + "0x2596d10178999c2ed646acafa8a43cdd4926f9fb8a2ab3abfb75fcc010291440" +)] +#[test_case("0x21d8695d6abc0fcb4f070a45ebab7ce86ca8f82d948222b5ba0d572819c967f6", "")] +#[test_case("0x11c65bcf21136b93e15a23ce980383e30670e0d7106aff3c38b0558237421ce3", "")] +#[test_case("0x21c35530c4705937da4166a4f4e0c29b3a5ae1803610cd658638b33637261728", "")] +#[test_case( + "0x9a16530e10c85acbab040e9486fe1741e0a7c0d2249421a731d5c6403b4bcf8", + "0xd7117c72ace2d2cec54fa56ecbd5aa8760896d31308f57b9e56d79d7f7f1b34" +)] +#[test_case( + "0x42b9cfd6e74a002ee2ac03e230b74228359d49c0917784e0d2471867b593354", + "0x1e3a6b435798e6c845b77845991d6645771d7f6387fdca2f70b733749b432659" +)] +#[test_case("0x20ca4d81498b973879ae13e6e84532a80f770d11b2358d195ea2829ba3767afa", "")] +#[test_case("0x56eca00b1f4332c4ccc29256c0cde908b12f27f52767f223e47d276df7edaa2", "")] +#[test_case( + "0x2346bbe95aedd97d4e656c34e26428338e35c7557c401146378e26e325d9ccb1", + "0xe6529e3bbfe35be3a2f50fdfa139dd7b90d56817b091534e7b1328542a84c9b" +)] +#[test_case( + "0x2c18c2f96d049417a46388f4b33e9c027cda40949fc5ace55c0434ab14043389", + "0x1bdf570be306f3affe252fa1506ce317e034c073953a5459445ac5a9b9cde10e" +)] +#[test_case( + "0x1cb864f4de5a8449ffe9ffc9d24aae3893226c7fff60a9291729db5cbb6deae3", + "0x22b4d4c27ff59f4a87cd116fa68a2d4d708c06923d954e5b33a4a777fed7fc65" +)] +#[test_case("0x13920c77970b63884e6e9d7129740e8203d5bd427b784a020f204aebe10b9c8b", "")] +#[test_case("0x9098168314bd49d87562f2f67fe0bfc0d85f1dff08b42280804c25062bcfb56", "")] +#[test_case("0xd46cf808af0ec6a8986bae5904e937a3c40e542785b57b7705f4c2063ba636f", "")] +#[test_case( + "0x304f7642f50320bfd0fecd785aab9e2cb9aa4d78eea9d3db8c5e533ab1753f2c", + "0xd3a5d7fbc69cb057f7a36ed30c6b8bc1dee54bbea45a50a8459663f09aefc52" +)] +#[test_case("0xf37a8f1c56394ff52f1255a26d53f3fe24c7fb0765a3af1e58fc5d3fed84367", "")] +#[test_case("0x22edcd91537cba89f0a550b266ab6368fba606e7ded18042d0b33a11fa6d3362", "")] +#[test_case("0x2e1cf89cd4ee4b35ec15d0f9d475c52105f53ff6315629f7df36a9f02ed35646", "")] +#[test_case("0x1209ec936ef1e7e27bc51787f8be69646a87bffd72a7b1e52e7fcaf9932f74bf", "")] +#[test_case("0x162d48238d431d770a86ad6a013201cb0436d77ef7ddd2adcd39d6faa92b26d8", "")] +#[test_case("0xba0fa54d12fb286c4e0b9718c3e1410ea825dd4c2b5929001e064ee7f6361b6", "")] +#[test_case("0x1ebb509c2ecad8e01a0d86f0035ec68629aed3d5878300e044982292126783df", "")] +#[test_case("0x139c6113eee4057f6304b4e6472300b73c8c8a6d570b913d4d23528cfa661428", "")] +#[test_case( + "0xb177c6f82daed853261bc68080e17a4db51bc68d2c7e052dd483ddecca3d539", + "0x131902ccbeb6b5d5fcfd2902ff324b5ca03d26284f09932bbfb11c0cf25cbe04" +)] +#[test_case( + "0x11c2e51e2564719627780fa81f817ecdce8d34d3c4bce40a4164aa68a331753c", + "0x8ff562cbcbb36c6efc63b7d681820039bc4e8dff8ba94f26078a60a89312789" +)] +#[test_case( + "0x101d78729b63303abee4f033c2bc62de1a6aa85abd38c8b1ce5f61ff312e0b7", + "0xd2c903227d0b4da36a01788f84ca0abd481f75cd255907e99cfc90290af84c" +)] +#[test_case("0xb4aaee9e08f72a75fd4163a04ef9c2cef8467f388906a9dba01cc0a2707b31d", "")] +#[test_case("0xbb0249aa04e94c409b4edafe3b0b5473b85fc3a73db9e7105dc18ff567b665b", "")] +#[test_case("0x303dfe6b7f04759adb9c16d9a5f5e5e97ea4e059d7e4a3bc3d2bd466359c9d4b", "")] +#[test_case("0x1cf614533dfc15bcc167f4ffc7853ae15ce0364b51eb9b44df0ce3a081b55fc6", "")] +#[test_case( + "0x10954baebe08defd59e4a9c21c2506a16b41c7529d23674471c9a05371070abf", + "0x1732f7503ba205a5c39430040b2fd3f7f590eee539e01b1d645ca4de7f7ea5af" +)] +#[test_case("0x10f58303b4a4e4484ba1ce4c4a9e7e2df534ed15af0cfb0f283f9175af9fb6e5", "")] +#[test_case("0x7025c6378827384463d968e6bacfec443ec08a194104fd28bf6b247930c9bdd", "")] +#[test_case("0x1057d4959c007fa74d0e30dd6d15fbdaadd9c90279ca639c96d4f10d3af8a231", "")] +#[test_case( + "0xc2b2dcb2d9fe4efacc93808d9eb008d73712dacc9d4e5c2bc7a4a9fd1ff1ac0", + "0xa3ea41649a906bcb181557dc75e7409961e372b32b22901257763b83f96e409" +)] +#[test_case( + "0x28b8345b48442d41b77865f9fadb2e6e738f2e7d7d751d6b28d9e993fe361fd6", + "0x203e0b4abb8bda20274a279082157b465e7ac24a307c31d5470d6423a3d66d59" +)] +#[test_case( + "0x1f21a85e740f062dc35e7b38b7971c9224f17148f61035964d0741e8fa7ef795", + "0xcc4ba641136ca82b8318f2a223be701a30de95619badb5866c37d6349e8f3ab" +)] +#[test_case("0x851d8e29b71af39041cca092d1186d6aaf0e29571707b706f7bea1f6536dea5", "")] +#[test_case("0x22583cc7c94461fe8c221c39d3e3f9c0cf845c46b941f0685eb9d92b948d57b4", "")] +#[test_case( + "0x3677deec6a292db9413192996bef7e54cf765765e76f0442b48d03d78c1b4ca", + "0x14a8e165c8d161b9da152a082e891aac336deb6ef0a107e1372bcfc14a25bf1e" +)] +#[test_case( + "0x2b35222e0028f08e2cbe1b5b57880a5269d10f98d92e4c7ff173cbca5acc3c25", + "0x1254ccfdd7ed6582a08f6f14e202b8426b8401154f201b49c6554439f475d053" +)] +#[test_case( + "0x1a0559b8639f6ca09fbe7de8e573c75b52fcf271f59bb8d42de27e5b1dac5a51", + "0x2b5fe06267f5728b6b07066a6371f717b7a881e06ac81f94e671e13c116f5ba2" +)] +#[test_case("0xbb47c6119cf19d7adde393c20995152d0a4602634d2438998be00e7f8948b06", "")] +#[test_case( + "0x2bdd9b5112703affe898f3dc4467c54029a5e765a2932eae1911cfe9cd8a01fd", + "0x101458bcc903d98b9da338deebb97fa332a3b18eebeeeffca42fc360e8de37a7" +)] +#[test_case( + "0x2fed5842e08f871e084de333470ad5ff7f863cad37927a8e8efd0428a9a532c0", + "0x9ebd892a4e342dd6f4ebb4dd3a1f170192472e9a1f6f8de949f358fc36390ac" +)] +#[test_case( + "0x28af3fa7a519c2b8fc659dae65d3b2b81218da021dad2a1234b3a26b058ef32b", + "0x28a7177a3426b76cca4c9330911dd0c6d03fdc9458bc6f33b0b705aa642bc7a7" +)] +#[test_case("0x26e60dc63ec623b6b26972e6027d684925e7f482cd181eb73aabbf040f3b7ff6", "")] +#[test_case("0x20d6c202cbe710e1086135b2aeae79c613f87fab0ad907224f37e65512401e51", "")] +#[test_case("0x1b8a83401cce664812a7fd33096367451bc4d7a63178c84bc0fcfe7f5e7a866f", "")] +#[test_case( + "0x2420da207121c4043e1d956963a5bf85162ea695b29bfdd3fc6c62c5edf74ec0", + "0x15643f4438f6a5ebd13ee69527a3c2d74e2f6e17f37a107d74787e11c28e04b" +)] +#[test_case("0x7fc07f648a39c0ec90b3d67229e4476cf67f7b9e53e765bd4d42d0cfbb7457e", "")] +#[test_case( + "0x42c25def65eb895a1b0180670625743ac57607f1c405ad9d574474382f35d62", + "0x1d9d4fccdc146e3a621bfb55dbf8abcc07f98b2033e63a92416fdf705c12f7b7" +)] +#[test_case( + "0x101d3fbe5e5b208434fe6cd8b059fb313207f50871135ee6d218b0e2474828fb", + "0x113cf809c4e19b12bccb04341f807d6bd7ac373ef10f42d19788f6cef2a6685a" +)] +#[test_case("0x7947c8aa8cbdabafc8a2850e235cea4975b5a5579998ac2296c9d1e89bbb9de", "")] +#[test_case("0x995594be605fbf8da0939f1141cc5a34200e5e3081b0f0b240684c95e36b4ce", "")] +#[test_case( + "0x285c71f8a0fdf24ffd992a7d052270d1a091d5af3e7adc468b3f5eb2e888f2d4", + "0x276b94e9be42dac2f1311a5983897f69eaca89823559f3017dddff990f67a793" +)] +#[test_case( + "0x21dffeca30717d80192b3aa7a4e783a4eca4505f82d035d784dac171e277a3bf", + "0x2cb0757b9380194e04cd5963fff757de50d03831e4c283c46f7b0aa3d3e65641" +)] +#[test_case( + "0x2b8e383d30dba6b63255e01cea467a612b61fde33259dd090d222d28c0c1d77d", + "0x280980898b96035f9bf42af16aff52612b5e6016109e1aeb1ac525ca2477fce2" +)] +#[test_case( + "0x150625fe25855faaacd55f741003c4a372eaf7abbb3c01552bfd24339a365ea4", + "0x1826aead148f59af79633af3b978acbae66a3f1d4a49c1f1644a7507036d9a77" +)] +#[test_case("0xe98804dee68836ff4b4e42958efe51f4f1f669a9b8a1563f77f4e5805c42f51", "")] +#[test_case("0x84ed2d57e8a1c8ca2bd33b931ac29379667203438af460553519fec8175541a", "")] +#[test_case( + "0x191ec6a5b4fa90143c450666337adae84179dd007da85721b9c9dec27d89e46f", + "0x21b6ca804747058e4494e37f8013d244a343bb7b87f7f98f5577ab72c14b61c3" +)] +#[test_case("0x1ebf595517ff0e4c47793858604b0246b9c1a7677f372e1b7e46fa68dcf02a4d", "")] +#[test_case( + "0x17354a528bd9b24d83e741600566c8ff636dcfd04a8cb14b23eecc6c94857102", + "0x189ca63421d9c8c324d36169775e37d5ead71eb04fce35a1e5cea3bf581ae621" +)] +#[test_case("0x2a0bb756e81294701bc9d286914aeb9dcb5c803f73b04f91245e46be5120ae92", "")] +#[test_case("0x24dc380cd16479bb29e7651c506f23563f8a913b153d7c3a893981e8687bfb43", "")] +#[test_case( + "0x216db64e966475e96833d188725301fa0c04bb6b1d522fa93d161301b719c0bf", + "0x61b0bf3ab67041a1b606340242661a6e33f04f471ec172cf4b573d4129242f5" +)] +#[test_case( + "0x9f3a0b8ae4c51bd58da91095af52e6bef308ae8549b3a31d0358cdac7964976", + "0x1425a6c7ff8a1c54c63724d804a38b5084ab88fc6f01e162b8c78e8f32c5ead2" +)] +#[test_case( + "0x496a014cbfea4384fec7451a84b976ab0c28f76dd3ab3dc210be9b3bc4a7a94", + "0x1922462c88634ed2177c19df71fb80b6e5a5fca6ada05647ee00829a0a9cc482" +)] +#[test_case( + "0x2c751346419501eb3163f0e980a692ac5ff7ae880c23631737aba070d96a5068", + "0x10ec84d1aec1064a6168858d16d61742edb9b343c62af7749c853a13e319cb56" +)] +#[test_case( + "0x5aa5e9173ba1b12d79893accb863af98034dc10bee04a72745627805a96f432", + "0xa44fbe2c2f13907d2d26568156442f252f1499c67d5e89224ec9516efbeec9a" +)] +#[test_case("0xf11b967d4fd453f72ea17c192057163a4b86faeba58ae230cb93bdef0243a2d", "")] +#[test_case("0x17650d85a167378d8ecaed35d6c33f90011b5c1763b3a67edf99374e048e539", "")] +#[test_case("0x4ce9c9935a2727683a3b19bc28c2b1a9e3f629899539e3a3bdf1e438e06af27", "")] +#[test_case( + "0x29f83eef57bb73a548854860b466f8960265aef86ce96b03d3a694c8d1bd432a", + "0x2b6bf3ca49338fba98c6d0568093e7b350dfcf8eec2296d4dd514834a1f325d6" +)] +#[test_case("0x10f39b3b6e56245bcea1c0eaac5dde6f3dd4c245dc723dda85f28f7e8f059af1", "")] +#[test_case("0x802c3e67cb4052e844722a8cdffbc32929468ee86a395e7a381706b47be3ac9", "")] +#[test_case( + "0x1867b07712a6519c3c32c8f6571812ba523d3bce9b20eced5b926ce452b359b6", + "0x2c06e3a418433f6e8a72841772eb8968fba3eff8b6346b4d2d31730f6bda6254" +)] +#[test_case("0x127305af097d76b7ba80afb3b4d648ea1ef18137a6ed87193f79433a135dfcf1", "")] +#[test_case( + "0xf21ccbb974fd751baf632cc4d283d41ef3bf3692831d63c5f127c75b8568373", + "0x13c3f7f4f167ac914f3f1ef9cfe0b0825939eda1612fbdc1e72c30fd16c2cbb0" +)] +#[test_case( + "0x29c06ca72b9dd0e167319520dc58b330b0589fec9940e46b36e7e6d89f2d5f51", + "0x2b863a476d7134bae804560ce9deb21507009fcb38ed7e891da5330e6cebf843" +)] +#[test_case( + "0x24593c874871b77055cd668994aed955b3fd217707fc1e1aa548c46cce8b097", + "0x1334a18f5cee20d4424129d25910fcf6f6df0a110ed190fca6790ca75f0f2a34" +)] +#[test_case("0x1d5f05febf7f601d031491c42106903a5092f00c5768b95d0fea08a5fda111d0", "")] +#[test_case("0x28670112f911193a4fc626adce6ce40c8a4edf239379f69955b38d7e591a12ae", "")] +#[test_case( + "0x24e8ecef3e8905546882f8f34aaaa6936470006ed6d344ea9d9026ab3df5c224", + "0x1f19c628f201c1132c459133f21a2084e11d5f102e0fcf0662093193bffcea86" +)] +#[test_case( + "0x5d1d993e9dee8cd8065df381781ae2a452b54443dc4bd7b25aedf7e3c93b903", + "0x1af0c09e4e5fa57be5d4a00a3f3a1a4ca7a4cc98f02e1d75d7353a4822071b58" +)] +#[test_case("0x195235e8cf1639843590e2c504b32d98a1e6f6c238088a9602f2e01080303c90", "")] +#[test_case("0x19ed8c7dbc98179279c78eb4934e284f65bd8ad2840225e9c7cabe116620430b", "")] +#[test_case( + "0x17f87b57ea3ea75b10a58a5f4f14e77b7a3dc37980f750a1acbdc9f3eb7161a4", + "0x18d3efc9587da0fad2549b9435b3a8dea192a296f00951fa204cae6177e70b99" +)] +#[test_case("0x235bee2c7fb649ce30cd804d98133ab5dfcc793528f7a776f1768a28ce24371", "")] +#[test_case("0x2ad3dfe16d847f078d0ee7a573a41ff13fcd57a30f0c15ab22da5698c572facd", "")] +#[test_case("0x2efa31393facd1c3916012ef31a5d854e6c1d4a9ad2960fc962c804b90d98e59", "")] +#[test_case("0xede9f77d62bb62fbe81b267435fcf72fd0251b2814d10df320f6ea215eed56", "")] +#[test_case("0x1db1fd22a8fc9dbc46a5d4cefe21ec711cfadae76414c4f6ab3cb6cd95738764", "")] +#[test_case( + "0xcf6c703c780e6f72140faa60768188edcaaf7bfc75b68757083888e477a3f28", + "0x2bdea6633e0f2780a80366d9bc91d10a75904c9cd4555d462ef6166747fb629e" +)] +#[test_case( + "0x23f6e9a2d6f32a5255f33846b7611cc5043a7d965a17996fa4c1f3ad0ed56ebc", + "0x2f869f25efedaa551614153f9c706b3da92eeaab22137386e591865d25482306" +)] +#[test_case( + "0x29949070e2f5f3055c3afdeb38955a6d407b30b260b256e151df6208d90c12eb", + "0x1ec27fcc641f0d36c77d4ee49c905a562a5c25fb72f7cd1174b09b3537c0542e" +)] +#[test_case("0x1b8e001f30a427f88bebdb44d5ad768e417f79b6a087f135b26a1c872437fc99", "")] +#[test_case( + "0x16828439e722906f70b29c4837cdff27680ba81e01c7b1f210e558318090eb4d", + "0x2f3a7f5710ff2bc13fd4b2cd3a84a61f6c7bc7ea3a2e125ae89fcfabfd9e737d" +)] +fn matching_y_coordinate_works(x: &str, expected: &str) { + let x = Fq::from_hex_str(x); + let expected = if expected.is_empty() { None } else { Some(Fq::from_hex_str(expected)) }; + + let result = matching_y_coordinate(x); + assert_eq!(result, expected); + + // Ensure that the result is actually a point on `alt_bn128`. + if let Some(y) = result { + let xx = x * x; + let xxx = x * xx; + let xxx_plus_3 = xxx + Fq::from(3); + let yy = y * y; + assert_eq!(yy, xxx_plus_3); + } +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/mod.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/mod.rs new file mode 100644 index 000000000..efb7710b7 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/mod.rs @@ -0,0 +1,50 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(test)] + +use super::*; + +mod decompress_collection_id; +mod decompress_hash; +mod get_collection_id; +mod matching_y_coordinate; +mod pow_magic_number; + +trait FromHexStr { + fn from_hex_str(hex_str: &str) -> Self + where + Self: Sized; +} + +impl FromHexStr for Fq { + fn from_hex_str(hex_str: &str) -> Fq { + let hex_str_sans_prefix = &hex_str[2..]; + + // Pad with zeroes on the left. + let hex_str_padded = format!("{:0>64}", hex_str_sans_prefix); + + let bytes: Vec = (0..hex_str_padded.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex_str_padded[i..i + 2], 16).unwrap()) + .collect(); + + let fixed_bytes: [u8; 32] = bytes.try_into().unwrap(); + + Fq::from_be_bytes_mod_order(&fixed_bytes) + } +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/pow_magic_number.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/pow_magic_number.rs new file mode 100644 index 000000000..65ba7b609 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/decompressor/tests/pow_magic_number.rs @@ -0,0 +1,1098 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +#[test_case("0x0", "0x0")] +#[test_case("0x1", "0x1")] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x279d7bc4e184e3a57f5fa684690c6df6b484a7f1daa1de608d266a2a4be6593f" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000003", + "0x0000000000000000b3c4d79d41a91759a9e4c7e359b6b89eaec68e62effffffd" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000000000000000000000000000002" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000005", + "0x2bbffb7b85b84d517b91517bcc7429cfc7fb18a7d88cfd7641df308c6d1a3517" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x357d8998da08d51735597f5035c7a6c3cc58cca2a1e008b29c700e675c609ab" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000007", + "0x1dada9100531c64cbe18cee1c3fabfe5082f0ce8505483dc5b9d6fd2be57cae1" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x1ed6a916e1d82721466f07525097838fd187e5524cd1f233de2c483dbf4fb537" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000000000000000009", + "0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd44" +)] +#[test_case( + "0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff", + "0x1c078130f1d71a2b85a61f5a04d5b4ff2ee5f874e9bad4d7e5bb79b1be5e892e" +)] +#[test_case( + "0x0000000000000000000000000000000100000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000010000000000000000" +)] +#[test_case( + "0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x05f01ae4b9556bff71e988a7268d8faeddb82ac1f0f472e74f5e05f3c524cbb2" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000000002bacf38d0ee9d", + "0x2bb83c2e6a71464d3072fb62139c9e13a419cf13d5b31b17f3438cdcc2ab5a79" +)] +#[test_case( + "0x0000000000000000000000000000000000000000000000269fa8c16b8066ea69", + "0x0c250f940031ee8f6e14788aaec7bb1bb3cf23fe6ea76db7ee8dea724681c57c" +)] +#[test_case( + "0x000000000000000000000000000000000000000801241bb1f6295e704fba336b", + "0x0228ae1957bfe58548d834b28463d1d98fe69e2de54b873fcf78cd3e9d0fa195" +)] +#[test_case( + "0x00000000000000000000000000000000000000e132f566fa6d16bf5486f5bf6a", + "0x2c0dbf7f0f4afe4421700aa8ed8788757b0b12b1197931b7be00281772c0dc27" +)] +#[test_case( + "0x123456789abcde00000000000000000fffffffffffffffffffffffffffffffff", + "0x21b8fe191fb7d5a2ebf018c8c52f4317a41dddcb1a1ffc4ec9141bbbc97bcc62" +)] +#[test_case( + "0x00fedcbafedcbafedcbafedcbaffffffffffffffffffffffffffffffffffffff", + "0x00b2577cf5861468ac05eb9334b380f22bb78c575cb69061fc10f2358e539a31" +)] +// Generated using Gnosis implementation: +#[test_case( + "0x2354c122221ffc8680c57566ebd1f3970afe06d53facd4117a43aa1def82b557", + "0x1de6fa86b13267005611590911a66f775c303ae545a85e6f66356063836709bb" +)] +#[test_case( + "0x24fedbc02622ac674c98380f0d80ce10c43abe50b2264d16dcd12a0af6c2609d", + "0x1ab93efca782cd854bb5372470201592c1cd2b5f25a471abb2c7175c07792baa" +)] +#[test_case( + "0x75a840f2ef2c84472c42df4f53e095d26e5b4be57078cb000f3eb90667b05a9", + "0x29665bde39f038131074909fce09c0b794b74b1ff9ab189e8c5a4592f23be3d6" +)] +#[test_case( + "0xfacd5657c56f55eed66b72b74980fcc198a2cb23043e8a725001aa0995e5188", + "0x1d5e6d113cecf9a9c0a1896289f5c3483e1f0b5b547cbe7486b2c6b843274da5" +)] +#[test_case( + "0x5fb0c7c63f31999e68e2a2bd01bc7a95d05d558acedf49ffc054d25f78c0cf8", + "0x2bcc1855af3ab7e82dc70e97284736aecba06de8613e9e328a79ae12934ead01" +)] +#[test_case( + "0x1788074e4f196b2f08fd0d2259cc15e85c378a7468002991d018e8962d23af56", + "0x2c531e481bf64ed74c551f2a421c600d49894af8d408f136cc90418d2cc21277" +)] +#[test_case( + "0x9193bffd27dc61c61c79180e65dc7774bfb8282333fe039e864042f175e8afe", + "0x12b2834cf20f27203b810f287db287caa03c9259538a8ba65874866122373e06" +)] +#[test_case( + "0x112bd6c30c4438ae381f0a2a9fad2492b87270c63ff9fb8920937611d3b55cb4", + "0x2b0f81356115396a5a677d12ad18f13b62c626221963c4a1d8b1a93dafe478dc" +)] +#[test_case( + "0x13a26ba3b2d697fc55bbecd3510200468bc0ba0e7637ce13fbf54167121f5f13", + "0x1b409f6679274b016de55448f8ba69b77636cf75581e3cbce2841e7318e178e1" +)] +#[test_case( + "0x2bce28974dcfe586fb6eea5dde6d9efb91ee7ac94d3bab482d0e73136976b7a3", + "0x25539ea969433a9a46f52805de3243572d53a19a6d082e6908c393aa0429ac37" +)] +#[test_case( + "0x21b1257812346ebb0dfcf0a3c212e4619b789713557d76ee5310240be590cf27", + "0x18192eea11af08727c73131ea95b6885e118e687c8aeceb2f8d2b803227c5aee" +)] +#[test_case( + "0x1b0ce6c54d89b9e577c5ac893d12d3cdaf71609a4bae66d06bd95353d7722306", + "0x1b056f18f53791a773f2eec525fc09a3ad3b87963c9b2738809140f8c627f1d4" +)] +#[test_case( + "0x2e2f05a7a5952517e92834acc01cccea2d45e932d9e62433e841bc5574d9cb52", + "0x27c6cbc51000cc53e42e1200873ff5f3079eef6108afb351deb1207844e6a0f8" +)] +#[test_case( + "0x2b483511c5d2445dc876806f56d96c99ac7858013059c7ccea0821bc587b4945", + "0x13bfd6ec70991017d8d7e2ac37a08d505f0b51314509904817c2d7e7b2615938" +)] +#[test_case( + "0xb09f7171c618f1f3debe2cceccb452c5f1785173fa021b1eca6b9598519636a", + "0x9bbefdddae55c42cd4740a8103d8388dafa93cfcc68c1b1a8afdd4d22e5f411" +)] +#[test_case( + "0x684aba84d1f6436011195074e326ca68feae4ef4f4b505211ca13425d3c1b7", + "0x180ee0f41d5cb3e37a1e0d123cb605120c23a98da00739245dd29da73b7bf715" +)] +#[test_case( + "0x2a0dcbb881a771aa81d622ccd5c4cc3e6e2b18b5611e460f48add75df7ffbda2", + "0x6c9d6721042e1ccc0b3840b0c9a16a6373973aeab6ac474c0ef31896291b466" +)] +#[test_case( + "0x22a235b1ffd6a6f26fbbdab352088ad04ac3a90a06a309429b90f619fe47ad63", + "0x2cda39f4076bf3bbe07be17e4d41a2a430d0528ec17e1cb9b5d39930ea477261" +)] +#[test_case( + "0x141dbc7b4620096b85b492fac2df889353e1f0ae021b26b9d3b7c19171e3983c", + "0xe9e2f62e2ee75ee90d3ba561dc6f931b0a836f6a7ecfba9015cc41f91185a6f" +)] +#[test_case( + "0x17e0b43fcba8929d9c4f275c449c2c92195e1f6a364a51a6b5ce80fb3539d6d7", + "0x14f6886d669d73d6cc4ac31b787f55889e64eca1acfa89e3e4cde3c13f620c7f" +)] +#[test_case( + "0x4753eee5c232a1b78698e289da515d5540150f9fc9d5fda0dcc5b5113cffa69", + "0x1e1445559bffe23b8dbec67292850ebfa9135ac3027ea5de29929456083520a3" +)] +#[test_case( + "0x195b5985dc2ae928e1a0d098bb04e0d0750bd17ba249a6fad642b29ef0299ed9", + "0x1afc14672b9df4c141a9b20643434c197fe610bcd95855c8d7b9f193615ce807" +)] +#[test_case( + "0x15510b2b2d41c4d41b9bf7de8ce7453ba6d101ab5b298c83f6b8d21a3ab5fe5d", + "0x30216b11e53f919936fa4d5fca5bec88891f8c59407e7e043f156e7cfaf98ad" +)] +#[test_case( + "0x12358a54316e0412177026f53af2025e7ae28a10f574a1fb2834e1e449f79447", + "0x19f6b4c86f02e3fe5ac1c3f7356ca32e1f45c620a4b649501413018dffcf3cb1" +)] +#[test_case( + "0xcd2b6fbe475e734bbf0e7be0e7b64954b9dd33a571d5d03a4d9f6906afacef1", + "0x2cb10e299812d74d02c8c5f010fd787280f4b131f1233cba91bf9c25bf55a73f" +)] +#[test_case( + "0xd77c37d1367bbefc9a5955344a0e8c38ad03808f327b0ade3fdab5438199578", + "0x1c7f29a1a2d8fa2deeed36b97735e8483c3451da9dc7ffcb1004cad8b49f1861" +)] +#[test_case( + "0x23f0a43fba6dca923fa3081b857e4dd4c4055fc9a9a0c6a876049e2af998159f", + "0x4b4763b6047cf4930311921461ced618942c083cb81d198d5d3c6169d7e22fd" +)] +#[test_case( + "0x21b2b431ff068e1e30a05f9a2570a89469f334e4d5dc09bd41fe156c4f40df6c", + "0x1b61353cc37c1db2ff1cf5c0079e4648060ca8e5077bfb3123d2d7418624b046" +)] +#[test_case( + "0x6de94f99eb1da34ead03e09a0b72bb6e2881bd51dc96dc92b0891bc41b4b109", + "0xe294983fadc22f4e10612fc9d2d8c9b9b22ffef2986fc008764ba44c4d2ad18" +)] +#[test_case( + "0x2f17bf634f48c738caba4d42e4cc2ec3334ae98b8a2c82524f2dfd17a70620c8", + "0x299c9930426810ef78894f2ee8a47440463f3db583a745bc9a4e941b9207dd31" +)] +#[test_case( + "0x241b59c2e4d782f3afa89b28b336bfe2b0848d570d300b1c41fd0ef4d3054214", + "0x97eb9b64afe8299382fc790ed2d253ceb19463474bd01ca5b50095fcd33eab7" +)] +#[test_case( + "0x2ba18996e25bc6066756c7f9a388c1a746193b12fd56e39231395e5d96d06816", + "0xc7aee951b9b0a5c887aef547ffaec5a4e272ba627f44781c7173b1f89ef1ed7" +)] +#[test_case( + "0x2e2f2bcb14958b66d51d0ad8e30d83245f08972dbe72f8d759645113150a5d43", + "0x12136e3b37eade27d694e8bb0e31b6a563f074844375b273d70e823d990c4e63" +)] +#[test_case( + "0x1e51edbf2f604da89d2ba200d6a3872f73cc828dc8b642a61e0397990f086fc9", + "0x2bd93682d1122c1fcedbfe4403dd6f0b3488fd746b3231359f86e897c55a58d1" +)] +#[test_case( + "0x1608a8e8353015ca9afd7b4f4671db355ab64b56dbee919968d4d99c8da8dc59", + "0x112127e6f781c2abd9ff0e1b62712b16566d9b90506a5e5c68df494c552a599c" +)] +#[test_case( + "0x20fe1cb977b943d0541fdc5a5bf136f3db7011c7bbf36fe6e2f2c68d1dc50e67", + "0x23103fcadbcde4feeab37808c6c9c548e4a4320d25d7c3e14129e2d03b9a8fc9" +)] +#[test_case( + "0x1055689b55c0874a419dd4118b6adada3c93ab1609c7aa902e437840e798743e", + "0x1e52df64c4d282d9c7bbd7e6822bb3beb62776d58b317ad30137eec0a3febbb9" +)] +#[test_case( + "0x233e59a9b7b15e97edd03cb365fbd860df1bf9c2204f79b75298c43022605a37", + "0x14c976744a666aef10ef39e1dd873910789fe651f42a8398104a513dd37e3269" +)] +#[test_case( + "0x1cfa85789a2c8e93bd34d5f387e0ecaf3108c8a121cc986b361155ce2ac11cb0", + "0x277374b1c47704a063aa891969b41ac40241aac52ee9c4cf6b41770a7e2c77c9" +)] +#[test_case( + "0x196851b066fdbb555b5b81175f712805dede8449be697726f325a6fad8d5fddd", + "0x2b131a525f9630b24c2f9884b98ba541fbfecaec2499c9c57987b543543f0c21" +)] +#[test_case( + "0x2b07f1d0b3be425b6e54085c2a36170cd4692a68d30f071f32cc7a73234f0545", + "0x255085e9274e407b70d0c7fa163f2ab38b6a4cd55ec56ceb7286ea63c8991de3" +)] +#[test_case( + "0x2ab94649a62976a445688157861373a3774d144203362decc26feb65286541fa", + "0xc7cd7f76e45f7126649d448d70c7bd1db92d64dd431db6c333e89abdec4ae13" +)] +#[test_case( + "0x239136e344ae444cee3dac47937e8c275ef0d97d2659d17f4fbeb86c89ea4b75", + "0x158fd2a72745452819cf3b28ae52b3e17b8a4a24ed6d148fd04ae8ce424a0ea1" +)] +#[test_case( + "0x241e0cd8d249155b10c485b5dc0b07ac6c4b96af18ff8b88c005088d8f8f764c", + "0x23df5bcf5e7414159bf6397b93d5567ec2ae6d8245681e110527d531340f4322" +)] +#[test_case( + "0x19f25f19ce5e03e807cb86dc5166e9bb6ad3cde094402a2e03f18bb15793b421", + "0x1b1c24ee4dbdb2eca6cc36eb7aa8abd92ad4abd5ebb5d09ce543c492b435af04" +)] +#[test_case( + "0x139628f946e8e6ae364c76444ece6c5903c2a79d7b647a1e4563cad636f5b793", + "0x2b452dc3f48526d93f79364ed1fac59bb6908ddd149a51b40aff6aae2b55849" +)] +#[test_case( + "0x2b4405636d4ba139c8fb6f2ea9625282ed0731d7cb7de9a6019216251d96dd78", + "0x153224e84078d2805ac8acc14147a0670913c6bf9cfa5033bce10270f89feea8" +)] +#[test_case( + "0x209f53a57dbe9c5e7771737a6bea2f390df00ee9fdc60730b419139f9ffa3d04", + "0x15ed703c5e68539f26c73c28d98e14b87539f3ca5196b98cde2ab5fde6640639" +)] +#[test_case( + "0xd491ca9d59671992cbb519471940cc5d3bbeed8a2110a611649882589c4c774", + "0x1085ea5b92511a510a20c03a7974c08d822e9c42282dc5b024c12c0e8dd77d5f" +)] +#[test_case( + "0x15f0d0433d24a54402fb8cb84a23afb09501c829c59ce3a5660a57f175bdec34", + "0x1bf1b7bf37e25c8e1608bf5a51788a8883ada2d13d856507126b820ba7faf7a1" +)] +#[test_case( + "0x1616cc74f6732135f096bb3152c31f1fd255ebb2cec28aa5dce33a3089d6fbc8", + "0xc78bb843de8b7f3bfb5d2371c5b7a209392f9122406b2c4ca843062e5b9a840" +)] +#[test_case( + "0x185d82402cc3600275d4606f9aa0bc9b35845ade09f104c5c39382550b58eb24", + "0xc423b24d395eced3de2148bafe2f7898f3647ec17ff677ba0cef7aa6520a050" +)] +#[test_case( + "0x387ef556dccfbca8df28cf43e3b59bfe2b9e2935ee8b406f2cc075dd631eee6", + "0x152af2790bc93c155ac2598a57c8813bc28a05a29e5aee1247d7acdba73c862c" +)] +#[test_case( + "0x2bd06d520de42ec02fba49e235dca1778f512dfca5922de360a88673b21493b6", + "0x1ae0c86b7b3702566e356ac4d689bf8bb0e1afa9970961a0d6b2ef63eebb7574" +)] +#[test_case( + "0x176966499fd973f0d3d0366467e6e488730930fb43890aa8b177702f3a0b05f2", + "0x2cae575e00b58a74f3cb81e4223b175e09a7a66dfebae6d1be2a145d5eca4d21" +)] +#[test_case( + "0xe0a1b86632a7a010880754e792a1b655badadfded7971485e359c1157ee3565", + "0x11fe1c510d7a6144419b9c81b4d7039df681b97fcacb1cf4eaf4bd2e1e03198f" +)] +#[test_case( + "0x18a751dbd45c179b53f48bec1f3680ec4edc4c8fefd9b1934129724c25ef00f3", + "0x4cccf662fe8f4a260f178cfa8119870b93279ba323ee2f2dbf004376f3113a0" +)] +#[test_case( + "0x294cc553274f4c1147b277763e14ded5ccf337c6438fde9e69c2c767a51b10e7", + "0x27c095c3b1fca003c634c8933718911c0440d41565ded28ad6d38c6a8daa268" +)] +#[test_case( + "0x6ef20946ebada23d09c7167b95edd86ed8eee029927ce0f581dabd9b7645859", + "0x61b0c49ddfd7f064f4729131a4dbdfd4568283bc4fef8acde2721991c4e3f3f" +)] +#[test_case( + "0x9b205a3a97f2c638aa2d2ec9b0db6626a795d091e129a37c196fbce1ca7ad1f", + "0x9b7815c258d4b8035772e030b841ac131f32012c934488a5ea8d906152d4c50" +)] +#[test_case( + "0x502f9865bf63391ae24b8e9f3916c907a6b602d53be9c7ffbcd5d6fac35bfe3", + "0xd39cf062200d8b13abce798151a48e90b97267cf9d82f0a215160d2f2f960f0" +)] +#[test_case( + "0xccf4f0a11f6fe5ed3ffa674a2ccbd2caa733ed990b6dea5a6cf9aedcfe6215b", + "0x29d076ac9f5e6602c1ec3211b2beb0c31afb903b42e505ccba90ea255ec13a86" +)] +#[test_case( + "0x147f26d61f84fd5b9788fb08e6ab806c14405687a10539651f4901cf5043b69d", + "0x2e0f2541f0450db6f69006c3b44287941338800eaf087e4ce89ade713fbb8707" +)] +#[test_case( + "0x2483d51b769934debb3a943d85e547359727728ae0347cbf787e7af2448fb5aa", + "0x2541be86692f6efd2ebbf5a958959aaa0cb71f854405ca74d84b1fd8d4b439cf" +)] +#[test_case( + "0x1e33f31e367e4e7c4dc712985e0a54c4b7de742dd4e3cd316a09c71023f9d597", + "0x257b621ee15812e62bd9b8b0b85e3aa62b377e3871840312718fe1f3c90dcaf" +)] +#[test_case( + "0x1e359f812824489acb1cba316dbfcef3d2b7751710adf91179b086b9cea631fe", + "0x2245438b1cb85c8e0bd9612aacd2e80145333885819ac342ef5b938e75394bf5" +)] +#[test_case( + "0x13f408afe13c0224c8b147363cd8536d65cf475f7f6c437061e71418543e8cc3", + "0x68c85f58bd05d929e83fb9046fdfdbeaf87b507400df5fae0a569fb78c2c90d" +)] +#[test_case( + "0x1fa5471cba8562ba144f3d5be81aa7382c45c1de82c1ef93d1b4a8f557b2810d", + "0x2ddce2470c756ab1633f3a676e2a5145d5e262b075396c48be3c354243a10544" +)] +#[test_case( + "0x2bacd1057e935b7536d34b9f068dbd77266192f8854f92575fbf2f5a394d741c", + "0x1d4b3a1aebfcbd89f70acf083e1e267226038076614bb5bfa0641a8fb8a7ff50" +)] +#[test_case( + "0x53c791f801f297d5c03099659a5e4716a3dda9157d230c82a65eab1322c8992", + "0x15400ea5393513ac718c0a6c0fa615a4b862bc601aab1a79f17c25dd7628f072" +)] +#[test_case( + "0x13c9d53ffb115abb64a72f82f56127f7700eb52cdc7a5b83e9f5e5b848a56059", + "0x2fbe129483b47b0d60109830ba641b7c62841f898bf1b6f1ca2a8e64fc61c041" +)] +#[test_case( + "0x227e0f3e939bce0986024c953faebcaf144e5269d7bf9217351a429b5f688b1e", + "0x2939f37529d6282e8ffdb72874645618d13c2b61962570a4a59748a33eb52544" +)] +#[test_case( + "0x2d15888ecfa89595f82f096ceb01bd7196022f0e8c5d00b60f630300836fef37", + "0x2c2b04c58c6bf23752caebf5cf023c9ff2fb56e185ffeb005b48f9aba74b42f2" +)] +#[test_case( + "0x21441a377ae74dfbe448db5a3daf0f0a8d0f6897646b1c07869083fa4e6671c7", + "0x1ce46c42247a833720505d4138b198cafda1e624bd34a105445373451884c6f6" +)] +#[test_case( + "0x67965676a05d25adc2956f33859e87b1e244f7e3204125125f1ffb8efbd03c0", + "0x3f3fa9ba0f6a2bbb83ed096683279c6fc70182c6470bb1e5f5645ebeded5cfd" +)] +#[test_case( + "0x83f16198f036f472d6750ef862b5ac34ac4473bd056415e3568da9d730114e4", + "0x11e53e97c5ff41c6c6f981e5f59a7335799f83d7a71a5fb746ac0d4b579c7f5b" +)] +#[test_case( + "0x1cc9fdde2b75006a9abf8457a189c5f79afdab1c6ce63137575e1b74a0508995", + "0x3bc70435799feecdc5e7b5c2d1dd46ae076e626955b95802f69454e029429d7" +)] +#[test_case( + "0x2d246f374692631611eb9a02ee4907070d28887837845257e170a030c716bd13", + "0x193d617dfcd6392625da7bae5f313a5910a8b577e1abb9338c18614ef938931a" +)] +#[test_case( + "0x262b94eb71256dd7ec1e4b2b97ee562d4b977a7a7e5ebfe7cb9c41a3947efcb8", + "0x2f97463b4f05c3ab7f93cc98a2135530ad3a55beb5d6ee8e2846c44ebb182812" +)] +#[test_case( + "0x1491001ad8736a24a82bb64075c042023cb7ba9d2d709a84147dc4ef0bef7a20", + "0xb26973f748d8372ae5185a7a76ac9ea488dbb1366892aa1bd716260258540a9" +)] +#[test_case( + "0x2665f121fa2b054d5a5f4c0929cbffd4f8c008f88910d41a6e418166e7ed882f", + "0x25f3c1bae6c0def1650c8e8647cd768486100a513aab42b5ec90895aa660d1d" +)] +#[test_case( + "0x266f2a8e026378f15e398d030b4f556e00c0479b2b13a056ccdaa068111aa718", + "0x2339aeaf432f0a2e82848f69db7800014e68864ca9f13ad6ab1c26a634885304" +)] +#[test_case( + "0x14b78c618d1fd090d251b7abd7f7acfdecd668c48ef5e64d9c59d74ebedee960", + "0x18dc3dc2626c7bbeb733ef7a87149aa1143afd9cbc5ecbcda6ea4914beb9586d" +)] +#[test_case( + "0x233776dbd1b0ae61b72abb3cdf438c041dca957eb6dc25f7ce568f4f5acac14d", + "0x1cea4fd035651a82a4c3353d538b4310452949fbfbbf1057cdea4aa74661e4a9" +)] +#[test_case( + "0x277c5b62ead9931e1fa5f2cfeaaa77281451f89912df125d239ada32aa11f63e", + "0x16feb53a2ae546c9a663cff07e2b5ccd5c69a164df39b9620f10f4353279cd5c" +)] +#[test_case( + "0x16e9a9ec7ace195477fb7a08376a4f586811dd5ed1415f2926e4f3c78ebbe731", + "0x2669afac1014727ca473e0d88a0e1b1ea6ccb357f4f3949048ba7f22c81c14a4" +)] +#[test_case( + "0x2519f5da57a3f30043801ebe62115213cb6cdcef84d747ebb93628f2698d50fb", + "0x2e8ad65f5845ce324803a66e4f2110a60b0dfc014272a5097b61ca58ccf519ea" +)] +#[test_case( + "0x1aa1c72105370ecb28eebc32eca71a19104765eeb6f711cf5235eb87692de0d4", + "0x191acf349969702acdb8e963beb15653d7371ec34a02590f65d4c635487540" +)] +#[test_case( + "0x2a2bc990b5bf71485a4a8dee0683d123f5f71e77623ca07a30ce1e2c2676b167", + "0xf4f32804774aef11a3749dac93e5f5141531bb30b9d8b8d3912cb05b09dbbe9" +)] +#[test_case( + "0xe3c9c824a566e3432c02c284fb2f8363db4676cefae2e6355b24996908d2b8d", + "0x1b3fe8d1eab6c222c54d8ff4b958ba008de1eea7e319c3249fe55ca068228270" +)] +#[test_case( + "0x20c2d4a3019f45085e549daebad566d9ee4a4a429d1a928a244aebfa4233d5da", + "0x132582146ed242a554750ee95788acbc2c3172594f3e834b033b8e3c17540c05" +)] +#[test_case( + "0x2fdbf1efee35c9c7d94962251ccb9604b7ce53d89b144f0c5729ce618642ca94", + "0x10ba762fe7f91fa7e1476a22e56771e22c9320c5b21ed1591db78a630604e9f4" +)] +#[test_case( + "0x189352877c36efc33ae7c8f2ea974cd280a5cab22d5b14fc976d4115babcc09a", + "0x7af3cdfd3925232d2298f65706cb4e362d8ca5ea8b7f860b6bcd04d9373d938" +)] +#[test_case( + "0x206ba61317fb7ecc200706e5c5ef40d5422258e13728c0405ebe59137359ab3a", + "0x1338285e79deddc57e25bb8b69f0c71a993e81690fc25505a2a4957fbb1c62d5" +)] +#[test_case( + "0xbf70e0026bd8c03411aff5786b34da4079bddc3da0e957a32f27422ef3bed53", + "0x140d99cb25901d245ab6182665981db23f87140b578af38a4805ee43571ab347" +)] +#[test_case( + "0x15b3a542d0cc7cef48ed98178f2bb9540b663afaa3863e1258e3b9c3900bdbef", + "0x1a3e34051be5649640f1d6658c78f2ad219d5b82155b185da0d71663858e4f59" +)] +#[test_case( + "0x84b12f410c77939d561102b2b25decac4bd6b4c1caa0d93a212ffb7b26ca4d1", + "0x28d08e65568558d8d0edc3750ff4d8bebed2f022a286251ed87902744eb1455e" +)] +#[test_case( + "0x186384bd9f5ba026ac6fae1bf15ee4bd88268751941f9dfeefab8890080da767", + "0x101f9e607da2d440390faab2e58ecaa35497c61c410d44a6b3cd9292f912a7c5" +)] +#[test_case( + "0x362c451cb3b9d33292eb061e801c5a98d50613a67eb5b3b273742d81dad9273", + "0x1f115bd90d72204281efaac2afc5bfd4c5efe572267b13adc7e495640dc61c20" +)] +#[test_case( + "0x1349e6cc590e4954fc551e76d93ec5d6f342b5d7a9195db47ee63069dbf74b2d", + "0xdb453e6389b7e43027d0bf4c45b114bab8615d74860f97bb05271f5d0b5a2f7" +)] +#[test_case( + "0x10e2c61d7e369db368a05f02a2cda93c9c5e9a330f084a628cb1fb53c6d255db", + "0xfb1b229496d73280c1c156ccad9c10ebfeaf1d62253b01d58fd60bb2f68caad" +)] +#[test_case( + "0x1500111ac021fd81246012e138b9eda8aa616939892df222a7e2cfa9826ccd9a", + "0x1778361bc74efeb1525c906ab0fe8a1e3389b9334a38364ac88fbe8ce2df14f2" +)] +#[test_case( + "0x2501fcc7cf66f7855b80a99f61a1aaf4da79f15e733b5c162aae779cd899f7fe", + "0x1ee83d1fd40c18ca194aad652d0ccc646c8fdba07f96a8a3dc8811c60f5856d" +)] +#[test_case( + "0x22f86b046a3061ec416739cd769d1ee88528134d7efff80b380c3c75d965cd7b", + "0x2ede9051c70b409f9b161331c78de40f976a76e60b7ff762b6f0aa874af71979" +)] +#[test_case( + "0x257b9a3b85164b570a3acf910ee5ed3a42723605c7a3a20440f6ede770e19633", + "0x1d9b106156c3279a618931fbddc913e391e8caf2d818c4aa914b35bd88bd19aa" +)] +#[test_case( + "0x300bf0e4501d17fbc102d3c3e70fed3234d048f7648616e49b4b23ef3cb4d51c", + "0xd498e81ca2577112e6f6828ea8cdc3ca354e282224827349b76f1dd1bbefe64" +)] +#[test_case( + "0x14cfaf7e94abf742481f345124713140d4d1342ae8a962baa3b7f65547ec6924", + "0x2889f002b35b78413c8441379aecefc0c3ff55905bc2d719cb3518a079bbe4ff" +)] +#[test_case( + "0x16a851fa9cb2a27bb2b5bf453ca0becce53162bbfc263677f8376e8c2518b225", + "0x14d2a53d0871577ae830d08d38b66209b72891becef5e1ed23b016916eca5ad6" +)] +#[test_case( + "0x21de9280f09d946498268ae02b8d8d81bee8903b591b92cc3def80aedf90db09", + "0x132c4d2fa3356956754f4f1247c856bfe76857329a317f663217565a7bd90334" +)] +#[test_case( + "0x16a09bad60267721122fe1a9d729d9baa7706c9f61fbef5a4d6f5ee36ab4ef13", + "0xcfe2414cef63b161d8139fa85f26a86cb2a00db95415bdfcab29a0211b5356e" +)] +#[test_case( + "0x7b4de2295293811d707f2190c63f72391200355d62dbd59d426b627842d8cfb", + "0x3f0380d3101465ff3b5143a9d3b93481a74751e737a0a79e1275fa2f6695591" +)] +#[test_case( + "0x13bf8783d8204af58937db2e397c5ddb61e6caa7b84420fffeb7e759e4467323", + "0x7057404a9219fbd71ef1b308731c0269e601eae01652305c0f3e7aa9efecc2c" +)] +#[test_case( + "0x993e08d75da08185411f995c20e16745ea379dfd4185f73caf434cf650f5727", + "0x1f7559dd91a5c9b45a4dc73240815e2b4ee6e6fd824dfd67b6d743c81fe1e27d" +)] +#[test_case( + "0x2535ee3ca4609c3f34c03e65976dde76028ebda7d2a1c9f3c7bf7703e03ec958", + "0xf78b186166bc33c2d1123138474ad59dc6ff68d3af40c06eddd3a0664a983df" +)] +#[test_case( + "0x136554371983ef155879777e38b94fa8d1760dd98cfd7107fd2c85edcb2cd405", + "0x72cce4cb9def52526b490d338d80c0603778409b5bd95513b2341e2744af1da" +)] +#[test_case( + "0x4ddb9c2c28b29dca5d89fd38d9fde22e416a8bb9f6b3815cbdd62b226ea6f3f", + "0x2893e6637a84cd1355d8ed026877d582a1734b2798eb90c2b2245ca7c4919996" +)] +#[test_case( + "0x228f58ab1cab846c87211fffb92053ef2c7efb0406f8e0078ca56794c8c7bbd7", + "0x162da3d4f6f9bf7a7de6d4100bacb6d9cf26e581a542d289e5048e153a478842" +)] +#[test_case( + "0x298a47d0e457aef872496d0a6058a20c0d0de976f4abcfeaf77c7ba62a6c85f0", + "0x97ed3849361a48adeeda2b1b965f766888bd5ea86e2bd2c1557d9d6112cc9ef" +)] +#[test_case( + "0x23461d9d06998da33f825a43a9d9404683276247982cd246804a95541e4b43ec", + "0x1ae2b0c60cbad700eb49676a1c168426a100fc3f513c56014abcf2e3738cc3c6" +)] +#[test_case( + "0xcc026e32762deb9eb877f8a685f8e98436374bb63f890bb68726dc0fd55c7c5", + "0x159fb466e83311f0beebde81342ee8053ded5b305e2ed7b52137dbcdecd6ba9c" +)] +#[test_case( + "0x26fc751063de000a640f48ed75faa36f28ea42602d10b8bf929cbb2b31637e28", + "0x110d7550473d227718817190b68e5782f7cecf94884bddedff5bfa3ba73c33fc" +)] +#[test_case( + "0x25d5773bc558d40a75f23b7a5599b7ac6fd00baa41e6e17a6d860621d0aac597", + "0x29421c8b0b99e5fecb154e2c297aba0b622e775912c4ac23e008c7afe42db0a1" +)] +#[test_case( + "0xccdbd8a83a7e3345d6dd502b4977afc50195b586a2eab7e5c8acd96b77af1b8", + "0xc862bfc4d21cd661a2cceff3d9f07a375d264b8586abb89e2e174fbbccf022b" +)] +#[test_case( + "0x8e132b417a8197928d55e603e7d43103aae1dab03e386a816ef813caa44f58d", + "0x9bc7fae5a35ef284c1aa104fe83f895521a47f26046fb0217629afd42d8c5b1" +)] +#[test_case( + "0xaa22fabd917e5a0de47b98167f65117a9e445f2dd15b2f6207a832d3aeac1d9", + "0x2180f9dbc447a10449a847221daaaa3078bbacb3157d2068d9acfe478c205e15" +)] +#[test_case( + "0x1bcf716ba5418321de4bc2729833b9a6eca98cac0f66a66748f8dc95ac847425", + "0x252498df4065a1ecab39b386a73f5ae77b0099408c9e5178cd45f0a88f140f46" +)] +#[test_case( + "0x24bc31040e89ac8640f77510f7aabed0f62c33f7e174cf23a8e5c125b09e5ce7", + "0x26ff5d9eec53a8147791d60dec7334f0e15c0c1562bd39a5507b25a55fdc1117" +)] +#[test_case( + "0x47bd7a63bc81d5630fb6947a7a997b8f71810b21b76246553fb8abae24ceda0", + "0x86b905261721192ae90fcb738ede465b021e6f21e131648213b9c18e5f27a4d" +)] +#[test_case( + "0x216a2d2f7c18050abaf32c9a8d0c6e6ebd37d30591dbeabba69f19558be37fd9", + "0x229782d104cd55b2255bc599d7903eea5ee45f2a6361d8721487eb8345e0e495" +)] +#[test_case( + "0x82604fa7c06b4e38e62c5e7eeb23728c4abe4ffdedb56d2a1c0072cca87f5c8", + "0xbaa0693f0ade943fc7ba01deb817c6b7388abc36fcea8996f0da5e737b4c17a" +)] +#[test_case( + "0x2a4bb9a545b055e62e92b6d00169dc4561eefc34c6cdc287bb6ed139fe180b89", + "0x8116d95da4da06976f4ed7acd0a84b19f45e9be22f13a9cb334c3b4fa5e26e" +)] +#[test_case( + "0x22160fe0f41fc35a7bcd52616c3e618d2b3070ea58805938250cadd9d02b2c8f", + "0x7983df4fcc4b04301be325ceefad2ef40ccf1620f551881e1cbe3d512376fba" +)] +#[test_case( + "0x2c133130b0190187a11ba3e5fa520aaadf5ebb470aab4d4e386b007520ab50f", + "0x20260047fa8d1fad6f5e75d575e4c7d9861fd38d2fa9f3da7d3b1cdf73fffedf" +)] +#[test_case( + "0x22315f658108887f29c99f4876b62b0d40a6fa3014274db2d04831ca36179f15", + "0x202a1b12731bb44996d4223e623a0507e0c2f91140bc0655454c2e87f594b1fa" +)] +#[test_case( + "0x2c4d0bacc44d53bebc8f0429f39708adfd43bf145155705721be0de3460f94bf", + "0x57a992098a1e36d340b17d33fb89577108adb827d571f9c6d9824a6923217d7" +)] +#[test_case( + "0x11849bca64f56566527fe827288dc6d3540e7e929efc5e4d7eafd509f095305d", + "0x13813f285b8214856bc99fbeaa441c6838be9f4e5fb6971edf0b22364395b7a" +)] +#[test_case( + "0x18ca311fd3804a43a56ce7fa94fc7052e1d56437314dcdb03b3151c7c86331e3", + "0x1d7a1d018f905cd4212c8d6456eba53f7b4631eae6ac5f55051e16e11789afc0" +)] +#[test_case( + "0x15748ac4dbe0a797ab1eac7959e1cfeb804d04ed3ed8bc9accacb643254db99d", + "0x5c863ddf01e2e60916fa54489a3f7f90fd8dac5a39fcb07cdaf293756cfa8f" +)] +#[test_case( + "0x27a611ee7aac4b7fdd20edeb5878948d4c609315b9e33daecbd89356db8d0a27", + "0x403f5943ef58f9eb2de53a52173b4a91d24c90c3fb9cd55a554264d0d1ee874" +)] +#[test_case( + "0x2d30e8e7214f50b604fb598c1efaa457d9db73de8d4cf5c7857265dcbfa581da", + "0x1576cfeabe6034354bcb1cb32dc405599a803571a1a66bb5e15516c2c78e6606" +)] +#[test_case( + "0x20711e31b7b5bc43d5ca145b9cfeb0be30c41edd881b1d833f2e54ee0d4be0c1", + "0x1e8e340529e0d63c1b0a997e59b82bad4a94ff335b5a5f8a5a3b213014e924b2" +)] +#[test_case( + "0x18c287dc14fe88b0adbef8d1ef2d58d422d0fab8de961340ceb7299ad7fe636a", + "0x1af4d9c17247b56835cf26e29827ad91d1938f1605c5ceb3da3e6caf12ea30d9" +)] +#[test_case( + "0xc2cc6f790940d65de04675d511459d0bfdad3a43d9a4c4677e381315c1452ec", + "0x11288e1e3b0221d1aab000997c79f01eff73965e6fa18386b4b0bc265b2b7d7b" +)] +#[test_case( + "0x19b3db61f6f3630bae4dc6b48015c8c5f467ea3419711bf0b5aad428f0a16802", + "0x1fcc156cd904fc210f7b78a942e12e5fc3119dcd2a3dd82b498cbb1cd73089e" +)] +#[test_case( + "0x21c92e918870e59b6987a256d184f010dd02adcbe2142b6ed4243019b7b9ab30", + "0x1316f508bfcb400cd688f70dc30fce11e54f22bed5c31ea40c91800cc44ebde8" +)] +#[test_case( + "0x624af0698ca0f02b1a2ed852b238e8b979ee621b223afca4f945aa569657167", + "0x1ca5e2f0e4295e0149e5ba772121e9ef3e2fe0ac83f24b4aa8b471b40a9019fb" +)] +#[test_case( + "0x22a5e74c6f717869bec28f8eac0e2f0c1f2fc5a1d67e797e2d89e2568710b94c", + "0x2283ee08f3361ac672bcb978193c158f2af5169532cb3a612fc3e16ba070c077" +)] +#[test_case( + "0x22cbd852b1135aed92cc6ccffa5aa346a0cebad916cf6fc93407f799c7f0b51", + "0x1e48d46617abbe1221b75ebf92d02715bc7a5933239be0e1c154ffd0a2d6ad2f" +)] +#[test_case( + "0x1b4ce54781392eeaed00d156eb301df99aed84044022acda6ef3880971f785f6", + "0x22116f6d5a5056ac8d6a94b0944dd5b6be597c3def921d89069d21b5bd567e63" +)] +#[test_case( + "0x195dad8e80a40291befcc391d6d90d96ac5da23517bf785bd43f9a694d0ace34", + "0x10fa4dfb4cbf444f0cc15afbfe546e3da23ba9b31ed189f5bb788b2d2406abc8" +)] +#[test_case( + "0x1b079c1ec1929783ed16ffbe1b3ce3cf799317cf50b9ccf4c7341c50ec003461", + "0x1011ada74bb36a88828d810855f95725400d5c1f96bd16c234dc993a80b55f8b" +)] +#[test_case( + "0xa79cdb7dadb55b1b2c86dabe430652ce2322793aa1ad3b8fbddc455b7b5bd48", + "0x277c82ed5154c26d4b5005c0167a8824f4c4f78bd3ad73daf818f14551e5e235" +)] +#[test_case( + "0x22b1ce3c072298900163dc4c8ef37c8abf6045bd8eac512ee84973723a889413", + "0x18b349fa56cd37819656b1ca9870f3c5395c60dd2ff30812b02e865b88e1101d" +)] +#[test_case( + "0x2d0c639d5a35a1391dc74e6f33152c898e7a963eaced5ebe7bc1ba283b6220a6", + "0x1441b4908bea213aee6d1a8d798f6bf7392ee0c854faa12d71c918ae2c68c917" +)] +#[test_case( + "0x2c55a5feedc228b663424fa4779406f58f90e5e0c378b14a01e0a5f99ecc3768", + "0x229aaa49d261a890a9408edf4e53f8a807d107c82720d5af9c447adbd3e93df8" +)] +#[test_case( + "0x2afcf0984f35aa2c805cdd1dbfa56c9fa869a32325115e633d855a2c4dc7bc7a", + "0x22afc3e5832384badabfe1379d664ef0ce42504f1eebf4652562c12ff5fcd257" +)] +#[test_case( + "0x2bd3c2c2b03cbb1bb9ea88c701e5331850345cdac3943ea4539fe18019cbcb5a", + "0x1bbd5402cc56aa14b613632c83b44e17f32ddc90cb491c4fe36fc3db693870e3" +)] +#[test_case( + "0x22b98fd8367db019f6e709e9761546868e1bd1b939cb37207ad7d2f4e61af6f3", + "0x158196138d686cb065500e993011921b5a5e6b205aba5a1a917b61c8e6b61cfd" +)] +#[test_case( + "0x2a00ef95a6225d666a5d0589b6ae18d3da939728ac6db46927f710a5902f46c1", + "0x1b9d6226897f1ea4699b28dda521da10c0d1f928ba596ad25760dc701c2c4bf4" +)] +#[test_case( + "0xaba72bc0da80375377698c6ab06913f5a1142eff7567e05f8de904f05399e2a", + "0x25dc3060f58c0ce8180ccabf0853a82fdd59435859ca08a9bf8f442931b7c053" +)] +#[test_case( + "0x17214d772a126a4bbe0939b3745d0741c333a1492004301411bae7a8e2ca5ef7", + "0x208f48cf1b85082d8af1244483cf65bc3dd4a59f0090870be8892606b3dfad82" +)] +#[test_case( + "0x650adf2d70e0d7b6c9da45b62125447e93f5af95674ac637b766f3a2d7e3b61", + "0x11bf807322cbcf29edde405d0d5674b15e17c0abd160049d9b32b8bc3ff8aff7" +)] +#[test_case( + "0x13782e4510c3427a26c6b1275e087148048bf7c630fa008de9aa579bac954dcc", + "0x82821d57bf038dce7610450ddb72115b0371abd588dc00e85c49f09026e2553" +)] +#[test_case( + "0x268b3c2f9b1c3f9fb23dfb7e3e8562170ecfd7b8b22cf37711cf82a5a3a1256b", + "0x38495a00c1ea10a49f6347a1649c1f44e8000be59146c2d01da0fd6a699d48c" +)] +#[test_case( + "0x1ff54deb17fed0d2aabd02bf5f96bd1c43c8a149e68ccc4f58c09fb1c45a39a5", + "0xe7ba15a7898cb0b1aa319c2aca08a17e3cf3a2975298bb9e18a51e3e25192be" +)] +#[test_case( + "0x1c030143aa7bedb029228a35e42e35e750b97601d674bd9f40595d8652cac93d", + "0x1dda16d607c3571724edf8d8ac519fdd27c8e2fda0fa3f1e975b1d965589011f" +)] +#[test_case( + "0x2239d7a1669c6bb9371b76d4eb5da03cf57b0916f6aa88e0646fecdc30deedd4", + "0x15f84eb11e496cab754210bde053404f5b9117ca8c4046924d24969c29693519" +)] +#[test_case( + "0xd6d12c3ae70dabba3da219d3e917b1cda174ee015cff2cb1a89991dcee456a6", + "0x18dea13e72aed477445ee1649c506306aea918b1f05c572248827fed23db6c7b" +)] +#[test_case( + "0x38de326bedaac59a687789272a28fff3d110f20c037306f7232791c463b90ad", + "0x1dbeff41063016afd8b6e612eef54b6f02aef08e4aa527efe5bf4c908de1416" +)] +#[test_case( + "0x1c1fe12683d8d267c934bc0a3254bcc8056e9cb9e969ad170e3dfaa8e5586", + "0x183fd1dd6df336293c7ad0df4c4591c582103710ad9fdd20af29d2f00f2d8829" +)] +#[test_case( + "0x243f3876808e9351c8bec2a03382141c21e7264ddab391f92859c8acbcd2d06b", + "0x251e6a8eb4a0cb951331a5d4d311af0cfcf3e5e8ec38d8ba5da2616f3123f6ce" +)] +#[test_case( + "0x2a33a6cb3f56d63941e7feffa23ee3c4d442849f0fae2a35126c6c8e983ab4b8", + "0x25348748df332a5914ad1c5b3898683fa6d4413eeda26d9f37ec427dba5979b8" +)] +#[test_case( + "0x1a4c2fc79b31fd5eebf158a263d367de37ff049ffecd6dac2f83f68f2789ad67", + "0x1c7a391ae6060b2325d04c1afac3b2d9791f405e8149077309b6c61c33d1d59f" +)] +#[test_case( + "0x1ba4663350cee7dc26b4b77acc236d62b1c220cd5cb021524fce492632062322", + "0x237fb10067864ae6faa02aa24f13a92b8609d9617404d971176390e6e22057c9" +)] +#[test_case( + "0x29c9cd26d1b2711cc25abc1631fd25b2cb3566954e818db4b243aa7c3f55269f", + "0x15aa87b2b641244ea11090e09c38f46fcff0790c4b7d4881d60d1963ea7a7384" +)] +#[test_case( + "0x14ab2b813699abc3ffcfdef130f3a7d1de942db1060e8b942de7a5b7455f5579", + "0x12ea09450cabdc795b0eaa71106f49eabf27f617311ec4934f88381e778ca36" +)] +#[test_case( + "0x261d723c8f3446e0fa887e3994c192d36a9b7ae89bad278d641495a31707bdaf", + "0x177798c723be2859188df10cd6bfd6d22900fea5bcb17a8269c47c4ec57ecbc4" +)] +#[test_case( + "0x2415743f394b16da0ad801e5c0458816fe6879e2b104e4b932d25bbae1e4f495", + "0x21bbeef0c708be4177e609a980574acc4ee35694f7192bae93267480523dbfa6" +)] +#[test_case( + "0x1e019a2b15a59774495646a2c6e90af5f9513855e8790fda0410d821aa85c77a", + "0x2499a400283616bb886d1a50fae5aea968f2a9ba569e6ca66b3834bf24b5fe90" +)] +#[test_case( + "0x18e594f40bebfd4378559de7cfb8134792f54201e6f97f13eeec495db4b72043", + "0x2a05674c906911dd4174eff658b723d9cd196cfdb37899d5d5dd64033af8e049" +)] +#[test_case( + "0xb026f975ba448d1862b063999cf926a0926b149d2af3cf4174e56e060b1e40", + "0x18afdc9a34dfa214e17f298a56ac2cbf8e6da180fa3e83ab60f3310b5574f223" +)] +#[test_case( + "0x162b6cb9b09cfbd3eddd0665bdeab6f7696e63e57c883d74160b360e55bda75a", + "0x1dbcbbd18641d2b477e19ef0186c608f0e2775a3e80c6d12c30b2df6920b869" +)] +#[test_case( + "0x3053a306366ae73004adb183c86cc3a3e6b0e2d317d08da708c9e5731927d427", + "0x12a78b1fb6535610e34c60342df9aa6b7deba119bb371523b2554ff2490cc63d" +)] +#[test_case( + "0xa257789e2277e79f78bcda818dbe60264be4ba74a7f0e5512b77ebde009ab2", + "0x1ea57d595383619dacb68111240ba48a0fc88aac272c7c99b6d6bb52a28fa650" +)] +#[test_case( + "0x13c2c6e174d814c5d7eae609741101a78a451faf30d8c4ada0ddb50c2c32bb0c", + "0x37e2eb482c3ccd3db5eca84aadc20da4a7f62709892006753382d9c7d7efdd" +)] +#[test_case( + "0x29a1b6724661494c7d92de23d383dfee36bcd9a50948985fa15afcdc9fb4eefb", + "0x261bc9b7013c833ec7a28ea5ef230053165f00baec582163d1773fad0344e793" +)] +#[test_case( + "0x271d404a15841bfa3649f393f97e93740334697e842cdbaf6a506672cbb33ba1", + "0x1caa0bcbb5781345da0a936ce4fe20bbe70292aa5d6875ebba25cb901b1a8756" +)] +#[test_case( + "0x168dc2d2fc6ef1b60935ce41605391fee9d8293bae910b398093c645e352ceac", + "0x2e2cf3475af52022033343ba610d4648ee9d8e929f1156ee58422871973c0f02" +)] +#[test_case( + "0x697a67cafbabb021ac4e263bab1a3135b8eb0510287e63afed023d10e68faad", + "0x124fef2af38736220515457ef35a016ef1e93d7a07301a7fcab4492fbcb540d6" +)] +#[test_case( + "0x2d1a15910aea25812f7a713b58f69c67d1c1dd53afefcb469ce5ea550c63a0e1", + "0x23437cd1744acee9b189947dd7df550d86c9177928c1da7d29b42737b9f753aa" +)] +#[test_case( + "0x1923b06c682b6ab159e4179d06f5211606291fc5dac808d96ee9d92e06e8c19d", + "0x19ac34336363b25e6a6f3689e311f7ee402685073aba9751c677fc4fe5d37a67" +)] +#[test_case( + "0x23a31f65ff514776538165d29e89d78300cca5372e9846c8b753d3f9f8044be4", + "0x92804e5af8af3bd1f48ac2385ead58df2d844d2f734054704cea69463b1347f" +)] +#[test_case( + "0xf289591fdaf5296221ad7242f5df12d8a76043909630dbacfbac0a855fb0adf", + "0x374b18432af1867a8151a59b22a1825a361f46f17d5b1eec617d2436f41f925" +)] +#[test_case( + "0x16996e0caa62e15e8a84366e2cf51fb63c08b99be3bfc0fe611c15b1ffdb92d5", + "0x35eea3531ad25b1b30965c1c28405def501afc7f05e77cd4420f3b5f97fcab8" +)] +#[test_case( + "0x43eb07ab2d420f827b29288f5fc7d35c419690cf49555d779061809d652c0d5", + "0x289a0cf358c7694fa5ededf12f69ad9d1fe4135fdd66c8b24b98d4149ea22f9e" +)] +#[test_case( + "0x2c9d3863863bc9575b5674d0d89685e6c003afabde723d0d68de5a3337f34dfd", + "0x1a71ab341620d6ffe5b16000cbd4fc618bd1970a824239c6d45f2c25bb99eeab" +)] +#[test_case( + "0x279dfab7a5b2953cc11002895072278fcdac890e8ada0d5b7e51fb2471b92cca", + "0xbcee4169b31286d0e2fc40fe9da800ba7b4949807d1ccb4538e99ef0334628" +)] +#[test_case( + "0x14205cd4096bbca14a1155dfc76ceaed08d864aa680f2ff47cdf03aad0aad0ff", + "0x11501a4f908f5cd18c3920b6d0604b77391e2490fc92c626eeaf5f88201c4f57" +)] +#[test_case( + "0x1e0c56db882cb1718ed784d36b33fafd67bd7a836fe7b902e4d140bfe0b5f603", + "0x2f6b1576269ab9c5d9d6958c068f3c1e23b120ab08a1c9158d069e283597dc06" +)] +#[test_case( + "0x4f83ad44d04d568c28ae648ee6d61ceeb439c38e4bac0668655e623aa2519b3", + "0x1438f4f055d8add1f00cef9ec83e1cdf4ea3c19b31ecd5939670ea6c005b0e60" +)] +#[test_case( + "0xd136345c46ec42cd307d41cec11479e13f9f63a5b417c451d508b430eb70c59", + "0xb41a0fdbd96f5e6641dc32030cfb727479824c38cd155b8a2a5b3985de8ca5b" +)] +#[test_case( + "0x21d0e540338f197962ca54281b078cb0f2f1377e2ba8af4f4c317650cbc8474d", + "0x251712ec63d4586c2f626fa7c326211c1008624a7137acd58c89c749f3c45910" +)] +#[test_case( + "0x1a48e43fc78439d7830c56e6ad10939f412e49dd5bb0f91a0e6c691207c84fd1", + "0x28d487cf2e89c9ebcdbde1d36c2880fc3cec52de2fe81e24be84d7c29edba734" +)] +#[test_case( + "0x13dac95faecede510707379cc3d8aad5667c55ab22d88c541d92d5029311ec6b", + "0x8da8cb35003282b91955580d729d83bb24da6c3e2329a9b3f59946dc1101fd5" +)] +#[test_case( + "0x1005c9ba92045b0d2c091823efceb3e94c1309384093248fef60a7e4d214984a", + "0x2a29050ef5c587f114c022906469727cc992065ab652589bbb602769cb624a1d" +)] +#[test_case( + "0x26f00b244a6e09e33f340d3d9d9c70fe8bc8605ceba4ff60c4981ab964291cc8", + "0x61b7ef308917ff7b6546b4ac579d00a2d23c4c6e7148ab0a76f0cb58553f992" +)] +#[test_case( + "0x5ec65154b527ccee4fca4352dd70ee92af0c2acefc59efa0c3a01cf76666eb1", + "0x71c4282827915fe5aa46f7cd29bff6439af37b8c36626ef163c1632bbcd627" +)] +#[test_case( + "0x141d86e8db4e7dd156f37ef1b3e098397b91a16a7a958466ac3c1a4b88e93d59", + "0x676f6e126cd32af7b2c04f5344b3e61c988d78ab477825566831d9c65003312" +)] +#[test_case( + "0xba149f9370ab0600a294405707cf09266cebcc502e7a45ac3981ce267f91e97", + "0x110c91fd089137d7a14d413be56177a5a16624965c4ca2751b16357fc70f69f2" +)] +#[test_case( + "0x2e6f97e69d5118df6067cfcefaaf2911a823574180e7d33e81b614ac3993a31d", + "0x53e6cc4a2355704346053c7fc1e4e019975a4c1917fee4d2099ecf9d9bee025" +)] +#[test_case( + "0x1bc8fe8ca3b70b89b16c92588d9806b56d9ec5f800a6d8c4c9976de96c7276f9", + "0x1df64626b2aae1f91194590703bfc4ed4f9d3fbe8b0937f2c7ea5aa5c5376d0c" +)] +#[test_case( + "0x30428f6f569ab3465ef5ded3a0fe6d85622a4dfd4939749c5c3fb9d309a08a69", + "0xfe83078ead3d36826a82ba1f0ed693b81cc56586094770fe3b3a9b18db47e60" +)] +#[test_case( + "0x16c1f522802c3245334e31fb458f494b6dd60039e788e7cb24881abfc9c2c58", + "0x1daf898f07c8391c96339014c8235d424bc60f0df2da2a0286520c6e01c2e899" +)] +#[test_case( + "0x13ac03c94d96332158c4b7819cd271b032e9b88877284a2aca9620ee38e22ed9", + "0x14ed68b189d663a58373656cd3944a5f6d0555fd4bec19b5605ef575b1424851" +)] +#[test_case( + "0x542dbd7469f5c8501461055f3194ea70bbd3bdc5eeec2c5ded7899252799046", + "0x236f13e247951025c8f2324d469dc4ef4c2a9dc39c8fbaa0be30290379cb49f3" +)] +#[test_case( + "0x236078d5c1900bd459906b8393995385bc92ee0d53ba0464110f535cfd3acd60", + "0x2f90c6c26b7665eb35d95212f19f24c493596cc81be5e7bf27abb6cdb86774a4" +)] +#[test_case( + "0x1633354c0f110a08c8f587902ed44de52e02b0d819272f930ee4ecb4c31eeee", + "0x28c72daa079017ac2e59f90eb088dc9f2900d1d7a49427166173891827827541" +)] +#[test_case( + "0x16a8790a8c6cd1015cda32e8040e9b0e071f03e3c6c8e5fca421193c397cea9d", + "0x165e900e7abfcca2cea1a088f89779d194e903f39659f00666483afd2410416e" +)] +#[test_case( + "0x18e26d616cfc39c834d7e59ea96e6c289ad02426cbb195e3f6df65b1dc23055d", + "0x1a8cad9931fe0e7bef40d3b6c1770de5e472bdfca5e2665c83620548fd577b90" +)] +#[test_case( + "0xc263cf26d8fc42a248d39703c3712ad3b6e667c68287dfaa84651ad2f674ef5", + "0x1aa6e9e20da131b2d0522e57913c27e51dc9d5797ab1c3e9813e3502c1e7c0ad" +)] +#[test_case( + "0x23ad686412dd73cc77ffc7670572db2485ee7394837025ae48c289015c7e978d", + "0xdbf9264771c45378a72e5b5c3335adccab42552a9064cbc1c622675090f53a" +)] +#[test_case( + "0x220bfae06a1dceee83c753ea8e508cc774132e075a02f2096e3e7d81bd72172d", + "0x1b4e2f80a83b905494000aed02b250f4d79d62551e024c5aaed162b8d79c3e6d" +)] +#[test_case( + "0x24849513d1e063d79d8fa521a18ff3fbb7f3effb0d94f00877885d904d0314e6", + "0x1f3b52e053cea7725920af07142a03110811f73822d1193d7a7f71f71dd14c12" +)] +#[test_case( + "0x67d5f9c418e30438b48ec83ebeb2262a7a1ff60577bf38074d8abf732f2f264", + "0x10000009fd7e1620b7b607f2740183853d6c37a77bed816294f2c3dad6282105" +)] +#[test_case( + "0x5e88d3f870951bb54e1a186f064b988957d0e18acc6e9177576e15db5d9ef29", + "0x1087dff3386b75e87737469875476093efc5726d79fc6ef6d605b65694fd9c4c" +)] +#[test_case( + "0xc7ae5cabcc87938fbbd6ca8ce43032e04e77b5a31382dd3f155c32eaafba312", + "0x1d88f46f8b490622b5dc8ecb21496010ea7dcfedbdb03b0379b7a82b13e8d345" +)] +#[test_case( + "0x1f17d00517a321ddf7b37580b49ab1019704fb58c398cccc661574f7ad3bce84", + "0x10480c2fca990545bf4c75ebb9f3e0d2c1d1202064bec0a2538c361ed5383b40" +)] +#[test_case( + "0x186440545c491379b155591f000ee1a2966e0b06edf7f04821b6c5aa32560ae5", + "0x5fbc982c0451c06ad62cc26690b79eaa41a328e0c2dd0a2efc30142d800066c" +)] +#[test_case( + "0x1920006d20470b1e1c8915483ea8fc39d9c8d6d28ee99067fb3eacf811264b36", + "0x18cbd767b3a0abfbe14074c1ca66674582d21811b6db4d1646f0e3974a2bdd80" +)] +#[test_case( + "0x283a1eba0ffb8d78c6d6a83192fe0a3e8c8a85bf676ea15638aaed2298b5e4c2", + "0xc2f16dbd66f12ddbdca4d2598e1bfb0b4609c6ece9846c9ad206f9a84360c13" +)] +#[test_case( + "0x2a6b2dffe1bbbb905cc8ae33f94979d221254f2ee312f447931e78ae760f384", + "0x1223e3acf24d68f700fde34dda5b99d3d14a2e19e521d6bae5eb3208ec85da93" +)] +#[test_case( + "0x16ec2b2cfde5cdcc0ad46b2819883b7648cafc79c0ffa6b37495ce963fd9e73a", + "0x13efd7c9427a9230e2dd4f9912b2f89f2b1b7b8a5381b1c3545eea5329e3760b" +)] +#[test_case( + "0x1af1e3ac1ca9c2c6e50de497a5110f4fab04765a35c2e69f282a4622230f12c9", + "0x230a32aa27ab6ba91b54885db3f7743eddfce719c86d2f1dc53a66f933bb1d9c" +)] +#[test_case( + "0xa4ce73df237a1bc5586b30c1a90abf2635d85b3d05c19a2559f576c4aca68b2", + "0x2a8964830d7bb4137e6ccf55bd2d1769a3eb21883f4210a11898f047c990c54c" +)] +#[test_case( + "0x2013f902abddeaf7a59420acfca012978c86fb145710a778a1eb9e3a8cd3e0e", + "0xfa674fc2825d5cdf1c0b4354f82b81349f21423ac35e21c2941e07309094e99" +)] +#[test_case( + "0x1c8ca38b0b4b152d76bd669baff3df0c2fb5821f3a3409b02a27243920fc092d", + "0x245c4a67f70014be0c35c3249d26700bb93c8ce5944476fd8e080ccdcedc4f3c" +)] +#[test_case( + "0x28d993d130104cf376503225bb871cb6c7c04f6884d1966b856f0cb22cf12896", + "0x153ea86b6aced470fedb21490d39a49107706264da949981127ab06016df4413" +)] +#[test_case( + "0x255a0d5a19ab5fb26aadc41155a468d721044645b99a7e5d37f3832723703029", + "0x2cb62312a43afd94a89aa39701a030a88ce6f0f6fffe7cad0d2464b5b43a8b0c" +)] +#[test_case( + "0x8794e5bfc7d0af0a931f44e0be72f96dce52de694163df1dde09d1ed8f5acad", + "0x1271b41eaa2a672bfc42d7da1d001de8d2d92fd315f90335c13ea496249dd207" +)] +#[test_case( + "0x1ccc02c48f9c79d7185058e3e987a32f82df5cb5cb393ce878f9d72b87f4cc84", + "0x144c8f26f6199f4e7c847b18542ed6300491857b307a74e0b43341731752d43b" +)] +#[test_case( + "0x1d9d714ff4edea908f1cba490bbfe172e4678f302e60e43dd8d12a4db829e2cb", + "0x19493334bf21e33536df25e60eaf793b68ece2f347fd4f18774204e3366f3d2a" +)] +#[test_case( + "0x2c724d15b6f2a12d3c480012fe0b5ca437c82cf489e2d3a2dbdbdc2df75ebf8b", + "0x107b89affc90a92d066d2be0a0fab2c3c13cfeb3669a149ae70eef3e57b72a5c" +)] +#[test_case( + "0x14b878fab29234cf324d2ca450a17ea47dc6c8c163ee8e599e3bfd88929edda8", + "0x5db74c45062dcf13c08ac1ad090c4a94d205616846e2db65c6514df4dc4511c" +)] +#[test_case( + "0x19af4d9da562ab34189ead73da36968f1cfe5341764a07076227ac6ddc40331f", + "0x184ff15b8e41cc46504cea5ed36b2514b81fd02b0a0c0b8cb810ed763b9b5c68" +)] +#[test_case( + "0x8aa7975a01a9e3b97f38f81886f50f01d5f98c1dd342d00fbe0d058dce2ffbf", + "0x1e88df023cf4833fed24c591a93b757b7c81e102e370b26bd9669ded5b32a912" +)] +#[test_case( + "0x25134030c65aa48bc9f52e9f9de7d1700a0b2bc10a86d7fc8d935716959c398c", + "0x1349a04b5d6495b421f8bee2e6c5445b381a24612fe8fc43db658a72a538677c" +)] +#[test_case( + "0x21b068deff9b75e9607f7678139e876d3e0479dd9287a60ecbefb588f5fbdf4b", + "0x1fc5aa3aec42426da9585fc66cf4281f21d7c745a750d0dc6b0bb073e4b66eb1" +)] +#[test_case( + "0x81594083a63da6b2c349931c9236d758d57afa3f81eddb3f4a5fe2d53cb588c", + "0xfb30f6c0fdf0a8e4fb0a207baae49af20b4eb11bf95497e2b255eea6a7dc461" +)] +#[test_case( + "0x17492b875f2dda6c9b2c49c17b9095ca65de415170d3e9a4d834e5b5ef579182", + "0x31374fb9c8e46406fa83f5db0274d8d799a480a24b396db3e4df165691d02a6" +)] +#[test_case( + "0x1e6f690b9b2c5b4d3440c1907b38a8cbe2b3267e988c760d2bb426d19014eef9", + "0xb13a6b0f347da61aff1a5273fc125b6a050cc903022f0ebf366a5d2ed3debc3" +)] +fn pow_magic_number_works(x: &str, expected: &str) { + let x = Fq::from_hex_str(x); + let expected = Fq::from_hex_str(expected); + + let actual = pow_magic_number(x); + assert_eq!(actual, expected); +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/hash_tuple.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/hash_tuple.rs new file mode 100644 index 000000000..b86f93c68 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/hash_tuple.rs @@ -0,0 +1,134 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::types::Hash256; +use alloc::{vec, vec::Vec}; + +use frame_support::{Blake2_256, StorageHasher}; +use parity_scale_codec::Encode; +use zeitgeist_primitives::types::Asset; + +pub trait ToBytes { + fn to_bytes(&self) -> Vec; +} + +pub trait HashTuple { + fn hash_tuple(tuple: (T1, T2)) -> Hash256 + where + T1: ToBytes, + T2: ToBytes; +} + +impl HashTuple for Blake2_256 { + fn hash_tuple(tuple: (T1, T2)) -> Hash256 + where + T1: ToBytes, + T2: ToBytes, + { + let mut bytes = Vec::new(); + + bytes.extend_from_slice(&tuple.0.to_bytes()); + bytes.extend_from_slice(&tuple.1.to_bytes()); + + Blake2_256::hash(&bytes) + } +} + +/// Implements `ToBytes` for any type implementing `to_be_bytes`. +macro_rules! impl_to_bytes { + ($($t:ty),*) => { + $( + impl ToBytes for $t { + fn to_bytes(&self) -> Vec { + self.to_be_bytes().to_vec() + } + } + )* + }; +} + +impl_to_bytes!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); + +impl ToBytes for bool { + fn to_bytes(&self) -> Vec { + vec![*self as u8] + } +} + +impl ToBytes for Hash256 { + fn to_bytes(&self) -> Vec { + self.to_vec() + } +} + +impl ToBytes for Vec +where + T: ToBytes, +{ + fn to_bytes(&self) -> Vec { + let mut result = Vec::new(); + + for b in self.iter() { + result.extend_from_slice(&b.to_bytes()); + } + + result + } +} + +/// Beware! All changes to this implementation need to be backwards compatible. Failure to follow this +/// restriction will result in assets changing hashes between versions, causing unreachable funds. +/// +/// Of course, this is true of any modification of the collection ID manager, but this is the place +/// where it's most likely to happen. We're using tests below to ensure that unintentional changes +/// are caught. +impl ToBytes for Asset +where + MarketId: Encode, +{ + fn to_bytes(&self) -> Vec { + self.encode() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + type MarketId = u128; + + // Beware! If you have to modify these tests, that means that you broke encoding of assets in a + // way that's not backwards compatible. + #[test_case(Asset::Ztg, vec![4])] + #[test_case(Asset::ForeignAsset(0), vec![5, 0, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(1), vec![5, 1, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(2), vec![5, 2, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(3), vec![5, 3, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(4), vec![5, 4, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(5), vec![5, 5, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(6), vec![5, 6, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(7), vec![5, 7, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(8), vec![5, 8, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(9), vec![5, 9, 0, 0, 0])] + #[test_case(Asset::ForeignAsset(u32::MAX - 1), vec![5, 254, 255, 255, 255])] + #[test_case(Asset::ForeignAsset(u32::MAX), vec![5, 255, 255, 255, 255])] + fn asset_to_bytes_works(asset: Asset, expected: Vec) { + let actual = asset.to_bytes(); + assert_eq!(actual, expected); + } +} diff --git a/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/mod.rs b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/mod.rs new file mode 100644 index 000000000..f29975101 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/cryptographic_id_manager/mod.rs @@ -0,0 +1,102 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work licensed under the GNU Lesser General +// Public License 3.0 but published without copyright notice by Gnosis +// (, info@gnosis.io) in the +// conditional-tokens-contracts repository +// , +// and has been relicensed under GPL-3.0-or-later in this repository. + +mod decompressor; +mod hash_tuple; + +use super::CollectionIdError; +use crate::traits::CombinatorialIdManager; +use alloc::vec::Vec; +use core::marker::PhantomData; +use hash_tuple::{HashTuple, ToBytes}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use zeitgeist_primitives::{ + traits::CombinatorialTokensFuel, + types::{Asset, CombinatorialId}, +}; + +#[derive(Clone, Debug, Decode, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct Fuel { + /// The maximum number of iterations to perform in the main loop of `get_collection_id`. + total: u32, + + /// Perform `self.total` of iterations in the main loop of `get_collection_id`. Useful for + /// benchmarking purposes and should probably not be used in production. + consume_all: bool, +} + +impl Fuel { + pub fn new(total: u32, consume_all: bool) -> Self { + Fuel { total, consume_all } + } + + pub fn consume_all(&self) -> bool { + self.consume_all + } +} + +impl CombinatorialTokensFuel for Fuel { + fn from_total(total: u32) -> Fuel { + Fuel { total, consume_all: true } + } + + fn total(&self) -> u32 { + self.total + } +} + +pub struct CryptographicIdManager(PhantomData<(MarketId, Hasher)>); + +impl CombinatorialIdManager for CryptographicIdManager +where + MarketId: ToBytes + Encode, + Hasher: HashTuple, +{ + type Asset = Asset; + type CombinatorialId = CombinatorialId; + type MarketId = MarketId; + type Fuel = Fuel; + + fn get_collection_id( + parent_collection_id: Option, + market_id: Self::MarketId, + index_set: Vec, + fuel: Self::Fuel, + ) -> Result { + let input = (market_id, index_set); + let hash = Hasher::hash_tuple(input); + + decompressor::get_collection_id(hash, parent_collection_id, fuel) + } + + fn get_position_id( + collateral: Self::Asset, + collection_id: Self::CombinatorialId, + ) -> Self::CombinatorialId { + let input = (collateral, collection_id); + + Hasher::hash_tuple(input) + } +} diff --git a/zrml/combinatorial-tokens/src/types/hash.rs b/zrml/combinatorial-tokens/src/types/hash.rs new file mode 100644 index 000000000..396e2722a --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/hash.rs @@ -0,0 +1,18 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +pub type Hash256 = [u8; 32]; diff --git a/zrml/combinatorial-tokens/src/types/mod.rs b/zrml/combinatorial-tokens/src/types/mod.rs new file mode 100644 index 000000000..f23b68450 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod collection_id_error; +pub(crate) mod cryptographic_id_manager; +pub(crate) mod hash; +mod transmutation_type; + +pub use collection_id_error::CollectionIdError; +pub use cryptographic_id_manager::{CryptographicIdManager, Fuel}; +pub(crate) use hash::Hash256; +pub use transmutation_type::TransmutationType; diff --git a/zrml/combinatorial-tokens/src/types/transmutation_type.rs b/zrml/combinatorial-tokens/src/types/transmutation_type.rs new file mode 100644 index 000000000..a0aa0ae89 --- /dev/null +++ b/zrml/combinatorial-tokens/src/types/transmutation_type.rs @@ -0,0 +1,29 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work licensed under the GNU Lesser General +// Public License 3.0 but published without copyright notice by Gnosis +// (, info@gnosis.io) in the +// conditional-tokens-contracts repository +// , +// and has been relicensed under GPL-3.0-or-later in this repository. + +pub enum TransmutationType { + VerticalWithParent, + VerticalSansParent, + Horizontal, +} diff --git a/zrml/combinatorial-tokens/src/weights.rs b/zrml/combinatorial-tokens/src/weights.rs new file mode 100644 index 000000000..0196556e9 --- /dev/null +++ b/zrml/combinatorial-tokens/src/weights.rs @@ -0,0 +1,253 @@ +// Copyright 2022-2025 Forecasting Technologies LTD. +// Copyright 2021-2022 Zeitgeist PM LLC. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +//! Autogenerated weights for zrml_combinatorial_tokens +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: `2024-12-05`, STEPS: `2`, REPEAT: `0`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Mac`, CPU: `` +//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/zeitgeist +// benchmark +// pallet +// --chain=dev +// --steps=2 +// --repeat=0 +// --pallet=zrml_combinatorial_tokens +// --extrinsic=* +// --execution=native +// --wasm-execution=compiled +// --heap-pages=4096 +// --template=./misc/weight_template.hbs +// --header=./HEADER_GPL3 +// --output=./zrml/combinatorial-tokens/src/weights.rs + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use core::marker::PhantomData; +use frame_support::{traits::Get, weights::Weight}; + +/// Trait containing the required functions for weight retrival within +/// zrml_combinatorial_tokens (automatically generated) +pub trait WeightInfoZeitgeist { + fn split_position_vertical_sans_parent(n: u32, m: u32) -> Weight; + fn split_position_vertical_with_parent(n: u32, m: u32) -> Weight; + fn split_position_horizontal(n: u32, m: u32) -> Weight; + fn merge_position_vertical_sans_parent(n: u32, m: u32) -> Weight; + fn merge_position_vertical_with_parent(n: u32, m: u32) -> Weight; + fn merge_position_horizontal(n: u32, m: u32) -> Weight; + fn redeem_position_sans_parent(n: u32, m: u32) -> Weight; + fn redeem_position_with_parent(n: u32, m: u32) -> Weight; +} + +/// Weight functions for zrml_combinatorial_tokens (automatically generated) +pub struct WeightInfo(PhantomData); +impl WeightInfoZeitgeist for WeightInfo { + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:32 w:32) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:32 w:32) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn split_position_vertical_sans_parent(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `441` + // Estimated: `4173 + n * (2612 ±0)` + // Minimum execution time: 3_358_000 nanoseconds. + Weight::from_parts(3_358_000_000, 4173) + // Standard Error: 397_575_770 + .saturating_add(Weight::from_parts(1_183_275_893, 0).saturating_mul(n.into())) + // Standard Error: 191_113_523 + .saturating_add(Weight::from_parts(73_290_272, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2612).saturating_mul(n.into())) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:33 w:33) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:33 w:33) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn split_position_vertical_with_parent(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `671` + // Estimated: `4173 + n * (2612 ±0)` + // Minimum execution time: 3_816_000 nanoseconds. + Weight::from_parts(3_816_000_000, 4173) + // Standard Error: 404_411_306 + .saturating_add(Weight::from_parts(1_418_273_449, 0).saturating_mul(n.into())) + // Standard Error: 194_399_346 + .saturating_add(Weight::from_parts(67_832_101, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2612).saturating_mul(n.into())) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:33 w:33) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:33 w:33) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn split_position_horizontal(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `633` + // Estimated: `4173 + n * (2612 ±0)` + // Minimum execution time: 4_950_000 nanoseconds. + Weight::from_parts(4_950_000_000, 4173) + // Standard Error: 407_636_194 + .saturating_add(Weight::from_parts(1_190_982_890, 0).saturating_mul(n.into())) + // Standard Error: 195_949_540 + .saturating_add(Weight::from_parts(75_730_302, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2612).saturating_mul(n.into())) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:32 w:32) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:32 w:32) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn merge_position_vertical_sans_parent(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `623 + n * (160 ±0)` + // Estimated: `4173 + n * (2612 ±0)` + // Minimum execution time: 3_357_000 nanoseconds. + Weight::from_parts(3_357_000_000, 4173) + // Standard Error: 398_813_800 + .saturating_add(Weight::from_parts(1_188_250_534, 0).saturating_mul(n.into())) + // Standard Error: 191_708_641 + .saturating_add(Weight::from_parts(73_478_154, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2612).saturating_mul(n.into())) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:33 w:33) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:33 w:33) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn merge_position_vertical_with_parent(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `520 + n * (159 ±0)` + // Estimated: `4173 + n * (2612 ±0)` + // Minimum execution time: 3_791_000 nanoseconds. + Weight::from_parts(3_791_000_000, 4173) + // Standard Error: 403_628_733 + .saturating_add(Weight::from_parts(1_426_263_672, 0).saturating_mul(n.into())) + // Standard Error: 194_023_165 + .saturating_add(Weight::from_parts(67_374_417, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2612).saturating_mul(n.into())) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:33 w:33) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:33 w:33) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn merge_position_horizontal(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `481 + n * (159 ±0)` + // Estimated: `4173 + n * (2612 ±0)` + // Minimum execution time: 4_948_000 nanoseconds. + Weight::from_parts(4_948_000_000, 4173) + // Standard Error: 408_972_386 + .saturating_add(Weight::from_parts(1_174_151_237, 0).saturating_mul(n.into())) + // Standard Error: 196_591_844 + .saturating_add(Weight::from_parts(76_736_050, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2612).saturating_mul(n.into())) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:1 w:1) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:1 w:1) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn redeem_position_sans_parent(n: u32, _m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `780` + // Estimated: `4173` + // Minimum execution time: 342_000 nanoseconds. + Weight::from_parts(298_833_333, 4173) + // Standard Error: 86_602 + .saturating_add(Weight::from_parts(22_083_333, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(3)) + } + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:2 w:2) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:2 w:2) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 32]`. + /// The range of component `m` is `[32, 64]`. + fn redeem_position_with_parent(n: u32, _m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `674` + // Estimated: `6214` + // Minimum execution time: 572_000 nanoseconds. + Weight::from_parts(549_299_999, 6214) + // Standard Error: 202_072 + .saturating_add(Weight::from_parts(21_850_000, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) + } +} diff --git a/zrml/futarchy/Cargo.toml b/zrml/futarchy/Cargo.toml new file mode 100644 index 000000000..1fd6f7bfd --- /dev/null +++ b/zrml/futarchy/Cargo.toml @@ -0,0 +1,55 @@ +[dependencies] +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true, features = ["derive"] } +sp-runtime = { workspace = true } +zeitgeist-primitives = { workspace = true } + +# mock + +env_logger = { workspace = true, optional = true } +pallet-balances = { workspace = true, optional = true } +sp-io = { workspace = true, optional = true } + +# fuzz + +arbitrary = { workspace = true, features = ["derive"], optional = true } +sp-core = { workspace = true, optional = true } + +[dev-dependencies] +test-case = { workspace = true } +zrml-futarchy = { workspace = true, features = ["default", "mock"] } + +[features] +default = ["std"] +fuzzing = ["arbitrary", "sp-core"] +mock = [ + "env_logger/default", + "sp-io/default", + "pallet-balances/default", + "zeitgeist-primitives/mock", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "parity-scale-codec/std", + "sp-runtime/std", + "zeitgeist-primitives/std", +] +try-runtime = [ + "frame-support/try-runtime", +] + +[package] +authors = ["Zeitgeist PM "] +edition.workspace = true +name = "zrml-futarchy" +version = "0.5.5" diff --git a/zrml/futarchy/README.md b/zrml/futarchy/README.md new file mode 100644 index 000000000..a41fb2199 --- /dev/null +++ b/zrml/futarchy/README.md @@ -0,0 +1,46 @@ +# Futarchy Module + +The futarchy module provides a straightforward, "no bells and whistles" +implementation of the +[futarchy governance system](https://docs.zeitgeist.pm/docs/learn/futarchy). + +## Overview + +The futarchy module is essentially an oracle based governance system: When a +proposal is submitted, an oracle is specified which evaluates whether the +proposal should be executed. The type of the oracle is configured using the +associated type `Oracle`, which must implement `FutarchyOracle`. + +The typical oracle implementation for futarchy is the `DecisionMarketOracle` +implementation exposed by the neo-swaps module, which allows making decisions +based on prices in prediction markets. A `DecisionMarketOracle` is defined by +providing a pool ID and two outcomes, the _positive_ and _negative_ outcome. The +oracle evaluates positively (meaning that it will allow the proposal to pass) if +and only if the positive outcome is more valuable than the negative outcome over +a period of time for a certain absolute and relative threshold determined by a +`DecisionMarketOracleScoreboard`. + +The standard governance flow is the following: + +- The root origin submits a proposal to be approved or rejected via futarchy by + running a governance proposal through + [pallet-democracy](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/democracy) + and calling into this pallet's sole extrinsic `submit_proposal`. Assuming that + the thesis of futarchy is correct and the market used to evaluate the proposal + is well-configured and sufficiently liquid, submitting a proposal to futarchy + rather than pallet-democracy gives a stronger guarantee on the efficacy of the + proposal. +- Wait until the `duration` specified in `submit_proposal` has passed. The + oracle will be automatically evaluated and will either schedule + `proposal.call` at `proposal.when` where `proposal` is the proposal specified + in `submit_proposal`. + +### Terminology + +- _Call_: Refers to an on-chain extrinsic call. +- _Oracle_: A means of making a decision about a proposal. At any block, an + oracle evaluates to `true` (proposal is accepted) or `false` (proposal is + rejected). +- _Proposal_: Consists of a call, an oracle and a time of execution. If and only + if the proposal is accepted, the call is scheduled for the specified time of + execution. diff --git a/zrml/futarchy/fuzz/Cargo.toml b/zrml/futarchy/fuzz/Cargo.toml new file mode 100644 index 000000000..e12990f80 --- /dev/null +++ b/zrml/futarchy/fuzz/Cargo.toml @@ -0,0 +1,26 @@ +[[bin]] +doc = false +name = "submit_proposal" +path = "submit_proposal.rs" +test = false + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +frame-support = { workspace = true, features = ["default"] } +frame-system = { workspace = true } +libfuzzer-sys = { workspace = true } +orml-traits = { workspace = true, features = ["default"] } +rand = { workspace = true, features = ["default"] } +sp-runtime = { workspace = true, features = ["default"] } +zeitgeist-primitives = { workspace = true, features = ["default", "mock"] } +zrml-futarchy = { workspace = true, features = ["default", "fuzzing", "mock"] } + +[package] +authors = ["Forecasting Technologies Ltd"] +edition.workspace = true +name = "zrml-futarchy-fuzz" +publish = false +version = "0.5.5" + +[package.metadata] +cargo-fuzz = true diff --git a/zrml/futarchy/fuzz/submit_proposal.rs b/zrml/futarchy/fuzz/submit_proposal.rs new file mode 100644 index 000000000..1c39ead34 --- /dev/null +++ b/zrml/futarchy/fuzz/submit_proposal.rs @@ -0,0 +1,61 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use frame_system::pallet_prelude::{BlockNumberFor, OriginFor}; +use libfuzzer_sys::fuzz_target; +use zrml_futarchy::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{Futarchy, Runtime, RuntimeOrigin}, + }, + types::Proposal, +}; + +#[derive(Debug)] +struct SubmitProposalParams { + origin: OriginFor, + duration: BlockNumberFor, + proposal: Proposal, +} + +impl<'a> Arbitrary<'a> for SubmitProposalParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let origin = RuntimeOrigin::signed(account_id); + + let duration = Arbitrary::arbitrary(u)?; + + let proposal = Arbitrary::arbitrary(u)?; + + let params = SubmitProposalParams { origin, duration, proposal }; + + Ok(params) + } +} + +fuzz_target!(|params: SubmitProposalParams| { + let mut ext = ExtBuilder::build(); + + ext.execute_with(|| { + let _ = Futarchy::submit_proposal(params.origin, params.duration, params.proposal); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/futarchy/src/benchmarking.rs b/zrml/futarchy/src/benchmarking.rs new file mode 100644 index 000000000..b33aceeda --- /dev/null +++ b/zrml/futarchy/src/benchmarking.rs @@ -0,0 +1,102 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use crate::{traits::ProposalStorage, types::Proposal, Call, Config, Event, Pallet, Proposals}; +use alloc::vec; +use frame_benchmarking::v2::*; +use frame_support::{ + dispatch::RawOrigin, + traits::{Bounded, Get}, +}; +use frame_system::Pallet as System; +use zeitgeist_primitives::traits::FutarchyBenchmarkHelper; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn submit_proposal() { + let duration = T::MinDuration::get(); + + let oracle = T::BenchmarkHelper::create_oracle(true); + let proposal = Proposal { + when: Default::default(), + call: Bounded::Inline(vec![7u8; 128].try_into().unwrap()), + oracle, + }; + + let now = System::::block_number(); + let to_be_scheduled_at = now + duration; + let mut proposals = Proposals::::get(to_be_scheduled_at); + for _ in 0..(T::MaxProposals::get() - 1) { + proposals.try_push(proposal.clone()).unwrap(); + } + Proposals::::insert(to_be_scheduled_at, proposals); + + #[extrinsic_call] + _(RawOrigin::Root, duration, proposal.clone()); + + let expected_event = + ::RuntimeEvent::from(Event::::Submitted { duration, proposal }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn maybe_schedule_proposal() { + let when = u32::MAX.into(); + let oracle = T::BenchmarkHelper::create_oracle(true); + let proposal = + Proposal { when, call: Bounded::Inline(vec![7u8; 128].try_into().unwrap()), oracle }; + + #[block] + { + Pallet::::maybe_schedule_proposal(proposal.clone()); + } + + let expected_event = ::RuntimeEvent::from(Event::::Scheduled { proposal }); + System::::assert_last_event(expected_event.into()); + } + + #[benchmark] + fn take_proposals(n: Linear<1, 4>) { + let when = u32::MAX.into(); + let oracle = T::BenchmarkHelper::create_oracle(true); + let proposal = + Proposal { when, call: Bounded::Inline(vec![7u8; 128].try_into().unwrap()), oracle }; + + let now = System::::block_number(); + let mut proposals = Proposals::::get(now); + for _ in 0..n { + proposals.try_push(proposal.clone()).unwrap(); + } + Proposals::::insert(now, proposals); + + #[block] + { + let _ = as ProposalStorage>::take(now); + } + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::ext_builder::ExtBuilder::build(), + crate::mock::runtime::Runtime + ); +} diff --git a/zrml/futarchy/src/dispatchable_impls.rs b/zrml/futarchy/src/dispatchable_impls.rs new file mode 100644 index 000000000..13ff4574d --- /dev/null +++ b/zrml/futarchy/src/dispatchable_impls.rs @@ -0,0 +1,40 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{traits::ProposalStorage, types::Proposal, Config, Error, Event, Pallet}; +use frame_support::{ensure, require_transactional, traits::Get}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::{DispatchResult, Saturating}; + +impl Pallet { + #[require_transactional] + pub(crate) fn do_submit_proposal( + duration: BlockNumberFor, + proposal: Proposal, + ) -> DispatchResult { + ensure!(duration >= T::MinDuration::get(), Error::::DurationTooShort); + + let now = frame_system::Pallet::::block_number(); + let to_be_scheduled_at = now.saturating_add(duration); + + as ProposalStorage>::add(to_be_scheduled_at, proposal.clone())?; + + Self::deposit_event(Event::::Submitted { duration, proposal }); + + Ok(()) + } +} diff --git a/zrml/futarchy/src/lib.rs b/zrml/futarchy/src/lib.rs new file mode 100644 index 000000000..8e3eb40e1 --- /dev/null +++ b/zrml/futarchy/src/lib.rs @@ -0,0 +1,219 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// 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. + +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +mod benchmarking; +mod dispatchable_impls; +pub mod mock; +mod pallet_impls; +mod proposal_storage; +mod tests; +pub mod traits; +pub mod types; +pub mod weights; + +pub use pallet::*; + +#[frame_support::pallet] +mod pallet { + use crate::{traits::ProposalStorage, types::Proposal, weights::WeightInfoZeitgeist}; + use alloc::fmt::Debug; + use core::marker::PhantomData; + use frame_support::{ + pallet_prelude::{IsType, StorageMap, StorageValue, StorageVersion, ValueQuery, Weight}, + traits::{schedule::v3::Anon as ScheduleAnon, Bounded, Hooks, OriginTrait}, + transactional, Blake2_128Concat, BoundedVec, + }; + use frame_system::{ + ensure_root, + pallet_prelude::{BlockNumberFor, OriginFor}, + }; + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + use sp_runtime::{traits::Get, DispatchResult, SaturatedConversion}; + use zeitgeist_primitives::traits::FutarchyOracle; + + #[cfg(feature = "runtime-benchmarks")] + use zeitgeist_primitives::traits::FutarchyBenchmarkHelper; + + #[pallet::config] + pub trait Config: frame_system::Config { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper: FutarchyBenchmarkHelper; + + /// The maximum number of proposals allowed to be in flight simultaneously. + type MaxProposals: Get; + + /// The minimum allowed duration between the creation of a proposal and its evaluation. + type MinDuration: Get>; + + /// The type used to define the oracle for a proposal. + type Oracle: FutarchyOracle> + + Clone + + Debug + + Decode + + Encode + + Eq + + MaxEncodedLen + + PartialEq + + TypeInfo; + + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Scheduler interface for executing proposals. + type Scheduler: ScheduleAnon, CallOf, PalletsOriginOf>; + + type WeightInfo: WeightInfoZeitgeist; + } + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData); + + pub(crate) type CallOf = ::RuntimeCall; + pub(crate) type BoundedCallOf = Bounded>; + pub(crate) type OracleOf = ::Oracle; + pub(crate) type PalletsOriginOf = + <::RuntimeOrigin as OriginTrait>::PalletsOrigin; + pub(crate) type ProposalsOf = BoundedVec, ::MaxProposals>; + + pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + + #[pallet::storage] + pub type Proposals = + StorageMap<_, Blake2_128Concat, BlockNumberFor, ProposalsOf, ValueQuery>; + + #[pallet::storage] + pub type ProposalCount = StorageValue<_, u32, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event + where + T: Config, + { + /// A proposal has been submitted. + Submitted { duration: BlockNumberFor, proposal: Proposal }, + + /// A proposal has been rejected by the oracle. + Rejected { proposal: Proposal }, + + /// A proposal has been scheduled for execution. + Scheduled { proposal: Proposal }, + + /// This is a logic error. You shouldn't see this. + UnexpectedSchedulerError, + } + + #[pallet::error] + pub enum Error { + /// The cache for this particular block is full. Try another block. + CacheFull, + + /// The specified duration must be at least equal to `MinDuration`. + DurationTooShort, + + /// This is a logic error. You shouldn't see this. + UnexpectedStorageFailure, + } + + #[pallet::call] + impl Pallet { + /// Submits a `proposal` for evaluation in `duration` blocks. + /// + /// If, after `duration` blocks, the oracle `proposal.oracle` is evaluated positively, the + /// proposal is scheduled for execution at `proposal.when`. + #[pallet::call_index(0)] + #[transactional] + #[pallet::weight(T::WeightInfo::submit_proposal())] + pub fn submit_proposal( + origin: OriginFor, + duration: BlockNumberFor, + proposal: Proposal, + ) -> DispatchResult { + ensure_root(origin)?; + + Self::do_submit_proposal(duration, proposal) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + let mut total_weight = Weight::zero(); + + // Update all oracles. + let mutate_all_result = + as ProposalStorage>::mutate_all(|p| p.oracle.update(now)); + if let Ok(block_to_weights) = mutate_all_result { + // We did one storage read per vector cached. Shouldn't saturate, but technically + // might. + let reads: u64 = block_to_weights.len().saturated_into(); + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(reads)); + + for weights in block_to_weights.values() { + for &weight in weights.iter() { + total_weight = total_weight.saturating_add(weight); + } + } + } else { + // Unreachable! + return total_weight; + } + + let proposals = if let Ok(proposals) = as ProposalStorage>::take(now) { + total_weight = total_weight + .saturating_add(T::WeightInfo::take_proposals(proposals.len() as u32)); + proposals + } else { + // assumes the worst case scenario + total_weight = total_weight + .saturating_add(T::WeightInfo::take_proposals(T::MaxProposals::get())); + return total_weight; + }; + + for proposal in proposals.into_iter() { + let weight = Self::maybe_schedule_proposal(proposal); + total_weight = total_weight.saturating_add(weight); + } + + total_weight + } + } +} diff --git a/zrml/futarchy/src/mock/ext_builder.rs b/zrml/futarchy/src/mock/ext_builder.rs new file mode 100644 index 000000000..fd3825657 --- /dev/null +++ b/zrml/futarchy/src/mock/ext_builder.rs @@ -0,0 +1,73 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::mock::runtime::{Runtime, System}; +use sp_io::TestExternalities; +use sp_runtime::BuildStorage; + +#[cfg(feature = "parachain")] +use {crate::mock::consts::FOREIGN_ASSET, zeitgeist_primitives::types::CustomMetadata}; + +#[derive(Default)] +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build() -> TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + // See the logs in tests when using `RUST_LOG=debug cargo test -- --nocapture` + let _ = env_logger::builder().is_test(true).try_init(); + + pallet_balances::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + #[cfg(feature = "parachain")] + { + orml_tokens::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + let custom_metadata = + CustomMetadata { allow_as_base_asset: true, ..Default::default() }; + + orml_asset_registry::GenesisConfig:: { + assets: vec![( + FOREIGN_ASSET, + AssetMetadata { + decimals: 18, + name: "MKL".as_bytes().to_vec().try_into().unwrap(), + symbol: "MKL".as_bytes().to_vec().try_into().unwrap(), + existential_deposit: 0, + location: None, + additional: custom_metadata, + } + .encode(), + )], + last_asset_id: FOREIGN_ASSET, + } + .assimilate_storage(&mut t) + .unwrap(); + } + + let mut test_ext: sp_io::TestExternalities = t.into(); + + test_ext.execute_with(|| System::set_block_number(1)); + + test_ext + } +} diff --git a/zrml/futarchy/src/mock/mod.rs b/zrml/futarchy/src/mock/mod.rs new file mode 100644 index 000000000..698dc06e8 --- /dev/null +++ b/zrml/futarchy/src/mock/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(feature = "mock")] + +pub mod ext_builder; +pub mod runtime; +pub mod types; +pub mod utility; diff --git a/zrml/futarchy/src/mock/runtime.rs b/zrml/futarchy/src/mock/runtime.rs new file mode 100644 index 000000000..6027963f6 --- /dev/null +++ b/zrml/futarchy/src/mock/runtime.rs @@ -0,0 +1,99 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate as zrml_futarchy; +use crate::{ + mock::types::{MockOracle, MockScheduler}, + weights::WeightInfo, +}; +use frame_support::{construct_runtime, parameter_types, traits::Everything}; +use frame_system::mocking::MockBlock; +use sp_runtime::traits::{BlakeTwo256, ConstU32, IdentityLookup}; +use zeitgeist_primitives::{ + constants::mock::{BlockHashCount, ExistentialDeposit, MaxLocks, MaxReserves}, + types::{AccountIdTest, Balance, BlockNumber, Hash}, +}; + +#[cfg(feature = "runtime-benchmarks")] +use crate::mock::types::MockBenchmarkHelper; + +parameter_types! { + // zrml-futarchy + pub const MaxProposals: u32 = 16; + pub const MinDuration: BlockNumber = 10; +} + +construct_runtime! { + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Futarchy: zrml_futarchy, + } +} + +impl frame_system::Config for Runtime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountIdTest; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type RuntimeCall = RuntimeCall; + type DbWeight = (); + type RuntimeEvent = RuntimeEvent; + type Hash = Hash; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type Nonce = u64; + type MaxConsumers = ConstU32<16>; + type OnKilledAccount = (); + type OnNewAccount = (); + type RuntimeOrigin = RuntimeOrigin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); + type OnSetCode = (); +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type FreezeIdentifier = (); + type RuntimeHoldReason = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type MaxHolds = (); + type MaxFreezes = (); + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl zrml_futarchy::Config for Runtime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = MockBenchmarkHelper; + type MaxProposals = MaxProposals; + type MinDuration = MinDuration; + type Oracle = MockOracle; + type RuntimeEvent = RuntimeEvent; + type Scheduler = MockScheduler; + type WeightInfo = WeightInfo; +} diff --git a/zrml/futarchy/src/mock/types/benchmark_helper.rs b/zrml/futarchy/src/mock/types/benchmark_helper.rs new file mode 100644 index 000000000..ae5dd53e3 --- /dev/null +++ b/zrml/futarchy/src/mock/types/benchmark_helper.rs @@ -0,0 +1,30 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{ + mock::{runtime::Runtime, types::MockOracle}, + OracleOf, +}; +use zeitgeist_primitives::traits::FutarchyBenchmarkHelper; + +pub struct MockBenchmarkHelper; + +impl FutarchyBenchmarkHelper> for MockBenchmarkHelper { + fn create_oracle(value: bool) -> OracleOf { + MockOracle::new(Default::default(), value) + } +} diff --git a/zrml/futarchy/src/mock/types/mod.rs b/zrml/futarchy/src/mock/types/mod.rs new file mode 100644 index 000000000..585eba939 --- /dev/null +++ b/zrml/futarchy/src/mock/types/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#[cfg(feature = "runtime-benchmarks")] +mod benchmark_helper; +mod oracle; +mod scheduler; + +#[cfg(feature = "runtime-benchmarks")] +pub use benchmark_helper::MockBenchmarkHelper; +pub(crate) use oracle::MockOracle; +pub(crate) use scheduler::MockScheduler; diff --git a/zrml/futarchy/src/mock/types/oracle.rs b/zrml/futarchy/src/mock/types/oracle.rs new file mode 100644 index 000000000..045c46de3 --- /dev/null +++ b/zrml/futarchy/src/mock/types/oracle.rs @@ -0,0 +1,69 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use alloc::fmt::Debug; +use frame_support::pallet_prelude::Weight; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::traits::Zero; +use zeitgeist_primitives::{traits::FutarchyOracle, types::BlockNumber}; + +#[cfg(feature = "fuzzing")] +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; + +#[derive(Clone, Debug, Decode, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct MockOracle { + weight: Weight, + value: bool, +} + +impl Default for MockOracle { + fn default() -> Self { + MockOracle { weight: Default::default(), value: true } + } +} + +impl MockOracle { + pub fn new(weight: Weight, value: bool) -> Self { + Self { weight, value } + } +} + +impl FutarchyOracle for MockOracle { + type BlockNumber = BlockNumber; + + fn evaluate(&self) -> (Weight, bool) { + (self.weight, self.value) + } + + fn update(&mut self, _: Self::BlockNumber) -> Weight { + Zero::zero() + } +} + +#[cfg(feature = "fuzzing")] +impl<'a> Arbitrary<'a> for MockOracle { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let ref_time = u64::arbitrary(u)?; + let proof_size = u64::arbitrary(u)?; + let weight = Weight::from_parts(ref_time, proof_size); + + let value = bool::arbitrary(u)?; + + Ok(MockOracle::new(weight, value)) + } +} diff --git a/zrml/futarchy/src/mock/types/scheduler.rs b/zrml/futarchy/src/mock/types/scheduler.rs new file mode 100644 index 000000000..499d2678d --- /dev/null +++ b/zrml/futarchy/src/mock/types/scheduler.rs @@ -0,0 +1,96 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{mock::runtime::Runtime, BoundedCallOf, CallOf, PalletsOriginOf}; +use core::cell::RefCell; +use frame_support::traits::schedule::{v3::Anon as ScheduleAnon, DispatchTime, Period, Priority}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::{DispatchError, DispatchResult}; + +pub struct MockScheduler; + +impl MockScheduler { + pub fn set_return_value(value: DispatchResult) { + SCHEDULER_RETURN_VALUE.with(|v| *v.borrow_mut() = value); + } + + pub fn not_called() -> bool { + SCHEDULER_CALL_DATA.with(|values| values.borrow().is_empty()) + } + + pub fn called_once_with( + when: DispatchTime>, + call: BoundedCallOf, + ) -> bool { + if SCHEDULER_CALL_DATA.with(|values| values.borrow().len()) != 1 { + return false; + } + + let args = SCHEDULER_CALL_DATA + .with(|value| value.borrow().first().expect("can't be empty").clone()); + + args == SchedulerCallData { when, call } + } +} + +#[derive(Clone, PartialEq)] +struct SchedulerCallData { + when: DispatchTime>, + call: BoundedCallOf, +} + +impl ScheduleAnon, CallOf, PalletsOriginOf> + for MockScheduler +{ + type Address = (); + + fn schedule( + when: DispatchTime>, + _maybe_periodic: Option>>, + _priority: Priority, + _origin: PalletsOriginOf, + call: BoundedCallOf, + ) -> Result { + SCHEDULER_CALL_DATA + .with(|values| values.borrow_mut().push(SchedulerCallData { when, call })); + + SCHEDULER_RETURN_VALUE.with(|value| *value.borrow()) + } + + fn cancel(_address: Self::Address) -> Result<(), DispatchError> { + unimplemented!(); + } + + fn reschedule( + _address: Self::Address, + _when: DispatchTime>, + ) -> Result { + unimplemented!(); + } + + fn next_dispatch_time( + _address: Self::Address, + ) -> Result, DispatchError> { + unimplemented!(); + } +} + +thread_local! { + pub static SCHEDULER_CALL_DATA: RefCell> = + const { RefCell::new(vec![]) }; + pub static SCHEDULER_RETURN_VALUE: RefCell = const { RefCell::new(Ok(())) }; +} diff --git a/zrml/futarchy/src/mock/utility.rs b/zrml/futarchy/src/mock/utility.rs new file mode 100644 index 000000000..fe2d8a16c --- /dev/null +++ b/zrml/futarchy/src/mock/utility.rs @@ -0,0 +1,37 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::mock::runtime::{Balances, Futarchy, System}; +use frame_support::traits::Hooks; +use zeitgeist_primitives::types::BlockNumber; + +pub fn run_to_block(to: BlockNumber) { + while System::block_number() < to { + let now = System::block_number(); + + Futarchy::on_finalize(now); + Balances::on_finalize(now); + System::on_finalize(now); + + let next = now + 1; + System::set_block_number(next); + + System::on_initialize(next); + Balances::on_initialize(next); + Futarchy::on_initialize(next); + } +} diff --git a/zrml/futarchy/src/pallet_impls.rs b/zrml/futarchy/src/pallet_impls.rs new file mode 100644 index 000000000..c53dbfd3d --- /dev/null +++ b/zrml/futarchy/src/pallet_impls.rs @@ -0,0 +1,52 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{types::Proposal, weights::WeightInfoZeitgeist, Config, Event, Pallet}; +use frame_support::{ + dispatch::RawOrigin, + pallet_prelude::Weight, + traits::schedule::{v3::Anon, DispatchTime, HARD_DEADLINE}, +}; +use zeitgeist_primitives::traits::FutarchyOracle; + +impl Pallet { + /// Evaluates `proposal` using the specified oracle and schedules the contained call if the + /// oracle approves. + pub(crate) fn maybe_schedule_proposal(proposal: Proposal) -> Weight { + let (evaluate_weight, approved) = proposal.oracle.evaluate(); + + if approved { + let result = T::Scheduler::schedule( + DispatchTime::At(proposal.when), + None, + HARD_DEADLINE, + RawOrigin::Root.into(), + proposal.call.clone(), + ); + + if result.is_ok() { + Self::deposit_event(Event::::Scheduled { proposal }); + } else { + Self::deposit_event(Event::::UnexpectedSchedulerError); + } + } else { + Self::deposit_event(Event::::Rejected { proposal }); + } + + T::WeightInfo::maybe_schedule_proposal().saturating_add(evaluate_weight) + } +} diff --git a/zrml/futarchy/src/proposal_storage.rs b/zrml/futarchy/src/proposal_storage.rs new file mode 100644 index 000000000..da7ce7e09 --- /dev/null +++ b/zrml/futarchy/src/proposal_storage.rs @@ -0,0 +1,105 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{ + traits::ProposalStorage, types::Proposal, Config, Error, Pallet, ProposalCount, Proposals, + ProposalsOf, +}; +use alloc::{collections::BTreeMap, vec, vec::Vec}; +use frame_support::{ensure, require_transactional, traits::Get}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::{DispatchError, SaturatedConversion}; +use zeitgeist_primitives::math::checked_ops_res::{CheckedIncRes, CheckedSubRes}; + +impl ProposalStorage for Pallet +where + T: Config, +{ + fn count() -> u32 { + ProposalCount::::get() + } + + #[require_transactional] + fn add(block_number: BlockNumberFor, proposal: Proposal) -> Result<(), DispatchError> { + let proposal_count = ProposalCount::::get(); + ensure!(proposal_count < T::MaxProposals::get(), Error::::CacheFull); + + let new_proposal_count = proposal_count.checked_inc_res()?; + ProposalCount::::put(new_proposal_count); + + // Can't error unless state is invalid. + let mutate_result = Proposals::::try_mutate(block_number, |proposals| { + proposals.try_push(proposal).map_err(|_| Error::::CacheFull) + }); + + Ok(mutate_result?) + } + + /// Take all proposals scheduled at `block_number`. + fn take(block_number: BlockNumberFor) -> Result, DispatchError> { + let proposals = Proposals::::take(block_number); + + // Can't error unless state is invalid. + let proposal_count = ProposalCount::::get(); + let proposals_len: u32 = proposals.len().try_into().map_err(|_| Error::::CacheFull)?; + let new_proposal_count = proposal_count.checked_sub_res(&proposals_len)?; + ProposalCount::::put(new_proposal_count); + + Ok(proposals) + } + + /// Returns all proposals scheduled at `block_number`. + fn get(block_number: BlockNumberFor) -> ProposalsOf { + Proposals::::get(block_number) + } + + fn mutate_all( + mut mutator: F, + ) -> Result, Vec>, DispatchError> + where + F: FnMut(&mut Proposal) -> R, + { + // Collect keys to avoid iterating over the keys whilst modifying the map. Won't saturate + // unless `usize` has fewer bits than `u32` for some reason. + let keys: Vec<_> = + Proposals::::iter_keys().take(T::MaxProposals::get().saturated_into()).collect(); + + let mut result_map = BTreeMap::new(); + + for k in keys.into_iter() { + let proposals = Self::get(k); + + let mut results = vec![]; + + // If mutation goes out of bounds, we've clearly failed. + let proposals = proposals + .try_mutate(|v| { + for p in v.iter_mut() { + let r = mutator(p); + results.push(r); + } + }) + .ok_or(Error::::UnexpectedStorageFailure)?; + + result_map.insert(k, results); + + Proposals::::insert(k, proposals); + } + + Ok(result_map) + } +} diff --git a/zrml/futarchy/src/tests/mod.rs b/zrml/futarchy/src/tests/mod.rs new file mode 100644 index 000000000..ea615c72b --- /dev/null +++ b/zrml/futarchy/src/tests/mod.rs @@ -0,0 +1,53 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(all(feature = "mock", test))] + +mod submit_proposal; + +use crate::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{Futarchy, Runtime, RuntimeOrigin, System}, + types::{MockOracle, MockScheduler}, + utility, + }, + types::Proposal, + Config, Error, Event, Proposals, ProposalsOf, +}; +use frame_support::{ + assert_noop, assert_ok, + dispatch::RawOrigin, + traits::{schedule::DispatchTime, Bounded}, +}; +use sp_runtime::DispatchError; + +/// Utility struct for managing test accounts. +pub(crate) struct Account { + id: ::AccountId, +} + +impl Account { + // TODO Not a pressing issue, but double booking accounts should be illegal. + pub(crate) fn new(id: ::AccountId) -> Account { + Account { id } + } + + pub(crate) fn signed(&self) -> RuntimeOrigin { + RuntimeOrigin::signed(self.id) + } +} diff --git a/zrml/futarchy/src/tests/submit_proposal.rs b/zrml/futarchy/src/tests/submit_proposal.rs new file mode 100644 index 000000000..7f687902c --- /dev/null +++ b/zrml/futarchy/src/tests/submit_proposal.rs @@ -0,0 +1,147 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; + +#[test] +fn submit_proposal_schedules_proposals() { + ExtBuilder::build().execute_with(|| { + let duration = ::MinDuration::get(); + + let call = Bounded::Inline(vec![7u8; 128].try_into().unwrap()); + let oracle = MockOracle::new(Default::default(), true); + let proposal = Proposal { when: Default::default(), call, oracle }; + + // This ensures that if the scheduler is erroneously called, the test doesn't fail due to a + // failure to configure the return value. + MockScheduler::set_return_value(Ok(())); + + assert_ok!(Futarchy::submit_proposal(RawOrigin::Root.into(), duration, proposal.clone())); + + System::assert_last_event( + Event::::Submitted { duration, proposal: proposal.clone() }.into(), + ); + + // Check that vector now contains proposal. + let now = System::block_number(); + let to_be_scheduled_at = now + duration; + assert_eq!(Proposals::get(to_be_scheduled_at).pop(), Some(proposal.clone())); + + utility::run_to_block(to_be_scheduled_at); + + // The proposal has now been removed and failed. + assert!(Proposals::::get(to_be_scheduled_at).is_empty()); + assert!(MockScheduler::called_once_with( + DispatchTime::At(proposal.when), + proposal.call.clone() + )); + + System::assert_last_event(Event::::Scheduled { proposal }.into()); + }); +} + +#[test] +fn submit_proposal_rejects_proposals() { + ExtBuilder::build().execute_with(|| { + let duration = ::MinDuration::get(); + + let call = Bounded::Inline(vec![7u8; 128].try_into().unwrap()); + let oracle = MockOracle::new(Default::default(), false); + let proposal = Proposal { when: Default::default(), call, oracle }; + + // This ensures that if the scheduler is erroneously called, the test doesn't fail due to a + // failure to configure the return value. + MockScheduler::set_return_value(Ok(())); + + assert_ok!(Futarchy::submit_proposal(RawOrigin::Root.into(), duration, proposal.clone())); + + System::assert_last_event( + Event::::Submitted { duration, proposal: proposal.clone() }.into(), + ); + + // Check that vector now contains proposal. + let now = System::block_number(); + let to_be_scheduled_at = now + duration; + assert_eq!(Proposals::get(to_be_scheduled_at).pop(), Some(proposal.clone())); + + utility::run_to_block(to_be_scheduled_at); + + // The proposal has now been removed and failed. + assert!(Proposals::::get(to_be_scheduled_at).is_empty()); + assert!(MockScheduler::not_called()); + + System::assert_last_event(Event::::Rejected { proposal }.into()); + }); +} + +#[test] +fn submit_proposal_fails_on_bad_origin() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0); + + let duration = ::MinDuration::get(); + + let call = Bounded::Inline(vec![7u8; 128].try_into().unwrap()); + let oracle = MockOracle::new(Default::default(), Default::default()); + let proposal = Proposal { when: Default::default(), call, oracle }; + + assert_noop!( + Futarchy::submit_proposal(alice.signed(), duration, proposal), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn submit_proposal_fails_if_duration_is_too_short() { + ExtBuilder::build().execute_with(|| { + let duration = ::MinDuration::get() - 1; + + let call = Bounded::Inline(vec![7u8; 128].try_into().unwrap()); + let oracle = MockOracle::new(Default::default(), Default::default()); + let proposal = Proposal { when: Default::default(), call, oracle }; + + assert_noop!( + Futarchy::submit_proposal(RawOrigin::Root.into(), duration, proposal), + Error::::DurationTooShort + ); + }); +} + +#[test] +fn submit_proposal_fails_if_cache_is_full() { + ExtBuilder::build().execute_with(|| { + let duration = ::MinDuration::get(); + + let call = Bounded::Inline(vec![7u8; 128].try_into().unwrap()); + let oracle = MockOracle::new(Default::default(), Default::default()); + let proposal = Proposal { when: Default::default(), call, oracle }; + + // Mock up a full vector of proposals. + let now = System::block_number(); + let to_be_scheduled_at = now + duration; + let max_proposals: u32 = ::MaxProposals::get(); + let proposals_vec = vec![proposal.clone(); max_proposals as usize]; + let proposals: ProposalsOf = proposals_vec.try_into().unwrap(); + Proposals::::insert(to_be_scheduled_at, proposals); + + assert_noop!( + Futarchy::submit_proposal(RawOrigin::Root.into(), duration, proposal), + Error::::CacheFull + ); + }); +} diff --git a/zrml/futarchy/src/traits/mod.rs b/zrml/futarchy/src/traits/mod.rs new file mode 100644 index 000000000..f092719ce --- /dev/null +++ b/zrml/futarchy/src/traits/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod proposal_storage; + +pub(crate) use proposal_storage::ProposalStorage; diff --git a/zrml/futarchy/src/traits/proposal_storage.rs b/zrml/futarchy/src/traits/proposal_storage.rs new file mode 100644 index 000000000..127c3dd91 --- /dev/null +++ b/zrml/futarchy/src/traits/proposal_storage.rs @@ -0,0 +1,45 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{types::Proposal, Config, ProposalsOf}; +use alloc::{collections::BTreeMap, vec::Vec}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_runtime::DispatchError; + +pub(crate) trait ProposalStorage +where + T: Config, +{ + /// Returns the number of proposals currently in flight. + #[allow(dead_code)] + fn count() -> u32; + + /// Schedule `proposal` for evaluation at `block_number`. + fn add(block_number: BlockNumberFor, proposal: Proposal) -> Result<(), DispatchError>; + + /// Take all proposals scheduled at `block_number`. + fn take(block_number: BlockNumberFor) -> Result, DispatchError>; + + /// Returns all proposals scheduled at `block_number`. + #[allow(dead_code)] + fn get(block_number: BlockNumberFor) -> ProposalsOf; + + /// Mutates all scheduled proposals. + fn mutate_all(mutator: F) -> Result, Vec>, DispatchError> + where + F: FnMut(&mut Proposal) -> R; +} diff --git a/zrml/futarchy/src/types/mod.rs b/zrml/futarchy/src/types/mod.rs new file mode 100644 index 000000000..db9310023 --- /dev/null +++ b/zrml/futarchy/src/types/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod proposal; + +pub use proposal::Proposal; diff --git a/zrml/futarchy/src/types/proposal.rs b/zrml/futarchy/src/types/proposal.rs new file mode 100644 index 000000000..722b058b9 --- /dev/null +++ b/zrml/futarchy/src/types/proposal.rs @@ -0,0 +1,68 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{BoundedCallOf, Config, OracleOf}; +use frame_support::{CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound}; +use frame_system::pallet_prelude::BlockNumberFor; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +#[cfg(feature = "fuzzing")] +use { + arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}, + frame_support::traits::Bounded, + sp_core::H256, +}; + +// TODO Make config a generic, keeps things simple. +#[derive( + CloneNoBound, Decode, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, +)] +#[scale_info(skip_type_params(S, T))] +pub struct Proposal +where + T: Config, +{ + /// The time at which the proposal will be enacted. + pub when: BlockNumberFor, + + /// The proposed call. + pub call: BoundedCallOf, + + /// The oracle that evaluates if the proposal should be enacted. + pub oracle: OracleOf, +} + +#[cfg(feature = "fuzzing")] +impl<'a, T> Arbitrary<'a> for Proposal +where + OracleOf: Arbitrary<'a>, + T: Config, +{ + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let when = u32::arbitrary(u)?.into(); + + let raw: [u8; 32] = Arbitrary::arbitrary(u)?; + let hash = H256(raw); + let len = u32::arbitrary(u)?; + let call = Bounded::Lookup { hash, len }; + + let oracle = Arbitrary::arbitrary(u)?; + + Ok(Proposal { when, call, oracle }) + } +} diff --git a/zrml/futarchy/src/weights.rs b/zrml/futarchy/src/weights.rs new file mode 100644 index 000000000..5a944e28c --- /dev/null +++ b/zrml/futarchy/src/weights.rs @@ -0,0 +1,100 @@ +// Copyright 2022-2025 Forecasting Technologies LTD. +// Copyright 2021-2022 Zeitgeist PM LLC. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +//! Autogenerated weights for zrml_futarchy +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: `2025-02-11`, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `msi-pro-b650-s`, CPU: `AMD Ryzen 9 7950X3D 16-Core Processor` +//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/zeitgeist +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=zrml_futarchy +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --template=./misc/weight_template.hbs +// --header=./HEADER_GPL3 +// --output=./zrml/futarchy/src/weights.rs + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use core::marker::PhantomData; +use frame_support::{traits::Get, weights::Weight}; + +/// Trait containing the required functions for weight retrival within +/// zrml_futarchy (automatically generated) +pub trait WeightInfoZeitgeist { + fn submit_proposal() -> Weight; + fn maybe_schedule_proposal() -> Weight; + fn take_proposals(n: u32) -> Weight; +} + +/// Weight functions for zrml_futarchy (automatically generated) +pub struct WeightInfo(PhantomData); +impl WeightInfoZeitgeist for WeightInfo { + /// Storage: `Futarchy::ProposalCount` (r:1 w:1) + /// Proof: `Futarchy::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Futarchy::Proposals` (r:1 w:1) + /// Proof: `Futarchy::Proposals` (`max_values`: None, `max_size`: Some(1261), added: 3736, mode: `MaxEncodedLen`) + fn submit_proposal() -> Weight { + // Proof Size summary in bytes: + // Measured: `122` + // Estimated: `4726` + // Minimum execution time: 14_000 nanoseconds. + Weight::from_parts(14_500_000, 4726) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + /// Storage: `Scheduler::Agenda` (r:1 w:1) + /// Proof: `Scheduler::Agenda` (`max_values`: None, `max_size`: Some(109074), added: 111549, mode: `MaxEncodedLen`) + fn maybe_schedule_proposal() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `112539` + // Minimum execution time: 10_120 nanoseconds. + Weight::from_parts(10_550_000, 112539) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Futarchy::Proposals` (r:1 w:1) + /// Proof: `Futarchy::Proposals` (`max_values`: None, `max_size`: Some(1261), added: 3736, mode: `MaxEncodedLen`) + /// Storage: `Futarchy::ProposalCount` (r:1 w:0) + /// Proof: `Futarchy::ProposalCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 4]`. + fn take_proposals(n: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `94 + n * (309 ±0)` + // Estimated: `4726` + // Minimum execution time: 5_610 nanoseconds. + Weight::from_parts(5_757_222, 4726) + // Standard Error: 3_602 + .saturating_add(Weight::from_parts(274_609, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} diff --git a/zrml/hybrid-router/Cargo.toml b/zrml/hybrid-router/Cargo.toml index 8525d961c..93e06cdd7 100644 --- a/zrml/hybrid-router/Cargo.toml +++ b/zrml/hybrid-router/Cargo.toml @@ -10,6 +10,7 @@ zeitgeist-primitives = { workspace = true } zrml-market-commons = { workspace = true } cfg-if = { workspace = true, optional = true } +env_logger = { workspace = true, optional = true } orml-asset-registry = { workspace = true, optional = true } orml-currencies = { workspace = true, optional = true } orml-tokens = { workspace = true, optional = true } @@ -23,6 +24,7 @@ sp-io = { workspace = true, optional = true } xcm = { workspace = true, optional = true } xcm-builder = { workspace = true, optional = true } zrml-authorized = { workspace = true, optional = true } +zrml-combinatorial-tokens = { workspace = true, optional = true } zrml-court = { workspace = true, optional = true } zrml-global-disputes = { workspace = true, optional = true } zrml-neo-swaps = { workspace = true, optional = true } @@ -30,7 +32,6 @@ zrml-orderbook = { workspace = true, optional = true } zrml-prediction-markets = { workspace = true, optional = true } [dev-dependencies] -env_logger = { workspace = true } test-case = { workspace = true } zrml-hybrid-router = { workspace = true, features = ["mock"] } @@ -38,6 +39,7 @@ zrml-hybrid-router = { workspace = true, features = ["mock"] } default = ["std"] mock = [ "cfg-if", + "env_logger/default", "orml-asset-registry/default", "orml-currencies/default", "orml-tokens/default", @@ -50,6 +52,7 @@ mock = [ "sp-io/default", "xcm/default", "zeitgeist-primitives/mock", + "zrml-combinatorial-tokens/default", "zrml-market-commons/default", "zrml-neo-swaps/default", "zrml-orderbook/default", @@ -68,6 +71,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", + "zrml-combinatorial-tokens/runtime-benchmarks", "zrml-prediction-markets/runtime-benchmarks", ] std = [ diff --git a/zrml/hybrid-router/src/lib.rs b/zrml/hybrid-router/src/lib.rs index 1544559d2..4dcd61b02 100644 --- a/zrml/hybrid-router/src/lib.rs +++ b/zrml/hybrid-router/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Forecasting Technologies LTD. +// Copyright 2024-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -20,9 +20,7 @@ extern crate alloc; -#[cfg(feature = "runtime-benchmarks")] mod benchmarking; -#[cfg(test)] mod mock; mod tests; mod types; diff --git a/zrml/hybrid-router/src/mock.rs b/zrml/hybrid-router/src/mock.rs index 1a7a454ad..3b3460849 100644 --- a/zrml/hybrid-router/src/mock.rs +++ b/zrml/hybrid-router/src/mock.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Forecasting Technologies LTD. +// Copyright 2024-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -28,6 +28,7 @@ use core::marker::PhantomData; use frame_support::{ construct_runtime, ord_parameter_types, parameter_types, traits::{Contains, Everything, NeverEnsureOrigin}, + Blake2_256, }; use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSignedBy}; use orml_traits::MultiCurrency; @@ -40,23 +41,26 @@ use zeitgeist_primitives::{ AddOutcomePeriod, AggregationPeriod, AppealBond, AppealPeriod, AuthorizedPalletId, BlockHashCount, BlocksPerYear, CloseEarlyBlockPeriod, CloseEarlyDisputeBond, CloseEarlyProtectionBlockPeriod, CloseEarlyProtectionTimeFramePeriod, - CloseEarlyRequestBond, CloseEarlyTimeFramePeriod, CorrectionPeriod, CourtPalletId, - ExistentialDeposit, ExistentialDeposits, GdVotingPeriod, GetNativeCurrencyId, - GlobalDisputeLockId, GlobalDisputesPalletId, HybridRouterPalletId, InflationPeriod, LockId, - MaxAppeals, MaxApprovals, MaxCourtParticipants, MaxCreatorFee, MaxDelegations, - MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, MaxGlobalDisputeVotes, MaxGracePeriod, - MaxLiquidityTreeDepth, MaxLocks, MaxMarketLifetime, MaxOracleDuration, MaxOrders, - MaxOwners, MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, MaxYearlyInflation, - MinCategories, MinDisputeDuration, MinJurorStake, MinOracleDuration, MinOutcomeVoteAmount, - MinimumPeriod, NeoMaxSwapFee, NeoSwapsPalletId, OrderbookPalletId, OutsiderBond, - PmPalletId, RemoveKeysLimit, RequestInterval, TreasuryPalletId, VotePeriod, + CloseEarlyRequestBond, CloseEarlyTimeFramePeriod, CombinatorialTokensPalletId, + CorrectionPeriod, CourtPalletId, ExistentialDeposit, ExistentialDeposits, GdVotingPeriod, + GetNativeCurrencyId, GlobalDisputeLockId, GlobalDisputesPalletId, HybridRouterPalletId, + InflationPeriod, LockId, MaxAppeals, MaxApprovals, MaxCourtParticipants, MaxCreatorFee, + MaxDelegations, MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, MaxGlobalDisputeVotes, + MaxGracePeriod, MaxLiquidityTreeDepth, MaxLocks, MaxMarketLifetime, MaxOracleDuration, + MaxOrders, MaxOwners, MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, + MaxYearlyInflation, MinCategories, MinDisputeDuration, MinJurorStake, MinOracleDuration, + MinOutcomeVoteAmount, MinimumPeriod, NeoMaxSwapFee, NeoSwapsPalletId, OrderbookPalletId, + OutsiderBond, PmPalletId, RemoveKeysLimit, RequestInterval, TreasuryPalletId, VotePeriod, VotingOutcomeFee, BASE, CENT, MAX_ASSETS, }, traits::DistributeFees, types::{ - AccountIdTest, Amount, Balance, BasicCurrencyAdapter, CurrencyId, Hash, MarketId, Moment, + AccountIdTest, Amount, Balance, BasicCurrencyAdapter, CombinatorialId, CurrencyId, Hash, + MarketId, Moment, }, }; +use zrml_combinatorial_tokens::types::{CryptographicIdManager, Fuel}; + #[cfg(feature = "parachain")] use { orml_traits::asset_registry::AssetProcessor, parity_scale_codec::Encode, @@ -64,6 +68,9 @@ use { zeitgeist_primitives::types::CustomMetadata, }; +#[cfg(feature = "runtime-benchmarks")] +use zeitgeist_primitives::types::NoopCombinatorialTokensBenchmarkHelper; + pub const ALICE: AccountIdTest = 0; #[allow(unused)] pub const BOB: AccountIdTest = 1; @@ -74,6 +81,7 @@ pub const FEE_ACCOUNT: AccountIdTest = 5; pub const SUDO: AccountIdTest = 123456; pub const EXTERNAL_FEES: Balance = CENT; pub const INITIAL_BALANCE: Balance = 100 * BASE; +#[allow(unused)] pub const MARKET_CREATOR: AccountIdTest = ALICE; #[cfg(feature = "parachain")] @@ -90,6 +98,7 @@ ord_parameter_types! { } parameter_types! { pub storage NeoMinSwapFee: Balance = 0; + pub storage MaxSplits: u16 = 128; } parameter_types! { pub const AdvisoryBond: Balance = 0; @@ -154,6 +163,7 @@ construct_runtime!( AssetRegistry: orml_asset_registry, Authorized: zrml_authorized, Balances: pallet_balances, + CombinatorialTokens: zrml_combinatorial_tokens, Court: zrml_court, AssetManager: orml_currencies, MarketCommons: zrml_market_commons, @@ -192,12 +202,17 @@ impl zrml_orderbook::Config for Runtime { } impl zrml_neo_swaps::Config for Runtime { - type MultiCurrency = AssetManager; + type CombinatorialId = CombinatorialId; + type CombinatorialTokens = CombinatorialTokens; + type CombinatorialTokensUnsafe = CombinatorialTokens; type CompleteSetOperations = PredictionMarkets; type ExternalFees = ExternalFees; type MarketCommons = MarketCommons; + type MultiCurrency = AssetManager; + type PoolId = MarketId; type RuntimeEvent = RuntimeEvent; type MaxLiquidityTreeDepth = MaxLiquidityTreeDepth; + type MaxSplits = MaxSplits; type MaxSwapFee = NeoMaxSwapFee; type PalletId = NeoSwapsPalletId; type WeightInfo = zrml_neo_swaps::weights::WeightInfo; @@ -262,6 +277,19 @@ impl zrml_authorized::Config for Runtime { type WeightInfo = zrml_authorized::weights::WeightInfo; } +impl zrml_combinatorial_tokens::Config for Runtime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = NoopCombinatorialTokensBenchmarkHelper; + type CombinatorialIdManager = CryptographicIdManager; + type Fuel = Fuel; + type MarketCommons = MarketCommons; + type MultiCurrency = AssetManager; + type Payout = PredictionMarkets; + type RuntimeEvent = RuntimeEvent; + type PalletId = CombinatorialTokensPalletId; + type WeightInfo = zrml_combinatorial_tokens::weights::WeightInfo; +} + impl zrml_court::Config for Runtime { type AppealBond = AppealBond; type BlocksPerYear = BlocksPerYear; @@ -304,7 +332,7 @@ impl frame_system::Config for Runtime { type Hashing = BlakeTwo256; type Lookup = IdentityLookup; type Nonce = u64; - type MaxConsumers = frame_support::traits::ConstU32<16>; + type MaxConsumers = ConstU32<16>; type OnKilledAccount = (); type OnNewAccount = (); type RuntimeOrigin = RuntimeOrigin; diff --git a/zrml/hybrid-router/src/tests/buy.rs b/zrml/hybrid-router/src/tests/buy.rs index dec84799d..8b8bade2a 100644 --- a/zrml/hybrid-router/src/tests/buy.rs +++ b/zrml/hybrid-router/src/tests/buy.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Forecasting Technologies LTD. +// Copyright 2024-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -69,7 +69,7 @@ fn buy_from_amm_and_then_fill_specified_order() { System::assert_has_event( NeoSwapsEvent::::BuyExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_out: asset, amount_in: amm_amount_in, amount_out: 5608094333, @@ -427,7 +427,7 @@ fn buy_from_amm() { System::assert_has_event( NeoSwapsEvent::::BuyExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_out: asset, amount_in: 20000000000, amount_out: 36852900215, @@ -532,7 +532,7 @@ fn buy_from_amm_but_low_amount() { System::assert_has_event( NeoSwapsEvent::::BuyExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_out: asset, amount_in: 30, amount_out: 60, @@ -595,7 +595,7 @@ fn buy_from_amm_only() { System::assert_has_event( NeoSwapsEvent::::BuyExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_out: asset, amount_in: 20000000000, amount_out: 36852900215, @@ -758,7 +758,7 @@ fn buy_emits_event() { asset_in: BASE_ASSET, amount_in, asset_out: asset, - amount_out: 2301256894490, + amount_out: 2301256894491, external_fee_amount: 3423314400, swap_fee_amount: 2273314407, } diff --git a/zrml/hybrid-router/src/tests/sell.rs b/zrml/hybrid-router/src/tests/sell.rs index 1d76359f5..3034e657f 100644 --- a/zrml/hybrid-router/src/tests/sell.rs +++ b/zrml/hybrid-router/src/tests/sell.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Forecasting Technologies LTD. +// Copyright 2024-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -71,7 +71,7 @@ fn sell_to_amm_and_then_fill_specified_order() { System::assert_has_event( NeoSwapsEvent::::SellExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_in: asset, amount_in: amm_amount_in, amount_out: 2775447716, @@ -445,7 +445,7 @@ fn sell_to_amm() { System::assert_has_event( NeoSwapsEvent::::SellExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_in: asset, amount_in: 20000000000, amount_out: 9460629504, @@ -556,7 +556,7 @@ fn sell_to_amm_but_low_amount() { System::assert_has_event( NeoSwapsEvent::::SellExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_in: asset, amount_in: 58, amount_out: 29, @@ -672,7 +672,7 @@ fn sell_to_amm_only() { System::assert_has_event( NeoSwapsEvent::::SellExecuted { who: ALICE, - market_id, + pool_id: market_id, asset_in: asset, amount_in: 20000000000, amount_out: 9460629504, diff --git a/zrml/neo-swaps/Cargo.toml b/zrml/neo-swaps/Cargo.toml index 6ca939dcb..d0fbb4e51 100644 --- a/zrml/neo-swaps/Cargo.toml +++ b/zrml/neo-swaps/Cargo.toml @@ -31,6 +31,7 @@ sp-io = { workspace = true, optional = true } xcm = { workspace = true, optional = true } xcm-builder = { workspace = true, optional = true } zrml-authorized = { workspace = true, optional = true } +zrml-combinatorial-tokens = { workspace = true, optional = true } zrml-court = { workspace = true, optional = true } zrml-global-disputes = { workspace = true, optional = true } zrml-prediction-markets = { workspace = true, optional = true } @@ -62,6 +63,9 @@ mock = [ "pallet-timestamp/default", "sp-api/default", "sp-io/default", + "zrml-combinatorial-tokens/std", + "zrml-combinatorial-tokens/mock", + "zrml-combinatorial-tokens/default", "zrml-court/std", "zrml-authorized/std", "zrml-global-disputes/std", @@ -78,6 +82,7 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", + "zrml-combinatorial-tokens/runtime-benchmarks", ] std = [ "frame-benchmarking?/std", diff --git a/zrml/neo-swaps/docs/docs.tex b/zrml/neo-swaps/docs/docs.tex index 3b18abca4..3733af17f 100644 --- a/zrml/neo-swaps/docs/docs.tex +++ b/zrml/neo-swaps/docs/docs.tex @@ -36,7 +36,7 @@ \section{Introduction} -This document provides the mathematical and technical details for zrml-neo-swaps. The automatic market maker (AMM) implemented by zrml-neo-swaps is a variant of the Logarithmic Market Scoring Rule (LMSR; \cite{hanson_2003}) which was first developed by Gnosis (see \url{https://docs.gnosis.io/conditionaltokens/docs/introduction3/}). We often refer to it as AMM 2.0. +This document provides the mathematical and technical details for zrml-neo-swaps. The automatic market maker (AMM) implemented by zrml-neo-swaps is a variant of the Logarithmic Market Scoring Rule (LMSR; \cite{hanson_2003}) which was first developed by Gnosis (see \url{https://gnosis-conditional-tokens.readthedocs.io/en/latest/developer-guide.html#}). We often refer to it as AMM 2.0. Unlike the typical implementation using a cost function (see \cite{chen_vaughan_2010}), this implementation of LMSR is a \emph{constant-function market maker} (CFMM), similar to the classical constant product market maker, which allows us to implement \emph{dynamic liquidity}. In other words, liquidity providers (LPs) can come and go as they please, allowing the market to self-regulate how much price resistance the AMM should provide. diff --git a/zrml/neo-swaps/fuzz/Cargo.toml b/zrml/neo-swaps/fuzz/Cargo.toml new file mode 100644 index 000000000..cf5683e08 --- /dev/null +++ b/zrml/neo-swaps/fuzz/Cargo.toml @@ -0,0 +1,38 @@ +[[bin]] +doc = false +name = "deploy_combinatorial_pool" +path = "deploy_combinatorial_pool.rs" +test = false + +[[bin]] +doc = false +name = "combo_buy" +path = "combo_buy.rs" +test = false + +[[bin]] +doc = false +name = "combo_sell" +path = "combo_sell.rs" +test = false + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +frame-support = { workspace = true, features = ["default"] } +frame-system = { workspace = true } +libfuzzer-sys = { workspace = true } +orml-traits = { workspace = true, features = ["default"] } +rand = { workspace = true, features = ["default"] } +sp-runtime = { workspace = true, features = ["default"] } +zeitgeist-primitives = { workspace = true, features = ["default", "mock"] } +zrml-neo-swaps = { workspace = true, features = ["default", "mock"] } + +[package] +authors = ["Forecasting Technologies Ltd"] +edition.workspace = true +name = "zrml-neo-swaps-fuzz" +publish = false +version = "0.5.5" + +[package.metadata] +cargo-fuzz = true diff --git a/zrml/neo-swaps/fuzz/combo_buy.rs b/zrml/neo-swaps/fuzz/combo_buy.rs new file mode 100644 index 000000000..121807b6a --- /dev/null +++ b/zrml/neo-swaps/fuzz/combo_buy.rs @@ -0,0 +1,190 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use rand::seq::SliceRandom; +use sp_runtime::traits::Zero; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::{CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, MarketType}, +}; +use zrml_neo_swaps::{ + mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, + AccountIdOf, BalanceOf, Config, FuelOf, MarketIdOf, MAX_SPOT_PRICE, MIN_SPOT_PRICE, + MIN_SWAP_FEE, +}; + +#[derive(Debug)] +struct ComboBuyFuzzParams { + account_id: AccountIdOf, + pool_id: ::PoolId, + market_ids: Vec>, + spot_prices: Vec>, + swap_fee: BalanceOf, + category_counts: Vec, + asset_count: u16, + buy: Vec, + keep: Vec, + sell: Vec, + amount_buy: BalanceOf, + amount_keep: BalanceOf, + min_amount_out: BalanceOf, +} + +impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let pool_id = 0; + let market_ids = vec![0, 1, 2]; + + let min_category_count = 2; + let max_category_count = 16; + let mut category_counts = vec![]; + for _ in market_ids.iter() { + // We're just assuming three markets here! + let category_count = u.int_in_range(min_category_count..=max_category_count)? as u16; + category_counts.push(category_count); + } + + let asset_count = category_counts.iter().product(); + let asset_count_usize = asset_count as usize; + + // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding + // value to them in increments until a total spot price of one is reached. It's possible + // that this results in invalid spot prices, for example if `total_assets` is too large. + let mut spot_prices = vec![MIN_SPOT_PRICE; asset_count_usize]; + let increment = MIN_SPOT_PRICE; + while spot_prices.iter().sum::() < _1 { + let index = u.int_in_range(0..=asset_count_usize - 1)?; + if spot_prices[index] < MAX_SPOT_PRICE { + spot_prices[index] += increment; + } + } + + let swap_fee = u.int_in_range(MIN_SWAP_FEE..=::MaxSwapFee::get())?; + + // Shuffle 0..asset_count_usize and then obtain `buy` and `sell` from the result. + let mut indices: Vec = (0..asset_count_usize).collect(); + for i in (1..indices.len()).rev() { + let j = u.int_in_range(0..=i)?; + indices.swap(i, j); + } + + // This isn't perfectly random, but biased towards producing larger `buy` sets. + let buy_len = u.int_in_range(1..=asset_count_usize - 1)?; + let keep_len = u.int_in_range(0..=asset_count_usize - 1 - buy_len)?; + let buy = indices[0..buy_len].to_vec(); + let keep = indices[buy_len..buy_len + keep_len].to_vec(); + let sell = indices[buy_len + keep_len..asset_count_usize].to_vec(); + + let amount_buy = u.int_in_range(_1..=_100)?; + let amount_keep = + if keep.is_empty() { Zero::zero() } else { u.int_in_range(_1..=amount_buy)? }; + + let min_amount_out = Arbitrary::arbitrary(u)?; + + let params = ComboBuyFuzzParams { + account_id, + pool_id, + market_ids, + spot_prices, + swap_fee, + category_counts, + asset_count, + buy, + keep, + sell, + amount_buy, + amount_keep, + min_amount_out, + }; + + Ok(params) + } +} + +fuzz_target!(|params: ComboBuyFuzzParams| { + let mut ext = ExtBuilder::default().build(); + + ext.execute_with(|| { + // We create the required markets and deposit collateral in the user's account. + let collateral = Asset::Ztg; + for (market_id, &category_count) in params.category_counts.iter().enumerate() { + let market = common::market::( + market_id as u128, + collateral, + MarketType::Categorical(category_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + } + <::MultiCurrency>::deposit( + collateral, + ¶ms.account_id, + 100 * params.amount_buy, + ) + .unwrap(); + + // Create a pool to trade on. + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(params.account_id), + params.asset_count, + params.market_ids, + 10 * params.amount_buy, + params.spot_prices, + params.swap_fee, + FuelOf::::from_total(16), + ) + .unwrap(); + + // Convert indices to assets an deposit funds for the user. + let assets = NeoSwaps::assets(params.pool_id).unwrap(); + for &asset in assets.iter() { + <::MultiCurrency>::deposit( + asset, + ¶ms.account_id, + params.amount_buy, + ) + .unwrap(); + } + + let buy = params.buy.into_iter().map(|i| assets[i]).collect(); + let keep = params.keep.into_iter().map(|i| assets[i]).collect(); + let sell = params.sell.into_iter().map(|i| assets[i]).collect(); + + let _ = NeoSwaps::combo_sell( + RuntimeOrigin::signed(params.account_id), + params.pool_id, + params.asset_count, + buy, + keep, + sell, + params.amount_buy, + params.amount_keep, + params.min_amount_out, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/neo-swaps/fuzz/combo_sell.rs b/zrml/neo-swaps/fuzz/combo_sell.rs new file mode 100644 index 000000000..5c44c19b7 --- /dev/null +++ b/zrml/neo-swaps/fuzz/combo_sell.rs @@ -0,0 +1,166 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use rand::seq::SliceRandom; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::{CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, MarketType}, +}; +use zrml_neo_swaps::{ + mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, + AccountIdOf, BalanceOf, Config, FuelOf, MarketIdOf, MAX_SPOT_PRICE, MIN_SPOT_PRICE, + MIN_SWAP_FEE, +}; + +#[derive(Debug)] +struct ComboBuyFuzzParams { + account_id: AccountIdOf, + pool_id: ::PoolId, + market_ids: Vec>, + spot_prices: Vec>, + swap_fee: BalanceOf, + category_counts: Vec, + asset_count: u16, + buy: Vec, + sell: Vec, + amount_in: BalanceOf, + min_amount_out: BalanceOf, +} + +impl<'a> Arbitrary<'a> for ComboBuyFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + let pool_id = 0; + let market_ids = vec![0, 1, 2]; + + let min_category_count = 2; + let max_category_count = 16; + let mut category_counts = vec![]; + for _ in market_ids.iter() { + // We're just assuming three markets here! + let category_count = u.int_in_range(min_category_count..=max_category_count)? as u16; + category_counts.push(category_count); + } + + let asset_count = category_counts.iter().product(); + let asset_count_usize = asset_count as usize; + + // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding + // value to them in increments until a total spot price of one is reached. It's possible + // that this results in invalid spot prices, for example if `total_assets` is too large. + let mut spot_prices = vec![MIN_SPOT_PRICE; asset_count_usize]; + let increment = MIN_SPOT_PRICE; + while spot_prices.iter().sum::() < _1 { + let index = u.int_in_range(0..=asset_count_usize - 1)?; + if spot_prices[index] < MAX_SPOT_PRICE { + spot_prices[index] += increment; + } + } + + let swap_fee = u.int_in_range(MIN_SWAP_FEE..=::MaxSwapFee::get())?; + + // Shuffle 0..asset_count_usize and then obtain `buy` and `sell` from the result. + let mut indices: Vec = (0..asset_count_usize).collect(); + for i in (1..indices.len()).rev() { + let j = u.int_in_range(0..=i)?; + indices.swap(i, j); + } + let buy_len = u.int_in_range(1..=asset_count_usize - 1)?; + let buy = indices[0..buy_len].to_vec(); + let sell = indices[buy_len..asset_count_usize].to_vec(); + + let amount_in = u.int_in_range(_1..=_100)?; + let min_amount_out = Arbitrary::arbitrary(u)?; + + let params = ComboBuyFuzzParams { + account_id, + pool_id, + market_ids, + spot_prices, + swap_fee, + category_counts, + asset_count, + buy, + sell, + amount_in, + min_amount_out, + }; + + Ok(params) + } +} + +fuzz_target!(|params: ComboBuyFuzzParams| { + let mut ext = ExtBuilder::default().build(); + + ext.execute_with(|| { + // We create the required markets and deposit enough funds for the user. + let collateral = Asset::Ztg; + for (market_id, &category_count) in params.category_counts.iter().enumerate() { + let market = common::market::( + market_id as u128, + collateral, + MarketType::Categorical(category_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + } + <::MultiCurrency>::deposit( + collateral, + ¶ms.account_id, + 100 * params.amount_in, + ) + .unwrap(); + + // Create a pool to trade on. + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(params.account_id), + params.asset_count, + params.market_ids, + 10 * params.amount_in, + params.spot_prices, + params.swap_fee, + FuelOf::::from_total(16), + ) + .unwrap(); + + // Convert indices to assets. + let assets = NeoSwaps::assets(params.pool_id).unwrap(); + let buy = params.buy.into_iter().map(|i| assets[i]).collect(); + let sell = params.sell.into_iter().map(|i| assets[i]).collect(); + + let _ = NeoSwaps::combo_buy( + RuntimeOrigin::signed(params.account_id), + params.pool_id, + params.asset_count, + buy, + sell, + params.amount_in, + params.min_amount_out, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/neo-swaps/fuzz/common.rs b/zrml/neo-swaps/fuzz/common.rs new file mode 100644 index 000000000..8ee4e7a18 --- /dev/null +++ b/zrml/neo-swaps/fuzz/common.rs @@ -0,0 +1,52 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use zeitgeist_primitives::{ + traits::MarketOf, + types::{Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, +}; +use zrml_neo_swaps::{AssetOf, Config, MarketIdOf}; + +pub(crate) fn market( + market_id: MarketIdOf, + base_asset: AssetOf, + market_type: MarketType, +) -> MarketOf<::MarketCommons> +where + T: Config, + ::AccountId: Default, +{ + Market { + market_id, + base_asset, + creator: Default::default(), + creation: MarketCreation::Permissionless, + creator_fee: Default::default(), + oracle: Default::default(), + metadata: Default::default(), + market_type, + period: MarketPeriod::Block(0u8.into()..10u8.into()), + deadlines: Default::default(), + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Active, + report: None, + resolved_outcome: None, + dispute_mechanism: None, + bonds: Default::default(), + early_close: None, + } +} diff --git a/zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs b/zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs new file mode 100644 index 000000000..738b2f1e5 --- /dev/null +++ b/zrml/neo-swaps/fuzz/deploy_combinatorial_pool.rs @@ -0,0 +1,138 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![no_main] + +mod common; + +use arbitrary::{Arbitrary, Result as ArbitraryResult, Unstructured}; +use libfuzzer_sys::fuzz_target; +use orml_traits::currency::MultiCurrency; +use zeitgeist_primitives::{ + constants::base_multiples::*, + traits::{CombinatorialTokensFuel, MarketCommonsPalletApi}, + types::{Asset, MarketType}, +}; +use zrml_neo_swaps::{ + mock::{ExtBuilder, NeoSwaps, Runtime, RuntimeOrigin}, + AccountIdOf, BalanceOf, Config, FuelOf, MarketIdOf, COMBO_MAX_SPOT_PRICE, COMBO_MIN_SPOT_PRICE, + MIN_SWAP_FEE, +}; + +#[derive(Debug)] +struct DeployCombinatorialPoolFuzzParams { + account_id: AccountIdOf, + asset_count: u16, + market_ids: Vec>, + category_counts: Vec, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, + fuel: FuelOf, +} + +impl<'a> Arbitrary<'a> for DeployCombinatorialPoolFuzzParams { + fn arbitrary(u: &mut Unstructured<'a>) -> ArbitraryResult { + let account_id = u128::arbitrary(u)?; + + let min_market_ids = 1; + let max_market_ids = 16; + let market_ids_len: usize = u.int_in_range(min_market_ids..=max_market_ids)?; + let market_ids = + (0..market_ids_len).map(|x| (x as u32).into()).collect::>>(); + + let min_category_count = 2; + let max_category_count = 16; + let mut category_counts = vec![]; + for _ in market_ids.iter() { + let category_count = u.int_in_range(min_category_count..=max_category_count)? as u16; + category_counts.push(category_count); + } + + let amount = Arbitrary::arbitrary(u)?; + + let asset_count: u16 = category_counts.iter().product(); + let asset_count_usize = asset_count as usize; + + // Create arbitrary spot price vector by creating a vector of `MinSpotPrice` and then adding + // value to them in increments until a total spot price of one is reached. It's possible + // that this results in invalid spot prices, for example if `total_assets` is too large. + let mut spot_prices = vec![COMBO_MIN_SPOT_PRICE; asset_count_usize]; + let increment = COMBO_MIN_SPOT_PRICE; + while spot_prices.iter().sum::() < _1 { + let index = u.int_in_range(0..=asset_count_usize - 1)?; + if spot_prices[index] < COMBO_MAX_SPOT_PRICE { + spot_prices[index] += increment; + } + } + + let swap_fee = u.int_in_range(MIN_SWAP_FEE..=::MaxSwapFee::get())?; + + let fuel = FuelOf::::from_total(u.int_in_range(1..=100)?); + + let params = DeployCombinatorialPoolFuzzParams { + account_id, + asset_count: asset_count as u16, + market_ids, + category_counts, + amount, + spot_prices, + swap_fee, + fuel, + }; + + Ok(params) + } +} + +fuzz_target!(|params: DeployCombinatorialPoolFuzzParams| { + let mut ext = ExtBuilder::default().build(); + + ext.execute_with(|| { + // We create the required markets and deposit enough funds for the user. + let collateral = Asset::Ztg; + for (&market_id, &category_count) in + params.market_ids.iter().zip(params.category_counts.iter()) + { + let market = common::market::( + market_id, + collateral, + MarketType::Categorical(category_count), + ); + <::MarketCommons as MarketCommonsPalletApi>::push_market(market) + .unwrap(); + } + <::MultiCurrency>::deposit( + collateral, + ¶ms.account_id, + params.amount, + ) + .unwrap(); + + let _ = NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(params.account_id), + params.asset_count, + params.market_ids, + params.amount, + params.spot_prices, + params.swap_fee, + params.fuel, + ); + }); + + let _ = ext.commit_all(); +}); diff --git a/zrml/neo-swaps/src/benchmarking.rs b/zrml/neo-swaps/src/benchmarking.rs index 2290dba9d..795090e98 100644 --- a/zrml/neo-swaps/src/benchmarking.rs +++ b/zrml/neo-swaps/src/benchmarking.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -20,7 +20,8 @@ use super::*; use crate::{ liquidity_tree::{traits::LiquidityTreeHelper, types::LiquidityTree}, - traits::{liquidity_shares_manager::LiquiditySharesManager, pool_operations::PoolOperations}, + traits::{LiquiditySharesManager, PoolOperations, PoolStorage}, + types::{DecisionMarketOracle, DecisionMarketOracleScoreboard}, AssetOf, BalanceOf, MarketIdOf, Pallet as NeoSwaps, Pools, MIN_SPOT_PRICE, }; use alloc::{vec, vec::Vec}; @@ -39,7 +40,7 @@ use sp_runtime::{ use zeitgeist_primitives::{ constants::{base_multiples::*, CENT}, math::fixed::{BaseProvider, FixedDiv, FixedMul, ZeitgeistBase}, - traits::CompleteSetOperationsApi, + traits::{CombinatorialTokensFuel, CompleteSetOperationsApi, FutarchyOracle}, types::{Asset, Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule}, }; use zrml_market_commons::MarketCommonsPalletApi; @@ -492,6 +493,228 @@ mod benchmarks { ); } + // Remark on benchmarks for combinatorial pools: Combinatorial buying, selling and deploying + // pools depends on the number of assets as well as the number of markets. But these parameters + // depend on each other (the more markets, the more assets). The benchmark parameter is the + // market count and the logarithm of the number of assets. This maximizes the number of markets + // per asset. + + #[benchmark] + fn combo_buy(n: Linear<1, 7>) { + let market_count = n; + + let alice: T::AccountId = whitelisted_caller(); + let base_asset = Asset::Ztg; + let asset_count = 2u16.pow(market_count); + + let mut market_ids = vec![]; + for _ in 0..market_count { + let market_id = create_market::(alice.clone(), base_asset, 2); + market_ids.push(market_id); + } + + let amount = (100 * _100).saturated_into(); + let total_cost = amount + T::MultiCurrency::minimum_balance(base_asset); + assert_ok!(T::MultiCurrency::deposit(base_asset, &alice, total_cost)); + assert_ok!(NeoSwaps::::deploy_combinatorial_pool( + RawOrigin::Signed(alice).into(), + asset_count, + market_ids, + amount, + create_spot_prices::(asset_count), + CENT.saturated_into(), + FuelOf::::from_total(16), + )); + + let pool_id = 0u8.into(); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + let amount_in = _1.saturated_into(); + let min_amount_out = Zero::zero(); + + // Work is maximized by having no keep indicies. + let middle = asset_count / 2; + let buy_arg = (0..middle).map(|i| assets[i as usize]).collect::>(); + let sell_arg = (middle..asset_count).map(|i| assets[i as usize]).collect::>(); + + let helper = BenchmarkHelper::::new(); + let bob = helper.accounts().next().unwrap(); + assert_ok!(T::MultiCurrency::deposit(base_asset, &bob, amount_in)); + + #[extrinsic_call] + _( + RawOrigin::Signed(bob), + pool_id, + asset_count, + buy_arg, + sell_arg, + amount_in, + min_amount_out, + ); + } + + #[benchmark] + fn combo_sell(n: Linear<1, 7>) { + let market_count = n; + + let alice: T::AccountId = whitelisted_caller(); + let base_asset = Asset::Ztg; + let asset_count = 2u16.pow(market_count); + + let mut market_ids = vec![]; + for _ in 0..market_count { + let market_id = create_market::(alice.clone(), base_asset, 2); + market_ids.push(market_id); + } + + let amount = (100 * _100).saturated_into(); + let total_cost = amount + T::MultiCurrency::minimum_balance(base_asset); + assert_ok!(T::MultiCurrency::deposit(base_asset, &alice, total_cost)); + assert_ok!(NeoSwaps::::deploy_combinatorial_pool( + RawOrigin::Signed(alice).into(), + asset_count, + market_ids, + amount, + create_spot_prices::(asset_count), + CENT.saturated_into(), + FuelOf::::from_total(16), + )); + + let pool_id = 0u8.into(); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + // Work is maximized by having as few sell indices as possible. + let buy_arg = vec![assets[0]]; + let sell_arg = vec![assets[1]]; + let keep_arg = (2..asset_count).map(|i| assets[i as usize]).collect::>(); + + let amount_buy: BalanceOf = (100 * _2).saturated_into(); + let amount_keep = if keep_arg.is_empty() { + // If n = 1; + Zero::zero() + } else { + _1.saturated_into() + }; + let min_amount_out = Zero::zero(); + + let helper = BenchmarkHelper::::new(); + let bob = helper.accounts().next().unwrap(); + + // We don't care about being precise here and just deposit a huge bunch of tokens for Bob. + for &asset in assets.iter() { + let amount_for_bob = amount_buy; + assert_ok!(T::MultiCurrency::deposit(asset, &bob, amount_for_bob)); + } + + #[extrinsic_call] + _( + RawOrigin::Signed(bob), + pool_id, + asset_count, + buy_arg, + keep_arg, + sell_arg, + amount_buy, + amount_keep, + min_amount_out, + ); + } + + #[benchmark] + fn deploy_combinatorial_pool(n: Linear<1, 7>, m: Linear<32, 64>) { + let market_count = n; + let total = m; + + let alice: T::AccountId = whitelisted_caller(); + let base_asset = Asset::Ztg; + let asset_count = 2u16.pow(market_count); + + let mut market_ids = vec![]; + for _ in 0..market_count { + let market_id = create_market::(alice.clone(), base_asset, 2); + market_ids.push(market_id); + } + + let amount = (100 * _100).saturated_into(); + let total_cost = amount + T::MultiCurrency::minimum_balance(base_asset); + assert_ok!(T::MultiCurrency::deposit(base_asset, &alice, total_cost)); + + let spot_prices = create_spot_prices::(asset_count); + let swap_fee = CENT.saturated_into(); + + #[extrinsic_call] + _( + RawOrigin::Signed(alice), + asset_count, + market_ids, + amount, + spot_prices, + swap_fee, + FuelOf::::from_total(total), + ); + } + + #[benchmark] + fn decision_market_oracle_evaluate() { + let alice = whitelisted_caller(); + let base_asset = Asset::Ztg; + let asset_count = 2; + let market_id = create_market_and_deploy_pool::( + alice, + base_asset, + asset_count, + (100 * _100).saturated_into(), + ); + + let pool = Pools::::get(market_id).unwrap(); + let assets = pool.assets(); + + let scoreboard = DecisionMarketOracleScoreboard::::new( + Zero::zero(), + Zero::zero(), + Zero::zero(), + Zero::zero(), + ); + let oracle = DecisionMarketOracle::::new(market_id, assets[0], assets[1], scoreboard); + + #[block] + { + let _ = oracle.evaluate(); + } + } + + #[benchmark] + fn decision_market_oracle_update() { + let alice = whitelisted_caller(); + let base_asset = Asset::Ztg; + let asset_count = 2; + let market_id = create_market_and_deploy_pool::( + alice, + base_asset, + asset_count, + (100 * _100).saturated_into(), + ); + + let pool = Pools::::get(market_id).unwrap(); + let assets = pool.assets(); + + let scoreboard = DecisionMarketOracleScoreboard::::new( + Zero::zero(), + Zero::zero(), + Zero::zero(), + Zero::zero(), + ); + let mut oracle = + DecisionMarketOracle::::new(market_id, assets[0], assets[1], scoreboard); + + #[block] + { + let _ = oracle.update(1u8.into()); + } + } + impl_benchmark_test_suite!( NeoSwaps, crate::mock::ExtBuilder::default().build(), diff --git a/zrml/neo-swaps/src/lib.rs b/zrml/neo-swaps/src/lib.rs index 07d3d3566..b86d24d21 100644 --- a/zrml/neo-swaps/src/lib.rs +++ b/zrml/neo-swaps/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -17,6 +17,8 @@ #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] extern crate alloc; @@ -27,10 +29,12 @@ mod liquidity_tree; mod macros; mod math; pub mod migration; -mod mock; +pub mod mock; +mod pool_storage; mod tests; pub mod traits; pub mod types; +mod utility; pub mod weights; pub use pallet::*; @@ -40,74 +44,107 @@ mod pallet { use crate::{ consts::LN_NUMERICAL_LIMIT, liquidity_tree::types::{BenchmarkInfo, LiquidityTree, LiquidityTreeError}, - math::{Math, MathOps}, - traits::{pool_operations::PoolOperations, LiquiditySharesManager}, - types::{FeeDistribution, MaxAssets, Pool}, + math::{traits::MathOps, types::Math}, + traits::{LiquiditySharesManager, PoolOperations, PoolStorage}, + types::{FeeDistribution, MaxAssets, Pool, PoolType}, + utility::LogCeil, weights::*, }; - use alloc::{collections::BTreeMap, vec, vec::Vec}; + use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec, + vec::Vec, + }; use core::marker::PhantomData; use frame_support::{ dispatch::DispatchResultWithPostInfo, ensure, - pallet_prelude::StorageMap, + pallet_prelude::{StorageMap, StorageValue, ValueQuery}, require_transactional, traits::{Get, IsType, StorageVersion}, - transactional, PalletError, PalletId, Twox64Concat, + transactional, PalletError, PalletId, Parameter, Twox64Concat, }; use frame_system::{ ensure_signed, pallet_prelude::{BlockNumberFor, OriginFor}, }; use orml_traits::MultiCurrency; - use parity_scale_codec::{Decode, Encode}; + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{ - traits::{AccountIdConversion, CheckedSub, Saturating, Zero}, + traits::{ + AccountIdConversion, AtLeast32Bit, CheckedSub, MaybeSerializeDeserialize, Member, + Saturating, Zero, + }, DispatchError, DispatchResult, Perbill, RuntimeDebug, SaturatedConversion, }; use zeitgeist_primitives::{ constants::{BASE, CENT}, hybrid_router_api_types::{AmmSoftFail, AmmTrade, ApiError}, math::{ - checked_ops_res::{CheckedAddRes, CheckedSubRes}, + checked_ops_res::{CheckedAddRes, CheckedMulRes, CheckedSubRes}, fixed::{BaseProvider, FixedDiv, FixedMul, ZeitgeistBase}, }, - traits::{CompleteSetOperationsApi, DeployPoolApi, DistributeFees, HybridRouterAmmApi}, + traits::{ + CombinatorialTokensApi, CombinatorialTokensFuel, CombinatorialTokensUnsafeApi, + CompleteSetOperationsApi, DeployPoolApi, DistributeFees, HybridRouterAmmApi, + }, types::{Asset, MarketStatus, ScoringRule}, }; use zrml_market_commons::MarketCommonsPalletApi; - pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); + pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(3); // These should not be config parameters to avoid misconfigurations. pub(crate) const EXIT_FEE: u128 = CENT / 10; /// The minimum allowed swap fee. Hardcoded to avoid misconfigurations which may lead to /// exploits. - pub(crate) const MIN_SWAP_FEE: u128 = BASE / 1_000; // 0.1%. + pub const MIN_SWAP_FEE: u128 = BASE / 1_000; // 0.1%. /// The maximum allowed spot price when creating a pool. - pub(crate) const MAX_SPOT_PRICE: u128 = BASE - CENT / 2; + pub const MAX_SPOT_PRICE: u128 = BASE - CENT / 2; /// The minimum allowed spot price when creating a pool. - pub(crate) const MIN_SPOT_PRICE: u128 = CENT / 2; + pub const MIN_SPOT_PRICE: u128 = CENT / 2; + /// The maximum value the spot price is allowed to take in a combinatorial market. + pub const COMBO_MAX_SPOT_PRICE: u128 = BASE - CENT / 10; + /// The minimum value the spot price is allowed to take in a combinatorial market. + pub const COMBO_MIN_SPOT_PRICE: u128 = CENT / 10; /// The minimum vallowed value of a pool's liquidity parameter. pub(crate) const MIN_LIQUIDITY: u128 = BASE; /// The minimum percentage each new LP position must increase the liquidity by, represented as /// fractional (0.0139098411 represents 1.39098411%). pub(crate) const MIN_RELATIVE_LP_POSITION_VALUE: u128 = 139098411; // 1.39098411% - pub(crate) type AccountIdOf = ::AccountId; - pub(crate) type AssetOf = Asset>; - pub(crate) type BalanceOf = + pub type AccountIdOf = ::AccountId; + pub type AssetOf = Asset>; + pub type BalanceOf = <::MultiCurrency as MultiCurrency>>::Balance; + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub type FuelOf = <::CombinatorialTokens as CombinatorialTokensApi>::Fuel; pub(crate) type AssetIndexType = u16; - pub(crate) type MarketIdOf = - <::MarketCommons as MarketCommonsPalletApi>::MarketId; pub(crate) type LiquidityTreeOf = LiquidityTree::MaxLiquidityTreeDepth>; pub(crate) type PoolOf = Pool, MaxAssets>; pub(crate) type AmmTradeOf = AmmTrade>; #[pallet::config] pub trait Config: frame_system::Config { + /// Type of combinatorial ID used by the combinatorial tokens APIs. + type CombinatorialId: Clone; + + /// API used for calculating splits of tokens when creating combinatorial pools. + type CombinatorialTokens: CombinatorialTokensApi< + AccountId = Self::AccountId, + Balance = BalanceOf, + CombinatorialId = Self::CombinatorialId, + MarketId = MarketIdOf, + >; + + /// API for fast creation of tokens when buying or selling combinatorial tokens. + type CombinatorialTokensUnsafe: CombinatorialTokensUnsafeApi< + AccountId = Self::AccountId, + Balance = BalanceOf, + MarketId = MarketIdOf, + >; + type CompleteSetOperations: CompleteSetOperationsApi< AccountId = Self::AccountId, Balance = BalanceOf, @@ -123,10 +160,23 @@ mod pallet { MarketId = MarketIdOf, >; - type MarketCommons: MarketCommonsPalletApi>; + type MarketCommons: MarketCommonsPalletApi< + AccountId = Self::AccountId, + BlockNumber = BlockNumberFor, + Balance = BalanceOf, + MarketId = Self::PoolId, + >; type MultiCurrency: MultiCurrency>; + type PoolId: AtLeast32Bit + + Copy + + Default + + MaxEncodedLen + + MaybeSerializeDeserialize + + Member + + Parameter; + type RuntimeEvent: From> + IsType<::RuntimeEvent>; type WeightInfo: WeightInfoZeitgeist; @@ -136,6 +186,10 @@ mod pallet { #[pallet::constant] type MaxLiquidityTreeDepth: Get; + /// The maximum number of splits allowed when creating a combinatorial pool. + #[pallet::constant] + type MaxSplits: Get; + #[pallet::constant] type MaxSwapFee: Get>; @@ -148,7 +202,14 @@ mod pallet { pub struct Pallet(PhantomData); #[pallet::storage] - pub(crate) type Pools = StorageMap<_, Twox64Concat, MarketIdOf, PoolOf>; + pub(crate) type Pools = StorageMap<_, Twox64Concat, T::PoolId, PoolOf>; + + #[pallet::storage] + pub(crate) type PoolCount = StorageValue<_, T::PoolId, ValueQuery>; + + #[pallet::storage] + pub(crate) type MarketIdToPoolId = + StorageMap<_, Twox64Concat, MarketIdOf, T::PoolId>; #[pallet::event] #[pallet::generate_deposit(fn deposit_event)] @@ -160,7 +221,7 @@ mod pallet { /// including swap and external fees. BuyExecuted { who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, asset_out: AssetOf, amount_in: BalanceOf, amount_out: BalanceOf, @@ -171,7 +232,7 @@ mod pallet { /// with swap and external fees already deducted. SellExecuted { who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, asset_in: AssetOf, amount_in: BalanceOf, amount_out: BalanceOf, @@ -179,11 +240,11 @@ mod pallet { external_fee_amount: BalanceOf, }, /// Liquidity provider withdrew fees. - FeesWithdrawn { who: T::AccountId, market_id: MarketIdOf, amount: BalanceOf }, + FeesWithdrawn { who: T::AccountId, pool_id: T::PoolId, amount: BalanceOf }, /// Liquidity provider joined the pool. JoinExecuted { who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, pool_shares_amount: BalanceOf, amounts_in: Vec>, new_liquidity_parameter: BalanceOf, @@ -191,7 +252,7 @@ mod pallet { /// Liquidity provider left the pool. ExitExecuted { who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, pool_shares_amount: BalanceOf, amounts_out: Vec>, new_liquidity_parameter: BalanceOf, @@ -200,6 +261,7 @@ mod pallet { PoolDeployed { who: T::AccountId, market_id: MarketIdOf, + pool_id: T::PoolId, account_id: T::AccountId, reserves: BTreeMap, BalanceOf>, collateral: AssetOf, @@ -208,10 +270,42 @@ mod pallet { swap_fee: BalanceOf, }, /// Pool was destroyed. - PoolDestroyed { + PoolDestroyed { who: T::AccountId, pool_id: T::PoolId, amounts_out: Vec> }, + /// A combinatorial position was opened. + ComboBuyExecuted { + who: AccountIdOf, + pool_id: T::PoolId, + buy: Vec>, + sell: Vec>, + amount_in: BalanceOf, + amount_out: BalanceOf, + swap_fee_amount: BalanceOf, + external_fee_amount: BalanceOf, + }, + /// A combinatorial position was closed. + ComboSellExecuted { + who: AccountIdOf, + pool_id: T::PoolId, + buy: Vec>, + keep: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_keep: BalanceOf, + amount_out: BalanceOf, + swap_fee_amount: BalanceOf, + external_fee_amount: BalanceOf, + }, + /// Pool was createed. + CombinatorialPoolDeployed { who: T::AccountId, - market_id: MarketIdOf, - amounts_out: Vec>, + market_ids: Vec>, + pool_id: T::PoolId, + account_id: T::AccountId, + reserves: BTreeMap, BalanceOf>, + collateral: AssetOf, + liquidity_parameter: BalanceOf, + pool_shares_amount: BalanceOf, + swap_fee: BalanceOf, }, } @@ -272,18 +366,41 @@ mod pallet { MinRelativeLiquidityThresholdViolated, /// Narrowing type conversion occurred. NarrowingConversion, + + /// The buy/sell/keep partition specified is empty, or contains overlaps or assets that don't + /// belong to the market. + InvalidPartition, + + /// The `amount_keep` parameter must be zero if `keep` is empty and less than `amount_buy` + /// if `keep` is not empty. + InvalidAmountKeep, + + /// The number of market IDs specified must be greater than two and no more than the + /// maximum. + InvalidMarketCount, + + /// Creating a combinatorial pool for these markets will require more splits than allowed. + MaxSplitsExceeded, + + /// The specified markets do not all use the same collateral. + CollateralMismatch, + + /// This function is not allowed to be called for this type of pool. + InvalidPoolType, } #[derive(Decode, Encode, Eq, PartialEq, PalletError, RuntimeDebug, TypeInfo)] pub enum NumericalLimitsError { /// Selling is not allowed at prices this low. SpotPriceTooLow, - /// Sells which move the price below this threshold are not allowed. + /// Interactions which move the price below a particular threshold are not allowed. SpotPriceSlippedTooLow, /// The maximum buy or sell amount was exceeded. MaxAmountExceeded, /// The minimum buy or sell amount was exceeded. MinAmountNotMet, + /// Interactions which move the price above a particular threshold are not allowed. + SpotPriceSlippedTooHigh, } #[pallet::call] @@ -305,7 +422,7 @@ mod pallet { /// # Parameters /// /// - `origin`: The origin account making the purchase. - /// - `market_id`: Identifier for the market related to the trade. + /// - `pool_id`: Identifier for the pool used to trade on. /// - `asset_count`: Number of assets in the pool. /// - `asset_out`: Asset to be purchased. /// - `amount_in`: Amount of collateral paid by the user. @@ -316,20 +433,26 @@ mod pallet { /// Depends on the implementation of `CompleteSetOperationsApi` and `ExternalFees`; when /// using the canonical implementations, the runtime complexity is `O(asset_count)`. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::buy((*asset_count).saturated_into()))] + #[pallet::weight(T::WeightInfo::buy((*asset_count).into()))] #[transactional] pub fn buy( origin: OriginFor, - #[pallet::compact] market_id: MarketIdOf, + #[pallet::compact] pool_id: T::PoolId, asset_count: AssetIndexType, asset_out: AssetOf, #[pallet::compact] amount_in: BalanceOf, #[pallet::compact] min_amount_out: BalanceOf, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let asset_count_real = T::MarketCommons::market(&market_id)?.outcomes(); - ensure!(asset_count == asset_count_real, Error::::IncorrectAssetCount); - let _ = Self::do_buy(who, market_id, asset_out, amount_in, min_amount_out)?; + + let pool = ::get(pool_id)?; + let asset_count_real = pool.assets().len(); + let asset_count_real_u16: u16 = + asset_count_real.try_into().map_err(|_| Error::::NarrowingConversion)?; + ensure!(asset_count == asset_count_real_u16, Error::::IncorrectAssetCount); + + let _ = Self::do_buy(who, pool_id, asset_out, amount_in, min_amount_out)?; + Ok(Some(T::WeightInfo::buy(asset_count.into())).into()) } @@ -349,7 +472,7 @@ mod pallet { /// # Parameters /// /// - `origin`: The origin account making the sale. - /// - `market_id`: Identifier for the market related to the trade. + /// - `pool_id`: Identifier for the pool used to trade on. /// - `asset_count`: Number of assets in the pool. /// - `asset_in`: Asset to be sold. /// - `amount_in`: Amount of outcome tokens paid by the user. @@ -360,21 +483,27 @@ mod pallet { /// Depends on the implementation of `CompleteSetOperationsApi` and `ExternalFees`; when /// using the canonical implementations, the runtime complexity is `O(asset_count)`. #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::sell((*asset_count).saturated_into()))] + #[pallet::weight(T::WeightInfo::sell((*asset_count).into()))] #[transactional] pub fn sell( origin: OriginFor, - #[pallet::compact] market_id: MarketIdOf, + #[pallet::compact] pool_id: T::PoolId, asset_count: AssetIndexType, asset_in: AssetOf, #[pallet::compact] amount_in: BalanceOf, #[pallet::compact] min_amount_out: BalanceOf, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let asset_count_real = T::MarketCommons::market(&market_id)?.outcomes(); - ensure!(asset_count == asset_count_real, Error::::IncorrectAssetCount); - let _ = Self::do_sell(who, market_id, asset_in, amount_in, min_amount_out)?; - Ok(Some(T::WeightInfo::sell(asset_count.into())).into()) + + let pool = ::get(pool_id)?; + let asset_count_real = pool.assets().len(); + let asset_count_real_u16: u16 = + asset_count_real.try_into().map_err(|_| Error::::NarrowingConversion)?; + ensure!(asset_count == asset_count_real_u16, Error::::IncorrectAssetCount); + + let _ = Self::do_sell(who, pool_id, asset_in, amount_in, min_amount_out)?; + + Ok(Some(T::WeightInfo::sell(asset_count_real_u16.into())).into()) } /// Join the liquidity pool for the specified market. @@ -389,7 +518,7 @@ mod pallet { /// /// # Parameters /// - /// - `market_id`: Identifier for the market related to the pool. + /// - `pool_id`: Identifier for the pool to add liquidity to. /// - `pool_shares_amount`: The number of new pool shares the LP will receive. /// - `max_amounts_in`: Vector of the maximum amounts of each outcome token the LP is /// willing to deposit (with outcomes specified in the order of `MarketCommonsApi`). @@ -408,18 +537,21 @@ mod pallet { #[transactional] pub fn join( origin: OriginFor, - #[pallet::compact] market_id: MarketIdOf, + #[pallet::compact] pool_id: T::PoolId, #[pallet::compact] pool_shares_amount: BalanceOf, max_amounts_in: Vec>, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let asset_count = T::MarketCommons::market(&market_id)?.outcomes(); - let asset_count_usize: usize = asset_count.into(); + // Ensure that the conversion in the weight calculation doesn't saturate. let _: u32 = max_amounts_in.len().try_into().map_err(|_| Error::::NarrowingConversion)?; - ensure!(max_amounts_in.len() == asset_count_usize, Error::::IncorrectVecLen); - Self::do_join(who, market_id, pool_shares_amount, max_amounts_in) + + let pool = ::get(pool_id)?; + let asset_count_real = pool.assets().len(); + ensure!(max_amounts_in.len() == asset_count_real, Error::::IncorrectVecLen); + + Self::do_join(who, pool_id, pool_shares_amount, max_amounts_in) } /// Exit the liquidity pool for the specified market. @@ -444,7 +576,7 @@ mod pallet { /// /// # Parameters /// - /// - `market_id`: Identifier for the market related to the pool. + /// - `pool_id`: Identifier for the pool to withdraw liquidity from. /// - `pool_shares_amount_out`: The number of pool shares the LP will relinquish. /// - `min_amounts_out`: Vector of the minimum amounts of each outcome token the LP expects /// to withdraw (with outcomes specified in the order given by `MarketCommonsApi`). @@ -459,18 +591,24 @@ mod pallet { #[transactional] pub fn exit( origin: OriginFor, - #[pallet::compact] market_id: MarketIdOf, + #[pallet::compact] pool_id: T::PoolId, #[pallet::compact] pool_shares_amount_out: BalanceOf, min_amounts_out: Vec>, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let asset_count = T::MarketCommons::market(&market_id)?.outcomes(); - let asset_count_u32: u32 = asset_count.into(); - let min_amounts_out_len: u32 = - min_amounts_out.len().try_into().map_err(|_| Error::::NarrowingConversion)?; - ensure!(min_amounts_out_len == asset_count_u32, Error::::IncorrectVecLen); - Self::do_exit(who, market_id, pool_shares_amount_out, min_amounts_out)?; - Ok(Some(T::WeightInfo::exit(min_amounts_out_len)).into()) + + let pool = ::get(pool_id)?; + let asset_count_real = pool.assets().len(); + let min_amounts_out_len = min_amounts_out.len(); + ensure!(min_amounts_out_len == asset_count_real, Error::::IncorrectVecLen); + + // Ensure that the conversion in the weight calculation doesn't saturate. + let min_amounts_out_len_u32: u32 = + min_amounts_out_len.try_into().map_err(|_| Error::::NarrowingConversion)?; + + Self::do_exit(who, pool_id, pool_shares_amount_out, min_amounts_out)?; + + Ok(Some(T::WeightInfo::exit(min_amounts_out_len_u32)).into()) } /// Withdraw swap fees from the specified market. @@ -480,7 +618,7 @@ mod pallet { /// /// # Parameters /// - /// - `market_id`: Identifier for the market related to the pool. + /// - `pool_id`: Identifier for the market related to the pool. /// /// # Complexity /// @@ -490,10 +628,12 @@ mod pallet { #[transactional] pub fn withdraw_fees( origin: OriginFor, - #[pallet::compact] market_id: MarketIdOf, + #[pallet::compact] pool_id: T::PoolId, ) -> DispatchResult { let who = ensure_signed(origin)?; - Self::do_withdraw_fees(who, market_id)?; + + Self::do_withdraw_fees(who, pool_id)?; + Ok(()) } @@ -532,36 +672,230 @@ mod pallet { #[pallet::compact] swap_fee: BalanceOf, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; + let asset_count = T::MarketCommons::market(&market_id)?.outcomes(); let asset_count_u32: u32 = asset_count.into(); let spot_prices_len: u32 = spot_prices.len().try_into().map_err(|_| Error::::NarrowingConversion)?; ensure!(spot_prices_len == asset_count_u32, Error::::IncorrectVecLen); + Self::do_deploy_pool(who, market_id, amount, spot_prices, swap_fee)?; + Ok(Some(T::WeightInfo::deploy_pool(spot_prices_len)).into()) } + + /// Make a combinatorial bet on the specified pool. + /// + /// The `amount_in` is paid in collateral. The transaction fails if the amount of outcome + /// tokens received is smaller than `min_amount_out`. The user must correctly specify the + /// number of outcomes for benchmarking reasons. + /// + /// The user's collateral is used to mint complete sets of the combinatorial tokens in the + /// pool. The parameters `buy` and `sell` are used to specify which of these tokens the user + /// wants and doesn't want: The assets in `sell` are sold to buy more of `buy` from the + /// pool. The assets not contained in either of these will remain in the users wallet + /// unchanged. + /// + /// The function will error if certain numerical constraints are violated. + /// + /// # Parameters + /// + /// - `origin`: The origin account making the purchase. + /// - `pool_id`: Identifier for the pool used to trade on. + /// - `asset_count`: Number of assets in the pool. + /// - `buy`: The assets that the user want to have more of. Must not be empty. + /// - `sell`: The assets that the user doesn't want any of. Must not be empty. + /// - `amount_in`: Amount of collateral paid by the user. + /// - `min_amount_out`: Minimum number of outcome tokens the user expects to receive. + /// + /// # Complexity + /// + /// Depends on the implementation of `CombinatorialTokensUnsafeApi` and `ExternalFees`; when + /// using the canonical implementations, the runtime complexity is `O(asset_count)`. + #[allow(clippy::too_many_arguments)] + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::combo_buy(asset_count.log_ceil().into()))] + #[transactional] + pub fn combo_buy( + origin: OriginFor, + #[pallet::compact] pool_id: T::PoolId, + asset_count: AssetIndexType, + buy: Vec>, + sell: Vec>, + #[pallet::compact] amount_in: BalanceOf, + #[pallet::compact] min_amount_out: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let pool = ::get(pool_id)?; + let asset_count_real = pool.assets().len(); + let asset_count_real_u16: u16 = + asset_count_real.try_into().map_err(|_| Error::::NarrowingConversion)?; + ensure!(asset_count == asset_count_real_u16, Error::::IncorrectAssetCount); + + Self::do_combo_buy(who, pool_id, buy, sell, amount_in, min_amount_out) + } + + /// Cancel a combinatorial bet on the specified pool. + /// + /// The `buy`, `keep` and `sell` parameters are used to specify the amounts of the bet the + /// user wishes to cancel. The user must hold `amount_buy` units of each asset in `buy` and + /// `amount_keep` of each asset in `keep` in their wallet. If `keep` is empty, then + /// `amount_keep` must be zero. + /// + /// The transaction fails if the amount of outcome tokens received is smaller than + /// `min_amount_out`. The user must correctly specify the number of outcomes for + /// benchmarking reasons. + /// + /// The function will error if certain numerical constraints are violated. + /// + /// # Parameters + /// + /// - `origin`: The origin account making the purchase. + /// - `pool_id`: Identifier for the pool used to trade on. + /// - `asset_count`: Number of assets in the pool. + /// - `buy`: The `buy` of the bet that the user wishes to cancel. Must not be empty. + /// - `keep`: The tokens not contained in `buy` or `sell` of the bet that the user wishes to + /// cancel. May be empty. + /// - `sell`: The `sell` of the bet that the user wishes to cancel. Must not be empty. + /// - `amount_buy`: Amount of tokens in `buy` the user wishes to let go. + /// - `amount_keep`: Amount of tokens in `keep` the user wishes to let go. + /// - `min_amount_out`: Minimum number of outcome tokens the user expects to receive. + /// + /// # Complexity + /// + /// Depends on the implementation of `CombinatorialTokensUnsafeApi` and `ExternalFees`; when + /// using the canonical implementations, the runtime complexity is `O(asset_count)`. + #[allow(clippy::too_many_arguments)] + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::combo_sell(asset_count.log_ceil().into()))] + #[transactional] + pub fn combo_sell( + origin: OriginFor, + #[pallet::compact] pool_id: T::PoolId, + asset_count: AssetIndexType, + buy: Vec>, + keep: Vec>, + sell: Vec>, + #[pallet::compact] amount_buy: BalanceOf, + #[pallet::compact] amount_keep: BalanceOf, + #[pallet::compact] min_amount_out: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let pool = ::get(pool_id)?; + let asset_count_real = pool.assets().len(); + let asset_count_real_u16: u16 = + asset_count_real.try_into().map_err(|_| Error::::NarrowingConversion)?; + ensure!(asset_count == asset_count_real_u16, Error::::IncorrectAssetCount); + + Self::do_combo_sell( + who, + pool_id, + buy, + keep, + sell, + amount_buy, + amount_keep, + min_amount_out, + ) + } + + /// Deploy a combinatorial pool for the specified markets and provide liquidity. + /// + /// The tokens of each of the markets specified by `market_ids` are split into atoms. For + /// each combination of outcome tokens `x, ..., z` from the markets, there is one + /// combinatorial token `x & ... & z` in the pool. + /// + /// The pool's assets are ordered by lexicographical order, using the ordering of tokens of + /// each individual market provided by the `MarketCommonsApi`. For example, if three markets + /// with outcomes `x_1, x_2`, `y_1, y_2` and `z_1, z_2` are involved, the outcomes of the + /// pool are (in order): + /// + /// x_1 & y_1 & z_1 + /// x_1 & y_1 & z_2 + /// x_1 & y_2 & z_1 + /// x_1 & y_2 & z_2 + /// x_2 & y_1 & z_1 + /// x_2 & y_1 & z_2 + /// x_2 & y_2 & z_1 + /// x_2 & y_2 & z_2 + /// + /// The sender specifies a vector of `spot_prices` for the assets of the new pool, in the + /// order as described above. + /// + /// Depending on the values in the `spot_prices`, the transaction will transfer different + /// amounts of each outcome to the pool. The sender specifies a maximum `amount` of outcome + /// tokens to spend. + /// + /// Unlike in the `deploy_pool` extrinsic, the sender need not acquire the outcome tokens + /// themselves. Instead, all they need is `amount` units of collateral. + /// + /// Deploying the pool will cost the signer an additional fee to the tune of the + /// collateral's existential deposit. This fee is placed in the pool account and ensures + /// that swap fees can be stored in the pool account without triggering dusting or failed + /// transfers. + /// + /// The `fuel` parameter specifies how much work the cryptographic id manager will do + /// and can be used for benchmarking purposes. + /// + /// # Complexity + /// + /// `O(n)` where `n` is the number of splits required to create the pool. + /// The `fuel` parameter specifies how much work the cryptographic id manager will do + /// and can be used for benchmarking purposes. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::deploy_combinatorial_pool( + asset_count.log_ceil().into(), + fuel.total(), + ))] + #[transactional] + pub fn deploy_combinatorial_pool( + origin: OriginFor, + asset_count: AssetIndexType, + market_ids: Vec>, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, + fuel: FuelOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let mut real_asset_count = 1u16; + for market_id in market_ids.iter() { + let market = T::MarketCommons::market(market_id)?; + real_asset_count = real_asset_count.saturating_mul(market.outcomes()); + } + ensure!(asset_count == real_asset_count, Error::::IncorrectAssetCount); + + Self::do_deploy_combinatorial_pool(who, market_ids, amount, spot_prices, swap_fee, fuel) + } } impl Pallet { #[require_transactional] - fn do_buy( + pub(crate) fn do_buy( who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, asset_out: AssetOf, amount_in: BalanceOf, min_amount_out: BalanceOf, ) -> Result, DispatchError> { ensure!(amount_in != Zero::zero(), Error::::ZeroAmount); - let market = T::MarketCommons::market(&market_id)?; - ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive); - Self::try_mutate_pool(&market_id, |pool| { + + ::try_mutate_pool(&pool_id, |pool| { + ensure!(pool.is_active()?, Error::::MarketNotActive); + ensure!( + matches!(pool.pool_type, PoolType::Standard(_)), + Error::::InvalidPoolType + ); ensure!(pool.contains(&asset_out), Error::::AssetNotFound); T::MultiCurrency::transfer(pool.collateral, &who, &pool.account_id, amount_in)?; let FeeDistribution { remaining: amount_in_minus_fees, swap_fees: swap_fee_amount, external_fees: external_fee_amount, - } = Self::distribute_fees(market_id, pool, amount_in)?; + } = Self::distribute_fees(pool, &pool.account_id.clone(), amount_in)?; ensure!( amount_in_minus_fees <= pool.calculate_numerical_threshold(), Error::::NumericalLimits(NumericalLimitsError::MaxAmountExceeded), @@ -571,13 +905,22 @@ mod pallet { >= LN_NUMERICAL_LIMIT.saturated_into(), Error::::NumericalLimits(NumericalLimitsError::MinAmountNotMet), ); + let buy = vec![asset_out]; + let sell = pool.assets_complement(&buy); + // `swap_amount_out` is the amount of assets in sell (S) that are sold for more + // assets of buy (B). In the reference documentation it's called `y(x)` let swap_amount_out = - pool.calculate_swap_amount_out_for_buy(asset_out, amount_in_minus_fees)?; + pool.calculate_swap_amount_out_for_buy(buy, sell, amount_in_minus_fees)?; + // The following is the buy complete set amount plus the additional amount + // that was received through the sale of the unwanted outcomes in the sell. let amount_out = swap_amount_out.checked_add_res(&amount_in_minus_fees)?; ensure!(amount_out >= min_amount_out, Error::::AmountOutBelowMin); // Instead of letting `who` buy the complete sets and then transfer almost all of // the outcomes to the pool account, we prevent `(n-1)` storage reads by using the // pool account to buy. Note that the fees are already in the pool at this point. + let PoolType::Standard(market_id) = pool.pool_type else { + return Err(Error::::Unexpected.into()); + }; T::CompleteSetOperations::buy_complete_set( pool.account_id.clone(), market_id, @@ -592,7 +935,7 @@ mod pallet { } Self::deposit_event(Event::::BuyExecuted { who: who.clone(), - market_id, + pool_id, asset_out, amount_in, amount_out, @@ -604,17 +947,21 @@ mod pallet { } #[require_transactional] - fn do_sell( + pub(crate) fn do_sell( who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, asset_in: AssetOf, amount_in: BalanceOf, min_amount_out: BalanceOf, ) -> Result, DispatchError> { ensure!(amount_in != Zero::zero(), Error::::ZeroAmount); - let market = T::MarketCommons::market(&market_id)?; - ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive); - Self::try_mutate_pool(&market_id, |pool| { + + ::try_mutate_pool(&pool_id, |pool| { + ensure!(pool.is_active()?, Error::::MarketNotActive); + ensure!( + matches!(pool.pool_type, PoolType::Standard(_)), + Error::::InvalidPoolType + ); ensure!(pool.contains(&asset_in), Error::::AssetNotFound); // Ensure that the price of `asset_in` is at least `exp(-EXP_NUMERICAL_LIMITS) = // 4.5399...e-05`. @@ -626,15 +973,32 @@ mod pallet { amount_in <= pool.calculate_numerical_threshold(), Error::::NumericalLimits(NumericalLimitsError::MaxAmountExceeded), ); + + // `asset_in` is sold in order to get the amount of full sets of all possible + // outcomes, the `amount_out` is calculated in which all other assets are sold to + // get an equal amount of each possible asset back, + // which can then be burned for collateral + let buy = vec![asset_in]; + let keep = vec![]; + let sell = pool.assets_complement(&buy); + let amount_out = pool.calculate_swap_amount_out_for_sell( + buy, + keep, + sell, + amount_in, + Zero::zero(), + )?; + // Instead of first executing a swap with `(n-1)` transfers from the pool account to // `who` and then selling complete sets, we prevent `(n-1)` storage reads: 1) // Transfer `amount_in` units of `asset_in` to the pool account, 2) sell // `amount_out` complete sets using the pool account, 3) transfer // `amount_out_minus_fees` units of collateral to `who`. The fees automatically end // up in the pool. - let amount_out = pool.calculate_swap_amount_out_for_sell(asset_in, amount_in)?; - // Beware! This transfer **must** happen _after_ calculating `amount_out`: T::MultiCurrency::transfer(asset_in, &who, &pool.account_id, amount_in)?; + let PoolType::Standard(market_id) = pool.pool_type else { + return Err(Error::::Unexpected.into()); + }; T::CompleteSetOperations::sell_complete_set( pool.account_id.clone(), market_id, @@ -644,7 +1008,7 @@ mod pallet { remaining: amount_out_minus_fees, swap_fees: swap_fee_amount, external_fees: external_fee_amount, - } = Self::distribute_fees(market_id, pool, amount_out)?; + } = Self::distribute_fees(pool, &pool.account_id.clone(), amount_out)?; ensure!(amount_out_minus_fees >= min_amount_out, Error::::AmountOutBelowMin); T::MultiCurrency::transfer( pool.collateral, @@ -666,7 +1030,7 @@ mod pallet { ); Self::deposit_event(Event::::SellExecuted { who: who.clone(), - market_id, + pool_id, asset_in, amount_in, amount_out: amount_out_minus_fees, @@ -683,20 +1047,22 @@ mod pallet { } #[require_transactional] - fn do_join( + pub(crate) fn do_join( who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, pool_shares_amount: BalanceOf, max_amounts_in: Vec>, ) -> DispatchResultWithPostInfo { ensure!(pool_shares_amount != Zero::zero(), Error::::ZeroAmount); - let market = T::MarketCommons::market(&market_id)?; - ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive); - let asset_count_u16: u16 = - max_amounts_in.len().try_into().map_err(|_| Error::::NarrowingConversion)?; - let asset_count_u32: u32 = asset_count_u16.into(); - ensure!(asset_count_u16 == market.outcomes(), Error::::IncorrectAssetCount); - let benchmark_info = Self::try_mutate_pool(&market_id, |pool| { + + let weight = ::try_mutate_pool(&pool_id, |pool| { + ensure!(pool.is_active()?, Error::::MarketNotActive); + ensure!( + max_amounts_in.len() == pool.assets().len(), + Error::::IncorrectAssetCount + ); + let asset_count_u32 = + max_amounts_in.len().try_into().map_err(|_| Error::::NarrowingConversion)?; let ratio = pool_shares_amount.bdiv_ceil(pool.liquidity_shares_manager.total_shares()?)?; // Ensure that new LPs contribute at least MIN_RELATIVE_LP_POSITION_VALUE. Note that @@ -726,37 +1092,35 @@ mod pallet { pool.liquidity_parameter = new_liquidity_parameter; Self::deposit_event(Event::::JoinExecuted { who: who.clone(), - market_id, + pool_id, pool_shares_amount, amounts_in, new_liquidity_parameter, }); - Ok(benchmark_info) + let weight = match benchmark_info { + BenchmarkInfo::InPlace => T::WeightInfo::join_in_place(asset_count_u32), + BenchmarkInfo::Reassigned => T::WeightInfo::join_reassigned(asset_count_u32), + BenchmarkInfo::Leaf => T::WeightInfo::join_leaf(asset_count_u32), + }; + Ok(weight) })?; - let weight = match benchmark_info { - BenchmarkInfo::InPlace => T::WeightInfo::join_in_place(asset_count_u32), - BenchmarkInfo::Reassigned => T::WeightInfo::join_reassigned(asset_count_u32), - BenchmarkInfo::Leaf => T::WeightInfo::join_leaf(asset_count_u32), - }; Ok((Some(weight)).into()) } #[require_transactional] - fn do_exit( + pub(crate) fn do_exit( who: T::AccountId, - market_id: MarketIdOf, + pool_id: T::PoolId, pool_shares_amount: BalanceOf, min_amounts_out: Vec>, ) -> DispatchResult { ensure!(pool_shares_amount != Zero::zero(), Error::::ZeroAmount); - let market = T::MarketCommons::market(&market_id)?; - Pools::::try_mutate_exists(market_id, |maybe_pool| { - let pool = - maybe_pool.as_mut().ok_or::(Error::::PoolNotFound.into())?; + + ::try_mutate_exists(&pool_id, |pool| { let ratio = { let mut ratio = pool_shares_amount .bdiv_floor(pool.liquidity_shares_manager.total_shares()?)?; - if market.status == MarketStatus::Active { + if pool.is_active()? { let multiplier = ZeitgeistBase::>::get()? .checked_sub_res(&EXIT_FEE.saturated_into())?; ratio = ratio.bmul_floor(multiplier)?; @@ -788,12 +1152,14 @@ mod pallet { for asset in pool.assets().iter() { withdraw_remaining(asset)?; } - *maybe_pool = None; // Delete the storage map entry. Self::deposit_event(Event::::PoolDestroyed { who: who.clone(), - market_id, + pool_id, amounts_out, }); + + // Delete the pool. No need to clear `MarketIdToPoolId`. + Ok(((), true)) } else { let old_liquidity_parameter = pool.liquidity_parameter; let new_liquidity_parameter = old_liquidity_parameter @@ -818,24 +1184,25 @@ mod pallet { pool.liquidity_parameter = new_liquidity_parameter; Self::deposit_event(Event::::ExitExecuted { who: who.clone(), - market_id, + pool_id, pool_shares_amount, amounts_out, new_liquidity_parameter, }); + + Ok(((), false)) } - Ok(()) }) } #[require_transactional] - fn do_withdraw_fees(who: T::AccountId, market_id: MarketIdOf) -> DispatchResult { - Self::try_mutate_pool(&market_id, |pool| { + pub(crate) fn do_withdraw_fees(who: T::AccountId, pool_id: T::PoolId) -> DispatchResult { + ::try_mutate_pool(&pool_id, |pool| { let amount = pool.liquidity_shares_manager.withdraw_fees(&who)?; T::MultiCurrency::transfer(pool.collateral, &pool.account_id, &who, amount)?; // Should never fail. Self::deposit_event(Event::::FeesWithdrawn { who: who.clone(), - market_id, + pool_id, amount, }); Ok(()) @@ -843,14 +1210,19 @@ mod pallet { } #[require_transactional] - fn do_deploy_pool( + pub(crate) fn do_deploy_pool( who: T::AccountId, market_id: MarketIdOf, amount: BalanceOf, spot_prices: Vec>, swap_fee: BalanceOf, ) -> DispatchResult { - ensure!(!Pools::::contains_key(market_id), Error::::DuplicatePool); + // MarketIdToPoolId is not cleared when a pool is destroyed, so checking if + // `MarketIdToPoolId` holds a key is not enough. + if let Some(pool_id) = MarketIdToPoolId::::get(market_id) { + ensure!(!Pools::::contains_key(pool_id), Error::::DuplicatePool); + } + let market = T::MarketCommons::market(&market_id)?; ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive); ensure!( @@ -896,11 +1268,13 @@ mod pallet { let collateral = market.base_asset; let pool = Pool { account_id: pool_account_id.clone(), + assets: market.outcome_assets().try_into().map_err(|_| Error::::Unexpected)?, reserves: reserves.clone().try_into().map_err(|_| Error::::Unexpected)?, collateral, liquidity_parameter, liquidity_shares_manager: LiquidityTree::new(who.clone(), amount)?, swap_fee, + pool_type: PoolType::Standard(market_id), }; // TODO(#1220): Ensure that the existential deposit doesn't kill fees. This is an ugly // hack and system should offer the option to whitelist accounts. @@ -910,10 +1284,97 @@ mod pallet { &pool.account_id, T::MultiCurrency::minimum_balance(collateral), )?; - Pools::::insert(market_id, pool); + let pool_id = ::add(pool)?; + MarketIdToPoolId::::insert(market_id, pool_id); Self::deposit_event(Event::::PoolDeployed { who, market_id, + pool_id, + account_id: pool_account_id, + reserves, + collateral, + liquidity_parameter, + pool_shares_amount: amount, + swap_fee, + }); + Ok(()) + } + + #[require_transactional] + pub(crate) fn do_deploy_combinatorial_pool( + who: T::AccountId, + market_ids: Vec>, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, + fuel: FuelOf, + ) -> DispatchResult { + ensure!(swap_fee >= MIN_SWAP_FEE.saturated_into(), Error::::SwapFeeBelowMin); + ensure!(swap_fee <= T::MaxSwapFee::get(), Error::::SwapFeeAboveMax); + + let (collection_ids, position_ids, collateral) = + Self::split_markets(who.clone(), market_ids.clone(), amount, fuel)?; + + ensure!(spot_prices.len() == collection_ids.len(), Error::::IncorrectVecLen); + ensure!( + spot_prices + .iter() + .fold(Zero::zero(), |acc: BalanceOf, &val| acc.saturating_add(val)) + == BASE.saturated_into(), + Error::::InvalidSpotPrices + ); + for &p in spot_prices.iter() { + ensure!( + p.saturated_into::() >= MIN_SPOT_PRICE, + Error::::SpotPriceBelowMin + ); + ensure!( + p.saturated_into::() <= MAX_SPOT_PRICE, + Error::::SpotPriceAboveMax + ); + } + + let (liquidity_parameter, amounts_in) = + Math::::calculate_reserves_from_spot_prices(amount, spot_prices)?; + ensure!( + liquidity_parameter >= MIN_LIQUIDITY.saturated_into(), + Error::::LiquidityTooLow + ); + let pool_id = ::next_pool_id(); + let pool_account_id = Self::pool_account_id(&pool_id); + let mut reserves = BTreeMap::new(); + for (&amount_in, &asset) in amounts_in.iter().zip(position_ids.iter()) { + T::MultiCurrency::transfer(asset, &who, &pool_account_id, amount_in)?; + let _ = reserves.insert(asset, amount_in); + } + let pool = Pool { + account_id: pool_account_id.clone(), + assets: position_ids.try_into().map_err(|_| Error::::Unexpected)?, + reserves: reserves.clone().try_into().map_err(|_| Error::::Unexpected)?, + collateral, + liquidity_parameter, + liquidity_shares_manager: LiquidityTree::new(who.clone(), amount)?, + swap_fee, + pool_type: PoolType::Combinatorial( + market_ids.clone().try_into().map_err(|_| Error::::Unexpected)?, + ), + }; + + ensure!(pool.is_active()?, Error::::MarketNotActive); + + // TODO(#1220): Ensure that the existential deposit doesn't kill fees. This is an ugly + // hack and system should offer the option to whitelist accounts. + T::MultiCurrency::transfer( + pool.collateral, + &who, + &pool.account_id, + T::MultiCurrency::minimum_balance(collateral), + )?; + let _ = ::add(pool); + Self::deposit_event(Event::::CombinatorialPoolDeployed { + who, + market_ids, + pool_id, account_id: pool_account_id, reserves, collateral, @@ -924,46 +1385,403 @@ mod pallet { Ok(()) } + #[allow(clippy::too_many_arguments)] + #[require_transactional] + pub(crate) fn do_combo_buy( + who: T::AccountId, + pool_id: T::PoolId, + buy: Vec>, + sell: Vec>, + amount_in: BalanceOf, + min_amount_out: BalanceOf, + ) -> DispatchResult { + ensure!(amount_in != Zero::zero(), Error::::ZeroAmount); + + ::try_mutate_pool(&pool_id, |pool| { + ensure!(pool.is_active()?, Error::::MarketNotActive); + ensure!( + matches!(pool.pool_type, PoolType::Combinatorial(_)), + Error::::InvalidPoolType + ); + + // Ensure that `buy` and `sell` partition are disjoint, only contain assets from + // the market and don't contain dupliates. + ensure!(!buy.is_empty(), Error::::InvalidPartition); + ensure!(!sell.is_empty(), Error::::InvalidPartition); + for asset in buy.iter() { + ensure!(!sell.contains(asset), Error::::InvalidPartition); + ensure!(pool.assets().contains(asset), Error::::InvalidPartition); + } + for asset in sell.iter() { + ensure!(pool.assets().contains(asset), Error::::InvalidPartition); + } + let buy_set = buy.iter().collect::>(); + let sell_set = sell.iter().collect::>(); + ensure!(buy_set.len() == buy.len(), Error::::InvalidPartition); + ensure!(sell_set.len() == sell.len(), Error::::InvalidPartition); + + let FeeDistribution { + remaining: amount_in_minus_fees, + swap_fees: swap_fee_amount, + external_fees: external_fee_amount, + } = Self::distribute_fees(pool, &who, amount_in)?; + // `swap_amount_out` is the amount of assets in sell (S) that are sold for more + // assets of buy (B). In the reference documentation it's called `y(x)` + let swap_amount_out = pool.calculate_swap_amount_out_for_buy( + buy.clone(), + sell.clone(), + amount_in_minus_fees, + )?; + // The following is the buy complete set amount plus the additional amount + // that was received through the sale of the unwanted outcomes in the sell. + let amount_out = swap_amount_out.checked_add_res(&amount_in_minus_fees)?; + ensure!(amount_out >= min_amount_out, Error::::AmountOutBelowMin); + + // Using unsafe API to avoid doing work. This is perfectly safe as long as + // `pool.assets()` returns a "full set" of split tokens. + T::CombinatorialTokensUnsafe::split_position_unsafe( + who.clone(), + pool.collateral, + pool.assets(), + amount_in_minus_fees, + )?; + + for &asset in buy.iter() { + T::MultiCurrency::transfer(asset, &pool.account_id, &who, swap_amount_out)?; + pool.decrease_reserve(&asset, &swap_amount_out)?; + } + for &asset in sell.iter() { + T::MultiCurrency::transfer( + asset, + &who, + &pool.account_id, + amount_in_minus_fees, + )?; + pool.increase_reserve(&asset, &amount_in_minus_fees)?; + } + + // Ensure that numerical limits of all prices are respected. + for &asset in pool.assets().iter() { + let spot_price = pool.calculate_spot_price(asset)?; + ensure!( + spot_price >= COMBO_MIN_SPOT_PRICE.saturated_into(), + Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow) + ); + ensure!( + spot_price <= COMBO_MAX_SPOT_PRICE.saturated_into(), + Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooHigh) + ); + } + + Self::deposit_event(Event::::ComboBuyExecuted { + who: who.clone(), + pool_id, + buy: buy.clone(), + sell: sell.clone(), + amount_in, + amount_out, + swap_fee_amount, + external_fee_amount, + }); + + Ok(()) + }) + } + + #[allow(clippy::too_many_arguments)] + #[require_transactional] + pub(crate) fn do_combo_sell( + who: T::AccountId, + pool_id: T::PoolId, + buy: Vec>, + keep: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_keep: BalanceOf, + min_amount_out: BalanceOf, + ) -> DispatchResult { + ensure!(amount_buy != Zero::zero(), Error::::ZeroAmount); + + if keep.is_empty() { + ensure!(amount_keep.is_zero(), Error::::InvalidAmountKeep); + } else { + ensure!(amount_keep < amount_buy, Error::::InvalidAmountKeep); + } + + ::try_mutate_pool(&pool_id, |pool| { + ensure!(pool.is_active()?, Error::::MarketNotActive); + ensure!( + matches!(pool.pool_type, PoolType::Combinatorial(_)), + Error::::InvalidPoolType + ); + + // Ensure that `buy` and `sell` partition are disjoint and only contain assets from + // the market. + ensure!(!buy.is_empty(), Error::::InvalidPartition); + ensure!(!sell.is_empty(), Error::::InvalidPartition); + for asset in buy.iter() { + ensure!(!keep.contains(asset), Error::::InvalidPartition); + ensure!(!sell.contains(asset), Error::::InvalidPartition); + ensure!(pool.assets().contains(asset), Error::::InvalidPartition); + } + for asset in sell.iter() { + ensure!(!keep.contains(asset), Error::::InvalidPartition); + ensure!(pool.assets().contains(asset), Error::::InvalidPartition); + } + for asset in keep.iter() { + ensure!(pool.assets().contains(asset), Error::::InvalidPartition); + } + let buy_set = buy.iter().collect::>(); + let keep_set = keep.iter().collect::>(); + let sell_set = sell.iter().collect::>(); + ensure!(buy_set.len() == buy.len(), Error::::InvalidPartition); + ensure!(keep_set.len() == keep.len(), Error::::InvalidPartition); + ensure!(sell_set.len() == sell.len(), Error::::InvalidPartition); + let total_assets = buy.len().saturating_add(keep.len()).saturating_add(sell.len()); + ensure!(total_assets == pool.assets().len(), Error::::InvalidPartition); + + // This is the amount of collateral the user will receive in the end, or, + // equivalently, the amount of each asset in `sell` that the user intermittently + // receives from the pool (before selling complete sets). + let amount_out = pool.calculate_swap_amount_out_for_sell( + buy.clone(), + keep.clone(), + sell.clone(), + amount_buy, + amount_keep, + )?; + ensure!(amount_out >= min_amount_out, Error::::AmountOutBelowMin); + + // The deal is that the user gives up all of the assets specified in the function + // parameters and receives `amount_out` (minus fees) units of collateral. To create + // the collateral, the pool has to call `sell_complete_set`. This approach is more + // stable than letting the user call `sell_complete_set` after equalizing their + // assets, as doing so may lead to `sell_complete_set` failing due to rounding + // errors. + + for &asset in buy.iter() { + T::MultiCurrency::transfer(asset, &who, &pool.account_id, amount_buy)?; + pool.increase_reserve(&asset, &amount_buy)?; + } + + for &asset in keep.iter() { + T::MultiCurrency::transfer(asset, &who, &pool.account_id, amount_keep)?; + pool.increase_reserve(&asset, &amount_keep)?; + } + + // Using unsafe API to avoid doing work. This is perfectly safe as long as + // `pool.assets()` returns a "full set" of split tokens. + T::CombinatorialTokensUnsafe::merge_position_unsafe( + pool.account_id.clone(), + pool.collateral, + pool.assets(), + amount_out, + )?; + + for &asset in pool.assets().iter() { + pool.decrease_reserve(&asset, &amount_out)?; + } + + let FeeDistribution { + remaining: amount_out_minus_fees, + swap_fees: swap_fee_amount, + external_fees: external_fee_amount, + } = Self::distribute_fees(pool, &pool.account_id.clone(), amount_out)?; + + T::MultiCurrency::transfer( + pool.collateral, + &pool.account_id, + &who, + amount_out_minus_fees, + )?; + + // Ensure that numerical limits of all prices are respected. + for &asset in pool.assets().iter() { + let spot_price = pool.calculate_spot_price(asset)?; + ensure!( + spot_price >= COMBO_MIN_SPOT_PRICE.saturated_into(), + Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow) + ); + ensure!( + spot_price <= COMBO_MAX_SPOT_PRICE.saturated_into(), + Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooHigh) + ); + } + + Self::deposit_event(Event::::ComboSellExecuted { + who: who.clone(), + pool_id, + buy: buy.clone(), + keep: keep.clone(), + sell: sell.clone(), + amount_buy, + amount_keep, + amount_out: amount_out_minus_fees, + swap_fee_amount, + external_fee_amount, + }); + + Ok(()) + }) + } + #[inline] - pub(crate) fn pool_account_id(market_id: &MarketIdOf) -> T::AccountId { - T::PalletId::get().into_sub_account_truncating((*market_id).saturated_into::()) + pub(crate) fn pool_account_id(pool_id: &T::PoolId) -> T::AccountId { + T::PalletId::get().into_sub_account_truncating((*pool_id).saturated_into::()) + } + + /// Returns the assets contained in the pool given by `pool_id`. + pub fn assets(pool_id: T::PoolId) -> Result>, DispatchError> { + let pool = ::get(pool_id)?; + + Ok(pool.assets.into_inner()) } /// Distribute swap fees and external fees and returns the remaining amount. /// /// # Arguments /// - /// - `market_id`: The ID of the market to which the pool belongs. + /// - `pool_id`: The ID of the pool on which the trade was executed. /// - `pool`: The pool on which the trade was executed. + /// - `account`: The account that the fee is deducted from. /// - `amount`: The gross amount from which the fee is deduced. /// /// Will fail if the total amount of fees is more than the gross amount. In particular, the /// function will fail if the external fees exceed the gross amount. #[require_transactional] fn distribute_fees( - market_id: MarketIdOf, pool: &mut PoolOf, + account: &AccountIdOf, amount: BalanceOf, ) -> Result, DispatchError> { let swap_fees = pool.swap_fee.bmul(amount)?; + T::MultiCurrency::transfer(pool.collateral, account, &pool.account_id, swap_fees)?; pool.liquidity_shares_manager.deposit_fees(swap_fees)?; // Should only error unexpectedly! - let external_fees = - T::ExternalFees::distribute(market_id, pool.collateral, &pool.account_id, amount); + + let mut external_fees: BalanceOf = Zero::zero(); + for &market_id in pool.pool_type.iter_market_ids() { + let f = T::ExternalFees::distribute(market_id, pool.collateral, account, amount); + external_fees = external_fees.saturating_add(f); + } + let total_fees = external_fees.saturating_add(swap_fees); let remaining = amount.checked_sub(&total_fees).ok_or(Error::::Unexpected)?; Ok(FeeDistribution { remaining, swap_fees, external_fees }) } - pub(crate) fn try_mutate_pool( - market_id: &MarketIdOf, - mutator: F, - ) -> Result - where - F: FnMut(&mut PoolOf) -> Result, - { - Pools::::try_mutate(market_id, |maybe_pool| { - maybe_pool.as_mut().ok_or(Error::::PoolNotFound.into()).and_then(mutator) - }) + /// Takes `amount` units of collateral and splits these tokens into the elementary outcome + /// tokens of the combinatorial market comprised of the specified markets (all specified + /// markets must have the same collateral). Returns the collateral token type and a list of + /// outcome tokens. + pub(crate) fn split_markets( + who: T::AccountId, + market_ids: Vec>, + amount: BalanceOf, + fuel: FuelOf, + ) -> Result<(Vec, Vec>, AssetOf), DispatchError> { + let markets = + market_ids.iter().map(T::MarketCommons::market).collect::, _>>()?; + + // Calculate the total amount of split operations required. One split for splitting + // collateral into the positions of the first market, and then it's one split for each + // position created in the previous step. + let mut total_splits = 0u16; + let mut prev_positions = 0u16; + for market in markets.iter() { + ensure!( + market.scoring_rule == ScoringRule::AmmCdaHybrid, + Error::::InvalidTradingMechanism + ); + + if total_splits == 0u16 { + total_splits = 1u16; + prev_positions = market.outcomes(); + } else { + total_splits = total_splits.checked_add_res(&prev_positions)?; + prev_positions = prev_positions.checked_mul_res(&market.outcomes())?; + } + } + ensure!(total_splits <= T::MaxSplits::get(), Error::::MaxSplitsExceeded); + + let collateral = Self::try_common_collateral(market_ids.clone())?; + + let mut split_count = 0u16; + let mut collection_ids: Vec = vec![]; + let mut position_ids = vec![]; + for market_id in market_ids.iter() { + let asset_count = T::MarketCommons::market(market_id)?.outcomes() as usize; + let partition: Vec> = (0..asset_count) + .map(|index| { + let mut index_set = vec![false; asset_count]; + if let Some(value) = index_set.get_mut(index) { + *value = true; + } + + index_set + }) + .collect(); + + if split_count == 0 { + let split_position_info = T::CombinatorialTokens::split_position( + who.clone(), + None, + *market_id, + partition.clone(), + amount, + fuel.clone(), + )?; + + collection_ids.extend_from_slice(&split_position_info.collection_ids); + position_ids.extend_from_slice(&split_position_info.position_ids); + + split_count.saturating_inc(); + } else { + let mut new_collection_ids = vec![]; + let mut new_position_ids = vec![]; + + for parent_collection_id in collection_ids.iter() { + if split_count > total_splits { + return Err(Error::::Unexpected.into()); + } + + let split_position_info = T::CombinatorialTokens::split_position( + who.clone(), + Some(parent_collection_id.clone()), + *market_id, + partition.clone(), + amount, + fuel.clone(), + )?; + + new_collection_ids.extend_from_slice(&split_position_info.collection_ids); + new_position_ids.extend_from_slice(&split_position_info.position_ids); + + split_count.saturating_inc(); + } + + collection_ids = new_collection_ids; + position_ids = new_position_ids; + } + } + + let result = (collection_ids, position_ids, collateral); + + Ok(result) + } + + pub(crate) fn try_common_collateral( + market_ids: Vec>, + ) -> Result, DispatchError> { + let first_market_id = market_ids.first().ok_or(Error::::InvalidMarketCount)?; + let first_market = T::MarketCommons::market(first_market_id)?; + let collateral = first_market.base_asset; + + for market_id in market_ids.iter() { + let market = T::MarketCommons::market(market_id)?; + ensure!(market.base_asset == collateral, Error::::CollateralMismatch); + } + + Ok(collateral) } } @@ -1031,14 +1849,17 @@ mod pallet { type Asset = AssetOf; fn pool_exists(market_id: Self::MarketId) -> bool { - Pools::::contains_key(market_id) + let Some(pool_id) = MarketIdToPoolId::::get(market_id) else { + return false; + }; + Pools::::contains_key(pool_id) } fn get_spot_price( market_id: Self::MarketId, asset: Self::Asset, ) -> Result { - let pool = Pools::::get(market_id).ok_or(Error::::PoolNotFound)?; + let pool = ::get(market_id)?; pool.calculate_spot_price(asset) } @@ -1047,7 +1868,7 @@ mod pallet { asset: Self::Asset, until: Self::Balance, ) -> Result { - let pool = Pools::::get(market_id).ok_or(Error::::PoolNotFound)?; + let pool = ::get(market_id)?; let buy_amount = pool.calculate_buy_amount_until(asset, until)?; let total_fee_fractional = Self::total_fee_fractional( pool.swap_fee, @@ -1074,7 +1895,7 @@ mod pallet { asset: Self::Asset, until: Self::Balance, ) -> Result { - let pool = Pools::::get(market_id).ok_or(Error::::PoolNotFound)?; + let pool = ::get(market_id)?; pool.calculate_sell_amount_until(asset, until) } diff --git a/zrml/neo-swaps/src/liquidity_tree/tests/mod.rs b/zrml/neo-swaps/src/liquidity_tree/tests/mod.rs index 49ff92aab..bb4e287bc 100644 --- a/zrml/neo-swaps/src/liquidity_tree/tests/mod.rs +++ b/zrml/neo-swaps/src/liquidity_tree/tests/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -20,11 +20,11 @@ use crate::{ assert_liquidity_tree_state, create_b_tree_map, liquidity_tree::{ - traits::liquidity_tree_helper::LiquidityTreeHelper, + traits::LiquidityTreeHelper, types::{LiquidityTreeError, Node}, }, mock::Runtime, - traits::liquidity_shares_manager::LiquiditySharesManager, + traits::LiquiditySharesManager, LiquidityTreeOf, }; use alloc::collections::BTreeMap; diff --git a/zrml/neo-swaps/src/liquidity_tree/traits/mod.rs b/zrml/neo-swaps/src/liquidity_tree/traits/mod.rs index 08dcddd9d..78fc4b648 100644 --- a/zrml/neo-swaps/src/liquidity_tree/traits/mod.rs +++ b/zrml/neo-swaps/src/liquidity_tree/traits/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -15,6 +15,6 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -pub(crate) mod liquidity_tree_helper; +mod liquidity_tree_helper; pub(crate) use liquidity_tree_helper::*; diff --git a/zrml/neo-swaps/src/macros.rs b/zrml/neo-swaps/src/macros.rs index 0c7bbb986..3e5e63109 100644 --- a/zrml/neo-swaps/src/macros.rs +++ b/zrml/neo-swaps/src/macros.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -61,11 +61,8 @@ macro_rules! assert_pool_state { $(,)? ) => { let pool = Pools::::get($market_id).unwrap(); - assert_eq!( - pool.reserves.values().cloned().collect::>(), - $reserves, - "assert_pool_state: Reserves mismatch" - ); + let pool_reserves = pool.assets().iter().map(|a| pool.reserves[a]).collect::>(); + assert_eq!(pool_reserves, $reserves, "assert_pool_state: Reserves mismatch"); let actual_spot_prices = pool .assets() .iter() @@ -100,7 +97,7 @@ macro_rules! assert_pool_state { .fold(0u128, |acc, node| acc + node.fees + node.lazy_fees); assert_eq!(actual_total_fees, $total_fees); let invariant = actual_spot_prices.iter().sum::(); - assert_approx!(invariant, _1, 1); + assert_approx!(invariant, _1, 2); }; } diff --git a/zrml/neo-swaps/src/math/mod.rs b/zrml/neo-swaps/src/math/mod.rs new file mode 100644 index 000000000..f0f7bc594 --- /dev/null +++ b/zrml/neo-swaps/src/math/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +pub(crate) mod traits; +mod transcendental; +pub(crate) mod types; diff --git a/zrml/neo-swaps/src/math/traits/combo_math_ops.rs b/zrml/neo-swaps/src/math/traits/combo_math_ops.rs new file mode 100644 index 000000000..5c0f790c3 --- /dev/null +++ b/zrml/neo-swaps/src/math/traits/combo_math_ops.rs @@ -0,0 +1,65 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{BalanceOf, Config}; +use alloc::vec::Vec; +use sp_runtime::DispatchError; + +pub(crate) trait ComboMathOps +where + T: Config, +{ + /// Calculates the amount swapped out of a pool with liquidity parameter `liquidity` when + /// swapping in `amount_in` units of assets whose reserves in the pool are `sell` and swapping + /// out assets whose reserves in the pool are `buy`. + fn calculate_swap_amount_out_for_buy( + buy: Vec>, + sell: Vec>, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + /// Calculates the amount eventually held by the user after equalizing holdings. + #[allow(dead_code)] + fn calculate_equalize_amount( + buy: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_sell: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + /// Calculates the amount of each asset of a pool with liquidity parameter `liquidity` held + /// in the user's wallet after equalizing positions whose reserves in the pool are `buy`, `keep` + /// and `sell`, resp. The parameters `amount_buy` and `amount_keep` refer to the user's holdings + /// of `buy` and `keep`. + fn calculate_swap_amount_out_for_sell( + buy: Vec>, + keep: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_keep: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + #[allow(dead_code)] + fn calculate_spot_price( + buy: Vec>, + sell: Vec>, + liquidity: BalanceOf, + ) -> Result, DispatchError>; +} diff --git a/zrml/neo-swaps/src/math/traits/math_ops.rs b/zrml/neo-swaps/src/math/traits/math_ops.rs new file mode 100644 index 000000000..904f07a5c --- /dev/null +++ b/zrml/neo-swaps/src/math/traits/math_ops.rs @@ -0,0 +1,67 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{BalanceOf, Config}; +use alloc::vec::Vec; +use sp_runtime::DispatchError; + +pub(crate) trait MathOps +where + T: Config, +{ + #[allow(dead_code)] + fn calculate_swap_amount_out_for_buy( + reserve: BalanceOf, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + #[allow(dead_code)] + fn calculate_swap_amount_out_for_sell( + reserve: BalanceOf, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + fn calculate_spot_price( + reserve: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + fn calculate_reserves_from_spot_prices( + amount: BalanceOf, + spot_prices: Vec>, + ) -> Result<(BalanceOf, Vec>), DispatchError>; + + fn calculate_buy_ln_argument( + reserve: BalanceOf, + amount: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError>; + + fn calculate_buy_amount_until( + until: BalanceOf, + liquidity: BalanceOf, + spot_price: BalanceOf, + ) -> Result, DispatchError>; + + fn calculate_sell_amount_until( + until: BalanceOf, + liquidity: BalanceOf, + spot_price: BalanceOf, + ) -> Result, DispatchError>; +} diff --git a/zrml/neo-swaps/src/math/traits/mod.rs b/zrml/neo-swaps/src/math/traits/mod.rs new file mode 100644 index 000000000..df48ce479 --- /dev/null +++ b/zrml/neo-swaps/src/math/traits/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod combo_math_ops; +mod math_ops; + +pub(crate) use combo_math_ops::ComboMathOps; +pub(crate) use math_ops::MathOps; diff --git a/zrml/neo-swaps/src/math/transcendental.rs b/zrml/neo-swaps/src/math/transcendental.rs new file mode 100644 index 000000000..6768083ab --- /dev/null +++ b/zrml/neo-swaps/src/math/transcendental.rs @@ -0,0 +1,120 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +// +// This file incorporates work covered by the following copyright and +// permission notice: +// +// Copyright (c) 2019 Alain Brenzikofer, modified by GalacticCouncil(2021) +// +// 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. +// +// Original source: https://github.com/encointer/substrate-fixed +// +// The changes applied are: Re-used and extended tests for `exp` and other +// functions. + +pub(crate) use hydra_dx_math::transcendental::{exp, ln}; + +#[cfg(test)] +mod tests { + use super::*; + use alloc::str::FromStr; + use fixed::types::U64F64; + use test_case::test_case; + + type S = U64F64; + type D = U64F64; + + #[test_case("0", false, "1")] + #[test_case("0", true, "1")] + #[test_case("1", false, "2.7182818284590452353")] + #[test_case("1", true, "0.367879441171442321595523770161460867445")] + #[test_case("2", false, "7.3890560989306502265")] + #[test_case("2", true, "0.13533528323661269186")] + #[test_case("0.1", false, "1.1051709180756476246")] + #[test_case("0.1", true, "0.9048374180359595733")] + #[test_case("0.9", false, "2.4596031111569496633")] + #[test_case("0.9", true, "0.40656965974059911195")] + #[test_case("1.5", false, "4.481689070338064822")] + #[test_case("1.5", true, "0.22313016014842982894")] + #[test_case("3.3", false, "27.1126389206578874259")] + #[test_case("3.3", true, "0.03688316740124000543")] + #[test_case("7.3456", false, "1549.3643050275008503592")] + #[test_case("7.3456", true, "0.00064542599616831253")] + #[test_case("12.3456789", false, "229964.194569082134542849")] + #[test_case("12.3456789", true, "0.00000434850304358833")] + #[test_case("13", false, "442413.39200892050332603603")] + #[test_case("13", true, "0.0000022603294069810542")] + fn exp_works(operand: &str, neg: bool, expected: &str) { + let o = U64F64::from_str(operand).unwrap(); + let e = U64F64::from_str(expected).unwrap(); + assert_eq!(exp::(o, neg).unwrap(), e); + } + + #[test_case("1", "0", false)] + #[test_case("2", "0.69314718055994530943", false)] + #[test_case("3", "1.09861228866810969136", false)] + #[test_case("2.718281828459045235360287471352662497757", "1", false)] + #[test_case("1.1051709180756476246", "0.09999999999999999975", false)] + #[test_case("2.4596031111569496633", "0.89999999999999999976", false)] + #[test_case("4.481689070338064822", "1.49999999999999999984", false)] + #[test_case("27.1126389206578874261", "3.3", false)] + #[test_case("1549.3643050275008503592", "7.34560000000000000003", false)] + #[test_case("229964.194569082134542849", "12.3456789000000000002", false)] + #[test_case("442413.39200892050332603603", "13.0000000000000000002", false)] + #[test_case("0.9048374180359595733", "0.09999999999999999975", true)] + #[test_case("0.40656965974059911195", "0.8999999999999999998", true)] + #[test_case("0.22313016014842982894", "1.4999999999999999999", true)] + #[test_case("0.03688316740124000543", "3.3000000000000000005", true)] + #[test_case("0.00064542599616831253", "7.34560000000000002453", true)] + #[test_case("0.00000434850304358833", "12.34567890000000711117", true)] + #[test_case("0.0000022603294069810542", "13.0000000000000045352", true)] + #[test_case("1.0001", "0.00009999500033330827", false)] + #[test_case("1.00000001", "0.0000000099999999499", false)] + #[test_case("0.9999", "0.00010000500033335825", true)] + #[test_case("0.99999999", "0.00000001000000004987", true)] + // Powers of 2 (since we're using squares when calculating the fractional part of log2. + #[test_case("3.999999999", "1.38629436086989061877", false)] + #[test_case("4", "1.38629436111989061886", false)] + #[test_case("4.000000001", "1.3862943613698906188", false)] + #[test_case("7.999999999", "2.07944154155483592824", false)] + #[test_case("8", "2.0794415416798359283", false)] + #[test_case("8.000000001", "2.0794415418048359282", false)] + #[test_case("0.499999999", "0.69314718255994531136", true)] + #[test_case("0.5", "0.69314718055994530943", true)] + #[test_case("0.500000001", "0.69314717855994531135", true)] + #[test_case("0.249999999", "1.38629436511989062684", true)] + #[test_case("0.25", "1.38629436111989061886", true)] + #[test_case("0.250000001", "1.38629435711989062676", true)] + fn ln_works(operand: &str, expected_abs: &str, expected_neg: bool) { + let o = U64F64::from_str(operand).unwrap(); + let e = U64F64::from_str(expected_abs).unwrap(); + let (a, n) = ln::(o).unwrap(); + assert_eq!(a, e); + assert_eq!(n, expected_neg); + } +} diff --git a/zrml/neo-swaps/src/math/types/combo_math.rs b/zrml/neo-swaps/src/math/types/combo_math.rs new file mode 100644 index 000000000..bb553d211 --- /dev/null +++ b/zrml/neo-swaps/src/math/types/combo_math.rs @@ -0,0 +1,741 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{ + math::{traits::ComboMathOps, transcendental::ln, types::common::FixedType}, + BalanceOf, Config, Error, +}; +use alloc::vec::Vec; +use core::marker::PhantomData; +use sp_runtime::{traits::Zero, DispatchError, SaturatedConversion}; + +pub(crate) struct ComboMath(PhantomData); + +impl ComboMathOps for ComboMath +where + T: Config, +{ + fn calculate_swap_amount_out_for_buy( + buy: Vec>, + sell: Vec>, + amount_in: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + detail::calculate_swap_amount_out_for_buy( + buy.into_iter().map(|x| x.saturated_into()).collect(), + sell.into_iter().map(|x| x.saturated_into()).collect(), + amount_in.saturated_into(), + liquidity.saturated_into(), + ) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } + + fn calculate_equalize_amount( + buy: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_sell: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + detail::calculate_equalize_amount( + buy.into_iter().map(|x| x.saturated_into()).collect(), + sell.into_iter().map(|x| x.saturated_into()).collect(), + amount_buy.saturated_into(), + amount_sell.saturated_into(), + liquidity.saturated_into(), + ) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } + + fn calculate_swap_amount_out_for_sell( + buy: Vec>, + keep: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_keep: BalanceOf, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + detail::calculate_swap_amount_out_for_sell( + buy.into_iter().map(|x| x.saturated_into()).collect(), + keep.into_iter().map(|x| x.saturated_into()).collect(), + sell.into_iter().map(|x| x.saturated_into()).collect(), + amount_buy.saturated_into(), + amount_keep.saturated_into(), + liquidity.saturated_into(), + ) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } + + fn calculate_spot_price( + buy: Vec>, + sell: Vec>, + liquidity: BalanceOf, + ) -> Result, DispatchError> { + detail::calculate_spot_price( + buy.into_iter().map(|x| x.saturated_into()).collect(), + sell.into_iter().map(|x| x.saturated_into()).collect(), + liquidity.saturated_into(), + ) + .map(|result| result.saturated_into()) + .ok_or_else(|| Error::::MathError.into()) + } +} + +mod detail { + use super::*; + use crate::math::types::common::{from_fixed, protected_exp, to_fixed}; + + /// Converts `Vec` of fixed decimal numbers to a `Vec` of fixed point numbers; + /// returns `None` if any of them fail. + fn vec_to_fixed(vec: Vec) -> Option> { + vec.into_iter().map(to_fixed).collect() + } + + /// Returns `\sum_{r \in R} e^{-r/b}`, where `R` denotes `reserves` and `b` denotes `liquidity`. + /// The result is `None` if and only if any of the `exp` calculations has failed. + fn exp_sum(reserves: Vec, liquidity: FixedType) -> Option { + reserves + .iter() + .map(|r| protected_exp(r.checked_div(liquidity)?, true)) + .collect::>>()? + .iter() + .try_fold(FixedType::zero(), |acc, &val| acc.checked_add(val)) + } + + pub(super) fn calculate_swap_amount_out_for_buy( + buy: Vec, + sell: Vec, + amount_in: u128, + liquidity: u128, + ) -> Option { + let result_fixed = calculate_swap_amount_out_for_buy_fixed( + vec_to_fixed(buy)?, + vec_to_fixed(sell)?, + to_fixed(amount_in)?, + to_fixed(liquidity)?, + )?; + from_fixed(result_fixed) + } + + pub(super) fn calculate_equalize_amount( + buy: Vec, + sell: Vec, + amount_buy: u128, + amount_sell: u128, + liquidity: u128, + ) -> Option { + let result_fixed = calculate_equalize_amount_fixed( + vec_to_fixed(buy)?, + vec_to_fixed(sell)?, + to_fixed(amount_buy)?, + to_fixed(amount_sell)?, + to_fixed(liquidity)?, + )?; + from_fixed(result_fixed) + } + + pub(super) fn calculate_swap_amount_out_for_sell( + buy: Vec, + keep: Vec, + sell: Vec, + amount_buy: u128, + amount_keep: u128, + liquidity: u128, + ) -> Option { + let result_fixed = calculate_swap_amount_out_for_sell_fixed( + vec_to_fixed(buy)?, + vec_to_fixed(keep)?, + vec_to_fixed(sell)?, + to_fixed(amount_buy)?, + to_fixed(amount_keep)?, + to_fixed(liquidity)?, + )?; + from_fixed(result_fixed) + } + + pub(super) fn calculate_spot_price( + buy: Vec, + sell: Vec, + liquidity: u128, + ) -> Option { + let result_fixed = calculate_spot_price_fixed( + vec_to_fixed(buy)?, + vec_to_fixed(sell)?, + to_fixed(liquidity)?, + )?; + from_fixed(result_fixed) + } + + fn calculate_swap_amount_out_for_buy_fixed( + buy: Vec, + sell: Vec, + amount_in: FixedType, + liquidity: FixedType, + ) -> Option { + if buy.is_empty() || sell.is_empty() || amount_in.is_zero() { + return None; + } + + let exp_sum_buy = exp_sum(buy, liquidity)?; + let exp_sum_sell = exp_sum(sell, liquidity)?; + let amount_in_div_liquidity = amount_in.checked_div(liquidity)?; + let exp_of_minus_amount_in: FixedType = protected_exp(amount_in_div_liquidity, true)?; + let exp_of_minus_amount_in_times_exp_sum_sell = + exp_of_minus_amount_in.checked_mul(exp_sum_sell)?; + // Reminder from the documentation: `exp_sum_buy + exp_sum_sell = 1 - exp_sum_keep` + let numerator = exp_sum_buy + .checked_add(exp_sum_sell)? + .checked_sub(exp_of_minus_amount_in_times_exp_sum_sell)?; + let ln_arg = numerator.checked_div(exp_sum_buy)?; + let (ln_val, _): (FixedType, _) = ln(ln_arg).ok()?; + ln_val.checked_mul(liquidity) + } + + fn calculate_equalize_amount_fixed( + buy: Vec, + sell: Vec, + amount_buy: FixedType, + amount_sell: FixedType, + liquidity: FixedType, + ) -> Option { + if buy.is_empty() || sell.is_empty() || amount_buy.is_zero() { + return None; + } + + let exp_sum_buy = exp_sum(buy, liquidity)?; + let exp_sum_sell = exp_sum(sell, liquidity)?; + let numerator = exp_sum_buy.checked_add(exp_sum_sell)?; + let delta = amount_buy.checked_sub(amount_sell)?; + let delta_div_liquidity = delta.checked_div(liquidity)?; + let exp_delta: FixedType = protected_exp(delta_div_liquidity, false)?; + let exp_delta_times_exp_sum_sell = exp_delta.checked_mul(exp_sum_sell)?; + let denominator = exp_sum_buy.checked_add(exp_delta_times_exp_sum_sell)?; + let ln_arg = numerator.checked_div(denominator)?; + let (ln_val, _): (FixedType, _) = ln(ln_arg).ok()?; + ln_val.checked_mul(liquidity) + } + + fn calculate_swap_amount_out_for_sell_fixed( + buy: Vec, + keep: Vec, + sell: Vec, + amount_buy: FixedType, + amount_keep: FixedType, + liquidity: FixedType, + ) -> Option { + // Ensure that either `keep` is empty and `amount_keep` is zero, or `keep` is non-empty and + // `amount_keep` is non-zero. + if (keep.is_empty() && !amount_keep.is_zero()) + || (!keep.is_empty() && amount_keep.is_zero()) + { + return None; + } + + // Reserves change after the first equalization. Since we do two equalization calculations + // in one, we need to determine the intermediate reserves for the second calculation. + let (amount_buy_keep, buy_keep) = if keep.is_empty() { + (amount_buy, buy) + } else { + let delta_buy = calculate_equalize_amount_fixed( + buy.clone(), + keep.clone(), + amount_buy, + amount_keep, + liquidity, + )?; + + let delta_keep = amount_buy.checked_sub(delta_buy)?.checked_sub(amount_keep)?; + + let buy_intermediate = + buy.into_iter().map(|x| x.checked_add(delta_buy)).collect::>>()?; + let keep_intermediate = + keep.into_iter().map(|x| x.checked_sub(delta_keep)).collect::>>()?; + let buy_keep = buy_intermediate.into_iter().chain(keep_intermediate).collect(); + + (amount_buy.checked_sub(delta_buy)?, buy_keep) + }; + + let delta_buy_keep = calculate_equalize_amount_fixed( + buy_keep, + sell, + amount_buy_keep, + FixedType::zero(), + liquidity, + )?; + + amount_buy_keep.checked_sub(delta_buy_keep) + } + + fn calculate_spot_price_fixed( + buy: Vec, + sell: Vec, + liquidity: FixedType, + ) -> Option { + let exp_sum_buy = exp_sum(buy, liquidity)?; + let exp_sum_sell = exp_sum(sell, liquidity)?; + let denominator = exp_sum_buy.checked_add(exp_sum_sell)?; + exp_sum_buy.checked_div(denominator) + } +} + +#[cfg(test)] +mod tests { + // TODO(#1328): Remove after rustc nightly-2024-04-22 + #![allow(clippy::duplicated_attributes)] + + use super::*; + use crate::mock::Runtime as MockRuntime; + use frame_support::assert_err; + use test_case::test_case; + use zeitgeist_primitives::constants::base_multiples::*; + + type MockBalance = BalanceOf; + type MockMath = ComboMath; + + // Example taken from + // https://github.com/gnosis/conditional-tokens-docs/blob/e73aa18ab82446049bca61df31fc88efd3cdc5cc/docs/intro3.md?plain=1#L78-L88 + #[test_case(vec![_10], vec![_10], _10, 144_269_504_088, 58_496_250_072)] + #[test_case(vec![_1], vec![4_586_751_453], _1, _1, 7_353_256_641)] + #[test_case(vec![_2], vec![9_173_502_907], _2, _2, 14_706_513_281; "positive ln")] + #[test_case(vec![_1], vec![37_819_608_145], _1_10, _3, 386_589_943; "negative ln")] + // Tests generated with Python. + #[test_case(vec![_100, _100], vec![_100], _10, 721_347_520_444, 45_240_236_913)] + #[test_case(vec![_100, _100, _100], vec![_100], _10, 721_347_520_444, 30_473_182_882)] + #[test_case(vec![_100, _100], vec![_100, _100], _10, 721_347_520_444, 87_809_842_736)] + #[test_case(vec![_100], vec![_100, _100, _100], _10, 721_347_520_444, 236_684_778_998)] + #[test_case( + vec![848_358_525_162, 482_990_395_533], + vec![730_736_259_258], + _10, + 527_114_788_714, + 36_648_762_089 + )] + #[test_case( + vec![848_358_525_162, _100, 482_990_395_533], + vec![730_736_259_258], + _10, + 527_114_788_714, + 29_520_025_573 + )] + #[test_case( + vec![848_358_525_162, 482_990_395_533, _100], + vec![730_736_259_258], + _10, + 527_114_788_714, + 29_520_025_573 + )] + #[test_case( + vec![848_358_525_162, 482_990_395_533], + vec![730_736_259_258, _100], + _10, + 527_114_788_714, + 57_474_148_073 + )] + #[test_case( + vec![482_990_395_533], + vec![730_736_259_258, _100, 848_358_525_162], + _10, + 527_114_788_714, + 121_489_297_813 + )] + #[test_case( + vec![848_358_525_162, 482_990_395_533], + vec![730_736_259_258, _100], + 1_00, + 527_114_788_714, + 67 + )] + #[test_case( + vec![848_358_525_162, 482_990_395_533], + vec![730_736_259_258, _100], + 1, + 527_114_788_714, + 1 + )] + fn calculate_swap_amount_out_for_buy_works( + buy: Vec, + sell: Vec, + amount_in: MockBalance, + liquidity: MockBalance, + expected: MockBalance, + ) { + assert_eq!( + MockMath::calculate_swap_amount_out_for_buy(buy, sell, amount_in, liquidity).unwrap(), + expected + ); + } + + #[test_case(vec![_1], vec![_1], _1, 0)] // Division by zero + #[test_case(vec![_1], vec![_1], 1_000 * _1, _1)] // Overflow + #[test_case(vec![u128::MAX], vec![_1], _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![u128::MAX], _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], u128::MAX, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], _1, u128::MAX)] // to_fixed error + #[test_case(vec![], vec![_1], _1, _1)] // empty vector + #[test_case(vec![_1], vec![], _1, _1)] // empty vector + #[test_case(vec![_1], vec![_1], 0, _1)] // zero value + fn calculate_swap_amount_out_for_buy_throws_math_error( + buy: Vec, + sell: Vec, + amount_in: MockBalance, + liquidity: MockBalance, + ) { + assert_err!( + MockMath::calculate_swap_amount_out_for_buy(buy, sell, amount_in, liquidity), + Error::::MathError + ); + } + + // "Reversing" the tests for `calculate_swap_amount_for_buy`. + #[test_case(vec![_11], vec![_12], _10, _10, 144_269_504_088, 0)] + #[test_case( + vec![_10 - 58_496_250_072], + vec![_20], + _10 + 58_496_250_072, + 0, + 144_269_504_088, + 58_496_250_072 + )] + #[test_case( + vec![_1 - 7_353_256_641], + vec![14_586_751_453], + 17_353_256_641, + 0, + _1, + 7_353_256_641 + )] + #[test_case( + vec![_2 - 14_706_513_281], + vec![_2 + 9_173_502_907], + _2 + 14_706_513_281, + 0, + _2, + 14_706_513_281; + "positive ln" + )] + #[test_case( + vec![_1 - 386_589_943], + vec![37_819_608_145 + _1_10], + _1_10 + 386_589_943, + 0, + _3, + 386_589_943; + "negative ln" + )] + // Tests generated with Python + #[test_case( + vec![537_243_573_680, 305_865_360_520], + vec![462_756_426_319], + 76_500_000_000, + 43_200_000_000, + 333_808_200_695, + 10_143_603_301 + )] + #[test_case( + vec![537_243_573_680, 305_865_360_520, 768_621_786_840], + vec![462_756_426_319], + 232_000_000_000, + 112_300_000_000, + 333_808_200_695, + 35_887_802_365 + )] + #[test_case( + vec![537_243_573_680, 305_865_360_520], + vec![462_756_426_319, _100], + _10, + _5, + 333_808_200_695, + 17_512_119_761 + )] + #[test_case( + vec![537_243_573_680, 305_865_360_520], + vec![_100, 462_756_426_319], + _10, + _5, + 333_808_200_695, + 17_512_119_761 + )] + #[test_case( + vec![305_865_360_520, 537_243_573_680], + vec![462_756_426_319, _100], + _10, + _5, + 333_808_200_695, + 17_512_119_761 + )] + #[test_case( + vec![305_865_360_520, 537_243_573_680], + vec![_100, 462_756_426_319], + _10, + _5, + 333_808_200_695, + 17_512_119_761 + )] + #[test_case( + vec![305_865_360_520, 537_243_573_680], + vec![_100, 462_756_426_319], + _10, + 100, + 333_808_200_695, + 36_763_618_626 + )] + #[test_case( + vec![305_865_360_520, 537_243_573_680], + vec![_100, 462_756_426_319], + _10, + 1, + 333_808_200_695, + 36_763_618_666 + )] + #[test_case( + vec![305_865_360_520, 537_243_573_680], + vec![_100, 462_756_426_319], + 2, + 1, + 333_808_200_695, + 0 + )] + #[test_case( + vec![305_865_360_520, 537_243_573_680], + vec![_100, 462_756_426_319], + 1, + 0, + 333_808_200_695, + 0 + )] + fn calculate_equalize_amount_works( + buy: Vec, + sell: Vec, + amount_buy: MockBalance, + amount_sell: MockBalance, + liquidity: MockBalance, + expected: MockBalance, + ) { + assert_eq!( + MockMath::calculate_equalize_amount(buy, sell, amount_buy, amount_sell, liquidity) + .unwrap(), + expected + ); + } + + #[test_case(vec![_1], vec![_1], _1, _1, 0)] // Division by zero + #[test_case(vec![_1], vec![_1], 1_000 * _1, _1, _1)] // Overflow + #[test_case(vec![_1], vec![_1], _1, 1_000 * _1, _1)] // Overflow + #[test_case(vec![u128::MAX], vec![_1], _1, _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![u128::MAX], _1, _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], u128::MAX, _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], _1, u128::MAX, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], _1, _1, u128::MAX)] // to_fixed error + #[test_case(vec![], vec![_1], _1, _1, _1)] // empty vector + #[test_case(vec![_1], vec![], _1, _1, _1)] // empty vector + #[test_case(vec![_1], vec![_1], 0, _1, _1)] // zero value + fn calculate_equalize_amount_throws_error( + buy: Vec, + sell: Vec, + amount_buy: MockBalance, + amount_sell: MockBalance, + liquidity: MockBalance, + ) { + assert_err!( + MockMath::calculate_equalize_amount(buy, sell, amount_buy, amount_sell, liquidity), + Error::::MathError + ); + } + + // Tests for `calculate_equalize`. + #[test_case( + vec![_10 - 58_496_250_072], + vec![], + vec![_20], + _10 + 58_496_250_072, + 0, + 144_269_504_088, + _10 + )] + #[test_case( + vec![_1 - 7_353_256_641], + vec![], + vec![14_586_751_453], + 17_353_256_641, + 0, + _1, + _1 + )] + #[test_case( + vec![_2 - 14_706_513_281], + vec![], + vec![_2 + 9_173_502_907], + _2 + 14_706_513_281, + 0, + _2, + _2; + "positive ln" + )] + #[test_case( + vec![_1 - 386_589_943], + vec![], + vec![37_819_608_145 + _1_10], + _1_10 + 386_589_943, + 0, + _3, + _1_10; + "negative ln" + )] + // Tests generated by Python. + #[test_case( + vec![_100, 305_865_360_520], + vec![768_621_786_840, _100, 768_621_786_840, _100], + vec![462_756_426_319], + 76_500_000_000, + 43_200_000_000, + 333_808_200_695, + 45_943_057_520 + )] + #[test_case( + vec![_100, 305_865_360_520], + vec![768_621_786_840, _100, 768_621_786_840, _100], + vec![462_756_426_319], + _2, + _1, + 333_808_200_695, + 11_900_842_524 + )] + #[test_case( + vec![_100, 305_865_360_520, 768_621_786_840], + vec![_100, 768_621_786_840], + vec![462_756_426_319, _100], + 123_400_000_000, + _1, + 333_808_200_695, + 63_972_215_306 + )] + #[test_case( + vec![_100, 305_865_360_520, 768_621_786_840], + vec![_100], + vec![462_756_426_319, _100, 768_621_786_840], + 123_400_000_000, + 1, + 333_808_200_695, + 62_187_083_257 + )] + #[test_case( + vec![_100, 305_865_360_520, 768_621_786_840], + vec![_100], + vec![462_756_426_319, _100, 768_621_786_840], + 2, + 1, + 333_808_200_695, + 1 + )] + #[test_case( + vec![_100, 305_865_360_520, 768_621_786_840], + vec![], + vec![462_756_426_319, _100, 768_621_786_840, _100], + 123_400_000_000, + 0, + 333_808_200_695, + 62_187_083_257 + )] + fn calculate_swap_amount_out_for_sell_works( + buy: Vec, + keep: Vec, + sell: Vec, + amount_buy: MockBalance, + amount_sell: MockBalance, + liquidity: MockBalance, + expected: MockBalance, + ) { + assert_eq!( + MockMath::calculate_swap_amount_out_for_sell( + buy, + keep, + sell, + amount_buy, + amount_sell, + liquidity + ) + .unwrap(), + expected + ); + } + + #[test_case(vec![_1], vec![_1], vec![_1], _1, _1, 0)] // Division by zero + #[test_case(vec![_1], vec![_1], vec![_1], 1_000 * _1, _1, _1)] // Overflow + #[test_case(vec![_1], vec![_1], vec![_1], _1, 1_000 * _1, _1)] // Overflow + #[test_case(vec![u128::MAX], vec![_1], vec![_1], _1, _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![u128::MAX], vec![_1], _1, _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], vec![u128::MAX], u128::MAX, _1, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], vec![_1], _1, u128::MAX, _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], vec![_1], _1, _1, u128::MAX)] // to_fixed error + #[test_case(vec![], vec![_1], vec![_1], _1, _1, _1)] // empty vector + #[test_case(vec![_1], vec![_1], vec![], _1, _1, _1)] // empty vector + #[test_case(vec![_1], vec![], vec![_1], _1, _1, _1)] // empty vector + #[test_case(vec![_1], vec![_1], vec![_1], 0, _1, _1)] // zero value + #[test_case(vec![_1], vec![_1], vec![_1], _1, 0, _1)] // zero value + fn calculate_swap_amount_out_for_sell_throws_error( + buy: Vec, + keep: Vec, + sell: Vec, + amount_buy: MockBalance, + amount_keep: MockBalance, + liquidity: MockBalance, + ) { + assert_err!( + MockMath::calculate_swap_amount_out_for_sell( + buy, + keep, + sell, + amount_buy, + amount_keep, + liquidity + ), + Error::::MathError + ); + } + + #[test_case(vec![_10], vec![_10], 144_269_504_088, _1_2)] + #[test_case(vec![_10 - 58_496_250_072], vec![_20], 144_269_504_088, _3_4)] + #[test_case(vec![_20], vec![_10 - 58_496_250_072], 144_269_504_088, _1_4)] + fn calcuate_spot_price_works( + buy: Vec, + sell: Vec, + liquidity: MockBalance, + expected: MockBalance, + ) { + assert_eq!(MockMath::calculate_spot_price(buy, sell, liquidity).unwrap(), expected); + } + + #[test_case(vec![_1], vec![_1], 0)] // Division by zero + #[test_case(vec![1_000 * _1], vec![_1], _1)] // Overflow + #[test_case(vec![_1], vec![1_000 * _1], _1)] // Overflow + #[test_case(vec![u128::MAX], vec![_1], _1)] // to_fixed error + #[test_case(vec![_1], vec![u128::MAX], _1)] // to_fixed error + #[test_case(vec![_1], vec![_1], u128::MAX)] // to_fixed error + fn calculate_spot_price_throws_math_error( + buy: Vec, + sell: Vec, + liquidity: MockBalance, + ) { + assert_err!( + MockMath::calculate_spot_price(buy, sell, liquidity), + Error::::MathError + ); + } +} diff --git a/zrml/neo-swaps/src/math/types/common.rs b/zrml/neo-swaps/src/math/types/common.rs new file mode 100644 index 000000000..0f632d196 --- /dev/null +++ b/zrml/neo-swaps/src/math/types/common.rs @@ -0,0 +1,51 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::math::transcendental::exp; +use fixed::FixedU128; +use typenum::U80; +use zeitgeist_primitives::{ + constants::DECIMALS, + math::fixed::{IntoFixedDecimal, IntoFixedFromDecimal}, +}; + +type Fractional = U80; +pub(crate) type FixedType = FixedU128; + +// 32.44892769177272 +pub(crate) const EXP_NUMERICAL_THRESHOLD: FixedType = + FixedType::from_bits(0x20_72EC_ECDA_6EBE_EACC_40C7); + +pub(crate) fn to_fixed(value: B) -> Option +where + B: Into + From, +{ + value.to_fixed_from_fixed_decimal(DECIMALS).ok() +} + +pub(crate) fn from_fixed(value: FixedType) -> Option +where + B: Into + From, +{ + value.to_fixed_decimal(DECIMALS).ok() +} + +/// Calculates `exp(value)` but returns `None` if `value` lies outside of the numerical +/// boundaries. +pub(crate) fn protected_exp(value: FixedType, neg: bool) -> Option { + if value < EXP_NUMERICAL_THRESHOLD { exp(value, neg).ok() } else { None } +} diff --git a/zrml/neo-swaps/src/math.rs b/zrml/neo-swaps/src/math/types/math.rs similarity index 74% rename from zrml/neo-swaps/src/math.rs rename to zrml/neo-swaps/src/math/types/math.rs index 401dfb8be..6660ed0b3 100644 --- a/zrml/neo-swaps/src/math.rs +++ b/zrml/neo-swaps/src/math/types/math.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -14,93 +14,28 @@ // // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -// -// This file incorporates work covered by the following copyright and -// permission notice: -// -// Copyright (c) 2019 Alain Brenzikofer, modified by GalacticCouncil(2021) -// -// 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. -// -// Original source: https://github.com/encointer/substrate-fixed -// -// The changes applied are: Re-used and extended tests for `exp` and other -// functions. use crate::{ - math::transcendental::{exp, ln}, + math::{ + traits::MathOps, + transcendental::ln, + types::common::{FixedType, EXP_NUMERICAL_THRESHOLD}, + }, BalanceOf, Config, Error, }; use alloc::vec::Vec; use core::marker::PhantomData; -use fixed::FixedU128; use sp_runtime::{ traits::{One, Zero}, DispatchError, SaturatedConversion, }; -use typenum::U80; - -type Fractional = U80; -type FixedType = FixedU128; - -// 32.44892769177272 -const EXP_OVERFLOW_THRESHOLD: FixedType = FixedType::from_bits(0x20_72EC_ECDA_6EBE_EACC_40C7); - -pub(crate) trait MathOps { - fn calculate_swap_amount_out_for_buy( - reserve: BalanceOf, - amount_in: BalanceOf, - liquidity: BalanceOf, - ) -> Result, DispatchError>; - - fn calculate_swap_amount_out_for_sell( - reserve: BalanceOf, - amount_in: BalanceOf, - liquidity: BalanceOf, - ) -> Result, DispatchError>; - - fn calculate_spot_price( - reserve: BalanceOf, - liquidity: BalanceOf, - ) -> Result, DispatchError>; - - fn calculate_reserves_from_spot_prices( - amount: BalanceOf, - spot_prices: Vec>, - ) -> Result<(BalanceOf, Vec>), DispatchError>; - - fn calculate_buy_ln_argument( - reserve: BalanceOf, - amount: BalanceOf, - liquidity: BalanceOf, - ) -> Result, DispatchError>; - - fn calculate_buy_amount_until( - until: BalanceOf, - liquidity: BalanceOf, - spot_price: BalanceOf, - ) -> Result, DispatchError>; - - fn calculate_sell_amount_until( - until: BalanceOf, - liquidity: BalanceOf, - spot_price: BalanceOf, - ) -> Result, DispatchError>; -} pub(crate) struct Math(PhantomData); -impl MathOps for Math { +impl MathOps for Math +where + T: Config, +{ fn calculate_swap_amount_out_for_buy( reserve: BalanceOf, amount_in: BalanceOf, @@ -194,10 +129,7 @@ impl MathOps for Math { mod detail { use super::*; - use zeitgeist_primitives::{ - constants::DECIMALS, - math::fixed::{IntoFixedDecimal, IntoFixedFromDecimal}, - }; + use crate::math::types::common::{from_fixed, protected_exp, to_fixed}; /// Calculate b * ln( e^(x/b) − 1 + e^(−r_i/b) ) + r_i − x. pub(super) fn calculate_swap_amount_out_for_buy( @@ -286,20 +218,6 @@ mod detail { from_fixed(result_fixed) } - fn to_fixed(value: B) -> Option - where - B: Into + From, - { - value.to_fixed_from_fixed_decimal(DECIMALS).ok() - } - - fn from_fixed(value: FixedType) -> Option - where - B: Into + From, - { - value.to_fixed_decimal(DECIMALS).ok() - } - fn calculate_swap_amount_out_for_buy_fixed( reserve: FixedType, amount_in: FixedType, @@ -322,8 +240,8 @@ mod detail { // Ensure that if the reserve is zero, we don't accidentally return a non-zero value. return None; } - let exp_neg_x_over_b: FixedType = exp(amount_in.checked_div(liquidity)?, true).ok()?; - let exp_r_over_b = exp(reserve.checked_div(liquidity)?, false).ok()?; + let exp_neg_x_over_b: FixedType = protected_exp(amount_in.checked_div(liquidity)?, true)?; + let exp_r_over_b = protected_exp(reserve.checked_div(liquidity)?, false)?; let inside_ln = exp_neg_x_over_b .checked_add(exp_r_over_b)? .checked_sub(FixedType::checked_from_num(1)?)?; @@ -336,7 +254,7 @@ mod detail { reserve: FixedType, liquidity: FixedType, ) -> Option { - exp(reserve.checked_div(liquidity)?, true).ok() + protected_exp(reserve.checked_div(liquidity)?, true) } fn calculate_reserve_from_spot_prices_fixed( @@ -366,10 +284,10 @@ mod detail { amount_in: FixedType, liquidity: FixedType, ) -> Option { - let exp_x_over_b: FixedType = exp(amount_in.checked_div(liquidity)?, false).ok()?; + let exp_x_over_b: FixedType = protected_exp(amount_in.checked_div(liquidity)?, false)?; let r_over_b = reserve.checked_div(liquidity)?; - let exp_neg_r_over_b = if r_over_b < EXP_OVERFLOW_THRESHOLD { - exp(reserve.checked_div(liquidity)?, true).ok()? + let exp_neg_r_over_b = if r_over_b < EXP_NUMERICAL_THRESHOLD { + protected_exp(r_over_b, true)? } else { FixedType::checked_from_num(0)? // Underflow to zero. }; @@ -418,98 +336,15 @@ mod detail { } } -mod transcendental { - pub(crate) use hydra_dx_math::transcendental::{exp, ln}; - - #[cfg(test)] - mod tests { - - use super::*; - use alloc::str::FromStr; - use fixed::types::U64F64; - use test_case::test_case; - - type S = U64F64; - type D = U64F64; - - #[test_case("0", false, "1")] - #[test_case("0", true, "1")] - #[test_case("1", false, "2.7182818284590452353")] - #[test_case("1", true, "0.367879441171442321595523770161460867445")] - #[test_case("2", false, "7.3890560989306502265")] - #[test_case("2", true, "0.13533528323661269186")] - #[test_case("0.1", false, "1.1051709180756476246")] - #[test_case("0.1", true, "0.9048374180359595733")] - #[test_case("0.9", false, "2.4596031111569496633")] - #[test_case("0.9", true, "0.40656965974059911195")] - #[test_case("1.5", false, "4.481689070338064822")] - #[test_case("1.5", true, "0.22313016014842982894")] - #[test_case("3.3", false, "27.1126389206578874259")] - #[test_case("3.3", true, "0.03688316740124000543")] - #[test_case("7.3456", false, "1549.3643050275008503592")] - #[test_case("7.3456", true, "0.00064542599616831253")] - #[test_case("12.3456789", false, "229964.194569082134542849")] - #[test_case("12.3456789", true, "0.00000434850304358833")] - #[test_case("13", false, "442413.39200892050332603603")] - #[test_case("13", true, "0.0000022603294069810542")] - fn exp_works(operand: &str, neg: bool, expected: &str) { - let o = U64F64::from_str(operand).unwrap(); - let e = U64F64::from_str(expected).unwrap(); - assert_eq!(exp::(o, neg).unwrap(), e); - } - - #[test_case("1", "0", false)] - #[test_case("2", "0.69314718055994530943", false)] - #[test_case("3", "1.09861228866810969136", false)] - #[test_case("2.718281828459045235360287471352662497757", "1", false)] - #[test_case("1.1051709180756476246", "0.09999999999999999975", false)] - #[test_case("2.4596031111569496633", "0.89999999999999999976", false)] - #[test_case("4.481689070338064822", "1.49999999999999999984", false)] - #[test_case("27.1126389206578874261", "3.3", false)] - #[test_case("1549.3643050275008503592", "7.34560000000000000003", false)] - #[test_case("229964.194569082134542849", "12.3456789000000000002", false)] - #[test_case("442413.39200892050332603603", "13.0000000000000000002", false)] - #[test_case("0.9048374180359595733", "0.09999999999999999975", true)] - #[test_case("0.40656965974059911195", "0.8999999999999999998", true)] - #[test_case("0.22313016014842982894", "1.4999999999999999999", true)] - #[test_case("0.03688316740124000543", "3.3000000000000000005", true)] - #[test_case("0.00064542599616831253", "7.34560000000000002453", true)] - #[test_case("0.00000434850304358833", "12.34567890000000711117", true)] - #[test_case("0.0000022603294069810542", "13.0000000000000045352", true)] - #[test_case("1.0001", "0.00009999500033330827", false)] - #[test_case("1.00000001", "0.0000000099999999499", false)] - #[test_case("0.9999", "0.00010000500033335825", true)] - #[test_case("0.99999999", "0.00000001000000004987", true)] - // Powers of 2 (since we're using squares when calculating the fractional part of log2. - #[test_case("3.999999999", "1.38629436086989061877", false)] - #[test_case("4", "1.38629436111989061886", false)] - #[test_case("4.000000001", "1.3862943613698906188", false)] - #[test_case("7.999999999", "2.07944154155483592824", false)] - #[test_case("8", "2.0794415416798359283", false)] - #[test_case("8.000000001", "2.0794415418048359282", false)] - #[test_case("0.499999999", "0.69314718255994531136", true)] - #[test_case("0.5", "0.69314718055994530943", true)] - #[test_case("0.500000001", "0.69314717855994531135", true)] - #[test_case("0.249999999", "1.38629436511989062684", true)] - #[test_case("0.25", "1.38629436111989061886", true)] - #[test_case("0.250000001", "1.38629435711989062676", true)] - fn ln_works(operand: &str, expected_abs: &str, expected_neg: bool) { - let o = U64F64::from_str(operand).unwrap(); - let e = U64F64::from_str(expected_abs).unwrap(); - let (a, n) = ln::(o).unwrap(); - assert_eq!(a, e); - assert_eq!(n, expected_neg); - } - } -} - #[cfg(test)] mod tests { // TODO(#1328): Remove after rustc nightly-2024-04-22 #![allow(clippy::duplicated_attributes)] use super::*; - use crate::{mock::Runtime as MockRuntime, MAX_SPOT_PRICE, MIN_SPOT_PRICE}; + use crate::{ + math::transcendental::exp, mock::Runtime as MockRuntime, MAX_SPOT_PRICE, MIN_SPOT_PRICE, + }; use alloc::str::FromStr; use frame_support::assert_err; use test_case::test_case; @@ -519,7 +354,7 @@ mod tests { type MockMath = Math; // Example taken from - // https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-lmsr + // https://github.com/gnosis/conditional-tokens-docs/blob/e73aa18ab82446049bca61df31fc88efd3cdc5cc/docs/intro3.md?plain=1#L78-L88 #[test_case(_10, _10, 144_269_504_088, 58_496_250_072)] #[test_case(_1, _1, _1, 7_353_256_641)] #[test_case(_2, _2, _2, 14_706_513_281; "positive ln")] @@ -716,7 +551,7 @@ mod tests { #[test_case(true, FixedType::from_str("0.000000000000008083692034").unwrap())] fn exp_does_not_overflow_or_underflow(neg: bool, expected: FixedType) { let result: FixedType = - exp(FixedType::checked_from_num(EXP_OVERFLOW_THRESHOLD).unwrap(), neg).unwrap(); + exp(FixedType::checked_from_num(EXP_NUMERICAL_THRESHOLD).unwrap(), neg).unwrap(); assert_eq!(result, expected); } diff --git a/zrml/neo-swaps/src/math/types/mod.rs b/zrml/neo-swaps/src/math/types/mod.rs new file mode 100644 index 000000000..8de08770f --- /dev/null +++ b/zrml/neo-swaps/src/math/types/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod combo_math; +mod common; +mod math; + +pub(crate) use combo_math::ComboMath; +pub(crate) use math::Math; diff --git a/zrml/neo-swaps/src/migration.rs b/zrml/neo-swaps/src/migration.rs index 2e9ba478f..b2d9857a7 100644 --- a/zrml/neo-swaps/src/migration.rs +++ b/zrml/neo-swaps/src/migration.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -14,3 +14,401 @@ // // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . + +use crate::{ + traits::LiquiditySharesManager, + types::{MaxAssets, Pool, PoolType}, + AssetOf, BalanceOf, Config, LiquidityTreeOf, MarketIdOf, MarketIdToPoolId, Pallet, PoolCount, + Pools, +}; +use alloc::{fmt::Debug, vec, vec::Vec}; +use core::marker::PhantomData; +use frame_support::{ + migration::storage_key_iter, + pallet_prelude::Twox64Concat, + storage::bounded_btree_map::BoundedBTreeMap, + traits::{Get, OnRuntimeUpgrade, StorageVersion}, + weights::Weight, + CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, +}; +use log; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{SaturatedConversion, Saturating}; +use zeitgeist_primitives::math::checked_ops_res::CheckedAddRes; +use zrml_market_commons::MarketCommonsPalletApi; + +cfg_if::cfg_if! { + if #[cfg(feature = "try-runtime")] { + use alloc::{format, collections::BTreeMap}; + use sp_runtime::DispatchError; + } +} + +const NEO_SWAPS: &[u8] = b"NeoSwaps"; +const POOLS: &[u8] = b"Pools"; + +const NEO_SWAPS_REQUIRED_STORAGE_VERSION: u16 = 2; +const NEO_SWAPS_NEXT_STORAGE_VERSION: u16 = NEO_SWAPS_REQUIRED_STORAGE_VERSION + 1; + +#[derive( + CloneNoBound, Decode, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, +)] +#[scale_info(skip_type_params(S, T))] +pub struct OldPool +where + T: Config, + LSM: Clone + Debug + LiquiditySharesManager + PartialEq, + S: Get, +{ + pub account_id: T::AccountId, + pub reserves: BoundedBTreeMap, BalanceOf, S>, + pub collateral: AssetOf, + pub liquidity_parameter: BalanceOf, + pub liquidity_shares_manager: LSM, + pub swap_fee: BalanceOf, +} + +type OldPoolOf = OldPool, MaxAssets>; + +// https://substrate.stackexchange.com/questions/10472/pallet-storage-migration-fails-try-runtime-idempotent-test +// idempotent test fails, because of the manual storage version increment +// VersionedMigration is still an experimental feature for the currently used polkadot version +// that's why the idempotent test is ignored for this migration +pub struct MigratePoolStorageItems(PhantomData, RemovableMarketIds); + +impl OnRuntimeUpgrade for MigratePoolStorageItems +where + T: Config, + RemovableMarketIds: Get>, +{ + fn on_runtime_upgrade() -> Weight { + let mut total_weight = T::DbWeight::get().reads(1); + let neo_swaps_version = StorageVersion::get::>(); + if neo_swaps_version != NEO_SWAPS_REQUIRED_STORAGE_VERSION { + log::info!( + "MigratePoolStorageItems: neo-swaps version is {:?}, but {:?} is required", + neo_swaps_version, + NEO_SWAPS_REQUIRED_STORAGE_VERSION, + ); + return total_weight; + } + log::info!("MigratePoolStorageItems: Starting..."); + // NeoSwaps: 7de9893ad4de67f3510fd09678a13412 + // Pools: 4c72016d74b63ae83d79b02efdb5528e + // failed to decode pool with market id 880: 0x7de9893ad4de67f3510fd09678a134124c72016d74b63ae83d79b02efdb5528e00251b42e33e726f70030000000000000000000000000000 + // failed to decode pool with market id 878: 0x7de9893ad4de67f3510fd09678a134124c72016d74b63ae83d79b02efdb5528e0f7a0cea0db6ee406e030000000000000000000000000000 + // failed to decode pool with market id 882: 0x7de9893ad4de67f3510fd09678a134124c72016d74b63ae83d79b02efdb5528eb49736cf4bc6723372030000000000000000000000000000 + // failed to decode pool with market id 879: 0x7de9893ad4de67f3510fd09678a134124c72016d74b63ae83d79b02efdb5528ed857f1051e4281a76f030000000000000000000000000000 + // failed to decode pool with market id 877: 0x7de9893ad4de67f3510fd09678a134124c72016d74b63ae83d79b02efdb5528ee0edd4b43beb361f6d030000000000000000000000000000 + // The decode failure happens, because the old pool used a CampaignAsset as asset, which is not supported anymore, since the asset system overhaul has been reverted. + + let mut max_pool_id: T::PoolId = Default::default(); + for (market_id, _) in + storage_key_iter::, OldPoolOf, Twox64Concat>(NEO_SWAPS, POOLS) + { + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(2)); + if T::MarketCommons::market(&market_id).is_err() { + log::error!("MigratePoolStorageItems: Market {:?} not found", market_id); + return total_weight; + }; + let pool_id = market_id; + max_pool_id = max_pool_id.max(pool_id); + } + let next_pool_count_id = if let Ok(id) = max_pool_id.checked_add_res(&1u8.into()) { + id + } else { + log::error!("MigratePoolStorageItems: Pool id overflow"); + return total_weight; + }; + let mut translated = 0u64; + Pools::::translate::, _>(|market_id, pool| { + translated.saturating_inc(); + let pool_id = market_id; + MarketIdToPoolId::::insert(pool_id, market_id); + let assets = if let Ok(market) = T::MarketCommons::market(&market_id) { + market.outcome_assets().try_into().ok()? + } else { + log::error!( + "MigratePoolStorageItems: Market {:?} not found. This should not happen, \ + because it is checked above.", + market_id + ); + pool.reserves.keys().cloned().collect::>().try_into().ok()? + }; + Some(Pool { + account_id: pool.account_id, + assets, + reserves: pool.reserves, + collateral: pool.collateral, + liquidity_parameter: pool.liquidity_parameter, + liquidity_shares_manager: pool.liquidity_shares_manager, + swap_fee: pool.swap_fee, + pool_type: PoolType::Standard(market_id), + }) + }); + PoolCount::::set(next_pool_count_id); + // Write for the PoolCount storage item + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + log::info!("MigratePoolStorageItems: Upgraded {} pools.", translated); + // Reads and writes for the Pools storage item + total_weight = + total_weight.saturating_add(T::DbWeight::get().reads_writes(translated, translated)); + // Read for the market and write for the MarketIdToPoolId storage item + total_weight = + total_weight.saturating_add(T::DbWeight::get().reads_writes(translated, translated)); + + // remove pools that contain a corrupted campaign asset from the reverted asset system overhaul + let mut corrupted_pools = vec![]; + for &market_id in RemovableMarketIds::get().iter() { + let market_id = market_id.saturated_into::>(); + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(2)); + let is_corrupted = + || Pools::::contains_key(market_id) && Pools::::get(market_id).is_none(); + if is_corrupted() { + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + Pools::::remove(market_id); + corrupted_pools.push(market_id); + } else { + log::warn!( + "RemoveMarkets: Pool with market id {:?} was expected to be corrupted, but \ + isn't.", + market_id + ); + } + } + log::info!("RemovePools: Removed pools with market ids: {:?}.", corrupted_pools); + StorageVersion::new(NEO_SWAPS_NEXT_STORAGE_VERSION).put::>(); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + log::info!("MigratePoolStorageItems: Done!"); + total_weight + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, DispatchError> { + let old_pools = + storage_key_iter::, OldPoolOf, Twox64Concat>(NEO_SWAPS, POOLS) + .collect::>(); + Ok(old_pools.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(previous_state: Vec) -> Result<(), DispatchError> { + let old_pools: BTreeMap, OldPoolOf> = + Decode::decode(&mut &previous_state[..]) + .map_err(|_| "Failed to decode state: Invalid state")?; + let new_pool_count = Pools::::iter().count(); + assert_eq!(old_pools.len(), new_pool_count); + let mut max_pool_id: T::PoolId = Default::default(); + for (market_id, new_pool) in Pools::::iter() { + let old_pool = + old_pools.get(&market_id).expect(&format!("Pool {:?} not found", market_id)[..]); + max_pool_id = max_pool_id.max(market_id); + assert_eq!(new_pool.account_id, old_pool.account_id); + let market = T::MarketCommons::market(&market_id)?; + let outcome_assets = market.outcome_assets(); + for asset in &outcome_assets { + assert!(new_pool.assets.contains(asset)); + } + assert_eq!(new_pool.assets.len(), outcome_assets.len()); + assert_eq!(new_pool.reserves, old_pool.reserves); + assert_eq!(new_pool.collateral, old_pool.collateral); + assert_eq!(new_pool.liquidity_parameter, old_pool.liquidity_parameter); + assert_eq!(new_pool.liquidity_shares_manager, old_pool.liquidity_shares_manager); + assert_eq!(new_pool.swap_fee, old_pool.swap_fee); + assert_eq!(new_pool.pool_type, PoolType::Standard(market_id)); + + assert_eq!( + MarketIdToPoolId::::get(market_id).expect("MarketIdToPoolId mapping not found"), + market_id + ); + } + let next_pool_count_id = PoolCount::::get(); + assert_eq!(next_pool_count_id, max_pool_id.checked_add_res(&1u8.into())?); + log::info!( + "MigratePoolStorageItems: Post-upgrade next pool count id is {:?}!", + next_pool_count_id + ); + for &market_id in RemovableMarketIds::get().iter() { + let market_id = market_id.saturated_into::>(); + assert!(!Pools::::contains_key(market_id)); + assert!(Pools::::try_get(market_id).is_err()); + } + log::info!("MigratePoolStorageItems: Post-upgrade pool count is {}!", new_pool_count); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + liquidity_tree::types::LiquidityTree, + mock::{ExtBuilder, MarketCommons, Runtime, ALICE, BOB}, + MarketIdOf, PoolOf, Pools, + }; + use alloc::collections::BTreeMap; + use core::fmt::Debug; + use frame_support::{migration::put_storage_value, StorageHasher, Twox64Concat}; + use parity_scale_codec::Encode; + use sp_io::storage::root as storage_root; + use sp_runtime::{Perbill, StateVersion}; + use zeitgeist_primitives::types::{ + Asset, Market, MarketCreation, MarketPeriod, MarketStatus, MarketType, ScoringRule, + }; + + struct RemovableMarketIds; + impl Get> for RemovableMarketIds { + fn get() -> Vec { + vec![] + } + } + + #[test] + fn on_runtime_upgrade_increments_the_storage_version() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + MigratePoolStorageItems::::on_runtime_upgrade(); + assert_eq!(StorageVersion::get::>(), NEO_SWAPS_NEXT_STORAGE_VERSION); + }); + } + + #[test] + fn on_runtime_upgrade_is_noop_if_versions_are_not_correct() { + ExtBuilder::default().build().execute_with(|| { + StorageVersion::new(NEO_SWAPS_NEXT_STORAGE_VERSION).put::>(); + let (_, new_pools) = construct_old_new_tuple(); + populate_test_data::, PoolOf>( + NEO_SWAPS, POOLS, new_pools, + ); + let tmp = storage_root(StateVersion::V1); + MigratePoolStorageItems::::on_runtime_upgrade(); + assert_eq!(tmp, storage_root(StateVersion::V1)); + }); + } + + #[test] + fn on_runtime_upgrade_correctly_updates_pool_storages() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + create_markets(3); + let (old_pools, new_pools) = construct_old_new_tuple(); + populate_test_data::, OldPoolOf>( + NEO_SWAPS, POOLS, old_pools, + ); + MigratePoolStorageItems::::on_runtime_upgrade(); + let actual = Pools::get(0u128).unwrap(); + assert_eq!(actual, new_pools[0]); + let next_pool_count_id = PoolCount::::get(); + assert_eq!(next_pool_count_id, 3u128); + assert_eq!(MarketIdToPoolId::::get(0u128).unwrap(), 0u128); + assert_eq!(MarketIdToPoolId::::get(1u128).unwrap(), 1u128); + assert_eq!(MarketIdToPoolId::::get(2u128).unwrap(), 2u128); + assert!(MarketIdToPoolId::::get(3u128).is_none()); + assert!(MarketIdToPoolId::::iter_keys().count() == 3); + }); + } + + fn set_up_version() { + StorageVersion::new(NEO_SWAPS_REQUIRED_STORAGE_VERSION).put::>(); + } + + fn create_markets(count: u8) { + for _ in 0..count { + let base_asset = Asset::Ztg; + let market = Market { + market_id: 0u8.into(), + base_asset, + creation: MarketCreation::Permissionless, + creator_fee: Perbill::zero(), + creator: ALICE, + oracle: BOB, + metadata: vec![0, 50], + market_type: MarketType::Categorical(3), + period: MarketPeriod::Block(0u32.into()..1u32.into()), + deadlines: Default::default(), + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Active, + report: None, + resolved_outcome: None, + dispute_mechanism: None, + bonds: Default::default(), + early_close: None, + }; + MarketCommons::push_market(market).unwrap(); + } + } + + fn construct_old_new_tuple() -> (Vec>, Vec>) { + let account_id = 1; + let mut reserves = BTreeMap::new(); + let asset_0 = Asset::CategoricalOutcome(0, 0); + let asset_1 = Asset::CategoricalOutcome(0, 1); + let asset_2 = Asset::CategoricalOutcome(0, 2); + reserves.insert(asset_0, 4); + reserves.insert(asset_1, 5); + reserves.insert(asset_2, 6); + let reserves: BoundedBTreeMap, BalanceOf, MaxAssets> = + reserves.clone().try_into().unwrap(); + let collateral = Asset::Ztg; + let liquidity_parameter = 5; + let swap_fee = 6; + let total_shares = 7; + let fees = 8; + + let mut liquidity_shares_manager = LiquidityTree::new(account_id, total_shares).unwrap(); + liquidity_shares_manager.nodes.get_mut(0).unwrap().fees = fees; + + let old_pool = OldPoolOf { + account_id, + reserves: reserves.clone(), + collateral, + liquidity_parameter, + liquidity_shares_manager: liquidity_shares_manager.clone(), + swap_fee, + }; + let new_pool = Pool { + account_id, + assets: vec![asset_0, asset_1, asset_2].try_into().unwrap(), + reserves, + collateral, + liquidity_parameter, + liquidity_shares_manager, + swap_fee, + pool_type: PoolType::Standard(0), + }; + ( + vec![old_pool.clone(), old_pool.clone(), old_pool.clone()], + vec![new_pool.clone(), new_pool.clone(), new_pool.clone()], + ) + } + + #[allow(unused)] + fn populate_test_data(pallet: &[u8], prefix: &[u8], data: Vec) + where + H: StorageHasher, + K: TryFrom + Encode, + V: Encode + Clone, + >::Error: Debug, + { + for (key, value) in data.iter().enumerate() { + let storage_hash = utility::key_to_hash::(K::try_from(key).unwrap()); + put_storage_value::(pallet, prefix, &storage_hash, (*value).clone()); + } + } +} + +mod utility { + use alloc::vec::Vec; + use frame_support::StorageHasher; + use parity_scale_codec::Encode; + + #[allow(unused)] + pub fn key_to_hash(key: K) -> Vec + where + H: StorageHasher, + K: Encode, + { + key.using_encoded(H::hash).as_ref().to_vec() + } +} diff --git a/zrml/neo-swaps/src/mock.rs b/zrml/neo-swaps/src/mock.rs index 05d17fdcf..cac90375f 100644 --- a/zrml/neo-swaps/src/mock.rs +++ b/zrml/neo-swaps/src/mock.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -29,6 +29,7 @@ use core::marker::PhantomData; use frame_support::{ construct_runtime, ord_parameter_types, parameter_types, traits::{Contains, Everything, NeverEnsureOrigin}, + Blake2_256, }; use frame_system::{mocking::MockBlock, EnsureRoot, EnsureSignedBy}; use orml_traits::MultiCurrency; @@ -36,8 +37,6 @@ use sp_runtime::{ traits::{BlakeTwo256, ConstU32, Get, IdentityLookup, Zero}, BuildStorage, DispatchResult, Perbill, Percent, SaturatedConversion, }; -#[cfg(feature = "parachain")] -use zeitgeist_primitives::types::Asset; use zeitgeist_primitives::{ constants::{ base_multiples::*, @@ -45,32 +44,39 @@ use zeitgeist_primitives::{ AddOutcomePeriod, AggregationPeriod, AppealBond, AppealPeriod, AuthorizedPalletId, BlockHashCount, BlocksPerYear, CloseEarlyBlockPeriod, CloseEarlyDisputeBond, CloseEarlyProtectionBlockPeriod, CloseEarlyProtectionTimeFramePeriod, - CloseEarlyRequestBond, CloseEarlyTimeFramePeriod, CorrectionPeriod, CourtPalletId, - ExistentialDeposit, ExistentialDeposits, GdVotingPeriod, GetNativeCurrencyId, - GlobalDisputeLockId, GlobalDisputesPalletId, InflationPeriod, LockId, MaxAppeals, - MaxApprovals, MaxCourtParticipants, MaxCreatorFee, MaxDelegations, MaxDisputeDuration, - MaxDisputes, MaxEditReasonLen, MaxGlobalDisputeVotes, MaxGracePeriod, - MaxLiquidityTreeDepth, MaxLocks, MaxMarketLifetime, MaxOracleDuration, MaxOwners, - MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, MaxYearlyInflation, MinCategories, - MinDisputeDuration, MinJurorStake, MinOracleDuration, MinOutcomeVoteAmount, - MinimumPeriod, NeoMaxSwapFee, NeoSwapsPalletId, OutsiderBond, PmPalletId, - RemoveKeysLimit, RequestInterval, TreasuryPalletId, VotePeriod, VotingOutcomeFee, BASE, - CENT, + CloseEarlyRequestBond, CloseEarlyTimeFramePeriod, CombinatorialTokensPalletId, + CorrectionPeriod, CourtPalletId, ExistentialDeposit, ExistentialDeposits, + GdVotingPeriod, GetNativeCurrencyId, GlobalDisputeLockId, GlobalDisputesPalletId, + InflationPeriod, LockId, MaxAppeals, MaxApprovals, MaxCourtParticipants, MaxCreatorFee, + MaxDelegations, MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, + MaxGlobalDisputeVotes, MaxGracePeriod, MaxLiquidityTreeDepth, MaxLocks, + MaxMarketLifetime, MaxOracleDuration, MaxOwners, MaxRejectReasonLen, MaxReserves, + MaxSelectedDraws, MaxYearlyInflation, MinCategories, MinDisputeDuration, MinJurorStake, + MinOracleDuration, MinOutcomeVoteAmount, MinimumPeriod, NeoMaxSwapFee, + NeoSwapsPalletId, OutsiderBond, PmPalletId, RemoveKeysLimit, RequestInterval, + TreasuryPalletId, VotePeriod, VotingOutcomeFee, BASE, CENT, }, }, math::fixed::FixedMul, traits::{DeployPoolApi, DistributeFees}, types::{ - AccountIdTest, Amount, Balance, BasicCurrencyAdapter, CurrencyId, Hash, MarketId, Moment, + AccountIdTest, Amount, Balance, BasicCurrencyAdapter, CombinatorialId, CurrencyId, Hash, + MarketId, Moment, }, }; +use zrml_combinatorial_tokens::types::{CryptographicIdManager, Fuel}; use zrml_neo_swaps::BalanceOf; + #[cfg(feature = "parachain")] use { orml_traits::asset_registry::AssetProcessor, parity_scale_codec::Encode, - sp_runtime::DispatchError, zeitgeist_primitives::types::CustomMetadata, + sp_runtime::DispatchError, zeitgeist_primitives::types::Asset, + zeitgeist_primitives::types::CustomMetadata, }; +#[cfg(feature = "runtime-benchmarks")] +use zeitgeist_primitives::types::NoopCombinatorialTokensBenchmarkHelper; + pub const ALICE: AccountIdTest = 0; #[allow(unused)] pub const BOB: AccountIdTest = 1; @@ -95,6 +101,7 @@ ord_parameter_types! { } parameter_types! { pub storage NeoMinSwapFee: Balance = 0; + pub storage MaxSplits: u16 = 128; } parameter_types! { pub const AdvisoryBond: Balance = 0; @@ -168,6 +175,7 @@ construct_runtime!( AssetRegistry: orml_asset_registry, Authorized: zrml_authorized, Balances: pallet_balances, + CombinatorialTokens: zrml_combinatorial_tokens, Court: zrml_court, MarketCommons: zrml_market_commons, PredictionMarkets: zrml_prediction_markets, @@ -181,12 +189,17 @@ construct_runtime!( ); impl crate::Config for Runtime { - type MultiCurrency = AssetManager; + type CombinatorialId = CombinatorialId; + type CombinatorialTokens = CombinatorialTokens; + type CombinatorialTokensUnsafe = CombinatorialTokens; type CompleteSetOperations = PredictionMarkets; type ExternalFees = ExternalFees; type MarketCommons = MarketCommons; + type MultiCurrency = AssetManager; + type PoolId = MarketId; type RuntimeEvent = RuntimeEvent; type MaxLiquidityTreeDepth = MaxLiquidityTreeDepth; + type MaxSplits = MaxSplits; type MaxSwapFee = NeoMaxSwapFee; type PalletId = NeoSwapsPalletId; type WeightInfo = zrml_neo_swaps::weights::WeightInfo; @@ -251,6 +264,19 @@ impl zrml_authorized::Config for Runtime { type WeightInfo = zrml_authorized::weights::WeightInfo; } +impl zrml_combinatorial_tokens::Config for Runtime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = NoopCombinatorialTokensBenchmarkHelper; + type CombinatorialIdManager = CryptographicIdManager; + type Fuel = Fuel; + type MarketCommons = MarketCommons; + type MultiCurrency = AssetManager; + type Payout = PredictionMarkets; + type RuntimeEvent = RuntimeEvent; + type PalletId = CombinatorialTokensPalletId; + type WeightInfo = zrml_combinatorial_tokens::weights::WeightInfo; +} + impl zrml_court::Config for Runtime { type AppealBond = AppealBond; type BlocksPerYear = BlocksPerYear; diff --git a/zrml/neo-swaps/src/pool_storage.rs b/zrml/neo-swaps/src/pool_storage.rs new file mode 100644 index 000000000..843ffd565 --- /dev/null +++ b/zrml/neo-swaps/src/pool_storage.rs @@ -0,0 +1,73 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{traits::PoolStorage, Config, Error, Pallet, PoolCount, PoolOf, Pools}; +use frame_support::require_transactional; +use sp_runtime::DispatchError; +use zeitgeist_primitives::math::checked_ops_res::CheckedIncRes; + +impl PoolStorage for Pallet +where + T: Config, +{ + type PoolId = T::PoolId; + type Pool = PoolOf; + + fn next_pool_id() -> T::PoolId { + PoolCount::::get() + } + + #[require_transactional] + fn add(pool: Self::Pool) -> Result { + let pool_id = Self::next_pool_id(); + Pools::::insert(pool_id, pool); + + let pool_count = pool_id.checked_inc_res()?; + PoolCount::::set(pool_count); + + Ok(pool_id) + } + + fn get(pool_id: Self::PoolId) -> Result { + Pools::::get(pool_id).ok_or(Error::::PoolNotFound.into()) + } + + fn try_mutate_pool(pool_id: &Self::PoolId, mutator: F) -> Result + where + F: FnMut(&mut Self::Pool) -> Result, + { + Pools::::try_mutate(pool_id, |maybe_pool| { + maybe_pool.as_mut().ok_or(Error::::PoolNotFound.into()).and_then(mutator) + }) + } + + fn try_mutate_exists(pool_id: &Self::PoolId, mutator: F) -> Result + where + F: FnMut(&mut Self::Pool) -> Result<(R, bool), DispatchError>, + { + Pools::::try_mutate_exists(pool_id, |maybe_pool| { + let (result, delete) = + maybe_pool.as_mut().ok_or(Error::::PoolNotFound.into()).and_then(mutator)?; + + if delete { + *maybe_pool = None; + } + + Ok(result) + }) + } +} diff --git a/zrml/neo-swaps/src/tests/buy.rs b/zrml/neo-swaps/src/tests/buy.rs index 4267fca21..ac3bde7c8 100644 --- a/zrml/neo-swaps/src/tests/buy.rs +++ b/zrml/neo-swaps/src/tests/buy.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -21,7 +21,7 @@ use sp_runtime::{DispatchError, TokenError}; use test_case::test_case; // Example taken from -// https://docs.gnosis.io/conditionaltokens/docs/introduction3/#an-example-with-lmsr +// https://github.com/gnosis/conditional-tokens-docs/blob/e73aa18ab82446049bca61df31fc88efd3cdc5cc/docs/intro3.md?plain=1#L78-L88 #[test] fn buy_works() { ExtBuilder::default().build().execute_with(|| { @@ -86,7 +86,7 @@ fn buy_works() { System::assert_last_event( Event::BuyExecuted { who: BOB, - market_id, + pool_id: market_id, asset_out, amount_in, amount_out: expected_amount_out, @@ -346,3 +346,25 @@ fn buy_fails_on_amount_out_below_min() { ); }); } + +#[test] +fn buy_fails_on_invalid_pool_type() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Scalar(0..=1)], + _10, + vec![_1_2, _1_2], + CENT, + ); + + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + assert_noop!( + NeoSwaps::buy(RuntimeOrigin::signed(BOB), pool_id, 2, assets[0], _1, 0), + Error::::InvalidPoolType, + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/buy_and_sell.rs b/zrml/neo-swaps/src/tests/buy_and_sell.rs index cce3d02a7..710315c1c 100644 --- a/zrml/neo-swaps/src/tests/buy_and_sell.rs +++ b/zrml/neo-swaps/src/tests/buy_and_sell.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -61,7 +61,7 @@ fn buy_and_sell() { )); assert_pool_state!( market_id, - vec![1_807_876_540_789, 113_931_597_104, 1_976_969_097_720], + vec![1_807_876_540_789, 113_931_597_105, 1_976_969_097_720], [815_736_444, 8_538_986_828, 645_276_728], 721_347_520_444, create_b_tree_map!({ ALICE => _100 }), @@ -78,7 +78,7 @@ fn buy_and_sell() { )); assert_pool_state!( market_id, - vec![76_875_275, 6_650_531_597_104, 8_513_569_097_720], + vec![76_875_276, 6_650_531_597_105, 8_513_569_097_720], [9_998_934_339, 990_789, 74_872], 721_347_520_444, create_b_tree_map!({ ALICE => _100 }), @@ -108,7 +108,7 @@ fn buy_and_sell() { )); assert_pool_state!( market_id, - vec![77_948_356, 6_640_532_670_185, 8_503_570_170_801], + vec![77_948_357, 6_640_532_670_186, 8_503_570_170_801], [9_998_919_465, 1_004_618, 75_917], 721_347_520_444, create_b_tree_map!({ ALICE => _100 }), @@ -165,7 +165,7 @@ fn buy_and_sell() { )); assert_pool_state!( market_id, - vec![980_077_948_356, 7_620_532_670_185, 214_308_675_476], + vec![980_077_948_357, 7_620_532_670_186, 214_308_675_477], [2_570_006_838, 258_215, 7_429_734_946], 721_347_520_444, create_b_tree_map!({ ALICE => _100 }), diff --git a/zrml/neo-swaps/src/tests/combo_buy.rs b/zrml/neo-swaps/src/tests/combo_buy.rs new file mode 100644 index 000000000..ad8de0630 --- /dev/null +++ b/zrml/neo-swaps/src/tests/combo_buy.rs @@ -0,0 +1,519 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +// Example taken from +// https://github.com/gnosis/conditional-tokens-docs/blob/e73aa18ab82446049bca61df31fc88efd3cdc5cc/docs/intro3.md?plain=1#L78-L88 +#[test] +fn combo_buy_works() { + ExtBuilder::default().build().execute_with(|| { + let liquidity = _10; + let spot_prices = vec![_1_2, _1_2]; + let swap_fee = CENT; + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2)], + liquidity, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let total_fee_percentage = swap_fee + EXTERNAL_FEES; + let amount_in_minus_fees = _10; + let amount_in = amount_in_minus_fees.bdiv(_1 - total_fee_percentage).unwrap(); // This is exactly _10 after deducting fees. + let expected_fees = amount_in - amount_in_minus_fees; + let expected_swap_fee_amount = expected_fees / 2; + let expected_external_fee_amount = expected_fees / 2; + let pool_outcomes_before: Vec<_> = + pool.assets().iter().map(|a| pool.reserve_of(a).unwrap()).collect(); + let liquidity_parameter_before = pool.liquidity_parameter; + let buy = vec![pool.assets()[0]]; + let sell = pool.assets_complement(&buy); + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + // Deposit some stuff in the pool account to check that the pools `reserves` fields tracks + // the reserve correctly. + assert_ok!(AssetManager::deposit(sell[0], &pool.account_id, _100)); + assert_ok!(NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + 2, + buy.clone(), + sell.clone(), + amount_in, + 0, + )); + let pool = Pools::::get(pool_id).unwrap(); + let expected_swap_amount_out = 58496250072; + let expected_amount_in_minus_fees = _10 + 1; // Note: This is 1 Pennock off of the correct result. + let expected_reserves = vec![ + pool_outcomes_before[0] - expected_swap_amount_out, + pool_outcomes_before[0] + expected_amount_in_minus_fees, + ]; + assert_pool_state!( + pool_id, + expected_reserves, + vec![_3_4, _1_4], + liquidity_parameter_before, + create_b_tree_map!({ ALICE => liquidity }), + expected_swap_fee_amount, + ); + let expected_amount_out = expected_swap_amount_out + expected_amount_in_minus_fees; + assert_balance!(BOB, BASE_ASSET, 0); + assert_balance!(BOB, buy[0], expected_amount_out); + assert_balance!( + pool.account_id, + BASE_ASSET, + expected_swap_fee_amount + AssetManager::minimum_balance(pool.collateral) + ); + assert_balance!(FEE_ACCOUNT, BASE_ASSET, expected_external_fee_amount); + System::assert_last_event( + Event::ComboBuyExecuted { + who: BOB, + pool_id, + buy, + sell, + amount_in, + amount_out: expected_amount_out, + swap_fee_amount: expected_swap_fee_amount, + external_fee_amount: expected_external_fee_amount, + } + .into(), + ); + }); +} + +#[test_case( + vec![MarketType::Categorical(5)], + 333 * _1, + vec![10 * CENT, 30 * CENT, 25 * CENT, 13 * CENT, 22 * CENT], + vec![0, 2], + vec![3], + vec![1, 4], + 102_040_816_327, + 236_865_613_849, + 100_000_000_001, + vec![3193134386152, 1841186221785, 1867994157274, 2950568636818, 2289732472863], + vec![1_099_260_911, 2_799_569_315, 2_748_152_277, 1_300_000_000, 2_053_017_497], + 1_020_408_163 +)] +#[test_case( + vec![MarketType::Categorical(5)], + _100, + vec![80 * CENT, 5 * CENT, 5 * CENT, 5 * CENT, 5 * CENT], + vec![4], + vec![1, 2, 3], + vec![0], + 336_734_693_877, + 1_131_842_030_026, + 329_999_999_999, + vec![404_487_147_360, _100, _100, _100, 198_157_969_973], + vec![2_976_802_957, 5 * CENT, 5 * CENT, 5 * CENT, 5_523_197_043], + 3_367_346_939 +)] +#[test_case( + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + 1000 * _1, + vec![1_250_000_000; 8], + vec![0, 2, 5, 6, 7], + vec![], + vec![1, 3, 4], + 5_208_333_333_333, + 6_576_234_413_778, + // keep_indices vector is empty anyways, so `expected_amount_out_keep` amount has no effect + 0, + vec![ + 8_423_765_586_223, + 1500 * _1 + 1, + 8_423_765_586_223, + 1500 * _1 + 1, + 1500 * _1 + 1, + 8_423_765_586_223, + 8_423_765_586_223, + 8_423_765_586_223, + ], + vec![ + 1_734_834_957, + 441_941_738, + 1_734_834_957, + 441_941_738, + 441_941_738, + 1_734_834_957, + 1_734_834_957, + 1_734_834_957, + ], + 52_083_333_333 +)] +fn combo_buy_works_multi_market( + market_types: Vec, + liquidity: u128, + spot_prices: Vec, + buy_indices: Vec, + keep_indices: Vec, + sell_indices: Vec, + amount_in: u128, + expected_amount_out_buy: u128, + expected_amount_out_keep: u128, + expected_reserves: Vec, + expected_spot_prices: Vec, + expected_fees: u128, // pool fees, not market fees +) { + ExtBuilder::default().build().execute_with(|| { + let asset_count = spot_prices.len() as u16; + let swap_fee = CENT; + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + market_types, + liquidity, + spot_prices.clone(), + swap_fee, + ); + let sentinel = 123_456_789; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in + sentinel)); + + let pool = Pools::::get(pool_id).unwrap(); + let expected_liquidity = pool.liquidity_parameter; + + let buy: Vec<_> = buy_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let keep: Vec<_> = keep_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let sell: Vec<_> = sell_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + + assert_ok!(NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + asset_count, + buy.clone(), + sell.clone(), + amount_in, + 0, + )); + + assert_balance!(BOB, BASE_ASSET, sentinel); + for &asset in buy.iter() { + assert_balance!(BOB, asset, expected_amount_out_buy); + } + for &asset in keep.iter() { + assert_balance!(BOB, asset, expected_amount_out_keep); + } + for &asset in sell.iter() { + assert_balance!(BOB, asset, 0); + } + + assert_pool_state!( + pool_id, + expected_reserves, + expected_spot_prices, + expected_liquidity, + create_b_tree_map!({ ALICE => liquidity }), + expected_fees, + ); + }); +} + +#[test] +fn combo_buy_fails_on_incorrect_asset_count() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _10, + vec![_1_4, _1_4, _1_4, _1_4], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + assert_noop!( + NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + 3, + vec![assets[0]], + vec![assets[1]], + _1, + 0 + ), + Error::::IncorrectAssetCount + ); + }); +} + +#[test] +fn combo_buy_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _10, + vec![_1_4, _1_4, _1_4, _1_4], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + assert_noop!( + NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + 1, + 4, + vec![assets[0]], + vec![assets[1]], + _1, + 0 + ), + Error::::PoolNotFound, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn combo_buy_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _10, + vec![_1_4, _1_4, _1_4, _1_4], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + MarketCommons::mutate_market(&market_ids[1], |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + 4, + vec![assets[0]], + vec![assets[1]], + _1, + 0 + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn combo_buy_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _10, + vec![_1_4, _1_4, _1_4, _1_4], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + let amount_in = _10; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in - 1)); + + #[cfg(feature = "parachain")] + let expected_error = orml_tokens::Error::::BalanceTooLow; + #[cfg(not(feature = "parachain"))] + let expected_error = orml_currencies::Error::::BalanceTooLow; + + assert_noop!( + NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + 4, + vec![assets[0]], + vec![assets[1]], + amount_in, + 0, + ), + expected_error + ); + }); +} + +#[test] +fn combo_buy_fails_on_amount_out_below_min() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Scalar(0..=1)], + 100_000_000 * _100, // Massive liquidity to keep slippage low. + vec![_1_4, _1_4, _1_4, _1_4], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + let asset_count = 4; + let buy = assets[0..1].to_vec(); + let sell = assets[1..4].to_vec(); + // amount_in is _1 / 0.97 (i.e. _1 after deducting 3% fees - 1% trading fees, 1% external + // fees _for each market_) + let amount_in = 10_309_278_350; + + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + // Buying for 1 at a price of .25 will return less than 4 outcomes due to slippage. + assert_noop!( + NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + asset_count, + buy.clone(), + sell.clone(), + amount_in, + _4, + ), + Error::::AmountOutBelowMin, + ); + + // Post OakSecurity audit: Show that the slippage limit is tight. + assert_ok!(NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + pool_id, + asset_count, + buy, + sell, + amount_in, + _4 - 33, + )); + }); +} + +#[test_case(vec![0], vec![0]; "overlap")] +#[test_case(vec![], vec![0, 1]; "empty_buy")] +#[test_case(vec![2, 3], vec![]; "empty_sell")] +#[test_case(vec![0, 2, 3], vec![1, 3, 4]; "overlap2")] +#[test_case(vec![0, 1, 3, 1], vec![2]; "duplicate_buy")] +#[test_case(vec![0, 1, 3], vec![4, 2, 4]; "duplicate_sell")] +fn combo_buy_fails_on_invalid_partition(buy_indices: Vec, sell_indices: Vec) { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(5)], + _10, + vec![_1_5, _1_5, _1_5, _1_5, _1_5], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + let amount_in = _1; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + + let buy: Vec<_> = buy_indices.iter().map(|&i| assets[i as usize]).collect(); + let sell: Vec<_> = sell_indices.iter().map(|&i| assets[i as usize]).collect(); + + assert_noop!( + NeoSwaps::combo_buy(RuntimeOrigin::signed(BOB), pool_id, 5, buy, sell, amount_in, 0), + Error::::InvalidPartition, + ); + }); +} + +#[test] +fn combo_buy_fails_on_spot_price_slipping_too_low() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(5)], + _10, + vec![_1_5, _1_5, _1_5, _1_5, _1_5], + CENT, + ); + let amount_in = _100; + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + + let buy = assets[0..4].to_vec(); + let sell = vec![assets[4]]; + + assert_noop!( + NeoSwaps::combo_buy(RuntimeOrigin::signed(BOB), pool_id, 5, buy, sell, amount_in, 0), + Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow), + ); + }); +} + +#[test] +fn combo_buy_fails_on_large_buy() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(5)], + _10, + vec![_1_5, _1_5, _1_5, _1_5, _1_5], + CENT, + ); + let amount_in = 100 * _100; + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + + let buy = vec![assets[4]]; + let sell = assets[0..2].to_vec(); + + assert_noop!( + NeoSwaps::combo_buy(RuntimeOrigin::signed(BOB), pool_id, 5, buy, sell, amount_in, 0), + Error::::MathError, + ); + }); +} + +#[test] +fn combo_buy_fails_on_invalid_pool_type() { + ExtBuilder::default().build().execute_with(|| { + let pool_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Categorical(5), + _10, + vec![_1_5, _1_5, _1_5, _1_5, _1_5], + CENT, + ); + + let amount_in = _1; + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); + + let buy = vec![assets[4]]; + let sell = assets[0..2].to_vec(); + + assert_noop!( + NeoSwaps::combo_buy(RuntimeOrigin::signed(BOB), pool_id, 5, buy, sell, amount_in, 0), + Error::::InvalidPoolType, + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/combo_sell.rs b/zrml/neo-swaps/src/tests/combo_sell.rs new file mode 100644 index 000000000..67f261af4 --- /dev/null +++ b/zrml/neo-swaps/src/tests/combo_sell.rs @@ -0,0 +1,644 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; + +#[test] +fn combo_sell_works() { + ExtBuilder::default().build().execute_with(|| { + let liquidity = _10; + let spot_prices = vec![_1_4, _3_4]; + let swap_fee = CENT; + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Scalar(0..=1)], + liquidity, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let amount_buy = _10; + let amount_keep = 0; + let liquidity_parameter_before = pool.liquidity_parameter; + + let buy_asset = pool.assets()[1]; + let sell_asset = pool.assets()[0]; + let buy = vec![buy_asset]; + let keep = vec![]; + let sell = vec![sell_asset]; + + for &asset in buy.iter() { + assert_ok!(AssetManager::deposit(asset, &BOB, amount_buy)); + } + + assert_ok!(NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 2, + buy.clone(), + keep.clone(), + sell.clone(), + amount_buy, + amount_keep, + 0, + )); + let total_fee_percentage = swap_fee + EXTERNAL_FEES; + let expected_amount_out = 59632253897; + let expected_fees = total_fee_percentage.bmul(expected_amount_out).unwrap(); + let expected_swap_fee_amount = expected_fees / 2; + let expected_external_fee_amount = expected_fees - expected_swap_fee_amount; + let expected_amount_out_minus_fees = expected_amount_out - expected_fees; + assert_balance!(BOB, BASE_ASSET, expected_amount_out_minus_fees); + assert_balance!(BOB, buy_asset, 0); + assert_pool_state!( + pool_id, + vec![40367746103, 61119621067], + [5_714_285_714, 4_285_714_286], + liquidity_parameter_before, + create_b_tree_map!({ ALICE => liquidity }), + expected_swap_fee_amount, + ); + assert_balance!( + pool.account_id, + BASE_ASSET, + expected_swap_fee_amount + AssetManager::minimum_balance(pool.collateral) + ); + assert_balance!(FEE_ACCOUNT, BASE_ASSET, expected_external_fee_amount); + System::assert_last_event( + Event::ComboSellExecuted { + who: BOB, + pool_id, + buy, + keep, + sell, + amount_buy, + amount_keep, + amount_out: expected_amount_out_minus_fees, + swap_fee_amount: expected_swap_fee_amount, + external_fee_amount: expected_external_fee_amount, + } + .into(), + ); + }); +} + +#[test_case( + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + 1000 * _1, + vec![1_250_000_000; 8], + vec![0, 2, 5], + vec![6, 7], + vec![1, 3, 4], + _500, + _300, + 2_049_142_184_080, + vec![ + 12_865_476_891_584, + 7_865_476_891_584, + 12_865_476_891_584, + 7_865_476_891_584, + 7_865_476_891_584, + 12_865_476_891_584, + 10_865_476_891_584, + 10_865_476_891_584, + ], + vec![ + 688_861_105, + 1_948_393_435, + 688_861_105, + 1_948_393_435, + 1_948_393_435, + 688_861_105, + 1_044_118_189, + 1_044_118_189, + ], + 21_345_231_084 +)] +#[test_case( + vec![MarketType::Categorical(3)], + _321, + vec![20 * CENT, 30 * CENT, 50 * CENT], + vec![0, 2], + vec![], + vec![1], + _500, + 0, + 2_012_922_832_062, + vec![ + 6_155_997_110_140, + 347_302_977_256, + 4_328_468_861_556, + ], + vec![ + 456_610_616, + 8_401_862_845, + 1_141_526_539, + ], + 20_540_028_899 +)] +fn combo_sell_works_multi_market( + market_types: Vec, + liquidity: u128, + spot_prices: Vec, + buy_indices: Vec, + keep_indices: Vec, + sell_indices: Vec, + amount_in_buy: u128, + amount_in_keep: u128, + expected_amount_out: u128, + expected_reserves: Vec, + expected_spot_prices: Vec, + expected_fees: u128, +) { + ExtBuilder::default().build().execute_with(|| { + let asset_count = spot_prices.len() as u16; + let swap_fee = CENT; + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + market_types, + liquidity, + spot_prices.clone(), + swap_fee, + ); + + let pool = as PoolStorage>::get(pool_id).unwrap(); + let expected_liquidity = pool.liquidity_parameter; + + let buy: Vec<_> = buy_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let keep: Vec<_> = keep_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let sell: Vec<_> = sell_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + + for &asset in buy.iter() { + assert_ok!(AssetManager::deposit(asset, &BOB, amount_in_buy)); + } + for &asset in keep.iter() { + assert_ok!(AssetManager::deposit(asset, &BOB, amount_in_keep)); + } + + assert_ok!(NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + asset_count, + buy.clone(), + keep.clone(), + sell.clone(), + amount_in_buy, + amount_in_keep, + 0, + )); + + assert_balance!(BOB, BASE_ASSET, expected_amount_out); + for asset in pool.assets() { + assert_balance!(BOB, asset, 0); + } + assert_pool_state!( + pool_id, + expected_reserves, + expected_spot_prices, + expected_liquidity, + create_b_tree_map!({ ALICE => liquidity }), + expected_fees, + ); + }); +} + +#[test] +fn combo_sell_fails_on_incorrect_asset_count() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 7, + vec![pool.assets()[0]], + vec![], + vec![pool.assets()[1]], + _1, + 0, + 0 + ), + Error::::IncorrectAssetCount + ); + }); +} + +#[test] +fn combo_sell_fails_on_pool_not_found() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + 1, + 2, + vec![pool.assets()[0]], + vec![], + vec![pool.assets()[1]], + _1, + 0, + 0 + ), + Error::::PoolNotFound, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn combo_sell_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + MarketCommons::mutate_market(&market_ids[1], |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 8, + vec![pool.assets()[0]], + vec![], + vec![pool.assets()[1]], + _1, + 0, + 0 + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn combo_sell_fails_on_insufficient_funds_with_keep() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let buy_amount = _10; + let keep_amount = _9; + + assert_ok!(AssetManager::deposit(pool.assets()[0], &BOB, buy_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[1], &BOB, buy_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[2], &BOB, buy_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[3], &BOB, buy_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[4], &BOB, buy_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[5], &BOB, buy_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[6], &BOB, keep_amount)); + assert_ok!(AssetManager::deposit(pool.assets()[7], &BOB, keep_amount - 1)); + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 8, + vec![pool.assets()[0], pool.assets()[2], pool.assets()[4]], + vec![pool.assets()[6], pool.assets()[7]], + vec![pool.assets()[1], pool.assets()[3], pool.assets()[5]], + buy_amount, + keep_amount, + 0, + ), + orml_tokens::Error::::BalanceTooLow, + ); + }); +} + +#[test] +fn combo_sell_fails_on_insufficient_funds_sans_keep() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let amount_in = _10; + + assert_ok!(AssetManager::deposit(pool.assets()[0], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[1], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[2], &BOB, amount_in - 1)); + assert_ok!(AssetManager::deposit(pool.assets()[3], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[4], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[5], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[6], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[7], &BOB, amount_in)); + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 8, + vec![pool.assets()[0], pool.assets()[2], pool.assets()[4], pool.assets()[6]], + vec![], + vec![pool.assets()[1], pool.assets()[3], pool.assets()[5], pool.assets()[7]], + amount_in, + 0, + 0, + ), + orml_tokens::Error::::BalanceTooLow, + ); + }); +} + +#[test] +fn combo_sell_fails_on_amount_out_below_min() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + + let pool = as PoolStorage>::get(pool_id).unwrap(); + let amount_in = _10; + assert_ok!(AssetManager::deposit(pool.assets()[0], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[1], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[2], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[3], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[4], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[5], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[6], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[7], &BOB, amount_in)); + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 8, + vec![pool.assets()[0], pool.assets()[2], pool.assets()[4], pool.assets()[6]], + vec![], + vec![pool.assets()[1], pool.assets()[3], pool.assets()[5], pool.assets()[7]], + amount_in, + 0, + _10 + ), + Error::::AmountOutBelowMin, + ); + }); +} + +#[test_case(vec![], vec![], vec![2]; "empty_buy")] +#[test_case(vec![0], vec![], vec![]; "empty_sell")] +#[test_case(vec![0, 1], vec![2, 1], vec![3, 4]; "buy_keep_overlap")] +#[test_case(vec![0, 1], vec![2, 4], vec![3, 1]; "buy_sell_overlap")] +#[test_case(vec![0, 1], vec![2, 4], vec![4, 3]; "keep_sell_overlap")] +#[test_case(vec![0, 6, 1, 6], vec![2, 4], vec![5, 3]; "duplicate_buy")] +#[test_case(vec![0, 1], vec![2, 2, 4], vec![5, 3]; "duplicate_keep")] +#[test_case(vec![0, 1], vec![2, 4], vec![5, 3, 6, 6, 6]; "duplicate_sell")] +fn combo_sell_fails_on_invalid_partition( + buy_indices: Vec, + keep_indices: Vec, + sell_indices: Vec, +) { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2), MarketType::Categorical(2), MarketType::Scalar(0..=1)], + _100, + vec![1_250_000_000; 8], + CENT, + ); + + let pool = as PoolStorage>::get(pool_id).unwrap(); + let amount_in = _10; + assert_ok!(AssetManager::deposit(pool.assets()[0], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[1], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[2], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[3], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[4], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[5], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[6], &BOB, amount_in)); + assert_ok!(AssetManager::deposit(pool.assets()[7], &BOB, amount_in)); + + let buy: Vec<_> = buy_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let keep: Vec<_> = keep_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let sell: Vec<_> = sell_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + + // Buying 1 at price of .5 will return less than 2 outcomes due to slippage. + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 8, + buy, + keep, + sell, + _2, + 0, // Keep this zero to avoid a different error due to invalid `amount_keep` param. + 0 + ), + Error::::InvalidPartition, + ); + }); +} + +#[test] +fn combo_sell_fails_on_spot_price_slipping_too_low() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(5)], + _100, + vec![20 * CENT; 5], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let amount_buy = 1_000 * _1; + + let buy = pool.assets()[0..4].to_vec(); + let sell = pool.assets()[4..5].to_vec(); + + for &asset in buy.iter() { + assert_ok!(AssetManager::deposit(asset, &BOB, amount_buy)); + } + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 5, + buy, + vec![], + sell, + amount_buy, + 0, + 0 + ), + Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow), + ); + }); +} + +#[test] +fn combo_sell_fails_on_large_amount() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(5)], + _100, + vec![20 * CENT; 5], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let amount_buy = 100 * _100; + + let sell = pool.assets()[0..4].to_vec(); + let buy = pool.assets()[4..5].to_vec(); + + for &asset in buy.iter() { + assert_ok!(AssetManager::deposit(asset, &BOB, amount_buy)); + } + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 5, + buy, + vec![], + sell, + amount_buy, + 0, + 0 + ), + Error::::MathError, + ); + }); +} + +#[test_case(vec![], 1)] +#[test_case(vec![2], _2)] +fn combo_sell_fails_on_invalid_amount_keep(keep_indices: Vec, amount_in_keep: u128) { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(5)], + _100, + vec![20 * CENT; 5], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + + let buy = vec![pool.assets()[1]]; + let keep: Vec<_> = keep_indices.iter().map(|&i| pool.assets()[i as usize]).collect(); + let sell = vec![pool.assets()[0]]; + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 5, + buy, + keep, + sell, + _2, + amount_in_keep, + 0, + ), + Error::::InvalidAmountKeep + ); + }); +} + +#[test] +fn combo_sell_fails_on_invalid_pool_type() { + ExtBuilder::default().build().execute_with(|| { + let pool_id = create_market_and_deploy_pool( + ALICE, + BASE_ASSET, + MarketType::Categorical(5), + _10, + vec![_1_5, _1_5, _1_5, _1_5, _1_5], + CENT, + ); + let pool = as PoolStorage>::get(pool_id).unwrap(); + let amount_buy = _1; + + let sell = pool.assets()[0..4].to_vec(); + let buy = pool.assets()[4..5].to_vec(); + + for &asset in buy.iter() { + assert_ok!(AssetManager::deposit(asset, &BOB, amount_buy)); + } + + assert_noop!( + NeoSwaps::combo_sell( + RuntimeOrigin::signed(BOB), + pool_id, + 5, + buy, + vec![], + sell, + amount_buy, + 0, + 0 + ), + Error::::InvalidPoolType, + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/deploy_combinatorial_pool.rs b/zrml/neo-swaps/src/tests/deploy_combinatorial_pool.rs new file mode 100644 index 000000000..dc204abb9 --- /dev/null +++ b/zrml/neo-swaps/src/tests/deploy_combinatorial_pool.rs @@ -0,0 +1,539 @@ +// Copyright 2023-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use crate::liquidity_tree::types::Node; +use alloc::collections::BTreeMap; +use test_case::test_case; +use zeitgeist_primitives::constants::BASE; + +#[test] +fn deploy_combinatorial_pool_works_with_single_market() { + ExtBuilder::default().build().execute_with(|| { + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _10; + let asset_count = 2usize; + let spot_prices = vec![BASE / (asset_count as u128); asset_count]; + let swap_fee = CENT; + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2)], + amount, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let assets = pool.assets(); + let expected_liquidity = 144_269_504_089; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1); + assert_eq!(pool.collateral, BASE_ASSET); + assert_liquidity_tree_state!( + pool.liquidity_shares_manager, + [Node:: { + account: Some(ALICE), + stake: amount, + fees: 0u128, + descendant_stake: 0u128, + lazy_fees: 0u128, + }], + create_b_tree_map!({ ALICE => 0 }), + Vec::::new(), + ); + assert_eq!(pool.swap_fee, swap_fee); + assert_balance!(pool.account_id, pool.collateral, buffer); + + let mut reserves = BTreeMap::new(); + for (&asset, &price) in assets.iter().zip(spot_prices.iter()) { + assert_balance!(pool.account_id, asset, amount); + assert_eq!(pool.reserve_of(&asset).unwrap(), amount); + assert_eq!(pool.calculate_spot_price(asset).unwrap(), price); + assert_balance!(ALICE, asset, 0); + reserves.insert(asset, amount); + } + assert_balance!(ALICE, BASE_ASSET, alice_before - amount - buffer); + + System::assert_last_event( + Event::CombinatorialPoolDeployed { + who: ALICE, + market_ids, + pool_id, + account_id: pool.account_id, + reserves, + collateral: pool.collateral, + liquidity_parameter: pool.liquidity_parameter, + pool_shares_amount: amount, + swap_fee, + } + .into(), + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_works_with_single_market_uneven_spot_prices() { + ExtBuilder::default().build().execute_with(|| { + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _10; + let spot_prices = vec![_1_4, _3_4]; + let expected_reserves = [_10, 20_751_874_964]; + let swap_fee = CENT; + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2)], + amount, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let assets = pool.assets(); + let expected_liquidity = 72_134_752_044; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1); + assert_eq!(pool.collateral, BASE_ASSET); + assert_liquidity_tree_state!( + pool.liquidity_shares_manager, + [Node:: { + account: Some(ALICE), + stake: amount, + fees: 0u128, + descendant_stake: 0u128, + lazy_fees: 0u128, + }], + create_b_tree_map!({ ALICE => 0 }), + Vec::::new(), + ); + assert_eq!(pool.swap_fee, swap_fee); + assert_balance!(pool.account_id, pool.collateral, buffer); + + let mut reserves = BTreeMap::new(); + for ((&asset, &price), &reserve) in + assets.iter().zip(spot_prices.iter()).zip(expected_reserves.iter()) + { + assert_balance!(pool.account_id, asset, reserve); + assert_eq!(pool.reserve_of(&asset).unwrap(), reserve); + assert_eq!(pool.calculate_spot_price(asset).unwrap(), price); + assert_balance!(ALICE, asset, amount - reserve); + reserves.insert(asset, reserve); + } + assert_balance!(ALICE, BASE_ASSET, alice_before - amount - buffer); + + System::assert_last_event( + Event::CombinatorialPoolDeployed { + who: ALICE, + market_ids, + pool_id, + account_id: pool.account_id, + reserves, + collateral: pool.collateral, + liquidity_parameter: pool.liquidity_parameter, + pool_shares_amount: amount, + swap_fee, + } + .into(), + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_works_with_multiple_markets() { + ExtBuilder::default().build().execute_with(|| { + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _10; + let asset_count = 16usize; + let spot_prices = vec![BASE / (asset_count as u128); asset_count]; + let swap_fee = CENT; + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![ + MarketType::Categorical(2), + MarketType::Categorical(4), + MarketType::Scalar(0u128..=1u128), + ], + amount, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let assets = pool.assets(); + let expected_liquidity = 36_067_376_022; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_eq!(pool.assets(), assets); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1); + assert_eq!(pool.collateral, BASE_ASSET); + assert_liquidity_tree_state!( + pool.liquidity_shares_manager, + [Node:: { + account: Some(ALICE), + stake: amount, + fees: 0u128, + descendant_stake: 0u128, + lazy_fees: 0u128, + }], + create_b_tree_map!({ ALICE => 0 }), + Vec::::new(), + ); + assert_eq!(pool.swap_fee, swap_fee); + assert_balance!(pool.account_id, pool.collateral, buffer); + + let mut reserves = BTreeMap::new(); + for (&asset, &price) in assets.iter().zip(spot_prices.iter()) { + assert_balance!(pool.account_id, asset, amount); + assert_eq!(pool.reserve_of(&asset).unwrap(), amount); + assert_eq!(pool.calculate_spot_price(asset).unwrap(), price); + assert_balance!(ALICE, asset, 0); + reserves.insert(asset, amount); + } + assert_balance!(ALICE, BASE_ASSET, alice_before - amount - buffer); + + System::assert_last_event( + Event::CombinatorialPoolDeployed { + who: ALICE, + market_ids, + pool_id, + account_id: pool.account_id, + reserves, + collateral: pool.collateral, + liquidity_parameter: pool.liquidity_parameter, + pool_shares_amount: amount, + swap_fee, + } + .into(), + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_incorrect_vec_len() { + ExtBuilder::default().build().execute_with(|| { + // The following markets will produce 6 collections: LONG & 0, LONG & 1, LONG & 2, SHORT & 0, SHORT & 1, SHORT & 2 + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(3), ScoringRule::AmmCdaHybrid), + ]; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 6, + market_ids, + _10, + // Here it's five spot prices although the above market ids will have 6 spot prices. + vec![20 * CENT; 5], + CENT, + Fuel::new(16, false), + ), + Error::::IncorrectVecLen + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let _ = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid); + let _ = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::AmmCdaHybrid); + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 10, + vec![0, 2, 1], + _10, + vec![10 * CENT; 10], + CENT, + Fuel::new(16, false), + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn deploy_combinatorial_pool_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::AmmCdaHybrid), + ]; + MarketCommons::mutate_market(market_ids.last().unwrap(), |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 10, + market_ids, + _100, + vec![10 * CENT; 10], + CENT, + Fuel::new(16, false), + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_invalid_trading_mechanism() { + ExtBuilder::default().build().execute_with(|| { + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::Parimutuel), + ]; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 10, + market_ids, + _100, + vec![10 * CENT; 10], + CENT, + Fuel::new(16, false), + ), + Error::::InvalidTradingMechanism + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_max_splits_exceeded() { + ExtBuilder::default().build().execute_with(|| { + // log2(MaxSplits + 1) + let market_count = u16::BITS - ::MaxSplits::get().leading_zeros(); + + let mut market_ids = vec![]; + for _ in 0..market_count { + let market_id = create_market( + ALICE, + BASE_ASSET, + MarketType::Categorical(2), + ScoringRule::AmmCdaHybrid, + ); + + market_ids.push(market_id); + } + let liquidity = 1_000 * BASE; + + let asset_count = 2u128.pow(market_count); + let mut spot_prices = vec![_1 / asset_count; asset_count as usize - 1]; + spot_prices.push(_1 - spot_prices.iter().sum::()); + + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2u16.pow(market_count), + market_ids, + liquidity, + spot_prices, + CENT, + Fuel::new(16, false), + ), + Error::::MaxSplitsExceeded + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_swap_fee_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2, + vec![market_id], + liquidity, + vec![_1_4, _3_4], + MIN_SWAP_FEE - 1, + Fuel::new(16, false), + ), + Error::::SwapFeeBelowMin + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_swap_fee_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2, + vec![market_id], + liquidity, + vec![_1_4, _3_4], + ::MaxSwapFee::get() + 1, + Fuel::new(16, false), + ), + Error::::SwapFeeAboveMax + ); + }); +} + +#[test_case(vec![_1_4, _3_4 - 1])] +#[test_case(vec![_1_4 + 1, _3_4])] +fn deploy_combinatorial_pool_fails_on_invalid_spot_prices(spot_prices: Vec>) { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2, + vec![market_id], + liquidity, + spot_prices, + CENT, + Fuel::new(16, false), + ), + Error::::InvalidSpotPrices + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_spot_price_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + let spot_price = MIN_SPOT_PRICE - 1; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2, + vec![market_id], + liquidity, + vec![spot_price, _1 - spot_price], + CENT, + Fuel::new(16, false), + ), + Error::::SpotPriceBelowMin + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_spot_price_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + let spot_price = MAX_SPOT_PRICE + 1; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2, + vec![market_id], + liquidity, + vec![spot_price, _1 - spot_price], + CENT, + Fuel::new(16, false), + ), + Error::::SpotPriceAboveMax + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + + #[cfg(feature = "parachain")] + let expected_error = orml_tokens::Error::::BalanceTooLow; + #[cfg(not(feature = "parachain"))] + let expected_error = orml_currencies::Error::::BalanceTooLow; + + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + // BOB doesn't have enough funds + RuntimeOrigin::signed(BOB), + 2, + vec![market_id], + liquidity, + vec![_3_4, _1_4], + CENT, + Fuel::new(16, false), + ), + expected_error + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_liquidity_too_low() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid); + let amount = _1_2; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 2, + vec![market_id], + amount, + vec![_1_2, _1_2], + CENT, + Fuel::new(16, false), + ), + Error::::LiquidityTooLow + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_incorrect_asset_count() { + ExtBuilder::default().build().execute_with(|| { + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Categorical(3), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(4), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::AmmCdaHybrid), + ]; + let amount = _1_2; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + 61, + market_ids, + amount, + vec![_1_2, _1_2], // Incorrect, but doesn't matter! + CENT, + Fuel::new(16, false), + ), + Error::::IncorrectAssetCount, + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/deploy_pool.rs b/zrml/neo-swaps/src/tests/deploy_pool.rs index 8d7f4fdec..cc78873a6 100644 --- a/zrml/neo-swaps/src/tests/deploy_pool.rs +++ b/zrml/neo-swaps/src/tests/deploy_pool.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -73,6 +73,7 @@ fn deploy_pool_works_with_binary_markets() { Event::PoolDeployed { who: ALICE, market_id, + pool_id: market_id, account_id: pool.account_id, reserves, collateral: pool.collateral, @@ -152,6 +153,7 @@ fn deploy_pool_works_with_scalar_marktes() { Event::PoolDeployed { who: ALICE, market_id, + pool_id: market_id, account_id: pool.account_id, reserves, collateral: pool.collateral, diff --git a/zrml/neo-swaps/src/tests/exit.rs b/zrml/neo-swaps/src/tests/exit.rs index 233b1f527..87a309f23 100644 --- a/zrml/neo-swaps/src/tests/exit.rs +++ b/zrml/neo-swaps/src/tests/exit.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -87,7 +87,7 @@ fn exit_works( System::assert_last_event( Event::ExitExecuted { who: ALICE, - market_id, + pool_id: market_id, pool_shares_amount, amounts_out, new_liquidity_parameter, @@ -138,7 +138,7 @@ fn last_exit_destroys_pool(market_status: MarketStatus, amounts_out: Vec::contains_key(market_id)); assert_balances!(pool_account, outcomes, [0, 0]); System::assert_last_event( - Event::PoolDestroyed { who: ALICE, market_id, amounts_out }.into(), + Event::PoolDestroyed { who: ALICE, pool_id: market_id, amounts_out }.into(), ); }); } diff --git a/zrml/neo-swaps/src/tests/join.rs b/zrml/neo-swaps/src/tests/join.rs index 1e6bc0183..325434b35 100644 --- a/zrml/neo-swaps/src/tests/join.rs +++ b/zrml/neo-swaps/src/tests/join.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -18,9 +18,7 @@ use super::*; use crate::{ helpers::create_spot_prices, - liquidity_tree::{ - traits::liquidity_tree_helper::LiquidityTreeHelper, types::LiquidityTreeError, - }, + liquidity_tree::{traits::LiquidityTreeHelper, types::LiquidityTreeError}, }; use alloc::collections::BTreeMap; use test_case::test_case; @@ -65,7 +63,7 @@ fn join_works( System::assert_last_event( Event::JoinExecuted { who, - market_id, + pool_id: market_id, pool_shares_amount, amounts_in, new_liquidity_parameter, diff --git a/zrml/neo-swaps/src/tests/mod.rs b/zrml/neo-swaps/src/tests/mod.rs index 8dc8eb78d..3b79b8548 100644 --- a/zrml/neo-swaps/src/tests/mod.rs +++ b/zrml/neo-swaps/src/tests/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -19,6 +19,9 @@ mod buy; mod buy_and_sell; +mod combo_buy; +mod combo_sell; +mod deploy_combinatorial_pool; mod deploy_pool; mod exit; mod join; @@ -38,6 +41,7 @@ use zeitgeist_primitives::{ MarketType, MultiHash, ScalarPosition, ScoringRule, }, }; +use zrml_combinatorial_tokens::types::Fuel; use zrml_market_commons::{MarketCommonsPalletApi, Markets}; #[cfg(not(feature = "parachain"))] @@ -98,6 +102,39 @@ fn create_market_and_deploy_pool( market_id } +fn create_markets_and_deploy_combinatorial_pool( + creator: AccountIdOf, + base_asset: Asset, + market_types: Vec, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, +) -> (Vec, ::PoolId) { + let mut market_ids = vec![]; + let mut asset_count = 1u16; + for market_type in market_types.iter() { + let market_id = + create_market(creator, base_asset, market_type.clone(), ScoringRule::AmmCdaHybrid); + let market = ::MarketCommons::market(&market_id).unwrap(); + asset_count *= market.outcomes(); + + market_ids.push(market_id); + } + + let pool_id = as PoolStorage>::next_pool_id(); + assert_ok!(NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + asset_count, + market_ids.clone(), + amount, + spot_prices.clone(), + swap_fee, + Fuel::new(16, false), + )); + + (market_ids, pool_id) +} + fn deposit_complete_set( market_id: MarketId, account: AccountIdOf, diff --git a/zrml/neo-swaps/src/tests/sell.rs b/zrml/neo-swaps/src/tests/sell.rs index 5c1b9dd60..d6aa370e2 100644 --- a/zrml/neo-swaps/src/tests/sell.rs +++ b/zrml/neo-swaps/src/tests/sell.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -78,7 +78,7 @@ fn sell_works() { System::assert_last_event( Event::SellExecuted { who: BOB, - market_id, + pool_id: market_id, asset_in, amount_in, amount_out: expected_amount_out_minus_fees, @@ -378,3 +378,25 @@ fn sell_fails_on_amount_out_below_min() { ); }); } + +#[test] +fn sell_fails_on_invalid_pool_type() { + ExtBuilder::default().build().execute_with(|| { + let (_, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Scalar(0..=1)], + _10, + vec![_1_2, _1_2], + CENT, + ); + + let pool = as PoolStorage>::get(pool_id).unwrap(); + let assets = pool.assets(); + + assert_noop!( + NeoSwaps::sell(RuntimeOrigin::signed(BOB), pool_id, 2, assets[0], _1, 0), + Error::::InvalidPoolType, + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/withdraw_fees.rs b/zrml/neo-swaps/src/tests/withdraw_fees.rs index 55100908c..768b0f20c 100644 --- a/zrml/neo-swaps/src/tests/withdraw_fees.rs +++ b/zrml/neo-swaps/src/tests/withdraw_fees.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -74,7 +74,7 @@ fn withdraw_fees_works() { fees_remaining, ); System::assert_last_event( - Event::FeesWithdrawn { who, market_id, amount: fees_withdrawn }.into(), + Event::FeesWithdrawn { who, pool_id: market_id, amount: fees_withdrawn }.into(), ); }; test_withdraw(ALICE, _1_4, _3_4); diff --git a/zrml/neo-swaps/src/traits/mod.rs b/zrml/neo-swaps/src/traits/mod.rs index b6c796c11..f28df2a92 100644 --- a/zrml/neo-swaps/src/traits/mod.rs +++ b/zrml/neo-swaps/src/traits/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -15,8 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -pub(crate) mod liquidity_shares_manager; -pub(crate) mod pool_operations; +mod liquidity_shares_manager; +mod pool_operations; +mod pool_storage; pub(crate) use liquidity_shares_manager::LiquiditySharesManager; pub(crate) use pool_operations::PoolOperations; +pub(crate) use pool_storage::PoolStorage; diff --git a/zrml/neo-swaps/src/traits/pool_operations.rs b/zrml/neo-swaps/src/traits/pool_operations.rs index 57c352661..b6126fcee 100644 --- a/zrml/neo-swaps/src/traits/pool_operations.rs +++ b/zrml/neo-swaps/src/traits/pool_operations.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -32,6 +32,14 @@ pub(crate) trait PoolOperations { /// Beware! The reserve need not coincide with the balance in the pool account. fn reserve_of(&self, asset: &AssetOf) -> Result, DispatchError>; + /// Return the reserves of the specified `assets`, in the same order. + /// + /// Beware! The reserve need not coincide with the balance in the pool account. + fn reserves_of(&self, assets: &[AssetOf]) -> Result>, DispatchError>; + + /// Checks if the pool can be traded on. + fn is_active(&self) -> Result; + /// Perform a checked addition to the balance of `asset`. fn increase_reserve( &mut self, @@ -46,32 +54,30 @@ pub(crate) trait PoolOperations { decrease_amount: &BalanceOf, ) -> DispatchResult; - /// Calculate the amount received from the swap that is executed when buying (the function - /// `y(x)` from the documentation). - /// - /// Note that `y(x)` does not include the amount of `asset_out` received from buying complete - /// sets and is therefore _not_ the total amount received from the buy. - /// - /// # Parameters - /// - /// - `asset_out`: The outcome being bought. - /// - `amount_in`: The amount of collateral paid. + /// Calculate the amount received when opening the specified combinatorial position. fn calculate_swap_amount_out_for_buy( &self, - asset_out: AssetOf, + buy: Vec>, + sell: Vec>, amount_in: BalanceOf, ) -> Result, DispatchError>; - /// Calculate the amount receives from selling an outcome to the pool. + /// Calculate the amount receives from closing the specified combinatorial bet. /// /// # Parameters /// - /// - `asset_in`: The outcome being sold. - /// - `amount_in`: The amount of `asset_in` sold. + /// - `buy`: The buy of the combinatorial bet to close. + /// - `keep`: The keep of the combinatorial bet to close. + /// - `sell`: The sell of the combinatorial bet to close. + /// - `amount_buy`: The amount of the buy held in the combinatorial position. + /// - `amount_sell`: The amount of the sell held in the combinatorial position. fn calculate_swap_amount_out_for_sell( &self, - asset_in: AssetOf, - amount_in: BalanceOf, + buy: Vec>, + keep: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_sell: BalanceOf, ) -> Result, DispatchError>; /// Calculate the spot price of `asset`. @@ -120,4 +126,7 @@ pub(crate) trait PoolOperations { asset: AssetOf, until: BalanceOf, ) -> Result, DispatchError>; + + /// Calculates the complement of `assets` in the set of assets contained in the pool. + fn assets_complement(&self, assets: &[AssetOf]) -> Vec>; } diff --git a/zrml/neo-swaps/src/traits/pool_storage.rs b/zrml/neo-swaps/src/traits/pool_storage.rs new file mode 100644 index 000000000..a5b73275f --- /dev/null +++ b/zrml/neo-swaps/src/traits/pool_storage.rs @@ -0,0 +1,42 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use sp_runtime::DispatchError; + +/// Slot map interface for pool storage. Undocumented functions behave like their counterparts in +/// substrate's `StorageMap`. +pub(crate) trait PoolStorage { + type PoolId; + type Pool; + + fn next_pool_id() -> Self::PoolId; + + fn add(pool: Self::Pool) -> Result; + + fn get(pool_id: Self::PoolId) -> Result; + + fn try_mutate_pool(pool_id: &Self::PoolId, mutator: F) -> Result + where + F: FnMut(&mut Self::Pool) -> Result; + + /// Mutate and maybe remove the pool indexed by `pool_id`. Unlike `try_mutate_exists` in + /// `StorageMap`, the `mutator` must return a `(R, bool)`. If and only if the pool is positive, + /// the pool is removed. + fn try_mutate_exists(pool_id: &Self::PoolId, mutator: F) -> Result + where + F: FnMut(&mut Self::Pool) -> Result<(R, bool), DispatchError>; +} diff --git a/zrml/neo-swaps/src/types/decision_market_benchmark_helper.rs b/zrml/neo-swaps/src/types/decision_market_benchmark_helper.rs new file mode 100644 index 000000000..a57857422 --- /dev/null +++ b/zrml/neo-swaps/src/types/decision_market_benchmark_helper.rs @@ -0,0 +1,84 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use crate::{ + liquidity_tree::types::LiquidityTree, + types::{DecisionMarketOracle, DecisionMarketOracleScoreboard, Pool, PoolType}, + BalanceOf, Config, MarketIdOf, Pallet, Pools, +}; +use alloc::{collections::BTreeMap, vec}; +use core::marker::PhantomData; +use sp_runtime::{traits::Zero, Saturating}; +use zeitgeist_primitives::{ + math::fixed::{BaseProvider, ZeitgeistBase}, + traits::FutarchyBenchmarkHelper, + types::Asset, +}; + +pub struct DecisionMarketBenchmarkHelper(PhantomData); + +impl FutarchyBenchmarkHelper> for DecisionMarketBenchmarkHelper +where + T: Config, +{ + /// Creates a mocked up pool with prices so that the returned decision market oracle evaluates + /// to `value`. The pool is technically in invalid state. + fn create_oracle(value: bool) -> DecisionMarketOracle { + let pool_id: MarketIdOf = 0u8.into(); + let collateral = Asset::Ztg; + + // Create a `reserves` map so that `positive_outcome` has a higher price if and only if + // `value` is `true`. + let positive_outcome = Asset::CombinatorialToken([0u8; 32]); + let negative_outcome = Asset::CombinatorialToken([1u8; 32]); + let mut reserves = BTreeMap::new(); + let one: BalanceOf = ZeitgeistBase::get().unwrap(); + let two: BalanceOf = one.saturating_mul(2u8.into()); + if value { + reserves.insert(positive_outcome, one); + reserves.insert(negative_outcome, two); + } else { + reserves.insert(positive_outcome, two); + reserves.insert(negative_outcome, one); + } + + let scoreboard = DecisionMarketOracleScoreboard::new( + Zero::zero(), + Zero::zero(), + Zero::zero(), + Zero::zero(), + ); + + let account_id: T::AccountId = Pallet::::pool_account_id(&pool_id); + let pool = Pool { + account_id: account_id.clone(), + assets: vec![positive_outcome, negative_outcome].try_into().unwrap(), + reserves: reserves.try_into().unwrap(), + collateral, + liquidity_parameter: one, + liquidity_shares_manager: LiquidityTree::new(account_id, one).unwrap(), + swap_fee: Zero::zero(), + pool_type: PoolType::Standard(0u8.into()), + }; + + Pools::::insert(pool_id, pool); + + DecisionMarketOracle::new(pool_id, positive_outcome, negative_outcome, scoreboard) + } +} diff --git a/zrml/neo-swaps/src/types/decision_market_oracle.rs b/zrml/neo-swaps/src/types/decision_market_oracle.rs new file mode 100644 index 000000000..f2f105e7f --- /dev/null +++ b/zrml/neo-swaps/src/types/decision_market_oracle.rs @@ -0,0 +1,91 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{ + traits::PoolOperations, types::DecisionMarketOracleScoreboard, weights::WeightInfoZeitgeist, + AssetOf, BalanceOf, Config, Error, Pools, +}; +use frame_support::pallet_prelude::Weight; +use frame_system::pallet_prelude::BlockNumberFor; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::DispatchError; +use zeitgeist_primitives::traits::FutarchyOracle; + +/// Struct that implements `FutarchyOracle` using price measurements from liquidity pools. +/// +/// The oracle evaluates to `true` if and only if the `positive_outcome` is more valuable than the +/// `negative_outcome` in the liquidity pool specified by `pool_id` over +/// a period of time for a certain absolute and relative threshold determined by a +/// [`DecisionMarketOracleScoreboard`]. +#[derive(Clone, Debug, Decode, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct DecisionMarketOracle +where + T: Config, +{ + pool_id: T::PoolId, + positive_outcome: AssetOf, + negative_outcome: AssetOf, + scoreboard: DecisionMarketOracleScoreboard, +} + +impl DecisionMarketOracle +where + T: Config, +{ + pub fn new( + pool_id: T::PoolId, + positive_outcome: AssetOf, + negative_outcome: AssetOf, + scoreboard: DecisionMarketOracleScoreboard, + ) -> Self { + Self { pool_id, positive_outcome, negative_outcome, scoreboard } + } + + fn try_get_prices(&self) -> Result<(BalanceOf, BalanceOf), DispatchError> { + let pool = Pools::::get(self.pool_id) + .ok_or::(Error::::PoolNotFound.into())?; + + let positive_value = pool.calculate_spot_price(self.positive_outcome)?; + let negative_value = pool.calculate_spot_price(self.negative_outcome)?; + + Ok((positive_value, negative_value)) + } +} + +impl FutarchyOracle for DecisionMarketOracle +where + T: Config, +{ + type BlockNumber = BlockNumberFor; + + fn evaluate(&self) -> (Weight, bool) { + (T::WeightInfo::decision_market_oracle_evaluate(), self.scoreboard.evaluate()) + } + + fn update(&mut self, now: Self::BlockNumber) -> Weight { + if let Ok((positive_outcome_price, negative_outcome_price)) = self.try_get_prices() { + self.scoreboard.update(now, positive_outcome_price, negative_outcome_price); + } else { + // Err on the side of caution if the pool is not found or a calculation fails by not + // enacting the policy. + self.scoreboard.skip_update(now); + } + + T::WeightInfo::decision_market_oracle_update() + } +} diff --git a/zrml/neo-swaps/src/types/decision_market_oracle_scoreboard.rs b/zrml/neo-swaps/src/types/decision_market_oracle_scoreboard.rs new file mode 100644 index 000000000..64e745762 --- /dev/null +++ b/zrml/neo-swaps/src/types/decision_market_oracle_scoreboard.rs @@ -0,0 +1,117 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{BalanceOf, Config}; +use frame_system::pallet_prelude::BlockNumberFor; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{traits::Zero, Saturating}; +use zeitgeist_primitives::math::fixed::FixedDiv; + +/// Records until the end of time. +#[derive(Clone, Debug, Decode, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct DecisionMarketOracleScoreboard +where + T: Config, +{ + /// The block at which the oracle records its first tick. + start: BlockNumberFor, + + /// The number of ticks the positive outcome requires to have + victory_margin: u128, + + /// The absolute minimum difference in prices required for the positive outcome to receive a + /// point. + price_margin_abs: BalanceOf, + + /// The relative minimum difference in prices required for the positive outcome to receive a + /// point, specified as fractional (i.e. 0.1 represents 10%). + price_margin_rel: BalanceOf, + + /// The number of ticks for the positive outcome. + pass_score: u128, + + /// The number of ticks for the negative outcome. + reject_score: u128, +} + +impl DecisionMarketOracleScoreboard +where + T: Config, +{ + pub fn new( + start: BlockNumberFor, + victory_margin: u128, + price_margin_abs: BalanceOf, + price_margin_rel: BalanceOf, + ) -> DecisionMarketOracleScoreboard { + DecisionMarketOracleScoreboard { + start, + victory_margin, + price_margin_abs, + price_margin_rel, + pass_score: 0, + reject_score: 0, + } + } + + pub fn update( + &mut self, + now: BlockNumberFor, + positive_outcome_price: BalanceOf, + negative_outcome_price: BalanceOf, + ) { + if now < self.start { + return; + } + + // Saturation is fine as that just means that the negative outcome is more valuable than the + // positive outcome. + let margin_abs = positive_outcome_price.saturating_sub(negative_outcome_price); + // In case of error, we're using zero here as a defensive default value. + let margin_rel = margin_abs.bdiv(negative_outcome_price).unwrap_or(Zero::zero()); + + if margin_abs >= self.price_margin_abs && margin_rel >= self.price_margin_rel { + // Saturation is fine as that would mean the oracle has been collecting data for + // hundreds of years. + self.pass_score.saturating_inc(); + } else { + // Saturation is fine as that would mean the oracle has been collecting data for + // hundreds of years. + self.reject_score.saturating_inc(); + } + } + + pub fn evaluate(&self) -> bool { + // Saturating is fine as that just means that the `reject_score` is higher than `pass_score` + // anyways. + let score_margin = self.pass_score.saturating_sub(self.reject_score); + + score_margin >= self.victory_margin + } + + /// Skips update on this block and awards a point to the negative outcome. + pub fn skip_update(&mut self, now: BlockNumberFor) { + if now < self.start { + return; + } + + // Saturation is fine as that would mean the oracle has been collecting data for + // hundreds of years. + self.reject_score.saturating_inc(); + } +} diff --git a/zrml/neo-swaps/src/types/mod.rs b/zrml/neo-swaps/src/types/mod.rs index 14da6c7fc..7d77f9e62 100644 --- a/zrml/neo-swaps/src/types/mod.rs +++ b/zrml/neo-swaps/src/types/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -15,10 +15,19 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . +mod decision_market_benchmark_helper; +mod decision_market_oracle; +mod decision_market_oracle_scoreboard; mod fee_distribution; mod max_assets; mod pool; +mod pool_type; +#[cfg(feature = "runtime-benchmarks")] +pub use decision_market_benchmark_helper::*; +pub use decision_market_oracle::*; +pub use decision_market_oracle_scoreboard::*; pub(crate) use fee_distribution::*; pub(crate) use max_assets::*; pub(crate) use pool::*; +pub(crate) use pool_type::*; diff --git a/zrml/neo-swaps/src/types/pool.rs b/zrml/neo-swaps/src/types/pool.rs index 401a51d39..a174c77c0 100644 --- a/zrml/neo-swaps/src/types/pool.rs +++ b/zrml/neo-swaps/src/types/pool.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Forecasting Technologies LTD. +// Copyright 2023-2025 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -17,14 +17,18 @@ use crate::{ consts::EXP_NUMERICAL_LIMIT, - math::{Math, MathOps}, + math::{ + traits::{ComboMathOps, MathOps}, + types::{ComboMath, Math}, + }, pallet::{AssetOf, BalanceOf, Config}, traits::{LiquiditySharesManager, PoolOperations}, - Error, + types::PoolType, + Error, MarketIdOf, }; use alloc::{fmt::Debug, vec::Vec}; use frame_support::{ - storage::bounded_btree_map::BoundedBTreeMap, CloneNoBound, PartialEqNoBound, + storage::bounded_btree_map::BoundedBTreeMap, BoundedVec, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -33,6 +37,8 @@ use sp_runtime::{ traits::{CheckedAdd, CheckedSub, Get}, DispatchError, DispatchResult, SaturatedConversion, Saturating, }; +use zeitgeist_primitives::types::MarketStatus; +use zrml_market_commons::MarketCommonsPalletApi; #[derive( CloneNoBound, Decode, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, @@ -45,11 +51,13 @@ where S: Get, { pub account_id: T::AccountId, + pub assets: BoundedVec, S>, pub reserves: BoundedBTreeMap, BalanceOf, S>, pub collateral: AssetOf, pub liquidity_parameter: BalanceOf, pub liquidity_shares_manager: LSM, pub swap_fee: BalanceOf, + pub pool_type: PoolType, S>, } impl PoolOperations for Pool @@ -60,7 +68,7 @@ where S: Get, { fn assets(&self) -> Vec> { - self.reserves.keys().cloned().collect() + self.assets.to_vec() } fn contains(&self, asset: &AssetOf) -> bool { @@ -71,6 +79,23 @@ where Ok(*self.reserves.get(asset).ok_or(Error::::AssetNotFound)?) } + fn reserves_of(&self, assets: &[AssetOf]) -> Result>, DispatchError> { + assets.iter().map(|a| self.reserve_of(a)).collect() + } + + /// Checks if the pool can be traded on. + fn is_active(&self) -> Result { + for market_id in self.pool_type.iter_market_ids() { + let market = T::MarketCommons::market(market_id)?; + + if market.status != MarketStatus::Active { + return Ok(false); + } + } + + Ok(true) + } + fn increase_reserve( &mut self, asset: &AssetOf, @@ -93,20 +118,41 @@ where fn calculate_swap_amount_out_for_buy( &self, - asset_out: AssetOf, + buy: Vec>, + sell: Vec>, amount_in: BalanceOf, ) -> Result, DispatchError> { - let reserve = self.reserve_of(&asset_out)?; - Math::::calculate_swap_amount_out_for_buy(reserve, amount_in, self.liquidity_parameter) + let reserves_buy = self.reserves_of(&buy)?; + let reserves_sell = self.reserves_of(&sell)?; + + ComboMath::::calculate_swap_amount_out_for_buy( + reserves_buy, + reserves_sell, + amount_in, + self.liquidity_parameter, + ) } fn calculate_swap_amount_out_for_sell( &self, - asset_in: AssetOf, - amount_in: BalanceOf, + buy: Vec>, + keep: Vec>, + sell: Vec>, + amount_buy: BalanceOf, + amount_sell: BalanceOf, ) -> Result, DispatchError> { - let reserve = self.reserve_of(&asset_in)?; - Math::::calculate_swap_amount_out_for_sell(reserve, amount_in, self.liquidity_parameter) + let reserves_buy = self.reserves_of(&buy)?; + let reserves_keep = self.reserves_of(&keep)?; + let reserves_sell = self.reserves_of(&sell)?; + + ComboMath::::calculate_swap_amount_out_for_sell( + reserves_buy, + reserves_keep, + reserves_sell, + amount_buy, + amount_sell, + self.liquidity_parameter, + ) } fn calculate_spot_price(&self, asset: AssetOf) -> Result, DispatchError> { @@ -147,4 +193,8 @@ where let spot_price = Math::::calculate_spot_price(reserve, self.liquidity_parameter)?; Math::::calculate_sell_amount_until(until, self.liquidity_parameter, spot_price) } + + fn assets_complement(&self, assets: &[AssetOf]) -> Vec> { + self.reserves.keys().filter(|a| !assets.contains(a)).cloned().collect() + } } diff --git a/zrml/neo-swaps/src/types/pool_type.rs b/zrml/neo-swaps/src/types/pool_type.rs new file mode 100644 index 000000000..65c652b1f --- /dev/null +++ b/zrml/neo-swaps/src/types/pool_type.rs @@ -0,0 +1,49 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use alloc::{boxed::Box, fmt::Debug}; +use core::iter; +use frame_support::{CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{traits::Get, BoundedVec}; + +#[derive( + CloneNoBound, Decode, Encode, Eq, MaxEncodedLen, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo, +)] +#[scale_info(skip_type_params(MaxMarkets))] +pub(crate) enum PoolType +where + MarketId: Clone + Decode + Debug + Encode + MaxEncodedLen + PartialEq + Eq + TypeInfo, + MaxMarkets: Get, +{ + Standard(MarketId), + Combinatorial(BoundedVec), +} + +impl PoolType +where + MarketId: Clone + Decode + Debug + Encode + MaxEncodedLen + PartialEq + Eq + TypeInfo, + MaxMarkets: Get, +{ + pub fn iter_market_ids(&self) -> Box + '_> { + match self { + PoolType::Standard(market_id) => Box::new(iter::once(market_id)), + PoolType::Combinatorial(market_ids) => Box::new(market_ids.iter()), + } + } +} diff --git a/zrml/neo-swaps/src/utility.rs b/zrml/neo-swaps/src/utility.rs new file mode 100644 index 000000000..5e16e0b48 --- /dev/null +++ b/zrml/neo-swaps/src/utility.rs @@ -0,0 +1,73 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use sp_runtime::{ + traits::{One, Zero}, + SaturatedConversion, +}; + +pub(crate) trait LogCeil { + /// Calculates the ceil of the log with base 2 of `self`. + fn log_ceil(&self) -> Self; +} + +impl LogCeil for u16 { + fn log_ceil(&self) -> Self { + let x = *self; + + if x.is_zero() { + return One::one(); + } + + let bits_minus_one: u16 = u16::BITS.saturating_sub(1).saturated_into(); + let leading_zeros: u16 = x.leading_zeros().saturated_into(); + let floor_log2 = bits_minus_one.saturating_sub(leading_zeros); + + if x.is_power_of_two() { floor_log2 } else { floor_log2.saturating_add(1) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + #[test_case(0, 1)] + #[test_case(1, 0)] + #[test_case(2, 1)] + #[test_case(3, 2)] + #[test_case(4, 2)] + #[test_case(5, 3)] + #[test_case(6, 3)] + #[test_case(7, 3)] + #[test_case(8, 3)] + #[test_case(9, 4)] + #[test_case(15, 4)] + #[test_case(16, 4)] + #[test_case(17, 5)] + #[test_case(1023, 10)] + #[test_case(1024, 10)] + #[test_case(1025, 11)] + #[test_case(32767, 15)] + #[test_case(32768, 15)] + #[test_case(32769, 16)] + #[test_case(65535, 16)] + fn log_ceil_works(value: u16, expected: u16) { + let actual = value.log_ceil(); + assert_eq!(actual, expected); + } +} diff --git a/zrml/neo-swaps/src/weights.rs b/zrml/neo-swaps/src/weights.rs index 2f297f5f6..7df6e6c38 100644 --- a/zrml/neo-swaps/src/weights.rs +++ b/zrml/neo-swaps/src/weights.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -19,21 +19,21 @@ //! Autogenerated weights for zrml_neo_swaps //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: `2024-12-06`, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: `2024-12-10`, STEPS: `2`, REPEAT: `0`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `ztg-benchmark`, CPU: `AMD EPYC 7601 32-Core Processor` +//! HOSTNAME: `Mac`, CPU: `` //! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` // Executed Command: -// ./target/production/zeitgeist +// ./target/release/zeitgeist // benchmark // pallet // --chain=dev -// --steps=50 -// --repeat=20 +// --steps=2 +// --repeat=0 // --pallet=zrml_neo_swaps // --extrinsic=* -// --execution=wasm +// --execution=native // --wasm-execution=compiled // --heap-pages=4096 // --template=./misc/weight_template.hbs @@ -57,187 +57,247 @@ pub trait WeightInfoZeitgeist { fn exit(n: u32) -> Weight; fn withdraw_fees() -> Weight; fn deploy_pool(n: u32) -> Weight; + fn combo_buy(n: u32) -> Weight; + fn combo_sell(n: u32) -> Weight; + fn deploy_combinatorial_pool(n: u32, m: u32) -> Weight; + fn decision_market_oracle_evaluate() -> Weight; + fn decision_market_oracle_update() -> Weight; } /// Weight functions for zrml_neo_swaps (automatically generated) pub struct WeightInfo(PhantomData); impl WeightInfoZeitgeist for WeightInfo { - /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) - /// Storage: `Tokens::Accounts` (r:5 w:5) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) - /// Storage: `Tokens::TotalIssuance` (r:4 w:4) - /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) - /// The range of component `n` is `[2, 4]`. - fn buy(n: u32) -> Weight { + /// Storage: `Tokens::Accounts` (r:129 w:129) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:128 w:128) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[2, 128]`. + fn buy(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `1248 + n * (163 ±0)` - // Estimated: `148211 + n * (2598 ±0)` - // Minimum execution time: 365_079 nanoseconds. - Weight::from_parts(343_507_801, 148211) - // Standard Error: 431_574 - .saturating_add(Weight::from_parts(17_324_088, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(5)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 2598).saturating_mul(n.into())) + // Measured: `1335 + n * (183 ±0)` + // Estimated: `337938` + // Minimum execution time: 231_000 nanoseconds. + Weight::from_parts(3_880_000_000, 337938) + .saturating_add(T::DbWeight::get().reads(262)) + .saturating_add(T::DbWeight::get().writes(261)) } - /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) /// Storage: `Tokens::Accounts` (r:129 w:129) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:3 w:3) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) /// Storage: `Tokens::TotalIssuance` (r:128 w:128) - /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(43), added: 2518, mode: `MaxEncodedLen`) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) /// The range of component `n` is `[2, 128]`. - fn sell(n: u32) -> Weight { + fn sell(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `1414 + n * (163 ±0)` - // Estimated: `148211 + n * (2598 ±0)` - // Minimum execution time: 298_498 nanoseconds. - Weight::from_parts(258_418_653, 148211) - // Standard Error: 48_348 - .saturating_add(Weight::from_parts(23_697_361, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(6)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(5)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 2598).saturating_mul(n.into())) + // Measured: `1466 + n * (183 ±0)` + // Estimated: `337938` + // Minimum execution time: 207_000 nanoseconds. + Weight::from_parts(4_459_000_000, 337938) + .saturating_add(T::DbWeight::get().reads(262)) + .saturating_add(T::DbWeight::get().writes(261)) } - /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) /// Storage: `Tokens::Accounts` (r:256 w:256) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:0) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) /// The range of component `n` is `[2, 128]`. - fn join_in_place(n: u32) -> Weight { + fn join_in_place(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `139311 + n * (197 ±0)` - // Estimated: `148211 + n * (5196 ±0)` - // Minimum execution time: 288_366 nanoseconds. - Weight::from_parts(196_787_859, 148211) - // Standard Error: 147_176 - .saturating_add(Weight::from_parts(33_115_527, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(1)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 5196).saturating_mul(n.into())) + // Measured: `139361 + n * (217 ±0)` + // Estimated: `669662` + // Minimum execution time: 235_000 nanoseconds. + Weight::from_parts(2_362_000_000, 669662) + .saturating_add(T::DbWeight::get().reads(259)) + .saturating_add(T::DbWeight::get().writes(257)) } - /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) /// Storage: `Tokens::Accounts` (r:256 w:256) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:0) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) /// The range of component `n` is `[2, 128]`. - fn join_reassigned(n: u32) -> Weight { + fn join_reassigned(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `139107 + n * (197 ±0)` - // Estimated: `148211 + n * (5196 ±0)` - // Minimum execution time: 313_248 nanoseconds. - Weight::from_parts(218_808_714, 148211) - // Standard Error: 166_770 - .saturating_add(Weight::from_parts(34_084_894, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(1)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 5196).saturating_mul(n.into())) + // Measured: `139157 + n * (217 ±0)` + // Estimated: `669662` + // Minimum execution time: 230_000 nanoseconds. + Weight::from_parts(2_864_000_000, 669662) + .saturating_add(T::DbWeight::get().reads(259)) + .saturating_add(T::DbWeight::get().writes(257)) } - /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) /// Storage: `Tokens::Accounts` (r:256 w:256) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:0) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) /// The range of component `n` is `[2, 128]`. - fn join_leaf(n: u32) -> Weight { + fn join_leaf(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `138611 + n * (197 ±0)` - // Estimated: `148211 + n * (5196 ±0)` - // Minimum execution time: 337_347 nanoseconds. - Weight::from_parts(302_848_226, 148211) - // Standard Error: 137_902 - .saturating_add(Weight::from_parts(33_259_771, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(1)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 5196).saturating_mul(n.into())) + // Measured: `138661 + n * (217 ±0)` + // Estimated: `669662` + // Minimum execution time: 279_000 nanoseconds. + Weight::from_parts(2_348_000_000, 669662) + .saturating_add(T::DbWeight::get().reads(259)) + .saturating_add(T::DbWeight::get().writes(257)) } - /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:1 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) /// Storage: `Tokens::Accounts` (r:256 w:256) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:0) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) /// The range of component `n` is `[2, 128]`. - fn exit(n: u32) -> Weight { + fn exit(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `139208 + n * (197 ±0)` - // Estimated: `148211 + n * (5196 ±0)` - // Minimum execution time: 323_147 nanoseconds. - Weight::from_parts(281_366_920, 148211) - // Standard Error: 151_037 - .saturating_add(Weight::from_parts(32_812_812, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(1)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 5196).saturating_mul(n.into())) + // Measured: `139258 + n * (217 ±0)` + // Estimated: `669662` + // Minimum execution time: 235_000 nanoseconds. + Weight::from_parts(2_551_000_000, 669662) + .saturating_add(T::DbWeight::get().reads(259)) + .saturating_add(T::DbWeight::get().writes(257)) } /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) fn withdraw_fees() -> Weight { // Proof Size summary in bytes: - // Measured: `137756` - // Estimated: `148211` - // Minimum execution time: 327_087 nanoseconds. - Weight::from_parts(357_207_000, 148211) + // Measured: `137883` + // Estimated: `156294` + // Minimum execution time: 183_000 nanoseconds. + Weight::from_parts(183_000_000, 156294) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) } /// Storage: `MarketCommons::Markets` (r:1 w:0) - /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(694), added: 3169, mode: `MaxEncodedLen`) - /// Storage: `NeoSwaps::Pools` (r:1 w:1) - /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(144746), added: 147221, mode: `MaxEncodedLen`) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `NeoSwaps::MarketIdToPoolId` (r:1 w:1) + /// Proof: `NeoSwaps::MarketIdToPoolId` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) /// Storage: `Tokens::Accounts` (r:256 w:256) - /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(123), added: 2598, mode: `MaxEncodedLen`) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `NeoSwaps::PoolCount` (r:1 w:1) + /// Proof: `NeoSwaps::PoolCount` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NeoSwaps::Pools` (r:0 w:1) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) /// The range of component `n` is `[2, 128]`. - fn deploy_pool(n: u32) -> Weight { + fn deploy_pool(_n: u32) -> Weight { // Proof Size summary in bytes: - // Measured: `611 + n * (81 ±0)` - // Estimated: `148211 + n * (5196 ±0)` - // Minimum execution time: 163_234 nanoseconds. - Weight::from_parts(113_413_550, 148211) - // Standard Error: 60_414 - .saturating_add(Weight::from_parts(35_231_124, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) - .saturating_add(T::DbWeight::get().writes(2)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) - .saturating_add(Weight::from_parts(0, 5196).saturating_mul(n.into())) + // Measured: `593 + n * (81 ±0)` + // Estimated: `669662` + // Minimum execution time: 106_000 nanoseconds. + Weight::from_parts(3_041_000_000, 669662) + .saturating_add(T::DbWeight::get().reads(260)) + .saturating_add(T::DbWeight::get().writes(260)) + } + /// Storage: `NeoSwaps::Pools` (r:1 w:1) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:7 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:256 w:256) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:128 w:128) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 7]`. + fn combo_buy(_n: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + n * (5912 ±0)` + // Estimated: `669662` + // Minimum execution time: 257_000 nanoseconds. + Weight::from_parts(8_850_000_000, 669662) + .saturating_add(T::DbWeight::get().reads(395)) + .saturating_add(T::DbWeight::get().writes(388)) + } + /// Storage: `NeoSwaps::Pools` (r:1 w:1) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// Storage: `MarketCommons::Markets` (r:7 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:255 w:255) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:128 w:128) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 7]`. + fn combo_sell(_n: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + n * (7938 ±0)` + // Estimated: `667050` + // Minimum execution time: 298_000 nanoseconds. + Weight::from_parts(14_050_000_000, 667050) + .saturating_add(T::DbWeight::get().reads(394)) + .saturating_add(T::DbWeight::get().writes(387)) + } + /// Storage: `MarketCommons::Markets` (r:7 w:0) + /// Proof: `MarketCommons::Markets` (`max_values`: None, `max_size`: Some(708), added: 3183, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(132), added: 2607, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:382 w:382) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(137), added: 2612, mode: `MaxEncodedLen`) + /// Storage: `Tokens::TotalIssuance` (r:254 w:254) + /// Proof: `Tokens::TotalIssuance` (`max_values`: None, `max_size`: Some(57), added: 2532, mode: `MaxEncodedLen`) + /// Storage: `NeoSwaps::PoolCount` (r:1 w:1) + /// Proof: `NeoSwaps::PoolCount` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `NeoSwaps::Pools` (r:0 w:1) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 7]`. + /// The range of component `m` is `[32, 64]`. + fn deploy_combinatorial_pool(n: u32, m: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `351 + n * (185 ±0)` + // Estimated: `11438 + n * (156551 ±6_414)` + // Minimum execution time: 3_344_000 nanoseconds. + Weight::from_parts(3_344_000_000, 11438) + // Standard Error: 16_685_962_537 + .saturating_add(Weight::from_parts(57_843_965_765, 0).saturating_mul(n.into())) + // Standard Error: 1_759_383_308 + .saturating_add(Weight::from_parts(108_277_083, 0).saturating_mul(m.into())) + .saturating_add(T::DbWeight::get().reads(10)) + .saturating_add(T::DbWeight::get().reads((101_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(10)) + .saturating_add(T::DbWeight::get().writes((100_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 156551).saturating_mul(n.into())) + } + fn decision_market_oracle_evaluate() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 0 nanoseconds. + Weight::from_parts(0, 0) + } + /// Storage: `NeoSwaps::Pools` (r:1 w:0) + /// Proof: `NeoSwaps::Pools` (`max_values`: None, `max_size`: Some(152829), added: 155304, mode: `MaxEncodedLen`) + fn decision_market_oracle_update() -> Weight { + // Proof Size summary in bytes: + // Measured: `492` + // Estimated: `156294` + // Minimum execution time: 47_000 nanoseconds. + Weight::from_parts(47_000_000, 156294).saturating_add(T::DbWeight::get().reads(1)) } } diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index 6a07968e3..e677aed77 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -27,6 +27,7 @@ mod benchmarks; pub mod migrations; pub mod mock; mod tests; +pub mod types; pub mod weights; pub use pallet::*; @@ -60,14 +61,15 @@ mod pallet { use orml_traits::{MultiCurrency, NamedMultiReservableCurrency}; use sp_arithmetic::per_things::{Perbill, Percent}; use sp_runtime::{ - traits::{Saturating, Zero}, + traits::{CheckedSub, Saturating, Zero}, DispatchError, DispatchResult, SaturatedConversion, }; use zeitgeist_primitives::{ constants::MILLISECS_PER_BLOCK, + math::fixed::{BaseProvider, FixedDiv, ZeitgeistBase}, traits::{ CompleteSetOperationsApi, DeployPoolApi, DisputeApi, DisputeMaxWeightApi, - DisputeResolutionApi, MarketBuilderTrait, + DisputeResolutionApi, MarketBuilderTrait, PayoutApi, }, types::{ Asset, Bond, Deadlines, EarlyClose, EarlyCloseState, GlobalDisputeItem, Market, @@ -3052,4 +3054,52 @@ mod pallet { Self::do_sell_complete_set(who, market_id, amount) } } + + impl PayoutApi for Pallet + where + T: Config, + { + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + fn payout_vector(market_id: Self::MarketId) -> Option> { + let market = >::market(&market_id).ok()?; + + if market.status != MarketStatus::Resolved || !market.is_redeemable() { + return None; + } + let resolved_outcome = market.resolved_outcome.clone()?; + + let result = match resolved_outcome { + OutcomeReport::Categorical(category_index) => { + let mut result = vec![Zero::zero(); market.outcomes() as usize]; + *result.get_mut(category_index as usize)? = ZeitgeistBase::get().ok()?; + + result + } + OutcomeReport::Scalar(value) => { + let MarketType::Scalar(range) = market.market_type else { + return None; + }; + let low = *range.start(); + let high = *range.end(); + + let low_bal: BalanceOf = low.saturated_into(); + let high_bal: BalanceOf = high.saturated_into(); + let value_bal: BalanceOf = value.saturated_into(); + + let value_clamped = value_bal.max(low_bal).min(high_bal); + let nominator = value_clamped.checked_sub(&low_bal)?; + let denominator = high_bal.checked_sub(&low_bal)?; + let payout_long = nominator.bdiv(denominator).ok()?; + let payout_short = + ZeitgeistBase::>::get().ok()?.checked_sub(&payout_long)?; + + vec![payout_long, payout_short] + } + }; + + Some(result) + } + } } diff --git a/zrml/prediction-markets/src/tests/mod.rs b/zrml/prediction-markets/src/tests/mod.rs index 80a6962ee..05bb82cc6 100644 --- a/zrml/prediction-markets/src/tests/mod.rs +++ b/zrml/prediction-markets/src/tests/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Forecasting Technologies LTD. +// Copyright 2022-2025 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -33,6 +33,7 @@ mod manually_close_market; mod on_initialize; mod on_market_close; mod on_resolution; +mod payout_vector; mod redeem_shares; mod reject_early_close; mod reject_market; diff --git a/zrml/prediction-markets/src/tests/payout_vector.rs b/zrml/prediction-markets/src/tests/payout_vector.rs new file mode 100644 index 000000000..a7e57f3b6 --- /dev/null +++ b/zrml/prediction-markets/src/tests/payout_vector.rs @@ -0,0 +1,131 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use test_case::test_case; +use zeitgeist_primitives::traits::PayoutApi; + +#[test] +fn payout_vector_works_categorical() { + ExtBuilder::default().build().execute_with(|| { + let end = 2; + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..end, + ScoringRule::AmmCdaHybrid, + ); + + let market_id = 0; + + let market = MarketCommons::market(&market_id).unwrap(); + let grace_period = end + market.deadlines.grace_period; + run_to_block(grace_period + 1); + + assert_ok!(PredictionMarkets::report( + RuntimeOrigin::signed(BOB), + 0, + OutcomeReport::Categorical(1) + )); + + run_blocks(market.deadlines.dispute_duration); + + assert_eq!(PredictionMarkets::payout_vector(market_id), Some(vec![0, BASE])); + }); +} + +#[test_case(50, vec![0, BASE])] +#[test_case(100, vec![0, BASE])] +#[test_case(130, vec![30 * CENT, 70 * CENT])] +#[test_case(200, vec![BASE, 0])] +#[test_case(250, vec![BASE, 0])] +fn payout_vector_works_scalar(value: u128, expected: Vec>) { + ExtBuilder::default().build().execute_with(|| { + let end = 2; + simple_create_scalar_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..end, + ScoringRule::AmmCdaHybrid, + ); + + let market_id = 0; + + let market = MarketCommons::market(&market_id).unwrap(); + let grace_period = end + market.deadlines.grace_period; + run_to_block(grace_period + 1); + + assert_ok!(PredictionMarkets::report( + RuntimeOrigin::signed(BOB), + 0, + OutcomeReport::Scalar(value) + )); + + run_blocks(market.deadlines.dispute_duration); + + assert_eq!(PredictionMarkets::payout_vector(market_id), Some(expected)); + }); +} + +#[test] +fn payout_vector_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(PredictionMarkets::payout_vector(1), None); + }); +} + +#[test] +fn payout_vector_fails_if_market_is_not_redeemable() { + ExtBuilder::default().build().execute_with(|| { + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..2, + ScoringRule::Parimutuel, + ); + + assert_ok!(MarketCommons::mutate_market(&0, |market_inner| { + market_inner.status = MarketStatus::Resolved; + Ok(()) + })); + + assert_eq!(PredictionMarkets::payout_vector(0), None); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Active)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +fn payout_vector_fails_on_invalid_market_status(status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..2, + ScoringRule::AmmCdaHybrid, + ); + + assert_ok!(MarketCommons::mutate_market(&0, |market_inner| { + market_inner.status = status; + Ok(()) + })); + + assert_eq!(PredictionMarkets::payout_vector(0), None); + }); +} diff --git a/zrml/prediction-markets/src/types/combinatorial_tokens_benchmark_helper.rs b/zrml/prediction-markets/src/types/combinatorial_tokens_benchmark_helper.rs new file mode 100644 index 000000000..1f5920e1d --- /dev/null +++ b/zrml/prediction-markets/src/types/combinatorial_tokens_benchmark_helper.rs @@ -0,0 +1,57 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use crate::{BalanceOf, Config, MarketIdOf}; +use alloc::vec::Vec; +use core::marker::PhantomData; +use sp_runtime::{traits::Zero, DispatchResult}; +use zeitgeist_primitives::{ + traits::{CombinatorialTokensBenchmarkHelper, MarketCommonsPalletApi}, + types::{MarketStatus, OutcomeReport}, +}; + +pub struct PredictionMarketsCombinatorialTokensBenchmarkHelper(PhantomData); + +impl CombinatorialTokensBenchmarkHelper + for PredictionMarketsCombinatorialTokensBenchmarkHelper +where + T: Config, +{ + type Balance = BalanceOf; + type MarketId = MarketIdOf; + + /// Aggressively modifies the market specified by `market_id` to be resolved. The payout vector + /// must contain exactly one non-zero entry. Does absolutely no error management. + fn setup_payout_vector( + market_id: Self::MarketId, + payout_vector: Option>, + ) -> DispatchResult { + let payout_vector = payout_vector.unwrap(); + let index = payout_vector.iter().position(|&value| !value.is_zero()).unwrap(); + + as MarketCommonsPalletApi>::mutate_market( + &market_id, + |market| { + market.resolved_outcome = + Some(OutcomeReport::Categorical(index.try_into().unwrap())); + market.status = MarketStatus::Resolved; + + Ok(()) + }, + ) + } +} diff --git a/zrml/prediction-markets/src/types/mod.rs b/zrml/prediction-markets/src/types/mod.rs new file mode 100644 index 000000000..1eb94a82e --- /dev/null +++ b/zrml/prediction-markets/src/types/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024-2025 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +mod combinatorial_tokens_benchmark_helper; + +pub use combinatorial_tokens_benchmark_helper::PredictionMarketsCombinatorialTokensBenchmarkHelper;