From 8654030bec99b4eafa994ea9393e0482e0a57459 Mon Sep 17 00:00:00 2001 From: Chralt Date: Sat, 26 Aug 2023 19:27:42 +0200 Subject: [PATCH] Implement production dispute system (#1085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ZIP-0 Part II (#938) * move disputes to simple-disputes * move more to simple-disputes * wip * wip * some mock preparation * wip * fix tests * taplo fmt * update court authorized mdms * add dispute bond to market storage * mdm funds flow * fix clippy * fix pm benchmarks * add migration * simplify migration * correct migration * migration fixes and call filter * correct admin_destroy_market benchmark * improve simple-disputes mock * benchmark reserve_outcome * benchmark reserve_outcome * fix weights file * fix after merge * add migration tests * add migration reserve id test * apply review suggestions * rename reserve_outcome to suggest_outcome * separate resolve_disputed_market into parts * edit exchange API documentation * slash dispute bond * add empty commit * correct admin_destroy_market test * remove gd dependency from simple disputes * Update zrml/simple-disputes/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/simple-disputes/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/prediction-markets/src/lib.rs Co-authored-by: Harald Heckmann * add doc string * add doc strings * Reduce settle_bonds LOC * cargo fmt * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Harald Heckmann * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Harald Heckmann * apply review suggestion * add correct mdm benchmarking on_dispute * use on_dispute_weight inside api * improve mdm weight technique * add mdm weights to on_resolution * add tests for pm * modify migration logs, fix try-runtime * little benchmark fix * Update zrml/authorized/src/benchmarks.rs Co-authored-by: Harald Heckmann * Update zrml/authorized/src/benchmarks.rs Co-authored-by: Harald Heckmann * use result with weight struct * improve dispute api weight system * Use accurate dispute weight instead of max * fix clippy * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Malte Kliemann * add copyrights --------- Co-authored-by: Harald Heckmann Co-authored-by: Malte Kliemann * fix clippy * fix clippy * Update dependencies to v0.9.38 * More dependency updates * Update standalone runtimes * Fix build with feature runtime-benchmark * Update Zeitgeist parachain runtime * Update Battery Station parachain runtime * Deduplicated dependencies * Make tests succeed for parachain runtime * Partially fix Zeitgeist xcm tests * Make all runtime tests succeed * Update standalone client * Update parachain node * Add migrations * Cargo fmt * Satisfy Clippy * Cargo fmt * Taplo format * Update licenses * remove outsider bond from migration * Reduce runtime test dependencies by half * small changes * Add missing runtime-benchmark feature * Use upper case copyright name Strange, locally the copyright script says: "All copyright notices are up to date! 🍉" On GH pipeline: "error: zrml/prediction-markets/src/benchmarks.rs: no copyright notice for Forecasting Technologies LTD found" * Remove test logger * Implement Production Court (#976) * apply review suggestions * rename reserve_outcome to suggest_outcome * separate resolve_disputed_market into parts * add debug asserts * document types * modify sort check * avoid double remove * clarify docs * avoid mut * binary search by account id * Revert "binary search by account id" This reverts commit c04d5ff12b73287b8552f2762babb80c5f79617d. * make pool item key unique * use secure random number generator * add tests * add tests * found bug in exit court * correct slashable * fix exit court * correct tests * fix bug, add tests * fix second get_winner bug * modify tests * fix draw active lock bug, add tests * rename total_slashable to consumed_stake * update config parameters, tests * modify params and add tests * add denounce tests * increase code safety * add tests * add tests * edit exchange API documentation * slash dispute bond * add empty commit * correct admin_destroy_market test * revert get_resolution_outcome to on_resolution * remove gd dependency from simple disputes * use appealed outcomes for global dispute * modify appeal bond formula * remove slash percentages * benchmark court * Update zrml/court/src/types.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/types.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/mock_storage.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/tests.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/tests.rs Co-authored-by: Malte Kliemann * periods to round timing, modify tests * modify doc comments * use onunbalanced * improve RequestInterval documentation * improve consumed_stake documentation * remove court from filter * add treasury to court * correct errors * update comment * update get_pool_item comment * update get_pool_item comment * comment juror pool * improve doc comments * cargo fmt * rename constants * edit court pallet doc * update tests * rename secret to commitment * make commit reveal failsafer * outsource commitment function * update doc comments * merge punish tardy jurors reassign juror stakes * outsource get_n_random_numbers * correct logs * rename default to get * improve extrinsic comments * simplify choose_multiple_weighted * improve naming * remove back_global_dispute * unlock jurors from last draws * improve error description * shorten mock periods * document private functions * add assert to check max appeals * add stake to juror joined event * correct test * improve test * update reassigned event comment * correct test * add assert check * improve tests for draws * add reveal vote invalid salt fails test * denounce slash inside reassign_juror_stakes * improve test readibility * rename periods to cycle_ends * add select jurors test * change randomness source in court tests * fix clippy * imrpove tests * remove cccount lookup in tests * improve test setup * add API tests, add missing unlockings * add full cycle court tests * remove invalid test * add missing WeightInfo * add inflation inside court * improve benchmarking and testing * add integrity_test * Update zrml/simple-disputes/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/simple-disputes/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/prediction-markets/src/lib.rs Co-authored-by: Harald Heckmann * add doc string * add doc strings * Reduce settle_bonds LOC * cargo fmt * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Harald Heckmann * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Harald Heckmann * apply review suggestion * add GDItem integration * add delegations * improve code structure * fix benchmarks * optimize logic * add correct mdm benchmarking on_dispute * use on_dispute_weight inside api * improve mdm weight technique * add mdm weights to on_resolution * add tests for pm * modify migration logs, fix try-runtime * adjust tests for binary search * improve benchmarking * do small change * little benchmark fix * Update zrml/authorized/src/benchmarks.rs Co-authored-by: Harald Heckmann * Update zrml/authorized/src/benchmarks.rs Co-authored-by: Harald Heckmann * use result with weight struct * improve code * update benchmark and weights * improve dispute api weight system * test delegations for reassign * correct benchmarks and test * remove comment * add API benchmarks * update weights * use generic vote item instead of outcome report * update to court id and generic court * fix stuff and add documentation * add court readme, use new parameters * improve naming, weighting * Use accurate dispute weight instead of max * take random ranges without replacement * switch get_random_number algo * integrate court and reject simple disputes * update start_global_dispute bench * fix clippy * use efficient draw without replacement algorithm * small edits * round to min juror stake, use other draw algo * use partial fisher yates algorithm * improve naming and comments * update changelog for devs * improve dispute api documentation * correct call filters * add copyright notice * remove is_outcome and is_binary * remove unnecessary "Court" prefix * update copyright notices * remove front end comment * Update zrml/court/README.md Co-authored-by: Malte Kliemann * npx prettier court readme * improve style of terminology * update readme * remove unnecessary impl block * update comment * remove todo * make reward_pot package private * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * update integrity test * improve join court doc * improve extrinsic documentation * update delegate doc * make some types package private * fix struct comment * update readme * update readme * remove commitment matcher * fix doc string * remove unnecessary error * improve import * improve test case * fix inflation misbehaviour * improve round timing structure * improve error names * fix get_valid_delegated_juror * remove wrong break * update weight for select jurors * allow rejoin after exit preparation * remove unnecessary apis * improve naming and docs * fix clippy * fix copyrights * update changelog for devs * Update runtime/common/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/benchmarks.rs Co-authored-by: Harald Heckmann * comment benchmark components * comment benchmark component * Update zrml/court/README.md Co-authored-by: Harald Heckmann * add terminology for court * Update zrml/prediction-markets/src/benchmarks.rs Co-authored-by: Harald Heckmann * restructure import * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * use mul_floor and reduce indentation * safe guard inflation emission * use imbalance for minting * use saturated div * logging if inflation period mint too high * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * reduce court InflationPeriod for Battery Station * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * fmt * fix tests * fix issue tokens without burn * remove unnecessary error return * allow equal amount to previous stake --------- Co-authored-by: Malte Kliemann Co-authored-by: Harald Heckmann * [Global Disputes] Fix issues after first battery station live test (#912) * apply review suggestions * use keep alive * cargo fmt * use function instead of raw * improve documentation * improve 2 outcome check * start gd requires two unqiue outcomes * fix error * add empty commit * correct admin_destroy_market test * revert get_resolution_outcome to on_resolution * remove gd dependency from simple disputes * fix clippy * use appealed outcomes for global dispute * fix tests and benchmarks * modify appeal bond formula * remove slash percentages * test default toolchain * benchmark court * Update zrml/court/src/types.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/types.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/mock_storage.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/tests.rs Co-authored-by: Malte Kliemann * Update zrml/court/src/tests.rs Co-authored-by: Malte Kliemann * periods to round timing, modify tests * modify doc comments * use onunbalanced * improve RequestInterval documentation * improve consumed_stake documentation * remove court from filter * add treasury to court * correct errors * update comment * update get_pool_item comment * update get_pool_item comment * comment juror pool * improve doc comments * cargo fmt * rename constants * edit court pallet doc * update tests * rename secret to commitment * make commit reveal failsafer * outsource commitment function * Update scripts/tests/misc.sh * update doc comments * merge punish tardy jurors reassign juror stakes * outsource get_n_random_numbers * correct logs * rename default to get * improve extrinsic comments * simplify choose_multiple_weighted * improve naming * remove back_global_dispute * unlock jurors from last draws * improve error description * shorten mock periods * document private functions * add assert to check max appeals * add stake to juror joined event * correct test * improve test * update reassigned event comment * correct test * add assert check * improve tests for draws * add reveal vote invalid salt fails test * denounce slash inside reassign_juror_stakes * improve test readibility * rename periods to cycle_ends * add select jurors test * change randomness source in court tests * fix clippy * imrpove tests * remove cccount lookup in tests * improve test setup * add API tests, add missing unlockings * add full cycle court tests * remove invalid test * add missing WeightInfo * add inflation inside court * improve benchmarking and testing * add integrity_test * Update zrml/simple-disputes/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/simple-disputes/src/mock.rs Co-authored-by: Harald Heckmann * Update zrml/prediction-markets/src/lib.rs Co-authored-by: Harald Heckmann * add doc string * add doc strings * Reduce settle_bonds LOC * cargo fmt * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Harald Heckmann * Update zrml/prediction-markets/src/migrations.rs Co-authored-by: Harald Heckmann * apply review suggestion * add GDItem integration * add delegations * improve code structure * fix benchmarks * optimize logic * add correct mdm benchmarking on_dispute * use on_dispute_weight inside api * improve mdm weight technique * add mdm weights to on_resolution * add tests for pm * modify migration logs, fix try-runtime * adjust tests for binary search * improve benchmarking * do small change * little benchmark fix * Update zrml/authorized/src/benchmarks.rs Co-authored-by: Harald Heckmann * Update zrml/authorized/src/benchmarks.rs Co-authored-by: Harald Heckmann * use result with weight struct * improve code * update benchmark and weights * improve dispute api weight system * test delegations for reassign * correct benchmarks and test * remove comment * add API benchmarks * update weights * use generic vote item instead of outcome report * update to court id and generic court * fix stuff and add documentation * add court readme, use new parameters * improve naming, weighting * Use accurate dispute weight instead of max * take random ranges without replacement * add start_global_dispute tests * add test * fix typo * switch get_random_number algo * integrate court and reject simple disputes * update start_global_dispute bench * fix clippy * use efficient draw without replacement algorithm * small edits * round to min juror stake, use other draw algo * use partial fisher yates algorithm * improve naming and comments * update changelog for devs * improve dispute api documentation * correct call filters * add copyright notice * remove is_outcome and is_binary * remove unnecessary "Court" prefix * update copyright notices * remove front end comment * Update zrml/court/README.md Co-authored-by: Malte Kliemann * npx prettier court readme * improve style of terminology * update readme * remove unnecessary impl block * update comment * remove todo * make reward_pot package private * Update zrml/court/src/lib.rs Co-authored-by: Malte Kliemann * update integrity test * improve join court doc * improve extrinsic documentation * update delegate doc * make some types package private * fix struct comment * update readme * update readme * remove commitment matcher * fix doc string * remove unnecessary error * improve import * improve test case * fix inflation misbehaviour * improve round timing structure * improve error names * fix get_valid_delegated_juror * remove wrong break * update weight for select jurors * allow rejoin after exit preparation * remove unnecessary apis * improve naming and docs * fix clippy * correct migrations * fix copyrights * fmt * fix clippy * fix copyrights * update changelog for devs * Update runtime/common/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/benchmarks.rs Co-authored-by: Harald Heckmann * comment benchmark components * comment benchmark component * Update zrml/court/README.md Co-authored-by: Harald Heckmann * add terminology for court * Update zrml/prediction-markets/src/benchmarks.rs Co-authored-by: Harald Heckmann * restructure import * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * use mul_floor and reduce indentation * safe guard inflation emission * use imbalance for minting * use saturated div * logging if inflation period mint too high * fix after merge * update changelog for devs * use imperative * fmt * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * reduce court InflationPeriod for Battery Station * Update zrml/court/src/lib.rs Co-authored-by: Harald Heckmann * fmt * fix tests * fix issue tokens without burn * remove unnecessary error return * Update Makefile * allow equal amount to previous stake --------- Co-authored-by: Malte Kliemann Co-authored-by: Harald Heckmann * activate court and GD on battery station * bump storage version of market commons * Update runtime/zeitgeist/src/lib.rs * Update runtime/zeitgeist/src/lib.rs * Revert "Reduce runtime test dependencies by half" This reverts commit fe589a5e8008ecf53f178c930cfa156bd2bb12d4. * Update weight templates * Use header option for benchmarks Also format benchmark script and moves header file into HEADER_GPL3. * Update rust-toolchain * Satisfy clippy * Use patched wasm-builder for new rustc * Format * Update licenses --------- Co-authored-by: Harald Heckmann Co-authored-by: Malte Kliemann --- Cargo.lock | 609 ++-- Cargo.toml | 1 + docs/changelog_for_devs.md | 93 +- primitives/Cargo.toml | 1 - primitives/src/constants.rs | 10 +- primitives/src/constants/mock.rs | 21 +- primitives/src/market.rs | 27 +- primitives/src/traits.rs | 2 +- primitives/src/traits/dispute_api.rs | 85 +- runtime/battery-station/Cargo.toml | 11 +- runtime/battery-station/src/lib.rs | 26 +- runtime/battery-station/src/parameters.rs | 62 +- runtime/common/Cargo.toml | 1 - runtime/common/src/lib.rs | 52 +- runtime/zeitgeist/Cargo.toml | 9 +- runtime/zeitgeist/src/lib.rs | 8 +- runtime/zeitgeist/src/parameters.rs | 60 +- scripts/benchmarks/configuration.sh | 2 +- scripts/benchmarks/quick_check.sh | 3 +- zrml/authorized/src/authorized_pallet_api.rs | 5 +- zrml/authorized/src/benchmarks.rs | 77 +- zrml/authorized/src/lib.rs | 163 +- zrml/authorized/src/mock.rs | 18 +- zrml/authorized/src/tests.rs | 24 +- zrml/authorized/src/weights.rs | 28 + zrml/court/Cargo.toml | 5 + zrml/court/README.md | 57 +- zrml/court/src/benchmarks.rs | 709 +++- zrml/court/src/court_pallet_api.rs | 5 +- zrml/court/src/juror.rs | 33 - zrml/court/src/juror_status.rs | 31 - zrml/court/src/lib.rs | 2650 +++++++++++--- zrml/court/src/mock.rs | 149 +- zrml/court/src/mock_storage.rs | 62 + zrml/court/src/tests.rs | 3138 ++++++++++++++++- zrml/court/src/types.rs | 360 ++ zrml/court/src/weights.rs | 264 +- zrml/global-disputes/README.md | 14 +- zrml/global-disputes/src/benchmarks.rs | 272 +- .../src/global_disputes_pallet_api.rs | 66 +- zrml/global-disputes/src/lib.rs | 570 ++- zrml/global-disputes/src/migrations.rs | 383 ++ zrml/global-disputes/src/mock.rs | 61 +- zrml/global-disputes/src/tests.rs | 814 +++-- zrml/global-disputes/src/types.rs | 100 +- zrml/global-disputes/src/utils.rs | 63 + zrml/global-disputes/src/weights.rs | 51 +- zrml/market-commons/src/lib.rs | 2 +- zrml/market-commons/src/tests.rs | 2 +- zrml/prediction-markets/Cargo.toml | 8 +- .../fuzz/pm_full_workflow.rs | 1 - zrml/prediction-markets/src/benchmarks.rs | 168 +- zrml/prediction-markets/src/lib.rs | 535 +-- zrml/prediction-markets/src/migrations.rs | 843 +++++ zrml/prediction-markets/src/mock.rs | 87 +- zrml/prediction-markets/src/tests.rs | 840 +++-- zrml/prediction-markets/src/weights.rs | 62 +- zrml/simple-disputes/Cargo.toml | 3 + zrml/simple-disputes/src/benchmarks.rs | 177 + zrml/simple-disputes/src/lib.rs | 504 ++- zrml/simple-disputes/src/mock.rs | 87 +- .../src/simple_disputes_pallet_api.rs | 5 +- zrml/simple-disputes/src/tests.rs | 44 +- zrml/simple-disputes/src/weights.rs | 133 + 64 files changed, 12197 insertions(+), 2559 deletions(-) delete mode 100644 zrml/court/src/juror.rs delete mode 100644 zrml/court/src/juror_status.rs create mode 100644 zrml/court/src/mock_storage.rs create mode 100644 zrml/court/src/types.rs create mode 100644 zrml/global-disputes/src/migrations.rs create mode 100644 zrml/global-disputes/src/utils.rs create mode 100644 zrml/simple-disputes/src/benchmarks.rs create mode 100644 zrml/simple-disputes/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index b915ab4bb..aeb03543a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,11 +23,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" dependencies = [ - "gimli 0.28.0", + "gimli 0.27.3", ] [[package]] @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -211,23 +211,24 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anstyle-parse" @@ -249,9 +250,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -259,9 +260,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "approx" @@ -330,7 +331,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.27", + "time 0.3.24", ] [[package]] @@ -346,7 +347,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.27", + "time 0.3.24", ] [[package]] @@ -418,22 +419,22 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.8.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" dependencies = [ "event-listener", ] [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -446,7 +447,7 @@ dependencies = [ "futures-sink", "futures-util", "memchr", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", ] [[package]] @@ -474,16 +475,16 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ - "addr2line 0.21.0", + "addr2line 0.20.0", "cc", "cfg-if", "libc", "miniz_oxide", - "object 0.32.0", + "object 0.31.1", "rustc-demangle", ] @@ -733,9 +734,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "bitvec" @@ -957,9 +958,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "51f1226cd9da55587234753d1245dd5b132343ea240f26b6a9003d68706141ba" dependencies = [ "jobserver", "libc", @@ -1109,9 +1110,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.0" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ "clap_builder", "clap_derive", @@ -1120,9 +1121,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.0" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" dependencies = [ "anstream", "anstyle", @@ -1132,21 +1133,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.0" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "coarsetime" @@ -1234,9 +1235,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" [[package]] name = "constant_time_eq" @@ -2027,37 +2028,23 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0" +version = "4.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2" +checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" dependencies = [ "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest 0.10.7", "fiat-crypto", + "packed_simd_2", "platforms 3.0.2", - "rustc_version 0.4.0", "subtle", "zeroize", ] -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.29", -] - [[package]] name = "cxx" -version = "1.0.106" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28403c86fc49e3401fdf45499ba37fad6493d9329449d6449d7f0e10f4654d28" +checksum = "f68e12e817cb19eaab81aaec582b4052d07debd3c3c6b083b9d361db47c7dc9d" dependencies = [ "cc", "cxxbridge-flags", @@ -2067,9 +2054,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.106" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78da94fef01786dc3e0c76eafcd187abcaa9972c78e05ff4041e24fdf059c285" +checksum = "e789217e4ab7cf8cc9ce82253180a9fe331f35f5d339f0ccfe0270b39433f397" dependencies = [ "cc", "codespan-reporting", @@ -2077,24 +2064,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] name = "cxxbridge-flags" -version = "1.0.106" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2a6f5e1dfb4b34292ad4ea1facbfdaa1824705b231610087b00b17008641809" +checksum = "78a19f4c80fd9ab6c882286fa865e92e07688f4387370a209508014ead8751d0" [[package]] name = "cxxbridge-macro" -version = "1.0.106" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c49547d73ba8dcfd4ad7325d64c6d5391ff4224d498fc39a6f3f49825a530d" +checksum = "b8fcfa71f66c8563c4fa9dd2bb68368d50267856f831ac5d85367e0805f9606c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -2169,16 +2156,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" -dependencies = [ - "const-oid", - "zeroize", -] - [[package]] name = "der-parser" version = "7.0.0" @@ -2209,9 +2186,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" [[package]] name = "derivative" @@ -2243,7 +2220,7 @@ checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -2374,7 +2351,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -2418,9 +2395,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.13" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" [[package]] name = "ecdsa" @@ -2428,10 +2405,10 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der 0.6.1", + "der", "elliptic-curve", "rfc6979", - "signature 1.6.4", + "signature", ] [[package]] @@ -2440,17 +2417,7 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature 1.6.4", -] - -[[package]] -name = "ed25519" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.1.0", + "signature", ] [[package]] @@ -2460,27 +2427,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519 1.5.3", + "ed25519", "rand 0.7.3", "serde", "sha2 0.9.9", "zeroize", ] -[[package]] -name = "ed25519-dalek" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" -dependencies = [ - "curve25519-dalek 4.0.0", - "ed25519 2.2.2", - "rand_core 0.6.4", - "serde", - "sha2 0.10.7", - "zeroize", -] - [[package]] name = "ed25519-zebra" version = "3.1.0" @@ -2509,14 +2462,14 @@ checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ "base16ct", "crypto-bigint", - "der 0.6.1", + "der", "digest 0.10.7", "ff", "generic-array 0.14.7", "group", "hkdf", "pem-rfc7468", - "pkcs8 0.9.0", + "pkcs8", "rand_core 0.6.4", "sec1", "subtle", @@ -2552,7 +2505,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -2563,7 +2516,7 @@ checksum = "b893c4eb2dc092c811165f84dc7447fae16fb66521717968c34c509b39b1a5c5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -2765,13 +2718,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.2.16", "windows-sys 0.48.0", ] @@ -2811,9 +2764,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "libz-sys", @@ -3214,7 +3167,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "waker-fn", ] @@ -3226,7 +3179,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -3272,7 +3225,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "pin-utils", "slab", ] @@ -3370,9 +3323,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "glob" @@ -3382,9 +3335,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" dependencies = [ "aho-corasick", "bstr", @@ -3406,9 +3359,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -3588,7 +3541,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", ] [[package]] @@ -3605,9 +3558,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "humantime" @@ -3631,7 +3584,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "socket2 0.4.9", "tokio", "tower-service", @@ -3651,7 +3604,7 @@ dependencies = [ "rustls 0.20.8", "rustls-native-certs", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", ] [[package]] @@ -3889,7 +3842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.8", + "rustix 0.38.4", "windows-sys 0.48.0", ] @@ -3928,9 +3881,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367a292944c07385839818bb71c8d76611138e2dedb0677d035b8da21d29c78b" +checksum = "7d291e3a5818a2384645fd9756362e6d89cf0541b0b916fa7702ea4a9833608e" dependencies = [ "jsonrpsee-core", "jsonrpsee-proc-macros", @@ -3942,9 +3895,9 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8b3815d9f5d5de348e5f162b316dc9cdf4548305ebb15b4eb9328e66cf27d7a" +checksum = "965de52763f2004bc91ac5bcec504192440f0b568a5d621c59d9dbd6f886c3fb" dependencies = [ "futures-util", "http", @@ -3955,17 +3908,17 @@ dependencies = [ "soketto", "thiserror", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-util", "tracing", - "webpki-roots 0.25.2", + "webpki-roots", ] [[package]] name = "jsonrpsee-core" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5dde66c53d6dcdc8caea1874a45632ec0fcf5b437789f1e45766a1512ce803" +checksum = "a4e70b4439a751a5de7dd5ed55eacff78ebf4ffe0fc009cb1ebb11417f5b536b" dependencies = [ "anyhow", "arrayvec 0.7.4", @@ -3991,9 +3944,9 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e8ab85614a08792b9bff6c8feee23be78c98d0182d4c622c05256ab553892a" +checksum = "baa6da1e4199c10d7b1d0a6e5e8bd8e55f351163b6f4b3cbb044672a69bd4c1c" dependencies = [ "heck", "proc-macro-crate", @@ -4004,9 +3957,9 @@ dependencies = [ [[package]] name = "jsonrpsee-server" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4d945a6008c9b03db3354fb3c83ee02d2faa9f2e755ec1dfb69c3551b8f4ba" +checksum = "1fb69dad85df79527c019659a992498d03f8495390496da2f07e6c24c2b356fc" dependencies = [ "futures-channel", "futures-util", @@ -4026,9 +3979,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245ba8e5aa633dd1c1e4fae72bce06e71f42d34c14a2767c6b4d173b57bee5e5" +checksum = "5bd522fe1ce3702fd94812965d7bb7a3364b1c9aba743944c5a00529aae80f8c" dependencies = [ "anyhow", "beef", @@ -4040,9 +3993,9 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.16.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1b3975ed5d73f456478681a417128597acd6a2487855fdb7b4a3d4d195bf5e" +checksum = "0b83daeecfc6517cfe210df24e570fb06213533dfb990318fae781f4c7119dd9" dependencies = [ "http", "jsonrpsee-client-transport", @@ -4254,6 +4207,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "libm" version = "0.2.7" @@ -4302,7 +4261,7 @@ checksum = "b6a8fcd392ff67af6cc3f03b1426c41f7f26b6b9aff2dc632c1c56dd649e571f" dependencies = [ "asn1_der", "bs58", - "ed25519-dalek 1.0.1", + "ed25519-dalek", "either", "fnv", "futures 0.3.28", @@ -4393,12 +4352,12 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276bb57e7af15d8f100d3c11cbdd32c6752b7eef4ba7a18ecf464972c07abcce" +checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" dependencies = [ "bs58", - "ed25519-dalek 2.0.0", + "ed25519-dalek", "log", "multiaddr 0.17.1", "multihash 0.17.0", @@ -4696,7 +4655,7 @@ dependencies = [ "rw-stream-sink", "soketto", "url", - "webpki-roots 0.22.6", + "webpki-roots", ] [[package]] @@ -4851,9 +4810,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "lru" @@ -5471,9 +5430,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ "autocfg", "num-integer", @@ -5482,9 +5441,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" dependencies = [ "num-traits", ] @@ -5528,7 +5487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", - "libm", + "libm 0.2.7", ] [[package]] @@ -5555,9 +5514,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ "memchr", ] @@ -5821,6 +5780,16 @@ dependencies = [ "sha2 0.10.7", ] +[[package]] +name = "packed_simd_2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" +dependencies = [ + "cfg-if", + "libm 0.1.4", +] + [[package]] name = "pallet-asset-tx-payment" version = "4.0.0-dev" @@ -7130,7 +7099,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -7146,32 +7115,32 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", - "indexmap 2.0.0", + "indexmap 1.9.3", ] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -7182,9 +7151,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" [[package]] name = "pin-utils" @@ -7198,18 +7167,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.8", - "spki 0.7.2", + "der", + "spki", ] [[package]] @@ -8553,7 +8512,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "windows-sys 0.48.0", ] @@ -8869,9 +8828,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -9008,7 +8967,7 @@ checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" dependencies = [ "pem", "ring", - "time 0.3.27", + "time 0.3.24", "x509-parser 0.13.2", "yasna", ] @@ -9021,7 +8980,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.27", + "time 0.3.24", "yasna", ] @@ -9069,22 +9028,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.20" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acde58d073e9c79da00f2b5b84eed919c8326832648a5b109b3fce1bb1175280" +checksum = "61ef7e18e8841942ddb1cf845054f8008410030a3997875d9e49b7a363063df1" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.20" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" +checksum = "2dfaf0c85b766276c797f3791f5bc6d5bd116b41d53049af2789666b0c0bc9fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -9101,13 +9060,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.6", + "regex-automata 0.3.4", "regex-syntax 0.7.4", ] @@ -9122,9 +9081,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" dependencies = [ "aho-corasick", "memchr", @@ -9425,11 +9384,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.3.3", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -9461,18 +9420,6 @@ dependencies = [ "webpki 0.22.0", ] -[[package]] -name = "rustls" -version = "0.21.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct 0.7.0", -] - [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -9494,16 +9441,6 @@ dependencies = [ "base64 0.21.2", ] -[[package]] -name = "rustls-webpki" -version = "0.101.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -10762,9 +10699,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct", - "der 0.6.1", + "der", "generic-array 0.14.7", - "pkcs8 0.9.0", + "pkcs8", "subtle", "zeroize", ] @@ -10854,29 +10791,29 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.186" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.186" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", @@ -11008,12 +10945,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "signature" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" - [[package]] name = "simba" version = "0.5.1" @@ -11028,15 +10959,15 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.11" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -11082,14 +11013,14 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" [[package]] name = "snow" -version = "0.9.3" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9d1425eb528a21de2755c75af4c9b5d57f50a0d4c3b7f1828a4cd03f8ba155" +checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" dependencies = [ "aes-gcm 0.9.4", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0", + "curve25519-dalek 4.0.0-rc.1", "rand_core 0.6.4", "ring", "rustc_version 0.4.0", @@ -11470,8 +11401,8 @@ version = "7.0.0" source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.38#bcff60a227d455d95b4712b6cb356ce56b1ff672" dependencies = [ "bytes", - "ed25519 1.5.3", - "ed25519-dalek 1.0.1", + "ed25519", + "ed25519-dalek", "futures 0.3.28", "libsecp256k1", "log", @@ -11863,24 +11794,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der 0.6.1", -] - -[[package]] -name = "spki" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" -dependencies = [ - "base64ct", - "der 0.7.8", + "der", ] [[package]] name = "ss58-registry" -version = "1.43.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6915280e2d0db8911e5032a5c275571af6bdded2916abd691a659be25d3439" +checksum = "bfc443bad666016e012538782d9e3006213a7db43e9fb1dda91657dc06a6fa08" dependencies = [ "Inflector", "num-format", @@ -12182,9 +12103,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -12238,14 +12159,14 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ "cfg-if", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.8", + "rustix 0.38.4", "windows-sys 0.48.0", ] @@ -12315,22 +12236,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -12405,9 +12326,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.27" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" dependencies = [ "deranged", "itoa", @@ -12424,9 +12345,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.13" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ "time-core", ] @@ -12477,19 +12398,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ + "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.4.9", "tokio-macros", "windows-sys 0.48.0", ] @@ -12502,7 +12424,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -12516,16 +12438,6 @@ dependencies = [ "webpki 0.22.0", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.6", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.14" @@ -12533,7 +12445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "tokio", "tokio-util", ] @@ -12548,7 +12460,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "tokio", "tracing", ] @@ -12603,7 +12515,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "tower-layer", "tower-service", ] @@ -12628,7 +12540,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", - "pin-project-lite 0.2.12", + "pin-project-lite 0.2.10", "tracing-attributes", "tracing-core", ] @@ -12641,7 +12553,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -13123,7 +13035,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -13157,7 +13069,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -13287,7 +13199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57d20cb3c59b788653d99541c646c561c9dd26506f25c0cebfe810659c54c6d7" dependencies = [ "downcast-rs", - "libm", + "libm 0.2.7", "memory_units", "num-rational", "num-traits", @@ -13300,7 +13212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5bf998ab792be85e20e771fe14182b4295571ad1d4f89d3da521c1bef5f597a" dependencies = [ "downcast-rs", - "libm", + "libm 0.2.7", "num-traits", ] @@ -13531,12 +13443,6 @@ dependencies = [ "webpki 0.22.0", ] -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" - [[package]] name = "webrtc" version = "0.6.0" @@ -13563,7 +13469,7 @@ dependencies = [ "sha2 0.10.7", "stun", "thiserror", - "time 0.3.27", + "time 0.3.24", "tokio", "turn", "url", @@ -13595,9 +13501,9 @@ dependencies = [ [[package]] name = "webrtc-dtls" -version = "0.7.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a00f4242f2db33307347bd5be53263c52a0331c96c14292118c9a6bb48d267" +checksum = "942be5bd85f072c3128396f6e5a9bfb93ca8c1939ded735d177b7bcba9a13d05" dependencies = [ "aes 0.6.0", "aes-gcm 0.10.2", @@ -13612,24 +13518,25 @@ dependencies = [ "hkdf", "hmac 0.12.1", "log", + "oid-registry 0.6.1", "p256", "p384", "rand 0.8.5", "rand_core 0.6.4", - "rcgen 0.10.0", + "rcgen 0.9.3", "ring", "rustls 0.19.1", "sec1", "serde", "sha1", "sha2 0.10.7", - "signature 1.6.4", + "signature", "subtle", "thiserror", "tokio", "webpki 0.21.4", "webrtc-util", - "x25519-dalek 2.0.0", + "x25519-dalek 2.0.0-pre.1", "x509-parser 0.13.2", ] @@ -13959,17 +13866,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -13980,9 +13887,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" @@ -14004,9 +13911,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" @@ -14028,9 +13935,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" @@ -14052,9 +13959,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" @@ -14076,9 +13983,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" @@ -14088,9 +13995,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" @@ -14112,15 +14019,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.5.15" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807" dependencies = [ "memchr", ] @@ -14157,13 +14064,12 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0" +version = "2.0.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" dependencies = [ - "curve25519-dalek 4.0.0", + "curve25519-dalek 3.2.0", "rand_core 0.6.4", - "serde", "zeroize", ] @@ -14183,7 +14089,7 @@ dependencies = [ "ring", "rusticata-macros", "thiserror", - "time 0.3.27", + "time 0.3.24", ] [[package]] @@ -14201,7 +14107,7 @@ dependencies = [ "oid-registry 0.6.1", "rusticata-macros", "thiserror", - "time 0.3.27", + "time 0.3.24", ] [[package]] @@ -14318,7 +14224,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.27", + "time 0.3.24", ] [[package]] @@ -14548,7 +14454,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.28", ] [[package]] @@ -14579,11 +14485,15 @@ dependencies = [ "pallet-balances", "pallet-randomness-collective-flip", "pallet-timestamp", + "pallet-treasury", "parity-scale-codec", "rand 0.8.5", + "rand_chacha 0.3.1", "scale-info", + "sp-arithmetic", "sp-io", "sp-runtime", + "test-case", "zeitgeist-primitives", "zrml-market-commons", ] @@ -14771,6 +14681,9 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "orml-currencies", + "orml-tokens", + "orml-traits", "pallet-balances", "pallet-timestamp", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index 89644a91c..899a30cb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -256,6 +256,7 @@ hex-literal = { version = "0.3.4", default-features = false } log = { version = "0.4.17", default-features = false } num-traits = { version = "0.2.15", default-features = false } rand = { version = "0.8.5", default-features = false } +rand_chacha = { version = "0.3.1", default-features = false } serde = { version = "1.0.152", default-features = false } [profile.dev.package] diff --git a/docs/changelog_for_devs.md b/docs/changelog_for_devs.md index e8420bcac..953ffe489 100644 --- a/docs/changelog_for_devs.md +++ b/docs/changelog_for_devs.md @@ -8,10 +8,88 @@ and does not represent a complete changelog for the zeitgeistpm/zeitgeist repository. As of 0.3.9, the changelog's format is based on -https://keepachangelog.com/en/1.0.0/ and ⚠️ marks changes that might break + and ⚠️ marks changes that might break components which query the chain's storage, the extrinsics or the runtime APIs/RPC interface. +## v0.4.0 + +[#976]: https://github.com/zeitgeistpm/zeitgeist/pull/976 + +### Changed + +All things about Global Disputes Fix ⚠️ : + +- Replace `WinnerInfo` by `GlobalDisputeInfo` with the following fields: + - `winner_outcome: OutcomeReport` + - `outcome_info: OutcomeInfo` + - `status: GdStatus` + +### Removed + +All things about Global Disputes Fix ⚠️ : + +- Remove the following event: + - `OutcomeOwnersRewardedWithNoFunds` + +### Added + +- ⚠️ Add court production implementation ([#976]). Dispatchable calls are: + - `join_court` - Join the court with a stake to become a juror in order to get + the stake-weighted chance to be selected for decision making. + - `delegate` - Join the court with a stake to become a delegator in order to + delegate the voting power to actively participating jurors. + - `prepare_exit_court` - Prepare as a court participant to leave the court + system. + - `exit_court` - Exit the court system in order to get the stake back. + - `vote` - An actively participating juror votes secretely on a specific court + case, in which the juror got selected. + - `denounce_vote` - Denounce a selected and active juror, if the secret and + vote is known before the actual reveal period. + - `reveal_vote` - An actively participating juror reveals the previously + casted secret vote. + - `appeal` - After the reveal phase (aggregation period), the jurors decision + can be appealed. + - `reassign_juror_stakes` - After the appeal period is over, losers pay the + winners for the jurors and delegators. + - `set_inflation` - Set the yearly inflation rate of the court system. Events + are: + - `JurorJoined` - A juror joined the court system. + - `ExitPrepared` - A court participant prepared to exit the court system. + - `ExitedCourt` - A court participant exited the court system. + - `JurorVoted` - A juror voted on a specific court case. + - `JurorRevealedVote` - A juror revealed the previously casted secret vote. + - `DenouncedJurorVote` - A juror was denounced. + - `DelegatorJoined` - A delegator joined the court system. + - `CourtAppealed` - A court case was appealed. + - `MintedInCourt` - A court participant was rewarded with newly minted tokens. + - `StakesReassigned` - The juror and delegator stakes have been reassigned. + The losing jurors have been slashed. The winning jurors have been rewarded + by the losers. The losing jurors are those, who did not vote, or did not + vote with the plurality, were denounced or did not reveal their vote. + - `InflationSet` - The yearly inflation rate of the court system was set. + +All things about Global Disputes Fix ⚠️ : + +- Add new dispatchable function: + - `refund_vote_fees` - Return all vote funds and fees, when a global dispute + was destroyed. +- Add the following events: + - `OutcomeOwnerRewarded` for `Possession::Paid` + - `OutcomeOwnersRewarded` for `Possession::Shared` + - `OutcomesFullyCleaned` and `OutcomesPartiallyCleaned` for extrinsic + `refund_vote_fees` +- Add enum `Possession` with variants: +- `Paid { owner: AccountId, fee: Balance }` +- `Shared { owners: BoundedVec }` +- `OutcomeInfo` has the following fields: + - `outcome_sum: Balance` + - `possession: Possession` +- Add `GdStatus` with the following enum variants: + - `Active { add_outcome_end: BlockNumber, vote_end: BlockNumber }` + - `Finished` + - `Destroyed` + ## v0.3.11 [#1049]: https://github.com/zeitgeistpm/zeitgeist/pull/1049 @@ -19,9 +97,10 @@ APIs/RPC interface. ### Changed - ⚠️ All tokens now use 10 fractional decimal places ([#1049]). -- Cross-consensus messages (XCM) assume the global canonical representation for token balances. -- The token metadata in the asset registry now assumes that the existential deposit and fee factor - are stored in base 10,000,000,000. +- Cross-consensus messages (XCM) assume the global canonical representation for + token balances. +- The token metadata in the asset registry now assumes that the existential + deposit and fee factor are stored in base 10,000,000,000. ## v0.3.10 @@ -113,7 +192,7 @@ APIs/RPC interface. - Added xTokens pallet to transfer tokens accross chains - Added AssetRegistry pallet to register foreign asset - Added UnknownTokens pallet to handle unknown foreign assets - - More information at https://github.com/zeitgeistpm/zeitgeist/pull/661#top + - More information at - Transformed integer scalar markets to fixed point with ten digits after the decimal point. As soon as this update is deployed, the interpretation of the @@ -212,8 +291,8 @@ APIs/RPC interface. - The `MarketCounter` of the `market-commons` pallet is incremented by one. This means that `MarketCounter` is now equal to the total number of markets ever created, instead of equal to the id of the last market created. For details - regarding this fix, see https://github.com/zeitgeistpm/zeitgeist/pull/636 and - https://github.com/zeitgeistpm/zeitgeist/issues/365. + regarding this fix, see + and . - Made the `min_asset_amount_out` and `max_price` parameters of `swap_exact_amount_in` and the `max_asset_amount_in` and `max_price` diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 63c6ce2e4..0e44d19a0 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -28,7 +28,6 @@ std = [ "sp-core/std", "sp-runtime/std", ] -with-global-disputes = [] [package] authors = ["Zeitgeist PM "] diff --git a/primitives/src/constants.rs b/primitives/src/constants.rs index f1a7f47dc..6e0b6d3a3 100644 --- a/primitives/src/constants.rs +++ b/primitives/src/constants.rs @@ -30,11 +30,11 @@ use crate::types::{Balance, BlockNumber}; use frame_support::{parameter_types, PalletId}; // Definitions for time -pub const BLOCKS_PER_YEAR: BlockNumber = (BLOCKS_PER_DAY * 36525) / 100; -pub const BLOCKS_PER_DAY: BlockNumber = BLOCKS_PER_HOUR * 24; +pub const BLOCKS_PER_YEAR: BlockNumber = (BLOCKS_PER_DAY * 36525) / 100; // 2_629_800 +pub const BLOCKS_PER_DAY: BlockNumber = BLOCKS_PER_HOUR * 24; // 7_200 pub const MILLISECS_PER_BLOCK: u32 = 12000; -pub const BLOCKS_PER_MINUTE: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); -pub const BLOCKS_PER_HOUR: BlockNumber = BLOCKS_PER_MINUTE * 60; +pub const BLOCKS_PER_MINUTE: BlockNumber = 60_000 / (MILLISECS_PER_BLOCK as BlockNumber); // 5 +pub const BLOCKS_PER_HOUR: BlockNumber = BLOCKS_PER_MINUTE * 60; // 300 // Definitions for currency pub const BASE: u128 = 10_000_000_000; @@ -70,6 +70,8 @@ pub const AUTHORIZED_PALLET_ID: PalletId = PalletId(*b"zge/atzd"); // Court /// Pallet identifier, mainly used for named balance reserves. pub const COURT_PALLET_ID: PalletId = PalletId(*b"zge/cout"); +/// Lock identifier, mainly used for the locks on the accounts. +pub const COURT_LOCK_ID: [u8; 8] = *b"zge/colk"; // Global Disputes pub const GLOBAL_DISPUTES_PALLET_ID: PalletId = PalletId(*b"zge/gldp"); diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs index 3c0f64294..05626c688 100644 --- a/primitives/src/constants/mock.rs +++ b/primitives/src/constants/mock.rs @@ -34,19 +34,33 @@ parameter_types! { // Court parameter_types! { - pub const CourtCaseDuration: u64 = BLOCKS_PER_DAY; + pub const AppealBond: Balance = 5 * BASE; + pub const AppealBondFactor: Balance = 2 * BASE; + pub const BlocksPerYear: BlockNumber = 10000; pub const CourtPalletId: PalletId = PalletId(*b"zge/cout"); - pub const StakeWeight: u128 = 2 * BASE; + pub const RequestInterval: BlockNumber = 15; + pub const VotePeriod: BlockNumber = 3; + pub const AggregationPeriod: BlockNumber = 4; + pub const AppealPeriod: BlockNumber = 5; + pub const LockId: LockIdentifier = *b"zge/cloc"; + pub const MaxAppeals: u32 = 4; + pub const MaxDelegations: u32 = 5; + pub const MaxSelectedDraws: u32 = 510; + pub const MaxCourtParticipants: u32 = 1_000; + pub const MinJurorStake: Balance = 50 * CENT; + pub const InflationPeriod: BlockNumber = 20; } // Global disputes parameters parameter_types! { + pub const AddOutcomePeriod: BlockNumber = 20; pub const GlobalDisputeLockId: LockIdentifier = *b"zge/vote"; pub const GlobalDisputesPalletId: PalletId = PalletId(*b"zge/gldp"); pub const MaxGlobalDisputeVotes: u32 = 50; pub const MaxOwners: u32 = 10; pub const MinOutcomeVoteAmount: Balance = 10 * CENT; pub const RemoveKeysLimit: u32 = 250; + pub const GdVotingPeriod: BlockNumber = 140; pub const VotingOutcomeFee: Balance = 100 * CENT; } @@ -60,7 +74,6 @@ parameter_types! { pub const AdvisoryBond: Balance = 25 * CENT; pub const DisputeBond: Balance = 5 * BASE; pub const DisputeFactor: Balance = 2 * BASE; - pub const GlobalDisputePeriod: BlockNumber = 7 * BLOCKS_PER_DAY; pub const MaxCategories: u16 = 10; pub const MaxDisputeDuration: BlockNumber = 50; pub const MaxDisputes: u16 = 6; @@ -85,6 +98,8 @@ parameter_types! { // Simple disputes parameters parameter_types! { pub const SimpleDisputesPalletId: PalletId = PalletId(*b"zge/sedp"); + pub const OutcomeBond: Balance = 5 * BASE; + pub const OutcomeFactor: Balance = 2 * BASE; } // Swaps parameters diff --git a/primitives/src/market.rs b/primitives/src/market.rs index 7a2cc3127..3deb85494 100644 --- a/primitives/src/market.rs +++ b/primitives/src/market.rs @@ -88,6 +88,7 @@ pub struct MarketBonds { pub creation: Option>, pub oracle: Option>, pub outsider: Option>, + pub dispute: Option>, } impl MarketBonds { @@ -100,13 +101,14 @@ impl MarketBonds { value_or_default(&self.creation) .saturating_add(value_or_default(&self.oracle)) .saturating_add(value_or_default(&self.outsider)) + .saturating_add(value_or_default(&self.dispute)) } } // Used primarily for testing purposes. impl Default for MarketBonds { fn default() -> Self { - MarketBonds { creation: None, oracle: None, outsider: None } + MarketBonds { creation: None, oracle: None, outsider: None, dispute: None } } } @@ -175,11 +177,32 @@ pub enum MarketCreation { Advised, } +/// Defines a global dispute item for the initialisation of a global dispute. +#[derive(Clone, Decode, Encode, MaxEncodedLen, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct GlobalDisputeItem { + /// The account that already paid somehow for the outcome. + pub owner: AccountId, + /// The outcome that was already paid for + /// and should be added as vote outcome inside global disputes. + pub outcome: OutcomeReport, + /// The initial amount added in the global dispute vote system initially for the outcome. + pub initial_vote_amount: Balance, +} + +// TODO to remove, when Disputes storage item is removed +#[derive(Clone, Decode, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +pub struct OldMarketDispute { + pub at: BlockNumber, + pub by: AccountId, + pub outcome: OutcomeReport, +} + #[derive(Clone, Decode, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] -pub struct MarketDispute { +pub struct MarketDispute { pub at: BlockNumber, pub by: AccountId, pub outcome: OutcomeReport, + pub bond: Balance, } /// How a market should resolve disputes diff --git a/primitives/src/traits.rs b/primitives/src/traits.rs index fe7ec219f..a1be3cd57 100644 --- a/primitives/src/traits.rs +++ b/primitives/src/traits.rs @@ -22,7 +22,7 @@ mod market_id; mod swaps; mod zeitgeist_multi_reservable_currency; -pub use dispute_api::{DisputeApi, DisputeResolutionApi}; +pub use dispute_api::{DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi}; pub use market_commons_pallet_api::MarketCommonsPalletApi; pub use market_id::MarketId; pub use swaps::Swaps; diff --git a/primitives/src/traits/dispute_api.rs b/primitives/src/traits/dispute_api.rs index caa7b94f3..7c7accf1c 100644 --- a/primitives/src/traits/dispute_api.rs +++ b/primitives/src/traits/dispute_api.rs @@ -16,12 +16,14 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . +extern crate alloc; + use crate::{ - market::MarketDispute, outcome_report::OutcomeReport, - types::{Asset, Market}, + types::{Asset, GlobalDisputeItem, Market, ResultWithWeightInfo}, }; -use frame_support::{dispatch::DispatchResult, pallet_prelude::Weight, BoundedVec}; +use alloc::vec::Vec; +use frame_support::pallet_prelude::Weight; use parity_scale_codec::MaxEncodedLen; use sp_runtime::DispatchError; @@ -35,9 +37,13 @@ type MarketOfDisputeApi = Market< Asset<::MarketId>, >; +type GlobalDisputeItemOfDisputeApi = + GlobalDisputeItem<::AccountId, ::Balance>; + pub trait DisputeApi { type AccountId; type Balance; + type NegativeImbalance; type BlockNumber; type MarketId: MaxEncodedLen; type Moment; @@ -48,10 +54,9 @@ pub trait DisputeApi { /// Further interaction with the dispute API (if necessary) **should** happen through an /// associated pallet. **May** assume that `market.dispute_mechanism` refers to the calling dispute API. fn on_dispute( - previous_disputes: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOfDisputeApi, - ) -> DispatchResult; + ) -> Result, DispatchError>; /// Manage market resolution of a disputed market. /// @@ -63,31 +68,81 @@ pub trait DisputeApi { /// Returns the dispute mechanism's report if available, otherwise `None`. If `None` is /// returned, this means that the dispute could not be resolved. fn on_resolution( - disputes: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOfDisputeApi, - ) -> Result, DispatchError>; + ) -> Result>, DispatchError>; - /// Query the future resolution block of a disputed market. + /// Allow the transfer of funds from the API caller to the API consumer and back. + /// This can be based on the final resolution outcome of the market. + /// **May** assume that `market.dispute_mechanism` refers to the calling dispute API. + /// + /// # Arguments + /// * `market_id` - The identifier of the market. + /// * `market` - The market data. + /// * `resolved_outcome` - The final resolution outcome of the market. + /// * `amount` - The amount of funds transferred to the dispute mechanism. + /// + /// # Returns + /// Returns a negative imbalance in order to transfer funds back to the caller. + fn exchange( + market_id: &Self::MarketId, + market: &MarketOfDisputeApi, + resolved_outcome: &OutcomeReport, + amount: Self::NegativeImbalance, + ) -> Result, DispatchError>; /// **May** assume that `market.dispute_mechanism` refers to the calling dispute API. /// /// # Returns /// /// Returns the future resolution block if available, otherwise `None`. fn get_auto_resolve( - disputes: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOfDisputeApi, - ) -> Result, DispatchError>; + ) -> ResultWithWeightInfo>; /// Returns `true` if the market dispute mechanism /// was unable to come to a conclusion. /// **May** assume that `market.dispute_mechanism` refers to the calling dispute API. fn has_failed( - disputes: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOfDisputeApi, - ) -> Result; + ) -> Result, DispatchError>; + + /// Initialise a global dispute for the failed market dispute mechanism. + /// **May** assume that `market.dispute_mechanism` refers to the calling dispute API. + /// + /// # Returns + /// Returns the initial vote outcomes with initial vote value and owner of the vote. + fn on_global_dispute( + market_id: &Self::MarketId, + market: &MarketOfDisputeApi, + ) -> Result>>, DispatchError>; + + /// Allow the API consumer to clear storage items of the dispute mechanism. + /// This may be called, when the dispute mechanism is no longer needed. + /// **May** assume that `market.dispute_mechanism` refers to the calling dispute API. + fn clear( + market_id: &Self::MarketId, + market: &MarketOfDisputeApi, + ) -> Result, DispatchError>; +} + +pub trait DisputeMaxWeightApi { + /// Return the max weight of the `on_dispute` function. + fn on_dispute_max_weight() -> Weight; + /// Return the max weight of the `on_resolution` function. + fn on_resolution_max_weight() -> Weight; + /// Return the max weight of the `exchange` function. + fn exchange_max_weight() -> Weight; + /// Query the future resolution block of a disputed market. + /// Return the max weight of the `get_auto_resolve` function. + fn get_auto_resolve_max_weight() -> Weight; + /// Return the max weight of the `has_failed` function. + fn has_failed_max_weight() -> Weight; + /// Return the max weight of the `on_global_dispute` function. + fn on_global_dispute_max_weight() -> Weight; + /// Return the max weight of the `clear` function. + fn clear_max_weight() -> Weight; } type MarketOfDisputeResolutionApi = Market< @@ -103,7 +158,6 @@ pub trait DisputeResolutionApi { type Balance; type BlockNumber; type MarketId: MaxEncodedLen; - type MaxDisputes; type Moment; /// Resolve a market. @@ -142,9 +196,4 @@ pub trait DisputeResolutionApi { /// /// Returns the number of elements in the storage structure. fn remove_auto_resolve(market_id: &Self::MarketId, resolve_at: Self::BlockNumber) -> u32; - - /// Get the disputes of a market. - fn get_disputes( - market_id: &Self::MarketId, - ) -> BoundedVec, Self::MaxDisputes>; } diff --git a/runtime/battery-station/Cargo.toml b/runtime/battery-station/Cargo.toml index 8805b97c9..ade6c68de 100644 --- a/runtime/battery-station/Cargo.toml +++ b/runtime/battery-station/Cargo.toml @@ -324,7 +324,7 @@ std = [ "zrml-prediction-markets/std", "zrml-rikiddo/std", "zrml-simple-disputes/std", - "zrml-global-disputes?/std", + "zrml-global-disputes/std", "zrml-styx/std", "zrml-swaps-runtime-api/std", "zrml-swaps/std", @@ -377,7 +377,7 @@ try-runtime = [ "zrml-prediction-markets/try-runtime", "zrml-rikiddo/try-runtime", "zrml-simple-disputes/try-runtime", - "zrml-global-disputes?/try-runtime", + "zrml-global-disputes/try-runtime", "zrml-styx/try-runtime", "zrml-swaps/try-runtime", @@ -386,6 +386,8 @@ try-runtime = [ "pallet-author-mapping?/try-runtime", "pallet-author-slot-filter?/try-runtime", "pallet-parachain-staking?/try-runtime", + # Required by pallet-parachain-staking@v0.26.1 + "parity-scale-codec/full", "pallet-proxy/try-runtime", "pallet-grandpa/try-runtime", "pallet-aura/try-runtime", @@ -399,11 +401,6 @@ try-runtime = [ "cumulus-pallet-xcmp-queue?/try-runtime", "parachain-info?/try-runtime", ] -with-global-disputes = [ - "zrml-global-disputes", - "common-runtime/with-global-disputes", - "zrml-prediction-markets/with-global-disputes", -] [package] authors = ["Zeitgeist PM "] diff --git a/runtime/battery-station/src/lib.rs b/runtime/battery-station/src/lib.rs index 4b050f78c..8e26a9494 100644 --- a/runtime/battery-station/src/lib.rs +++ b/runtime/battery-station/src/lib.rs @@ -166,29 +166,34 @@ impl Contains for ContractsCallfilter { #[derive(scale_info::TypeInfo)] pub struct IsCallable; -// Currently disables Court, Rikiddo and creation of markets using Court. +// Currently disables Rikiddo. impl Contains for IsCallable { fn contains(call: &RuntimeCall) -> bool { #[allow(clippy::match_like_matches_macro)] match call { - RuntimeCall::Court(_) => false, + RuntimeCall::SimpleDisputes(_) => false, RuntimeCall::LiquidityMining(_) => false, RuntimeCall::PredictionMarkets(inner_call) => { match inner_call { - // Disable Rikiddo markets + // Disable Rikiddo and SimpleDisputes markets create_market { scoring_rule: ScoringRule::RikiddoSigmoidFeeMarketEma, .. } => false, + create_market { + dispute_mechanism: MarketDisputeMechanism::SimpleDisputes, + .. + } => false, edit_market { scoring_rule: ScoringRule::RikiddoSigmoidFeeMarketEma, .. } => false, - // Disable Court dispute resolution mechanism - create_market { dispute_mechanism: MarketDisputeMechanism::Court, .. } => false, create_cpmm_market_and_deploy_assets { - dispute_mechanism: MarketDisputeMechanism::Court, + dispute_mechanism: MarketDisputeMechanism::SimpleDisputes, + .. + } => false, + edit_market { + dispute_mechanism: MarketDisputeMechanism::SimpleDisputes, .. } => false, - edit_market { dispute_mechanism: MarketDisputeMechanism::Court, .. } => false, _ => true, } } @@ -199,13 +204,6 @@ impl Contains for IsCallable { decl_common_types!(); -#[cfg(feature = "with-global-disputes")] -create_runtime_with_additional_pallets!( - GlobalDisputes: zrml_global_disputes::{Call, Event, Pallet, Storage} = 59, - Sudo: pallet_sudo::{Call, Config, Event, Pallet, Storage} = 150, -); - -#[cfg(not(feature = "with-global-disputes"))] create_runtime_with_additional_pallets!( Sudo: pallet_sudo::{Call, Config, Event, Pallet, Storage} = 150, ); diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs index c4e5cf874..b3317c3bc 100644 --- a/runtime/battery-station/src/parameters.rs +++ b/runtime/battery-station/src/parameters.rs @@ -26,7 +26,7 @@ use super::{Runtime, VERSION}; use frame_support::{ dispatch::DispatchClass, parameter_types, - traits::WithdrawReasons, + traits::{LockIdentifier, WithdrawReasons}, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, WEIGHT_REF_TIME_PER_SECOND}, Weight, @@ -43,9 +43,6 @@ use sp_runtime::{ use sp_version::RuntimeVersion; use zeitgeist_primitives::{constants::*, types::*}; -#[cfg(feature = "with-global-disputes")] -use frame_support::traits::LockIdentifier; - pub(crate) const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10); pub(crate) const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( WEIGHT_REF_TIME_PER_SECOND.saturating_div(2), @@ -105,13 +102,37 @@ parameter_types! { pub ContractsSchedule: pallet_contracts::Schedule = Default::default(); // Court - /// Duration of a single court case. - pub const CourtCaseDuration: u64 = BLOCKS_PER_DAY; + /// (Slashable) Bond that is provided for overriding the last appeal. + /// This bond increases exponentially with the number of appeals. + /// Slashed in case the final outcome does match the appealed outcome for which the `AppealBond` + /// was deposited. + pub const AppealBond: Balance = 5 * BASE; + /// The blocks per year required to calculate the yearly inflation for court incentivisation. + pub const BlocksPerYear: BlockNumber = BLOCKS_PER_YEAR; /// Pallet identifier, mainly used for named balance reserves. pub const CourtPalletId: PalletId = COURT_PALLET_ID; - /// This value is multiplied by the current number of jurors to determine the stake - /// the juror has to pay. - pub const StakeWeight: u128 = 2 * BASE; + /// The time in which the jurors can cast their secret vote. + pub const CourtVotePeriod: BlockNumber = 3 * BLOCKS_PER_DAY; + /// The time in which the jurors should reveal their secret vote. + pub const CourtAggregationPeriod: BlockNumber = 3 * BLOCKS_PER_DAY; + /// The time in which a court case can get appealed. + pub const CourtAppealPeriod: BlockNumber = BLOCKS_PER_DAY; + /// The court lock identifier. + pub const CourtLockId: LockIdentifier = COURT_LOCK_ID; + /// The time in which the inflation is periodically issued. + pub const InflationPeriod: BlockNumber = 3 * BLOCKS_PER_DAY; + /// The maximum number of appeals until the court fails. + pub const MaxAppeals: u32 = 4; + /// The maximum number of delegations per juror account. + pub const MaxDelegations: u32 = 5; + /// The maximum number of randomly selected `MinJurorStake` draws / atoms of jurors for a dispute. + pub const MaxSelectedDraws: u32 = 510; + /// The maximum number of jurors / delegators that can be registered. + pub const MaxCourtParticipants: u32 = 1_000; + /// The minimum stake a user needs to reserve to become a juror. + pub const MinJurorStake: Balance = 500 * BASE; + /// The interval for requesting multiple court votes at once. + pub const RequestInterval: BlockNumber = 7 * BLOCKS_PER_DAY; // Democracy /// How often (in blocks) new public referenda are launched. @@ -179,11 +200,9 @@ parameter_types! { /// The percentage of the advisory bond that gets slashed when a market is rejected. pub const AdvisoryBondSlashPercentage: Percent = Percent::from_percent(0); /// (Slashable) Bond that is provided for disputing the outcome. - /// Slashed in case the final outcome does not match the dispute for which the `DisputeBond` - /// was deposited. - pub const DisputeBond: Balance = 5 * BASE; - /// `DisputeBond` is increased by this factor after every dispute. - pub const DisputeFactor: Balance = 2 * BASE; + /// Unreserved in case the dispute was justified otherwise slashed. + /// This is when the resolved outcome is different to the default (reported) outcome. + pub const DisputeBond: Balance = 25 * BASE; /// Maximum Categories a prediciton market can have (excluding base asset). pub const MaxCategories: u16 = MAX_CATEGORIES; /// Maximum block period for a dispute. @@ -250,6 +269,12 @@ parameter_types! { // Simple disputes parameters /// Pallet identifier, mainly used for named balance reserves. pub const SimpleDisputesPalletId: PalletId = SD_PALLET_ID; + /// (Slashable) Bond that is provided for overriding the last outcome addition. + /// Slashed in case the final outcome does not match the dispute for which the `OutcomeBond` + /// was deposited. + pub const OutcomeBond: Balance = 5 * BASE; + /// `OutcomeBond` is increased by this factor after every new outcome addition. + pub const OutcomeFactor: Balance = 2 * BASE; // Swaps parameters /// A precentage from the withdrawal amount a liquidity provider wants to withdraw @@ -382,21 +407,22 @@ parameter_types! { WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); } -#[cfg(feature = "with-global-disputes")] parameter_types! { // Global Disputes + /// The time period in which the addition of new outcomes are allowed. + pub const AddOutcomePeriod: BlockNumber = BLOCKS_PER_DAY; /// Vote lock identifier, mainly used for the LockableCurrency on the native token. pub const GlobalDisputeLockId: LockIdentifier = GLOBAL_DISPUTES_LOCK_ID; /// Pallet identifier pub const GlobalDisputesPalletId: PalletId = GLOBAL_DISPUTES_PALLET_ID; - /// The period for a global dispute to end. - pub const GlobalDisputePeriod: BlockNumber = 3 * BLOCKS_PER_DAY; - /// The maximum number of owners for a voting outcome for private API calls of `push_voting_outcome`. + /// The maximum number of owners for a voting outcome for private API calls of `push_vote_outcome`. pub const MaxOwners: u32 = 10; /// The maximum number of market ids (participate in multiple different global disputes at the same time) for one account to vote on outcomes. pub const MaxGlobalDisputeVotes: u32 = 50; /// The minimum required amount to vote on an outcome. pub const MinOutcomeVoteAmount: Balance = 10 * BASE; + /// The time period in which votes are allowed. + pub const GdVotingPeriod: BlockNumber = 3 * BLOCKS_PER_DAY; /// The fee required to add a voting outcome. pub const VotingOutcomeFee: Balance = 200 * BASE; /// The remove limit for the Outcomes storage double map. diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index a33d82337..9ed0a7429 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -73,7 +73,6 @@ std = [ "pallet-vesting/std", "pallet-parachain-staking?/std", ] -with-global-disputes = [] [package] authors = ["Zeitgeist PM "] diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 530beaef1..c43311299 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -60,11 +60,19 @@ macro_rules! decl_common_types { orml_asset_registry::Migration, orml_unknown_tokens::Migration, pallet_xcm::migration::v1::MigrateToV1, + // IMPORTANT that AddDisputeBond comes before MoveDataToSimpleDisputes!!! + zrml_prediction_markets::migrations::AddDisputeBond, + zrml_prediction_markets::migrations::MoveDataToSimpleDisputes, + zrml_global_disputes::migrations::ModifyGlobalDisputesStructures, ); #[cfg(not(feature = "parachain"))] type Migrations = ( pallet_grandpa::migrations::CleanupSetIdSessionMap, + // IMPORTANT that AddDisputeBond comes before MoveDataToSimpleDisputes!!! + zrml_prediction_markets::migrations::AddDisputeBond, + zrml_prediction_markets::migrations::MoveDataToSimpleDisputes, + zrml_global_disputes::migrations::ModifyGlobalDisputesStructures, ); pub type Executive = frame_executive::Executive< @@ -186,6 +194,7 @@ macro_rules! decl_common_types { let mut pallets = vec![ AuthorizedPalletId::get(), CourtPalletId::get(), + GlobalDisputesPalletId::get(), LiquidityMiningPalletId::get(), PmPalletId::get(), SimpleDisputesPalletId::get(), @@ -193,9 +202,6 @@ macro_rules! decl_common_types { TreasuryPalletId::get(), ]; - #[cfg(feature = "with-global-disputes")] - pallets.push(GlobalDisputesPalletId::get()); - if let Some(pallet_id) = frame_support::PalletId::try_from_sub_account::(ai) { return pallets.contains(&pallet_id.0); } @@ -302,10 +308,11 @@ macro_rules! create_runtime { Court: zrml_court::{Call, Event, Pallet, Storage} = 52, LiquidityMining: zrml_liquidity_mining::{Call, Config, Event, Pallet, Storage} = 53, RikiddoSigmoidFeeMarketEma: zrml_rikiddo::::{Pallet, Storage} = 54, - SimpleDisputes: zrml_simple_disputes::{Event, Pallet, Storage} = 55, + SimpleDisputes: zrml_simple_disputes::{Call, Event, Pallet, Storage} = 55, Swaps: zrml_swaps::{Call, Event, Pallet, Storage} = 56, PredictionMarkets: zrml_prediction_markets::{Call, Event, Pallet, Storage} = 57, Styx: zrml_styx::{Call, Event, Pallet, Storage} = 58, + GlobalDisputes: zrml_global_disputes::{Call, Event, Pallet, Storage} = 59, $($additional_pallets)* } @@ -1042,13 +1049,27 @@ macro_rules! impl_config_traits { } impl zrml_court::Config for Runtime { - type CourtCaseDuration = CourtCaseDuration; + type AppealBond = AppealBond; + type BlocksPerYear = BlocksPerYear; + type VotePeriod = CourtVotePeriod; + type AggregationPeriod = CourtAggregationPeriod; + type AppealPeriod = CourtAppealPeriod; + type LockId = CourtLockId; + type PalletId = CourtPalletId; + type Currency = Balances; type DisputeResolution = zrml_prediction_markets::Pallet; type RuntimeEvent = RuntimeEvent; + type InflationPeriod = InflationPeriod; type MarketCommons = MarketCommons; - type PalletId = CourtPalletId; + type MaxAppeals = MaxAppeals; + type MaxDelegations = MaxDelegations; + type MaxSelectedDraws = MaxSelectedDraws; + type MaxCourtParticipants = MaxCourtParticipants; + type MinJurorStake = MinJurorStake; + type MonetaryGovernanceOrigin = EnsureRoot; type Random = RandomnessCollectiveFlip; - type StakeWeight = StakeWeight; + type RequestInterval = RequestInterval; + type Slash = Treasury; type TreasuryPalletId = TreasuryPalletId; type WeightInfo = zrml_court::weights::WeightInfo; } @@ -1098,12 +1119,8 @@ macro_rules! impl_config_traits { type CloseOrigin = EnsureRoot; type DestroyOrigin = EnsureRootOrAllAdvisoryCommittee; type DisputeBond = DisputeBond; - type DisputeFactor = DisputeFactor; type RuntimeEvent = RuntimeEvent; - #[cfg(feature = "with-global-disputes")] type GlobalDisputes = GlobalDisputes; - #[cfg(feature = "with-global-disputes")] - type GlobalDisputePeriod = GlobalDisputePeriod; // LiquidityMining is currently unstable. // NoopLiquidityMining will be applied only to mainnet once runtimes are separated. type LiquidityMining = NoopLiquidityMining; @@ -1153,15 +1170,21 @@ macro_rules! impl_config_traits { } impl zrml_simple_disputes::Config for Runtime { + type AssetManager = AssetManager; + type OutcomeBond = OutcomeBond; + type OutcomeFactor = OutcomeFactor; type DisputeResolution = zrml_prediction_markets::Pallet; type RuntimeEvent = RuntimeEvent; type MarketCommons = MarketCommons; + type MaxDisputes = MaxDisputes; type PalletId = SimpleDisputesPalletId; + type WeightInfo = zrml_simple_disputes::weights::WeightInfo; } - #[cfg(feature = "with-global-disputes")] impl zrml_global_disputes::Config for Runtime { + type AddOutcomePeriod = AddOutcomePeriod; type Currency = Balances; + type DisputeResolution = zrml_prediction_markets::Pallet; type RuntimeEvent = RuntimeEvent; type GlobalDisputeLockId = GlobalDisputeLockId; type GlobalDisputesPalletId = GlobalDisputesPalletId; @@ -1170,6 +1193,7 @@ macro_rules! impl_config_traits { type MaxOwners = MaxOwners; type MinOutcomeVoteAmount = MinOutcomeVoteAmount; type RemoveKeysLimit = RemoveKeysLimit; + type GdVotingPeriod = GdVotingPeriod; type VotingOutcomeFee = VotingOutcomeFee; type WeightInfo = zrml_global_disputes::weights::WeightInfo; } @@ -1312,7 +1336,7 @@ macro_rules! create_runtime_api { list_benchmark!(list, extra, zrml_swaps, Swaps); list_benchmark!(list, extra, zrml_authorized, Authorized); list_benchmark!(list, extra, zrml_court, Court); - #[cfg(feature = "with-global-disputes")] + list_benchmark!(list, extra, zrml_simple_disputes, SimpleDisputes); list_benchmark!(list, extra, zrml_global_disputes, GlobalDisputes); #[cfg(not(feature = "parachain"))] list_benchmark!(list, extra, zrml_prediction_markets, PredictionMarkets); @@ -1413,7 +1437,7 @@ macro_rules! create_runtime_api { add_benchmark!(params, batches, zrml_swaps, Swaps); add_benchmark!(params, batches, zrml_authorized, Authorized); add_benchmark!(params, batches, zrml_court, Court); - #[cfg(feature = "with-global-disputes")] + add_benchmark!(params, batches, zrml_simple_disputes, SimpleDisputes); add_benchmark!(params, batches, zrml_global_disputes, GlobalDisputes); #[cfg(not(feature = "parachain"))] add_benchmark!(params, batches, zrml_prediction_markets, PredictionMarkets); diff --git a/runtime/zeitgeist/Cargo.toml b/runtime/zeitgeist/Cargo.toml index 80260ddc6..0798fd41c 100644 --- a/runtime/zeitgeist/Cargo.toml +++ b/runtime/zeitgeist/Cargo.toml @@ -313,7 +313,7 @@ std = [ "zrml-prediction-markets/std", "zrml-rikiddo/std", "zrml-simple-disputes/std", - "zrml-global-disputes?/std", + "zrml-global-disputes/std", "zrml-swaps-runtime-api/std", "zrml-styx/std", "zrml-swaps/std", @@ -366,7 +366,7 @@ try-runtime = [ "zrml-prediction-markets/try-runtime", "zrml-rikiddo/try-runtime", "zrml-simple-disputes/try-runtime", - "zrml-global-disputes?/try-runtime", + "zrml-global-disputes/try-runtime", "zrml-styx/try-runtime", "zrml-swaps/try-runtime", @@ -389,11 +389,6 @@ try-runtime = [ "cumulus-pallet-xcmp-queue?/try-runtime", "parachain-info?/try-runtime", ] -with-global-disputes = [ - "zrml-global-disputes", - "common-runtime/with-global-disputes", - "zrml-prediction-markets/with-global-disputes", -] [package] authors = ["Zeitgeist PM "] diff --git a/runtime/zeitgeist/src/lib.rs b/runtime/zeitgeist/src/lib.rs index f9c8bca24..44811a014 100644 --- a/runtime/zeitgeist/src/lib.rs +++ b/runtime/zeitgeist/src/lib.rs @@ -166,6 +166,7 @@ impl Contains for IsCallable { RuntimeCall::Court(_) => false, #[cfg(feature = "parachain")] RuntimeCall::DmpQueue(service_overweight { .. }) => false, + RuntimeCall::GlobalDisputes(_) => false, RuntimeCall::LiquidityMining(_) => false, RuntimeCall::PredictionMarkets(inner_call) => { match inner_call { @@ -182,6 +183,7 @@ impl Contains for IsCallable { _ => true, } } + RuntimeCall::SimpleDisputes(_) => false, RuntimeCall::System(inner_call) => { match inner_call { // Some "waste" storage will never impact proper operation. @@ -213,12 +215,6 @@ impl Contains for IsCallable { decl_common_types!(); -#[cfg(feature = "with-global-disputes")] -create_runtime_with_additional_pallets!( - GlobalDisputes: zrml_global_disputes::{Call, Event, Pallet, Storage} = 59, -); - -#[cfg(not(feature = "with-global-disputes"))] create_runtime_with_additional_pallets!(); impl_config_traits!(); diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs index f82dbf5b1..2afd3dc66 100644 --- a/runtime/zeitgeist/src/parameters.rs +++ b/runtime/zeitgeist/src/parameters.rs @@ -26,7 +26,7 @@ use super::{Runtime, VERSION}; use frame_support::{ dispatch::DispatchClass, parameter_types, - traits::WithdrawReasons, + traits::{LockIdentifier, WithdrawReasons}, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, WEIGHT_REF_TIME_PER_SECOND}, Weight, @@ -43,9 +43,6 @@ use sp_runtime::{ use sp_version::RuntimeVersion; use zeitgeist_primitives::{constants::*, types::*}; -#[cfg(feature = "with-global-disputes")] -use frame_support::traits::LockIdentifier; - pub(crate) const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10); pub(crate) const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts( WEIGHT_REF_TIME_PER_SECOND.saturating_div(2), @@ -105,13 +102,37 @@ parameter_types! { pub ContractsSchedule: pallet_contracts::Schedule = Default::default(); // Court - /// Duration of a single court case. - pub const CourtCaseDuration: u64 = BLOCKS_PER_DAY; + /// (Slashable) Bond that is provided for overriding the last appeal. + /// This bond increases exponentially with the number of appeals. + /// Slashed in case the final outcome does match the appealed outcome for which the `AppealBond` + /// was deposited. + pub const AppealBond: Balance = 2000 * BASE; + /// The blocks per year required to calculate the yearly inflation for court incentivisation. + pub const BlocksPerYear: BlockNumber = BLOCKS_PER_YEAR; /// Pallet identifier, mainly used for named balance reserves. DO NOT CHANGE. pub const CourtPalletId: PalletId = COURT_PALLET_ID; - /// This value is multiplied by the current number of jurors to determine the stake - /// the juror has to pay. - pub const StakeWeight: u128 = 2 * BASE; + /// The time in which the jurors can cast their secret vote. + pub const CourtVotePeriod: BlockNumber = 3 * BLOCKS_PER_DAY; + /// The time in which the jurors should reveal their secret vote. + pub const CourtAggregationPeriod: BlockNumber = 3 * BLOCKS_PER_DAY; + /// The time in which a court case can get appealed. + pub const CourtAppealPeriod: BlockNumber = BLOCKS_PER_DAY; + /// The lock identifier for the court votes. + pub const CourtLockId: LockIdentifier = COURT_LOCK_ID; + /// The time in which the inflation is periodically issued. + pub const InflationPeriod: BlockNumber = 30 * BLOCKS_PER_DAY; + /// The maximum number of appeals until the court fails. + pub const MaxAppeals: u32 = 4; + /// The maximum number of delegations per juror account. + pub const MaxDelegations: u32 = 5; + /// The maximum number of randomly selected `MinJurorStake` draws / atoms of jurors for a dispute. + pub const MaxSelectedDraws: u32 = 510; + /// The maximum number of jurors / delegators that can be registered. + pub const MaxCourtParticipants: u32 = 1_000; + /// The minimum stake a user needs to reserve to become a juror. + pub const MinJurorStake: Balance = 500 * BASE; + /// The interval for requesting multiple court votes at once. + pub const RequestInterval: BlockNumber = 7 * BLOCKS_PER_DAY; // Democracy /// How often (in blocks) new public referenda are launched. @@ -179,11 +200,9 @@ parameter_types! { /// The percentage of the advisory bond that gets slashed when a market is rejected. pub const AdvisoryBondSlashPercentage: Percent = Percent::from_percent(0); /// (Slashable) Bond that is provided for disputing the outcome. - /// Slashed in case the final outcome does not match the dispute for which the `DisputeBond` - /// was deposited. + /// Unreserved in case the dispute was justified otherwise slashed. + /// This is when the resolved outcome is different to the default (reported) outcome. pub const DisputeBond: Balance = 2_000 * BASE; - /// `DisputeBond` is increased by this factor after every dispute. - pub const DisputeFactor: Balance = 2 * BASE; /// Maximum Categories a prediciton market can have (excluding base asset). pub const MaxCategories: u16 = MAX_CATEGORIES; /// Maximum block period for a dispute. @@ -250,6 +269,12 @@ parameter_types! { // Simple disputes parameters /// Pallet identifier, mainly used for named balance reserves. DO NOT CHANGE. pub const SimpleDisputesPalletId: PalletId = SD_PALLET_ID; + /// (Slashable) Bond that is provided for overriding the last outcome addition. + /// Slashed in case the final outcome does not match the dispute for which the `OutcomeBond` + /// was deposited. + pub const OutcomeBond: Balance = 2_000 * BASE; + /// `OutcomeBond` is increased by this factor after every new outcome addition. + pub const OutcomeFactor: Balance = 2 * BASE; // Swaps parameters /// A precentage from the withdrawal amount a liquidity provider wants to withdraw @@ -382,21 +407,22 @@ parameter_types! { WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); } -#[cfg(feature = "with-global-disputes")] parameter_types! { // Global Disputes + /// The time period in which the addition of new outcomes are allowed. + pub const AddOutcomePeriod: BlockNumber = BLOCKS_PER_DAY; /// Vote lock identifier, mainly used for the LockableCurrency on the native token. pub const GlobalDisputeLockId: LockIdentifier = GLOBAL_DISPUTES_LOCK_ID; /// Pallet identifier pub const GlobalDisputesPalletId: PalletId = GLOBAL_DISPUTES_PALLET_ID; - /// The period for a global dispute to end. - pub const GlobalDisputePeriod: BlockNumber = 7 * BLOCKS_PER_DAY; - /// The maximum number of owners for a voting outcome for private API calls of `push_voting_outcome`. + /// The maximum number of owners for a voting outcome for private API calls of `push_vote_outcome`. pub const MaxOwners: u32 = 10; /// The maximum number of market ids (participate in multiple different global disputes at the same time) for one account to vote on outcomes. pub const MaxGlobalDisputeVotes: u32 = 50; /// The minimum required amount to vote on an outcome. pub const MinOutcomeVoteAmount: Balance = 10 * BASE; + /// The time period in which votes are allowed. + pub const GdVotingPeriod: BlockNumber = 7 * BLOCKS_PER_DAY; /// The fee required to add a voting outcome. pub const VotingOutcomeFee: Balance = 200 * BASE; /// The remove limit for the Outcomes storage double map. diff --git a/scripts/benchmarks/configuration.sh b/scripts/benchmarks/configuration.sh index f8c9a9213..c4f71fe2c 100644 --- a/scripts/benchmarks/configuration.sh +++ b/scripts/benchmarks/configuration.sh @@ -43,5 +43,5 @@ else fi export EXECUTION="${EXECUTION:-wasm}" export ADDITIONAL_PARAMS="${ADDITIONAL:-}" -export ADDITIONAL_FEATURES="${ADDITIONAL_FEATURES:-with-global-disputes}" +export ADDITIONAL_FEATURES="${ADDITIONAL_FEATURES:-}" export HEADER="${HEADER:-./HEADER_GPL3}" diff --git a/scripts/benchmarks/quick_check.sh b/scripts/benchmarks/quick_check.sh index 73506b2e2..9ae754b68 100755 --- a/scripts/benchmarks/quick_check.sh +++ b/scripts/benchmarks/quick_check.sh @@ -25,7 +25,6 @@ export PROFILE=release export PROFILE_DIR=release export ADDITIONAL_PARAMS=--detailed-log-output export EXECUTION=native -# TODO(#848) Delete this, when global disputes is on main-net -export ADDITIONAL_FEATURES=with-global-disputes +export ADDITIONAL_FEATURES="" source ./scripts/benchmarks/run_benchmarks.sh diff --git a/zrml/authorized/src/authorized_pallet_api.rs b/zrml/authorized/src/authorized_pallet_api.rs index b9a1f4185..387fa6034 100644 --- a/zrml/authorized/src/authorized_pallet_api.rs +++ b/zrml/authorized/src/authorized_pallet_api.rs @@ -1,3 +1,4 @@ +// Copyright 2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -15,6 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -use zeitgeist_primitives::traits::DisputeApi; +use zeitgeist_primitives::traits::{DisputeApi, DisputeMaxWeightApi}; -pub trait AuthorizedPalletApi: DisputeApi {} +pub trait AuthorizedPalletApi: DisputeApi + DisputeMaxWeightApi {} diff --git a/zrml/authorized/src/benchmarks.rs b/zrml/authorized/src/benchmarks.rs index 77bd884b1..db649941b 100644 --- a/zrml/authorized/src/benchmarks.rs +++ b/zrml/authorized/src/benchmarks.rs @@ -22,17 +22,18 @@ )] #![cfg(feature = "runtime-benchmarks")] -#[cfg(test)] -use crate::Pallet as Authorized; -use crate::{market_mock, AuthorizedOutcomeReports, Call, Config, Pallet}; +use crate::{ + market_mock, AuthorizedOutcomeReports, Call, Config, NegativeImbalanceOf, Pallet as Authorized, + Pallet, +}; use frame_benchmarking::benchmarks; use frame_support::{ dispatch::UnfilteredDispatchable, - traits::{EnsureOrigin, Get}, + traits::{EnsureOrigin, Get, Imbalance}, }; use sp_runtime::traits::Saturating; use zeitgeist_primitives::{ - traits::DisputeResolutionApi, + traits::{DisputeApi, DisputeResolutionApi}, types::{AuthorityReport, OutcomeReport}, }; use zrml_market_commons::MarketCommonsPalletApi; @@ -96,6 +97,72 @@ benchmarks! { assert_eq!(AuthorizedOutcomeReports::::get(market_id).unwrap(), report); } + on_dispute_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + }: { + Authorized::::on_dispute(&market_id, &market).unwrap(); + } + + on_resolution_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + let report = AuthorityReport { resolve_at: 0u32.into(), outcome: OutcomeReport::Scalar(0) }; + AuthorizedOutcomeReports::::insert(market_id, report); + }: { + Authorized::::on_resolution(&market_id, &market).unwrap(); + } + + exchange_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + let outcome = OutcomeReport::Scalar(0); + let imb = NegativeImbalanceOf::::zero(); + }: { + Authorized::::exchange(&market_id, &market, &outcome, imb).unwrap(); + } + + get_auto_resolve_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + let report = AuthorityReport { resolve_at: 0u32.into(), outcome: OutcomeReport::Scalar(0) }; + AuthorizedOutcomeReports::::insert(market_id, report); + }: { + Authorized::::get_auto_resolve(&market_id, &market); + } + + has_failed_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + }: { + Authorized::::has_failed(&market_id, &market).unwrap(); + } + + on_global_dispute_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + }: { + Authorized::::on_global_dispute(&market_id, &market).unwrap(); + } + + clear_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + let report = AuthorityReport { resolve_at: 0u32.into(), outcome: OutcomeReport::Scalar(0) }; + AuthorizedOutcomeReports::::insert(market_id, report); + }: { + Authorized::::clear(&market_id, &market).unwrap(); + } + impl_benchmark_test_suite!( Authorized, crate::mock::ExtBuilder::default().build(), diff --git a/zrml/authorized/src/lib.rs b/zrml/authorized/src/lib.rs index 10bb0f8fe..1dc53f750 100644 --- a/zrml/authorized/src/lib.rs +++ b/zrml/authorized/src/lib.rs @@ -35,21 +35,22 @@ pub use pallet::*; #[frame_support::pallet] mod pallet { use crate::{weights::WeightInfoZeitgeist, AuthorizedPalletApi}; + use alloc::vec::Vec; use core::marker::PhantomData; use frame_support::{ - dispatch::{DispatchResult, DispatchResultWithPostInfo}, + dispatch::DispatchResultWithPostInfo, ensure, - pallet_prelude::{ConstU32, EnsureOrigin, OptionQuery, StorageMap}, + pallet_prelude::{ConstU32, EnsureOrigin, OptionQuery, StorageMap, Weight}, traits::{Currency, Get, Hooks, IsType, StorageVersion}, PalletId, Twox64Concat, }; use frame_system::pallet_prelude::OriginFor; use sp_runtime::{traits::Saturating, DispatchError}; use zeitgeist_primitives::{ - traits::{DisputeApi, DisputeResolutionApi}, + traits::{DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi}, types::{ - Asset, AuthorityReport, Market, MarketDispute, MarketDisputeMechanism, MarketStatus, - OutcomeReport, + Asset, AuthorityReport, GlobalDisputeItem, Market, MarketDisputeMechanism, + MarketStatus, OutcomeReport, ResultWithWeightInfo, }, }; use zrml_market_commons::MarketCommonsPalletApi; @@ -61,6 +62,8 @@ mod pallet { as Currency<::AccountId>>::Balance; pub(crate) type CurrencyOf = <::MarketCommons as MarketCommonsPalletApi>::Currency; + pub(crate) type NegativeImbalanceOf = + as Currency<::AccountId>>::NegativeImbalance; pub(crate) type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; @@ -101,16 +104,21 @@ mod pallet { let report_opt = AuthorizedOutcomeReports::::get(market_id); let (report, ids_len) = match &report_opt { - Some(report) => (AuthorityReport { resolve_at: report.resolve_at, outcome }, 0u32), + Some(report) => ( + AuthorityReport { resolve_at: report.resolve_at, outcome: outcome.clone() }, + 0u32, + ), None => { let resolve_at = now.saturating_add(T::CorrectionPeriod::get()); let ids_len = T::DisputeResolution::add_auto_resolve(&market_id, resolve_at)?; - (AuthorityReport { resolve_at, outcome }, ids_len) + (AuthorityReport { resolve_at, outcome: outcome.clone() }, ids_len) } }; AuthorizedOutcomeReports::::insert(market_id, report); + Self::deposit_event(Event::AuthorityReported { market_id, outcome }); + if report_opt.is_none() { Ok(Some(T::WeightInfo::authorize_market_outcome_first_report(ids_len)).into()) } else { @@ -155,16 +163,19 @@ mod pallet { MarketDoesNotHaveDisputeMechanismAuthorized, /// An account attempts to submit a report to an undisputed market. MarketIsNotDisputed, - /// Only one dispute is allowed. - OnlyOneDisputeAllowed, /// The report does not match the market's type. OutcomeMismatch, } #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event where - T: Config, {} + T: Config, + { + /// The Authority reported. + AuthorityReported { market_id: MarketIdOf, outcome: OutcomeReport }, + } #[pallet::hooks] impl Hooks for Pallet {} @@ -183,66 +194,170 @@ mod pallet { } } + impl DisputeMaxWeightApi for Pallet + where + T: Config, + { + fn on_dispute_max_weight() -> Weight { + T::WeightInfo::on_dispute_weight() + } + + fn on_resolution_max_weight() -> Weight { + T::WeightInfo::on_resolution_weight() + } + + fn exchange_max_weight() -> Weight { + T::WeightInfo::exchange_weight() + } + + fn get_auto_resolve_max_weight() -> Weight { + T::WeightInfo::get_auto_resolve_weight() + } + + fn has_failed_max_weight() -> Weight { + T::WeightInfo::has_failed_weight() + } + + fn on_global_dispute_max_weight() -> Weight { + T::WeightInfo::on_global_dispute_weight() + } + + fn clear_max_weight() -> Weight { + T::WeightInfo::clear_weight() + } + } + impl DisputeApi for Pallet where T: Config, { type AccountId = T::AccountId; type Balance = BalanceOf; + type NegativeImbalance = NegativeImbalanceOf; type BlockNumber = T::BlockNumber; type MarketId = MarketIdOf; type Moment = MomentOf; type Origin = T::RuntimeOrigin; fn on_dispute( - disputes: &[MarketDispute], _: &Self::MarketId, market: &MarketOf, - ) -> DispatchResult { + ) -> Result, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Authorized, Error::::MarketDoesNotHaveDisputeMechanismAuthorized ); - ensure!(disputes.is_empty(), Error::::OnlyOneDisputeAllowed); - Ok(()) + + let res = + ResultWithWeightInfo { result: (), weight: T::WeightInfo::on_dispute_weight() }; + + Ok(res) } fn on_resolution( - _: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOf, - ) -> Result, DispatchError> { + ) -> Result>, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Authorized, Error::::MarketDoesNotHaveDisputeMechanismAuthorized ); let report = AuthorizedOutcomeReports::::take(market_id); - Ok(report.map(|r| r.outcome)) + + let res = ResultWithWeightInfo { + result: report.map(|r| r.outcome), + weight: T::WeightInfo::on_resolution_weight(), + }; + + Ok(res) + } + + fn exchange( + _: &Self::MarketId, + market: &MarketOf, + _: &OutcomeReport, + overall_imbalance: NegativeImbalanceOf, + ) -> Result>, DispatchError> { + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::Authorized, + Error::::MarketDoesNotHaveDisputeMechanismAuthorized + ); + // all funds to treasury + let res = ResultWithWeightInfo { + result: overall_imbalance, + weight: T::WeightInfo::exchange_weight(), + }; + + Ok(res) } fn get_auto_resolve( - _: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOf, - ) -> Result, DispatchError> { + ) -> ResultWithWeightInfo> { + let mut res = ResultWithWeightInfo { + result: None, + weight: T::WeightInfo::get_auto_resolve_weight(), + }; + + if market.dispute_mechanism != MarketDisputeMechanism::Authorized { + return res; + } + + res.result = Self::get_auto_resolve(market_id); + + res + } + + fn has_failed( + _: &Self::MarketId, + market: &MarketOf, + ) -> Result, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Authorized, Error::::MarketDoesNotHaveDisputeMechanismAuthorized ); - Ok(Self::get_auto_resolve(market_id)) + + let res = + ResultWithWeightInfo { result: false, weight: T::WeightInfo::has_failed_weight() }; + + Ok(res) } - fn has_failed( - _: &[MarketDispute], + fn on_global_dispute( _: &Self::MarketId, market: &MarketOf, - ) -> Result { + ) -> Result< + ResultWithWeightInfo>>, + DispatchError, + > { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Authorized, Error::::MarketDoesNotHaveDisputeMechanismAuthorized ); - Ok(false) + let res = ResultWithWeightInfo { + result: Vec::new(), + weight: T::WeightInfo::on_global_dispute_weight(), + }; + + Ok(res) + } + + fn clear( + market_id: &Self::MarketId, + market: &MarketOf, + ) -> Result, DispatchError> { + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::Authorized, + Error::::MarketDoesNotHaveDisputeMechanismAuthorized + ); + + AuthorizedOutcomeReports::::remove(market_id); + + let res = ResultWithWeightInfo { result: (), weight: T::WeightInfo::clear_weight() }; + + Ok(res) } } diff --git a/zrml/authorized/src/mock.rs b/zrml/authorized/src/mock.rs index f34cdc7bc..529e27534 100644 --- a/zrml/authorized/src/mock.rs +++ b/zrml/authorized/src/mock.rs @@ -26,7 +26,6 @@ use frame_support::{ construct_runtime, ord_parameter_types, pallet_prelude::{DispatchError, Weight}, traits::Everything, - BoundedVec, }; use frame_system::EnsureSignedBy; use sp_runtime::{ @@ -40,8 +39,8 @@ use zeitgeist_primitives::{ }, traits::DisputeResolutionApi, types::{ - AccountIdTest, Asset, Balance, BlockNumber, BlockTest, Hash, Index, Market, MarketDispute, - MarketId, Moment, OutcomeReport, UncheckedExtrinsicTest, + AccountIdTest, Asset, Balance, BlockNumber, BlockTest, Hash, Index, Market, MarketId, + Moment, UncheckedExtrinsicTest, }, }; @@ -68,7 +67,6 @@ construct_runtime!( ord_parameter_types! { pub const AuthorizedDisputeResolutionUser: AccountIdTest = ALICE; - pub const MaxDisputes: u32 = 64; } // MockResolution implements DisputeResolutionApi with no-ops. @@ -79,7 +77,6 @@ impl DisputeResolutionApi for MockResolution { type Balance = Balance; type BlockNumber = BlockNumber; type MarketId = MarketId; - type MaxDisputes = MaxDisputes; type Moment = Moment; fn resolve( @@ -119,17 +116,6 @@ impl DisputeResolutionApi for MockResolution { ids.len() as u32 }) } - - fn get_disputes( - _market_id: &Self::MarketId, - ) -> BoundedVec, Self::MaxDisputes> { - BoundedVec::try_from(vec![MarketDispute { - at: 42u64, - by: BOB, - outcome: OutcomeReport::Scalar(42), - }]) - .unwrap() - } } impl crate::Config for Runtime { diff --git a/zrml/authorized/src/tests.rs b/zrml/authorized/src/tests.rs index 3682ac5e1..018db50eb 100644 --- a/zrml/authorized/src/tests.rs +++ b/zrml/authorized/src/tests.rs @@ -27,7 +27,7 @@ use crate::{ use frame_support::{assert_noop, assert_ok, dispatch::DispatchError}; use zeitgeist_primitives::{ traits::DisputeApi, - types::{AuthorityReport, MarketDispute, MarketDisputeMechanism, MarketStatus, OutcomeReport}, + types::{AuthorityReport, MarketDisputeMechanism, MarketStatus, OutcomeReport}, }; use zrml_market_commons::Markets; @@ -161,22 +161,10 @@ fn authorize_market_outcome_fails_on_unauthorized_account() { }); } -#[test] -fn on_dispute_fails_if_disputes_is_not_empty() { - ExtBuilder::default().build().execute_with(|| { - let dispute = - MarketDispute { by: crate::mock::ALICE, at: 0, outcome: OutcomeReport::Scalar(1) }; - assert_noop!( - Authorized::on_dispute(&[dispute], &0, &market_mock::()), - Error::::OnlyOneDisputeAllowed - ); - }); -} - #[test] fn on_resolution_fails_if_no_report_was_submitted() { ExtBuilder::default().build().execute_with(|| { - let report = Authorized::on_resolution(&[], &0, &market_mock::()).unwrap(); + let report = Authorized::on_resolution(&0, &market_mock::()).unwrap().result; assert!(report.is_none()); }); } @@ -191,7 +179,7 @@ fn on_resolution_removes_stored_outcomes() { 0, OutcomeReport::Scalar(2) )); - assert_ok!(Authorized::on_resolution(&[], &0, &market)); + assert_ok!(Authorized::on_resolution(&0, &market)); assert_eq!(AuthorizedOutcomeReports::::get(0), None); }); } @@ -213,7 +201,7 @@ fn on_resolution_returns_the_reported_outcome() { OutcomeReport::Scalar(2) )); assert_eq!( - Authorized::on_resolution(&[], &0, &market).unwrap(), + Authorized::on_resolution(&0, &market).unwrap().result, Some(OutcomeReport::Scalar(2)) ); }); @@ -260,7 +248,7 @@ fn get_auto_resolve_works() { )); let now = frame_system::Pallet::::block_number(); let resolve_at = now + ::CorrectionPeriod::get(); - assert_eq!(Authorized::get_auto_resolve(&[], &0, &market).unwrap(), Some(resolve_at),); + assert_eq!(Authorized::get_auto_resolve(&0, &market).result, Some(resolve_at),); }); } @@ -268,6 +256,6 @@ fn get_auto_resolve_works() { fn get_auto_resolve_returns_none_without_market_storage() { ExtBuilder::default().build().execute_with(|| { let market = market_mock::(); - assert_eq!(Authorized::get_auto_resolve(&[], &0, &market).unwrap(), None,); + assert_eq!(Authorized::get_auto_resolve(&0, &market).result, None,); }); } diff --git a/zrml/authorized/src/weights.rs b/zrml/authorized/src/weights.rs index c76b1faf8..8cd6968ff 100644 --- a/zrml/authorized/src/weights.rs +++ b/zrml/authorized/src/weights.rs @@ -48,6 +48,13 @@ use frame_support::{traits::Get, weights::Weight}; pub trait WeightInfoZeitgeist { fn authorize_market_outcome_first_report(m: u32) -> Weight; fn authorize_market_outcome_existing_report() -> Weight; + fn on_dispute_weight() -> Weight; + fn on_resolution_weight() -> Weight; + fn exchange_weight() -> Weight; + fn get_auto_resolve_weight() -> Weight; + fn has_failed_weight() -> Weight; + fn on_global_dispute_weight() -> Weight; + fn clear_weight() -> Weight; } /// Weight functions for zrml_authorized (automatically generated) @@ -70,4 +77,25 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + fn on_dispute_weight() -> Weight { + Weight::from_ref_time(0) + } + fn on_resolution_weight() -> Weight { + Weight::from_ref_time(0) + } + fn exchange_weight() -> Weight { + Weight::from_ref_time(0) + } + fn get_auto_resolve_weight() -> Weight { + Weight::from_ref_time(0) + } + fn has_failed_weight() -> Weight { + Weight::from_ref_time(0) + } + fn on_global_dispute_weight() -> Weight { + Weight::from_ref_time(0) + } + fn clear_weight() -> Weight { + Weight::from_ref_time(0) + } } diff --git a/zrml/court/Cargo.toml b/zrml/court/Cargo.toml index fd7b589df..d40612540 100644 --- a/zrml/court/Cargo.toml +++ b/zrml/court/Cargo.toml @@ -5,7 +5,9 @@ frame-support = { workspace = true } frame-system = { workspace = true } parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] } rand = { workspace = true, features = ["alloc", "std_rng"] } +rand_chacha = { workspace = true } scale-info = { workspace = true, features = ["derive"] } +sp-arithmetic = { workspace = true } sp-runtime = { workspace = true } zeitgeist-primitives = { workspace = true } zrml-market-commons = { workspace = true } @@ -14,9 +16,12 @@ zrml-market-commons = { workspace = true } pallet-balances = { workspace = true, features = ["default"] } pallet-randomness-collective-flip = { workspace = true, features = ["default"] } pallet-timestamp = { workspace = true, features = ["default"] } +pallet-treasury = { workspace = true, features = ["default"] } sp-io = { workspace = true, features = ["default"] } zeitgeist-primitives = { workspace = true, features = ["mock", "default"] } +test-case = { workspace = true } + [features] default = ["std"] runtime-benchmarks = [ diff --git a/zrml/court/README.md b/zrml/court/README.md index 53bbf35ec..ae8364d60 100644 --- a/zrml/court/README.md +++ b/zrml/court/README.md @@ -1 +1,56 @@ -# Court Module +# Court + +A pallet for stake-weighted plurality decision making. + +- [`Call`]() +- [`Config`]() +- [`Error`]() +- [`Event`]() + +## Overview + +Court is a market dispute resolution mechanism. It allows jurors to discover the truth. +If a juror does not vote with the plurality of the other jurors, the juror will be punished, +while those who did vote with the plurality will be rewarded. + +## Terminology + +- **Aggregation Period:** The period in which the actively participating jurors + need to reveal their vote secrets. +- **Appeal Period:** The period in which the jurors can appeal the decision of + the last court round. +- **Court:** The court is a dispute resolution mechanism to find the resolution + outcome of a market. +- **Delegator:** A delegator is a court participant who delegates their voting + power to an actively participating juror. +- **Juror:** A juror is a court participant who votes inside court cases. +- **Reveal Period / Aggregation Period:** The period in which the actively + participating jurors need to reveal their vote secrets. + +## Interface + +### Dispatches + +#### Public Dispatches + +- `join_court` - Join the court with a stake to become a juror in order to get + the stake-weighted chance to be selected for decision making. +- `delegate` - Join the court with a stake to become a delegator in order to + delegate the voting power to actively participating jurors. +- `prepare_exit_court` - Prepare as a court participant to leave the court + system. +- `exit_court` - Exit the court system in order to get the stake back. +- `vote` - An actively participating juror votes secretely on a specific court + case, in which the juror got selected. +- `denounce_vote` - Denounce a selected and active juror, if the secret and vote + is known before the actual reveal period. +- `reveal_vote` - An actively participating juror reveals the previously casted + secret vote. +- `appeal` - After the reveal phase (aggregation period), the jurors decision + can be appealed. +- `reassign_juror_stakes` - After the appeal period is over, losers pay the + winners for the jurors and delegators. + +#### `MonetaryGovernanceOrigin` Dispatches + +- `set_inflation` - Set the yearly inflation rate of the court system. diff --git a/zrml/court/src/benchmarks.rs b/zrml/court/src/benchmarks.rs index 829695001..d18860390 100644 --- a/zrml/court/src/benchmarks.rs +++ b/zrml/court/src/benchmarks.rs @@ -22,49 +22,716 @@ )] #![cfg(feature = "runtime-benchmarks")] -#[cfg(test)] -use crate::Pallet as Court; -use crate::{BalanceOf, Call, Config, CurrencyOf, Pallet}; -use frame_benchmarking::{benchmarks, whitelisted_caller}; -use frame_support::{dispatch::UnfilteredDispatchable, traits::Currency}; +extern crate alloc; +use crate::{ + types::{CourtParticipantInfo, CourtPoolItem, CourtStatus, Draw, Vote}, + AppealInfo, BalanceOf, Call, Config, CourtId, CourtPool, Courts, DelegatedStakesOf, + MarketIdToCourtId, MarketOf, Pallet as Court, Pallet, Participants, RequestBlock, + SelectedDraws, VoteItem, +}; +use alloc::{vec, vec::Vec}; +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_support::traits::{Currency, Get, NamedReservableCurrency}; use frame_system::RawOrigin; -use sp_runtime::traits::Bounded; -use zeitgeist_primitives::types::OutcomeReport; +use sp_arithmetic::Perbill; +use sp_runtime::{ + traits::{Bounded, Hash, Saturating, StaticLookup, Zero}, + SaturatedConversion, +}; +use zeitgeist_primitives::{ + traits::{DisputeApi, DisputeResolutionApi}, + types::{ + Asset, Deadlines, Market, MarketBonds, MarketCreation, MarketDisputeMechanism, + MarketPeriod, MarketStatus, MarketType, OutcomeReport, Report, ScoringRule, + }, +}; +use zrml_market_commons::MarketCommonsPalletApi; + +const ORACLE_REPORT: OutcomeReport = OutcomeReport::Scalar(u128::MAX); + +fn get_market() -> MarketOf +where + T: Config, +{ + Market { + base_asset: Asset::Ztg, + creation: MarketCreation::Permissionless, + creator_fee: 0, + creator: account("creator", 0, 0), + market_type: MarketType::Scalar(0..=100), + dispute_mechanism: MarketDisputeMechanism::Court, + metadata: vec![], + oracle: account("oracle", 0, 0), + period: MarketPeriod::Block( + 0u64.saturated_into::()..100u64.saturated_into::(), + ), + deadlines: Deadlines { + grace_period: 1_u64.saturated_into::(), + oracle_duration: 1_u64.saturated_into::(), + dispute_duration: 1_u64.saturated_into::(), + }, + report: Some(Report { + at: 1u64.saturated_into::(), + by: account("oracle", 0, 0), + outcome: ORACLE_REPORT, + }), + resolved_outcome: None, + status: MarketStatus::Disputed, + scoring_rule: ScoringRule::CPMM, + bonds: MarketBonds { creation: None, oracle: None, outsider: None, dispute: None }, + } +} fn deposit(caller: &T::AccountId) where T: Config, { - let _ = CurrencyOf::::deposit_creating(caller, BalanceOf::::max_value()); + let _ = T::Currency::deposit_creating( + caller, + BalanceOf::::max_value() / BalanceOf::::from(2u8), + ); +} + +fn fill_pool(number: u32) -> Result<(), &'static str> +where + T: Config, +{ + let mut pool = >::get(); + let min_amount = T::MinJurorStake::get(); + let max_amount = min_amount + min_amount + BalanceOf::::from(number); + let joined_at = >::block_number(); + for i in 0..number { + let juror: T::AccountId = account("juror", i, 0); + let stake = max_amount - BalanceOf::::from(i); + let _ = T::Currency::deposit_creating(&juror, stake); + >::insert( + juror.clone(), + CourtParticipantInfo { + stake, + active_lock: >::zero(), + prepare_exit_at: None, + delegations: Default::default(), + }, + ); + let consumed_stake = BalanceOf::::zero(); + let pool_item = + CourtPoolItem { stake, court_participant: juror.clone(), consumed_stake, joined_at }; + match pool.binary_search_by_key(&(stake, &juror), |pool_item| { + (pool_item.stake, &pool_item.court_participant) + }) { + Ok(_) => panic!("Juror already in pool"), + Err(index) => pool.try_insert(index, pool_item).unwrap(), + }; + } + >::put(pool); + Ok(()) +} + +// assume always worst case for delegations (MaxDelegations), +// because delegations are individual to each juror +fn fill_delegations() +where + T: Config, +{ + let pool = >::get(); + debug_assert!(pool.len() >= T::MaxDelegations::get() as usize); + let mut pool_iter = pool.iter(); + let mut delegated_jurors = vec![]; + for _ in 0..T::MaxDelegations::get() { + let delegated_juror = pool_iter.next().unwrap().court_participant.clone(); + delegated_jurors.push(delegated_juror); + } + for pool_item in pool_iter { + let juror = &pool_item.court_participant; + let mut j = >::get(juror).unwrap(); + j.delegations = Some(delegated_jurors.clone().try_into().unwrap()); + >::insert(juror, j); + } } -fn deposit_and_join_court(caller: &T::AccountId) +fn join_with_min_stake(caller: &T::AccountId) -> Result<(), &'static str> where T: Config, { + let stake = T::MinJurorStake::get(); deposit::(caller); - Call::::join_court {} - .dispatch_bypass_filter(RawOrigin::Signed(caller.clone()).into()) - .unwrap(); + Court::::join_court(RawOrigin::Signed(caller.clone()).into(), stake)?; + Ok(()) +} + +fn setup_court() -> Result<(crate::MarketIdOf, CourtId), &'static str> +where + T: Config, +{ + >::set_block_number(1u64.saturated_into::()); + + let now = >::block_number(); + >::put(now + 1u64.saturated_into::()); + + let market_id = T::MarketCommons::push_market(get_market::()).unwrap(); + Court::::on_dispute(&market_id, &get_market::()).unwrap(); + + let court_id = >::get(market_id).unwrap(); + + Ok((market_id, court_id)) +} + +fn fill_draws(court_id: CourtId, number: u32) -> Result<(), &'static str> +where + T: Config, +{ + // remove last random selections of on_dispute + >::remove(court_id); + let mut draws = >::get(court_id); + for i in 0..number { + let juror = account("juror", i, 0); + deposit::(&juror); + >::insert( + &juror, + CourtParticipantInfo { + stake: T::MinJurorStake::get(), + active_lock: T::MinJurorStake::get(), + prepare_exit_at: None, + delegations: Default::default(), + }, + ); + let draw = Draw { + court_participant: juror, + vote: Vote::Drawn, + weight: 1u32, + slashable: T::MinJurorStake::get(), + }; + let index = draws + .binary_search_by_key(&draw.court_participant, |draw| draw.court_participant.clone()) + .unwrap_or_else(|j| j); + draws.try_insert(index, draw).unwrap(); + } + >::insert(court_id, draws); + Ok(()) +} + +fn apply_revealed_draws(court_id: CourtId) +where + T: Config, +{ + let winner_outcome = OutcomeReport::Scalar(0u128); + let mut draws = >::get(court_id); + // change draws to have revealed votes + for draw in draws.iter_mut() { + let salt = Default::default(); + let commitment = + T::Hashing::hash_of(&(draw.court_participant.clone(), winner_outcome.clone(), salt)); + draw.vote = Vote::Revealed { + commitment, + vote_item: VoteItem::Outcome(winner_outcome.clone()), + salt, + }; + } + >::insert(court_id, draws); } benchmarks! { - exit_court { + join_court { + let j in 0..(T::MaxCourtParticipants::get() - 1); + + fill_pool::(j)?; + let caller: T::AccountId = whitelisted_caller(); - deposit_and_join_court::(&caller); - }: _(RawOrigin::Signed(caller)) + join_with_min_stake::(&caller)?; + + let new_stake = T::MinJurorStake::get() + .saturating_add(1u128.saturated_into::>()); + }: _(RawOrigin::Signed(caller), new_stake) + + delegate { + // jurors greater or equal to MaxDelegations, + // because we can not delegate to a non-existent juror + let j in 5..(T::MaxCourtParticipants::get() - 1); + let d in 1..T::MaxDelegations::get(); + + fill_pool::(j)?; - join_court { let caller: T::AccountId = whitelisted_caller(); - deposit::(&caller); + join_with_min_stake::(&caller)?; + + let juror_pool = >::get(); + let mut delegations = Vec::::new(); + juror_pool.iter() + .filter(|pool_item| pool_item.court_participant != caller).take(d as usize) + .for_each(|pool_item| delegations.push(pool_item.court_participant.clone())); + + let new_stake = T::MinJurorStake::get() + .saturating_add(1u128.saturated_into::>()); + }: _(RawOrigin::Signed(caller), new_stake, delegations) + + prepare_exit_court { + let j in 0..(T::MaxCourtParticipants::get() - 1); + + fill_pool::(j)?; + + let caller: T::AccountId = whitelisted_caller(); + join_with_min_stake::(&caller)?; }: _(RawOrigin::Signed(caller)) + exit_court_remove { + let caller: T::AccountId = whitelisted_caller(); + join_with_min_stake::(&caller)?; + + Court::::prepare_exit_court(RawOrigin::Signed(caller.clone()).into())?; + let now = >::block_number(); + >::set_block_number(now + T::InflationPeriod::get()); + + >::mutate(caller.clone(), |prev_p_info| { + prev_p_info.as_mut().unwrap().active_lock = >::zero(); + }); + + let juror = T::Lookup::unlookup(caller.clone()); + }: exit_court(RawOrigin::Signed(caller), juror) + + exit_court_set { + let caller: T::AccountId = whitelisted_caller(); + join_with_min_stake::(&caller)?; + + Court::::prepare_exit_court(RawOrigin::Signed(caller.clone()).into())?; + let now = >::block_number(); + >::set_block_number(now + T::InflationPeriod::get()); + + >::mutate(caller.clone(), |prev_p_info| { + prev_p_info.as_mut().unwrap().active_lock = T::MinJurorStake::get(); + }); + + let juror = T::Lookup::unlookup(caller.clone()); + }: exit_court(RawOrigin::Signed(caller), juror) + vote { + let d in 1..T::MaxSelectedDraws::get(); + + fill_pool::(T::MaxCourtParticipants::get() - 1)?; + + let caller: T::AccountId = whitelisted_caller(); + let (market_id, court_id) = setup_court::()?; + + let court = >::get(court_id).unwrap(); + let pre_vote = court.round_ends.pre_vote; + + fill_draws::(court_id, d)?; + + let mut draws = >::get(court_id); + let draws_len = draws.len(); + draws.remove(0); + let draw = Draw { + court_participant: caller.clone(), + vote: Vote::Drawn, + weight: 1u32, + slashable: >::zero(), + }; + let index = draws.binary_search_by_key(&caller, |draw| draw.court_participant.clone()).unwrap_or_else(|j| j); + draws.try_insert(index, draw).unwrap(); + >::insert(court_id, draws); + + >::set_block_number(pre_vote + 1u64.saturated_into::()); + + let commitment_vote = Default::default(); + }: _(RawOrigin::Signed(caller), court_id, commitment_vote) + + denounce_vote { + let d in 1..T::MaxSelectedDraws::get(); + + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + + let caller: T::AccountId = whitelisted_caller(); + let (market_id, court_id) = setup_court::()?; + + let court = >::get(court_id).unwrap(); + let pre_vote = court.round_ends.pre_vote; + + fill_draws::(court_id, d)?; + + let salt = Default::default(); + let outcome = OutcomeReport::Scalar(0u128); + let vote_item = VoteItem::Outcome(outcome); + let denounced_juror: T::AccountId = account("denounced_juror", 0, 0); + join_with_min_stake::(&denounced_juror)?; + >::insert(&denounced_juror, CourtParticipantInfo { + stake: T::MinJurorStake::get(), + active_lock: T::MinJurorStake::get(), + prepare_exit_at: None, + delegations: Default::default(), + }); + let denounced_juror_unlookup = T::Lookup::unlookup(denounced_juror.clone()); + let commitment = T::Hashing::hash_of(&(denounced_juror.clone(), vote_item.clone(), salt)); + + let mut draws = >::get(court_id); + draws.remove(0); + let draws_len = draws.len(); + let index = draws.binary_search_by_key(&denounced_juror, |draw| draw.court_participant.clone()).unwrap_or_else(|j| j); + let draw = Draw { + court_participant: denounced_juror, + vote: Vote::Secret { commitment }, + weight: 1u32, + slashable: T::MinJurorStake::get(), + }; + draws.try_insert(index, draw).unwrap(); + >::insert(court_id, draws); + + >::set_block_number(pre_vote + 1u64.saturated_into::()); + }: _(RawOrigin::Signed(caller), court_id, denounced_juror_unlookup, vote_item, salt) + + reveal_vote { + let d in 1..T::MaxSelectedDraws::get(); + + fill_pool::(T::MaxCourtParticipants::get() - 1)?; + + let caller: T::AccountId = whitelisted_caller(); + let (market_id, court_id) = setup_court::()?; + + let court = >::get(court_id).unwrap(); + let vote_end = court.round_ends.vote; + + fill_draws::(court_id, d)?; + + let salt = Default::default(); + let outcome = OutcomeReport::Scalar(0u128); + let vote_item = VoteItem::Outcome(outcome); + join_with_min_stake::(&caller)?; + >::insert(&caller, CourtParticipantInfo { + stake: T::MinJurorStake::get(), + active_lock: T::MinJurorStake::get(), + prepare_exit_at: None, + delegations: Default::default(), + }); + let commitment = T::Hashing::hash_of(&(caller.clone(), vote_item.clone(), salt)); + + let mut draws = >::get(court_id); + let draws_len = draws.len(); + draws.remove(0); + let index = draws.binary_search_by_key(&caller, |draw| draw.court_participant.clone()).unwrap_or_else(|j| j); + draws.try_insert(index, Draw { + court_participant: caller.clone(), + vote: Vote::Secret { commitment }, + weight: 1u32, + slashable: T::MinJurorStake::get(), + }).unwrap(); + >::insert(court_id, draws); + + >::set_block_number(vote_end + 1u64.saturated_into::()); + }: _(RawOrigin::Signed(caller), court_id, vote_item, salt) + + appeal { + // from 255 because in the last appeal round we need at least 255 jurors + let j in 255..T::MaxCourtParticipants::get(); + let a in 0..(T::MaxAppeals::get() - 2); + // the number of market ids inside MarketIdsPerCloseBlock at the old appeal end block + let r in 0..62; + // the number of market ids inside MarketIdsPerCloseBlock at the new appeal end block + let f in 0..62; + + let necessary_draws_weight = Court::::necessary_draws_weight((T::MaxAppeals::get() - 1) as usize); + debug_assert!(necessary_draws_weight == 255usize); + fill_pool::(j)?; + fill_delegations::(); + let caller: T::AccountId = whitelisted_caller(); - let market_id = Default::default(); - let outcome = OutcomeReport::Scalar(u128::MAX); - deposit_and_join_court::(&caller); - }: _(RawOrigin::Signed(caller), market_id, outcome) + deposit::(&caller); + let (market_id, court_id) = setup_court::()?; + + let mut court = >::get(court_id).unwrap(); + let appeal_end = court.round_ends.appeal; + for i in 0..r { + let market_id_i = (i + 100).saturated_into::>(); + T::DisputeResolution::add_auto_resolve(&market_id_i, appeal_end).unwrap(); + } + T::DisputeResolution::add_auto_resolve(&market_id, appeal_end).unwrap(); + + let aggregation = court.round_ends.aggregation; + for i in 0..a { + let appeal_info = AppealInfo { + backer: account("backer", i, 0), + bond: crate::get_appeal_bond::(i as usize), + appealed_vote_item: VoteItem::Outcome(OutcomeReport::Scalar(0u128)), + }; + court.appeals.try_push(appeal_info).unwrap(); + } + >::insert(court_id, court); + + let salt = Default::default(); + // remove last random selections of on_dispute + >::remove(court_id); + let mut draws = >::get(court_id); + let draws_len = Court::::necessary_draws_weight(a as usize) as u32; + for i in 0..draws_len { + let juror: T::AccountId = account("juror", i, 0); + >::insert(&juror, CourtParticipantInfo { + stake: T::MinJurorStake::get(), + active_lock: T::MinJurorStake::get(), + prepare_exit_at: None, + delegations: Default::default(), + }); + let vote_item: VoteItem = VoteItem::Outcome(OutcomeReport::Scalar(i as u128)); + let commitment = T::Hashing::hash_of(&(juror.clone(), vote_item.clone(), salt)); + let draw = + Draw { + court_participant: juror, + vote: Vote::Revealed { commitment, vote_item, salt }, + weight: 1u32, + slashable: >::zero() + }; + draws.try_push(draw).unwrap(); + } + >::insert(court_id, draws); + + >::set_block_number(aggregation + 1u64.saturated_into::()); + let now = >::block_number(); + >::put(now + 1u64.saturated_into::()); + + let new_resolve_at = >::get() + + T::VotePeriod::get() + + T::AggregationPeriod::get() + + T::AppealPeriod::get(); + for i in 0..f { + let market_id_i = (i + 100).saturated_into::>(); + T::DisputeResolution::add_auto_resolve(&market_id_i, new_resolve_at).unwrap(); + } + }: _(RawOrigin::Signed(caller), court_id) + verify { + let court = >::get(court_id).unwrap(); + assert_eq!(court.round_ends.appeal, new_resolve_at); + } + + reassign_court_stakes { + // because we have 5 MaxDelegations + let d in 5..T::MaxSelectedDraws::get(); + debug_assert!(T::MaxDelegations::get() < T::MaxSelectedDraws::get()); + + // just to initialize the court + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + + let caller: T::AccountId = whitelisted_caller(); + let (market_id, court_id) = setup_court::()?; + + let mut court = >::get(court_id).unwrap(); + let winner_outcome = OutcomeReport::Scalar(0u128); + let wrong_outcome = OutcomeReport::Scalar(1u128); + let winner_vote_item = VoteItem::Outcome(winner_outcome); + let wrong_vote_item = VoteItem::Outcome(wrong_outcome); + court.status = CourtStatus::Closed { winner: winner_vote_item.clone() }; + >::insert(court_id, court); + + let salt = Default::default(); + // remove last random selections of on_dispute + >::remove(court_id); + let mut draws = >::get(court_id); + let mut delegated_stakes: DelegatedStakesOf = Default::default(); + for i in 0..d { + let juror: T::AccountId = account("juror_i", i, 0); + deposit::(&juror); + >::insert(&juror, CourtParticipantInfo { + stake: T::MinJurorStake::get(), + active_lock: T::MinJurorStake::get(), + prepare_exit_at: None, + delegations: Default::default(), + }); + let draw = if i < T::MaxDelegations::get() { + delegated_stakes.try_push((juror.clone(), T::MinJurorStake::get())).unwrap(); + + let vote_item: VoteItem = if i % 2 == 0 { + wrong_vote_item.clone() + } else { + winner_vote_item.clone() + }; + let commitment = T::Hashing::hash_of(&(juror.clone(), vote_item.clone(), salt)); + Draw { + court_participant: juror, + vote: Vote::Revealed { commitment, vote_item, salt }, + weight: 1u32, + slashable: T::MinJurorStake::get(), + } + } else { + Draw { + court_participant: juror, + vote: Vote::Delegated { delegated_stakes: delegated_stakes.clone() }, + weight: 1u32, + slashable: T::MinJurorStake::get(), + } + }; + draws.try_push(draw).unwrap(); + } + >::insert(court_id, draws); + }: _(RawOrigin::Signed(caller), court_id) + + set_inflation { + let inflation = Perbill::from_percent(10); + }: _(RawOrigin::Root, inflation) + + handle_inflation { + let j in 1..T::MaxCourtParticipants::get(); + fill_pool::(j)?; + + >::set_block_number(T::InflationPeriod::get()); + let now = >::block_number(); + }: { + Court::::handle_inflation(now); + } + + select_participants { + let a in 0..(T::MaxAppeals::get() - 1); + fill_pool::(T::MaxCourtParticipants::get())?; + + fill_delegations::(); + }: { + let _ = Court::::select_participants(a as usize).unwrap(); + } + + on_dispute { + let j in 31..T::MaxCourtParticipants::get(); + // the number of market ids inside MarketIdsPerCloseBlock at the appeal end block + let r in 0..62; + + let now = >::block_number(); + let pre_vote_end = now + 1u64.saturated_into::(); + >::put(pre_vote_end); + + let appeal_end = pre_vote_end + + T::VotePeriod::get() + + T::AggregationPeriod::get() + + T::AppealPeriod::get(); + + for i in 0..r { + let market_id_i = (i + 100).saturated_into::>(); + T::DisputeResolution::add_auto_resolve(&market_id_i, appeal_end).unwrap(); + } + + fill_pool::(j)?; + + let market_id = 0u32.into(); + let market = get_market::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + }: { + Court::::on_dispute(&market_id, &market).unwrap(); + } + + on_resolution { + let d in 1..T::MaxSelectedDraws::get(); + + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + + let (market_id, court_id) = setup_court::()?; + let market = get_market::(); + + fill_draws::(court_id, d)?; + + let winner_outcome = OutcomeReport::Scalar(0u128); + let mut draws = >::get(court_id); + // change draws to have revealed votes + for draw in draws.iter_mut() { + let salt = Default::default(); + let commitment = T::Hashing::hash_of(&(draw.court_participant.clone(), winner_outcome.clone(), salt)); + draw.vote = Vote::Revealed { + commitment, + vote_item: VoteItem::Outcome(winner_outcome.clone()), + salt, + }; + } + >::insert(court_id, draws); + }: { + Court::::on_resolution(&market_id, &market).unwrap(); + } + + exchange { + let a in 0..T::MaxAppeals::get(); + + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + let (market_id, court_id) = setup_court::()?; + let market = get_market::(); + + let mut court = >::get(court_id).unwrap(); + + let resolved_outcome = OutcomeReport::Scalar(0u128); + for i in 0..a { + let backer = account("backer", i, 0); + let bond = T::MinJurorStake::get(); + let _ = T::Currency::deposit_creating(&backer, bond); + T::Currency::reserve_named(&Court::::reserve_id(), &backer, bond).unwrap(); + let appeal_info = AppealInfo { + backer, + bond, + appealed_vote_item: VoteItem::Outcome(resolved_outcome.clone()), + }; + court.appeals.try_push(appeal_info).unwrap(); + } + >::insert(court_id, court); + }: { + Court::::exchange(&market_id, &market, &resolved_outcome, Default::default()).unwrap(); + } + + get_auto_resolve { + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + let (market_id, court_id) = setup_court::()?; + let market = get_market::(); + }: { + Court::::get_auto_resolve(&market_id, &market); + } + + has_failed { + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + let (market_id, court_id) = setup_court::()?; + let market = get_market::(); + }: { + Court::::has_failed(&market_id, &market).unwrap(); + } + + on_global_dispute { + let a in 0..T::MaxAppeals::get(); + let d in 1..T::MaxSelectedDraws::get(); + + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + let (market_id, court_id) = setup_court::()?; + let market = get_market::(); + + fill_draws::(court_id, d)?; + apply_revealed_draws::(court_id); + + let resolved_outcome = OutcomeReport::Scalar(0u128); + + let mut court = >::get(court_id).unwrap(); + for i in 0..a { + let backer = account("backer", i, 0); + let bond = T::MinJurorStake::get(); + let _ = T::Currency::deposit_creating(&backer, bond); + T::Currency::reserve_named(&Court::::reserve_id(), &backer, bond).unwrap(); + let appeal_info = AppealInfo { + backer, + bond, + appealed_vote_item: VoteItem::Outcome(resolved_outcome.clone()), + }; + court.appeals.try_push(appeal_info).unwrap(); + } + >::insert(court_id, court); + }: { + Court::::on_global_dispute(&market_id, &market).unwrap(); + } + + clear { + let d in 1..T::MaxSelectedDraws::get(); + + let necessary_draws_weight: usize = Court::::necessary_draws_weight(0usize); + fill_pool::(necessary_draws_weight as u32)?; + + let (market_id, court_id) = setup_court::()?; + let market = get_market::(); + + fill_draws::(court_id, d)?; + apply_revealed_draws::(court_id); + }: { + Court::::clear(&market_id, &market).unwrap(); + } impl_benchmark_test_suite!( Court, diff --git a/zrml/court/src/court_pallet_api.rs b/zrml/court/src/court_pallet_api.rs index 73b9e2829..feebd7c35 100644 --- a/zrml/court/src/court_pallet_api.rs +++ b/zrml/court/src/court_pallet_api.rs @@ -1,3 +1,4 @@ +// Copyright 2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -15,6 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -use zeitgeist_primitives::traits::DisputeApi; +use zeitgeist_primitives::traits::{DisputeApi, DisputeMaxWeightApi}; -pub trait CourtPalletApi: DisputeApi {} +pub trait CourtPalletApi: DisputeApi + DisputeMaxWeightApi {} diff --git a/zrml/court/src/juror.rs b/zrml/court/src/juror.rs deleted file mode 100644 index 699dc1443..000000000 --- a/zrml/court/src/juror.rs +++ /dev/null @@ -1,33 +0,0 @@ -// 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 . - -use crate::JurorStatus; - -// Structure currently has only one field but acts as a container for possible future additions. -#[derive( - parity_scale_codec::Decode, - parity_scale_codec::Encode, - parity_scale_codec::MaxEncodedLen, - scale_info::TypeInfo, - Clone, - Debug, - PartialEq, - Eq, -)] -pub struct Juror { - pub(crate) status: JurorStatus, -} diff --git a/zrml/court/src/juror_status.rs b/zrml/court/src/juror_status.rs deleted file mode 100644 index 6b59fdb42..000000000 --- a/zrml/court/src/juror_status.rs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 . - -#[derive( - parity_scale_codec::Decode, - parity_scale_codec::Encode, - parity_scale_codec::MaxEncodedLen, - scale_info::TypeInfo, - Clone, - Debug, - PartialEq, - Eq, -)] -pub enum JurorStatus { - Ok, - Tardy, -} diff --git a/zrml/court/src/lib.rs b/zrml/court/src/lib.rs index 6ff3e42b5..0a19a1576 100644 --- a/zrml/court/src/lib.rs +++ b/zrml/court/src/lib.rs @@ -16,227 +16,1776 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -// It is important to note that if a categorical market has only two outcomes, then winners -// won't receive any rewards because accounts of the most voted outcome on the loser side are -// simply registered as `JurorStatus::Tardy`. - #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::type_complexity)] extern crate alloc; +use crate::{ + weights::WeightInfoZeitgeist, AppealInfo, CourtId, CourtInfo, CourtParticipantInfo, + CourtPoolItem, CourtStatus, Draw, JurorVoteWithStakes, RawCommitment, RoundTiming, + SelectionAdd, SelectionError, SelectionValue, SelfInfo, Vote, VoteItem, VoteItemType, +}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; +use core::marker::PhantomData; +use frame_support::{ + dispatch::DispatchResult, + ensure, log, + pallet_prelude::{ + ConstU32, DispatchResultWithPostInfo, EnsureOrigin, Hooks, OptionQuery, StorageMap, + StorageValue, ValueQuery, Weight, + }, + traits::{ + Currency, Get, Imbalance, IsType, LockIdentifier, LockableCurrency, + NamedReservableCurrency, OnUnbalanced, Randomness, ReservableCurrency, StorageVersion, + WithdrawReasons, + }, + transactional, Blake2_128Concat, BoundedVec, PalletId, Twox64Concat, +}; +use frame_system::{ + ensure_signed, + pallet_prelude::{BlockNumberFor, OriginFor}, +}; +use rand::{seq::SliceRandom, Rng, RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use sp_arithmetic::{per_things::Perquintill, traits::One}; +use sp_runtime::{ + traits::{AccountIdConversion, Hash, Saturating, StaticLookup, Zero}, + DispatchError, Perbill, SaturatedConversion, +}; +use zeitgeist_primitives::{ + traits::{DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi}, + types::{ + Asset, GlobalDisputeItem, Market, MarketDisputeMechanism, MarketStatus, OutcomeReport, + ResultWithWeightInfo, + }, +}; +use zrml_market_commons::MarketCommonsPalletApi; + mod benchmarks; mod court_pallet_api; -mod juror; -mod juror_status; pub mod migrations; mod mock; +mod mock_storage; mod tests; +pub mod types; pub mod weights; pub use court_pallet_api::CourtPalletApi; -pub use juror::Juror; -pub use juror_status::JurorStatus; pub use pallet::*; +pub use types::*; #[frame_support::pallet] mod pallet { - use crate::{weights::WeightInfoZeitgeist, CourtPalletApi, Juror, JurorStatus}; - use alloc::{ - collections::{BTreeMap, BTreeSet}, - vec::Vec, - }; - use arrayvec::ArrayVec; - use core::marker::PhantomData; - use frame_support::{ - dispatch::DispatchResult, - ensure, - pallet_prelude::{CountedStorageMap, StorageDoubleMap, StorageValue, ValueQuery}, - traits::{ - BalanceStatus, Currency, Get, Hooks, IsType, NamedReservableCurrency, Randomness, - StorageVersion, - }, - Blake2_128Concat, PalletId, - }; - use frame_system::{ensure_signed, pallet_prelude::OriginFor}; - use rand::{rngs::StdRng, seq::SliceRandom, RngCore, SeedableRng}; - use sp_runtime::{ - traits::{AccountIdConversion, CheckedDiv, Saturating}, - ArithmeticError, DispatchError, SaturatedConversion, - }; - use zeitgeist_primitives::{ - traits::{DisputeApi, DisputeResolutionApi}, - types::{Asset, Market, MarketDispute, MarketDisputeMechanism, OutcomeReport}, - }; - use zrml_market_commons::MarketCommonsPalletApi; - - // Number of jurors for an initial market dispute - const INITIAL_JURORS_NUM: usize = 3; - const MAX_RANDOM_JURORS: usize = 13; + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The required base bond in order to get an appeal initiated. + /// This bond increases exponentially with the number of appeals. + #[pallet::constant] + type AppealBond: Get>; + + /// The expected blocks per year to calculate the inflation emission. + #[pallet::constant] + type BlocksPerYear: Get; + + /// The time in which the jurors can cast their commitment vote. + #[pallet::constant] + type VotePeriod: Get; + + /// The time in which the jurors should reveal their commitment vote. + #[pallet::constant] + type AggregationPeriod: Get; + + /// The time in which a court case can get appealed. + #[pallet::constant] + type AppealPeriod: Get; + + /// The court lock identifier. + #[pallet::constant] + type LockId: Get; + + /// Identifier of this pallet + #[pallet::constant] + type PalletId: Get; + + /// The currency implementation used to transfer, lock and reserve tokens. + type Currency: Currency + + NamedReservableCurrency + + LockableCurrency; + + /// The functionality to allow controlling the markets resolution time. + type DisputeResolution: DisputeResolutionApi< + AccountId = Self::AccountId, + BlockNumber = Self::BlockNumber, + MarketId = MarketIdOf, + Moment = MomentOf, + >; + + /// Event + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The inflation period in which new tokens are minted. + #[pallet::constant] + type InflationPeriod: Get; + + /// Market commons + type MarketCommons: MarketCommonsPalletApi< + AccountId = Self::AccountId, + BlockNumber = Self::BlockNumber, + Currency = Self::Currency, + >; + + /// The maximum number of appeals until a court fails. + #[pallet::constant] + type MaxAppeals: Get; + + /// The maximum number of randomly selected n * `MinJurorStake` (n equals all draw weights) + /// out of all jurors and delegators stake. This configuration parameter should be + /// the maximum necessary_draws_weight multiplied by 2. + /// Each `MinJurorStake` (draw weight) out of `n * MinJurorStake` belongs + /// to one juror or one delegator. + /// (necessary_draws_weight = 2^(appeals_len) * 31 + 2^(appeals_len) - 1) + /// Assume MaxAppeals - 1 (= 3), example: 2^3 * 31 + 2^3 - 1 = 255 + /// => 2 * 255 = 510 = `MaxSelectedDraws`. + /// Why the multiplication by two? + /// Because each draw weight is associated with one juror account id and + /// potentially a delegator account id. + #[pallet::constant] + type MaxSelectedDraws: Get; + + /// The maximum number of possible delegations. + #[pallet::constant] + type MaxDelegations: Get; + + /// The maximum number of jurors and delegators that can be registered. + #[pallet::constant] + type MaxCourtParticipants: Get; + + /// The minimum stake a user needs to lock to become a juror. + #[pallet::constant] + type MinJurorStake: Get>; + + /// The origin for monetary governance to control the court inflation. + type MonetaryGovernanceOrigin: EnsureOrigin; + + /// Randomness source + type Random: Randomness; + + /// The global interval which schedules the start of new court vote periods. + #[pallet::constant] + type RequestInterval: Get; + + /// Handler for slashed funds. + type Slash: OnUnbalanced>; + + /// The treasury pallet identifier. + #[pallet::constant] + type TreasuryPalletId: Get; + + /// Weights generated by benchmarks + type WeightInfo: WeightInfoZeitgeist; + } + + /// Number of draws for the initial court round. + const INITIAL_DRAWS_NUM: usize = 31; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); - // Weight used to increase the number of jurors for subsequent disputes - // of the same market - const SUBSEQUENT_JURORS_FACTOR: usize = 2; - // Divides the reserved juror balance to calculate the slash amount. `5` here - // means that the output value will be 20% of the dividend. - const TARDY_PUNISHMENT_DIVISOR: u8 = 5; - - pub(crate) type BalanceOf = - as Currency<::AccountId>>::Balance; - pub(crate) type CurrencyOf = - <::MarketCommons as MarketCommonsPalletApi>::Currency; + /// Weight used to increase the number of jurors for subsequent appeals + /// of the same court. + const APPEAL_BASIS: usize = 2; + /// Basis used to increase the bond for subsequent appeals of the same market. + const APPEAL_BOND_BASIS: u32 = 2; + + pub(crate) type AccountIdOf = ::AccountId; + pub(crate) type BalanceOf = <::Currency as Currency>>::Balance; + pub(crate) type NegativeImbalanceOf = + <::Currency as Currency>>::NegativeImbalance; pub(crate) type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; pub(crate) type MarketOf = Market< - ::AccountId, + AccountIdOf, BalanceOf, ::BlockNumber, MomentOf, Asset>, >; + pub(crate) type HashOf = ::Hash; + pub(crate) type AccountIdLookupOf = + <::Lookup as StaticLookup>::Source; + pub(crate) type CourtOf = CourtInfo<::BlockNumber, AppealsOf>; + pub(crate) type DelegatedStakesOf = + BoundedVec<(AccountIdOf, BalanceOf), ::MaxDelegations>; + pub(crate) type SelectionValueOf = SelectionValue, DelegatedStakesOf>; + pub(crate) type DelegationsOf = BoundedVec, ::MaxDelegations>; + pub(crate) type VoteOf = Vote, DelegatedStakesOf>; + pub(crate) type JurorVoteWithStakesOf = JurorVoteWithStakes, BalanceOf>; + pub(crate) type CourtParticipantInfoOf = + CourtParticipantInfo, BlockNumberFor, DelegationsOf>; + pub(crate) type CourtPoolItemOf = + CourtPoolItem, BalanceOf, BlockNumberFor>; + pub(crate) type CourtPoolOf = + BoundedVec, ::MaxCourtParticipants>; + pub(crate) type DrawOf = Draw, BalanceOf, HashOf, DelegatedStakesOf>; + pub(crate) type SelectedDrawsOf = BoundedVec, ::MaxSelectedDraws>; + pub(crate) type AppealOf = AppealInfo, BalanceOf>; + pub(crate) type AppealsOf = BoundedVec, ::MaxAppeals>; + pub(crate) type RawCommitmentOf = RawCommitment, HashOf>; + pub(crate) type CacheSize = ConstU32<64>; + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData); + + /// The pool of jurors and delegators who can get randomly selected according to their stake. + /// The pool is sorted by `stake` in ascending order [min, ..., max]. + #[pallet::storage] + pub type CourtPool = StorageValue<_, CourtPoolOf, ValueQuery>; + + /// The general information about each juror and delegator. + #[pallet::storage] + pub type Participants = + StorageMap<_, Blake2_128Concat, T::AccountId, CourtParticipantInfoOf, OptionQuery>; + + /// An extra layer of pseudo randomness so that we can generate a new random seed with it. + #[pallet::storage] + pub type SelectionNonce = StorageValue<_, u64, ValueQuery>; + + /// The randomly selected jurors and delegators, their vote weight, + /// the status about their vote and their selected and risked funds. + #[pallet::storage] + pub type SelectedDraws = + StorageMap<_, Blake2_128Concat, CourtId, SelectedDrawsOf, ValueQuery>; + + /// The general information about each court. + #[pallet::storage] + pub type Courts = StorageMap<_, Blake2_128Concat, CourtId, CourtOf, OptionQuery>; + + /// The next identifier for a new court. + #[pallet::storage] + pub type NextCourtId = StorageValue<_, CourtId, ValueQuery>; + + /// Mapping from market id to court id. + #[pallet::storage] + pub type MarketIdToCourtId = + StorageMap<_, Twox64Concat, MarketIdOf, CourtId, OptionQuery>; + + /// Mapping from court id to market id. + #[pallet::storage] + pub type CourtIdToMarketId = + StorageMap<_, Twox64Concat, CourtId, MarketIdOf, OptionQuery>; + + /// The future block number when jurors should start voting. + /// This is useful for the user experience of the jurors to vote for multiple courts at once. + #[pallet::storage] + pub type RequestBlock = StorageValue<_, T::BlockNumber, ValueQuery>; + + #[pallet::type_value] + pub fn DefaultYearlyInflation() -> Perbill { + Perbill::from_perthousand(20u32) + } + + /// The current inflation rate. + #[pallet::storage] + pub type YearlyInflation = + StorageValue<_, Perbill, ValueQuery, DefaultYearlyInflation>; + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event + where + T: Config, + { + /// A juror has been added to the court. + JurorJoined { juror: T::AccountId, stake: BalanceOf }, + /// A court participant prepared to exit the court. + ExitPrepared { court_participant: T::AccountId }, + /// A court participant has been removed from the court. + ExitedCourt { + court_participant: T::AccountId, + exit_amount: BalanceOf, + active_lock: BalanceOf, + }, + /// A juror has voted in a court. + JurorVoted { court_id: CourtId, juror: T::AccountId, commitment: T::Hash }, + /// A juror has revealed their vote. + JurorRevealedVote { + juror: T::AccountId, + court_id: CourtId, + vote_item: VoteItem, + salt: T::Hash, + }, + /// A juror vote has been denounced. + DenouncedJurorVote { + denouncer: T::AccountId, + juror: T::AccountId, + court_id: CourtId, + vote_item: VoteItem, + salt: T::Hash, + }, + /// A delegator has delegated their stake to jurors. + DelegatorJoined { + delegator: T::AccountId, + stake: BalanceOf, + delegated_jurors: Vec, + }, + /// A market has been appealed. + CourtAppealed { court_id: CourtId, appeal_number: u32 }, + /// A new token amount was minted for a court participant. + MintedInCourt { court_participant: T::AccountId, amount: BalanceOf }, + /// The juror and delegator stakes have been reassigned. The losing jurors have been slashed. + /// The winning jurors have been rewarded by the losers. + /// The losing jurors are those, who did not vote, + /// were denounced or did not reveal their vote. + StakesReassigned { court_id: CourtId }, + /// The yearly inflation rate has been set. + InflationSet { inflation: Perbill }, + } + + #[pallet::error] + pub enum Error { + /// An account id does not exist on the jurors storage. + JurorDoesNotExist, + /// On dispute or resolution, someone tried to pass a non-court market type. + MarketDoesNotHaveCourtMechanism, + /// The market is not in a state where it can be disputed. + MarketIsNotDisputed, + /// This operation requires the caller to be a juror or delegator. + CallerIsNotACourtParticipant, + /// The vote is not commitment. + VoteAlreadyRevealed, + /// The vote item and salt reveal do not match the commitment vote. + CommitmentHashMismatch, + /// No court for this market id was found. + CourtNotFound, + /// This operation is only allowed in the voting period. + NotInVotingPeriod, + /// This operation is only allowed in the aggregation period. + NotInAggregationPeriod, + /// The maximum number of appeals has been reached. + MaxAppealsReached, + /// This operation is only allowed in the appeal period. + NotInAppealPeriod, + /// The caller of this extrinsic needs to be drawn or in the commitment vote state. + InvalidVoteState, + /// The amount is below the minimum required stake. + BelowMinJurorStake, + /// The maximum number of possible jurors has been reached. + MaxCourtParticipantsReached, + /// In order to exit the court the juror has to exit + /// the pool first with `prepare_exit_court`. + AlreadyPreparedExit, + /// The juror was not randomly selected for the court. + JurorNotDrawn, + /// The juror was drawn but did not manage to commitmently vote within the court. + JurorDidNotVote, + /// The juror was already denounced. + VoteAlreadyDenounced, + /// A juror tried to denounce herself. + CallerDenouncedItself, + /// The court is not in the closed state. + CourtNotClosed, + /// The juror stakes of the court already got reassigned. + CourtAlreadyReassigned, + /// There are not enough jurors in the pool. + NotEnoughJurorsAndDelegatorsStake, + /// The report of the market was not found. + MarketReportNotFound, + /// The maximum number of court ids is reached. + MaxCourtIdReached, + /// The caller has not enough funds to join the court with the specified amount. + AmountExceedsBalance, + /// After the first join of the court the amount has to be equal or higher than the current stake. + /// This is to ensure the slashable amount in active court rounds + /// is still smaller or equal to the stake. + /// It is also necessary to calculate the `unconsumed` stake properly. + /// Otherwise a juror could just reduce the probability to get selected whenever they want. + /// But this has to be done by `prepare_exit_court` and `exit_court`. + /// Additionally, the `join_court` and `delegate` extrinsics + /// use `extend_lock` and not `set_lock` or `remove_lock`. + /// This means those extrinsics are not meant to get out, but only to get into the court. + AmountBelowLastJoin, + /// The amount is too low to kick the lowest juror out of the stake-weighted pool. + AmountBelowLowestJuror, + /// This should not happen, because the juror account should only be once in a pool. + CourtParticipantTwiceInPool, + /// The caller of this function is not part of the juror draws. + CallerNotInSelectedDraws, + /// The callers balance is lower than the appeal bond. + AppealBondExceedsBalance, + /// The juror should at least wait one inflation period after the funds can be unstaked. + /// Otherwise hopping in and out for inflation rewards is possible. + PrematureExit, + /// The `prepare_exit_at` field is not present. + PrepareExitAtNotPresent, + /// The maximum number of delegations is reached for this account. + MaxDelegationsReached, + /// The juror decided to be a delegator. + JurorDelegated, + /// A delegation to the own account is not possible. + SelfDelegationNotAllowed, + /// The set of delegations has to be distinct. + IdenticalDelegationsNotAllowed, + /// The call to `delegate` is not valid if no delegations are provided. + NoDelegations, + /// The set of delegations should contain only valid and active juror accounts. + DelegatedToInvalidJuror, + /// The market id to court id mapping was not found. + MarketIdToCourtIdNotFound, + /// The court id to market id mapping was not found. + CourtIdToMarketIdNotFound, + /// The vote item is not valid for this (outcome) court. + InvalidVoteItemForOutcomeCourt, + /// The vote item is not valid for this (binary) court. + InvalidVoteItemForBinaryCourt, + /// The appealed vote item is not an outcome. + AppealedVoteItemIsNoOutcome, + /// The winner vote item is not an outcome. + WinnerVoteItemIsNoOutcome, + /// The outcome does not match the market outcomes. + OutcomeMismatch, + /// The vote item was expected to be an outcome, but is actually not an outcome. + VoteItemIsNoOutcome, + } + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_initialize(now: T::BlockNumber) -> Weight { + let mut total_weight: Weight = Weight::zero(); + total_weight = total_weight.saturating_add(Self::handle_inflation(now)); + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); + if now >= >::get() { + let future_request = now.saturating_add(T::RequestInterval::get()); + >::put(future_request); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + } + total_weight + } + + fn integrity_test() { + assert!(!T::BlocksPerYear::get().is_zero(), "Blocks per year musn't be zero!"); + } + } #[pallet::call] impl Pallet { - // MARK(non-transactional): `remove_juror_from_all_courts_of_all_markets` is infallible. + /// Join to become a juror, who is able to get randomly selected + /// for court cases according to the provided stake. + /// If the juror is already part of the court, + /// the `amount` needs to be higher than the previous amount to update the juror stake. + /// If the juror gets selected for a court case, the juror has to vote and reveal the vote. + /// If the juror does not vote or reveal the vote, the juror gets slashed + /// by the selected multiple of `MinJurorStake` for the court. + /// The risked amount depends on the juror random selection algorithm, + /// but is at most (`MaxSelectedDraws` / 2) mulitplied by the `MinJurorStake` + /// for all jurors and delegators in one court. + /// Assume you get randomly selected on one of these `MinJurorStake`'s. + /// Then you risk at most `MinJurorStake` for this court. + /// The probability to get selected is higher the more funds are staked. + /// The `amount` of this call represents the total stake of the juror. + /// If the pool is full, the lowest staked court participant is removed from the court pool. + /// If the `amount` is lower than the lowest staked court participant, the call fails. + /// + /// # Arguments + /// + /// - `amount`: The total stake associated with the joining juror. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of jurors in the stake-weighted pool. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::exit_court())] - pub fn exit_court(origin: OriginFor) -> DispatchResult { + #[pallet::weight(T::WeightInfo::join_court(T::MaxCourtParticipants::get()))] + #[transactional] + pub fn join_court( + origin: OriginFor, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let juror = Self::juror(&who)?; - Self::remove_juror_from_all_courts_of_all_markets(&who); - Self::deposit_event(Event::ExitedJuror(who, juror)); - Ok(()) + + let jurors_len = Self::do_join_court(&who, amount, None)?; + + Self::deposit_event(Event::JurorJoined { juror: who, stake: amount }); + + Ok(Some(T::WeightInfo::join_court(jurors_len)).into()) } - // MARK(non-transactional): Once `reserve_named` is successful, `insert` won't fail. + /// Join the court to become a delegator. + /// If the random selection algorithm chooses a delegators stake, + /// the caller delegates the vote power to the drawn delegated juror. + /// The delegator gets slashed or rewarded according to the delegated juror's decisions. + /// If the delegator is already part of the court, + /// the `amount` needs to be higher than the previous amount to update the delegators stake. + /// The `amount` of this call represents the total stake of the delegator. + /// If the pool is full, the lowest staked court participant is removed from the court pool. + /// If the `amount` is lower than the lowest staked court participant, the call fails. + /// + /// # Arguments + /// + /// - `amount`: The total stake associated with the joining delegator. + /// - `delegations`: The list of jurors to delegate the vote power to. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of jurors in the stake-weighted pool. #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::join_court())] - pub fn join_court(origin: OriginFor) -> DispatchResult { + #[pallet::weight(T::WeightInfo::delegate(T::MaxCourtParticipants::get(), delegations.len() as u32))] + #[transactional] + pub fn delegate( + origin: OriginFor, + amount: BalanceOf, + delegations: Vec, + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - if Jurors::::get(&who).is_some() { - return Err(Error::::JurorAlreadyExists.into()); - } - let jurors_num = Jurors::::count() as usize; - let jurors_num_plus_one = jurors_num.checked_add(1).ok_or(ArithmeticError::Overflow)?; - let stake = Self::current_required_stake(jurors_num_plus_one); - CurrencyOf::::reserve_named(&Self::reserve_id(), &who, stake)?; - let juror = Juror { status: JurorStatus::Ok }; - Jurors::::insert(&who, juror.clone()); - Self::deposit_event(Event::JoinedJuror(who, juror)); - Ok(()) + + ensure!(!delegations.is_empty(), Error::::NoDelegations); + let delegations_len = delegations.len() as u32; + let mut sorted_delegations: DelegationsOf = + delegations.clone().try_into().map_err(|_| Error::::MaxDelegationsReached)?; + + let pool = CourtPool::::get(); + let is_valid_set = sorted_delegations.iter().all(|pretended_juror| { + >::get(pretended_juror).map_or(false, |pretended_juror_info| { + Self::get_pool_item(&pool, pretended_juror_info.stake, pretended_juror) + .is_some() + && pretended_juror_info.delegations.is_none() + }) + }); + ensure!(is_valid_set, Error::::DelegatedToInvalidJuror); + // ensure all elements are different + sorted_delegations.sort(); + let has_duplicates = sorted_delegations + .iter() + .zip(sorted_delegations.iter().skip(1)) + .any(|(x, y)| x == y); + ensure!(!has_duplicates, Error::::IdenticalDelegationsNotAllowed); + ensure!(!sorted_delegations.contains(&who), Error::::SelfDelegationNotAllowed); + + let pool_len = Self::do_join_court(&who, amount, Some(sorted_delegations))?; + + Self::deposit_event(Event::DelegatorJoined { + delegator: who, + stake: amount, + delegated_jurors: delegations, + }); + + Ok(Some(T::WeightInfo::delegate(pool_len, delegations_len)).into()) } - // MARK(non-transactional): No fallible storage operation is performed. + /// Prepare as a court participant (juror or delegator) to exit the court. + /// When this is called the court participant is not anymore able to get drawn for new cases. + /// The court participant gets removed from the stake-weighted pool. + /// After that the court participant can exit the court. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of jurors in the stake-weighted pool. #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::vote())] + #[pallet::weight(T::WeightInfo::prepare_exit_court(T::MaxCourtParticipants::get()))] + #[transactional] + pub fn prepare_exit_court(origin: OriginFor) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let mut prev_p_info = + >::get(&who).ok_or(Error::::JurorDoesNotExist)?; + ensure!(prev_p_info.prepare_exit_at.is_none(), Error::::AlreadyPreparedExit); + + let mut pool = CourtPool::::get(); + let pool_len = pool.len() as u32; + + // do not error in the else case + // because the juror might have been already removed from the pool + if let Some((index, _)) = Self::get_pool_item(&pool, prev_p_info.stake, &who) { + pool.remove(index); + >::put(pool); + } + + let now = >::block_number(); + prev_p_info.prepare_exit_at = Some(now); + >::insert(&who, prev_p_info); + + Self::deposit_event(Event::ExitPrepared { court_participant: who }); + + Ok(Some(T::WeightInfo::prepare_exit_court(pool_len)).into()) + } + + /// Exit the court. + /// The stake which is not locked by any court case is unlocked. + /// `prepare_exit_court` must be called before + /// to remove the court participant (juror or delegator) from the stake-weighted pool. + /// + /// # Arguments + /// + /// - `court_participant`: The court participant, + /// who is assumed not to be part of the pool anymore. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of jurors in the stake-weighted pool. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::exit_court_set().max(T::WeightInfo::exit_court_remove()))] + #[transactional] + pub fn exit_court( + origin: OriginFor, + court_participant: AccountIdLookupOf, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + let who = T::Lookup::lookup(court_participant)?; + + let mut prev_p_info = + >::get(&who).ok_or(Error::::JurorDoesNotExist)?; + + let now = >::block_number(); + let prepare_exit_at = + prev_p_info.prepare_exit_at.ok_or(Error::::PrepareExitAtNotPresent)?; + ensure!( + now.saturating_sub(prepare_exit_at) >= T::InflationPeriod::get(), + Error::::PrematureExit + ); + + let (exit_amount, active_lock, weight) = if prev_p_info.active_lock.is_zero() { + T::Currency::remove_lock(T::LockId::get(), &who); + Participants::::remove(&who); + (prev_p_info.stake, >::zero(), T::WeightInfo::exit_court_remove()) + } else { + let active_lock = prev_p_info.active_lock; + let exit_amount = prev_p_info.stake.saturating_sub(active_lock); + T::Currency::set_lock(T::LockId::get(), &who, active_lock, WithdrawReasons::all()); + + prev_p_info.stake = active_lock; + Participants::::insert(&who, prev_p_info); + + (exit_amount, active_lock, T::WeightInfo::exit_court_set()) + }; + + Self::deposit_event(Event::ExitedCourt { + court_participant: who, + exit_amount, + active_lock, + }); + + Ok(Some(weight).into()) + } + + /// Vote as a randomly selected juror for a specific court case. + /// + /// # Arguments + /// + /// - `court_id`: The identifier of the court. + /// - `commitment_vote`: A hash which consists of `juror ++ vote_item ++ salt`. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of participants + /// in the list of random selections (draws). + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::vote(T::MaxSelectedDraws::get()))] + #[transactional] pub fn vote( origin: OriginFor, - #[pallet::compact] market_id: MarketIdOf, - outcome: OutcomeReport, - ) -> DispatchResult { + #[pallet::compact] court_id: CourtId, + commitment_vote: T::Hash, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + let now = >::block_number(); + ensure!( + court.round_ends.pre_vote < now && now <= court.round_ends.vote, + Error::::NotInVotingPeriod + ); + + let mut draws = >::get(court_id); + + match draws.binary_search_by_key(&who, |draw| draw.court_participant.clone()) { + Ok(index) => { + let draw = draws[index].clone(); + + // allow to override last vote + ensure!( + matches!(draws[index].vote, Vote::Drawn | Vote::Secret { commitment: _ }), + Error::::InvalidVoteState + ); + + let vote = Vote::Secret { commitment: commitment_vote }; + draws[index] = Draw { vote, ..draw }; + } + Err(_) => return Err(Error::::CallerNotInSelectedDraws.into()), + } + + let draws_len = draws.len() as u32; + + >::insert(court_id, draws); + + Self::deposit_event(Event::JurorVoted { + juror: who, + court_id, + commitment: commitment_vote, + }); + + Ok(Some(T::WeightInfo::vote(draws_len)).into()) + } + + /// Denounce a juror during the voting period for which the commitment vote is known. + /// This is useful to punish the behaviour that jurors reveal + /// their commitments to others before the voting period ends. + /// A check of `commitment_hash == hash(juror ++ vote_item ++ salt)` + /// is performed for validation. + /// + /// # Arguments + /// + /// - `court_id`: The identifier of the court. + /// - `juror`: The juror whose commitment vote might be known. + /// - `vote_item`: The raw vote item which should match with the commitment of the juror. + /// - `salt`: The hash which is used to proof that the juror did reveal + /// her vote during the voting period. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of selected draws + /// in the specified court. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::denounce_vote(T::MaxSelectedDraws::get()))] + #[transactional] + pub fn denounce_vote( + origin: OriginFor, + #[pallet::compact] court_id: CourtId, + juror: AccountIdLookupOf, + vote_item: VoteItem, + salt: T::Hash, + ) -> DispatchResultWithPostInfo { + let denouncer = ensure_signed(origin)?; + + if let Some(market_id) = >::get(court_id) { + let market = T::MarketCommons::market(&market_id)?; + let outcome = + vote_item.clone().into_outcome().ok_or(Error::::VoteItemIsNoOutcome)?; + ensure!(market.matches_outcome_report(&outcome), Error::::OutcomeMismatch); + } + + let juror = T::Lookup::lookup(juror)?; + + ensure!(denouncer != juror, Error::::CallerDenouncedItself); + + ensure!(>::contains_key(&juror), Error::::JurorDoesNotExist); + + let court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + Self::check_vote_item(&court, &vote_item)?; + + let now = >::block_number(); + // ensure in vote period + ensure!( + court.round_ends.pre_vote < now && now <= court.round_ends.vote, + Error::::NotInVotingPeriod + ); + + let mut draws = >::get(court_id); + match draws.binary_search_by_key(&juror, |draw| draw.court_participant.clone()) { + Ok(index) => { + let draw = draws[index].clone(); + + let raw_commmitment = + RawCommitment { juror: juror.clone(), vote_item: vote_item.clone(), salt }; + + let commitment = Self::get_hashed_commitment(draw.vote, raw_commmitment)?; + + // slash for the misbehaviour happens in reassign_court_stakes + let raw_vote = + Vote::Denounced { commitment, vote_item: vote_item.clone(), salt }; + draws[index] = Draw { vote: raw_vote, ..draw }; + } + Err(_) => return Err(Error::::JurorNotDrawn.into()), + } + + let draws_len = draws.len() as u32; + + >::insert(court_id, draws); + + Self::deposit_event(Event::DenouncedJurorVote { + denouncer, + juror, + court_id, + vote_item, + salt, + }); + + Ok(Some(T::WeightInfo::denounce_vote(draws_len)).into()) + } + + /// Reveal the commitment vote of the caller, who is a selected juror. + /// A check of `commitment_hash == hash(juror ++ vote_item ++ salt)` + /// is performed for validation. + /// + /// # Arguments + /// + /// - `court_id`: The identifier of the court. + /// - `vote_item`: The raw vote item which should match with the commitment of the juror. + /// - `salt`: The hash which is used for the validation. + /// + /// # Weight + /// + /// Complexity: `O(log(n))`, where `n` is the number of selected draws + /// in the specified court. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::reveal_vote(T::MaxSelectedDraws::get()))] + #[transactional] + pub fn reveal_vote( + origin: OriginFor, + #[pallet::compact] court_id: CourtId, + vote_item: VoteItem, + salt: T::Hash, + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - if Jurors::::get(&who).is_none() { - return Err(Error::::OnlyJurorsCanVote.into()); + + if let Some(market_id) = >::get(court_id) { + let market = T::MarketCommons::market(&market_id)?; + let outcome = + vote_item.clone().into_outcome().ok_or(Error::::VoteItemIsNoOutcome)?; + ensure!(market.matches_outcome_report(&outcome), Error::::OutcomeMismatch); } - Votes::::insert( - market_id, - who, - (>::block_number(), outcome), + + ensure!( + >::get(&who).is_some(), + Error::::CallerIsNotACourtParticipant ); + let court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + Self::check_vote_item(&court, &vote_item)?; + + let now = >::block_number(); + ensure!( + court.round_ends.vote < now && now <= court.round_ends.aggregation, + Error::::NotInAggregationPeriod + ); + + let mut draws = >::get(court_id); + match draws.binary_search_by_key(&who, |draw| draw.court_participant.clone()) { + Ok(index) => { + let draw = draws[index].clone(); + + let raw_commitment = + RawCommitment { juror: who.clone(), vote_item: vote_item.clone(), salt }; + + let commitment = Self::get_hashed_commitment(draw.vote, raw_commitment)?; + + let raw_vote = + Vote::Revealed { commitment, vote_item: vote_item.clone(), salt }; + draws[index] = Draw { court_participant: who.clone(), vote: raw_vote, ..draw }; + } + Err(_) => return Err(Error::::CallerNotInSelectedDraws.into()), + } + + let draws_len = draws.len() as u32; + + >::insert(court_id, draws); + + Self::deposit_event(Event::JurorRevealedVote { juror: who, court_id, vote_item, salt }); + + Ok(Some(T::WeightInfo::reveal_vote(draws_len)).into()) + } + + /// Initiate an appeal for a court + /// if the presumptive winner of the last vote round is believed to be incorrect. + /// The last appeal does not trigger a new court round + /// but instead it marks the court mechanism for this market as failed. + /// If the court failed, the prediction markets pallet takes over the dispute resolution. + /// The prediction markets pallet might allow to trigger a global token holder vote. + /// + /// # Arguments + /// + /// - `court_id`: The identifier of the court. + /// + /// # Weight + /// + /// Complexity: It depends heavily on the complexity of `select_participants`. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::appeal( + T::MaxCourtParticipants::get(), + T::MaxAppeals::get(), + CacheSize::get(), + CacheSize::get(), + ))] + #[transactional] + pub fn appeal( + origin: OriginFor, + #[pallet::compact] court_id: CourtId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + let mut court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + let appeal_number = court.appeals.len().saturating_add(1); + ensure!(appeal_number <= T::MaxAppeals::get() as usize, Error::::MaxAppealsReached); + let bond = get_appeal_bond::(appeal_number); + ensure!(T::Currency::can_reserve(&who, bond), Error::::AppealBondExceedsBalance); + let now = >::block_number(); + Self::check_appealable_market(court_id, &court, now)?; + + // the vote item which would be resolved on is appealed (including oracle report) + let old_draws = SelectedDraws::::get(court_id); + let appealed_vote_item = + Self::get_latest_winner_vote_item(court_id, old_draws.as_slice())?; + let appeal_info = AppealInfo { backer: who.clone(), bond, appealed_vote_item }; + court.appeals.try_push(appeal_info).map_err(|_| { + debug_assert!(false, "Appeal bound is checked above."); + Error::::MaxAppealsReached + })?; + + let last_resolve_at = court.round_ends.appeal; + + // used for benchmarking, juror pool is queried inside `select_participants` + let pool_len = >::decode_len().unwrap_or(0) as u32; + + let mut ids_len_1 = 0u32; + // if appeal_number == MaxAppeals, then don't start a new appeal round + if appeal_number < T::MaxAppeals::get() as usize { + let new_draws = Self::select_participants(appeal_number)?; + let request_block = >::get(); + debug_assert!(request_block >= now, "Request block must be greater than now."); + let round_timing = RoundTiming { + pre_vote: request_block, + vote: T::VotePeriod::get(), + aggregation: T::AggregationPeriod::get(), + appeal: T::AppealPeriod::get(), + }; + // sets round ends one after the other from now + court.update_round(round_timing); + let new_resolve_at = court.round_ends.appeal; + debug_assert!(new_resolve_at != last_resolve_at); + if let Some(market_id) = >::get(court_id) { + ids_len_1 = T::DisputeResolution::add_auto_resolve(&market_id, new_resolve_at)?; + } + >::insert(court_id, new_draws); + Self::unlock_participants_from_last_draw(court_id, old_draws); + } + + let mut ids_len_0 = 0u32; + if let Some(market_id) = >::get(court_id) { + ids_len_0 = T::DisputeResolution::remove_auto_resolve(&market_id, last_resolve_at); + } + + T::Currency::reserve_named(&Self::reserve_id(), &who, bond)?; + + >::insert(court_id, court); + + let appeal_number = appeal_number as u32; + Self::deposit_event(Event::CourtAppealed { court_id, appeal_number }); + + Ok(Some(T::WeightInfo::appeal(pool_len, appeal_number, ids_len_0, ids_len_1)).into()) + } + + /// Reassign the stakes of the jurors and delegators + /// for the selected draws of the specified court. + /// The losing jurors and delegators get slashed and + /// pay for the winning jurors and delegators. + /// The tardy (juror did not reveal or did not vote) or denounced jurors + /// and associated delegators get slashed and reward the winners. + /// + /// # Arguments + /// + /// - `court_id`: The identifier of the court. + /// + /// # Weight + /// + /// Complexity: O(N + M), with `N` being the number of draws and `M` being the total number of valid winners and losers. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::reassign_court_stakes(T::MaxSelectedDraws::get()))] + #[transactional] + pub fn reassign_court_stakes( + origin: OriginFor, + court_id: CourtId, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + let mut court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + let winner = match court.status { + CourtStatus::Closed { winner } => winner, + CourtStatus::Reassigned => return Err(Error::::CourtAlreadyReassigned.into()), + CourtStatus::Open => return Err(Error::::CourtNotClosed.into()), + }; + + let draws = SelectedDraws::::get(court_id); + let draws_len = draws.len() as u32; + + let reward_pot = Self::reward_pot(court_id); + let slash_juror = |ai: &T::AccountId, slashable: BalanceOf| { + let (imbalance, missing) = T::Currency::slash(ai, slashable); + debug_assert!( + missing.is_zero(), + "Could not slash all of the amount for juror {:?}.", + ai + ); + T::Currency::resolve_creating(&reward_pot, imbalance); + }; + + // map delegated jurors to own_slashable, vote item and Vec<(delegator, delegator_stake)> + let mut jurors_to_stakes = BTreeMap::>::new(); + + let mut handle_vote = |draw: DrawOf| { + match draw.vote { + Vote::Drawn + | Vote::Secret { commitment: _ } + | Vote::Denounced { commitment: _, vote_item: _, salt: _ } => { + slash_juror(&draw.court_participant, draw.slashable); + } + Vote::Revealed { commitment: _, vote_item, salt: _ } => { + jurors_to_stakes.entry(draw.court_participant).or_default().self_info = + Some(SelfInfo { slashable: draw.slashable, vote_item }); + } + Vote::Delegated { delegated_stakes } => { + let delegator = draw.court_participant; + for (j, delegated_stake) in delegated_stakes { + // fill the delegations for each juror + // [(juror_0, [(delegator_0, delegator_stake_0), ...]), + // (juror_1, [(delegator_42, delegator_stake_42), ...]), ...] + let jurors_to_stakes_entry = jurors_to_stakes.entry(j); + let juror_vote_with_stakes = jurors_to_stakes_entry.or_default(); + + // future-proof binary search by key + // because many delegators can back one juror + // we might want to fastly find elements later on + match juror_vote_with_stakes + .delegations + .binary_search_by_key(&delegator, |(d, _)| d.clone()) + { + Ok(i) => { + juror_vote_with_stakes.delegations[i].1 = + juror_vote_with_stakes.delegations[i] + .1 + .saturating_add(delegated_stake); + } + Err(i) => { + juror_vote_with_stakes + .delegations + .insert(i, (delegator.clone(), delegated_stake)); + } + } + } + } + } + }; + + for draw in draws { + if let Some(mut p_info) = >::get(&draw.court_participant) { + p_info.active_lock = p_info.active_lock.saturating_sub(draw.slashable); + >::insert(&draw.court_participant, p_info); + } else { + log::warn!( + "Participant {:?} not found in Participants storage \ + (reassign_court_stakes). Court id {:?}.", + draw.court_participant, + court_id + ); + debug_assert!(false); + } + + handle_vote(draw); + } + + Self::slash_losers_to_award_winners(court_id, jurors_to_stakes, &winner); + + court.status = CourtStatus::Reassigned; + >::insert(court_id, court); + + >::remove(court_id); + + Self::deposit_event(Event::StakesReassigned { court_id }); + + Ok(Some(T::WeightInfo::reassign_court_stakes(draws_len)).into()) + } + + /// Set the yearly inflation rate of the court system. + /// This is only allowed to be called by the `MonetaryGovernanceOrigin`. + /// + /// # Arguments + /// + /// - `inflation`: The desired yearly inflation rate. + /// + /// # Weight + /// + /// Complexity: `O(1)` + #[pallet::call_index(9)] + #[pallet::weight(T::WeightInfo::set_inflation())] + #[transactional] + pub fn set_inflation(origin: OriginFor, inflation: Perbill) -> DispatchResult { + T::MonetaryGovernanceOrigin::ensure_origin(origin)?; + + >::put(inflation); + + Self::deposit_event(Event::InflationSet { inflation }); + Ok(()) } } - #[pallet::config] - pub trait Config: frame_system::Config { - /// Block duration to cast a vote on an outcome. - #[pallet::constant] - type CourtCaseDuration: Get; + impl Pallet + where + T: Config, + { + fn do_join_court( + who: &T::AccountId, + amount: BalanceOf, + delegations: Option>, + ) -> Result { + ensure!(amount >= T::MinJurorStake::get(), Error::::BelowMinJurorStake); + let free_balance = T::Currency::free_balance(who); + ensure!(amount <= free_balance, Error::::AmountExceedsBalance); - type DisputeResolution: DisputeResolutionApi< - AccountId = Self::AccountId, - BlockNumber = Self::BlockNumber, - MarketId = MarketIdOf, - Moment = MomentOf, - >; + let mut pool = CourtPool::::get(); + + let now = >::block_number(); + + let remove_weakest_if_full = + |mut p: CourtPoolOf| -> Result, DispatchError> { + if p.is_full() { + let lowest_item = p.first(); + let lowest_stake = lowest_item + .map(|pool_item| pool_item.stake) + .unwrap_or_else(>::zero); + debug_assert!({ + let mut sorted = p.clone(); + sorted.sort_by_key(|pool_item| { + (pool_item.stake, pool_item.court_participant.clone()) + }); + p.len() == sorted.len() + && p.iter() + .zip(sorted.iter()) + .all(|(a, b)| lowest_stake <= a.stake && a == b) + }); + ensure!(amount > lowest_stake, Error::::AmountBelowLowestJuror); + // remove the lowest staked court participant + p.remove(0); + } + + Ok(p) + }; + + let mut active_lock = >::zero(); + let mut consumed_stake = >::zero(); + let mut joined_at = now; + + if let Some(prev_p_info) = >::get(who) { + ensure!(amount >= prev_p_info.stake, Error::::AmountBelowLastJoin); + + if let Some((index, pool_item)) = Self::get_pool_item(&pool, prev_p_info.stake, who) + { + active_lock = prev_p_info.active_lock; + consumed_stake = pool_item.consumed_stake; + joined_at = pool_item.joined_at; + + pool.remove(index); + } else { + active_lock = prev_p_info.active_lock; + consumed_stake = prev_p_info.active_lock; + + pool = remove_weakest_if_full(pool)?; + } + } else { + pool = remove_weakest_if_full(pool)?; + } + + let (active_lock, consumed_stake, joined_at) = (active_lock, consumed_stake, joined_at); + + match pool.binary_search_by_key(&(amount, who), |pool_item| { + (pool_item.stake, &pool_item.court_participant) + }) { + Ok(_) => { + debug_assert!( + false, + "This should never happen, because we are removing the court participant \ + above." + ); + return Err(Error::::CourtParticipantTwiceInPool.into()); + } + Err(i) => pool + .try_insert( + i, + CourtPoolItem { + stake: amount, + court_participant: who.clone(), + consumed_stake, + joined_at, + }, + ) + .map_err(|_| { + debug_assert!( + false, + "This should never happen, because we are removing the lowest staked \ + court participant above." + ); + Error::::MaxCourtParticipantsReached + })?, + }; + + T::Currency::set_lock(T::LockId::get(), who, amount, WithdrawReasons::all()); + + let pool_len = pool.len() as u32; + CourtPool::::put(pool); + + let p_info = CourtParticipantInfoOf:: { + stake: amount, + active_lock, + prepare_exit_at: None, + delegations, + }; + >::insert(who, p_info); + + Ok(pool_len) + } + + // Handle the external incentivisation of the court system. + pub(crate) fn handle_inflation(now: T::BlockNumber) -> Weight { + let inflation_period = T::InflationPeriod::get(); + if !(now % inflation_period).is_zero() { + return Weight::zero(); + } + + let yearly_inflation_rate = >::get(); + // example: 1049272791644671442 + let total_supply = T::Currency::total_issuance(); + // example: 0.02 * 1049272791644671442 = 20985455832893428 + let yearly_inflation_amount = + yearly_inflation_rate.mul_floor(total_supply).saturated_into::(); + let blocks_per_year = T::BlocksPerYear::get().saturated_into::(); + debug_assert!(!T::BlocksPerYear::get().is_zero()); + // example: 20985455832893428 / 2629800 = 7979867607 + let issue_per_block = + yearly_inflation_amount.saturating_div(blocks_per_year.max(One::one())); + + let yearly_inflation_amount = yearly_inflation_amount.saturated_into::>(); + let issue_per_block = issue_per_block.saturated_into::>(); + let inflation_period = + inflation_period.saturated_into::().saturated_into::>(); + + // example: 7979867607 * 7200 * 30 = 1723651403112000 + let inflation_period_mint = issue_per_block.saturating_mul(inflation_period); + + // inflation_period_mint shouldn't exceed 0.5% of the total issuance + let log_threshold = Perbill::from_perthousand(5u32) + .mul_floor(total_supply) + .saturated_into::() + .saturated_into::>(); + if inflation_period_mint >= log_threshold { + log::warn!( + "Inflation per period is greater than the threshold. Inflation per period: \ + {:?}, threshold: {:?}", + inflation_period_mint, + log_threshold + ); + debug_assert!(false); + } + + // safe guard: inflation per period should never exceed the yearly inflation amount + if inflation_period_mint > yearly_inflation_amount { + debug_assert!(false); + return T::WeightInfo::handle_inflation(0u32); + } + + let pool = >::get(); + let pool_len = pool.len() as u32; + let at_least_one_inflation_period = + |joined_at| now.saturating_sub(joined_at) >= T::InflationPeriod::get(); + let total_stake = pool + .iter() + .filter(|pool_item| at_least_one_inflation_period(pool_item.joined_at)) + .fold(0u128, |acc, pool_item| { + acc.saturating_add(pool_item.stake.saturated_into::()) + }); + if total_stake.is_zero() { + return T::WeightInfo::handle_inflation(0u32); + } + + let mut total_mint = T::Currency::issue(inflation_period_mint); + + for CourtPoolItem { stake, court_participant, joined_at, .. } in pool { + if !at_least_one_inflation_period(joined_at) { + // participants who joined and didn't wait + // at least one full inflation period won't get a reward + continue; + } + let share = Perquintill::from_rational(stake.saturated_into::(), total_stake); + let mint = share.mul_floor(inflation_period_mint.saturated_into::()); + let (mint_imb, remainder) = total_mint.split(mint.saturated_into::>()); + let mint_amount = mint_imb.peek(); + total_mint = remainder; + if let Ok(()) = T::Currency::resolve_into_existing(&court_participant, mint_imb) { + Self::deposit_event(Event::MintedInCourt { + court_participant: court_participant.clone(), + amount: mint_amount, + }); + } + } + + let remainder = total_mint.peek(); + if total_mint.drop_zero().is_err() { + log::debug!( + "Total issued tokens were not completely distributed, total: {:?}, leftover: \ + {:?}", + inflation_period_mint, + remainder + ); + + T::Currency::burn(remainder); + } + + T::WeightInfo::handle_inflation(pool_len) + } + + // Get `n` unique and ordered random `MinJurorStake` section ends + // from the random number generator. + // Uses Partial Fisher Yates shuffle and drawing without replacement. + // The time complexity is O(n). + // Return a vector of n unique random numbers between ´MinJurorStake´ and ´max´ (inclusive). + pub(crate) fn get_n_random_section_ends(n: usize, max: u128) -> BTreeSet { + let mut rng = Self::rng(); + + let min_juror_stake = T::MinJurorStake::get().saturated_into::(); + debug_assert!((max % min_juror_stake).is_zero(), "This is ensured by the caller."); + let sections_len = max.checked_div(min_juror_stake).unwrap_or(0); + debug_assert!(sections_len >= (n as u128)); + + let mut swaps = BTreeMap::::new(); + let mut random_section_ends = BTreeSet::new(); + + for i in 0..(n as u128) { + let visited_i = *swaps.get(&i).unwrap_or(&i); + + let unused_random_index = rng.gen_range(i..sections_len); + let unused_random_number = + *swaps.get(&unused_random_index).unwrap_or(&unused_random_index); + + // save the unused random number, which is between i and sections_len, to the map + // i can be found later on two, because we save it below as `visited_i` + swaps.insert(i, unused_random_number); + // save already visited i to the map, so that it can possibly inserted later on + swaps.insert(unused_random_index, visited_i); + + // add one because we need numbers between 1 and sections_len (inclusive) + let random_index = unused_random_number.saturating_add(1); + let random_section_end = random_index.saturating_mul(min_juror_stake); + random_section_ends.insert(random_section_end); + } + + debug_assert!(random_section_ends.len() == n); + + random_section_ends + } + + // Adds active lock amount. + // The added active lock amount is noted in the Participants map. + fn add_active_lock(court_participant: &T::AccountId, lock_added: BalanceOf) { + if let Some(mut p_info) = >::get(court_participant) { + p_info.active_lock = p_info.active_lock.saturating_add(lock_added); + >::insert(court_participant, p_info); + } else { + debug_assert!(false, "Participant should exist in the Participants map"); + } + } + + /// Add a delegated juror to the `delegated_stakes` vector. + fn add_delegated_juror( + mut delegated_stakes: DelegatedStakesOf, + delegated_juror: &T::AccountId, + amount: BalanceOf, + ) -> DelegatedStakesOf { + match delegated_stakes.binary_search_by_key(&delegated_juror, |(j, _)| j) { + Ok(index) => { + delegated_stakes[index].1 = delegated_stakes[index].1.saturating_add(amount); + } + Err(index) => { + let _ = delegated_stakes + .try_insert(index, (delegated_juror.clone(), amount)) + .map_err(|_| { + debug_assert!( + false, + "BoundedVec insertion should not fail, because the length of \ + jurors is ensured for delegations." + ); + }); + } + } + + delegated_stakes + } + + // Updates the `selections` map for the juror and the lock amount. + // If `court_participant` does not already exist in `selections`, + // the vote weight is set to 1 and the lock amount is initially set. + // For each call on the same juror, the vote weight is incremented by one + // and the lock amount is added to the previous amount. + fn update_selections( + selections: &mut BTreeMap>, + court_participant: &T::AccountId, + sel_add: SelectionAdd, BalanceOf>, + ) { + if let Some(SelectionValue { weight, slashable, delegated_stakes }) = + selections.get_mut(court_participant) + { + match sel_add { + SelectionAdd::SelfStake { lock } => { + *weight = weight.saturating_add(1); + *slashable = slashable.saturating_add(lock); + } + SelectionAdd::DelegationStake { delegated_juror, lock } => { + *slashable = slashable.saturating_add(lock); + *delegated_stakes = Self::add_delegated_juror( + delegated_stakes.clone(), + &delegated_juror, + lock, + ); + } + SelectionAdd::DelegationWeight => { + *weight = weight.saturating_add(1); + } + }; + } else { + match sel_add { + SelectionAdd::SelfStake { lock } => { + selections.insert( + court_participant.clone(), + SelectionValue { + weight: 1, + slashable: lock, + delegated_stakes: Default::default(), + }, + ); + } + SelectionAdd::DelegationStake { delegated_juror, lock } => { + let delegated_stakes = Self::add_delegated_juror( + DelegatedStakesOf::::default(), + &delegated_juror, + lock, + ); + selections.insert( + court_participant.clone(), + SelectionValue { weight: 0, slashable: lock, delegated_stakes }, + ); + } + SelectionAdd::DelegationWeight => { + selections.insert( + court_participant.clone(), + SelectionValue { + weight: 1, + slashable: >::zero(), + delegated_stakes: Default::default(), + }, + ); + } + }; + } + } + + /// Return one delegated juror out of the delegations randomly. + fn get_valid_delegated_juror(delegations: &[T::AccountId]) -> Option { + let mut rng = Self::rng(); + let pool: CourtPoolOf = CourtPool::::get(); + let mut valid_delegated_jurors = Vec::new(); + + for delegated_juror in delegations { + if let Some(delegated_juror_info) = >::get(delegated_juror) { + if delegated_juror_info.delegations.is_some() { + // skip if delegated juror is delegator herself + continue; + } + if Self::get_pool_item(&pool, delegated_juror_info.stake, delegated_juror) + .is_some() + { + valid_delegated_jurors.push(delegated_juror.clone()); + } + } + } + + valid_delegated_jurors.choose(&mut rng).cloned() + } + + /// Add a juror or delegator with the provided `lock_added` to the `selections` map. + fn add_to_selections( + selections: &mut BTreeMap>, + court_participant: &T::AccountId, + lock_added: BalanceOf, + ) -> Result<(), SelectionError> { + let delegations_opt = >::get(court_participant.clone()) + .and_then(|p_info| p_info.delegations); + match delegations_opt { + Some(delegations) => { + let delegated_juror = Self::get_valid_delegated_juror(delegations.as_slice()) + .ok_or(SelectionError::NoValidDelegatedJuror)?; + + // delegated juror gets the vote weight + let sel_add = SelectionAdd::DelegationWeight; + Self::update_selections(selections, &delegated_juror, sel_add); + + let sel_add = SelectionAdd::DelegationStake { + delegated_juror: delegated_juror.clone(), + lock: lock_added, + }; + // delegator risks his stake (to delegated juror), but gets no vote weight + Self::update_selections(selections, court_participant, sel_add); + } + None => { + let sel_add = SelectionAdd::SelfStake { lock: lock_added }; + Self::update_selections(selections, court_participant, sel_add); + } + } + + Ok(()) + } + + // Match the random numbers to select some jurors and delegators from the pool. + // The active lock (and consumed stake) of the selected jurors + // is increased by the random selection weight. + // If a delegator is chosen by a random number, one delegated juror gets the vote weight. + fn get_selections( + pool: &mut CourtPoolOf, + random_section_ends: BTreeSet, + cumulative_section_ends: Vec<(u128, bool)>, + ) -> BTreeMap> { + debug_assert!(pool.len() == cumulative_section_ends.len()); + debug_assert!({ + let prev = cumulative_section_ends.clone(); + let mut sorted = cumulative_section_ends.clone(); + sorted.sort(); + prev.len() == sorted.len() && prev.iter().zip(sorted.iter()).all(|(a, b)| a == b) + }); + debug_assert!({ + random_section_ends.iter().all(|random_section_end| { + let last = cumulative_section_ends.last().unwrap_or(&(0, false)).0; + *random_section_end <= last + }) + }); + + let mut selections = BTreeMap::>::new(); + let mut invalid_juror_indices = Vec::::new(); + + for random_section_end in random_section_ends { + let allow_zero_stake = false; + let range_index = cumulative_section_ends + .binary_search(&(random_section_end, allow_zero_stake)) + .unwrap_or_else(|i| i); + if let Some(pool_item) = pool.get_mut(range_index) { + let unconsumed = pool_item.stake.saturating_sub(pool_item.consumed_stake); + let lock_added = unconsumed.min(T::MinJurorStake::get()); + + match Self::add_to_selections( + &mut selections, + &pool_item.court_participant, + lock_added, + ) { + Ok(()) => {} + Err(SelectionError::NoValidDelegatedJuror) => { + // it would be pretty expensive to request another selection + // so just ignore this missing MinJurorStake + // I mean we also miss MinJurorStake in the case + // if the juror fails to vote or reveal or gets denounced + invalid_juror_indices.push(range_index); + } + } + + Self::add_active_lock(&pool_item.court_participant, lock_added); + pool_item.consumed_stake = pool_item.consumed_stake.saturating_add(lock_added); + } else { + debug_assert!(false, "Each range index should match to a juror."); + } + } + + for i in invalid_juror_indices { + pool.remove(i); + } + + selections + } + + // Converts the `selections` map into a vector of `Draw` structs. + fn convert_selections_to_draws( + selections: BTreeMap>, + ) -> Vec> { + selections + .into_iter() + .map( + |( + court_participant, + SelectionValue { weight, slashable, delegated_stakes }, + )| Draw { + court_participant, + weight, + vote: if !delegated_stakes.is_empty() { + debug_assert!( + weight.is_zero(), + "Delegators shouldn't have voting weight." + ); + debug_assert!( + delegated_stakes + .clone() + .into_iter() + .fold(Zero::zero(), |acc: BalanceOf, (_, stake)| acc + .saturating_add(stake)) + == slashable + ); + Vote::Delegated { delegated_stakes } + } else { + Vote::Drawn + }, + slashable, + }, + ) + .collect() + } + + // Choose `draw_weight` (multiple) of `MinJurorStake` from the pool randomly + // according to the weighted stake of all jurors and delegators. + // NOTE: The jurors and delegators are being cut by the remainder + // if the stake is not a multiple of `MinJurorStake`. + // Return the random draws. + pub(crate) fn choose_multiple_weighted( + draw_weight: usize, + ) -> Result>, DispatchError> { + let mut pool = >::get(); + + let min_juror_stake = T::MinJurorStake::get().saturated_into::(); + + let mut total_unconsumed = 0u128; + let mut cumulative_section_ends = Vec::new(); + let mut running_total = 0u128; + for pool_item in &pool { + let unconsumed = pool_item + .stake + .saturating_sub(pool_item.consumed_stake) + .saturated_into::(); + let remainder = unconsumed % min_juror_stake; + let unconsumed = unconsumed.saturating_sub(remainder); + total_unconsumed = total_unconsumed.saturating_add(unconsumed); + running_total = running_total.saturating_add(unconsumed); + // this is useful for binary search to match the correct juror + // if we don't do this, the binary search in `get_selections` + // might take the wrong juror (with zero stake) + // (running total would be the same for two consecutive jurors) + let zero_stake = unconsumed.is_zero(); + cumulative_section_ends.push((running_total, zero_stake)); + } + debug_assert!( + (total_unconsumed % min_juror_stake).is_zero(), + "Remainders are being cut in the above for loop." + ); - /// Event - type RuntimeEvent: From> + IsType<::RuntimeEvent>; + let required_stake = (draw_weight as u128).saturating_mul(min_juror_stake); + ensure!( + total_unconsumed >= required_stake, + Error::::NotEnoughJurorsAndDelegatorsStake + ); + let random_section_ends = + Self::get_n_random_section_ends(draw_weight, total_unconsumed); + let selections = + Self::get_selections(&mut pool, random_section_ends, cumulative_section_ends); + >::put(pool); - /// Market commons - type MarketCommons: MarketCommonsPalletApi; + Ok(Self::convert_selections_to_draws(selections)) + } - /// Identifier of this pallet - #[pallet::constant] - type PalletId: Get; + // Reduce the active lock of the jurors from the last draws. + // This is useful so that the jurors can thaw their non-locked stake. + fn unlock_participants_from_last_draw(court_id: CourtId, last_draws: SelectedDrawsOf) { + // keep in mind that the old draw likely contains different jurors and delegators + for old_draw in last_draws { + if let Some(mut p_info) = >::get(&old_draw.court_participant) { + p_info.active_lock = p_info.active_lock.saturating_sub(old_draw.slashable); + >::insert(&old_draw.court_participant, p_info); + } else { + log::warn!( + "Participant {:?} not found in Participants storage \ + (unlock_participants_from_last_draw). Court id {:?}.", + old_draw.court_participant, + court_id + ); + debug_assert!(false); + } + } + } - /// Randomness source - type Random: Randomness; + // Selects the jurors and delegators for the next court round. + // The `consumed_stake` in `CourtPool` and `active_lock` in `Participants` is increased + // equally according to the draw weight. + // With increasing `consumed_stake` the probability to get selected + // in further court rounds shrinks. + // + // Returns the new draws. + pub(crate) fn select_participants( + appeal_number: usize, + ) -> Result, DispatchError> { + let necessary_draws_weight = Self::necessary_draws_weight(appeal_number); + let random_jurors = Self::choose_multiple_weighted(necessary_draws_weight)?; - /// Weight used to calculate the necessary staking amount to become a juror - #[pallet::constant] - type StakeWeight: Get>; + // keep in mind that the number of draws is at maximum necessary_draws_weight * 2 + // because with delegations each juror draw weight + // could delegate an additional juror in addition to the delegator itself + debug_assert!(random_jurors.len() <= 2 * necessary_draws_weight); + debug_assert!({ + // proove that random jurors is sorted by juror account id + // this is helpful to use binary search later on + let prev = random_jurors.clone(); + let mut sorted = random_jurors.clone(); + sorted.sort_by_key(|draw| draw.court_participant.clone()); + prev.len() == sorted.len() && prev.iter().zip(sorted.iter()).all(|(a, b)| a == b) + }); - /// Slashed funds are send to the treasury - #[pallet::constant] - type TreasuryPalletId: Get; + // what is the maximum number of draws with delegations? + // It is using necessary_draws_weight (the number of atoms / draw weight) + // for the last round times two because each delegator + // could potentially add one juror account to the selections - /// Weights generated by benchmarks - type WeightInfo: WeightInfoZeitgeist; - } + // new appeal round should have a fresh set of draws - #[pallet::error] - pub enum Error { - /// It is not possible to insert a Juror that is already stored - JurorAlreadyExists, - /// An account id does not exist on the jurors storage. - JurorDoesNotExists, - /// On dispute or resolution, someone tried to pass a non-court market type - MarketDoesNotHaveCourtMechanism, - /// No-one voted on an outcome to resolve a market - NoVotes, - /// Forbids voting of unknown accounts - OnlyJurorsCanVote, - } + // ensure that we don't truncate some of the selections + debug_assert!( + random_jurors.len() <= T::MaxSelectedDraws::get() as usize, + "The number of randomly selected jurors and delegators should be less than or \ + equal to `MaxSelectedDraws`." + ); + Ok(>::truncate_from(random_jurors)) + } - #[pallet::event] - #[pallet::generate_deposit(fn deposit_event)] - pub enum Event - where - T: Config, - { - ExitedJuror(T::AccountId, Juror), - JoinedJuror(T::AccountId, Juror), - } + // Returns (index, pool_item) if the pool item is part of the juror pool. + // It returns None otherwise. + pub(crate) fn get_pool_item<'a>( + pool: &'a [CourtPoolItemOf], + stake: BalanceOf, + court_participant: &T::AccountId, + ) -> Option<(usize, &'a CourtPoolItemOf)> { + if let Ok(i) = pool.binary_search_by_key(&(stake, court_participant), |pool_item| { + (pool_item.stake, &pool_item.court_participant) + }) { + return Some((i, &pool[i])); + } + // this None case can happen whenever the court participant decided to leave the court + // or was kicked out of the court pool because of the lowest stake + None + } - #[pallet::hooks] - impl Hooks for Pallet {} + // Returns OK if the market is in a valid state to be appealed. + // Returns an error otherwise. + pub(crate) fn check_appealable_market( + court_id: CourtId, + court: &CourtOf, + now: T::BlockNumber, + ) -> Result<(), DispatchError> { + if let Some(market_id) = >::get(court_id) { + let market = T::MarketCommons::market(&market_id)?; + ensure!(market.status == MarketStatus::Disputed, Error::::MarketIsNotDisputed); + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::Court, + Error::::MarketDoesNotHaveCourtMechanism + ); + } - #[pallet::pallet] - #[pallet::storage_version(STORAGE_VERSION)] - pub struct Pallet(PhantomData); + ensure!( + court.round_ends.aggregation < now && now < court.round_ends.appeal, + Error::::NotInAppealPeriod + ); - impl Pallet - where - T: Config, - { - // Returns an unique random subset of `jurors` with length `len`. - // - // If `len` is greater than the length of `jurors`, then `len` will be capped. - pub(crate) fn random_jurors<'a, 'b, R>( - jurors: &'a [(T::AccountId, Juror)], - len: usize, - rng: &mut R, - ) -> ArrayVec<&'b (T::AccountId, Juror), MAX_RANDOM_JURORS> - where - 'a: 'b, - R: RngCore, - { - let actual_len = jurors.len().min(len); - jurors.choose_multiple(rng, actual_len).collect() + Ok(()) } /// The reserve ID of the court pallet. @@ -245,382 +1794,617 @@ mod pallet { T::PalletId::get().0 } - // Returns a pseudo random number generator implementation based on the seed - // provided by the `Config::Random` type and the `JurorsSelectionNonce` storage. - pub(crate) fn rng() -> impl RngCore { - let nonce = >::mutate(|n| { - let rslt = *n; - *n = n.wrapping_add(1); - rslt - }); - let mut seed = [0; 32]; - let (random_hash, _) = T::Random::random(&nonce.to_le_bytes()); - for (byte, el) in random_hash.as_ref().iter().copied().zip(seed.iter_mut()) { - *el = byte - } - StdRng::from_seed(seed) - } - - // Used to avoid code duplications. - pub(crate) fn set_stored_juror_as_tardy(account_id: &T::AccountId) -> DispatchResult { - Self::mutate_juror(account_id, |juror| { - juror.status = JurorStatus::Tardy; - Ok(()) - }) + /// The account ID which is used to reward the correct jurors. + #[inline] + pub(crate) fn reward_pot(court_id: CourtId) -> T::AccountId { + T::PalletId::get().into_sub_account_truncating(court_id) } + /// The account ID of the treasury. #[inline] pub(crate) fn treasury_account_id() -> T::AccountId { T::TreasuryPalletId::get().into_account_truncating() } - // No-one can stake more than BalanceOf::::max(), therefore, this function saturates - // arithmetic operations. - fn current_required_stake(jurors_num: usize) -> BalanceOf { - let jurors_len: BalanceOf = jurors_num.saturated_into(); - T::StakeWeight::get().saturating_mul(jurors_len) - } - - // Retrieves a juror from the storage - fn juror(account_id: &T::AccountId) -> Result { - Jurors::::get(account_id).ok_or_else(|| Error::::JurorDoesNotExists.into()) - } - - // # Manages tardy jurors and returns valid winners and valid losers. - // - // ## Management - // - // * Jurors that didn't vote within `CourtCaseDuration` or didn't vote at all are - // placed as tardy. - // - // * Slashes 20% of staked funds and removes tardy jurors that didn't vote or voted - // after the maximum allowed block. - // - // ## Returned list of accounts - // - // All new and old tardy jurors, excluding the ones that voted within `CourtCaseDuration`, - // are removed from the list of accounts that will be slashed to reward winners. Already - // tardy jurors that voted again on the second most voted outcome are also removed from the - // same list. - // - // In other words, does not yield slashed accounts, winners of the losing side, - // accounts that didn't vote or accounts that voted after the maximum allowed block - fn manage_tardy_jurors<'a, 'b, F>( - requested_jurors: &'a [( - T::AccountId, - Juror, - T::BlockNumber, - Option<&(T::BlockNumber, OutcomeReport)>, - )], - mut cb: F, - ) -> Result, DispatchError> - where - F: FnMut(&OutcomeReport) -> bool, - 'a: 'b, - { - let mut valid_winners_and_losers = Vec::with_capacity(requested_jurors.len()); - let treasury_account_id = Self::treasury_account_id(); - - let slash_and_remove_juror = |ai: &T::AccountId| { - let all_reserved = CurrencyOf::::reserved_balance_named(&Self::reserve_id(), ai); - // Unsigned division will never overflow - let slash = all_reserved - .checked_div(&BalanceOf::::from(TARDY_PUNISHMENT_DIVISOR)) - .ok_or(DispatchError::Other("Zero division"))?; - let _ = CurrencyOf::::repatriate_reserved_named( - &Self::reserve_id(), - ai, - &treasury_account_id, - slash, - BalanceStatus::Free, - )?; - Self::remove_juror_from_all_courts_of_all_markets(ai); - Ok::<_, DispatchError>(()) - }; - - for (ai, juror, max_block, vote_opt) in requested_jurors { - if let Some((block, outcome)) = vote_opt { - let vote_is_expired = block > max_block; - if vote_is_expired { - // Tardy juror voted after maximum allowed block. Slash - if let JurorStatus::Tardy = juror.status { - slash_and_remove_juror(ai)?; - } - // Ordinary juror voted after maximum allowed block. Set as tardy - else { - Self::set_stored_juror_as_tardy(ai)?; - } - } else { - let has_voted_on_the_second_most_outcome = cb(outcome); - if has_voted_on_the_second_most_outcome { - // Don't set already tardy juror as tardy again - if JurorStatus::Tardy != juror.status { - Self::set_stored_juror_as_tardy(ai)?; - } - } else { - valid_winners_and_losers.push((ai, outcome)); - } - } - // Tardy juror didn't vote. Slash - } else if let JurorStatus::Tardy = juror.status { - slash_and_remove_juror(ai)?; + /// The court has a specific vote item type. + /// We ensure that the vote item matches the predefined vote item type. + pub(crate) fn check_vote_item( + court: &CourtOf, + vote_item: &VoteItem, + ) -> Result<(), DispatchError> { + match court.vote_item_type { + VoteItemType::Outcome => { + ensure!( + matches!(vote_item, VoteItem::Outcome(_)), + Error::::InvalidVoteItemForOutcomeCourt + ); } - // Ordinary juror didn't vote. Set as tardy - else { - Self::set_stored_juror_as_tardy(ai)?; + VoteItemType::Binary => { + ensure!( + matches!(vote_item, VoteItem::Binary(_)), + Error::::InvalidVoteItemForBinaryCourt + ); } - } + }; - Ok(valid_winners_and_losers) + Ok(()) } - // Modifies a stored juror. - fn mutate_juror(account_id: &T::AccountId, mut cb: F) -> DispatchResult - where - F: FnMut(&mut Juror) -> DispatchResult, - { - Jurors::::try_mutate(account_id, |opt| { - if let Some(el) = opt { - cb(el)?; - } else { - return Err(Error::::JurorDoesNotExists.into()); - } - Ok(()) - }) + // Get a random seed based on a nonce. + pub(crate) fn get_random_seed(nonce: u64) -> [u8; 32] { + debug_assert!( + !>::block_number().is_zero(), + "When testing with the randomness of the collective flip pallet it produces a \ + underflow (block number substraction by one) panic if the block number is zero." + ); + let mut seed = [0; 32]; + let (random_hash, _) = T::Random::random(&nonce.to_le_bytes()); + seed.copy_from_slice(&random_hash.as_ref()[..32]); + seed } - // Calculates the necessary number of jurors depending on the number of market disputes. - // - // Result is capped to `usize::MAX` or in other words, capped to a very, very, very - // high number of jurors. - fn necessary_jurors_num(disputes: &[MarketDispute]) -> usize { - let len = disputes.len(); - INITIAL_JURORS_NUM.saturating_add(SUBSEQUENT_JURORS_FACTOR.saturating_mul(len)) + // Returns a cryptographically secure random number generator + // implementation based on the seed provided by the `Config::Random` type + // and the `SelectionNonce` storage. + pub(crate) fn rng() -> impl RngCore { + let nonce = >::mutate(|n| { + let rslt = *n; + *n = n.wrapping_add(1); + rslt + }); + let random_seed = Self::get_random_seed(nonce); + ChaCha20Rng::from_seed(random_seed) + } + + // Calculates the necessary number of draws depending on the number of market appeals. + pub fn necessary_draws_weight(appeals_len: usize) -> usize { + // 2^(appeals_len) * 31 + 2^(appeals_len) - 1 + // MaxAppeals - 1 (= 3) example: 2^3 * 31 + 2^3 - 1 = 255 + APPEAL_BASIS + .saturating_pow(appeals_len as u32) + .saturating_mul(INITIAL_DRAWS_NUM) + .saturating_add(APPEAL_BASIS.saturating_pow(appeals_len as u32).saturating_sub(1)) } - // Every juror that not voted on the first or second most voted outcome are slashed. + // Slash the losers and use the slashed amount plus the reward pot to reward the winners. fn slash_losers_to_award_winners( - valid_winners_and_losers: &[(&T::AccountId, &OutcomeReport)], - winner_outcome: &OutcomeReport, - ) -> DispatchResult { - let mut total_incentives = BalanceOf::::from(0u8); - let mut total_winners = BalanceOf::::from(0u8); + court_id: CourtId, + jurors_to_stakes: BTreeMap>, + winner_vote_item: &VoteItem, + ) { + let mut total_incentives = >::zero(); - for (jai, outcome) in valid_winners_and_losers { - if outcome == &winner_outcome { - total_winners = total_winners.saturating_add(BalanceOf::::from(1u8)); - } else { - let all_reserved = - CurrencyOf::::reserved_balance_named(&Self::reserve_id(), jai); - // Unsigned division will never overflow - let slash = all_reserved - .checked_div(&BalanceOf::::from(2u8)) - .ok_or(DispatchError::Other("Zero division"))?; - CurrencyOf::::slash_reserved_named(&Self::reserve_id(), jai, slash); - total_incentives = total_incentives.saturating_add(slash); + let slash_all_delegators = + |delegations: &[(T::AccountId, BalanceOf)]| -> NegativeImbalanceOf { + let mut total_imb = >::zero(); + for (delegator, d_slashable) in delegations.iter() { + let (imb, missing) = T::Currency::slash(delegator, *d_slashable); + total_imb.subsume(imb); + debug_assert!( + missing.is_zero(), + "Could not slash all of the amount for delegator {:?}.", + delegator + ); + } + total_imb + }; + + let mut total_winner_stake = BalanceOf::::zero(); + let mut winners = Vec::<(T::AccountId, BalanceOf)>::new(); + for (juror, JurorVoteWithStakes { self_info, delegations }) in jurors_to_stakes.iter() { + match self_info { + Some(SelfInfo { slashable, vote_item }) => { + if vote_item == winner_vote_item { + winners.push((juror.clone(), *slashable)); + total_winner_stake = total_winner_stake.saturating_add(*slashable); + + winners.extend(delegations.clone()); + let total_delegation_stake = delegations + .iter() + .fold(BalanceOf::::zero(), |acc, (_, delegator_stake)| { + acc.saturating_add(*delegator_stake) + }); + total_winner_stake = + total_winner_stake.saturating_add(total_delegation_stake); + } else { + let (imb, missing) = T::Currency::slash(juror, *slashable); + total_incentives.subsume(imb); + debug_assert!( + missing.is_zero(), + "Could not slash all of the amount for juror {:?}.", + juror + ); + + let imb = slash_all_delegators(delegations.as_slice()); + total_incentives.subsume(imb); + } + } + None => { + // in this case the delegators have delegated their vote + // to a tardy or denounced juror + let imb = slash_all_delegators(delegations.as_slice()); + total_incentives.subsume(imb); + } } } - let individual_winner_incentive = - if let Some(e) = total_incentives.checked_div(&total_winners) { - e - } else { - // No winners - return Ok(()); - }; + // reward from denounce slashes and tardy jurors of this market / court + let reward_pot = Self::reward_pot(court_id); + let reward = T::Currency::free_balance(&reward_pot); + let (imb, missing) = T::Currency::slash(&reward_pot, reward); + debug_assert!(missing.is_zero(), "Could not slash all of the amount for reward pot."); + total_incentives.subsume(imb); - for (jai, outcome) in valid_winners_and_losers { - if outcome == &winner_outcome { - CurrencyOf::::deposit_into_existing(jai, individual_winner_incentive)?; - } + let total_reward = total_incentives.peek(); + for (winner, risked_amount) in winners { + let r = risked_amount.saturated_into::(); + let t = total_winner_stake.saturated_into::(); + let share = Perquintill::from_rational(r, t); + let reward_per_each = (share * total_reward.saturated_into::()) + .saturated_into::>(); + let (actual_reward, leftover) = total_incentives.split(reward_per_each); + total_incentives = leftover; + T::Currency::resolve_creating(&winner, actual_reward); } - Ok(()) + if !total_incentives.peek().is_zero() { + // if there are no winners reward the treasury + T::Slash::on_unbalanced(total_incentives); + } } - // For market resolution based on the votes of a market - fn two_best_outcomes( - votes: &[(T::AccountId, (T::BlockNumber, OutcomeReport))], - ) -> Result<(OutcomeReport, Option), DispatchError> { - let mut scores = BTreeMap::::new(); + // Returns the winner of the current court round. + // If there is no element inside `draws`, returns `None`. + // If the best two vote items have the same score, returns the last court round winner. + pub(crate) fn get_winner( + draws: &[DrawOf], + last_winner: Option, + ) -> Option { + let mut scores = BTreeMap::::new(); - for (_, (_, outcome_report)) in votes { - if let Some(el) = scores.get_mut(outcome_report) { - *el = el.saturating_add(1); - } else { - scores.insert(outcome_report.clone(), 1); + for draw in draws { + if let Vote::Revealed { commitment: _, vote_item, salt: _ } = &draw.vote { + if let Some(el) = scores.get_mut(vote_item) { + *el = el.saturating_add(draw.weight); + } else { + scores.insert(vote_item.clone(), draw.weight); + } } } let mut iter = scores.iter(); - - let mut best_score = if let Some(first) = iter.next() { - first - } else { - return Err(Error::::NoVotes.into()); - }; - + let mut best_score = iter.next()?; let mut second_best_score = if let Some(second) = iter.next() { if second.1 > best_score.1 { + let new_second = best_score; best_score = second; - best_score + new_second } else { second } } else { - return Ok((best_score.0.clone(), None)); + return Some(best_score.0.clone()); }; for el in iter { if el.1 > best_score.1 { - best_score = el; second_best_score = best_score; + best_score = el; } else if el.1 > second_best_score.1 { second_best_score = el; } } - Ok((best_score.0.clone(), Some(second_best_score.0.clone()))) + if best_score.1 == second_best_score.1 { + return last_winner; + } + + Some(best_score.0.clone()) } - // Obliterates all stored references of a juror un-reserving balances. - fn remove_juror_from_all_courts_of_all_markets(ai: &T::AccountId) { - CurrencyOf::::unreserve_all_named(&Self::reserve_id(), ai); - Jurors::::remove(ai); - let mut market_ids = BTreeSet::new(); - market_ids.extend(RequestedJurors::::iter().map(|el| el.0)); - for market_id in &market_ids { - RequestedJurors::::remove(market_id, ai); - } - market_ids.clear(); - market_ids.extend(Votes::::iter().map(|el| el.0)); - for market_id in &market_ids { - Votes::::remove(market_id, ai); + // Returns the vote item, on which the market would resolve + // if the current court round is the final (not appealed) court round. + pub(crate) fn get_latest_winner_vote_item( + court_id: CourtId, + last_draws: &[DrawOf], + ) -> Result { + let court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + let last_winner: Option = court + .appeals + .last() + .map(|appeal_info| Some(appeal_info.appealed_vote_item.clone())) + .unwrap_or(None); + let market_id = >::get(court_id) + .ok_or(Error::::CourtIdToMarketIdNotFound)?; + let market = T::MarketCommons::market(&market_id)?; + let report = market.report.as_ref().ok_or(Error::::MarketReportNotFound)?; + let default_vote_item = VoteItem::Outcome(report.outcome.clone()); + let winner_vote_item = + Self::get_winner(last_draws, last_winner).unwrap_or(default_vote_item); + Ok(winner_vote_item) + } + + // Check if the (juror, vote_item, salt) combination matches the secret hash of the vote. + pub(crate) fn compare_commitment( + hashed_commitment: T::Hash, + raw_commitment: RawCommitmentOf, + ) -> DispatchResult { + let RawCommitment { juror, vote_item, salt } = raw_commitment; + + ensure!( + hashed_commitment == T::Hashing::hash_of(&(juror, vote_item, salt)), + Error::::CommitmentHashMismatch + ); + + Ok(()) + } + + // Convert the raw commitment to a hashed commitment, + // and check if it matches with the secret hash of the vote. + // Otherwise return an error. + pub(crate) fn get_hashed_commitment( + vote: VoteOf, + raw_commitment: RawCommitmentOf, + ) -> Result { + match vote { + Vote::Secret { commitment } => { + Self::compare_commitment(commitment, raw_commitment)?; + Ok(commitment) + } + Vote::Drawn => Err(Error::::JurorDidNotVote.into()), + Vote::Delegated { delegated_stakes: _ } => Err(Error::::JurorDelegated.into()), + Vote::Revealed { commitment: _, vote_item: _, salt: _ } => { + Err(Error::::VoteAlreadyRevealed.into()) + } + Vote::Denounced { commitment: _, vote_item: _, salt: _ } => { + Err(Error::::VoteAlreadyDenounced.into()) + } } } } + impl DisputeMaxWeightApi for Pallet + where + T: Config, + { + fn on_dispute_max_weight() -> Weight { + T::WeightInfo::on_dispute(T::MaxCourtParticipants::get(), CacheSize::get()) + } + + fn on_resolution_max_weight() -> Weight { + T::WeightInfo::on_resolution(T::MaxSelectedDraws::get()) + } + + fn exchange_max_weight() -> Weight { + T::WeightInfo::exchange(T::MaxAppeals::get()) + } + + fn get_auto_resolve_max_weight() -> Weight { + T::WeightInfo::get_auto_resolve() + } + + fn has_failed_max_weight() -> Weight { + T::WeightInfo::has_failed() + } + + fn on_global_dispute_max_weight() -> Weight { + T::WeightInfo::on_global_dispute(T::MaxAppeals::get(), T::MaxSelectedDraws::get()) + } + + fn clear_max_weight() -> Weight { + T::WeightInfo::clear(T::MaxSelectedDraws::get()) + } + } + impl DisputeApi for Pallet where T: Config, { type AccountId = T::AccountId; type Balance = BalanceOf; + type NegativeImbalance = NegativeImbalanceOf; type BlockNumber = T::BlockNumber; type MarketId = MarketIdOf; type Moment = MomentOf; type Origin = T::RuntimeOrigin; fn on_dispute( - disputes: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOf, - ) -> DispatchResult { + ) -> Result, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Court, Error::::MarketDoesNotHaveCourtMechanism ); - let jurors: Vec<_> = Jurors::::iter().collect(); - let necessary_jurors_num = Self::necessary_jurors_num(disputes); - let mut rng = Self::rng(); - let random_jurors = Self::random_jurors(&jurors, necessary_jurors_num, &mut rng); - let curr_block_num = >::block_number(); - let block_limit = curr_block_num.saturating_add(T::CourtCaseDuration::get()); - for (ai, _) in random_jurors { - RequestedJurors::::insert(market_id, ai, block_limit); - } - Ok(()) + + let court_id = >::get(); + let next_court_id = + court_id.checked_add(One::one()).ok_or(Error::::MaxCourtIdReached)?; + + let appeal_number = 0usize; + let pool_len = >::decode_len().unwrap_or(0) as u32; + let new_draws = Self::select_participants(appeal_number)?; + + let now = >::block_number(); + let request_block = >::get(); + debug_assert!(request_block >= now, "Request block must be greater than now."); + let round_timing = RoundTiming { + pre_vote: request_block, + vote: T::VotePeriod::get(), + aggregation: T::AggregationPeriod::get(), + appeal: T::AppealPeriod::get(), + }; + + let vote_item_type = VoteItemType::Outcome; + // sets round ends one after the other from now + let court = CourtInfo::new(round_timing, vote_item_type); + + let ids_len = + T::DisputeResolution::add_auto_resolve(market_id, court.round_ends.appeal)?; + + >::insert(court_id, new_draws); + >::insert(court_id, court); + >::insert(market_id, court_id); + >::insert(court_id, market_id); + >::put(next_court_id); + + let res = ResultWithWeightInfo { + result: (), + weight: T::WeightInfo::on_dispute(pool_len, ids_len), + }; + + Ok(res) } - // Set jurors that sided on the second most voted outcome as tardy. Jurors are only - // rewarded if sided on the most voted outcome but jurors that voted second most - // voted outcome (winner of the losing majority) are placed as tardy instead of - // being slashed. fn on_resolution( - _: &[MarketDispute], market_id: &Self::MarketId, market: &MarketOf, - ) -> Result, DispatchError> { + ) -> Result>, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Court, Error::::MarketDoesNotHaveCourtMechanism ); - let votes: Vec<_> = Votes::::iter_prefix(market_id).collect(); - let requested_jurors: Vec<_> = RequestedJurors::::iter_prefix(market_id) - .map(|(juror_id, max_allowed_block)| { - let juror = Self::juror(&juror_id)?; - let vote_opt = votes.iter().find(|el| el.0 == juror_id).map(|el| &el.1); - Ok((juror_id, juror, max_allowed_block, vote_opt)) - }) - .collect::>()?; - let (first, second_opt) = Self::two_best_outcomes(&votes)?; - let valid_winners_and_losers = if let Some(second) = second_opt { - Self::manage_tardy_jurors(&requested_jurors, |outcome| outcome == &second)? - } else { - Self::manage_tardy_jurors(&requested_jurors, |_| false)? + + let court_id = >::get(market_id) + .ok_or(Error::::MarketIdToCourtIdNotFound)?; + let mut court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + let draws = SelectedDraws::::get(court_id); + let draws_len = draws.len() as u32; + let winner_vote_item = Self::get_latest_winner_vote_item(court_id, draws.as_slice())?; + Self::unlock_participants_from_last_draw(court_id, draws); + court.status = CourtStatus::Closed { winner: winner_vote_item.clone() }; + >::insert(court_id, court); + + let winner_outcome = + winner_vote_item.into_outcome().ok_or(Error::::WinnerVoteItemIsNoOutcome)?; + + let res = ResultWithWeightInfo { + result: Some(winner_outcome), + weight: T::WeightInfo::on_resolution(draws_len), }; - Self::slash_losers_to_award_winners(&valid_winners_and_losers, &first)?; - let _ = Votes::::clear_prefix(market_id, u32::max_value(), None); - let _ = RequestedJurors::::clear_prefix(market_id, u32::max_value(), None); - Ok(Some(first)) + + Ok(res) } - fn get_auto_resolve( - _: &[MarketDispute], - _: &Self::MarketId, + fn exchange( + market_id: &Self::MarketId, market: &MarketOf, - ) -> Result, DispatchError> { + resolved_outcome: &OutcomeReport, + mut overall_imbalance: NegativeImbalanceOf, + ) -> Result>, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Court, Error::::MarketDoesNotHaveCourtMechanism ); - Ok(None) + + let court_id = >::get(market_id) + .ok_or(Error::::MarketIdToCourtIdNotFound)?; + let court = >::get(court_id).ok_or(Error::::CourtNotFound)?; + let appeals_len = court.appeals.len() as u32; + for AppealInfo { backer, bond, appealed_vote_item } in &court.appeals { + let appealed_vote_item_as_outcome = appealed_vote_item + .clone() + .into_outcome() + .ok_or(Error::::AppealedVoteItemIsNoOutcome)?; + if resolved_outcome == &appealed_vote_item_as_outcome { + let (imb, missing) = + T::Currency::slash_reserved_named(&Self::reserve_id(), backer, *bond); + debug_assert!(missing.is_zero()); + overall_imbalance.subsume(imb); + } else { + T::Currency::unreserve_named(&Self::reserve_id(), backer, *bond); + } + } + + let res = ResultWithWeightInfo { + result: overall_imbalance, + weight: T::WeightInfo::exchange(appeals_len), + }; + + Ok(res) + } + + fn get_auto_resolve( + market_id: &Self::MarketId, + market: &MarketOf, + ) -> ResultWithWeightInfo> { + let mut res = + ResultWithWeightInfo { result: None, weight: T::WeightInfo::get_auto_resolve() }; + + if market.dispute_mechanism != MarketDisputeMechanism::Court { + return res; + } + + if let Some(court_id) = >::get(market_id) { + res.result = >::get(court_id).map(|court| court.round_ends.appeal); + } + + res } fn has_failed( - _: &[MarketDispute], - _: &Self::MarketId, + market_id: &Self::MarketId, market: &MarketOf, - ) -> Result { + ) -> Result, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::Court, Error::::MarketDoesNotHaveCourtMechanism ); - Ok(false) + + let mut has_failed = false; + let now = >::block_number(); + + let court_id = >::get(market_id) + .ok_or(Error::::MarketIdToCourtIdNotFound)?; + + let pool = CourtPool::::get(); + let min_juror_stake = T::MinJurorStake::get().saturated_into::(); + let pool_unconsumed_stake = pool.iter().fold(0u128, |acc, pool_item| { + let unconsumed = pool_item + .stake + .saturating_sub(pool_item.consumed_stake) + .saturated_into::(); + let remainder = unconsumed % min_juror_stake; + let unconsumed = unconsumed.saturating_sub(remainder); + acc.saturating_add(unconsumed) + }); + + match >::get(court_id) { + Some(court) => { + let appeals = &court.appeals; + let appeal_number = appeals.len().saturating_add(1); + let necessary_draws_weight = Self::necessary_draws_weight(appeal_number); + let required_stake = (necessary_draws_weight as u128) + .saturating_mul(T::MinJurorStake::get().saturated_into::()); + let valid_period = Self::check_appealable_market(court_id, &court, now).is_ok(); + + if appeals.is_full() + || (valid_period && (pool_unconsumed_stake < required_stake)) + { + has_failed = true; + } + } + None => { + let report = market.report.as_ref().ok_or(Error::::MarketReportNotFound)?; + let report_block = report.at; + let block_after_dispute_duration = + report_block.saturating_add(market.deadlines.dispute_duration); + let during_dispute_duration = + report_block <= now && now < block_after_dispute_duration; + + let necessary_draws_weight = Self::necessary_draws_weight(0usize); + let required_stake = (necessary_draws_weight as u128) + .saturating_mul(T::MinJurorStake::get().saturated_into::()); + if during_dispute_duration && pool_unconsumed_stake < required_stake { + has_failed = true; + } + } + } + + let res = + ResultWithWeightInfo { result: has_failed, weight: T::WeightInfo::has_failed() }; + + Ok(res) } - } - impl CourtPalletApi for Pallet where T: Config {} + fn on_global_dispute( + market_id: &Self::MarketId, + market: &MarketOf, + ) -> Result< + ResultWithWeightInfo>>, + DispatchError, + > { + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::Court, + Error::::MarketDoesNotHaveCourtMechanism + ); - /// Accounts that stake funds to decide outcomes. - #[pallet::storage] - pub type Jurors = CountedStorageMap<_, Blake2_128Concat, T::AccountId, Juror>; + // oracle outcome is added by pm-pallet + let mut gd_outcomes: Vec> = + Vec::new(); - /// An extra layer of pseudo randomness. - #[pallet::storage] - pub type JurorsSelectionNonce = StorageValue<_, u64, ValueQuery>; + let mut appeals_len = 0u32; + let mut draws_len = 0u32; - /// Selected jurors that should vote a market outcome until a certain block number - #[pallet::storage] - pub type RequestedJurors = StorageDoubleMap< - _, - Blake2_128Concat, - MarketIdOf, - Blake2_128Concat, - T::AccountId, - T::BlockNumber, - >; + // None case can happen if no dispute could be created, + // because there is not enough juror and delegator stake, + // in this case allow a global dispute + if let Some(court_id) = >::get(market_id) { + let court = >::get(court_id).ok_or(Error::::CourtNotFound)?; - /// Votes of market outcomes for disputes - /// - /// Stores the vote block number and the submitted outcome. - #[pallet::storage] - pub type Votes = StorageDoubleMap< - _, - Blake2_128Concat, - MarketIdOf, - Blake2_128Concat, - T::AccountId, - (T::BlockNumber, OutcomeReport), - >; + appeals_len = court.appeals.len() as u32; + + let report = market.report.as_ref().ok_or(Error::::MarketReportNotFound)?; + let oracle_outcome = &report.outcome; + + gd_outcomes = court + .appeals + .iter() + .filter_map(|a| { + match a.appealed_vote_item.clone().into_outcome() { + // oracle outcome is added by pm pallet + Some(outcome) if outcome != *oracle_outcome => { + Some(GlobalDisputeItem { + outcome, + // we have no better global dispute outcome owner + owner: Self::treasury_account_id(), + // initial vote amount + initial_vote_amount: >::zero(), + }) + } + _ => None, + } + }) + .collect::>>(); + + let old_draws = SelectedDraws::::get(court_id); + draws_len = old_draws.len() as u32; + Self::unlock_participants_from_last_draw(court_id, old_draws); + >::remove(court_id); + >::remove(court_id); + } + + let res = ResultWithWeightInfo { + result: gd_outcomes, + weight: T::WeightInfo::on_global_dispute(appeals_len, draws_len), + }; + + Ok(res) + } + + fn clear( + market_id: &Self::MarketId, + market: &MarketOf, + ) -> Result, DispatchError> { + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::Court, + Error::::MarketDoesNotHaveCourtMechanism + ); + + let court_id = >::get(market_id) + .ok_or(Error::::MarketIdToCourtIdNotFound)?; + + let old_draws = SelectedDraws::::get(court_id); + let draws_len = old_draws.len() as u32; + Self::unlock_participants_from_last_draw(court_id, old_draws); + >::remove(court_id); + >::remove(court_id); + + let res = ResultWithWeightInfo { result: (), weight: T::WeightInfo::clear(draws_len) }; + + Ok(res) + } + } + + impl CourtPalletApi for Pallet where T: Config {} + + // No one can own more than `BalanceOf::MAX`, it doesn't matter if this function saturates. + pub fn get_appeal_bond(n: usize) -> BalanceOf + where + T: Config, + { + T::AppealBond::get().saturating_mul( + (APPEAL_BOND_BASIS.saturating_pow(n as u32)).saturated_into::>(), + ) + } } diff --git a/zrml/court/src/mock.rs b/zrml/court/src/mock.rs index 9167f6dc3..d6a366304 100644 --- a/zrml/court/src/mock.rs +++ b/zrml/court/src/mock.rs @@ -18,34 +18,45 @@ #![cfg(test)] -use crate::{self as zrml_court}; +use crate::{self as zrml_court, mock_storage::pallet as mock_storage}; use frame_support::{ - construct_runtime, + construct_runtime, ord_parameter_types, pallet_prelude::{DispatchError, Weight}, parameter_types, - traits::Everything, - BoundedVec, PalletId, + traits::{Everything, Hooks, NeverEnsureOrigin}, + PalletId, }; +use frame_system::{EnsureRoot, EnsureSignedBy}; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, }; use zeitgeist_primitives::{ constants::mock::{ - BlockHashCount, CourtCaseDuration, CourtPalletId, MaxReserves, MinimumPeriod, PmPalletId, - StakeWeight, BASE, + AggregationPeriod, AppealBond, AppealPeriod, BlockHashCount, BlocksPerYear, CourtPalletId, + InflationPeriod, LockId, MaxAppeals, MaxApprovals, MaxCourtParticipants, MaxDelegations, + MaxReserves, MaxSelectedDraws, MinJurorStake, MinimumPeriod, PmPalletId, RequestInterval, + VotePeriod, BASE, }, traits::DisputeResolutionApi, types::{ - AccountIdTest, Asset, Balance, BlockNumber, BlockTest, Hash, Index, Market, MarketDispute, - MarketId, Moment, UncheckedExtrinsicTest, + AccountIdTest, Asset, Balance, BlockNumber, BlockTest, Hash, Index, Market, MarketId, + Moment, UncheckedExtrinsicTest, }, }; pub const ALICE: AccountIdTest = 0; pub const BOB: AccountIdTest = 1; pub const CHARLIE: AccountIdTest = 2; +pub const DAVE: AccountIdTest = 3; +pub const EVE: AccountIdTest = 4; +pub const POOR_PAUL: AccountIdTest = 9; pub const INITIAL_BALANCE: u128 = 1000 * BASE; +pub const SUDO: AccountIdTest = 69; + +ord_parameter_types! { + pub const Sudo: AccountIdTest = SUDO; +} parameter_types! { pub const TreasuryPalletId: PalletId = PalletId(*b"3.141592"); @@ -61,21 +72,26 @@ construct_runtime!( Balances: pallet_balances::{Call, Config, Event, Pallet, Storage}, Court: zrml_court::{Event, Pallet, Storage}, MarketCommons: zrml_market_commons::{Pallet, Storage}, - RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Pallet, Storage}, System: frame_system::{Call, Config, Event, Pallet, Storage}, Timestamp: pallet_timestamp::{Pallet}, + Treasury: pallet_treasury::{Call, Event, Pallet, Storage}, + // Just a mock storage for testing. + MockStorage: mock_storage::{Storage}, } ); -// NoopResolution implements DisputeResolutionApi with no-ops. -pub struct NoopResolution; +// MockResolution implements DisputeResolutionApi with no-ops. +pub struct MockResolution; + +impl mock_storage::Config for Runtime { + type MarketCommons = MarketCommons; +} -impl DisputeResolutionApi for NoopResolution { +impl DisputeResolutionApi for MockResolution { type AccountId = AccountIdTest; type Balance = Balance; type BlockNumber = BlockNumber; type MarketId = MarketId; - type MaxDisputes = u32; type Moment = Moment; fn resolve( @@ -92,35 +108,53 @@ impl DisputeResolutionApi for NoopResolution { } fn add_auto_resolve( - _market_id: &Self::MarketId, - _resolve_at: Self::BlockNumber, + market_id: &Self::MarketId, + resolve_at: Self::BlockNumber, ) -> Result { - Ok(0u32) + let ids_len = >::try_mutate( + resolve_at, + |ids| -> Result { + ids.try_push(*market_id).map_err(|_| DispatchError::Other("Storage Overflow"))?; + Ok(ids.len() as u32) + }, + )?; + Ok(ids_len) } - fn auto_resolve_exists(_market_id: &Self::MarketId, _resolve_at: Self::BlockNumber) -> bool { - false + fn auto_resolve_exists(market_id: &Self::MarketId, resolve_at: Self::BlockNumber) -> bool { + >::get(resolve_at).contains(market_id) } - fn remove_auto_resolve(_market_id: &Self::MarketId, _resolve_at: Self::BlockNumber) -> u32 { - 0u32 - } - - fn get_disputes( - _market_id: &Self::MarketId, - ) -> BoundedVec, Self::MaxDisputes> { - Default::default() + fn remove_auto_resolve(market_id: &Self::MarketId, resolve_at: Self::BlockNumber) -> u32 { + >::mutate(resolve_at, |ids| -> u32 { + ids.retain(|id| id != market_id); + ids.len() as u32 + }) } } impl crate::Config for Runtime { - type CourtCaseDuration = CourtCaseDuration; - type DisputeResolution = NoopResolution; - type RuntimeEvent = (); + type AppealBond = AppealBond; + type BlocksPerYear = BlocksPerYear; + type LockId = LockId; + type Currency = Balances; + type VotePeriod = VotePeriod; + type AggregationPeriod = AggregationPeriod; + type AppealPeriod = AppealPeriod; + type DisputeResolution = MockResolution; + type RuntimeEvent = RuntimeEvent; + type InflationPeriod = InflationPeriod; type MarketCommons = MarketCommons; + type MaxAppeals = MaxAppeals; + type MaxDelegations = MaxDelegations; + type MaxSelectedDraws = MaxSelectedDraws; + type MaxCourtParticipants = MaxCourtParticipants; + type MinJurorStake = MinJurorStake; + type MonetaryGovernanceOrigin = EnsureRoot; type PalletId = CourtPalletId; - type Random = RandomnessCollectiveFlip; - type StakeWeight = StakeWeight; + type Random = MockStorage; + type RequestInterval = RequestInterval; + type Slash = Treasury; type TreasuryPalletId = TreasuryPalletId; type WeightInfo = crate::weights::WeightInfo; } @@ -135,7 +169,7 @@ impl frame_system::Config for Runtime { type BlockWeights = (); type RuntimeCall = RuntimeCall; type DbWeight = (); - type RuntimeEvent = (); + type RuntimeEvent = RuntimeEvent; type Hash = Hash; type Hashing = BlakeTwo256; type Header = Header; @@ -156,7 +190,7 @@ impl pallet_balances::Config for Runtime { type AccountStore = System; type Balance = Balance; type DustRemoval = (); - type RuntimeEvent = (); + type RuntimeEvent = RuntimeEvent; type ExistentialDeposit = (); type MaxLocks = (); type MaxReserves = MaxReserves; @@ -164,8 +198,6 @@ impl pallet_balances::Config for Runtime { type WeightInfo = (); } -impl pallet_randomness_collective_flip::Config for Runtime {} - impl zrml_market_commons::Config for Runtime { type Currency = Balances; type MarketId = MarketId; @@ -180,6 +212,25 @@ impl pallet_timestamp::Config for Runtime { type WeightInfo = (); } +impl pallet_treasury::Config for Runtime { + type ApproveOrigin = EnsureSignedBy; + type Burn = (); + type BurnDestination = (); + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type MaxApprovals = MaxApprovals; + type OnSlash = (); + type PalletId = TreasuryPalletId; + type ProposalBond = (); + type ProposalBondMinimum = (); + type ProposalBondMaximum = (); + type RejectOrigin = EnsureSignedBy; + type SpendFunds = (); + type SpendOrigin = NeverEnsureOrigin; + type SpendPeriod = (); + type WeightInfo = (); +} + pub struct ExtBuilder { balances: Vec<(AccountIdTest, Balance)>, } @@ -191,6 +242,8 @@ impl Default for ExtBuilder { (ALICE, 1_000 * BASE), (BOB, 1_000 * BASE), (CHARLIE, 1_000 * BASE), + (EVE, 1_000 * BASE), + (DAVE, 1_000 * BASE), (Court::treasury_account_id(), 1_000 * BASE), ], } @@ -205,6 +258,30 @@ impl ExtBuilder { .assimilate_storage(&mut t) .unwrap(); - t.into() + let mut t: sp_io::TestExternalities = t.into(); + // required to assert for events + t.execute_with(|| System::set_block_number(1)); + t + } +} + +pub fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + Balances::on_finalize(System::block_number()); + Court::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + + // new block + let parent_block_hash = System::parent_hash(); + let current_digest = System::digest(); + System::initialize(&System::block_number(), &parent_block_hash, ¤t_digest); + System::on_initialize(System::block_number()); + Court::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); } } + +pub fn run_blocks(n: BlockNumber) { + run_to_block(System::block_number() + n); +} diff --git a/zrml/court/src/mock_storage.rs b/zrml/court/src/mock_storage.rs new file mode 100644 index 000000000..13b5818dc --- /dev/null +++ b/zrml/court/src/mock_storage.rs @@ -0,0 +1,62 @@ +// Copyright 2023 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(test)] +#![allow(dead_code)] + +pub use pallet::*; +use parity_scale_codec::Encode; +use sp_runtime::traits::Hash; + +#[frame_support::pallet] +pub(crate) mod pallet { + use core::marker::PhantomData; + use frame_support::pallet_prelude::*; + use zrml_market_commons::MarketCommonsPalletApi; + + pub(crate) type MarketIdOf = + <::MarketCommons as MarketCommonsPalletApi>::MarketId; + + pub type CacheSize = ConstU32<64>; + + #[pallet::config] + pub trait Config: frame_system::Config { + type MarketCommons: MarketCommonsPalletApi; + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + /// Only used for testing the dispute resolution API to prediction-markets + #[pallet::storage] + pub(crate) type MarketIdsPerDisputeBlock = StorageMap< + _, + Twox64Concat, + T::BlockNumber, + BoundedVec, CacheSize>, + ValueQuery, + >; +} + +impl frame_support::traits::Randomness for Pallet { + fn random(subject: &[u8]) -> (T::Hash, T::BlockNumber) { + let block_number = >::block_number(); + let seed = subject.using_encoded(T::Hashing::hash); + + (seed, block_number) + } +} diff --git a/zrml/court/src/tests.rs b/zrml/court/src/tests.rs index 48fc37a10..61fb11d2d 100644 --- a/zrml/court/src/tests.rs +++ b/zrml/court/src/tests.rs @@ -18,25 +18,48 @@ #![cfg(test)] +extern crate alloc; use crate::{ mock::{ - Balances, Court, ExtBuilder, RandomnessCollectiveFlip, Runtime, RuntimeOrigin, System, - ALICE, BOB, CHARLIE, INITIAL_BALANCE, + run_blocks, run_to_block, Balances, Court, ExtBuilder, MarketCommons, Runtime, + RuntimeOrigin, System, ALICE, BOB, CHARLIE, DAVE, EVE, INITIAL_BALANCE, POOR_PAUL, }, - Error, Juror, JurorStatus, Jurors, MarketOf, RequestedJurors, Votes, + mock_storage::pallet::MarketIdsPerDisputeBlock, + types::{CourtStatus, Draw, Vote, VoteItem}, + AppealInfo, BalanceOf, CourtId, CourtIdToMarketId, CourtParticipantInfo, + CourtParticipantInfoOf, CourtPool, CourtPoolItem, CourtPoolOf, Courts, Error, Event, + MarketIdToCourtId, MarketOf, NegativeImbalanceOf, Participants, RequestBlock, SelectedDraws, }; +use alloc::collections::BTreeMap; use frame_support::{ assert_noop, assert_ok, - traits::{Hooks, NamedReservableCurrency}, + traits::{fungible::Balanced, tokens::imbalance::Imbalance, Currency, NamedReservableCurrency}, }; +use pallet_balances::{BalanceLock, NegativeImbalance}; +use rand::seq::SliceRandom; +use sp_runtime::{ + traits::{BlakeTwo256, Hash, Zero}, + Perquintill, +}; +use test_case::test_case; use zeitgeist_primitives::{ - constants::BASE, + constants::{ + mock::{ + AggregationPeriod, AppealBond, AppealPeriod, InflationPeriod, LockId, MaxAppeals, + MaxCourtParticipants, MinJurorStake, RequestInterval, VotePeriod, + }, + BASE, + }, traits::DisputeApi, types::{ - Asset, Deadlines, Market, MarketBonds, MarketCreation, MarketDisputeMechanism, - MarketPeriod, MarketStatus, MarketType, OutcomeReport, ScoringRule, + AccountIdTest, Asset, Deadlines, GlobalDisputeItem, Market, MarketBonds, MarketCreation, + MarketDisputeMechanism, MarketPeriod, MarketStatus, MarketType, OutcomeReport, Report, + ScoringRule, }, }; +use zrml_market_commons::{Error as MError, MarketCommonsPalletApi}; + +const ORACLE_REPORT: OutcomeReport = OutcomeReport::Scalar(u128::MAX); const DEFAULT_MARKET: MarketOf = Market { base_asset: Asset::Ztg, @@ -51,297 +74,3052 @@ const DEFAULT_MARKET: MarketOf = Market { deadlines: Deadlines { grace_period: 1_u64, oracle_duration: 1_u64, dispute_duration: 1_u64 }, report: None, resolved_outcome: None, - status: MarketStatus::Closed, + status: MarketStatus::Disputed, scoring_rule: ScoringRule::CPMM, - bonds: MarketBonds { creation: None, oracle: None, outsider: None }, + bonds: MarketBonds { creation: None, oracle: None, outsider: None, dispute: None }, }; -const DEFAULT_SET_OF_JURORS: &[(u128, Juror)] = &[ - (7, Juror { status: JurorStatus::Ok }), - (6, Juror { status: JurorStatus::Tardy }), - (5, Juror { status: JurorStatus::Ok }), - (4, Juror { status: JurorStatus::Tardy }), - (3, Juror { status: JurorStatus::Ok }), - (2, Juror { status: JurorStatus::Ok }), - (1, Juror { status: JurorStatus::Ok }), -]; + +fn initialize_court() -> CourtId { + let now = >::block_number(); + >::put(now + RequestInterval::get()); + let amount_alice = 2 * BASE; + let amount_bob = 3 * BASE; + let amount_charlie = 4 * BASE; + let amount_dave = 5 * BASE; + let amount_eve = 6 * BASE; + Court::join_court(RuntimeOrigin::signed(ALICE), amount_alice).unwrap(); + Court::join_court(RuntimeOrigin::signed(BOB), amount_bob).unwrap(); + Court::join_court(RuntimeOrigin::signed(CHARLIE), amount_charlie).unwrap(); + Court::join_court(RuntimeOrigin::signed(DAVE), amount_dave).unwrap(); + Court::join_court(RuntimeOrigin::signed(EVE), amount_eve).unwrap(); + let market_id = MarketCommons::push_market(DEFAULT_MARKET).unwrap(); + MarketCommons::mutate_market(&market_id, |market| { + market.report = Some(Report { at: 1, by: BOB, outcome: ORACLE_REPORT }); + Ok(()) + }) + .unwrap(); + Court::on_dispute(&market_id, &DEFAULT_MARKET).unwrap(); + >::get(market_id).unwrap() +} + +fn fill_juror_pool(jurors_len: u32) { + for i in 0..jurors_len { + let amount = MinJurorStake::get() + i as u128; + let juror = (i + 1000) as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } +} + +fn fill_appeals(court_id: CourtId, appeal_number: usize) { + assert!(appeal_number <= MaxAppeals::get() as usize); + let mut court = Courts::::get(court_id).unwrap(); + let mut number = 0u128; + while (number as usize) < appeal_number { + let appealed_vote_item: VoteItem = VoteItem::Outcome(OutcomeReport::Scalar(number)); + court + .appeals + .try_push(AppealInfo { + backer: number, + bond: crate::get_appeal_bond::(court.appeals.len()), + appealed_vote_item, + }) + .unwrap(); + number += 1; + } + Courts::::insert(court_id, court); +} + +fn put_alice_in_draw(court_id: CourtId, stake: BalanceOf) { + // trick a little bit to let alice be part of the ("random") selection + let mut draws = >::get(court_id); + assert!(!draws.is_empty()); + let slashable = MinJurorStake::get(); + draws[0] = Draw { court_participant: ALICE, weight: 1, vote: Vote::Drawn, slashable }; + >::insert(court_id, draws); + >::insert( + ALICE, + CourtParticipantInfo { + stake, + active_lock: slashable, + prepare_exit_at: None, + delegations: Default::default(), + }, + ); +} + +fn set_alice_after_vote( + outcome: OutcomeReport, +) -> (CourtId, ::Hash, ::Hash) { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + put_alice_in_draw(court_id, amount); + + run_to_block(>::get() + 1); + + let salt = ::Hash::default(); + let vote_item = VoteItem::Outcome(outcome); + let commitment = BlakeTwo256::hash_of(&(ALICE, vote_item, salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment)); + + (court_id, commitment, salt) +} + +fn the_lock(amount: u128) -> BalanceLock { + BalanceLock { id: LockId::get(), amount, reasons: pallet_balances::Reasons::All } +} #[test] fn exit_court_successfully_removes_a_juror_and_frees_balances() { ExtBuilder::default().build().execute_with(|| { - assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE))); - assert_eq!(Jurors::::iter().count(), 1); - assert_eq!(Balances::free_balance(ALICE), 998 * BASE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &ALICE), 2 * BASE); - assert_ok!(Court::exit_court(RuntimeOrigin::signed(ALICE))); - assert_eq!(Jurors::::iter().count(), 0); + let amount = 2 * BASE; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + run_blocks(InflationPeriod::get()); + assert_ok!(Court::exit_court(RuntimeOrigin::signed(ALICE), ALICE)); + assert_eq!(Participants::::iter().count(), 0); assert_eq!(Balances::free_balance(ALICE), INITIAL_BALANCE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &ALICE), 0); + assert_eq!(Balances::locks(ALICE), vec![]); }); } #[test] -fn exit_court_will_not_remove_an_unknown_juror() { +fn join_court_successfully_stores_required_data() { ExtBuilder::default().build().execute_with(|| { - assert_noop!( - Court::exit_court(RuntimeOrigin::signed(ALICE)), - Error::::JurorDoesNotExists + let amount = 2 * BASE; + let alice_free_balance_before = Balances::free_balance(ALICE); + let joined_at = >::block_number(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + System::assert_last_event(Event::JurorJoined { juror: ALICE, stake: amount }.into()); + assert_eq!( + Participants::::iter().next().unwrap(), + ( + ALICE, + CourtParticipantInfo { + stake: amount, + active_lock: 0u128, + prepare_exit_at: None, + delegations: Default::default() + } + ) + ); + assert_eq!(Balances::free_balance(ALICE), alice_free_balance_before); + assert_eq!(Balances::locks(ALICE), vec![the_lock(amount)]); + assert_eq!( + CourtPool::::get().into_inner(), + vec![CourtPoolItem { + stake: amount, + court_participant: ALICE, + consumed_stake: 0, + joined_at + }] ); }); } #[test] -fn join_court_reserves_balance_according_to_the_number_of_jurors() { +fn join_court_works_multiple_joins() { ExtBuilder::default().build().execute_with(|| { - assert_eq!(Balances::free_balance(ALICE), 1000 * BASE); - assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE))); - assert_eq!(Balances::free_balance(ALICE), 998 * BASE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &ALICE), 2 * BASE); + let min = MinJurorStake::get(); + let amount = 2 * min; + let joined_at_0 = >::block_number(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_eq!(Balances::locks(ALICE), vec![the_lock(amount)]); + assert_eq!( + CourtPool::::get().into_inner(), + vec![CourtPoolItem { + stake: amount, + court_participant: ALICE, + consumed_stake: 0, + joined_at: joined_at_0 + }] + ); + assert_eq!( + Participants::::iter() + .collect::)>>(), + vec![( + ALICE, + CourtParticipantInfo { + stake: amount, + active_lock: 0u128, + prepare_exit_at: None, + delegations: Default::default() + } + )] + ); + + let joined_at_1 = >::block_number(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB), amount)); + assert_eq!(Balances::locks(BOB), vec![the_lock(amount)]); + assert_eq!( + CourtPool::::get().into_inner(), + vec![ + CourtPoolItem { + stake: amount, + court_participant: ALICE, + consumed_stake: 0, + joined_at: joined_at_0 + }, + CourtPoolItem { + stake: amount, + court_participant: BOB, + consumed_stake: 0, + joined_at: joined_at_1 + } + ] + ); + assert_eq!(Participants::::iter().count(), 2); + assert_eq!( + Participants::::get(ALICE).unwrap(), + CourtParticipantInfo { + stake: amount, + active_lock: 0u128, + prepare_exit_at: None, + delegations: Default::default() + } + ); + assert_eq!( + Participants::::get(BOB).unwrap(), + CourtParticipantInfo { + stake: amount, + active_lock: 0u128, + prepare_exit_at: None, + delegations: Default::default() + } + ); - assert_eq!(Balances::free_balance(BOB), 1000 * BASE); - assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB))); - assert_eq!(Balances::free_balance(BOB), 996 * BASE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &BOB), 4 * BASE); + let higher_amount = amount + 1; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), higher_amount)); + assert_eq!(Balances::locks(BOB), vec![the_lock(amount)]); + assert_eq!(Balances::locks(ALICE), vec![the_lock(higher_amount)]); + assert_eq!( + CourtPool::::get().into_inner(), + vec![ + CourtPoolItem { + stake: amount, + court_participant: BOB, + consumed_stake: 0, + joined_at: joined_at_1 + }, + CourtPoolItem { + stake: higher_amount, + court_participant: ALICE, + consumed_stake: 0, + joined_at: joined_at_0 + }, + ] + ); + assert_eq!(Participants::::iter().count(), 2); + assert_eq!( + Participants::::get(BOB).unwrap(), + CourtParticipantInfo { + stake: amount, + active_lock: 0u128, + prepare_exit_at: None, + delegations: Default::default() + } + ); + assert_eq!( + Participants::::get(ALICE).unwrap(), + CourtParticipantInfo { + stake: higher_amount, + active_lock: 0u128, + prepare_exit_at: None, + delegations: Default::default() + } + ); }); } #[test] -fn join_court_successfully_stores_a_juror() { +fn join_court_saves_consumed_stake_and_active_lock_for_double_join() { ExtBuilder::default().build().execute_with(|| { - assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE))); - assert_eq!( - Jurors::::iter().next().unwrap(), - (ALICE, Juror { status: JurorStatus::Ok }) + let min = MinJurorStake::get(); + let amount = 2 * min; + + let consumed_stake = min; + let active_lock = min + 1; + Participants::::insert( + ALICE, + CourtParticipantInfo { + stake: amount, + active_lock, + prepare_exit_at: None, + delegations: Default::default(), + }, ); + let joined_at = >::block_number(); + let juror_pool = vec![CourtPoolItem { + stake: amount, + court_participant: ALICE, + consumed_stake, + joined_at, + }]; + CourtPool::::put::>(juror_pool.try_into().unwrap()); + + let higher_amount = amount + 1; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), higher_amount)); + assert_eq!(CourtPool::::get().into_inner()[0].consumed_stake, consumed_stake); + assert_eq!(Participants::::get(ALICE).unwrap().active_lock, active_lock); }); } #[test] -fn join_court_will_not_insert_an_already_stored_juror() { +fn join_court_fails_below_min_juror_stake() { ExtBuilder::default().build().execute_with(|| { - assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE))); + let min = MinJurorStake::get(); + let amount = min - 1; assert_noop!( - Court::join_court(RuntimeOrigin::signed(ALICE)), - Error::::JurorAlreadyExists + Court::join_court(RuntimeOrigin::signed(ALICE), amount), + Error::::BelowMinJurorStake ); }); } #[test] -fn on_dispute_denies_non_court_markets() { +fn join_court_fails_if_amount_exceeds_balance() { ExtBuilder::default().build().execute_with(|| { - let mut market = DEFAULT_MARKET; - market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + let min = MinJurorStake::get(); + let amount = min + 1; assert_noop!( - Court::on_dispute(&[], &0, &market), - Error::::MarketDoesNotHaveCourtMechanism + Court::join_court(RuntimeOrigin::signed(POOR_PAUL), amount), + Error::::AmountExceedsBalance ); }); } #[test] -fn on_resolution_denies_non_court_markets() { +fn join_court_fails_amount_below_last_join() { ExtBuilder::default().build().execute_with(|| { - let mut market = DEFAULT_MARKET; - market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + let min = MinJurorStake::get(); + let last_join_amount = 2 * min; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), last_join_amount)); + assert_noop!( - Court::on_resolution(&[], &0, &market), - Error::::MarketDoesNotHaveCourtMechanism + Court::join_court(RuntimeOrigin::signed(ALICE), last_join_amount - 1), + Error::::AmountBelowLastJoin ); }); } #[test] -fn on_dispute_stores_jurors_that_should_vote() { +fn join_court_after_prepare_exit_court() { + ExtBuilder::default().build().execute_with(|| { + let min = MinJurorStake::get(); + let amount = 2 * min; + let now = >::block_number(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + + let p_info = >::get(ALICE).unwrap(); + assert_eq!(Some(now), p_info.prepare_exit_at); + + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount + 1)); + + let p_info = >::get(ALICE).unwrap(); + assert_eq!(None, p_info.prepare_exit_at); + }); +} + +#[test] +fn join_court_fails_amount_below_lowest_juror() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(123); - let _ = Court::join_court(RuntimeOrigin::signed(ALICE)); - let _ = Court::join_court(RuntimeOrigin::signed(BOB)); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); + let min = MinJurorStake::get(); + let min_amount = 2 * min; + + let max_accounts = CourtPoolOf::::bound(); + let max_amount = min_amount + max_accounts as u128; + for i in 1..=max_accounts { + let amount = max_amount - i as u128; + let _ = Balances::deposit(&(i as u128), amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(i as u128), amount)); + } + + assert!(CourtPool::::get().is_full()); + assert_noop!( - Court::join_court(RuntimeOrigin::signed(ALICE)), - Error::::JurorAlreadyExists + Court::join_court(RuntimeOrigin::signed(0u128), min_amount - 1), + Error::::AmountBelowLowestJuror + ); + }); +} + +#[test] +fn prepare_exit_court_works() { + ExtBuilder::default().build().execute_with(|| { + let amount = 2 * BASE; + let joined_at = >::block_number(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_eq!( + CourtPool::::get().into_inner(), + vec![CourtPoolItem { + stake: amount, + court_participant: ALICE, + consumed_stake: 0, + joined_at + }] + ); + + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + System::assert_last_event(Event::ExitPrepared { court_participant: ALICE }.into()); + assert!(CourtPool::::get().into_inner().is_empty()); + }); +} + +#[test] +fn prepare_exit_court_removes_lowest_staked_juror() { + ExtBuilder::default().build().execute_with(|| { + let min = MinJurorStake::get(); + let min_amount = 2 * min; + + for i in 0..CourtPoolOf::::bound() { + let amount = min_amount + i as u128; + let juror = i as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + + let len = CourtPool::::get().into_inner().len(); + assert!( + CourtPool::::get() + .into_inner() + .iter() + .any(|item| item.court_participant == 0u128) ); - assert_eq!(RequestedJurors::::iter().count(), 2); + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(0u128))); + assert_eq!(CourtPool::::get().into_inner().len(), len - 1); + CourtPool::::get().into_inner().iter().for_each(|item| { + assert_ne!(item.court_participant, 0u128); + }); }); } -// Alice is the winner, Bob is tardy and Charlie is the loser #[test] -fn on_resolution_awards_winners_and_slashes_losers() { +fn prepare_exit_court_removes_middle_staked_juror() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(2); - Court::join_court(RuntimeOrigin::signed(ALICE)).unwrap(); - Court::join_court(RuntimeOrigin::signed(BOB)).unwrap(); - Court::join_court(RuntimeOrigin::signed(CHARLIE)).unwrap(); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(BOB), 0, OutcomeReport::Scalar(2)).unwrap(); - Court::vote(RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Scalar(3)).unwrap(); - let _ = Court::on_resolution(&[], &0, &DEFAULT_MARKET).unwrap(); - assert_eq!(Balances::free_balance(ALICE), 998 * BASE + 3 * BASE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &ALICE), 2 * BASE); - assert_eq!(Balances::free_balance(BOB), 996 * BASE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &BOB), 4 * BASE); - assert_eq!(Balances::free_balance(CHARLIE), 994 * BASE); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &CHARLIE), 3 * BASE); + let min = MinJurorStake::get(); + let min_amount = 2 * min; + + for i in 0..CourtPoolOf::::bound() { + let amount = min_amount + i as u128; + let juror = i as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + + let middle_index = (CourtPoolOf::::bound() / 2) as u128; + + let len = CourtPool::::get().into_inner().len(); + assert!( + CourtPool::::get() + .into_inner() + .iter() + .any(|item| item.court_participant == middle_index) + ); + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(middle_index))); + assert_eq!(CourtPool::::get().into_inner().len(), len - 1); + CourtPool::::get().into_inner().iter().for_each(|item| { + assert_ne!(item.court_participant, middle_index); + }); }); } #[test] -fn on_resolution_decides_market_outcome_based_on_the_majority() { +fn prepare_exit_court_removes_highest_staked_juror() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(2); - Court::join_court(RuntimeOrigin::signed(ALICE)).unwrap(); - Court::join_court(RuntimeOrigin::signed(BOB)).unwrap(); - Court::join_court(RuntimeOrigin::signed(CHARLIE)).unwrap(); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(BOB), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Scalar(2)).unwrap(); - let outcome = Court::on_resolution(&[], &0, &DEFAULT_MARKET).unwrap(); - assert_eq!(outcome, Some(OutcomeReport::Scalar(1))); + let min = MinJurorStake::get(); + let min_amount = 2 * min; + + for i in 0..CourtPoolOf::::bound() { + let amount = min_amount + i as u128; + let juror = i as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + + let last_index = (CourtPoolOf::::bound() - 1) as u128; + + let len = CourtPool::::get().into_inner().len(); + assert!( + CourtPool::::get() + .into_inner() + .iter() + .any(|item| item.court_participant == last_index) + ); + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(last_index))); + assert_eq!(CourtPool::::get().into_inner().len(), len - 1); + CourtPool::::get().into_inner().iter().for_each(|item| { + assert_ne!(item.court_participant, last_index); + }); }); } #[test] -fn on_resolution_sets_late_jurors_as_tardy() { +fn join_court_binary_search_sorted_insert_works() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(2); - Court::join_court(RuntimeOrigin::signed(ALICE)).unwrap(); - Court::join_court(RuntimeOrigin::signed(BOB)).unwrap(); - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); - let _ = Court::on_resolution(&[], &0, &DEFAULT_MARKET).unwrap(); - assert_eq!(Jurors::::get(ALICE).unwrap().status, JurorStatus::Ok); - assert_eq!(Jurors::::get(BOB).unwrap().status, JurorStatus::Tardy); + let min = MinJurorStake::get(); + let min_amount = 2 * min; + + let max_accounts = CourtPoolOf::::bound(); + let mut rng = rand::thread_rng(); + let mut random_numbers: Vec = (0u32..max_accounts as u32).collect(); + random_numbers.shuffle(&mut rng); + let max_amount = min_amount + max_accounts as u128; + for i in random_numbers { + let amount = max_amount - i as u128; + let juror = i as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + + let mut last_stake = 0; + for pool_item in CourtPool::::get().into_inner().iter() { + assert!(pool_item.stake >= last_stake); + last_stake = pool_item.stake; + } }); } #[test] -fn on_resolution_sets_jurors_that_voted_on_the_second_most_voted_outcome_as_tardy() { +fn prepare_exit_court_fails_juror_already_prepared_to_exit() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(2); - Court::join_court(RuntimeOrigin::signed(ALICE)).unwrap(); - Court::join_court(RuntimeOrigin::signed(BOB)).unwrap(); - Court::join_court(RuntimeOrigin::signed(CHARLIE)).unwrap(); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(BOB), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Scalar(2)).unwrap(); - let _ = Court::on_resolution(&[], &0, &DEFAULT_MARKET).unwrap(); - assert_eq!(Jurors::::get(CHARLIE).unwrap().status, JurorStatus::Tardy); + let amount = 2 * BASE; + let joined_at = >::block_number(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_eq!( + CourtPool::::get().into_inner(), + vec![CourtPoolItem { + stake: amount, + court_participant: ALICE, + consumed_stake: 0, + joined_at + }] + ); + + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + + assert_noop!( + Court::prepare_exit_court(RuntimeOrigin::signed(ALICE)), + Error::::AlreadyPreparedExit + ); }); } #[test] -fn on_resolution_punishes_tardy_jurors_that_failed_to_vote_a_second_time() { +fn prepare_exit_court_fails_juror_does_not_exist() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(2); - Court::join_court(RuntimeOrigin::signed(ALICE)).unwrap(); - Court::join_court(RuntimeOrigin::signed(BOB)).unwrap(); - Court::set_stored_juror_as_tardy(&BOB).unwrap(); - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); - let _ = Court::on_resolution(&[], &0, &DEFAULT_MARKET).unwrap(); - let join_court_stake = 40000000000; - let slash = join_court_stake / 5; - assert_eq!(Balances::free_balance(Court::treasury_account_id()), INITIAL_BALANCE + slash); - assert_eq!(Balances::free_balance(BOB), INITIAL_BALANCE - slash); - assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &BOB), 0); + assert!(Participants::::iter().next().is_none()); + + assert_noop!( + Court::prepare_exit_court(RuntimeOrigin::signed(ALICE)), + Error::::JurorDoesNotExist + ); }); } #[test] -fn on_resolution_removes_requested_jurors_and_votes() { +fn exit_court_works_without_active_lock() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(2); - Court::join_court(RuntimeOrigin::signed(ALICE)).unwrap(); - Court::join_court(RuntimeOrigin::signed(BOB)).unwrap(); - Court::join_court(RuntimeOrigin::signed(CHARLIE)).unwrap(); - Court::on_dispute(&[], &0, &DEFAULT_MARKET).unwrap(); - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(BOB), 0, OutcomeReport::Scalar(1)).unwrap(); - Court::vote(RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Scalar(2)).unwrap(); - let _ = Court::on_resolution(&[], &0, &DEFAULT_MARKET).unwrap(); - assert_eq!(RequestedJurors::::iter().count(), 0); - assert_eq!(Votes::::iter().count(), 0); + let min = MinJurorStake::get(); + let amount = 2 * min; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert!(!CourtPool::::get().into_inner().is_empty()); + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + assert!(CourtPool::::get().into_inner().is_empty()); + assert!(Participants::::get(ALICE).is_some()); + + run_blocks(InflationPeriod::get()); + + assert_eq!(Balances::locks(ALICE), vec![the_lock(amount)]); + assert_ok!(Court::exit_court(RuntimeOrigin::signed(ALICE), ALICE)); + System::assert_last_event( + Event::ExitedCourt { + court_participant: ALICE, + exit_amount: amount, + active_lock: 0u128, + } + .into(), + ); + assert!(Participants::::iter().next().is_none()); + assert!(Balances::locks(ALICE).is_empty()); }); } #[test] -fn random_jurors_returns_an_unique_different_subset_of_jurors() { +fn exit_court_works_with_active_lock() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(123); + let active_lock = MinJurorStake::get(); + let amount = 3 * active_lock; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert!(!CourtPool::::get().into_inner().is_empty()); + + assert_eq!( + >::get(ALICE).unwrap(), + CourtParticipantInfo { + stake: amount, + active_lock: 0, + prepare_exit_at: None, + delegations: Default::default() + } + ); + // assume that `choose_multiple_weighted` has set the active_lock + >::insert( + ALICE, + CourtParticipantInfo { + stake: amount, + active_lock, + prepare_exit_at: None, + delegations: Default::default(), + }, + ); - let mut rng = Court::rng(); - let random_jurors = Court::random_jurors(DEFAULT_SET_OF_JURORS, 2, &mut rng); - let mut at_least_one_set_is_different = false; + assert_eq!(Balances::locks(ALICE), vec![the_lock(amount)]); - for _ in 0..100 { - setup_blocks(1); + let now = >::block_number(); + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + assert!(CourtPool::::get().into_inner().is_empty()); - let another_set_of_random_jurors = - Court::random_jurors(DEFAULT_SET_OF_JURORS, 2, &mut rng); - let mut iter = another_set_of_random_jurors.iter(); + run_blocks(InflationPeriod::get()); - if let Some(juror) = iter.next() { - at_least_one_set_is_different = random_jurors.iter().all(|el| el != juror); - } else { - continue; + assert_ok!(Court::exit_court(RuntimeOrigin::signed(ALICE), ALICE)); + System::assert_last_event( + Event::ExitedCourt { + court_participant: ALICE, + exit_amount: amount - active_lock, + active_lock, } - for juror in iter { - at_least_one_set_is_different &= random_jurors.iter().all(|el| el != juror); + .into(), + ); + assert_eq!( + Participants::::get(ALICE).unwrap(), + CourtParticipantInfo { + stake: active_lock, + active_lock, + prepare_exit_at: Some(now), + delegations: Default::default() } + ); + assert_eq!(Balances::locks(ALICE), vec![the_lock(active_lock)]); + }); +} - if at_least_one_set_is_different { - break; +#[test] +fn exit_court_fails_juror_does_not_exist() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Court::exit_court(RuntimeOrigin::signed(ALICE), ALICE), + Error::::JurorDoesNotExist + ); + }); +} + +#[test] +fn exit_court_fails_juror_not_prepared_to_exit() { + ExtBuilder::default().build().execute_with(|| { + let amount = 2 * BASE; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + run_blocks(InflationPeriod::get()); + + assert_noop!( + Court::exit_court(RuntimeOrigin::signed(ALICE), ALICE), + Error::::PrepareExitAtNotPresent + ); + }); +} + +#[test] +fn exit_court_fails_if_inflation_period_not_over() { + ExtBuilder::default().build().execute_with(|| { + let amount = 2 * BASE; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + assert_ok!(Court::prepare_exit_court(RuntimeOrigin::signed(ALICE))); + + run_blocks(InflationPeriod::get() - 1); + + assert_noop!( + Court::exit_court(RuntimeOrigin::signed(ALICE), ALICE), + Error::::PrematureExit + ); + }); +} + +#[test] +fn vote_works() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + // trick a little bit to let alice be part of the ("random") selection + let mut draws = >::get(court_id); + assert_eq!( + draws.iter().map(|draw| draw.weight).sum::() as usize, + Court::necessary_draws_weight(0usize) + ); + let slashable = MinJurorStake::get(); + let alice_index = + draws.binary_search_by_key(&ALICE, |draw| draw.court_participant).unwrap_or_else(|j| j); + draws[alice_index] = + Draw { court_participant: ALICE, weight: 1, vote: Vote::Drawn, slashable }; + >::insert(court_id, draws); + >::insert( + ALICE, + CourtParticipantInfo { + stake: amount, + active_lock: slashable, + prepare_exit_at: None, + delegations: Default::default(), + }, + ); + + let old_draws = >::get(court_id); + + run_to_block(>::get() + 1); + + let outcome = OutcomeReport::Scalar(42u128); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, outcome, salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment)); + System::assert_last_event(Event::JurorVoted { court_id, juror: ALICE, commitment }.into()); + + let new_draws = >::get(court_id); + for (i, (old_draw, new_draw)) in old_draws.iter().zip(new_draws.iter()).enumerate() { + if i == alice_index { + continue; + } else { + assert_eq!(old_draw, new_draw); } } + assert_eq!( + old_draws[alice_index].court_participant, + new_draws[alice_index].court_participant + ); + assert_eq!(old_draws[alice_index].weight, new_draws[alice_index].weight); + assert_eq!(old_draws[alice_index].slashable, new_draws[alice_index].slashable); + assert_eq!(old_draws[alice_index].vote, Vote::Drawn); + assert_eq!(new_draws[alice_index].vote, Vote::Secret { commitment }); + }); +} + +#[test] +fn vote_overwrite_works() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + put_alice_in_draw(court_id, amount); - assert!(at_least_one_set_is_different); + run_to_block(>::get() + 1); + + let wrong_outcome = OutcomeReport::Scalar(69u128); + let salt = ::Hash::default(); + let wrong_commitment = BlakeTwo256::hash_of(&(ALICE, wrong_outcome, salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(ALICE), court_id, wrong_commitment)); + assert_eq!( + >::get(court_id)[0].vote, + Vote::Secret { commitment: wrong_commitment } + ); + + run_blocks(1); + + let right_outcome = OutcomeReport::Scalar(42u128); + let new_commitment = BlakeTwo256::hash_of(&(ALICE, right_outcome, salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(ALICE), court_id, new_commitment)); + assert_ne!(wrong_commitment, new_commitment); + assert_eq!( + >::get(court_id)[0].vote, + Vote::Secret { commitment: new_commitment } + ); }); } #[test] -fn random_jurors_returns_a_subset_of_jurors() { +fn vote_fails_if_court_not_found() { + ExtBuilder::default().build().execute_with(|| { + let court_id = 0; + let commitment = ::Hash::default(); + assert_noop!( + Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment), + Error::::CourtNotFound + ); + }); +} + +#[test_case( + Vote::Revealed { + commitment: ::Hash::default(), + vote_item: VoteItem::Outcome(OutcomeReport::Scalar(1u128)), + salt: ::Hash::default(), + }; "revealed" +)] +#[test_case( + Vote::Denounced { + commitment: ::Hash::default(), + vote_item: VoteItem::Outcome(OutcomeReport::Scalar(1u128)), + salt: ::Hash::default(), + }; "denounced" +)] +fn vote_fails_if_vote_state_incorrect( + vote: crate::Vote<::Hash, crate::DelegatedStakesOf>, +) { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + let mut draws = >::get(court_id); + assert!(!draws.is_empty()); + draws[0] = Draw { court_participant: ALICE, weight: 101, vote, slashable: 42u128 }; + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + let outcome = OutcomeReport::Scalar(42u128); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, outcome, salt)); + assert_noop!( + Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment), + Error::::InvalidVoteState + ); + }); +} + +#[test] +fn vote_fails_if_caller_not_in_draws() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let mut draws = >::get(court_id); + draws.retain(|draw| draw.court_participant != ALICE); + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + let outcome = OutcomeReport::Scalar(42u128); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, outcome, salt)); + assert_noop!( + Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment), + Error::::CallerNotInSelectedDraws + ); + }); +} + +#[test] +fn vote_fails_if_not_in_voting_period() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + put_alice_in_draw(court_id, amount); + + run_to_block(>::get() + VotePeriod::get() + 1); + + let outcome = OutcomeReport::Scalar(42u128); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, outcome, salt)); + assert_noop!( + Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment), + Error::::NotInVotingPeriod + ); + }); +} + +#[test] +fn reveal_vote_works() { ExtBuilder::default().build().execute_with(|| { - setup_blocks(123); - let mut rng = Court::rng(); - let random_jurors = Court::random_jurors(DEFAULT_SET_OF_JURORS, 2, &mut rng); - for (_, juror) in random_jurors { - assert!(DEFAULT_SET_OF_JURORS.iter().any(|el| &el.1 == juror)); + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + + // trick a little bit to let alice be part of the ("random") selection + let mut draws = >::get(court_id); + assert_eq!( + draws.iter().map(|draw| draw.weight).sum::() as usize, + Court::necessary_draws_weight(0usize) + ); + let slashable = MinJurorStake::get(); + let alice_index = + draws.binary_search_by_key(&ALICE, |draw| draw.court_participant).unwrap_or_else(|j| j); + draws[alice_index] = + Draw { court_participant: ALICE, weight: 1, vote: Vote::Drawn, slashable }; + >::insert(court_id, draws); + >::insert( + ALICE, + CourtParticipantInfo { + stake: amount, + active_lock: slashable, + prepare_exit_at: None, + delegations: Default::default(), + }, + ); + + run_to_block(>::get() + 1); + + let outcome = OutcomeReport::Scalar(42u128); + let vote_item = VoteItem::Outcome(outcome); + + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, vote_item.clone(), salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment)); + + let old_draws = >::get(court_id); + + run_blocks(VotePeriod::get() + 1); + + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(ALICE), + court_id, + vote_item.clone(), + salt, + )); + System::assert_last_event( + Event::JurorRevealedVote { juror: ALICE, court_id, vote_item: vote_item.clone(), salt } + .into(), + ); + + let new_draws = >::get(court_id); + for (i, (old_draw, new_draw)) in old_draws.iter().zip(new_draws.iter()).enumerate() { + if i == alice_index { + continue; + } + assert_eq!(old_draw, new_draw); } + assert_eq!( + old_draws[alice_index].court_participant, + new_draws[alice_index].court_participant + ); + assert_eq!(old_draws[alice_index].weight, new_draws[alice_index].weight); + assert_eq!(old_draws[alice_index].slashable, new_draws[alice_index].slashable); + assert_eq!(old_draws[alice_index].vote, Vote::Secret { commitment }); + assert_eq!(new_draws[alice_index].vote, Vote::Revealed { commitment, vote_item, salt }); }); } #[test] -fn vote_will_not_accept_unknown_accounts() { +fn reveal_vote_fails_if_caller_not_juror() { ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + >::remove(ALICE); + + let vote_item = VoteItem::Outcome(outcome); + assert_noop!( - Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(0)), - Error::::OnlyJurorsCanVote + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::CallerIsNotACourtParticipant ); }); } #[test] -fn vote_will_stored_outcome_from_a_juror() { +fn reveal_vote_fails_if_court_not_found() { ExtBuilder::default().build().execute_with(|| { - let _ = Court::join_court(RuntimeOrigin::signed(ALICE)); - let _ = Court::vote(RuntimeOrigin::signed(ALICE), 0, OutcomeReport::Scalar(0)); - assert_eq!(Votes::::get(ALICE, 0).unwrap(), (0, OutcomeReport::Scalar(0))); + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + run_blocks(VotePeriod::get() + 1); + + >::remove(court_id); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::CourtNotFound + ); }); } -fn setup_blocks(num_blocks: u32) { - for _ in 0..num_blocks { - let current_block_number = System::block_number() + 1; - let parent_block_hash = System::parent_hash(); - let current_digest = System::digest(); +#[test] +fn reveal_vote_fails_if_not_in_aggregation_period() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); - System::initialize(¤t_block_number, &parent_block_hash, ¤t_digest); - RandomnessCollectiveFlip::on_initialize(current_block_number); - System::finalize(); - System::set_block_number(current_block_number); - } + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::NotInAggregationPeriod + ); + }); +} + +#[test] +fn reveal_vote_fails_if_juror_not_drawn() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + >::mutate(court_id, |draws| { + draws.retain(|draw| draw.court_participant != ALICE); + }); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::CallerNotInSelectedDraws + ); + }); +} + +#[test] +fn reveal_vote_fails_for_invalid_reveal() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + 1); + + let invalid_outcome = OutcomeReport::Scalar(43u128); + let invalid_vote_item = VoteItem::Outcome(invalid_outcome); + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, invalid_vote_item, salt), + Error::::CommitmentHashMismatch + ); + }); +} + +#[test] +fn reveal_vote_fails_for_invalid_salt() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, correct_salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + let incorrect_salt: ::Hash = [42; 32].into(); + assert_ne!(correct_salt, incorrect_salt); + + let vote_item = VoteItem::Outcome(outcome); + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, incorrect_salt), + Error::::CommitmentHashMismatch + ); + }); +} + +#[test] +fn reveal_vote_fails_if_juror_not_voted() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + >::mutate(court_id, |draws| { + draws.iter_mut().for_each(|draw| { + if draw.court_participant == ALICE { + draw.vote = Vote::Drawn; + } + }); + }); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::JurorDidNotVote + ); + }); +} + +#[test] +fn reveal_vote_fails_if_already_revealed() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + let vote_item = VoteItem::Outcome(outcome); + + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(ALICE), + court_id, + vote_item.clone(), + salt + )); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::VoteAlreadyRevealed + ); + }); +} + +#[test] +fn reveal_vote_fails_if_already_denounced() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + let vote_item = VoteItem::Outcome(outcome); + + assert_ok!(Court::denounce_vote( + RuntimeOrigin::signed(BOB), + court_id, + ALICE, + vote_item.clone(), + salt + )); + + run_blocks(VotePeriod::get() + 1); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::VoteAlreadyDenounced + ); + }); +} + +#[test] +fn denounce_vote_works() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, commitment, salt) = set_alice_after_vote(outcome.clone()); + + let old_draws = >::get(court_id); + assert!(old_draws.iter().any(|draw| { + draw.court_participant == ALICE && matches!(draw.vote, Vote::Secret { .. }) + })); + + let free_alice_before = Balances::free_balance(ALICE); + let pot_balance_before = Balances::free_balance(Court::reward_pot(court_id)); + + let vote_item = VoteItem::Outcome(outcome); + + assert_ok!(Court::denounce_vote( + RuntimeOrigin::signed(BOB), + court_id, + ALICE, + vote_item.clone(), + salt + )); + System::assert_last_event( + Event::DenouncedJurorVote { + denouncer: BOB, + juror: ALICE, + court_id, + vote_item: vote_item.clone(), + salt, + } + .into(), + ); + + let new_draws = >::get(court_id); + assert_eq!(old_draws[1..], new_draws[1..]); + assert_eq!(old_draws[0].court_participant, ALICE); + assert_eq!(old_draws[0].court_participant, new_draws[0].court_participant); + assert_eq!(old_draws[0].weight, new_draws[0].weight); + assert_eq!(old_draws[0].slashable, new_draws[0].slashable); + assert_eq!(old_draws[0].vote, Vote::Secret { commitment }); + assert_eq!(new_draws[0].vote, Vote::Denounced { commitment, vote_item, salt }); + + let free_alice_after = Balances::free_balance(ALICE); + let slash = old_draws[0].slashable; + assert!(!slash.is_zero()); + // slash happens in `reassign_court_stakes` + // see `reassign_court_stakes_slashes_tardy_jurors_and_rewards_winners` + assert_eq!(free_alice_after, free_alice_before); + + let pot_balance_after = Balances::free_balance(Court::reward_pot(court_id)); + assert_eq!(pot_balance_after, pot_balance_before); + }); +} + +#[test] +fn denounce_vote_fails_if_self_denounce() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(ALICE), court_id, ALICE, vote_item, salt), + Error::::CallerDenouncedItself + ); + }); +} + +#[test] +fn denounce_vote_fails_if_juror_does_not_exist() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + >::remove(ALICE); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(BOB), court_id, ALICE, vote_item, salt), + Error::::JurorDoesNotExist + ); + }); +} + +#[test] +fn denounce_vote_fails_if_court_not_found() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + >::remove(court_id); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(BOB), court_id, ALICE, vote_item, salt), + Error::::CourtNotFound + ); + }); +} + +#[test] +fn denounce_vote_fails_if_not_in_voting_period() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(BOB), court_id, ALICE, vote_item, salt), + Error::::NotInVotingPeriod + ); + }); +} + +#[test] +fn denounce_vote_fails_if_juror_not_drawn() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + >::mutate(court_id, |draws| { + draws.retain(|draw| draw.court_participant != ALICE); + }); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(BOB), court_id, ALICE, vote_item, salt), + Error::::JurorNotDrawn + ); + }); +} + +#[test] +fn denounce_vote_fails_if_invalid_reveal() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome); + + let invalid_outcome = OutcomeReport::Scalar(69u128); + let invalid_vote_item = VoteItem::Outcome(invalid_outcome); + assert_noop!( + Court::denounce_vote( + RuntimeOrigin::signed(BOB), + court_id, + ALICE, + invalid_vote_item, + salt + ), + Error::::CommitmentHashMismatch + ); + }); +} + +#[test] +fn denounce_vote_fails_if_juror_not_voted() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + >::mutate(court_id, |draws| { + draws.iter_mut().for_each(|draw| { + if draw.court_participant == ALICE { + draw.vote = Vote::Drawn; + } + }); + }); + + let vote_item = VoteItem::Outcome(outcome); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(BOB), court_id, ALICE, vote_item, salt), + Error::::JurorDidNotVote + ); + }); +} + +#[test] +fn denounce_vote_fails_if_vote_already_revealed() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + run_blocks(VotePeriod::get() + 1); + + let vote_item = VoteItem::Outcome(outcome); + + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(ALICE), + court_id, + vote_item.clone(), + salt + )); + + assert_noop!( + Court::reveal_vote(RuntimeOrigin::signed(ALICE), court_id, vote_item, salt), + Error::::VoteAlreadyRevealed + ); + }); +} + +#[test] +fn denounce_vote_fails_if_vote_already_denounced() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, salt) = set_alice_after_vote(outcome.clone()); + + let vote_item = VoteItem::Outcome(outcome); + + assert_ok!(Court::denounce_vote( + RuntimeOrigin::signed(BOB), + court_id, + ALICE, + vote_item.clone(), + salt + )); + + assert_noop!( + Court::denounce_vote(RuntimeOrigin::signed(CHARLIE), court_id, ALICE, vote_item, salt), + Error::::VoteAlreadyDenounced + ); + }); +} + +#[test] +fn appeal_updates_round_ends() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + let last_court = >::get(court_id).unwrap(); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let now = >::block_number(); + let court = >::get(court_id).unwrap(); + + let request_block = >::get(); + assert!(now < request_block); + assert_eq!(court.round_ends.pre_vote, request_block); + assert_eq!(court.round_ends.vote, request_block + VotePeriod::get()); + assert_eq!( + court.round_ends.aggregation, + request_block + VotePeriod::get() + AggregationPeriod::get() + ); + assert_eq!( + court.round_ends.appeal, + request_block + VotePeriod::get() + AggregationPeriod::get() + AppealPeriod::get() + ); + + assert!(last_court.round_ends.pre_vote < court.round_ends.pre_vote); + assert!(last_court.round_ends.vote < court.round_ends.vote); + assert!(last_court.round_ends.aggregation < court.round_ends.aggregation); + assert!(last_court.round_ends.appeal < court.round_ends.appeal); + }); +} + +#[test] +fn appeal_reserves_get_appeal_bond() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let free_charlie_before = Balances::free_balance(CHARLIE); + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let free_charlie_after = Balances::free_balance(CHARLIE); + let bond = crate::get_appeal_bond::(1usize); + assert!(!bond.is_zero()); + assert_eq!(free_charlie_after, free_charlie_before - bond); + assert_eq!(Balances::reserved_balance(CHARLIE), bond); + }); +} + +#[test] +fn appeal_emits_event() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + System::assert_last_event(Event::CourtAppealed { court_id, appeal_number: 1u32 }.into()); + }); +} + +#[test] +fn appeal_shifts_auto_resolve() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + let resolve_at_0 = >::get(court_id).unwrap().round_ends.appeal; + assert_eq!(MarketIdsPerDisputeBlock::::get(resolve_at_0), vec![0]); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let resolve_at_1 = >::get(court_id).unwrap().round_ends.appeal; + assert_eq!(MarketIdsPerDisputeBlock::::get(resolve_at_1), vec![0]); + assert_ne!(resolve_at_0, resolve_at_1); + assert_eq!(MarketIdsPerDisputeBlock::::get(resolve_at_0), vec![]); + }); +} + +#[test] +fn appeal_overrides_last_draws() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + let last_draws = >::get(court_id); + assert!(!last_draws.len().is_zero()); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let draws = >::get(court_id); + assert_ne!(draws, last_draws); + }); +} + +#[test] +fn appeal_draws_total_weight_is_correct() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + let last_draws = >::get(court_id); + let last_draws_total_weight = last_draws.iter().map(|draw| draw.weight).sum::(); + assert_eq!(last_draws_total_weight, Court::necessary_draws_weight(0usize) as u32); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let neccessary_juror_weight = Court::necessary_draws_weight(1usize) as u32; + let draws = >::get(court_id); + let draws_total_weight = draws.iter().map(|draw| draw.weight).sum::(); + assert_eq!(draws_total_weight, neccessary_juror_weight); + }); +} + +#[test] +fn appeal_get_latest_resolved_outcome_changes() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let last_appealed_vote_item = >::get(court_id) + .unwrap() + .appeals + .last() + .unwrap() + .appealed_vote_item + .clone(); + + let request_block = >::get(); + run_to_block(request_block + 1); + let outcome = OutcomeReport::Scalar(69u128); + let vote_item = VoteItem::Outcome(outcome.clone()); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, vote_item, salt)); + + // cheat a little to get alice in the draw for the new appeal + put_alice_in_draw(court_id, MinJurorStake::get()); + assert_ok!(Court::vote(RuntimeOrigin::signed(ALICE), court_id, commitment)); + + run_blocks(VotePeriod::get() + 1); + + let vote_item = VoteItem::Outcome(outcome); + + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(ALICE), + court_id, + vote_item.clone(), + salt + )); + + run_blocks(AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let new_appealed_vote_item = >::get(court_id) + .unwrap() + .appeals + .last() + .unwrap() + .appealed_vote_item + .clone(); + + // if the new appealed outcome were the last appealed outcome, + // then the wrong appealed outcome was added in `appeal` + assert_eq!(new_appealed_vote_item, vote_item); + assert_ne!(last_appealed_vote_item, new_appealed_vote_item); + }); +} + +#[test] +fn appeal_fails_if_court_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Court::appeal(RuntimeOrigin::signed(CHARLIE), 0), + Error::::CourtNotFound + ); + }); +} + +#[test] +fn appeal_fails_if_appeal_bond_exceeds_balance() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_noop!( + Court::appeal(RuntimeOrigin::signed(POOR_PAUL), court_id), + Error::::AppealBondExceedsBalance + ); + }); +} + +#[test] +fn appeal_fails_if_max_appeals_reached() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + fill_appeals(court_id, MaxAppeals::get() as usize); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_noop!( + Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id), + Error::::MaxAppealsReached + ); + }); +} + +#[test] +fn check_appealable_market_fails_if_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let now = >::block_number(); + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let court = >::get(court_id).unwrap(); + MarketCommons::remove_market(&court_id).unwrap(); + + assert_noop!( + Court::check_appealable_market(court_id, &court, now), + MError::::MarketDoesNotExist + ); + }); +} + +#[test] +fn check_appealable_market_fails_if_dispute_mechanism_wrong() { + ExtBuilder::default().build().execute_with(|| { + let now = >::block_number(); + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let court = >::get(court_id).unwrap(); + + let market_id = >::get(court_id).unwrap(); + MarketCommons::mutate_market(&market_id, |market| { + market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + Ok(()) + }) + .unwrap(); + + assert_noop!( + Court::check_appealable_market(court_id, &court, now), + Error::::MarketDoesNotHaveCourtMechanism + ); + }); +} + +#[test] +fn check_appealable_market_fails_if_not_in_appeal_period() { + ExtBuilder::default().build().execute_with(|| { + let now = >::block_number(); + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get()); + + let court = >::get(court_id).unwrap(); + + assert_noop!( + Court::check_appealable_market(court_id, &court, now), + Error::::NotInAppealPeriod + ); + }); +} + +#[test] +fn appeal_last_appeal_just_removes_auto_resolve() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + fill_appeals(court_id, (MaxAppeals::get() - 1) as usize); + + let court = >::get(court_id).unwrap(); + let resolve_at = court.round_ends.appeal; + + let market_id = >::get(court_id).unwrap(); + assert_eq!(MarketIdsPerDisputeBlock::::get(resolve_at), vec![market_id]); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + assert_eq!(MarketIdsPerDisputeBlock::::get(resolve_at), vec![]); + }); +} + +#[test] +fn appeal_adds_last_appeal() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + fill_appeals(court_id, (MaxAppeals::get() - 1) as usize); + + let last_draws = >::get(court_id); + let appealed_vote_item = + Court::get_latest_winner_vote_item(court_id, last_draws.as_slice()).unwrap(); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let court = >::get(court_id).unwrap(); + assert!(court.appeals.is_full()); + + let last_appeal = court.appeals.last().unwrap(); + assert_eq!(last_appeal.appealed_vote_item, appealed_vote_item); + }); +} + +#[test] +fn reassign_court_stakes_slashes_tardy_jurors_and_rewards_winners() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(CHARLIE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(DAVE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(EVE), amount)); + + let outcome = OutcomeReport::Scalar(42u128); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, outcome.clone(), salt)); + + let vote_item = VoteItem::Outcome(outcome); + + let draws: crate::SelectedDrawsOf = vec![ + Draw { + court_participant: ALICE, + weight: 1, + vote: Vote::Drawn, + slashable: MinJurorStake::get(), + }, + Draw { + court_participant: BOB, + weight: 1, + vote: Vote::Secret { commitment }, + slashable: 2 * MinJurorStake::get(), + }, + Draw { + court_participant: CHARLIE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: vote_item.clone(), salt }, + slashable: 3 * MinJurorStake::get(), + }, + Draw { + court_participant: DAVE, + weight: 1, + vote: Vote::Drawn, + slashable: 4 * MinJurorStake::get(), + }, + Draw { + court_participant: EVE, + weight: 1, + vote: Vote::Denounced { commitment, vote_item, salt }, + slashable: 5 * MinJurorStake::get(), + }, + ] + .try_into() + .unwrap(); + let old_draws = draws.clone(); + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + AppealPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let _ = Court::on_resolution(&market_id, &market).unwrap(); + + let free_alice_before = Balances::free_balance(ALICE); + let free_bob_before = Balances::free_balance(BOB); + let free_charlie_before = Balances::free_balance(CHARLIE); + let free_dave_before = Balances::free_balance(DAVE); + let free_eve_before = Balances::free_balance(EVE); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + let free_alice_after = Balances::free_balance(ALICE); + assert_ne!(free_alice_after, free_alice_before); + assert_eq!(free_alice_after, free_alice_before - old_draws[ALICE as usize].slashable); + + let free_bob_after = Balances::free_balance(BOB); + assert_ne!(free_bob_after, free_bob_before); + assert_eq!(free_bob_after, free_bob_before - old_draws[BOB as usize].slashable); + + let free_charlie_after = Balances::free_balance(CHARLIE); + let full_slashes = old_draws[ALICE as usize].slashable + + old_draws[BOB as usize].slashable + + old_draws[DAVE as usize].slashable + + old_draws[EVE as usize].slashable; + assert_eq!(free_charlie_after, free_charlie_before + full_slashes); + + let free_dave_after = Balances::free_balance(DAVE); + assert_ne!(free_dave_after, free_dave_before); + assert_eq!(free_dave_after, free_dave_before - old_draws[DAVE as usize].slashable); + + let free_eve_after = Balances::free_balance(EVE); + assert_ne!(free_eve_after, free_eve_before); + assert_eq!(free_eve_after, free_eve_before - old_draws[EVE as usize].slashable); + }); +} + +#[test] +fn reassign_court_stakes_fails_if_court_not_found() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), 0), + Error::::CourtNotFound + ); + }); +} + +#[test] +fn reassign_court_stakes_emits_event() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let _ = Court::on_resolution(&market_id, &market).unwrap().result.unwrap(); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + System::assert_last_event(Event::StakesReassigned { court_id }.into()); + }); +} + +#[test] +fn reassign_court_stakes_fails_if_juror_stakes_already_reassigned() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let _ = Court::on_resolution(&market_id, &market).unwrap().result.unwrap(); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + assert_noop!( + Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id), + Error::::CourtAlreadyReassigned + ); + }); +} + +#[test] +fn reassign_court_stakes_updates_court_status() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let resolution_outcome = Court::on_resolution(&market_id, &market).unwrap().result.unwrap(); + + let court = >::get(court_id).unwrap(); + let resolution_vote_item = VoteItem::Outcome(resolution_outcome); + assert_eq!(court.status, CourtStatus::Closed { winner: resolution_vote_item }); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + let court = >::get(court_id).unwrap(); + assert_eq!(court.status, CourtStatus::Reassigned); + }); +} + +#[test] +fn reassign_court_stakes_removes_draws() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let _ = Court::on_resolution(&market_id, &market).unwrap().result.unwrap(); + + let draws = >::get(court_id); + assert!(!draws.is_empty()); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + let draws = >::get(court_id); + assert!(draws.is_empty()); + }); +} + +#[test] +fn reassign_court_stakes_fails_if_court_not_closed() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + assert_noop!( + Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id), + Error::::CourtNotClosed + ); + }); +} + +#[test] +fn reassign_court_stakes_decreases_active_lock() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(CHARLIE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(DAVE), amount)); + + let outcome = OutcomeReport::Scalar(42u128); + let vote_item = VoteItem::Outcome(outcome.clone()); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, outcome, salt)); + + let alice_slashable = MinJurorStake::get(); + >::mutate(ALICE, |p_info| { + if let Some(ref mut info) = p_info { + info.active_lock = alice_slashable; + } + }); + let bob_slashable = 2 * MinJurorStake::get(); + >::mutate(BOB, |p_info| { + if let Some(ref mut p_info) = p_info { + p_info.active_lock = bob_slashable; + } + }); + let charlie_slashable = 3 * MinJurorStake::get(); + >::mutate(CHARLIE, |p_info| { + if let Some(ref mut p_info) = p_info { + p_info.active_lock = charlie_slashable; + } + }); + let dave_slashable = 4 * MinJurorStake::get(); + >::mutate(DAVE, |p_info| { + if let Some(ref mut p_info) = p_info { + p_info.active_lock = dave_slashable; + } + }); + + let draws: crate::SelectedDrawsOf = vec![ + Draw { + court_participant: ALICE, + weight: 1, + vote: Vote::Drawn, + slashable: alice_slashable, + }, + Draw { + court_participant: BOB, + weight: 1, + vote: Vote::Secret { commitment }, + slashable: bob_slashable, + }, + Draw { + court_participant: CHARLIE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: vote_item.clone(), salt }, + slashable: charlie_slashable, + }, + Draw { + court_participant: DAVE, + weight: 1, + vote: Vote::Denounced { commitment, vote_item, salt }, + slashable: dave_slashable, + }, + ] + .try_into() + .unwrap(); + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + AppealPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let _ = Court::on_resolution(&market_id, &market).unwrap(); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + assert!(>::get(ALICE).unwrap().active_lock.is_zero()); + assert!(>::get(BOB).unwrap().active_lock.is_zero()); + assert!(>::get(CHARLIE).unwrap().active_lock.is_zero()); + assert!(>::get(DAVE).unwrap().active_lock.is_zero()); + }); +} + +#[test] +fn reassign_court_stakes_slashes_loosers_and_awards_winners() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(CHARLIE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(DAVE), amount)); + + let outcome = OutcomeReport::Scalar(42u128); + let vote_item = VoteItem::Outcome(outcome.clone()); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, vote_item.clone(), salt)); + + let wrong_outcome_0 = OutcomeReport::Scalar(69u128); + let wrong_vote_item_0 = VoteItem::Outcome(wrong_outcome_0); + let wrong_outcome_1 = OutcomeReport::Scalar(56u128); + let wrong_vote_item_1 = VoteItem::Outcome(wrong_outcome_1); + + let alice_slashable = MinJurorStake::get(); + let bob_slashable = 2 * MinJurorStake::get(); + let charlie_slashable = 3 * MinJurorStake::get(); + let dave_slashable = 4 * MinJurorStake::get(); + + let draws: crate::SelectedDrawsOf = vec![ + Draw { + court_participant: ALICE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: vote_item.clone(), salt }, + slashable: alice_slashable, + }, + Draw { + court_participant: BOB, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item_0, salt }, + slashable: bob_slashable, + }, + Draw { + court_participant: CHARLIE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item, salt }, + slashable: charlie_slashable, + }, + Draw { + court_participant: DAVE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item_1, salt }, + slashable: dave_slashable, + }, + ] + .try_into() + .unwrap(); + let last_draws = draws.clone(); + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + AppealPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let resolution_outcome = Court::on_resolution(&market_id, &market).unwrap().result.unwrap(); + assert_eq!(resolution_outcome, outcome); + + let free_alice_before = Balances::free_balance(ALICE); + let free_bob_before = Balances::free_balance(BOB); + let free_charlie_before = Balances::free_balance(CHARLIE); + let free_dave_before = Balances::free_balance(DAVE); + + let reward_pot = Court::reward_pot(court_id); + let tardy_or_denounced_value = 5 * MinJurorStake::get(); + let _ = Balances::deposit(&reward_pot, tardy_or_denounced_value).unwrap(); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + let bob_slashed = last_draws[BOB as usize].slashable; + let dave_slashed = last_draws[DAVE as usize].slashable; + let slashed = bob_slashed + dave_slashed + tardy_or_denounced_value; + + let winners_risked_amount = charlie_slashable + alice_slashable; + + let alice_share = Perquintill::from_rational(alice_slashable, winners_risked_amount); + let free_alice_after = Balances::free_balance(ALICE); + assert_eq!(free_alice_after, free_alice_before + alice_share * slashed); + + let free_bob_after = Balances::free_balance(BOB); + assert_eq!(free_bob_after, free_bob_before - bob_slashed); + + let charlie_share = Perquintill::from_rational(charlie_slashable, winners_risked_amount); + let free_charlie_after = Balances::free_balance(CHARLIE); + assert_eq!(free_charlie_after, free_charlie_before + charlie_share * slashed); + + let free_dave_after = Balances::free_balance(DAVE); + assert_eq!(free_dave_after, free_dave_before - dave_slashed); + + assert!(Balances::free_balance(reward_pot).is_zero()); + }); +} + +#[test] +fn reassign_court_stakes_works_for_delegations() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(CHARLIE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(DAVE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(EVE), amount)); + + let outcome = OutcomeReport::Scalar(42u128); + let vote_item = VoteItem::Outcome(outcome.clone()); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, vote_item.clone(), salt)); + + let wrong_outcome = OutcomeReport::Scalar(69u128); + let wrong_vote_item = VoteItem::Outcome(wrong_outcome); + + let alice_slashable = MinJurorStake::get(); + let bob_slashable = 2 * MinJurorStake::get(); + let charlie_slashable = 3 * MinJurorStake::get(); + let dave_slashable = 3 * MinJurorStake::get(); + let eve_slashable = 5 * MinJurorStake::get(); + + let delegated_stakes_charlie: crate::DelegatedStakesOf = + vec![(ALICE, 2 * MinJurorStake::get()), (BOB, MinJurorStake::get())] + .try_into() + .unwrap(); + + let delegated_stakes_dave: crate::DelegatedStakesOf = + vec![(ALICE, 2 * MinJurorStake::get()), (BOB, MinJurorStake::get())] + .try_into() + .unwrap(); + + let draws: crate::SelectedDrawsOf = vec![ + Draw { + court_participant: ALICE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: vote_item.clone(), salt }, + slashable: alice_slashable, + }, + Draw { + court_participant: EVE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item, salt }, + slashable: eve_slashable, + }, + Draw { + court_participant: BOB, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item, salt }, + slashable: bob_slashable, + }, + Draw { + court_participant: CHARLIE, + weight: 1, + vote: Vote::Delegated { delegated_stakes: delegated_stakes_charlie.clone() }, + slashable: charlie_slashable, + }, + Draw { + court_participant: DAVE, + weight: 1, + vote: Vote::Delegated { delegated_stakes: delegated_stakes_dave.clone() }, + slashable: dave_slashable, + }, + ] + .try_into() + .unwrap(); + let last_draws = draws.clone(); + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + AppealPeriod::get() + 1); + + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let resolution_outcome = Court::on_resolution(&market_id, &market).unwrap().result.unwrap(); + assert_eq!(resolution_outcome, outcome); + + let free_alice_before = Balances::free_balance(ALICE); + let free_bob_before = Balances::free_balance(BOB); + let free_charlie_before = Balances::free_balance(CHARLIE); + let free_dave_before = Balances::free_balance(DAVE); + let free_eve_before = Balances::free_balance(EVE); + + let reward_pot = Court::reward_pot(court_id); + let tardy_or_denounced_value = 5 * MinJurorStake::get(); + let _ = Balances::deposit(&reward_pot, tardy_or_denounced_value).unwrap(); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + let bob_slashed = + last_draws.iter().find(|draw| draw.court_participant == BOB).unwrap().slashable; + let charlie_delegated_bob_slashed = + delegated_stakes_charlie.iter().find(|(acc, _)| *acc == BOB).unwrap().1; + let dave_delegated_bob_slashed = + delegated_stakes_dave.iter().find(|(acc, _)| *acc == BOB).unwrap().1; + let slashed = bob_slashed + + charlie_delegated_bob_slashed + + dave_delegated_bob_slashed + + tardy_or_denounced_value; + + let charlie_delegated_alice_slashable = + delegated_stakes_charlie.iter().find(|(acc, _)| *acc == ALICE).unwrap().1; + let dave_delegated_alice_slashable = + delegated_stakes_dave.iter().find(|(acc, _)| *acc == ALICE).unwrap().1; + let winners_risked_amount = charlie_delegated_alice_slashable + + dave_delegated_alice_slashable + + alice_slashable + + eve_slashable; + + let alice_share = Perquintill::from_rational(alice_slashable, winners_risked_amount); + let free_alice_after = Balances::free_balance(ALICE); + assert_eq!(free_alice_after, free_alice_before + alice_share * slashed); + + let eve_share = Perquintill::from_rational(eve_slashable, winners_risked_amount); + let free_eve_after = Balances::free_balance(EVE); + assert_eq!(free_eve_after, free_eve_before + eve_share * slashed); + + let free_bob_after = Balances::free_balance(BOB); + assert_eq!(free_bob_after, free_bob_before - bob_slashed); + + let charlie_share = + Perquintill::from_rational(charlie_delegated_alice_slashable, winners_risked_amount); + let free_charlie_after = Balances::free_balance(CHARLIE); + let charlie_rewarded = charlie_share * slashed; + assert_eq!( + free_charlie_after, + free_charlie_before + charlie_rewarded - charlie_delegated_bob_slashed + ); + + let dave_share = + Perquintill::from_rational(dave_delegated_alice_slashable, winners_risked_amount); + let free_dave_after = Balances::free_balance(DAVE); + let dave_rewarded = dave_share * slashed; + assert_eq!(free_dave_after, free_dave_before + dave_rewarded - dave_delegated_bob_slashed); + + assert!(Balances::free_balance(reward_pot).is_zero()); + }); +} + +#[test] +fn reassign_court_stakes_rewards_treasury_if_no_winner() { + ExtBuilder::default().build().execute_with(|| { + fill_juror_pool(MaxCourtParticipants::get()); + let court_id = initialize_court(); + + let amount = MinJurorStake::get() * 100; + assert_ok!(Court::join_court(RuntimeOrigin::signed(ALICE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(BOB), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(CHARLIE), amount)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(DAVE), amount)); + + let outcome = OutcomeReport::Scalar(42u128); + let vote_item = VoteItem::Outcome(outcome); + let salt = ::Hash::default(); + let commitment = BlakeTwo256::hash_of(&(ALICE, vote_item.clone(), salt)); + + let wrong_outcome_0 = OutcomeReport::Scalar(69u128); + let wrong_vote_item_0 = VoteItem::Outcome(wrong_outcome_0); + let wrong_outcome_1 = OutcomeReport::Scalar(56u128); + let wrong_vote_item_1 = VoteItem::Outcome(wrong_outcome_1); + + let draws: crate::SelectedDrawsOf = vec![ + Draw { + court_participant: ALICE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item_1.clone(), salt }, + slashable: MinJurorStake::get(), + }, + Draw { + court_participant: BOB, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item_0.clone(), salt }, + slashable: 2 * MinJurorStake::get(), + }, + Draw { + court_participant: CHARLIE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item_0, salt }, + slashable: 3 * MinJurorStake::get(), + }, + Draw { + court_participant: DAVE, + weight: 1, + vote: Vote::Revealed { commitment, vote_item: wrong_vote_item_1, salt }, + slashable: 4 * MinJurorStake::get(), + }, + ] + .try_into() + .unwrap(); + let last_draws = draws.clone(); + >::insert(court_id, draws); + + run_to_block(>::get() + 1); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + AppealPeriod::get() + 1); + + let mut court = >::get(court_id).unwrap(); + court.status = CourtStatus::Closed { winner: vote_item }; + >::insert(court_id, court); + + let free_alice_before = Balances::free_balance(ALICE); + let free_bob_before = Balances::free_balance(BOB); + let free_charlie_before = Balances::free_balance(CHARLIE); + let free_dave_before = Balances::free_balance(DAVE); + + let treasury_account = Court::treasury_account_id(); + let free_treasury_before = Balances::free_balance(treasury_account); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(EVE), court_id)); + + let alice_slashed = last_draws[ALICE as usize].slashable; + let bob_slashed = last_draws[BOB as usize].slashable; + let charlie_slashed = last_draws[CHARLIE as usize].slashable; + let dave_slashed = last_draws[DAVE as usize].slashable; + + let slashed = bob_slashed + dave_slashed + alice_slashed + charlie_slashed; + + let free_alice_after = Balances::free_balance(ALICE); + assert_eq!(free_alice_after, free_alice_before - alice_slashed); + + let free_bob_after = Balances::free_balance(BOB); + assert_eq!(free_bob_after, free_bob_before - bob_slashed); + + let free_charlie_after = Balances::free_balance(CHARLIE); + assert_eq!(free_charlie_after, free_charlie_before - charlie_slashed); + + let free_dave_after = Balances::free_balance(DAVE); + assert_eq!(free_dave_after, free_dave_before - dave_slashed); + + assert_eq!(Balances::free_balance(treasury_account), free_treasury_before + slashed); + }); +} + +#[test] +fn on_dispute_denies_non_court_markets() { + ExtBuilder::default().build().execute_with(|| { + let mut market = DEFAULT_MARKET; + market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + assert_noop!( + Court::on_dispute(&0, &market), + Error::::MarketDoesNotHaveCourtMechanism + ); + }); +} + +#[test] +fn on_resolution_sets_court_status() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.report.as_ref().unwrap().outcome, ORACLE_REPORT); + + assert_eq!(Court::on_resolution(&market_id, &market).unwrap().result, Some(ORACLE_REPORT)); + let court = >::get(court_id).unwrap(); + assert_eq!(court.status, CourtStatus::Closed { winner: VoteItem::Outcome(ORACLE_REPORT) }); + }); +} + +#[test] +fn on_resolution_fails_if_court_not_found() { + ExtBuilder::default().build().execute_with(|| { + let market_id = MarketCommons::push_market(DEFAULT_MARKET).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + + >::insert(market_id, 0); + assert_noop!(Court::on_resolution(&market_id, &market), Error::::CourtNotFound); + }); +} + +#[test] +fn on_resolution_denies_non_court_markets() { + ExtBuilder::default().build().execute_with(|| { + let mut market = DEFAULT_MARKET; + market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + assert_noop!( + Court::on_resolution(&0, &market), + Error::::MarketDoesNotHaveCourtMechanism + ); + }); +} + +#[test] +fn exchange_fails_if_non_court_markets() { + ExtBuilder::default().build().execute_with(|| { + let mut market = DEFAULT_MARKET; + market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + assert_noop!( + Court::exchange(&0, &market, &ORACLE_REPORT, NegativeImbalance::::zero()), + Error::::MarketDoesNotHaveCourtMechanism + ); + }); +} + +#[test] +fn exchange_slashes_unjustified_and_unreserves_justified_appealers() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + + let resolved_outcome = OutcomeReport::Scalar(1); + let other_outcome = OutcomeReport::Scalar(2); + + let mut court = >::get(court_id).unwrap(); + let mut free_balances_before = BTreeMap::new(); + let mut number = 0u128; + let mut slashed_bonds = >::zero(); + while (number as usize) < MaxAppeals::get() as usize { + let bond = crate::get_appeal_bond::(court.appeals.len()); + let appealed_vote_item = if number % 2 == 0 { + // The appeals are not justified, + // because the appealed outcomes are equal to the resolved outcome. + // it is punished to appeal the right outcome + slashed_bonds += bond; + VoteItem::Outcome(resolved_outcome.clone()) + } else { + VoteItem::Outcome(other_outcome.clone()) + }; + + let backer = number; + let _ = Balances::deposit(&backer, bond).unwrap(); + assert_ok!(Balances::reserve_named(&Court::reserve_id(), &backer, bond)); + let free_balance = Balances::free_balance(backer); + free_balances_before.insert(backer, free_balance); + court.appeals.try_push(AppealInfo { backer, bond, appealed_vote_item }).unwrap(); + number += 1; + } + Courts::::insert(court_id, court); + + let imbalance: NegativeImbalanceOf = + as Currency>>::issue( + 42_000_000_000, + ); + let prev_balance = imbalance.peek(); + let imb_remainder = + Court::exchange(&market_id, &market, &resolved_outcome, imbalance).unwrap(); + assert_eq!(imb_remainder.result.peek(), prev_balance + slashed_bonds); + + let court = >::get(court_id).unwrap(); + let appeals = court.appeals; + for AppealInfo { backer, bond, appealed_vote_item } in appeals { + assert_eq!(Balances::reserved_balance_named(&Court::reserve_id(), &backer), 0); + let free_balance_after = Balances::free_balance(backer); + let free_balance_before = free_balances_before.get(&backer).unwrap(); + + let resolved_vote_item = VoteItem::Outcome(resolved_outcome.clone()); + + if appealed_vote_item == resolved_vote_item { + assert_eq!(free_balance_after, *free_balance_before); + } else { + assert_eq!(free_balance_after, *free_balance_before + bond); + } + } + }); +} + +#[test] +fn get_auto_resolve_works() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let court = >::get(court_id).unwrap(); + let appeal_end = court.round_ends.appeal; + assert_eq!(Court::get_auto_resolve(&market_id, &market).result, Some(appeal_end)); + }); +} + +#[test] +fn on_global_dispute_removes_court() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + assert!(>::contains_key(court_id)); + assert_ok!(Court::on_global_dispute(&market_id, &market)); + assert!(!>::contains_key(court_id)); + }); +} + +#[test] +fn on_global_dispute_removes_draws() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + assert!(>::contains_key(court_id)); + assert_ok!(Court::on_global_dispute(&market_id, &market)); + assert!(!>::contains_key(court_id)); + }); +} + +#[test] +fn on_global_dispute_fails_if_wrong_dispute_mechanism() { + ExtBuilder::default().build().execute_with(|| { + let mut market = DEFAULT_MARKET; + market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + assert_noop!( + Court::on_global_dispute(&0, &market), + Error::::MarketDoesNotHaveCourtMechanism + ); + }); +} + +#[test] +fn on_global_dispute_fails_if_court_not_found() { + ExtBuilder::default().build().execute_with(|| { + >::insert(0, 0); + let market = DEFAULT_MARKET; + assert_noop!(Court::on_global_dispute(&0, &market), Error::::CourtNotFound); + }); +} + +#[test] +fn on_global_dispute_fails_if_market_report_not_found() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + MarketCommons::mutate_market(&market_id, |market| { + market.report = None; + Ok(()) + }) + .unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + assert_noop!( + Court::on_global_dispute(&market_id, &market), + Error::::MarketReportNotFound + ); + }); +} + +#[test] +fn on_global_dispute_returns_appealed_outcomes() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let mut court = >::get(court_id).unwrap(); + let mut gd_outcomes = Vec::new(); + + let initial_vote_amount = >::zero(); + let treasury_account = Court::treasury_account_id(); + for number in 0..MaxAppeals::get() { + let appealed_vote_item: VoteItem = + VoteItem::Outcome(OutcomeReport::Scalar(number as u128)); + let backer = number as u128; + let bond = crate::get_appeal_bond::(court.appeals.len()); + gd_outcomes.push(GlobalDisputeItem { + outcome: appealed_vote_item.clone().into_outcome().unwrap(), + owner: treasury_account, + initial_vote_amount, + }); + court.appeals.try_push(AppealInfo { backer, bond, appealed_vote_item }).unwrap(); + } + Courts::::insert(court_id, court); + assert_eq!(Court::on_global_dispute(&market_id, &market).unwrap().result, gd_outcomes); + }); +} + +#[test] +fn choose_multiple_weighted_works() { + ExtBuilder::default().build().execute_with(|| { + let necessary_draws_weight = Court::necessary_draws_weight(0usize); + for i in 0..necessary_draws_weight { + let amount = MinJurorStake::get() + i as u128; + let juror = i as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + let random_jurors = Court::choose_multiple_weighted(necessary_draws_weight).unwrap(); + assert_eq!( + random_jurors.iter().map(|draw| draw.weight).sum::() as usize, + necessary_draws_weight + ); + }); +} + +#[test] +fn select_participants_updates_juror_consumed_stake() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + fill_juror_pool(MaxCourtParticipants::get()); + // the last appeal is reserved for global dispute backing + let appeal_number = (MaxAppeals::get() - 1) as usize; + fill_appeals(court_id, appeal_number); + + let jurors = CourtPool::::get(); + let consumed_stake_before = jurors.iter().map(|juror| juror.consumed_stake).sum::(); + + let new_draws = Court::select_participants(appeal_number).unwrap(); + + let total_draw_slashable = new_draws.iter().map(|draw| draw.slashable).sum::(); + let jurors = CourtPool::::get(); + let consumed_stake_after = jurors.iter().map(|juror| juror.consumed_stake).sum::(); + assert_ne!(consumed_stake_before, consumed_stake_after); + assert_eq!(consumed_stake_before + total_draw_slashable, consumed_stake_after); + }); +} + +#[test_case(0usize; "first")] +#[test_case(1usize; "second")] +#[test_case(2usize; "third")] +#[test_case(3usize; "fourth")] +fn select_participants_fails_if_not_enough_jurors(appeal_number: usize) { + ExtBuilder::default().build().execute_with(|| { + let necessary_draws_weight = Court::necessary_draws_weight(appeal_number); + for i in 0..(necessary_draws_weight - 1usize) { + let amount = MinJurorStake::get() + i as u128; + let juror = (i + 1000) as u128; + let _ = Balances::deposit(&juror, amount).unwrap(); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + + assert_noop!( + Court::select_participants(appeal_number), + Error::::NotEnoughJurorsAndDelegatorsStake + ); + }); +} + +#[test] +fn appeal_reduces_active_lock_from_old_draws() { + ExtBuilder::default().build().execute_with(|| { + let outcome = OutcomeReport::Scalar(42u128); + let (court_id, _, _) = set_alice_after_vote(outcome); + + let old_draws = >::get(court_id); + assert!(!old_draws.is_empty()); + old_draws.iter().for_each(|draw| { + let juror = draw.court_participant; + let p_info = >::get(juror).unwrap(); + assert_ne!(draw.slashable, 0); + assert_eq!(p_info.active_lock, draw.slashable); + }); + + run_blocks(VotePeriod::get() + AggregationPeriod::get() + 1); + + assert_ok!(Court::appeal(RuntimeOrigin::signed(CHARLIE), court_id)); + + let new_draws = >::get(court_id); + old_draws.iter().for_each(|draw| { + let juror = draw.court_participant; + let p_info = >::get(juror).unwrap(); + if let Some(new_draw) = + new_draws.iter().find(|new_draw| new_draw.court_participant == juror) + { + assert_eq!(new_draw.slashable, p_info.active_lock); + } else { + assert_eq!(p_info.active_lock, 0); + } + }); + }); +} + +#[test] +fn on_dispute_creates_correct_court_info() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let court = >::get(court_id).unwrap(); + let round_ends = court.round_ends; + let request_block = >::get(); + assert_eq!(round_ends.pre_vote, request_block); + assert_eq!(round_ends.vote, round_ends.pre_vote + VotePeriod::get()); + assert_eq!(round_ends.aggregation, round_ends.vote + AggregationPeriod::get()); + assert_eq!(round_ends.appeal, round_ends.aggregation + AppealPeriod::get()); + assert_eq!(court.status, CourtStatus::Open); + assert!(court.appeals.is_empty()); + }); +} + +#[test] +fn on_dispute_inserts_draws() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let draws = >::get(court_id); + assert_eq!( + draws[0], + Draw { + court_participant: ALICE, + weight: 3, + vote: Vote::Drawn, + slashable: 3 * MinJurorStake::get() + } + ); + assert_eq!( + draws[1], + Draw { + court_participant: BOB, + weight: 5, + vote: Vote::Drawn, + slashable: 5 * MinJurorStake::get() + } + ); + assert_eq!( + draws[2], + Draw { + court_participant: CHARLIE, + weight: 6, + vote: Vote::Drawn, + slashable: 6 * MinJurorStake::get() + } + ); + assert_eq!( + draws[3], + Draw { + court_participant: DAVE, + weight: 7, + vote: Vote::Drawn, + slashable: 7 * MinJurorStake::get() + } + ); + assert_eq!( + draws[4], + Draw { + court_participant: EVE, + weight: 10, + vote: Vote::Drawn, + slashable: 10 * MinJurorStake::get() + } + ); + assert_eq!(draws.len(), 5usize); + }); +} + +#[test] +fn on_dispute_adds_auto_resolve() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let court = >::get(court_id).unwrap(); + let resolve_at = court.round_ends.appeal; + let market_id = >::get(court_id).unwrap(); + assert_eq!(MarketIdsPerDisputeBlock::::get(resolve_at), vec![market_id]); + }); +} + +#[test] +fn has_failed_returns_true_for_appealable_court_too_few_jurors() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + // force empty jurors pool + >::kill(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let court = >::get(court_id).unwrap(); + let aggregation = court.round_ends.aggregation; + run_to_block(aggregation + 1); + assert!(Court::has_failed(&market_id, &market).unwrap().result); + }); +} + +#[test] +fn has_failed_returns_true_for_appealable_court_appeals_full() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + + fill_appeals(court_id, MaxAppeals::get() as usize); + + assert!(Court::has_failed(&market_id, &market).unwrap().result); + }); +} + +#[test] +fn has_failed_returns_true_for_uninitialized_court() { + ExtBuilder::default().build().execute_with(|| { + // force empty jurors pool + >::kill(); + let market_id = MarketCommons::push_market(DEFAULT_MARKET).unwrap(); + let report_block = 42; + MarketCommons::mutate_market(&market_id, |market| { + market.report = Some(Report { at: report_block, by: BOB, outcome: ORACLE_REPORT }); + Ok(()) + }) + .unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + let block_after_dispute_duration = report_block + market.deadlines.dispute_duration; + run_to_block(block_after_dispute_duration - 1); + >::insert(market_id, 0); + assert!(Court::has_failed(&market_id, &market).unwrap().result); + }); +} + +#[test] +fn check_necessary_draws_weight() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(Court::necessary_draws_weight(0usize), 31usize); + assert_eq!(Court::necessary_draws_weight(1usize), 63usize); + assert_eq!(Court::necessary_draws_weight(2usize), 127usize); + assert_eq!(Court::necessary_draws_weight(3usize), 255usize); + }); +} + +#[test] +fn check_appeal_bond() { + ExtBuilder::default().build().execute_with(|| { + let appeal_bond = AppealBond::get(); + assert_eq!(crate::get_appeal_bond::(0usize), appeal_bond); + assert_eq!(crate::get_appeal_bond::(1usize), 2 * appeal_bond); + assert_eq!(crate::get_appeal_bond::(2usize), 4 * appeal_bond); + assert_eq!(crate::get_appeal_bond::(3usize), 8 * appeal_bond); + }); +} + +fn prepare_draws(court_id: CourtId, outcomes_with_weights: Vec<(u128, u32)>) { + let mut draws: crate::SelectedDrawsOf = vec![].try_into().unwrap(); + for (i, (outcome_index, weight)) in outcomes_with_weights.iter().enumerate() { + // offset to not conflict with other jurors + let offset_i = (i + 1000) as u128; + let juror = offset_i; + let salt = BlakeTwo256::hash_of(&offset_i); + let vote_item: VoteItem = VoteItem::Outcome(OutcomeReport::Scalar(*outcome_index)); + let commitment = BlakeTwo256::hash_of(&(juror, vote_item.clone(), salt)); + draws + .try_push(Draw { + court_participant: juror, + weight: *weight, + vote: Vote::Revealed { commitment, vote_item, salt }, + slashable: 0u128, + }) + .unwrap(); + } + >::insert(court_id, draws); +} + +#[test] +fn get_winner_works() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let outcomes_and_weights = + vec![(1000u128, 8), (1001u128, 5), (1002u128, 42), (1003u128, 13)]; + prepare_draws(court_id, outcomes_and_weights); + + let draws = >::get(court_id); + let winner = Court::get_winner(draws.as_slice(), None).unwrap(); + assert_eq!(winner.into_outcome().unwrap(), OutcomeReport::Scalar(1002u128)); + + let outcomes_and_weights = vec![(1000u128, 2), (1000u128, 4), (1001u128, 4), (1001u128, 3)]; + prepare_draws(court_id, outcomes_and_weights); + + let draws = >::get(court_id); + let winner = Court::get_winner(draws.as_slice(), None).unwrap(); + assert_eq!(winner.into_outcome().unwrap(), OutcomeReport::Scalar(1001u128)); + }); +} + +#[test] +fn get_winner_returns_none_for_no_revealed_draws() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let draws = >::get(court_id); + let winner = Court::get_winner(draws.as_slice(), None); + assert_eq!(winner, None); + }); +} + +#[test] +fn get_latest_winner_vote_item_selects_last_appealed_outcome_for_tie() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let mut court = >::get(court_id).unwrap(); + // create a tie of two best outcomes + let weights = vec![(1000u128, 42), (1001u128, 42)]; + let appealed_vote_item: VoteItem = + VoteItem::Outcome(OutcomeReport::Scalar(weights.len() as u128)); + prepare_draws(court_id, weights); + court + .appeals + .try_push(AppealInfo { + backer: CHARLIE, + bond: crate::get_appeal_bond::(1usize), + appealed_vote_item: appealed_vote_item.clone(), + }) + .unwrap(); + >::insert(court_id, court); + + let draws = >::get(court_id); + let latest = Court::get_latest_winner_vote_item(court_id, draws.as_slice()).unwrap(); + assert_eq!(latest, appealed_vote_item); + assert!(latest.into_outcome().unwrap() != ORACLE_REPORT); + }); +} + +#[test] +fn get_latest_winner_vote_item_selects_oracle_report() { + ExtBuilder::default().build().execute_with(|| { + let court_id = initialize_court(); + let market_id = >::get(court_id).unwrap(); + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.report.unwrap().outcome, ORACLE_REPORT); + let draws = >::get(court_id); + assert_eq!( + Court::get_latest_winner_vote_item(court_id, draws.as_slice()) + .unwrap() + .into_outcome() + .unwrap(), + ORACLE_REPORT + ); + }); +} + +#[test] +fn choose_multiple_weighted_returns_different_jurors_with_other_seed() { + ExtBuilder::default().build().execute_with(|| { + run_to_block(123); + + fill_juror_pool(MaxCourtParticipants::get()); + + let nonce_0 = 42u64; + >::put(nonce_0); + // randomness is mocked and purely based on the nonce + // thus a different nonce will result in a different seed (disregarding hash collisions) + let first_random_seed = Court::get_random_seed(nonce_0); + let first_random_list = Court::choose_multiple_weighted(3).unwrap(); + + run_blocks(1); + + let nonce_1 = 69u64; + >::put(nonce_1); + let second_random_seed = Court::get_random_seed(nonce_1); + + assert_ne!(first_random_seed, second_random_seed); + let second_random_list = Court::choose_multiple_weighted(3).unwrap(); + + // the two lists contain different jurors + for juror in &first_random_list { + assert!(second_random_list.iter().all(|el| el != juror)); + } + }); +} + +#[test] +fn get_random_seed_returns_equal_seeds_with_equal_nonce() { + ExtBuilder::default().build().execute_with(|| { + run_to_block(123); + + // this is useful to check that the random seed only depends on the nonce + // the same nonce always results in the same seed for testing deterministic + let nonce = 42u64; + >::put(nonce); + let first_random_seed = Court::get_random_seed(nonce); + + run_blocks(1); + + >::put(nonce); + let second_random_seed = Court::get_random_seed(nonce); + + assert_eq!(first_random_seed, second_random_seed); + }); +} + +#[test] +fn random_jurors_returns_a_subset_of_jurors() { + ExtBuilder::default().build().execute_with(|| { + run_to_block(123); + fill_juror_pool(MaxCourtParticipants::get()); + + let jurors = >::get(); + + let random_jurors = Court::choose_multiple_weighted(2).unwrap(); + for draw in random_jurors { + assert!(jurors.iter().any(|el| el.court_participant == draw.court_participant)); + } + }); +} + +#[test] +fn handle_inflation_works() { + ExtBuilder::default().build().execute_with(|| { + let mut jurors = >::get(); + let mut free_balances_before = BTreeMap::new(); + let jurors_list = [1000, 10_000, 100_000, 1_000_000, 10_000_000]; + run_to_block(InflationPeriod::get()); + let joined_at = >::block_number(); + for number in jurors_list.iter() { + let stake = *number; + let juror = *number; + let _ = Balances::deposit(&juror, stake).unwrap(); + free_balances_before.insert(juror, stake); + jurors + .try_push(CourtPoolItem { + stake, + court_participant: juror, + consumed_stake: 0, + joined_at, + }) + .unwrap(); + } + >::put(jurors.clone()); + + let inflation_period = InflationPeriod::get(); + run_blocks(inflation_period); + let now = >::block_number(); + Court::handle_inflation(now); + + let free_balance_after_0 = Balances::free_balance(jurors_list[0]); + assert_eq!(free_balance_after_0 - free_balances_before[&jurors_list[0]], 432_012); + + let free_balance_after_1 = Balances::free_balance(jurors_list[1]); + assert_eq!(free_balance_after_1 - free_balances_before[&jurors_list[1]], 4_320_129); + + let free_balance_after_2 = Balances::free_balance(jurors_list[2]); + assert_eq!(free_balance_after_2 - free_balances_before[&jurors_list[2]], 43_201_302); + + let free_balance_after_3 = Balances::free_balance(jurors_list[3]); + assert_eq!(free_balance_after_3 - free_balances_before[&jurors_list[3]], 432_013_038); + + let free_balance_after_4 = Balances::free_balance(jurors_list[4]); + assert_eq!(free_balance_after_4 - free_balances_before[&jurors_list[4]], 4_320_130_393); + }); +} + +#[test] +fn handle_inflation_without_waiting_one_inflation_period() { + ExtBuilder::default().build().execute_with(|| { + let mut jurors = >::get(); + let mut free_balances_before = BTreeMap::new(); + let jurors_list = [1000, 10_000, 100_000, 1_000_000, 10_000_000]; + run_to_block(InflationPeriod::get()); + let joined_at = >::block_number(); + for number in jurors_list.iter() { + let stake = *number; + let juror = *number; + let _ = Balances::deposit(&juror, stake).unwrap(); + free_balances_before.insert(juror, stake); + jurors + .try_push(CourtPoolItem { + stake, + court_participant: juror, + consumed_stake: 0, + joined_at, + }) + .unwrap(); + } + >::put(jurors.clone()); + + let inflation_period = InflationPeriod::get(); + run_blocks(inflation_period.saturating_sub(1)); + let now = >::block_number(); + Court::handle_inflation(now); + + let free_balance_after_0 = Balances::free_balance(jurors_list[0]); + assert_eq!(free_balance_after_0 - free_balances_before[&jurors_list[0]], 0); + + let free_balance_after_1 = Balances::free_balance(jurors_list[1]); + assert_eq!(free_balance_after_1 - free_balances_before[&jurors_list[1]], 0); + + let free_balance_after_2 = Balances::free_balance(jurors_list[2]); + assert_eq!(free_balance_after_2 - free_balances_before[&jurors_list[2]], 0); + + let free_balance_after_3 = Balances::free_balance(jurors_list[3]); + assert_eq!(free_balance_after_3 - free_balances_before[&jurors_list[3]], 0); + + let free_balance_after_4 = Balances::free_balance(jurors_list[4]); + assert_eq!(free_balance_after_4 - free_balances_before[&jurors_list[4]], 0); + }); } diff --git a/zrml/court/src/types.rs b/zrml/court/src/types.rs new file mode 100644 index 000000000..e9c8535ff --- /dev/null +++ b/zrml/court/src/types.rs @@ -0,0 +1,360 @@ +// Copyright 2022-2023 Forecasting Technologies LTD. +// Copyright 2021-2022 Zeitgeist PM LLC. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . +extern crate alloc; +use alloc::{vec, vec::Vec}; +use zeitgeist_primitives::types::OutcomeReport; + +/// The type of the court identifier. +pub type CourtId = u128; + +/// The different court vote types. This can be extended to allow different decision making options. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub enum VoteItemType { + Outcome, + Binary, +} + +/// The different court vote types with their raw values. +/// This can be extended to allow different decision making options. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, + Ord, + PartialOrd, +)] +pub enum VoteItem { + Outcome(OutcomeReport), + Binary(bool), +} + +/// Simple implementations to handle vote items easily. +impl VoteItem { + pub fn into_outcome(self) -> Option { + match self { + Self::Outcome(report) => Some(report), + _ => None, + } + } +} + +/// The general information about a particular court participant (juror or delegator). +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub struct CourtParticipantInfo { + /// The court participants amount in the stake weighted pool. + /// This amount is used to find a court participant with a binary search on the pool. + pub(crate) stake: Balance, + /// The current amount of funds which are locked in courts. + pub(crate) active_lock: Balance, + /// The block number when an exit from court was requested. + pub(crate) prepare_exit_at: Option, + /// The delegations of the court participant. This determines the account as a delegator. + pub(crate) delegations: Option, +} + +/// The raw information behind the secret hash of a juror's vote. +pub(crate) struct RawCommitment { + /// The juror's account id. + pub(crate) juror: AccountId, + /// The vote item which the juror voted for. + pub(crate) vote_item: VoteItem, + /// The salt which was used to hash the vote. + pub(crate) salt: Hash, +} + +/// All possible states of a vote. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub enum Vote { + /// The delegator delegated stake to other jurors. + Delegated { delegated_stakes: DelegatedStakes }, + /// The juror was randomly selected to vote in a specific court case. + Drawn, + /// The juror casted a vote, only providing a hash, which meaning is unknown. + Secret { commitment: Hash }, + /// The juror revealed her raw vote, letting anyone know what she voted. + Revealed { commitment: Hash, vote_item: VoteItem, salt: Hash }, + /// The juror was denounced, because she revealed her raw vote during the vote phase. + Denounced { commitment: Hash, vote_item: VoteItem, salt: Hash }, +} + +/// The status of a court case. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub enum CourtStatus { + /// The court case has been started. + Open, + /// The court case was closed, the winner vote item was determined. + Closed { winner: VoteItem }, + /// The juror stakes from the court were reassigned + Reassigned, +} + +/// The information about an appeal for a court case. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub struct AppealInfo { + /// The account which made the appeal. + pub(crate) backer: AccountId, + /// The amount of funds which were locked for the appeal. + pub(crate) bond: Balance, + /// The vote item which was appealed. + pub(crate) appealed_vote_item: VoteItem, +} + +/// The timing information about a court round. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub struct RoundTiming { + /// The end block of the pre-vote period. + pub pre_vote: BlockNumber, + /// The end block of the vote period. + /// Note this can also be used as the block duration for votes, + /// if it is used for the initialisation of the court round ends. + pub vote: BlockNumber, + /// The end block of the aggregation period. + /// Note this can also be used as the block duration for revealing votes, + /// if it is used for the initialisation of the court round ends. + pub aggregation: BlockNumber, + /// The end block of the appeal period. + /// Note this can also be used as the block duration for appeals, + /// if it is used for the initialisation of the court round ends. + pub appeal: BlockNumber, +} + +/// The information about a court case. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub struct CourtInfo { + /// The status of the court case. + pub status: CourtStatus, + /// The list of all appeals. + pub appeals: Appeals, + /// This specifies the end blocks of the court case phases. + pub round_ends: RoundTiming, + /// The type of the vote item. + pub vote_item_type: VoteItemType, +} + +impl + CourtInfo +{ + pub(crate) fn new( + round_timing: RoundTiming, + vote_item_type: VoteItemType, + ) -> Self { + let pre_vote = round_timing.pre_vote; + let vote = pre_vote.saturating_add(round_timing.vote); + let aggregation = vote.saturating_add(round_timing.aggregation); + let appeal = aggregation.saturating_add(round_timing.appeal); + let round_ends = RoundTiming { pre_vote, vote, aggregation, appeal }; + let status = CourtStatus::Open; + Self { status, appeals: Default::default(), round_ends, vote_item_type } + } + + pub(crate) fn update_round(&mut self, round_timing: RoundTiming) { + self.round_ends.pre_vote = round_timing.pre_vote; + self.round_ends.vote = self.round_ends.pre_vote.saturating_add(round_timing.vote); + self.round_ends.aggregation = self.round_ends.vote.saturating_add(round_timing.aggregation); + self.round_ends.appeal = self.round_ends.aggregation.saturating_add(round_timing.appeal); + } +} + +/// After a court participant was randomly selected to vote in a court case, +/// this information is relevant to handle the post-selection process. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub struct Draw { + /// The court participant who was randomly selected. + pub court_participant: AccountId, + /// The weight of the juror in this court case. + /// The higher the weight the more voice the juror has in the final winner decision. + pub weight: u32, + /// The information about the vote state. + pub vote: Vote, + /// The amount of funds which can be slashed for this court case. + /// This is equal to a multiple of `MinJurorStake` to mitigate Sybil attacks. + pub slashable: Balance, +} + +/// All information related to one item in the stake weighted juror pool. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub struct CourtPoolItem { + /// The amount of funds associated to a court participant + /// in order to get selected for a court case. + pub stake: Balance, + /// The account which is the juror that might be selected in court cases. + pub court_participant: AccountId, + /// The consumed amount of the stake for all draws. This is useful to reduce the probability + /// of a court participant to be selected again. + pub consumed_stake: Balance, + /// The block number at which the participant joined. + pub joined_at: BlockNumber, +} + +/// The information about an internal selected draw of a juror or delegator. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub(crate) struct SelectionValue { + /// The overall weight of the juror or delegator for a specific selected draw. + pub(crate) weight: u32, + /// The amount that can be slashed for this selected draw. + pub(crate) slashable: Balance, + /// The different portions of stake distributed over multiple jurors. + /// The sum of all delegated stakes should be equal to `slashable`. + pub(crate) delegated_stakes: DelegatedStakes, +} + +/// The type to add one weight to the selected draws. +#[derive( + parity_scale_codec::Decode, + parity_scale_codec::Encode, + parity_scale_codec::MaxEncodedLen, + scale_info::TypeInfo, + Clone, + Debug, + PartialEq, + Eq, +)] +pub(crate) enum SelectionAdd { + /// The variant to add an active juror, who is not a delegator. + SelfStake { lock: Balance }, + /// The variant to decide that a delegator is added + /// to the selected draws and locks stake on a delegated juror. + DelegationStake { delegated_juror: AccountId, lock: Balance }, + /// The variant to know that one weight for the delegation to the delegated juror is added. + DelegationWeight, +} + +/// The information about an active juror who voted for a court. +pub(crate) struct SelfInfo { + /// The slashable amount of the juror herself. + pub(crate) slashable: Balance, + /// The item for which the juror voted. + pub(crate) vote_item: VoteItem, +} + +pub(crate) struct JurorVoteWithStakes { + /// An optional information about an active juror herself, who was selected and voted. + /// This could be None, because delegators could have delegated to a juror who failed to vote. + pub(crate) self_info: Option>, + // many delegators can have delegated to the same juror + // that's why the value is a vector and should be sorted (binary search by key) + // the key is the delegator account + // the value is the delegated stake + pub(crate) delegations: Vec<(AccountId, Balance)>, +} + +impl Default for JurorVoteWithStakes { + fn default() -> Self { + JurorVoteWithStakes { self_info: None, delegations: vec![] } + } +} + +/// An internal error type to determine how the selection of draws fails. +pub(crate) enum SelectionError { + NoValidDelegatedJuror, +} diff --git a/zrml/court/src/weights.rs b/zrml/court/src/weights.rs index 8b36a4e2c..faf60e5fe 100644 --- a/zrml/court/src/weights.rs +++ b/zrml/court/src/weights.rs @@ -23,7 +23,7 @@ //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/zeitgeist +// ./target/release/zeitgeist // benchmark // pallet // --chain=dev @@ -34,8 +34,8 @@ // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --template=./misc/weight_template.hbs // --output=./zrml/court/src/weights.rs +// --template=./misc/weight_template.hbs #![allow(unused_parens)] #![allow(unused_imports)] @@ -46,37 +46,259 @@ use frame_support::{traits::Get, weights::Weight}; /// Trait containing the required functions for weight retrival within /// zrml_court (automatically generated) pub trait WeightInfoZeitgeist { - fn exit_court() -> Weight; - fn join_court() -> Weight; - fn vote() -> Weight; + fn join_court(j: u32) -> Weight; + fn delegate(j: u32, d: u32) -> Weight; + fn prepare_exit_court(j: u32) -> Weight; + fn exit_court_remove() -> Weight; + fn exit_court_set() -> Weight; + fn vote(d: u32) -> Weight; + fn denounce_vote(d: u32) -> Weight; + fn reveal_vote(d: u32) -> Weight; + fn appeal(j: u32, a: u32, r: u32, f: u32) -> Weight; + fn reassign_court_stakes(d: u32) -> Weight; + fn set_inflation() -> Weight; + fn handle_inflation(j: u32) -> Weight; + fn select_participants(a: u32) -> Weight; + fn on_dispute(j: u32, r: u32) -> Weight; + fn on_resolution(d: u32) -> Weight; + fn exchange(a: u32) -> Weight; + fn get_auto_resolve() -> Weight; + fn has_failed() -> Weight; + fn on_global_dispute(a: u32, d: u32) -> Weight; + fn clear(d: u32) -> Weight; } /// Weight functions for zrml_court (automatically generated) pub struct WeightInfo(PhantomData); impl WeightInfoZeitgeist for WeightInfo { + // Storage: Court JurorPool (r:1 w:1) // Storage: Court Jurors (r:1 w:1) - // Storage: Balances Reserves (r:1 w:1) - // Storage: Court CounterForJurors (r:1 w:1) - // Storage: Court RequestedJurors (r:1 w:0) - // Storage: Court Votes (r:1 w:0) - fn exit_court() -> Weight { - Weight::from_ref_time(96_020_000) - .saturating_add(T::DbWeight::get().reads(5)) + // Storage: Balances Locks (r:1 w:1) + fn join_court(j: u32) -> Weight { + Weight::from_ref_time(33_951_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(94_000).saturating_mul(j.into())) + .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) } - // Storage: Court Jurors (r:1 w:1) - // Storage: Court CounterForJurors (r:1 w:1) - // Storage: Balances Reserves (r:1 w:1) - fn join_court() -> Weight { - Weight::from_ref_time(66_170_000) + // Storage: Court JurorPool (r:1 w:1) + // Storage: Court Jurors (r:6 w:1) + // Storage: Balances Locks (r:1 w:1) + fn delegate(j: u32, d: u32) -> Weight { + Weight::from_ref_time(46_155_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(122_000).saturating_mul(j.into())) + // Standard Error: 51_000 + .saturating_add(Weight::from_ref_time(863_000).saturating_mul(d.into())) .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(d.into()))) .saturating_add(T::DbWeight::get().writes(3)) } + // Storage: Court Jurors (r:1 w:1) + // Storage: Court JurorPool (r:1 w:1) + fn prepare_exit_court(j: u32) -> Weight { + Weight::from_ref_time(19_325_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(84_000).saturating_mul(j.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Court Jurors (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn exit_court_remove() -> Weight { + Weight::from_ref_time(38_000_000) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Court Jurors (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn exit_court_set() -> Weight { + Weight::from_ref_time(37_000_000) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Court Courts (r:1 w:0) + // Storage: Court SelectedDraws (r:1 w:1) + fn vote(d: u32) -> Weight { + Weight::from_ref_time(48_629_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(90_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + // Storage: Court CourtIdToMarketId (r:1 w:0) + // Storage: MarketCommons Markets (r:1 w:0) // Storage: Court Jurors (r:1 w:0) - // Storage: Court Votes (r:0 w:1) - fn vote() -> Weight { - Weight::from_ref_time(29_180_000) - .saturating_add(T::DbWeight::get().reads(1)) + // Storage: Court Courts (r:1 w:0) + // Storage: Court SelectedDraws (r:1 w:1) + fn denounce_vote(d: u32) -> Weight { + Weight::from_ref_time(41_779_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(126_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(1)) + } + // Storage: Court CourtIdToMarketId (r:1 w:0) + // Storage: MarketCommons Markets (r:1 w:0) + // Storage: Court Jurors (r:1 w:0) + // Storage: Court Courts (r:1 w:0) + // Storage: Court SelectedDraws (r:1 w:1) + fn reveal_vote(d: u32) -> Weight { + Weight::from_ref_time(69_471_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(92_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(1)) } + // Storage: Court Courts (r:1 w:1) + // Storage: Court CourtIdToMarketId (r:1 w:0) + // Storage: MarketCommons Markets (r:1 w:0) + // Storage: Court SelectedDraws (r:1 w:1) + // Storage: Court JurorPool (r:1 w:1) + // Storage: Court JurorsSelectionNonce (r:1 w:1) + // Storage: RandomnessCollectiveFlip RandomMaterial (r:1 w:0) + // Storage: Court Jurors (r:223 w:222) + // Storage: Court RequestBlock (r:1 w:0) + // Storage: PredictionMarkets MarketIdsPerDisputeBlock (r:2 w:2) + // Storage: Balances Reserves (r:1 w:1) + fn appeal(j: u32, a: u32, r: u32, _f: u32) -> Weight { + Weight::from_ref_time(0) + // Standard Error: 26_000 + .saturating_add(Weight::from_ref_time(5_584_000).saturating_mul(j.into())) + // Standard Error: 7_923_000 + .saturating_add(Weight::from_ref_time(2_539_125_000).saturating_mul(a.into())) + // Standard Error: 320_000 + .saturating_add(Weight::from_ref_time(1_503_000).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads((128_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes((128_u64).saturating_mul(a.into()))) + } + // Storage: Court Courts (r:1 w:1) + // Storage: Court SelectedDraws (r:1 w:1) + // Storage: Court Jurors (r:5 w:5) + // Storage: System Account (r:6 w:5) + fn reassign_court_stakes(d: u32) -> Weight { + Weight::from_ref_time(0) + // Standard Error: 19_000 + .saturating_add(Weight::from_ref_time(44_416_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(d.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(d.into()))) + } + // Storage: Court YearlyInflation (r:0 w:1) + fn set_inflation() -> Weight { + Weight::from_ref_time(16_000_000).saturating_add(T::DbWeight::get().writes(1)) + } + // Storage: Court YearlyInflation (r:1 w:0) + // Storage: Court JurorPool (r:1 w:0) + // Storage: System Account (r:1 w:1) + fn handle_inflation(j: u32) -> Weight { + Weight::from_ref_time(0) + // Standard Error: 4_000 + .saturating_add(Weight::from_ref_time(12_853_000).saturating_mul(j.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(j.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(j.into()))) + } + // Storage: Court CourtPool (r:1 w:1) + // Storage: Court SelectionNonce (r:1 w:1) + // Storage: RandomnessCollectiveFlip RandomMaterial (r:1 w:0) + // Storage: Court Participants (r:35 w:31) + fn select_participants(a: u32) -> Weight { + Weight::from_ref_time(639_560_000) + // Standard Error: 11_776_000 + .saturating_add(Weight::from_ref_time(2_310_239_000).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(24)) + .saturating_add(T::DbWeight::get().reads((60_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes(19)) + .saturating_add(T::DbWeight::get().writes((60_u64).saturating_mul(a.into()))) + } + // Storage: Court NextCourtId (r:1 w:1) + // Storage: Court JurorPool (r:1 w:1) + // Storage: Court JurorsSelectionNonce (r:1 w:1) + // Storage: RandomnessCollectiveFlip RandomMaterial (r:1 w:0) + // Storage: Court Jurors (r:23 w:23) + // Storage: Court RequestBlock (r:1 w:0) + // Storage: PredictionMarkets MarketIdsPerDisputeBlock (r:1 w:1) + // Storage: Court SelectedDraws (r:0 w:1) + // Storage: Court CourtIdToMarketId (r:0 w:1) + // Storage: Court MarketIdToCourtId (r:0 w:1) + // Storage: Court Courts (r:0 w:1) + fn on_dispute(j: u32, r: u32) -> Weight { + Weight::from_ref_time(196_514_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(144_000).saturating_mul(j.into())) + // Standard Error: 3_000 + .saturating_add(Weight::from_ref_time(157_000).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(33)) + .saturating_add(T::DbWeight::get().writes(35)) + } + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court Courts (r:1 w:1) + // Storage: Court SelectedDraws (r:1 w:0) + // Storage: Court CourtIdToMarketId (r:1 w:0) + // Storage: MarketCommons Markets (r:1 w:0) + // Storage: Court Jurors (r:1 w:1) + fn on_resolution(d: u32) -> Weight { + Weight::from_ref_time(17_329_000) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(4_102_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(d.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(d.into()))) + } + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court Courts (r:1 w:0) + // Storage: Balances Reserves (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn exchange(a: u32) -> Weight { + Weight::from_ref_time(17_021_000) + // Standard Error: 29_000 + .saturating_add(Weight::from_ref_time(21_348_000).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(a.into()))) + } + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court Courts (r:1 w:0) + fn get_auto_resolve() -> Weight { + Weight::from_ref_time(9_000_000).saturating_add(T::DbWeight::get().reads(2)) + } + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court JurorPool (r:1 w:0) + // Storage: Court Courts (r:1 w:0) + // Storage: Court CourtIdToMarketId (r:1 w:0) + // Storage: MarketCommons Markets (r:1 w:0) + fn has_failed() -> Weight { + Weight::from_ref_time(24_000_000).saturating_add(T::DbWeight::get().reads(5)) + } + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court Courts (r:1 w:1) + // Storage: Court SelectedDraws (r:1 w:1) + // Storage: Court Jurors (r:510 w:510) + fn on_global_dispute(a: u32, d: u32) -> Weight { + Weight::from_ref_time(11_646_000) + // Standard Error: 588_000 + .saturating_add(Weight::from_ref_time(20_187_000).saturating_mul(a.into())) + // Standard Error: 5_000 + .saturating_add(Weight::from_ref_time(4_083_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(d.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(d.into()))) + } + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court SelectedDraws (r:1 w:1) + // Storage: Court Jurors (r:1 w:1) + // Storage: Court Courts (r:0 w:1) + fn clear(d: u32) -> Weight { + Weight::from_ref_time(4_229_000) + // Standard Error: 0 + .saturating_add(Weight::from_ref_time(4_115_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(d.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(d.into()))) + } } diff --git a/zrml/global-disputes/README.md b/zrml/global-disputes/README.md index b1bbf73fd..8417e2b42 100644 --- a/zrml/global-disputes/README.md +++ b/zrml/global-disputes/README.md @@ -32,11 +32,19 @@ on which the market finally resolves. get their reward. Fails if the global dispute is not concluded yet. - `reward_outcome_owner` - Reward the collected fees to the owner(s) of a voting outcome. Fails if not all outcomes are already purged. +- `refund_vote_fees` - Return all vote funds and fees, when a global dispute was + destroyed. #### Private Pallet API -- `push_voting_outcome` - Start a global dispute, add an initial voting outcome - and vote on it. +- `push_vote_outcome` - Add an initial voting outcome and vote on it with + `initial_vote_balance`. - `determine_voting_winner` - Determine the canonical voting outcome based on total locked tokens. -- `is_started` - Check if the global dispute started already. +- `does_exist` - Check if the global dispute does already exist. +- `is_active` - Check if the global dispute is active to get votes + (`vote_on_outcome`) and allow the addition of new voting outcomes with + `add_vote_outcome`. +- `start_global_dispute` - Start a global dispute. +- `destroy_global_dispute` - Allow the users to get their voting funds and fee + payments back. diff --git a/zrml/global-disputes/src/benchmarks.rs b/zrml/global-disputes/src/benchmarks.rs index 2fb52724b..cfa9c9a50 100644 --- a/zrml/global-disputes/src/benchmarks.rs +++ b/zrml/global-disputes/src/benchmarks.rs @@ -24,8 +24,8 @@ #![cfg(feature = "runtime-benchmarks")] use crate::{ - global_disputes_pallet_api::GlobalDisputesPalletApi, types::*, BalanceOf, Call, Config, - Pallet as GlobalDisputes, *, + global_disputes_pallet_api::GlobalDisputesPalletApi, types::*, utils::market_mock, BalanceOf, + Call, Config, Pallet as GlobalDisputes, *, }; use frame_benchmarking::{account, benchmarks, whitelisted_caller}; use frame_support::{ @@ -38,6 +38,7 @@ use num_traits::ops::checked::CheckedRem; use sp_runtime::traits::{Bounded, SaturatedConversion, Saturating}; use sp_std::prelude::*; use zeitgeist_primitives::types::OutcomeReport; +use zrml_market_commons::MarketCommonsPalletApi; fn deposit(caller: &T::AccountId) where @@ -53,8 +54,8 @@ fn assert_last_event(generic_event: ::RuntimeEvent) { benchmarks! { vote_on_outcome { - // only Outcomes owners, but not Winners owners is present during vote_on_outcome - let o in 1..T::MaxOwners::get(); + // only Outcomes owners, but not GlobalDisputesInfo owners is present during vote_on_outcome + let o in 2..T::MaxOwners::get(); // ensure we have one vote left for the call let v in 0..(T::MaxGlobalDisputeVotes::get() - 1); @@ -63,20 +64,36 @@ benchmarks! { // ensure that we get the worst case // to actually insert the new item at the end of the binary search let market_id: MarketIdOf = v.into(); + let market = market_mock::(); + for i in 0..=v { + T::MarketCommons::push_market(market.clone()).unwrap(); + } + let outcome = OutcomeReport::Scalar(0); let amount: BalanceOf = T::MinOutcomeVoteAmount::get().saturated_into(); deposit::(&caller); + + let mut initial_items: Vec> = Vec::new(); + initial_items.push(InitialItem { + outcome: outcome.clone(), + owner: caller.clone(), + amount: 1_000_000_000u128.saturated_into(), + }); for i in 1..=o { let owner = account("outcomes_owner", i, 0); - GlobalDisputes::::push_voting_outcome( - &market_id, - outcome.clone(), - &owner, - 1_000_000_000u128.saturated_into(), - ) - .unwrap(); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(i.saturated_into()), + owner, + amount: 1_000_000_000u128.saturated_into(), + }); } + GlobalDisputes::::start_global_dispute( + &market_id, + initial_items.as_slice(), + ) + .unwrap(); + let mut vote_locks: BoundedVec<( MarketIdOf, BalanceOf @@ -91,9 +108,18 @@ benchmarks! { // minus one to ensure, that we use the worst case // for using a new winner info after the vote_on_outcome call let vote_sum = amount - 1u128.saturated_into(); - let outcome_info = OutcomeInfo { outcome_sum: vote_sum, owners: Default::default() }; - let winner_info = WinnerInfo {outcome: outcome.clone(), is_finished: false, outcome_info}; - >::insert(market_id, winner_info); + let possession = Possession::Shared { owners: Default::default() }; + let outcome_info = OutcomeInfo { outcome_sum: vote_sum, possession }; + let now = >::block_number(); + let add_outcome_end = now + T::AddOutcomePeriod::get(); + let vote_end = add_outcome_end + T::GdVotingPeriod::get(); + let gd_info = GlobalDisputeInfo { + winner_outcome: outcome.clone(), + status: GdStatus::Active { add_outcome_end, vote_end }, + outcome_info, + }; + >::insert(market_id, gd_info); + >::set_block_number(add_outcome_end + 1u32.into()); }: _(RawOrigin::Signed(caller.clone()), market_id, outcome.clone(), amount) verify { assert_last_event::( @@ -119,10 +145,18 @@ benchmarks! { } let owners = BoundedVec::try_from(owners).unwrap(); let outcome = OutcomeReport::Scalar(0); - let outcome_info = OutcomeInfo { outcome_sum: vote_sum, owners }; + let possession = Possession::Shared { owners }; + let outcome_info = OutcomeInfo { outcome_sum: vote_sum, possession }; // is_finished is false, // because we need `lock_needed` to be greater zero to set a lock. - let winner_info = WinnerInfo {outcome, is_finished: false, outcome_info}; + let now = >::block_number(); + let add_outcome_end = now + T::AddOutcomePeriod::get(); + let vote_end = add_outcome_end + T::GdVotingPeriod::get(); + let gd_info = GlobalDisputeInfo { + winner_outcome: outcome, + status: GdStatus::Active { add_outcome_end, vote_end }, + outcome_info + }; let caller: T::AccountId = whitelisted_caller(); let voter: T::AccountId = account("voter", 0, 0); @@ -133,7 +167,7 @@ benchmarks! { let market_id: MarketIdOf = i.saturated_into(); let locked_balance: BalanceOf = i.saturated_into(); vote_locks.try_push((market_id, locked_balance)).unwrap(); - >::insert(market_id, winner_info.clone()); + >::insert(market_id, gd_info.clone()); } >::insert(voter.clone(), vote_locks.clone()); }: { @@ -159,10 +193,11 @@ benchmarks! { } let owners = BoundedVec::try_from(owners).unwrap(); let outcome = OutcomeReport::Scalar(0); - let outcome_info = OutcomeInfo { outcome_sum: vote_sum, owners }; + let possession = Possession::Shared { owners }; + let outcome_info = OutcomeInfo { outcome_sum: vote_sum, possession }; // is_finished is true, // because we need `lock_needed` to be zero to remove all locks. - let winner_info = WinnerInfo {outcome, is_finished: true, outcome_info}; + let gd_info = GlobalDisputeInfo {winner_outcome: outcome, status: GdStatus::Finished, outcome_info}; let caller: T::AccountId = whitelisted_caller(); let voter: T::AccountId = account("voter", 0, 0); @@ -175,7 +210,7 @@ benchmarks! { let market_id: MarketIdOf = i.saturated_into(); let locked_balance: BalanceOf = 1u128.saturated_into(); vote_locks.try_push((market_id, locked_balance)).unwrap(); - >::insert(market_id, winner_info.clone()); + >::insert(market_id, gd_info.clone()); } >::insert(voter.clone(), vote_locks); }: { @@ -192,48 +227,59 @@ benchmarks! { add_vote_outcome { // concious decision for using component 0..MaxOwners here // because although we check that is_finished is false, - // Winners counts processing time for the decoding of the owners vector. - // then if the owner information is not present on Winners, + // GlobalDisputesInfo counts processing time for the decoding of the owners vector. + // then if the owner information is not present on GlobalDisputesInfo, // the owner info is present on Outcomes // this happens in the case, that Outcomes is not none at the query time. let w in 1..T::MaxOwners::get(); - let mut owners = Vec::new(); + let mut owners: Vec> = Vec::new(); for i in 1..=w { - let owner = account("winners_owner", i, 0); + let owner: AccountIdOf = account("winners_owner", i, 0); owners.push(owner); } - let owners = BoundedVec::try_from(owners).unwrap(); - let outcome_info = OutcomeInfo { outcome_sum: 42u128.saturated_into(), owners }; - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(0), - is_finished: false, + let owners: BoundedVec, T::MaxOwners> = BoundedVec::try_from(owners) + .unwrap(); + + let possession = Possession::Shared { owners }; + let outcome_info = OutcomeInfo { outcome_sum: 42u128.saturated_into(), possession: possession.clone() }; + let now = >::block_number(); + let add_outcome_end = now + T::AddOutcomePeriod::get(); + let vote_end = add_outcome_end + T::GdVotingPeriod::get(); + let gd_info = GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(0), + status: GdStatus::Active { add_outcome_end, vote_end }, outcome_info, }; let caller: T::AccountId = whitelisted_caller(); let market_id: MarketIdOf = 0u128.saturated_into(); + let market = market_mock::(); + T::MarketCommons::push_market(market).unwrap(); let outcome = OutcomeReport::Scalar(20); - >::insert(market_id, winner_info); + >::insert(market_id, gd_info); deposit::(&caller); }: _(RawOrigin::Signed(caller.clone()), market_id, outcome.clone()) verify { assert_last_event::(Event::AddedVotingOutcome:: { market_id, - owner: caller, + owner: caller.clone(), outcome: outcome.clone(), }.into()); - let winner_info = >::get(market_id).unwrap(); - assert_eq!(winner_info.outcome_info.outcome_sum, T::VotingOutcomeFee::get()); - // zero owners as long as dispute not finished and reward_outcome_owner not happened - assert_eq!(winner_info.outcome_info.owners.len(), 0usize); + let gd_info = >::get(market_id).unwrap(); + assert_eq!(gd_info.outcome_info.outcome_sum, T::VotingOutcomeFee::get()); + // None as long as dispute not finished and reward_outcome_owner not happened + assert_eq!(gd_info.outcome_info.possession, possession); let outcomes_item = >::get(market_id, outcome).unwrap(); assert_eq!(outcomes_item.outcome_sum, T::VotingOutcomeFee::get()); - assert_eq!(outcomes_item.owners.len(), 1usize); + assert_eq!( + outcomes_item.possession, + Possession::Paid { owner: caller, fee: T::VotingOutcomeFee::get() }, + ); } - reward_outcome_owner_with_funds { + reward_outcome_owner_shared_possession { let o in 1..T::MaxOwners::get(); let market_id: MarketIdOf = 0u128.saturated_into(); @@ -244,16 +290,16 @@ benchmarks! { owners_vec.push(owner); } let owners = BoundedVec::try_from(owners_vec.clone()).unwrap(); - - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(0), - is_finished: true, + let possession = Possession::Shared { owners }; + let gd_info = GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(0), + status: GdStatus::Finished, outcome_info: OutcomeInfo { outcome_sum: 42u128.saturated_into(), - owners, + possession, }, }; - >::insert(market_id, winner_info.clone()); + >::insert(market_id, gd_info.clone()); let reward_account = GlobalDisputes::::reward_account(&market_id); let _ = T::Currency::deposit_creating( @@ -274,7 +320,7 @@ benchmarks! { ) .unwrap(); } verify { - assert!(winner_info.outcome_info.owners.len() == o as usize); + assert!(gd_info.outcome_info.possession.get_shared_owners().unwrap().len() == o as usize); assert_last_event::( Event::OutcomeOwnersRewarded:: { market_id, @@ -291,10 +337,76 @@ benchmarks! { assert_eq!(T::Currency::free_balance(&reward_account), expected); } - reward_outcome_owner_no_funds { + reward_outcome_owner_paid_possession { + let market_id: MarketIdOf = 0u128.saturated_into(); + + let owner: AccountIdOf = account("winners_owner", 0, 0); + let possession = Possession::Paid { owner: owner.clone(), fee: T::VotingOutcomeFee::get() }; + let gd_info = GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(0), + status: GdStatus::Finished, + outcome_info: OutcomeInfo { + outcome_sum: 42u128.saturated_into(), + possession, + }, + }; + >::insert(market_id, gd_info); + + let reward_account = GlobalDisputes::::reward_account(&market_id); + let _ = T::Currency::deposit_creating( + &reward_account, + T::VotingOutcomeFee::get().saturating_mul(10u128.saturated_into()), + ); + let reward_before = T::Currency::free_balance(&reward_account); + + let caller: T::AccountId = whitelisted_caller(); + + let outcome = OutcomeReport::Scalar(20); + + deposit::(&caller); + }: { + >::reward_outcome_owner( + RawOrigin::Signed(caller.clone()).into(), + market_id + ) + .unwrap(); + } verify { + assert_last_event::( + Event::OutcomeOwnerRewarded:: { + market_id, + owner: owner.clone(), + } + .into(), + ); + assert!(T::Currency::free_balance(&reward_account) == 0u128.saturated_into()); + } + + purge_outcomes { + // RemoveKeysLimit - 2 to ensure that we actually fully clean and return at the end + // at least two voting outcomes + let k in 2..(T::RemoveKeysLimit::get() - 2); + let o in 1..T::MaxOwners::get(); let market_id: MarketIdOf = 0u128.saturated_into(); + let market = market_mock::(); + T::MarketCommons::push_market(market).unwrap(); + + let mut initial_items: Vec> = Vec::new(); + for i in 1..=k { + let owner = account("outcomes_owner", i, 0); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(i.into()), + owner, + amount: 1_000_000_000u128.saturated_into(), + }); + } + + GlobalDisputes::::start_global_dispute( + &market_id, + initial_items.as_slice(), + ) + .unwrap(); let mut owners = Vec::new(); for i in 1..=o { @@ -302,51 +414,61 @@ benchmarks! { owners.push(owner); } let owners = BoundedVec::try_from(owners.clone()).unwrap(); + let winner_outcome = OutcomeReport::Scalar(0); - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(0), - is_finished: true, - outcome_info: OutcomeInfo { - outcome_sum: 42u128.saturated_into(), - owners, - }, + let possession = Possession::Shared { owners }; + let outcome_info = OutcomeInfo { + outcome_sum: 42u128.saturated_into(), + possession, }; - >::insert(market_id, winner_info); + >::insert(market_id, winner_outcome.clone(), outcome_info); + + let possession = Possession::Shared { owners: Default::default() }; + let outcome_info = OutcomeInfo { + outcome_sum: 42u128.saturated_into(), + possession, + }; + let gd_info = GlobalDisputeInfo {winner_outcome, status: GdStatus::Finished, outcome_info}; + >::insert(market_id, gd_info); let caller: T::AccountId = whitelisted_caller(); let outcome = OutcomeReport::Scalar(20); - let reward_account = GlobalDisputes::::reward_account(&market_id); - assert!(T::Currency::free_balance(&reward_account) == 0u128.saturated_into()); - deposit::(&caller); - }: { - >::reward_outcome_owner(RawOrigin::Signed(caller.clone()).into(), market_id) - .unwrap(); - } verify { - assert_last_event::(Event::OutcomeOwnersRewardedWithNoFunds:: { market_id }.into()); + }: _(RawOrigin::Signed(caller.clone()), market_id) + verify { + assert!(>::iter_prefix(market_id).next().is_none()); + assert_last_event::(Event::OutcomesFullyCleaned:: { market_id }.into()); } - purge_outcomes { + refund_vote_fees { // RemoveKeysLimit - 2 to ensure that we actually fully clean and return at the end - let k in 1..(T::RemoveKeysLimit::get() - 2); + // at least two voting outcomes + let k in 2..(T::RemoveKeysLimit::get() - 2); let o in 1..T::MaxOwners::get(); let market_id: MarketIdOf = 0u128.saturated_into(); + let market = market_mock::(); + T::MarketCommons::push_market(market).unwrap(); + let mut initial_items: Vec> = Vec::new(); for i in 1..=k { let owner = account("outcomes_owner", i, 0); - GlobalDisputes::::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(i.into()), - &owner, - 1_000_000_000u128.saturated_into(), - ) - .unwrap(); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(i.into()), + owner, + amount: 1_000_000_000u128.saturated_into(), + }); } + GlobalDisputes::::start_global_dispute( + &market_id, + initial_items.as_slice(), + ) + .unwrap(); + let mut owners = Vec::new(); for i in 1..=o { let owner = account("winners_owner", i, 0); @@ -355,18 +477,20 @@ benchmarks! { let owners = BoundedVec::try_from(owners.clone()).unwrap(); let winner_outcome = OutcomeReport::Scalar(0); + let possession = Possession::Shared { owners }; let outcome_info = OutcomeInfo { outcome_sum: 42u128.saturated_into(), - owners + possession, }; >::insert(market_id, winner_outcome.clone(), outcome_info); + let possession = Possession::Shared { owners: Default::default() }; let outcome_info = OutcomeInfo { outcome_sum: 42u128.saturated_into(), - owners: Default::default() + possession, }; - let winner_info = WinnerInfo {outcome: winner_outcome, is_finished: true, outcome_info}; - >::insert(market_id, winner_info); + let gd_info = GlobalDisputeInfo {winner_outcome, status: GdStatus::Destroyed, outcome_info}; + >::insert(market_id, gd_info); let caller: T::AccountId = whitelisted_caller(); diff --git a/zrml/global-disputes/src/global_disputes_pallet_api.rs b/zrml/global-disputes/src/global_disputes_pallet_api.rs index 26682c768..28dc58e5e 100644 --- a/zrml/global-disputes/src/global_disputes_pallet_api.rs +++ b/zrml/global-disputes/src/global_disputes_pallet_api.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Forecasting Technologies LTD. +// Copyright 2022-2023 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -17,46 +17,32 @@ extern crate alloc; -use alloc::vec::Vec; -use sp_runtime::DispatchResult; +use crate::types::InitialItem; +use sp_runtime::DispatchError; use zeitgeist_primitives::types::OutcomeReport; /// The trait to initiate and resolve the global disputes. -pub trait GlobalDisputesPalletApi { - /// Push a voting outcome for one global dispute. - /// - /// # Arguments - /// - `market_id` - The id of the market. - /// - `outcome` - The voting outcome to push. - /// - `owner` - The owner of the outcome. - /// - `initial_vote_balance` - The initial vote amount for the specified outcome. - /// - /// # Returns - /// - /// Returns the dispute mechanism's report if available, otherwise `None`. If `None` is - /// returned, this means that the dispute could not be resolved. - fn push_voting_outcome( - market_id: &MarketId, - outcome: OutcomeReport, - owner: &AccountId, - initial_vote_balance: Balance, - ) -> DispatchResult; +pub trait GlobalDisputesPalletApi { + /// Return the `AddOutcomePeriod` parameter. + fn get_add_outcome_period() -> BlockNumber; + + /// Return the `GdVotingPeriod` parameter. + fn get_vote_period() -> BlockNumber; - /// Get the information about a voting outcome for a global dispute. + /// Start a global dispute. /// /// # Arguments /// - `market_id` - The id of the market. - /// - `outcome` - The voting outcome to get. - /// - /// # Returns - /// - /// Returns the information stored for a particular outcome. - /// - outcome_sum - The current sum of all locks on this outcome. - /// - owners - The vector of owners of the outcome. - fn get_voting_outcome_info( + /// - `initial_items` - The initial vote options (outcome, owner, amount) + /// to add to the global dispute. One initial item consists of the vote outcome, + /// the owner of the outcome who is rewarded in case of a win, + /// and the initial vote amount for this outcome. + /// It is required to add at least two unique outcomes. + /// In case of a duplicated outcome, the owner and amount is added to the pre-existing outcome. + fn start_global_dispute( market_id: &MarketId, - outcome: &OutcomeReport, - ) -> Option<(Balance, Vec)>; + initial_items: &[InitialItem], + ) -> Result; /// Determine the winner of a global dispute. /// @@ -68,17 +54,19 @@ pub trait GlobalDisputesPalletApi { /// Returns the winning outcome. fn determine_voting_winner(market_id: &MarketId) -> Option; - /// Check if global dispute started. + /// Check if a global dispute exists for the specified market. + fn does_exist(market_id: &MarketId) -> bool; + + /// Check if global dispute is active. + /// This call is useful to check if a global dispute is ready for a destruction. /// /// # Arguments /// - `market_id` - The id of the market. - fn is_started(market_id: &MarketId) -> bool; + fn is_active(market_id: &MarketId) -> bool; - /// Check if a global dispute has not already been started. + /// Destroy a global dispute and allow to return all funds of the participants. /// /// # Arguments /// - `market_id` - The id of the market. - fn is_not_started(market_id: &MarketId) -> bool { - !Self::is_started(market_id) - } + fn destroy_global_dispute(market_id: &MarketId) -> Result<(), DispatchError>; } diff --git a/zrml/global-disputes/src/lib.rs b/zrml/global-disputes/src/lib.rs index e5968f693..298d83431 100644 --- a/zrml/global-disputes/src/lib.rs +++ b/zrml/global-disputes/src/lib.rs @@ -18,19 +18,26 @@ #![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + mod benchmarks; mod global_disputes_pallet_api; +pub mod migrations; mod mock; mod tests; pub mod types; +mod utils; pub mod weights; pub use global_disputes_pallet_api::GlobalDisputesPalletApi; pub use pallet::*; +pub type PossessionOf = crate::types::Possession, BalanceOf, OwnerInfoOf>; +pub type InitialItemOf = crate::types::InitialItem, BalanceOf>; + #[frame_support::pallet] mod pallet { - use crate::{types::*, weights::WeightInfoZeitgeist, GlobalDisputesPalletApi}; + use crate::{types::*, weights::WeightInfoZeitgeist, GlobalDisputesPalletApi, InitialItemOf}; use core::marker::PhantomData; use frame_support::{ ensure, log, @@ -40,26 +47,63 @@ mod pallet { sp_runtime::traits::StaticLookup, traits::{ Currency, ExistenceRequirement, Get, IsType, LockIdentifier, LockableCurrency, - WithdrawReasons, + StorageVersion, WithdrawReasons, }, Blake2_128Concat, BoundedVec, PalletId, Twox64Concat, }; use frame_system::{ensure_signed, pallet_prelude::OriginFor}; use sp_runtime::{ traits::{AccountIdConversion, CheckedDiv, Saturating, Zero}, - DispatchResult, + DispatchError, DispatchResult, }; use sp_std::{vec, vec::Vec}; - use zeitgeist_primitives::types::OutcomeReport; + use zeitgeist_primitives::{traits::DisputeResolutionApi, types::OutcomeReport}; use zrml_market_commons::MarketCommonsPalletApi; + pub(crate) type BalanceOf = <::Currency as Currency>>::Balance; + pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; + pub(crate) type MarketIdOf = + <::MarketCommons as MarketCommonsPalletApi>::MarketId; + + pub type AccountIdOf = ::AccountId; + + pub(crate) type OwnerInfoOf = BoundedVec, ::MaxOwners>; + pub type OutcomeInfoOf = OutcomeInfo, BalanceOf, OwnerInfoOf>; + pub type GlobalDisputeInfoOf = GlobalDisputeInfo< + AccountIdOf, + BalanceOf, + OwnerInfoOf, + ::BlockNumber, + >; + + type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + pub type LockInfoOf = + BoundedVec<(MarketIdOf, BalanceOf), ::MaxGlobalDisputeVotes>; + + // TODO(#968): to remove after the storage migration + pub type WinnerInfoOf = OldWinnerInfo, OwnerInfoOf>; + + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + #[pallet::config] pub trait Config: frame_system::Config { + /// The time period in which the addition of new outcomes are allowed. + #[pallet::constant] + type AddOutcomePeriod: Get; + /// The currency implementation used to lock tokens for voting. type Currency: LockableCurrency; type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type DisputeResolution: DisputeResolutionApi< + AccountId = Self::AccountId, + BlockNumber = Self::BlockNumber, + MarketId = MarketIdOf, + Moment = MomentOf, + >; + /// The vote lock identifier. #[pallet::constant] type GlobalDisputeLockId: Get; @@ -69,17 +113,21 @@ mod pallet { type GlobalDisputesPalletId: Get; /// To reference the market id type. - type MarketCommons: MarketCommonsPalletApi; + type MarketCommons: MarketCommonsPalletApi< + AccountId = Self::AccountId, + Currency = Self::Currency, + BlockNumber = Self::BlockNumber, + >; /// The maximum numbers of distinct markets /// on which one account can simultaneously vote on outcomes. - /// Otherwise users can just keep voting on different global disputes and never unlock. /// When the user unlocks, the user has again `MaxGlobalDisputeVotes` number of votes. + /// This constant is useful to limit the number of for-loop iterations (weight constraints). #[pallet::constant] type MaxGlobalDisputeVotes: Get; /// The maximum number of owners - /// for a voting outcome for private API calls of `push_voting_outcome`. + /// for a voting outcome for private API calls of `push_vote_outcome`. #[pallet::constant] type MaxOwners: Get; @@ -91,6 +139,10 @@ mod pallet { #[pallet::constant] type RemoveKeysLimit: Get; + /// The time period in which votes are allowed. + #[pallet::constant] + type GdVotingPeriod: Get; + /// The fee required to add a voting outcome. #[pallet::constant] type VotingOutcomeFee: Get>; @@ -98,19 +150,8 @@ mod pallet { type WeightInfo: WeightInfoZeitgeist; } - pub(crate) type BalanceOf = <::Currency as Currency>>::Balance; - pub(crate) type MarketIdOf = - <::MarketCommons as MarketCommonsPalletApi>::MarketId; - pub type AccountIdOf = ::AccountId; - - pub type OutcomeInfoOf = OutcomeInfo, OwnerInfoOf>; - pub type WinnerInfoOf = WinnerInfo, OwnerInfoOf>; - type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; - type OwnerInfoOf = BoundedVec, ::MaxOwners>; - pub type LockInfoOf = - BoundedVec<(MarketIdOf, BalanceOf), ::MaxGlobalDisputeVotes>; - #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData); /// All highest lock information (vote id, outcome index and locked balance) @@ -134,7 +175,12 @@ mod pallet { >; /// Maps the market id to all information - /// about the winner outcome and if the global dispute is finished. + /// about the global dispute. + #[pallet::storage] + pub type GlobalDisputesInfo = + StorageMap<_, Twox64Concat, MarketIdOf, GlobalDisputeInfoOf, OptionQuery>; + + // TODO(#986): to remove after the storage migration #[pallet::storage] pub type Winners = StorageMap<_, Twox64Concat, MarketIdOf, WinnerInfoOf, OptionQuery>; @@ -153,10 +199,10 @@ mod pallet { }, /// The winner of the global dispute system is determined. GlobalDisputeWinnerDetermined { market_id: MarketIdOf }, - /// No funds could be spent as reward to the outcome owner(s). - OutcomeOwnersRewardedWithNoFunds { market_id: MarketIdOf }, - /// The outcome owner has been rewarded. + /// The outcome owners have been rewarded. OutcomeOwnersRewarded { market_id: MarketIdOf, owners: Vec> }, + /// The outcome owner has been rewarded. + OutcomeOwnerRewarded { market_id: MarketIdOf, owner: AccountIdOf }, /// The outcomes storage item is partially cleaned. OutcomesPartiallyCleaned { market_id: MarketIdOf }, /// The outcomes storage item is fully cleaned. @@ -172,26 +218,40 @@ mod pallet { #[pallet::error] pub enum Error { - /// Sender tried to vote with an amount below a defined minium. + /// Sender tried to vote with an amount below a defined minimum. AmountTooLow, - /// The global dispute period is already over and the winner is determined. - GlobalDisputeAlreadyFinished, + /// The global dispute status is invalid for this operation. + InvalidGlobalDisputeStatus, /// Sender does not have enough funds for the vote on an outcome. InsufficientAmount, /// The maximum amount of owners is reached. MaxOwnersReached, + /// The maximum number of votes for this account is reached. + MaxVotesReached, + /// The amount in the reward pot is zero. + NoFundsToReward, /// No global dispute present at the moment. - NoGlobalDisputeStarted, + GlobalDisputeNotFound, /// The voting outcome has been already added. OutcomeAlreadyExists, /// The outcome specified is not present in the voting outcomes. OutcomeDoesNotExist, - /// The global dispute period is not over yet. The winner is not yet determined. - UnfinishedGlobalDispute, - /// The maximum number of votes for this account is reached. - MaxVotesReached, + /// Submitted outcome does not match market type. + OutcomeMismatch, /// The outcomes are not fully cleaned yet. OutcomesNotFullyCleaned, + /// Only a shared possession is allowed. + SharedPossessionRequired, + /// The global dispute period is not over yet. The winner is not yet determined. + UnfinishedGlobalDispute, + /// The period in which outcomes can be added is over. + AddOutcomePeriodIsOver, + /// It is not inside the period in which votes are allowed. + NotInGdVotingPeriod, + /// The operation requires a global dispute in a destroyed state. + GlobalDisputeNotDestroyed, + /// The global dispute was already started. + GlobalDisputeAlreadyExists, } #[pallet::call] @@ -219,9 +279,17 @@ mod pallet { ) -> DispatchResultWithPostInfo { let owner = ensure_signed(origin)?; - let winner_info = - >::get(market_id).ok_or(Error::::NoGlobalDisputeStarted)?; - ensure!(!winner_info.is_finished, Error::::GlobalDisputeAlreadyFinished); + let market = T::MarketCommons::market(&market_id)?; + ensure!(market.matches_outcome_report(&outcome), Error::::OutcomeMismatch); + + let gd_info = + >::get(market_id).ok_or(Error::::GlobalDisputeNotFound)?; + let now = >::block_number(); + if let GdStatus::Active { add_outcome_end, vote_end: _ } = gd_info.status { + ensure!(now <= add_outcome_end, Error::::AddOutcomePeriodIsOver); + } else { + return Err(Error::::InvalidGlobalDisputeStatus.into()); + } ensure!( !>::contains_key(market_id, &outcome), @@ -230,17 +298,20 @@ mod pallet { let voting_outcome_fee = T::VotingOutcomeFee::get(); - Self::push_voting_outcome(&market_id, outcome.clone(), &owner, voting_outcome_fee)?; - let reward_account = Self::reward_account(&market_id); T::Currency::transfer( &owner, &reward_account, voting_outcome_fee, - ExistenceRequirement::AllowDeath, + ExistenceRequirement::KeepAlive, )?; + let possession = Possession::Paid { owner: owner.clone(), fee: voting_outcome_fee }; + let outcome_info = OutcomeInfo { outcome_sum: voting_outcome_fee, possession }; + Self::update_winner(&market_id, &outcome, outcome_info.clone()); + >::insert(market_id, outcome.clone(), outcome_info); + Self::deposit_event(Event::AddedVotingOutcome { market_id, owner, outcome }); // charge weight for successfully have no owners in Winners // this is the case, because owners are not inserted @@ -248,6 +319,64 @@ mod pallet { Ok((Some(T::WeightInfo::add_vote_outcome(0u32))).into()) } + /// Return the voting outcome fees in case the global dispute was destroyed. + /// + /// # Arguments + /// + /// - `market_id`: The id of the market. + /// + /// # Weight + /// + /// Complexity: `O(n)`, + /// where `n` is the number of all existing outcomes for a global dispute. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::refund_vote_fees( + T::RemoveKeysLimit::get(), + T::MaxOwners::get(), + ))] + #[frame_support::transactional] + pub fn refund_vote_fees( + origin: OriginFor, + #[pallet::compact] market_id: MarketIdOf, + ) -> DispatchResultWithPostInfo { + ensure_signed(origin)?; + + let gd_info = + >::get(market_id).ok_or(Error::::GlobalDisputeNotFound)?; + ensure!(gd_info.status == GdStatus::Destroyed, Error::::GlobalDisputeNotDestroyed); + + let mut owners_len = 0u32; + let mut removed_keys_amount = 0u32; + for (_, outcome_info) in + >::drain_prefix(market_id).take(T::RemoveKeysLimit::get() as usize) + { + match outcome_info.possession { + Possession::Paid { owner, fee } => { + let res = T::Currency::transfer( + &Self::reward_account(&market_id), + &owner, + fee, + ExistenceRequirement::AllowDeath, + ); + debug_assert!(res.is_ok()); + } + Possession::Shared { owners } => { + owners_len = owners_len.saturating_add(owners.len() as u32); + } + } + removed_keys_amount = removed_keys_amount.saturating_add(1u32); + } + + if >::iter_prefix(market_id).next().is_none() { + Self::deposit_event(Event::OutcomesFullyCleaned { market_id }); + } else { + Self::deposit_event(Event::OutcomesPartiallyCleaned { market_id }); + } + + // weight for max owners, because we don't know + Ok((Some(T::WeightInfo::refund_vote_fees(removed_keys_amount, owners_len))).into()) + } + /// Purge all outcomes to allow the winning outcome owner(s) to get their reward. /// Fails if the global dispute is not concluded yet. /// @@ -271,28 +400,31 @@ mod pallet { ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; - let mut winner_info = - >::get(market_id).ok_or(Error::::NoGlobalDisputeStarted)?; - ensure!(winner_info.is_finished, Error::::UnfinishedGlobalDispute); + let mut gd_info = + >::get(market_id).ok_or(Error::::GlobalDisputeNotFound)?; + ensure!(gd_info.status == GdStatus::Finished, Error::::UnfinishedGlobalDispute); let winning_outcome: Option> = - >::get(market_id, &winner_info.outcome); + >::take(market_id, &gd_info.winner_outcome); let mut owners_len = 0u32; - // move the winning outcome info to Winners before it gets drained + // move the winning outcome info to GlobalDisputesInfo before it gets drained if let Some(outcome_info) = winning_outcome { - owners_len = outcome_info.owners.len() as u32; - // storage write is needed here in case, - // that the first call of reward_outcome_owner doesn't reward the owners - // this can happen if there are more than RemoveKeysLimit keys to remove - winner_info.outcome_info = outcome_info; - >::insert(market_id, winner_info); + if let Possession::Shared { owners } = &outcome_info.possession { + owners_len = owners.len() as u32; + } + // storage write is needed in case to save the owners + // of the winning outcome before they are drained + gd_info.outcome_info = outcome_info; + >::insert(market_id, gd_info); } let mut removed_keys_amount = 0u32; - for (_, i) in + for (_, outcome_info) in >::drain_prefix(market_id).take(T::RemoveKeysLimit::get() as usize) { - owners_len = owners_len.max(i.owners.len() as u32); + if let Possession::Shared { owners } = outcome_info.possession { + owners_len = owners_len.saturating_add(owners.len() as u32); + } removed_keys_amount = removed_keys_amount.saturating_add(1u32); } @@ -317,11 +449,8 @@ mod pallet { /// /// Complexity: `O(n)`, where `n` is the number of owners for the winning outcome. #[pallet::call_index(2)] - #[pallet::weight( - T::WeightInfo::reward_outcome_owner_no_funds(T::MaxOwners::get()).max( - T::WeightInfo::reward_outcome_owner_with_funds(T::MaxOwners::get()), - ) - )] + #[pallet::weight(T::WeightInfo::reward_outcome_owner_paid_possession() + .max(T::WeightInfo::reward_outcome_owner_shared_possession(T::MaxOwners::get())))] #[frame_support::transactional] pub fn reward_outcome_owner( origin: OriginFor, @@ -334,56 +463,28 @@ mod pallet { >::OutcomesNotFullyCleaned ); - let winner_info = - >::get(market_id).ok_or(Error::::NoGlobalDisputeStarted)?; - ensure!(winner_info.is_finished, Error::::UnfinishedGlobalDispute); + let gd_info = + >::get(market_id).ok_or(Error::::GlobalDisputeNotFound)?; + ensure!(gd_info.status == GdStatus::Finished, Error::::UnfinishedGlobalDispute); let reward_account = Self::reward_account(&market_id); let reward_account_free_balance = T::Currency::free_balance(&reward_account); - let owners_len = winner_info.outcome_info.owners.len() as u32; - - if reward_account_free_balance.is_zero() { - Self::deposit_event(Event::OutcomeOwnersRewardedWithNoFunds { market_id }); - // return early case if there is no reward - return Ok((Some(T::WeightInfo::reward_outcome_owner_no_funds(owners_len))).into()); - } - - let mut remainder = reward_account_free_balance; - let owners_len_in_balance: BalanceOf = >::from(owners_len); - if let Some(reward_per_each) = - reward_account_free_balance.checked_div(&owners_len_in_balance) - { - for winner in winner_info.outcome_info.owners.iter() { - // *Should* always be equal to `reward_per_each` - let reward = remainder.min(reward_per_each); - remainder = remainder.saturating_sub(reward); - // Reward the loosing funds to the winners - let res = T::Currency::transfer( - &reward_account, - winner, - reward, - ExistenceRequirement::AllowDeath, - ); - // not really much we can do if it fails - debug_assert!( - res.is_ok(), - "Global Disputes: Rewarding a outcome owner failed." - ); - } - } else { - log::error!( - "Global Disputes: There should be always at least one owner for a voting \ - outcome." - ); - debug_assert!(false); + ensure!(!reward_account_free_balance.is_zero(), Error::::NoFundsToReward); + + match gd_info.outcome_info.possession { + Possession::Shared { owners } => Self::reward_shared_possession( + market_id, + reward_account, + reward_account_free_balance, + owners, + ), + Possession::Paid { owner, fee: _ } => Self::reward_paid_possession( + market_id, + reward_account, + reward_account_free_balance, + owner, + ), } - - Self::deposit_event(Event::OutcomeOwnersRewarded { - market_id, - owners: winner_info.outcome_info.owners.to_vec(), - }); - - Ok((Some(T::WeightInfo::reward_outcome_owner_with_funds(owners_len))).into()) } /// Vote on existing voting outcomes by locking native tokens. @@ -416,13 +517,21 @@ mod pallet { ensure!(amount <= voter_free_balance, Error::::InsufficientAmount); ensure!(amount >= T::MinOutcomeVoteAmount::get(), Error::::AmountTooLow); - let winner_info = - >::get(market_id).ok_or(Error::::NoGlobalDisputeStarted)?; - ensure!(!winner_info.is_finished, Error::::GlobalDisputeAlreadyFinished); + let gd_info = + >::get(market_id).ok_or(Error::::GlobalDisputeNotFound)?; + let now = >::block_number(); + if let GdStatus::Active { add_outcome_end, vote_end } = gd_info.status { + ensure!(add_outcome_end < now && now <= vote_end, Error::::NotInGdVotingPeriod); + } else { + return Err(Error::::InvalidGlobalDisputeStatus.into()); + } let mut outcome_info = >::get(market_id, &outcome).ok_or(Error::::OutcomeDoesNotExist)?; - let outcome_owners_len = outcome_info.owners.len() as u32; + let outcome_owners_len = match outcome_info.possession { + Possession::Shared { ref owners } => owners.len() as u32, + Possession::Paid { .. } => 1u32, + }; // The `outcome_sum` never decreases (only increases) to allow // caching the outcome with the highest `outcome_sum`. @@ -431,7 +540,7 @@ mod pallet { // than the second highest `outcome_sum`. let add_to_outcome_sum = |a| { outcome_info.outcome_sum = outcome_info.outcome_sum.saturating_add(a); - Self::update_winner(&market_id, &outcome, outcome_info.outcome_sum); + Self::update_winner(&market_id, &outcome, outcome_info.clone()); >::insert(market_id, &outcome, outcome_info); }; @@ -475,7 +584,7 @@ mod pallet { Ok(Some(T::WeightInfo::vote_on_outcome(outcome_owners_len, vote_lock_counter)).into()) } - /// Return all locked native tokens in a global dispute. + /// Return all locked native tokens from a finished or destroyed global dispute. /// Fails if the global dispute is not concluded yet. /// /// # Arguments @@ -509,13 +618,13 @@ mod pallet { let mut lock_info = >::get(&voter); let vote_lock_counter = lock_info.len() as u32; // Inside retain we follow these rules: - // 1. Remove all locks from resolved (/ finished) global disputes. + // 1. Remove all locks from resolved (/ finished / destroyed) global disputes. // 2. Then find the maximum lock from all unresolved global disputes. lock_info.retain(|&(market_id, locked_balance)| { // weight component MaxOwners comes from querying the winner information - match >::get(market_id) { - Some(winner_info) => { - if winner_info.is_finished { + match >::get(market_id) { + Some(gd_info) => { + if matches!(gd_info.status, GdStatus::Finished | GdStatus::Destroyed) { false } else { lock_needed = lock_needed.max(locked_balance); @@ -567,79 +676,110 @@ mod pallet { T::GlobalDisputesPalletId::get().into_sub_account_truncating(market_id) } - fn update_winner(market_id: &MarketIdOf, outcome: &OutcomeReport, amount: BalanceOf) { - >::mutate(market_id, |highest: &mut Option>| { - *highest = Some(highest.clone().map_or( - WinnerInfo::new(outcome.clone(), amount), - |prev_winner_info| { - if amount >= prev_winner_info.outcome_info.outcome_sum { - WinnerInfo::new(outcome.clone(), amount) - } else { - prev_winner_info - } - }, - )); + fn update_winner( + market_id: &MarketIdOf, + outcome: &OutcomeReport, + outcome_info: OutcomeInfoOf, + ) { + let amount = outcome_info.outcome_sum; + >::mutate( + market_id, + |highest: &mut Option>| { + *highest = Some(highest.clone().map_or( + // if never a highest was present set the first here + GlobalDisputeInfo::new(outcome.clone(), outcome_info.possession, amount), + |mut prev_gd_info| { + if amount >= prev_gd_info.outcome_info.outcome_sum { + prev_gd_info.update_winner(outcome.clone(), amount); + prev_gd_info + } else { + prev_gd_info + } + }, + )); + }, + ); + } + + fn reward_shared_possession( + market_id: MarketIdOf, + reward_account: AccountIdOf, + reward: BalanceOf, + owners: OwnerInfoOf, + ) -> DispatchResultWithPostInfo { + let mut remainder = reward; + let owners_len = owners.len() as u32; + let owners_len_in_balance: BalanceOf = >::from(owners_len); + if let Some(reward_per_each) = reward.checked_div(&owners_len_in_balance) { + for winner in owners.iter() { + // *Should* always be equal to `reward_per_each` + let reward = remainder.min(reward_per_each); + remainder = remainder.saturating_sub(reward); + // Reward the losing funds to the winners + let res = T::Currency::transfer( + &reward_account, + winner, + reward, + ExistenceRequirement::AllowDeath, + ); + // not really much we can do if it fails + debug_assert!( + res.is_ok(), + "Global Disputes: Rewarding a outcome owner failed." + ); + } + } else { + log::error!( + "Global Disputes: There should be always at least one owner for a voting \ + outcome. This can also happen if reward is smaller than owners_len." + ); + debug_assert!(false); + } + Self::deposit_event(Event::OutcomeOwnersRewarded { + market_id, + owners: owners.into_inner(), }); + Ok((Some(T::WeightInfo::reward_outcome_owner_shared_possession(owners_len))).into()) + } + + fn reward_paid_possession( + market_id: MarketIdOf, + reward_account: AccountIdOf, + reward: BalanceOf, + owner: AccountIdOf, + ) -> DispatchResultWithPostInfo { + let res = T::Currency::transfer( + &reward_account, + &owner, + reward, + ExistenceRequirement::AllowDeath, + ); + // not really much we can do if it fails + debug_assert!(res.is_ok(), "Global Disputes: Rewarding a outcome owner failed."); + Self::deposit_event(Event::OutcomeOwnerRewarded { market_id, owner }); + Ok((Some(T::WeightInfo::reward_outcome_owner_paid_possession())).into()) } } - impl GlobalDisputesPalletApi, AccountIdOf, BalanceOf> for Pallet + impl GlobalDisputesPalletApi, AccountIdOf, BalanceOf, T::BlockNumber> + for Pallet where T: Config, { - fn push_voting_outcome( - market_id: &MarketIdOf, - outcome: OutcomeReport, - owner: &T::AccountId, - initial_vote_balance: BalanceOf, - ) -> DispatchResult { - match >::get(market_id) { - Some(winner_info) if winner_info.is_finished => { - return Err(Error::::GlobalDisputeAlreadyFinished.into()); - } - _ => (), - } - match >::get(market_id, &outcome) { - Some(mut outcome_info) => { - let outcome_sum = outcome_info.outcome_sum.saturating_add(initial_vote_balance); - outcome_info.outcome_sum = outcome_sum; - outcome_info - .owners - .try_push(owner.clone()) - .map_err(|_| Error::::MaxOwnersReached)?; - Self::update_winner(market_id, &outcome, outcome_sum); - >::insert(market_id, outcome, outcome_info); - } - None => { - // adding one item to BoundedVec can not fail - if let Ok(owners) = BoundedVec::try_from(vec![owner.clone()]) { - Self::update_winner(market_id, &outcome, initial_vote_balance); - let outcome_info = - OutcomeInfo { outcome_sum: initial_vote_balance, owners }; - >::insert(market_id, outcome, outcome_info); - } else { - log::error!("Global Disputes: Could not construct a bounded vector."); - debug_assert!(false); - } - } - } - Ok(()) + fn get_add_outcome_period() -> T::BlockNumber { + T::AddOutcomePeriod::get() } - fn get_voting_outcome_info( - market_id: &MarketIdOf, - outcome: &OutcomeReport, - ) -> Option<(BalanceOf, Vec>)> { - >::get(market_id, outcome) - .map(|outcome_info| (outcome_info.outcome_sum, outcome_info.owners.into_inner())) + fn get_vote_period() -> T::BlockNumber { + T::GdVotingPeriod::get() } fn determine_voting_winner(market_id: &MarketIdOf) -> Option { - match >::get(market_id) { - Some(mut winner_info) => { - winner_info.is_finished = true; - let winner_outcome = winner_info.outcome.clone(); - >::insert(market_id, winner_info); + match >::get(market_id) { + Some(mut gd_info) => { + gd_info.status = GdStatus::Finished; + let winner_outcome = gd_info.winner_outcome.clone(); + >::insert(market_id, gd_info); Self::deposit_event(Event::GlobalDisputeWinnerDetermined { market_id: *market_id, }); @@ -649,8 +789,90 @@ mod pallet { } } - fn is_started(market_id: &MarketIdOf) -> bool { - >::get(market_id).is_some() + fn does_exist(market_id: &MarketIdOf) -> bool { + >::get(market_id).is_some() + } + + fn is_active(market_id: &MarketIdOf) -> bool { + if let Some(gd_info) = >::get(market_id) { + if let GdStatus::Active { add_outcome_end: _, vote_end: _ } = gd_info.status { + return true; + } + } + false + } + + fn start_global_dispute( + market_id: &MarketIdOf, + initial_items: &[InitialItemOf], + ) -> Result { + let market = T::MarketCommons::market(market_id)?; + + ensure!( + >::get(market_id).is_none(), + Error::::GlobalDisputeAlreadyExists + ); + + for InitialItem { outcome, owner, amount } in initial_items { + ensure!(market.matches_outcome_report(outcome), Error::::OutcomeMismatch); + + match >::get(market_id, outcome) { + Some(mut outcome_info) => { + let outcome_sum = outcome_info.outcome_sum.saturating_add(*amount); + outcome_info.outcome_sum = outcome_sum; + let mut owners = outcome_info + .possession + .get_shared_owners() + .ok_or(Error::::SharedPossessionRequired)?; + owners.try_push(owner.clone()).map_err(|_| Error::::MaxOwnersReached)?; + outcome_info.possession = Possession::Shared { owners }; + Self::update_winner(market_id, outcome, outcome_info.clone()); + >::insert(market_id, outcome, outcome_info); + } + None => { + // adding one item to BoundedVec can not fail + if let Ok(owners) = BoundedVec::try_from(vec![owner.clone()]) { + let possession = Possession::Shared { owners }; + let outcome_info = OutcomeInfo { outcome_sum: *amount, possession }; + Self::update_winner(market_id, outcome, outcome_info.clone()); + >::insert(market_id, outcome, outcome_info); + } else { + log::error!("Global Disputes: Could not construct a bounded vector."); + debug_assert!(false); + } + } + } + } + + let now = >::block_number(); + let add_outcome_end = now.saturating_add(T::AddOutcomePeriod::get()); + let vote_end = add_outcome_end.saturating_add(T::GdVotingPeriod::get()); + let ids_len = T::DisputeResolution::add_auto_resolve(market_id, vote_end)?; + + >::try_mutate(market_id, |gd_info| -> DispatchResult { + let raw_gd_info = gd_info.as_mut().ok_or(Error::::GlobalDisputeNotFound)?; + raw_gd_info.status = GdStatus::Active { add_outcome_end, vote_end }; + *gd_info = Some(raw_gd_info.clone()); + Ok(()) + })?; + + Ok(ids_len) + } + + fn destroy_global_dispute(market_id: &MarketIdOf) -> Result<(), DispatchError> { + >::try_mutate(market_id, |gd_info| { + let raw_gd_info = gd_info.as_mut().ok_or(Error::::GlobalDisputeNotFound)?; + + // in case the global dispute is already finished nothing needs to be done + if let GdStatus::Active { add_outcome_end: _, vote_end } = raw_gd_info.status { + T::DisputeResolution::remove_auto_resolve(market_id, vote_end); + + raw_gd_info.status = GdStatus::Destroyed; + *gd_info = Some(raw_gd_info.clone()); + } + + Ok(()) + }) } } } diff --git a/zrml/global-disputes/src/migrations.rs b/zrml/global-disputes/src/migrations.rs new file mode 100644 index 000000000..597b46789 --- /dev/null +++ b/zrml/global-disputes/src/migrations.rs @@ -0,0 +1,383 @@ +// Copyright 2022-2023 Forecasting Technologies LTD. +// Copyright 2021-2022 Zeitgeist PM LLC. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +extern crate alloc; + +use crate::{types::*, Config, Pallet as GDPallet, *}; +#[cfg(feature = "try-runtime")] +use alloc::vec::Vec; +use frame_support::{ + dispatch::Weight, + log, + pallet_prelude::PhantomData, + traits::{Get, OnRuntimeUpgrade, StorageVersion}, +}; +use sp_runtime::traits::Saturating; + +#[cfg(feature = "try-runtime")] +use alloc::collections::BTreeMap; +#[cfg(feature = "try-runtime")] +use parity_scale_codec::{Decode, Encode}; +#[cfg(feature = "try-runtime")] +use scale_info::prelude::format; + +const GD_REQUIRED_STORAGE_VERSION: u16 = 0; +const GD_NEXT_STORAGE_VERSION: u16 = 1; + +pub struct ModifyGlobalDisputesStructures(PhantomData); + +impl OnRuntimeUpgrade + for ModifyGlobalDisputesStructures +{ + fn on_runtime_upgrade() -> Weight + where + T: Config, + { + let mut total_weight = T::DbWeight::get().reads(1); + let gd_version = StorageVersion::get::>(); + if gd_version != GD_REQUIRED_STORAGE_VERSION { + log::info!( + "ModifyGlobalDisputesStructures: global disputes version is {:?}, require {:?};", + gd_version, + GD_REQUIRED_STORAGE_VERSION, + ); + return total_weight; + } + log::info!("ModifyGlobalDisputesStructures: Starting..."); + + for (market_id, winner_info) in crate::Winners::::drain() { + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); + + let owners = winner_info.outcome_info.owners; + let owners_len = owners.len(); + let possession = match owners_len { + 1usize => Possession::Paid { + owner: owners + .get(0) + .expect("Owners len is 1, but could not get this owner.") + .clone(), + fee: T::VotingOutcomeFee::get(), + }, + _ => Possession::Shared { owners }, + }; + + let outcome_info = + OutcomeInfo { outcome_sum: winner_info.outcome_info.outcome_sum, possession }; + let gd_info = GlobalDisputeInfo { + winner_outcome: winner_info.outcome, + outcome_info, + status: GdStatus::Finished, + }; + crate::GlobalDisputesInfo::::insert(market_id, gd_info); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + } + + let mut translated = 0u64; + Outcomes::::translate::, OwnerInfoOf>, _>( + |_key1, _key2, old_value| { + translated.saturating_inc(); + + let owners = old_value.owners; + let owners_len = owners.len(); + let possession = match owners_len { + 1usize => Possession::Paid { + owner: owners + .get(0) + .expect("Owners len is 1, but could not get this owner.") + .clone(), + fee: T::VotingOutcomeFee::get(), + }, + _ => Possession::Shared { owners }, + }; + + let new_value = OutcomeInfo { outcome_sum: old_value.outcome_sum, possession }; + + Some(new_value) + }, + ); + log::info!("ModifyGlobalDisputesStructures: Upgraded {} outcomes.", translated); + total_weight = total_weight + .saturating_add(T::DbWeight::get().reads_writes(translated + 1, translated + 1)); + + StorageVersion::new(GD_NEXT_STORAGE_VERSION).put::>(); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + log::info!("ModifyGlobalDisputesStructures: Done!"); + total_weight + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + let old_winners = crate::Winners::::iter() + .collect::, OldWinnerInfo, OwnerInfoOf>>>(); + Ok(old_winners.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(previous_state: Vec) -> Result<(), &'static str> { + let mut markets_count = 0_u32; + let old_winners: BTreeMap, OldWinnerInfo, OwnerInfoOf>> = + Decode::decode(&mut &previous_state[..]) + .expect("Failed to decode state: Invalid state"); + for (market_id, gd_info) in crate::GlobalDisputesInfo::::iter() { + let GlobalDisputeInfo { winner_outcome, outcome_info, status } = gd_info; + + let winner_info: &OldWinnerInfo, OwnerInfoOf> = old_winners + .get(&market_id) + .expect(&format!("Market {:?} not found", market_id)[..]); + + assert_eq!(winner_outcome, winner_info.outcome); + assert_eq!(status, GdStatus::Finished); + + let owners = winner_info.outcome_info.owners.clone(); + let owners_len = owners.len(); + + let possession = match owners_len { + 1usize => Possession::Paid { + owner: owners + .get(0) + .expect("Owners len is 1, but could not get this owner.") + .clone(), + fee: T::VotingOutcomeFee::get(), + }, + _ => Possession::Shared { owners }, + }; + + let outcome_info_expected = + OutcomeInfo { outcome_sum: winner_info.outcome_info.outcome_sum, possession }; + assert_eq!(outcome_info, outcome_info_expected); + + markets_count += 1_u32; + } + let old_markets_count = old_winners.len() as u32; + assert_eq!(markets_count, old_markets_count); + + // empty Winners storage map + assert!(crate::Winners::::iter().next().is_none()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::{ExtBuilder, Runtime, ALICE, BOB}; + use frame_support::{ + migration::{get_storage_value, put_storage_value}, + BoundedVec, + }; + use sp_runtime::traits::SaturatedConversion; + use zeitgeist_primitives::{ + constants::mock::VotingOutcomeFee, + types::{MarketId, OutcomeReport}, + }; + + const GLOBAL_DISPUTES: &[u8] = b"GlobalDisputes"; + const GD_OUTCOMES: &[u8] = b"Outcomes"; + + type OldOutcomeInfoOf = OldOutcomeInfo, OwnerInfoOf>; + + #[test] + fn on_runtime_upgrade_increments_the_storage_versions() { + ExtBuilder::default().build().execute_with(|| { + set_up_chain(); + ModifyGlobalDisputesStructures::::on_runtime_upgrade(); + let gd_version = StorageVersion::get::>(); + assert_eq!(gd_version, GD_NEXT_STORAGE_VERSION); + }); + } + + #[test] + fn on_runtime_sets_new_global_disputes_storage_paid() { + ExtBuilder::default().build().execute_with(|| { + set_up_chain(); + + let market_id = 0u128; + + let outcome_sum = 42u128.saturated_into::>(); + let owners = BoundedVec::try_from(vec![ALICE]).unwrap(); + + let outcome_info = OldOutcomeInfo { outcome_sum, owners }; + let outcome = OutcomeReport::Categorical(42u16); + let winner_info = + OldWinnerInfo { outcome: outcome.clone(), outcome_info, is_finished: true }; + + crate::Winners::::insert(market_id, winner_info); + + ModifyGlobalDisputesStructures::::on_runtime_upgrade(); + + let possession = Possession::Paid { owner: ALICE, fee: VotingOutcomeFee::get() }; + + let new_outcome_info = OutcomeInfo { outcome_sum, possession }; + + let expected = GlobalDisputeInfo { + winner_outcome: outcome, + outcome_info: new_outcome_info, + status: GdStatus::Finished, + }; + + let actual = crate::GlobalDisputesInfo::::get(market_id).unwrap(); + assert_eq!(expected, actual); + + assert!(crate::Winners::::iter().next().is_none()); + }); + } + + #[test] + fn on_runtime_sets_new_global_disputes_storage_shared() { + ExtBuilder::default().build().execute_with(|| { + set_up_chain(); + + let market_id = 0u128; + + let outcome_sum = 42u128.saturated_into::>(); + let owners = BoundedVec::try_from(vec![ALICE, BOB]).unwrap(); + + let outcome_info = OldOutcomeInfo { outcome_sum, owners: owners.clone() }; + let outcome = OutcomeReport::Categorical(42u16); + let winner_info = + OldWinnerInfo { outcome: outcome.clone(), outcome_info, is_finished: true }; + + crate::Winners::::insert(market_id, winner_info); + + ModifyGlobalDisputesStructures::::on_runtime_upgrade(); + + let possession = Possession::Shared { owners }; + + let new_outcome_info = OutcomeInfo { outcome_sum, possession }; + + let expected = GlobalDisputeInfo { + winner_outcome: outcome, + outcome_info: new_outcome_info, + status: GdStatus::Finished, + }; + + let actual = crate::GlobalDisputesInfo::::get(market_id).unwrap(); + assert_eq!(expected, actual); + + assert!(crate::Winners::::iter().next().is_none()); + }); + } + + #[test] + fn on_runtime_sets_new_outcomes_storage_value_shared() { + ExtBuilder::default().build().execute_with(|| { + set_up_chain(); + + let outcome = OutcomeReport::Categorical(0u16); + let hash = + crate::Outcomes::::hashed_key_for::(0, outcome); + + let outcome_sum = 42u128.saturated_into::>(); + let owners = BoundedVec::try_from(vec![ALICE, BOB]).unwrap(); + + let outcome_info = OldOutcomeInfo { outcome_sum, owners: owners.clone() }; + + put_storage_value::>( + GLOBAL_DISPUTES, + GD_OUTCOMES, + &hash, + outcome_info, + ); + + ModifyGlobalDisputesStructures::::on_runtime_upgrade(); + + let possession = Possession::Shared { owners }; + let expected = OutcomeInfo { outcome_sum, possession }; + + let actual = frame_support::migration::get_storage_value::>( + GLOBAL_DISPUTES, + GD_OUTCOMES, + &hash, + ) + .unwrap(); + assert_eq!(expected, actual); + }); + } + + #[test] + fn on_runtime_sets_new_outcomes_storage_value_paid() { + ExtBuilder::default().build().execute_with(|| { + set_up_chain(); + + let outcome = OutcomeReport::Categorical(0u16); + let hash = + crate::Outcomes::::hashed_key_for::(0, outcome); + + let outcome_sum = 42u128.saturated_into::>(); + let owners = BoundedVec::try_from(vec![ALICE]).unwrap(); + + let outcome_info = OldOutcomeInfo { outcome_sum, owners }; + + put_storage_value::>( + GLOBAL_DISPUTES, + GD_OUTCOMES, + &hash, + outcome_info, + ); + + ModifyGlobalDisputesStructures::::on_runtime_upgrade(); + + let possession = Possession::Paid { owner: ALICE, fee: VotingOutcomeFee::get() }; + let expected = OutcomeInfo { outcome_sum, possession }; + + let actual = frame_support::migration::get_storage_value::>( + GLOBAL_DISPUTES, + GD_OUTCOMES, + &hash, + ) + .unwrap(); + assert_eq!(expected, actual); + }); + } + + #[test] + fn on_runtime_is_noop_if_versions_are_not_correct() { + ExtBuilder::default().build().execute_with(|| { + // storage migration already executed (storage version is incremented already) + StorageVersion::new(GD_NEXT_STORAGE_VERSION).put::>(); + + let outcome = OutcomeReport::Categorical(0u16); + let hash = + crate::Outcomes::::hashed_key_for::(0, outcome); + + let outcome_info = OldOutcomeInfo { + outcome_sum: 0u128.saturated_into::>(), + owners: BoundedVec::try_from(vec![ALICE]).unwrap(), + }; + + put_storage_value::>( + GLOBAL_DISPUTES, + GD_OUTCOMES, + &hash, + outcome_info, + ); + + ModifyGlobalDisputesStructures::::on_runtime_upgrade(); + + // no changes should have been made, because the storage version was already incremented + assert!( + get_storage_value::>(GLOBAL_DISPUTES, GD_OUTCOMES, &hash) + .is_none() + ); + }); + } + + fn set_up_chain() { + StorageVersion::new(GD_REQUIRED_STORAGE_VERSION).put::>(); + } +} diff --git a/zrml/global-disputes/src/mock.rs b/zrml/global-disputes/src/mock.rs index 82750e2ed..3f73f1ee2 100644 --- a/zrml/global-disputes/src/mock.rs +++ b/zrml/global-disputes/src/mock.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Forecasting Technologies LTD. +// Copyright 2022-2023 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -18,19 +18,26 @@ #![cfg(test)] use crate::{self as zrml_global_disputes}; -use frame_support::{construct_runtime, parameter_types, traits::Everything}; +use frame_support::{ + construct_runtime, + pallet_prelude::{DispatchError, Weight}, + parameter_types, + traits::Everything, +}; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, }; use zeitgeist_primitives::{ constants::mock::{ - BlockHashCount, GlobalDisputeLockId, GlobalDisputesPalletId, MaxReserves, - MinOutcomeVoteAmount, MinimumPeriod, PmPalletId, RemoveKeysLimit, VotingOutcomeFee, BASE, + AddOutcomePeriod, BlockHashCount, GdVotingPeriod, GlobalDisputeLockId, + GlobalDisputesPalletId, MaxReserves, MinOutcomeVoteAmount, MinimumPeriod, PmPalletId, + RemoveKeysLimit, VotingOutcomeFee, BASE, }, + traits::DisputeResolutionApi, types::{ - AccountIdTest, Balance, BlockNumber, BlockTest, Hash, Index, MarketId, Moment, - UncheckedExtrinsicTest, + AccountIdTest, Asset, Balance, BlockNumber, BlockTest, Hash, Index, Market, MarketId, + Moment, UncheckedExtrinsicTest, }, }; @@ -56,13 +63,54 @@ construct_runtime!( } ); +// NoopResolution implements DisputeResolutionApi with no-ops. +pub struct NoopResolution; + +impl DisputeResolutionApi for NoopResolution { + type AccountId = AccountIdTest; + type Balance = Balance; + type BlockNumber = BlockNumber; + type MarketId = MarketId; + type Moment = Moment; + + fn resolve( + _market_id: &Self::MarketId, + _market: &Market< + Self::AccountId, + Self::Balance, + Self::BlockNumber, + Self::Moment, + Asset, + >, + ) -> Result { + Ok(Weight::zero()) + } + + fn add_auto_resolve( + _market_id: &Self::MarketId, + _resolve_at: Self::BlockNumber, + ) -> Result { + Ok(0u32) + } + + fn auto_resolve_exists(_market_id: &Self::MarketId, _resolve_at: Self::BlockNumber) -> bool { + false + } + + fn remove_auto_resolve(_market_id: &Self::MarketId, _resolve_at: Self::BlockNumber) -> u32 { + 0u32 + } +} + parameter_types! { pub const MaxGlobalDisputeVotes: u32 = 50; pub const MaxOwners: u32 = 10; } impl crate::Config for Runtime { + type AddOutcomePeriod = AddOutcomePeriod; type Currency = Balances; + type DisputeResolution = NoopResolution; type RuntimeEvent = RuntimeEvent; type GlobalDisputeLockId = GlobalDisputeLockId; type GlobalDisputesPalletId = GlobalDisputesPalletId; @@ -71,6 +119,7 @@ impl crate::Config for Runtime { type MaxOwners = MaxOwners; type MinOutcomeVoteAmount = MinOutcomeVoteAmount; type RemoveKeysLimit = RemoveKeysLimit; + type GdVotingPeriod = GdVotingPeriod; type VotingOutcomeFee = VotingOutcomeFee; type WeightInfo = crate::weights::WeightInfo; } diff --git a/zrml/global-disputes/src/tests.rs b/zrml/global-disputes/src/tests.rs index d3efbfd30..bd861221b 100644 --- a/zrml/global-disputes/src/tests.rs +++ b/zrml/global-disputes/src/tests.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Forecasting Technologies LTD. +// Copyright 2022-2023 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -20,8 +20,9 @@ use crate::{ global_disputes_pallet_api::GlobalDisputesPalletApi, mock::*, - types::{OutcomeInfo, WinnerInfo}, - Error, Event, Locks, MarketIdOf, Outcomes, Winners, + types::{GdStatus, GlobalDisputeInfo, InitialItem, OutcomeInfo, Possession}, + utils::market_mock, + BalanceOf, Error, Event, GlobalDisputesInfo, InitialItemOf, Locks, MarketIdOf, Outcomes, }; use frame_support::{ assert_noop, assert_ok, @@ -29,11 +30,15 @@ use frame_support::{ BoundedVec, }; use pallet_balances::{BalanceLock, Error as BalancesError}; -use sp_runtime::traits::Zero; +use sp_runtime::{traits::Zero, SaturatedConversion}; +use test_case::test_case; use zeitgeist_primitives::{ - constants::mock::{GlobalDisputeLockId, MinOutcomeVoteAmount, VotingOutcomeFee, BASE}, - types::OutcomeReport, + constants::mock::{ + GlobalDisputeLockId, MinOutcomeVoteAmount, RemoveKeysLimit, VotingOutcomeFee, BASE, + }, + types::{BlockNumber, OutcomeReport}, }; +use zrml_market_commons::{Error as MarketError, Markets}; const SETUP_AMOUNT: u128 = 100 * BASE; @@ -41,17 +46,20 @@ fn the_lock(amount: u128) -> BalanceLock { BalanceLock { id: GlobalDisputeLockId::get(), amount, reasons: pallet_balances::Reasons::Misc } } -fn setup_vote_outcomes_with_hundred(market_id: &MarketIdOf) { - GlobalDisputes::push_voting_outcome(market_id, OutcomeReport::Scalar(0), &ALICE, SETUP_AMOUNT) - .unwrap(); - - GlobalDisputes::push_voting_outcome(market_id, OutcomeReport::Scalar(20), &ALICE, SETUP_AMOUNT) - .unwrap(); - GlobalDisputes::push_voting_outcome(market_id, OutcomeReport::Scalar(40), &ALICE, SETUP_AMOUNT) - .unwrap(); +fn get_initial_items() -> Vec> { + vec![ + InitialItem { outcome: OutcomeReport::Scalar(0), owner: ALICE, amount: SETUP_AMOUNT }, + InitialItem { outcome: OutcomeReport::Scalar(20), owner: ALICE, amount: SETUP_AMOUNT }, + InitialItem { outcome: OutcomeReport::Scalar(40), owner: ALICE, amount: SETUP_AMOUNT }, + InitialItem { outcome: OutcomeReport::Scalar(60), owner: ALICE, amount: SETUP_AMOUNT }, + ] +} - GlobalDisputes::push_voting_outcome(market_id, OutcomeReport::Scalar(60), &ALICE, SETUP_AMOUNT) - .unwrap(); +fn set_vote_period() { + let now = >::block_number(); + >::set_block_number( + now + ::AddOutcomePeriod::get() + 1, + ); } fn check_outcome_sum( @@ -63,7 +71,7 @@ fn check_outcome_sum( >::get(market_id, outcome).unwrap(), OutcomeInfo { outcome_sum: SETUP_AMOUNT + post_setup_amount, - owners: BoundedVec::try_from(vec![ALICE]).unwrap() + possession: Possession::Shared { owners: BoundedVec::try_from(vec![ALICE]).unwrap() } } ); } @@ -72,26 +80,25 @@ fn check_outcome_sum( fn add_vote_outcome_works() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - 10 * BASE, - ) - .unwrap(); + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + let free_balance_alice_before = Balances::free_balance(ALICE); let free_balance_reward_account = Balances::free_balance(GlobalDisputes::reward_account(&market_id)); assert_ok!(GlobalDisputes::add_vote_outcome( RuntimeOrigin::signed(ALICE), market_id, - OutcomeReport::Scalar(20), + OutcomeReport::Scalar(80), )); System::assert_last_event( Event::::AddedVotingOutcome { market_id, owner: ALICE, - outcome: OutcomeReport::Scalar(20), + outcome: OutcomeReport::Scalar(80), } .into(), ); @@ -106,28 +113,316 @@ fn add_vote_outcome_works() { }); } +#[test_case(GdStatus::Finished; "finished")] +#[test_case(GdStatus::Destroyed; "destroyed")] +fn is_active_works(status: GdStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + assert!(!GlobalDisputes::is_active(&market_id)); + + let outcome_info = OutcomeInfo { + outcome_sum: 0, + possession: Possession::Shared { owners: BoundedVec::try_from(vec![ALICE]).unwrap() }, + }; + >::insert( + market_id, + GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(0), + outcome_info: outcome_info.clone(), + status, + }, + ); + + assert!(!GlobalDisputes::is_active(&market_id)); + + >::insert( + market_id, + GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(0), + outcome_info, + status: GdStatus::Active { add_outcome_end: 0, vote_end: 0 }, + }, + ); + + assert!(GlobalDisputes::is_active(&market_id)); + }); +} + +#[test] +fn destroy_global_dispute_works() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + assert_ok!(GlobalDisputes::destroy_global_dispute(&market_id)); + + assert_eq!( + >::get(market_id).unwrap().status, + GdStatus::Destroyed + ); + }); +} + +#[test] +fn start_global_dispute_works() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + let outcome_info = OutcomeInfo { + outcome_sum: SETUP_AMOUNT, + possession: Possession::Shared { owners: BoundedVec::try_from(vec![ALICE]).unwrap() }, + }; + assert_eq!( + >::get(market_id).unwrap(), + GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(60), + outcome_info, + status: GdStatus::Active { add_outcome_end: 21, vote_end: 161 }, + } + ); + }); +} + +#[test] +fn start_global_dispute_fails_if_outcome_mismatch() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = vec![ + InitialItem { outcome: OutcomeReport::Scalar(0), owner: ALICE, amount: SETUP_AMOUNT }, + InitialItem { outcome: OutcomeReport::Scalar(20), owner: ALICE, amount: SETUP_AMOUNT }, + // categorical outcome mismatch + InitialItem { + outcome: OutcomeReport::Categorical(40), + owner: ALICE, + amount: SETUP_AMOUNT, + }, + InitialItem { outcome: OutcomeReport::Scalar(60), owner: ALICE, amount: SETUP_AMOUNT }, + ]; + assert_eq!( + GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice()), + Err(Error::::OutcomeMismatch.into()) + ); + }); +} + +#[test] +fn start_global_dispute_fails_if_already_exists() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice()).unwrap(); + assert_eq!( + GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice()), + Err(Error::::GlobalDisputeAlreadyExists.into()) + ); + }); +} + +#[test] +fn start_global_dispute_fails_if_max_owner_reached() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, &market); + + let mut initial_items = Vec::new(); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(0), + owner: 0u128, + amount: SETUP_AMOUNT, + }); + for i in 0..MaxOwners::get() + 1 { + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(42), + owner: i.into(), + amount: SETUP_AMOUNT, + }); + } + + assert_eq!( + GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice()), + Err(Error::::MaxOwnersReached.into()) + ); + }); +} + +#[test] +fn start_global_dispute_fails_if_shared_possession_required() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, &market); + + let outcome_info = OutcomeInfo { + outcome_sum: SETUP_AMOUNT, + possession: Possession::Paid { owner: ALICE, fee: VotingOutcomeFee::get() }, + }; + >::insert(market_id, OutcomeReport::Scalar(0), outcome_info); + + let mut initial_items = Vec::new(); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(0), + owner: 0u128, + amount: SETUP_AMOUNT, + }); + for i in 0..MaxOwners::get() + 1 { + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(42), + owner: i.into(), + amount: SETUP_AMOUNT, + }); + } + + assert_eq!( + GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice()), + Err(Error::::SharedPossessionRequired.into()) + ); + }); +} + +#[test] +fn start_global_dispute_adds_owners_for_existing_outcome() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, &market); + + let mut initial_items = Vec::new(); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(0), + owner: 0u128, + amount: SETUP_AMOUNT, + }); + for i in 0..MaxOwners::get() { + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(42), + owner: i.into(), + amount: SETUP_AMOUNT, + }); + } + + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + let outcome_info = >::get(market_id, OutcomeReport::Scalar(42)).unwrap(); + assert_eq!( + outcome_info.possession, + Possession::Shared { owners: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].try_into().unwrap() } + ); + }); +} + +#[test] +fn start_global_dispute_updates_to_highest_winner() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, &market); + + let mut initial_items = Vec::new(); + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(0), + owner: 0u128, + amount: SETUP_AMOUNT, + }); + for i in 0..MaxOwners::get() { + initial_items.push(InitialItem { + outcome: OutcomeReport::Scalar(42 + i.saturated_into::()), + owner: i.into(), + amount: SETUP_AMOUNT + i.saturated_into::(), + }); + } + + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + let gd_info = >::get(market_id).unwrap(); + assert_eq!(gd_info.outcome_info.outcome_sum, SETUP_AMOUNT + 9); + assert_eq!(gd_info.winner_outcome, OutcomeReport::Scalar(51)); + }); +} + +#[test] +fn add_vote_outcome_fails_with_outcome_mismatch() { + ExtBuilder::default().build().execute_with(|| { + // create scalar market + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + assert_noop!( + GlobalDisputes::add_vote_outcome( + RuntimeOrigin::signed(ALICE), + market_id, + OutcomeReport::Categorical(0u16), + ), + Error::::OutcomeMismatch + ); + }); +} + +#[test] +fn add_vote_outcome_fails_with_non_existing_market() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + assert_noop!( + GlobalDisputes::add_vote_outcome( + RuntimeOrigin::signed(ALICE), + market_id, + OutcomeReport::Scalar(80), + ), + MarketError::::MarketDoesNotExist + ); + }); +} + #[test] fn add_vote_outcome_fails_if_no_global_dispute_present() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); assert_noop!( GlobalDisputes::add_vote_outcome( RuntimeOrigin::signed(ALICE), market_id, OutcomeReport::Scalar(20), ), - Error::::NoGlobalDisputeStarted + Error::::GlobalDisputeNotFound ); }); } -#[test] -fn add_vote_outcome_fails_if_global_dispute_finished() { +#[test_case(GdStatus::Finished; "finished")] +#[test_case(GdStatus::Destroyed; "destroyed")] +fn add_vote_outcome_fails_if_global_dispute_is_in_wrong_state(status: GdStatus) { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - let mut winner_info = WinnerInfo::new(OutcomeReport::Scalar(0), 10 * BASE); - winner_info.is_finished = true; - >::insert(market_id, winner_info); + let market = market_mock::(); + Markets::::insert(market_id, market); + let possession = Possession::Shared { owners: BoundedVec::try_from(vec![ALICE]).unwrap() }; + let mut gd_info = GlobalDisputeInfo::new(OutcomeReport::Scalar(0), possession, 10 * BASE); + gd_info.status = status; + >::insert(market_id, gd_info); assert_noop!( GlobalDisputes::add_vote_outcome( @@ -135,7 +430,7 @@ fn add_vote_outcome_fails_if_global_dispute_finished() { market_id, OutcomeReport::Scalar(20), ), - Error::::GlobalDisputeAlreadyFinished + Error::::InvalidGlobalDisputeStatus ); }); } @@ -144,17 +439,18 @@ fn add_vote_outcome_fails_if_global_dispute_finished() { fn add_vote_outcome_fails_if_outcome_already_exists() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - 10 * BASE, - ) - .unwrap(); + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); >::insert( market_id, OutcomeReport::Scalar(20), - OutcomeInfo { outcome_sum: Zero::zero(), owners: Default::default() }, + OutcomeInfo { + outcome_sum: Zero::zero(), + possession: Possession::Shared { owners: Default::default() }, + }, ); assert_noop!( GlobalDisputes::add_vote_outcome( @@ -171,18 +467,17 @@ fn add_vote_outcome_fails_if_outcome_already_exists() { fn add_vote_outcome_fails_if_balance_too_low() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - 10 * BASE, - ) - .unwrap(); + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + assert_noop!( GlobalDisputes::add_vote_outcome( RuntimeOrigin::signed(POOR_PAUL), market_id, - OutcomeReport::Scalar(20), + OutcomeReport::Scalar(80), ), BalancesError::::InsufficientBalance ); @@ -198,19 +493,24 @@ fn reward_outcome_owner_works_for_multiple_owners() { OutcomeReport::Scalar(20), OutcomeInfo { outcome_sum: Zero::zero(), - owners: BoundedVec::try_from(vec![ALICE, BOB, CHARLIE]).unwrap(), + possession: Possession::Shared { + owners: BoundedVec::try_from(vec![ALICE, BOB, CHARLIE]).unwrap(), + }, }, ); let _ = Balances::deposit_creating( &GlobalDisputes::reward_account(&market_id), 3 * VotingOutcomeFee::get(), ); - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(20), - is_finished: true, - outcome_info: OutcomeInfo { outcome_sum: 10 * BASE, owners: Default::default() }, + let gd_info = GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(20), + status: GdStatus::Finished, + outcome_info: OutcomeInfo { + outcome_sum: 10 * BASE, + possession: Possession::Shared { owners: Default::default() }, + }, }; - >::insert(market_id, winner_info); + >::insert(market_id, gd_info); let free_balance_alice_before = Balances::free_balance(ALICE); let free_balance_bob_before = Balances::free_balance(BOB); @@ -252,17 +552,22 @@ fn reward_outcome_owner_has_dust() { OutcomeReport::Scalar(20), OutcomeInfo { outcome_sum: Zero::zero(), - owners: BoundedVec::try_from(vec![ALICE, BOB, CHARLIE, EVE, POOR_PAUL, DAVE]) - .unwrap(), + possession: Possession::Shared { + owners: BoundedVec::try_from(vec![ALICE, BOB, CHARLIE, EVE, POOR_PAUL, DAVE]) + .unwrap(), + }, }, ); let _ = Balances::deposit_creating(&GlobalDisputes::reward_account(&market_id), 100 * BASE); - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(20), - is_finished: true, - outcome_info: OutcomeInfo { outcome_sum: 10 * BASE, owners: Default::default() }, + let gd_info = GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(20), + status: GdStatus::Finished, + outcome_info: OutcomeInfo { + outcome_sum: 10 * BASE, + possession: Possession::Shared { owners: Default::default() }, + }, }; - >::insert(market_id, winner_info); + >::insert(market_id, gd_info); assert_ok!(GlobalDisputes::purge_outcomes(RuntimeOrigin::signed(ALICE), market_id,)); @@ -283,19 +588,24 @@ fn reward_outcome_owner_works_for_one_owner() { OutcomeReport::Scalar(20), OutcomeInfo { outcome_sum: Zero::zero(), - owners: BoundedVec::try_from(vec![ALICE]).unwrap(), + possession: Possession::Shared { + owners: BoundedVec::try_from(vec![ALICE]).unwrap(), + }, }, ); let _ = Balances::deposit_creating( &GlobalDisputes::reward_account(&market_id), 3 * VotingOutcomeFee::get(), ); - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(20), - is_finished: true, - outcome_info: OutcomeInfo { outcome_sum: 10 * BASE, owners: Default::default() }, + let gd_info = GlobalDisputeInfo { + winner_outcome: OutcomeReport::Scalar(20), + status: GdStatus::Finished, + outcome_info: OutcomeInfo { + outcome_sum: 10 * BASE, + possession: Possession::Shared { owners: BoundedVec::try_from(vec![]).unwrap() }, + }, }; - >::insert(market_id, winner_info); + >::insert(market_id, gd_info); assert_ok!(GlobalDisputes::purge_outcomes(RuntimeOrigin::signed(ALICE), market_id,)); @@ -318,76 +628,15 @@ fn reward_outcome_owner_works_for_one_owner() { }); } -#[test] -fn reward_outcome_owner_works_for_no_reward_funds() { - ExtBuilder::default().build().execute_with(|| { - let market_id = 0u128; - - setup_vote_outcomes_with_hundred(&market_id); - - let winner_info = WinnerInfo { - outcome: OutcomeReport::Scalar(20), - is_finished: true, - outcome_info: OutcomeInfo { outcome_sum: 10 * BASE, owners: Default::default() }, - }; - >::insert(market_id, winner_info); - - assert_ok!(GlobalDisputes::purge_outcomes(RuntimeOrigin::signed(ALICE), market_id,)); - - System::assert_last_event(Event::::OutcomesFullyCleaned { market_id }.into()); - - let free_balance_alice_before = Balances::free_balance(ALICE); - - let reward_account_free_balance = - Balances::free_balance(GlobalDisputes::reward_account(&market_id)); - // this case happens, when add_vote_outcome wasn't called - // so no loosers, who provided the VotingOutcomeFee - assert!(reward_account_free_balance.is_zero()); - - assert_ok!(GlobalDisputes::reward_outcome_owner(RuntimeOrigin::signed(ALICE), market_id)); - - System::assert_last_event( - Event::::OutcomeOwnersRewardedWithNoFunds { market_id }.into(), - ); - - assert_eq!(Balances::free_balance(ALICE), free_balance_alice_before); - assert!(Balances::free_balance(GlobalDisputes::reward_account(&market_id)).is_zero()); - assert!(>::iter_prefix(market_id).next().is_none()); - }); -} - #[test] fn vote_fails_if_amount_below_min_outcome_vote_amount() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - 10 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(20), - &ALICE, - 20 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(40), - &ALICE, - 30 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(60), - &ALICE, - 40 * BASE, - ) - .unwrap(); + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); assert_noop!( GlobalDisputes::vote_on_outcome( @@ -405,20 +654,12 @@ fn vote_fails_if_amount_below_min_outcome_vote_amount() { fn vote_fails_for_insufficient_funds() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - 10 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(20), - &ALICE, - 20 * BASE, - ) - .unwrap(); + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + // Paul does not have 50 * BASE assert_noop!( GlobalDisputes::vote_on_outcome( @@ -436,8 +677,13 @@ fn vote_fails_for_insufficient_funds() { fn determine_voting_winner_sets_the_last_outcome_for_same_vote_balances_as_the_canonical_outcome() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); - setup_vote_outcomes_with_hundred(&market_id); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -481,8 +727,13 @@ fn determine_voting_winner_sets_the_last_outcome_for_same_vote_balances_as_the_c fn vote_on_outcome_check_event() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id); + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(EVE), @@ -507,6 +758,8 @@ fn vote_on_outcome_check_event() { fn reserve_before_init_vote_outcome_is_not_allowed_for_voting() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); let disputor = &ALICE; let free_balance_disputor_before = Balances::free_balance(disputor); @@ -519,21 +772,22 @@ fn reserve_before_init_vote_outcome_is_not_allowed_for_voting() { free_balance_disputor_before - reserved_balance_disputor ); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - reserved_balance_disputor, - ) - .unwrap(); + let initial_items = vec![ + InitialItem { + outcome: OutcomeReport::Scalar(0), + owner: ALICE, + amount: reserved_balance_disputor, + }, + InitialItem { + outcome: OutcomeReport::Scalar(20), + owner: ALICE, + amount: reserved_balance_disputor * 2, + }, + ]; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(20), - &ALICE, - reserved_balance_disputor * 2, - ) - .unwrap(); + GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice()).unwrap(); + + set_vote_period(); assert_noop!( GlobalDisputes::vote_on_outcome( @@ -565,8 +819,13 @@ fn reserve_before_init_vote_outcome_is_not_allowed_for_voting() { fn transfer_fails_with_fully_locked_balance() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); - setup_vote_outcomes_with_hundred(&market_id); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + set_vote_period(); let disputor = &ALICE; let free_balance_disputor_before = Balances::free_balance(disputor); @@ -597,8 +856,13 @@ fn transfer_fails_with_fully_locked_balance() { fn reserve_fails_with_fully_locked_balance() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id); + set_vote_period(); let disputor = &ALICE; let free_balance_disputor_before = Balances::free_balance(disputor); @@ -629,8 +893,13 @@ fn reserve_fails_with_fully_locked_balance() { fn determine_voting_winner_works_four_outcome_votes() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); - setup_vote_outcomes_with_hundred(&market_id); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -668,7 +937,10 @@ fn determine_voting_winner_works_four_outcome_votes() { OutcomeReport::Scalar(40) ); - assert!(>::get(market_id).unwrap().is_finished); + assert_eq!( + >::get(market_id).unwrap().status, + GdStatus::Finished + ); }); } @@ -676,8 +948,13 @@ fn determine_voting_winner_works_four_outcome_votes() { fn determine_voting_winner_works_three_outcome_votes() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id); + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -720,8 +997,13 @@ fn determine_voting_winner_works_three_outcome_votes() { fn determine_voting_winner_works_two_outcome_votes() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); - setup_vote_outcomes_with_hundred(&market_id); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -764,8 +1046,13 @@ fn determine_voting_winner_works_two_outcome_votes() { fn determine_voting_winner_works_with_accumulated_votes_for_alice() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id); + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -819,11 +1106,16 @@ fn determine_voting_winner_works_with_accumulated_votes_for_alice() { } #[test] -fn reward_outcome_owner_cleans_outcome_info() { +fn purge_outcomes_fully_cleaned_works() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); - setup_vote_outcomes_with_hundred(&market_id); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -852,9 +1144,102 @@ fn reward_outcome_owner_cleans_outcome_info() { System::assert_last_event(Event::::OutcomesFullyCleaned { market_id }.into()); - assert_ok!(GlobalDisputes::reward_outcome_owner(RuntimeOrigin::signed(BOB), market_id,)); + assert_eq!(>::iter_prefix(market_id).next(), None); + }); +} + +#[test] +fn purge_outcomes_partially_cleaned_works() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, &market); + + let mut initial_items = Vec::new(); + for i in 0..(2 * RemoveKeysLimit::get()) { + initial_items.push(InitialItem { + owner: ALICE, + outcome: OutcomeReport::Scalar(i.into()), + amount: SETUP_AMOUNT, + }); + } + + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + assert!(GlobalDisputes::determine_voting_winner(&market_id).is_some()); + + assert_ok!(GlobalDisputes::purge_outcomes(RuntimeOrigin::signed(ALICE), market_id,)); + + System::assert_last_event(Event::::OutcomesPartiallyCleaned { market_id }.into()); + + assert!(>::iter_prefix(market_id).next().is_some()); + + assert_ok!(GlobalDisputes::purge_outcomes(RuntimeOrigin::signed(ALICE), market_id,)); + + System::assert_last_event(Event::::OutcomesFullyCleaned { market_id }.into()); + + assert_eq!(>::iter_prefix(market_id).next(), None); + }); +} + +#[test] +fn refund_vote_fees_works() { + ExtBuilder::default().build().execute_with(|| { + let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, &market); + + let pushed_outcome_1 = 0; + let pushed_outcome_2 = 20; + + let initial_items = vec![ + InitialItem { + owner: ALICE, + outcome: OutcomeReport::Scalar(pushed_outcome_1), + amount: SETUP_AMOUNT, + }, + InitialItem { + owner: ALICE, + outcome: OutcomeReport::Scalar(pushed_outcome_2), + amount: SETUP_AMOUNT, + }, + ]; + + let offset = pushed_outcome_1.max(pushed_outcome_2) + 1; + + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + let mut overall_fees = >::zero(); + // minus 2 because of the above push_vote_outcome calls + for i in 0..(2 * RemoveKeysLimit::get() - 2) { + assert_ok!(GlobalDisputes::add_vote_outcome( + RuntimeOrigin::signed(ALICE), + market_id, + // offset to not conflict with pushed outcomes + OutcomeReport::Scalar(offset + i as u128), + )); + overall_fees = overall_fees.saturating_add(VotingOutcomeFee::get()); + } + + assert_ok!(GlobalDisputes::destroy_global_dispute(&market_id)); + + let alice_free_balance_before = Balances::free_balance(ALICE); + assert_ok!(GlobalDisputes::refund_vote_fees(RuntimeOrigin::signed(ALICE), market_id,)); + + System::assert_last_event(Event::::OutcomesPartiallyCleaned { market_id }.into()); + + assert!(>::iter_prefix(market_id).next().is_some()); + + assert_ok!(GlobalDisputes::refund_vote_fees(RuntimeOrigin::signed(ALICE), market_id,)); + + System::assert_last_event(Event::::OutcomesFullyCleaned { market_id }.into()); assert_eq!(>::iter_prefix(market_id).next(), None); + + assert_eq!( + Balances::free_balance(ALICE), + alice_free_balance_before.saturating_add(overall_fees) + ); }); } @@ -862,8 +1247,13 @@ fn reward_outcome_owner_cleans_outcome_info() { fn unlock_clears_lock_info() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id); + set_vote_period(); assert_ok!(GlobalDisputes::vote_on_outcome( RuntimeOrigin::signed(ALICE), @@ -886,34 +1276,19 @@ fn unlock_clears_lock_info() { fn vote_fails_if_outcome_does_not_exist() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(0), - &ALICE, - 10 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(20), - &ALICE, - 20 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(40), - &ALICE, - 30 * BASE, - ) - .unwrap(); - GlobalDisputes::push_voting_outcome( - &market_id, - OutcomeReport::Scalar(60), - &ALICE, - 40 * BASE, - ) - .unwrap(); + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = vec![ + InitialItem { owner: ALICE, outcome: OutcomeReport::Scalar(0), amount: 10 * BASE }, + InitialItem { owner: ALICE, outcome: OutcomeReport::Scalar(20), amount: 20 * BASE }, + InitialItem { owner: ALICE, outcome: OutcomeReport::Scalar(40), amount: 30 * BASE }, + InitialItem { owner: ALICE, outcome: OutcomeReport::Scalar(60), amount: 40 * BASE }, + ]; + + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); + + set_vote_period(); assert_noop!( GlobalDisputes::vote_on_outcome( @@ -931,8 +1306,13 @@ fn vote_fails_if_outcome_does_not_exist() { fn locking_works_for_one_market() { ExtBuilder::default().build().execute_with(|| { let market_id = 0u128; + let market = market_mock::(); + Markets::::insert(market_id, market); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id); + set_vote_period(); assert_eq!(>::get(ALICE), vec![]); assert!(Balances::locks(ALICE).is_empty()); @@ -1017,10 +1397,19 @@ fn locking_works_for_one_market() { fn locking_works_for_two_markets_with_stronger_first_unlock() { ExtBuilder::default().build().execute_with(|| { let market_id_1 = 0u128; + let market_1 = market_mock::(); + Markets::::insert(market_id_1, market_1); + let market_id_2 = 1u128; + let market_2 = market_mock::(); + Markets::::insert(market_id_2, market_2); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id_1, initial_items.as_slice())); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id_2, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id_1); - setup_vote_outcomes_with_hundred(&market_id_2); + set_vote_period(); assert_eq!(>::get(ALICE), vec![]); assert!(Balances::locks(ALICE).is_empty()); @@ -1107,10 +1496,19 @@ fn locking_works_for_two_markets_with_stronger_first_unlock() { fn locking_works_for_two_markets_with_weaker_first_unlock() { ExtBuilder::default().build().execute_with(|| { let market_id_1 = 0u128; + let market_1 = market_mock::(); + Markets::::insert(market_id_1, market_1); + let market_id_2 = 1u128; + let market_2 = market_mock::(); + Markets::::insert(market_id_2, market_2); + + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id_1, initial_items.as_slice())); + let initial_items = get_initial_items(); + assert_ok!(GlobalDisputes::start_global_dispute(&market_id_2, initial_items.as_slice())); - setup_vote_outcomes_with_hundred(&market_id_1); - setup_vote_outcomes_with_hundred(&market_id_2); + set_vote_period(); assert_eq!(>::get(ALICE), vec![]); assert!(Balances::locks(ALICE).is_empty()); diff --git a/zrml/global-disputes/src/types.rs b/zrml/global-disputes/src/types.rs index 63b7c42e7..10c83a49b 100644 --- a/zrml/global-disputes/src/types.rs +++ b/zrml/global-disputes/src/types.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Forecasting Technologies LTD. +// Copyright 2022-2023 Forecasting Technologies LTD. // // This file is part of Zeitgeist. // @@ -19,9 +19,85 @@ use frame_support::pallet_prelude::{Decode, Encode, MaxEncodedLen, TypeInfo}; use sp_runtime::traits::Saturating; use zeitgeist_primitives::types::OutcomeReport; +/// The original voting outcome owner information. +#[derive(Debug, TypeInfo, Decode, Encode, MaxEncodedLen, Clone, PartialEq, Eq)] +pub enum Possession { + /// The outcome is owned by a single account. + /// This happens due to the call to `add_vote_outcome`. + Paid { owner: AccountId, fee: Balance }, + /// The outcome is owned by multiple accounts. + /// When a global dispute is triggered, these are the owners of the initially added outcomes. + Shared { owners: OwnerInfo }, +} + +impl Possession { + pub fn get_shared_owners(self) -> Option { + match self { + Possession::Shared { owners } => Some(owners), + _ => None, + } + } +} + /// The information about a voting outcome of a global dispute. #[derive(Debug, TypeInfo, Decode, Encode, MaxEncodedLen, Clone, PartialEq, Eq)] -pub struct OutcomeInfo { +pub struct OutcomeInfo { + /// The current sum of all locks on this outcome. + pub outcome_sum: Balance, + /// The information about the owner(s) and optionally additional fee. + pub possession: Possession, +} + +/// The general information about the global dispute. +#[derive(Debug, TypeInfo, Decode, Encode, MaxEncodedLen, Clone, PartialEq, Eq)] +pub struct GlobalDisputeInfo { + /// The outcome which is in the lead. + pub winner_outcome: OutcomeReport, + /// The information about the winning outcome. + pub outcome_info: OutcomeInfo, + /// The current status of the global dispute. + pub status: GdStatus, +} + +impl + GlobalDisputeInfo +{ + pub fn new( + outcome: OutcomeReport, + possession: Possession, + vote_sum: Balance, + ) -> Self { + let outcome_info = OutcomeInfo { outcome_sum: vote_sum, possession }; + // `add_outcome_end` and `vote_end` gets set in `start_global_dispute` + let status = + GdStatus::Active { add_outcome_end: Default::default(), vote_end: Default::default() }; + GlobalDisputeInfo { winner_outcome: outcome, status, outcome_info } + } + + pub fn update_winner(&mut self, outcome: OutcomeReport, vote_sum: Balance) { + self.winner_outcome = outcome; + self.outcome_info.outcome_sum = vote_sum; + } +} + +/// The current status of the global dispute. +#[derive(TypeInfo, Debug, Decode, Encode, MaxEncodedLen, Clone, PartialEq, Eq)] +pub enum GdStatus { + /// The global dispute is in progress. + /// The block number `add_outcome_end`, when the addition of new outcomes is over. + /// The block number `vote_end`, when the global dispute voting period is over. + Active { add_outcome_end: BlockNumber, vote_end: BlockNumber }, + /// The global dispute is finished. + Finished, + /// The global dispute is destroyed. + Destroyed, +} + +// TODO(#986): to remove after the storage migration + +/// The information about a voting outcome of a global dispute. +#[derive(Debug, TypeInfo, Decode, Encode, MaxEncodedLen, Clone, PartialEq, Eq)] +pub struct OldOutcomeInfo { /// The current sum of all locks on this outcome. pub outcome_sum: Balance, /// The vector of owners of the outcome. @@ -30,18 +106,28 @@ pub struct OutcomeInfo { /// The information about the current highest winning outcome. #[derive(TypeInfo, Decode, Encode, MaxEncodedLen, Clone, PartialEq, Eq)] -pub struct WinnerInfo { +pub struct OldWinnerInfo { /// The outcome, which is in the lead. pub outcome: OutcomeReport, /// The information about the winning outcome. - pub outcome_info: OutcomeInfo, + pub outcome_info: OldOutcomeInfo, /// Check, if the global dispute is finished. pub is_finished: bool, } -impl WinnerInfo { +impl OldWinnerInfo { pub fn new(outcome: OutcomeReport, vote_sum: Balance) -> Self { - let outcome_info = OutcomeInfo { outcome_sum: vote_sum, owners: Default::default() }; - WinnerInfo { outcome, is_finished: false, outcome_info } + let outcome_info = OldOutcomeInfo { outcome_sum: vote_sum, owners: Default::default() }; + OldWinnerInfo { outcome, is_finished: false, outcome_info } } } + +/// An initial vote outcome item with the outcome owner and the initial vote amount. +pub struct InitialItem { + /// The outcome which is added as initial global dispute vote possibility. + pub outcome: OutcomeReport, + /// The owner of the outcome. This account is rewarded in case the outcome is the winning one. + pub owner: AccountId, + /// The vote amount at the start of the global dispute. + pub amount: Balance, +} diff --git a/zrml/global-disputes/src/utils.rs b/zrml/global-disputes/src/utils.rs new file mode 100644 index 000000000..fd0fc7bbb --- /dev/null +++ b/zrml/global-disputes/src/utils.rs @@ -0,0 +1,63 @@ +// Copyright 2023 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![cfg(any(feature = "runtime-benchmarks", test))] + +use crate::*; +use frame_support::traits::Currency; + +type CurrencyOf = + <::MarketCommons as zrml_market_commons::MarketCommonsPalletApi>::Currency; +type BalanceOf = as Currency<::AccountId>>::Balance; +type MarketOf = zeitgeist_primitives::types::Market< + ::AccountId, + BalanceOf, + ::BlockNumber, + MomentOf, + zeitgeist_primitives::types::Asset>, +>; + +pub(crate) fn market_mock() -> MarketOf +where + T: crate::Config, +{ + use frame_support::traits::Get; + use sp_runtime::traits::AccountIdConversion; + use zeitgeist_primitives::types::ScoringRule; + + zeitgeist_primitives::types::Market { + base_asset: zeitgeist_primitives::types::Asset::Ztg, + creation: zeitgeist_primitives::types::MarketCreation::Permissionless, + creator_fee: 0, + creator: T::GlobalDisputesPalletId::get().into_account_truncating(), + market_type: zeitgeist_primitives::types::MarketType::Scalar(0..=u128::MAX), + dispute_mechanism: zeitgeist_primitives::types::MarketDisputeMechanism::SimpleDisputes, + metadata: Default::default(), + oracle: T::GlobalDisputesPalletId::get().into_account_truncating(), + period: zeitgeist_primitives::types::MarketPeriod::Block(Default::default()), + deadlines: zeitgeist_primitives::types::Deadlines { + grace_period: 1_u32.into(), + oracle_duration: 1_u32.into(), + dispute_duration: 1_u32.into(), + }, + report: None, + resolved_outcome: None, + scoring_rule: ScoringRule::CPMM, + status: zeitgeist_primitives::types::MarketStatus::Disputed, + bonds: Default::default(), + } +} diff --git a/zrml/global-disputes/src/weights.rs b/zrml/global-disputes/src/weights.rs index 50fea5dce..d5fbdcaa2 100644 --- a/zrml/global-disputes/src/weights.rs +++ b/zrml/global-disputes/src/weights.rs @@ -23,7 +23,7 @@ //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// ./target/production/zeitgeist +// ./target/release/zeitgeist // benchmark // pallet // --chain=dev @@ -34,8 +34,8 @@ // --execution=wasm // --wasm-execution=compiled // --heap-pages=4096 -// --template=./misc/weight_template.hbs // --output=./zrml/global-disputes/src/weights.rs +// --template=./misc/weight_template.hbs #![allow(unused_parens)] #![allow(unused_imports)] @@ -50,15 +50,16 @@ pub trait WeightInfoZeitgeist { fn unlock_vote_balance_set(l: u32, o: u32) -> Weight; fn unlock_vote_balance_remove(l: u32, o: u32) -> Weight; fn add_vote_outcome(w: u32) -> Weight; - fn reward_outcome_owner_with_funds(o: u32) -> Weight; - fn reward_outcome_owner_no_funds(o: u32) -> Weight; + fn reward_outcome_owner_shared_possession(o: u32) -> Weight; + fn reward_outcome_owner_paid_possession() -> Weight; fn purge_outcomes(k: u32, o: u32) -> Weight; + fn refund_vote_fees(k: u32, o: u32) -> Weight; } /// Weight functions for zrml_global_disputes (automatically generated) pub struct WeightInfo(PhantomData); impl WeightInfoZeitgeist for WeightInfo { - // Storage: GlobalDisputes Winners (r:1 w:1) + // Storage: GlobalDisputes GlobalDisputesInfo (r:1 w:1) // Storage: GlobalDisputes Outcomes (r:1 w:1) // Storage: GlobalDisputes Locks (r:1 w:1) // Storage: Balances Locks (r:1 w:1) @@ -72,7 +73,7 @@ impl WeightInfoZeitgeist for WeightInfo { // Storage: GlobalDisputes Locks (r:1 w:1) // Storage: Balances Locks (r:1 w:1) // Storage: System Account (r:1 w:0) - // Storage: GlobalDisputes Winners (r:5 w:0) + // Storage: GlobalDisputes GlobalDisputesInfo (r:5 w:0) fn unlock_vote_balance_set(l: u32, o: u32) -> Weight { Weight::from_ref_time(54_445_996) // Standard Error: 8_942 @@ -86,7 +87,7 @@ impl WeightInfoZeitgeist for WeightInfo { // Storage: GlobalDisputes Locks (r:1 w:1) // Storage: Balances Locks (r:1 w:1) // Storage: System Account (r:1 w:0) - // Storage: GlobalDisputes Winners (r:5 w:0) + // Storage: GlobalDisputes GlobalDisputesInfo (r:5 w:0) fn unlock_vote_balance_remove(l: u32, o: u32) -> Weight { Weight::from_ref_time(61_165_913) // Standard Error: 9_374 @@ -97,7 +98,8 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(l.into()))) .saturating_add(T::DbWeight::get().writes(2)) } - // Storage: GlobalDisputes Winners (r:1 w:1) + // Storage: MarketCommons Markets (r:1 w:0) + // Storage: GlobalDisputes GlobalDisputesInfo (r:1 w:1) // Storage: GlobalDisputes Outcomes (r:1 w:1) // Storage: System Account (r:1 w:1) fn add_vote_outcome(_w: u32) -> Weight { @@ -106,24 +108,26 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().writes(3)) } // Storage: GlobalDisputes Outcomes (r:1 w:0) - // Storage: GlobalDisputes Winners (r:1 w:0) + // Storage: GlobalDisputes GlobalDisputesInfo (r:1 w:0) // Storage: System Account (r:2 w:2) - fn reward_outcome_owner_with_funds(o: u32) -> Weight { - Weight::from_ref_time(86_274_827) - // Standard Error: 107_080 - .saturating_add(Weight::from_ref_time(31_467_061).saturating_mul(o.into())) + fn reward_outcome_owner_shared_possession(o: u32) -> Weight { + Weight::from_ref_time(36_741_000) + // Standard Error: 20_000 + .saturating_add(Weight::from_ref_time(22_017_000).saturating_mul(o.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(o.into()))) .saturating_add(T::DbWeight::get().writes(1)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(o.into()))) } // Storage: GlobalDisputes Outcomes (r:1 w:0) - // Storage: GlobalDisputes Winners (r:1 w:0) - // Storage: System Account (r:1 w:0) - fn reward_outcome_owner_no_funds(_o: u32) -> Weight { - Weight::from_ref_time(54_754_528).saturating_add(T::DbWeight::get().reads(3)) + // Storage: GlobalDisputes GlobalDisputesInfo (r:1 w:0) + // Storage: System Account (r:2 w:2) + fn reward_outcome_owner_paid_possession() -> Weight { + Weight::from_ref_time(56_000_000) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) } - // Storage: GlobalDisputes Winners (r:1 w:1) + // Storage: GlobalDisputes GlobalDisputesInfo (r:1 w:1) // Storage: GlobalDisputes Outcomes (r:3 w:2) fn purge_outcomes(k: u32, _o: u32) -> Weight { Weight::from_ref_time(168_932_238) @@ -134,4 +138,15 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().writes(2)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(k.into()))) } + // Storage: GlobalDisputes GlobalDisputesInfo (r:1 w:0) + // Storage: GlobalDisputes Outcomes (r:3 w:2) + fn refund_vote_fees(k: u32, _o: u32) -> Weight { + Weight::from_ref_time(31_076_000) + // Standard Error: 4_000 + .saturating_add(Weight::from_ref_time(13_543_000).saturating_mul(k.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(k.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(k.into()))) + } } diff --git a/zrml/market-commons/src/lib.rs b/zrml/market-commons/src/lib.rs index a5894c1e7..29aeec292 100644 --- a/zrml/market-commons/src/lib.rs +++ b/zrml/market-commons/src/lib.rs @@ -51,7 +51,7 @@ mod pallet { use zeitgeist_primitives::types::{Asset, Market, PoolId}; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(6); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(7); type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; diff --git a/zrml/market-commons/src/tests.rs b/zrml/market-commons/src/tests.rs index 2d027bb95..61c5feb5e 100644 --- a/zrml/market-commons/src/tests.rs +++ b/zrml/market-commons/src/tests.rs @@ -48,7 +48,7 @@ const MARKET_DUMMY: Market"] diff --git a/zrml/prediction-markets/fuzz/pm_full_workflow.rs b/zrml/prediction-markets/fuzz/pm_full_workflow.rs index edddb77ac..9f4b04df5 100644 --- a/zrml/prediction-markets/fuzz/pm_full_workflow.rs +++ b/zrml/prediction-markets/fuzz/pm_full_workflow.rs @@ -76,7 +76,6 @@ fuzz_target!(|data: Data| { let _ = PredictionMarkets::dispute( RuntimeOrigin::signed(data.report_origin.into()), dispute_market_id, - outcome(data.report_outcome), ); let _ = PredictionMarkets::on_initialize(5); diff --git a/zrml/prediction-markets/src/benchmarks.rs b/zrml/prediction-markets/src/benchmarks.rs index ff1923bb1..db2284574 100644 --- a/zrml/prediction-markets/src/benchmarks.rs +++ b/zrml/prediction-markets/src/benchmarks.rs @@ -37,7 +37,7 @@ use orml_traits::MultiCurrency; use sp_runtime::traits::{One, SaturatedConversion, Saturating, Zero}; use zeitgeist_primitives::{ constants::mock::{MaxSwapFee, MinWeight, BASE, MILLISECS_PER_BLOCK}, - traits::Swaps, + traits::{DisputeApi, Swaps}, types::{ Asset, Deadlines, MarketCreation, MarketDisputeMechanism, MarketPeriod, MarketStatus, MarketType, MaxRuntimeUsize, MultiHash, OutcomeReport, PoolStatus, ScoringRule, @@ -45,6 +45,7 @@ use zeitgeist_primitives::{ }, }; use zrml_authorized::Pallet as AuthorizedPallet; +use zrml_global_disputes::GlobalDisputesPalletApi; use zrml_market_commons::MarketCommonsPalletApi; use frame_support::{traits::Hooks, BoundedVec}; @@ -221,16 +222,14 @@ fn setup_reported_categorical_market_with_pool::MarketCommons as MarketCommonsPalletApi>::MarketId: From<::MarketId>, } - admin_destroy_disputed_market{ + admin_destroy_disputed_market { // The number of assets. let a in (T::MinCategories::get().into())..T::MaxCategories::get().into(); - // The number of disputes. - let d in 1..T::MaxDisputes::get(); // The number of market ids per open time frame. let o in 0..63; // The number of market ids per close time frame. @@ -243,15 +242,20 @@ benchmarks! { OutcomeReport::Categorical(0u16), )?; + >::mutate_market(&market_id, |market| { + market.dispute_mechanism = MarketDisputeMechanism::Authorized; + Ok(()) + })?; + let pool_id = >::market_pool(&market_id)?; - for i in 1..=d { - let outcome = OutcomeReport::Categorical((i % a).saturated_into()); - let disputor = account("disputor", i, 0); - let dispute_bond = crate::pallet::default_dispute_bond::(i as usize); - T::AssetManager::deposit(Asset::Ztg, &disputor, dispute_bond)?; - let _ = Pallet::::dispute(RawOrigin::Signed(disputor).into(), market_id, outcome)?; - } + let disputor = account("disputor", 1, 0); + ::AssetManager::deposit( + Asset::Ztg, + &disputor, + u128::MAX.saturated_into(), + ).unwrap(); + let _ = Pallet::::dispute(RawOrigin::Signed(disputor).into(), market_id)?; let market = >::market(&market_id)?; @@ -261,26 +265,34 @@ benchmarks! { }; for i in 0..o { + // shift of 1 to avoid collisions with first market id 0 MarketIdsPerOpenTimeFrame::::try_mutate( Pallet::::calculate_time_frame_of_moment(range_start), - |ids| ids.try_push(i.into()), + |ids| ids.try_push((i + 1).into()), ).unwrap(); } for i in 0..c { + // shift of 65 to avoid collisions with `o` MarketIdsPerCloseTimeFrame::::try_mutate( Pallet::::calculate_time_frame_of_moment(range_end), - |ids| ids.try_push(i.into()), + |ids| ids.try_push((i + 65).into()), ).unwrap(); } - let disputes = Disputes::::get(market_id); - let last_dispute = disputes.last().unwrap(); - let resolves_at = last_dispute.at.saturating_add(market.deadlines.dispute_duration); + AuthorizedPallet::::authorize_market_outcome( + T::AuthorizedDisputeResolutionOrigin::try_successful_origin().unwrap(), + market_id.into(), + OutcomeReport::Categorical(0u16), + )?; + + let now = >::block_number(); + let resolves_at = now.saturating_add(::CorrectionPeriod::get()); for i in 0..r { + // shift of 129 to avoid collisions with `o` and `c` MarketIdsPerDisputeBlock::::try_mutate( resolves_at, - |ids| ids.try_push(i.into()), + |ids| ids.try_push((i + 129).into()), ).unwrap(); } @@ -317,25 +329,28 @@ benchmarks! { }; for i in 0..o { + // shift of 1 to avoid collisions with first market id 0 MarketIdsPerOpenTimeFrame::::try_mutate( Pallet::::calculate_time_frame_of_moment(range_start), - |ids| ids.try_push(i.into()), + |ids| ids.try_push((i + 1).into()), ).unwrap(); } for i in 0..c { + // shift of 65 to avoid collisions with `o` MarketIdsPerCloseTimeFrame::::try_mutate( Pallet::::calculate_time_frame_of_moment(range_end), - |ids| ids.try_push(i.into()), + |ids| ids.try_push((i + 65).into()), ).unwrap(); } let report_at = market.report.unwrap().at; let resolves_at = report_at.saturating_add(market.deadlines.dispute_duration); for i in 0..r { + // shift of 129 to avoid collisions with `o` and `c` MarketIdsPerReportBlock::::try_mutate( resolves_at, - |ids| ids.try_push(i.into()), + |ids| ids.try_push((i + 129).into()), ).unwrap(); } @@ -466,24 +481,21 @@ benchmarks! { let outcome = OutcomeReport::Scalar(0); let disputor = account("disputor", 1, 0); - let dispute_bond = crate::pallet::default_dispute_bond::(0_usize); - T::AssetManager::deposit( + ::AssetManager::deposit( Asset::Ztg, &disputor, - dispute_bond, - )?; - Pallet::::dispute(RawOrigin::Signed(disputor).into(), market_id, outcome)?; - let disputes = Disputes::::get(market_id); - // Authorize the outcome with the highest number of correct reporters to maximize the - // number of transfers required (0 has (d+1)//2 reports, 1 has d//2 reports). + u128::MAX.saturated_into(), + ).unwrap(); + Pallet::::dispute(RawOrigin::Signed(disputor).into(), market_id)?; + + let now = >::block_number(); AuthorizedPallet::::authorize_market_outcome( T::AuthorizedDisputeResolutionOrigin::try_successful_origin().unwrap(), market_id.into(), OutcomeReport::Scalar(0), )?; - let last_dispute = disputes.last().unwrap(); - let resolves_at = last_dispute.at.saturating_add(market.deadlines.dispute_duration); + let resolves_at = now.saturating_add(::CorrectionPeriod::get()); for i in 0..r { MarketIdsPerDisputeBlock::::try_mutate( resolves_at, @@ -518,17 +530,14 @@ benchmarks! { Ok(()) })?; - let outcome = OutcomeReport::Categorical(0u16); let disputor = account("disputor", 1, 0); - let dispute_bond = crate::pallet::default_dispute_bond::(0_usize); - T::AssetManager::deposit( + ::AssetManager::deposit( Asset::Ztg, &disputor, - dispute_bond, - )?; - Pallet::::dispute(RawOrigin::Signed(disputor).into(), market_id, outcome)?; + u128::MAX.saturated_into(), + ).unwrap(); + Pallet::::dispute(RawOrigin::Signed(disputor).into(), market_id)?; - let disputes = Disputes::::get(market_id); // Authorize the outcome with the highest number of correct reporters to maximize the // number of transfers required (0 has (d+1)//2 reports, 1 has d//2 reports). AuthorizedPallet::::authorize_market_outcome( @@ -537,9 +546,9 @@ benchmarks! { OutcomeReport::Categorical(0), )?; - let last_dispute = disputes.last().unwrap(); let market = >::market(&market_id)?; - let resolves_at = last_dispute.at.saturating_add(market.deadlines.dispute_duration); + let now = >::block_number(); + let resolves_at = now.saturating_add(::CorrectionPeriod::get()); for i in 0..r { MarketIdsPerDisputeBlock::::try_mutate( resolves_at, @@ -790,7 +799,7 @@ benchmarks! { )?; >::mutate_market(&market_id, |market| { - market.dispute_mechanism = MarketDisputeMechanism::SimpleDisputes; + market.dispute_mechanism = MarketDisputeMechanism::Court; Ok(()) })?; @@ -801,49 +810,58 @@ benchmarks! { market_ids_1.try_push(i.saturated_into()).unwrap(); } - let max_dispute_len = T::MaxDisputes::get(); - for i in 0..max_dispute_len { - // ensure that the MarketIdsPerDisputeBlock does not interfere - // with the start_global_dispute execution block - >::set_block_number(i.saturated_into()); - let disputor: T::AccountId = account("Disputor", i, 0); - T::AssetManager::deposit(Asset::Ztg, &disputor, (u128::MAX).saturated_into())?; - let _ = Call::::dispute { - market_id, - outcome: OutcomeReport::Scalar(i.into()), - } - .dispatch_bypass_filter(RawOrigin::Signed(disputor.clone()).into())?; + >::on_initialize(1u32.into()); + >::set_block_number(1u32.into()); + + let min_amount = ::MinJurorStake::get(); + for i in 0..>::necessary_draws_weight(0usize) { + let juror: T::AccountId = account("Jurori", i.try_into().unwrap(), 0); + ::AssetManager::deposit( + Asset::Ztg, + &juror, + (u128::MAX / 2).saturated_into(), + ).unwrap(); + >::join_court( + RawOrigin::Signed(juror.clone()).into(), + min_amount + i.saturated_into(), + )?; } + let disputor: T::AccountId = account("Disputor", 1, 0); + ::AssetManager::deposit( + Asset::Ztg, + &disputor, + u128::MAX.saturated_into(), + ).unwrap(); + let _ = Call::::dispute { + market_id, + } + .dispatch_bypass_filter(RawOrigin::Signed(disputor).into())?; + let market = >::market(&market_id.saturated_into()).unwrap(); - let disputes = Disputes::::get(market_id); - let last_dispute = disputes.last().unwrap(); - let dispute_duration_ends_at_block = last_dispute.at + market.deadlines.dispute_duration; + let appeal_end = T::Court::get_auto_resolve(&market_id, &market).result.unwrap(); let mut market_ids_2: BoundedVec, CacheSize> = BoundedVec::try_from( vec![market_id], ).unwrap(); for i in 1..n { market_ids_2.try_push(i.saturated_into()).unwrap(); } - MarketIdsPerDisputeBlock::::insert(dispute_duration_ends_at_block, market_ids_2); + MarketIdsPerDisputeBlock::::insert(appeal_end, market_ids_2); - let current_block: T::BlockNumber = (max_dispute_len + 1).saturated_into(); - >::set_block_number(current_block); + >::set_block_number(appeal_end - 1u64.saturated_into::()); - #[cfg(feature = "with-global-disputes")] - { - let global_dispute_end = current_block + T::GlobalDisputePeriod::get(); - // the complexity depends on MarketIdsPerDisputeBlock at the current block - // this is because a variable number of market ids need to be decoded from the storage - MarketIdsPerDisputeBlock::::insert(global_dispute_end, market_ids_1); - } + let now = >::block_number(); + + let add_outcome_end = now + + ::GlobalDisputes::get_add_outcome_period(); + let vote_end = add_outcome_end + ::GlobalDisputes::get_vote_period(); + // the complexity depends on MarketIdsPerDisputeBlock at the current block + // this is because a variable number of market ids need to be decoded from the storage + MarketIdsPerDisputeBlock::::insert(vote_end, market_ids_1); let call = Call::::start_global_dispute { market_id }; }: { - #[cfg(feature = "with-global-disputes")] call.dispatch_bypass_filter(RawOrigin::Signed(caller).into())?; - #[cfg(not(feature = "with-global-disputes"))] - let _ = call.dispatch_bypass_filter(RawOrigin::Signed(caller).into()); } dispute_authorized { @@ -861,9 +879,7 @@ benchmarks! { let market = >::market(&market_id)?; - // only one dispute allowed for authorized mdm - let dispute_outcome = OutcomeReport::Scalar(1u128); - let call = Call::::dispute { market_id, outcome: dispute_outcome }; + let call = Call::::dispute { market_id }; }: { call.dispatch_bypass_filter(RawOrigin::Signed(caller).into())?; } @@ -911,10 +927,8 @@ benchmarks! { Pallet::::dispute( RawOrigin::Signed(caller).into(), market_id, - OutcomeReport::Categorical(0), )?; - // Authorize the outcome with the highest number of correct reporters to maximize the - // number of transfers required (0 has (d+1)//2 reports, 1 has d//2 reports). + AuthorizedPallet::::authorize_market_outcome( T::AuthorizedDisputeResolutionOrigin::try_successful_origin().unwrap(), market_id.into(), @@ -956,10 +970,8 @@ benchmarks! { Pallet::::dispute( RawOrigin::Signed(caller).into(), market_id, - OutcomeReport::Scalar(1) )?; - // Authorize the outcome with the highest number of correct reporters to maximize the - // number of transfers required (0 has (d+1)//2 reports, 1 has d//2 reports). + AuthorizedPallet::::authorize_market_outcome( T::AuthorizedDisputeResolutionOrigin::try_successful_origin().unwrap(), market_id.into(), diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index 665cb4a78..63ca4512e 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -55,21 +55,22 @@ mod pallet { use orml_traits::{MultiCurrency, NamedMultiReservableCurrency}; use sp_arithmetic::per_things::{Perbill, Percent}; use sp_runtime::{ - traits::{CheckedDiv, Saturating, Zero}, + traits::{Saturating, Zero}, DispatchError, DispatchResult, SaturatedConversion, }; use zeitgeist_primitives::{ constants::MILLISECS_PER_BLOCK, - traits::{DisputeApi, DisputeResolutionApi, Swaps, ZeitgeistAssetManager}, + traits::{ + DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi, Swaps, ZeitgeistAssetManager, + }, types::{ - Asset, Bond, Deadlines, Market, MarketBonds, MarketCreation, MarketDispute, + Asset, Bond, Deadlines, GlobalDisputeItem, Market, MarketBonds, MarketCreation, MarketDisputeMechanism, MarketPeriod, MarketStatus, MarketType, MultiHash, - OutcomeReport, Report, ScalarPosition, ScoringRule, SubsidyUntil, + OldMarketDispute, OutcomeReport, Report, ResultWithWeightInfo, ScalarPosition, + ScoringRule, SubsidyUntil, }, }; - #[cfg(feature = "with-global-disputes")] - use zrml_global_disputes::GlobalDisputesPalletApi; - + use zrml_global_disputes::{types::InitialItem, GlobalDisputesPalletApi}; use zrml_liquidity_mining::LiquidityMiningPalletApi; use zrml_market_commons::MarketCommonsPalletApi; @@ -96,6 +97,7 @@ mod pallet { pub type CacheSize = ConstU32<64>; pub type EditReason = BoundedVec::MaxEditReasonLen>; pub type RejectReason = BoundedVec::MaxRejectReasonLen>; + type InitialItemOf = InitialItem<::AccountId, BalanceOf>; macro_rules! impl_unreserve_bond { ($fn_name:ident, $bond_type:ident) => { @@ -313,7 +315,6 @@ mod pallet { ) .max(T::WeightInfo::admin_destroy_disputed_market( T::MaxCategories::get().into(), - T::MaxDisputes::get(), CacheSize::get(), CacheSize::get(), CacheSize::get(), @@ -342,6 +343,10 @@ mod pallet { MarketIdsForEdit::::remove(market_id); } + if T::GlobalDisputes::is_active(&market_id) { + T::GlobalDisputes::destroy_global_dispute(&market_id)?; + } + // NOTE: Currently we don't clean up outcome assets. // TODO(#792): Remove outcome assets for accounts! Delete "resolved" assets of `orml_tokens` with storage migration. T::AssetManager::slash( @@ -359,21 +364,9 @@ mod pallet { let open_ids_len = Self::clear_auto_open(&market_id)?; let close_ids_len = Self::clear_auto_close(&market_id)?; - let (ids_len, disputes_len) = Self::clear_auto_resolve(&market_id)?; - // `Disputes` is emtpy unless the market is disputed, so this is just a defensive - // check. - if market.status == MarketStatus::Disputed { - for (index, dispute) in Disputes::::take(market_id).iter().enumerate() { - T::AssetManager::unreserve_named( - &Self::reserve_id(), - Asset::Ztg, - &dispute.by, - default_dispute_bond::(index), - ); - } - } + let (ids_len, _) = Self::clear_auto_resolve(&market_id)?; + Self::clear_dispute_mechanism(&market_id)?; >::remove_market(&market_id)?; - Disputes::::remove(market_id); Self::deposit_event(Event::MarketDestroyed(market_id)); @@ -394,7 +387,6 @@ mod pallet { Ok(( Some(T::WeightInfo::admin_destroy_disputed_market( category_count, - disputes_len, open_ids_len, close_ids_len, ids_len, @@ -632,53 +624,53 @@ mod pallet { /// /// Complexity: `O(n)`, where `n` is the number of outstanding disputes. #[pallet::call_index(6)] - #[pallet::weight(T::WeightInfo::dispute_authorized())] + #[pallet::weight( + T::WeightInfo::dispute_authorized().saturating_add( + T::Court::on_dispute_max_weight().saturating_add( + T::SimpleDisputes::on_dispute_max_weight() + ) + ) + )] #[transactional] pub fn dispute( origin: OriginFor, #[pallet::compact] market_id: MarketIdOf, - outcome: OutcomeReport, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - let disputes = Disputes::::get(market_id); - let curr_block_num = >::block_number(); + let market = >::market(&market_id)?; - ensure!( - matches!(market.status, MarketStatus::Reported | MarketStatus::Disputed), - Error::::InvalidMarketStatus - ); - let num_disputes: u32 = disputes.len().saturated_into(); - Self::validate_dispute(&disputes, &market, num_disputes, &outcome)?; - T::AssetManager::reserve_named( - &Self::reserve_id(), - Asset::Ztg, - &who, - default_dispute_bond::(disputes.len()), - )?; - // TODO(#782): use multiple benchmarks paths for different dispute mechanisms - match market.dispute_mechanism { + ensure!(market.status == MarketStatus::Reported, Error::::InvalidMarketStatus); + + let weight = match market.dispute_mechanism { MarketDisputeMechanism::Authorized => { - T::Authorized::on_dispute(&disputes, &market_id, &market)? + T::Authorized::on_dispute(&market_id, &market)?; + T::WeightInfo::dispute_authorized() } MarketDisputeMechanism::Court => { - T::Court::on_dispute(&disputes, &market_id, &market)? + let court_weight = T::Court::on_dispute(&market_id, &market)?.weight; + T::WeightInfo::dispute_authorized() + .saturating_sub(T::Authorized::on_dispute_max_weight()) + .saturating_add(court_weight) } MarketDisputeMechanism::SimpleDisputes => { - T::SimpleDisputes::on_dispute(&disputes, &market_id, &market)? + let sd_weight = T::SimpleDisputes::on_dispute(&market_id, &market)?.weight; + T::WeightInfo::dispute_authorized() + .saturating_sub(T::Authorized::on_dispute_max_weight()) + .saturating_add(sd_weight) } - } + }; - Self::set_market_as_disputed(&market, &market_id)?; - let market_dispute = MarketDispute { at: curr_block_num, by: who, outcome }; - >::try_mutate(market_id, |disputes| { - disputes.try_push(market_dispute.clone()).map_err(|_| >::StorageOverflow) + let dispute_bond = T::DisputeBond::get(); + T::AssetManager::reserve_named(&Self::reserve_id(), Asset::Ztg, &who, dispute_bond)?; + + >::mutate_market(&market_id, |m| { + m.status = MarketStatus::Disputed; + m.bonds.dispute = Some(Bond::new(who.clone(), dispute_bond)); + Ok(()) })?; - Self::deposit_event(Event::MarketDisputed( - market_id, - MarketStatus::Disputed, - market_dispute, - )); - Ok((Some(T::WeightInfo::dispute_authorized())).into()) + + Self::deposit_event(Event::MarketDisputed(market_id, MarketStatus::Disputed)); + Ok((Some(weight)).into()) } /// Create a permissionless market, buy complete sets and deploy a pool with specified @@ -1439,15 +1431,14 @@ mod pallet { Ok(Some(T::WeightInfo::sell_complete_set(assets_len)).into()) } - /// When the `MaxDisputes` amount of disputes is reached, - /// this allows to start a global dispute. + /// Start a global dispute, if the market dispute mechanism fails. /// /// # Arguments /// /// * `market_id`: The identifier of the market. /// /// NOTE: - /// The outcomes of the disputes and the report outcome + /// The returned outcomes of the market dispute mechanism and the report outcome /// are added to the global dispute voting outcomes. /// The bond of each dispute is the initial vote amount. #[pallet::call_index(16)] @@ -1455,77 +1446,91 @@ mod pallet { #[transactional] pub fn start_global_dispute( origin: OriginFor, - #[allow(dead_code, unused)] - #[pallet::compact] - market_id: MarketIdOf, + #[pallet::compact] market_id: MarketIdOf, ) -> DispatchResultWithPostInfo { ensure_signed(origin)?; - #[cfg(feature = "with-global-disputes")] - { - let market = >::market(&market_id)?; - ensure!(market.status == MarketStatus::Disputed, Error::::InvalidMarketStatus); + let market = >::market(&market_id)?; + ensure!( + matches!(market.status, MarketStatus::Disputed | MarketStatus::Reported), + Error::::InvalidMarketStatus + ); - ensure!( - market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, - Error::::InvalidDisputeMechanism - ); + ensure!( + matches!(market.dispute_mechanism, MarketDisputeMechanism::Court), + Error::::InvalidDisputeMechanism + ); - let disputes = >::get(market_id); - ensure!( - disputes.len() == T::MaxDisputes::get() as usize, - Error::::MaxDisputesNeeded - ); + ensure!( + !T::GlobalDisputes::does_exist(&market_id), + Error::::GlobalDisputeExistsAlready + ); - ensure!( - T::GlobalDisputes::is_not_started(&market_id), - Error::::GlobalDisputeAlreadyStarted - ); + let report = market.report.as_ref().ok_or(Error::::MarketIsNotReported)?; - // add report outcome to voting choices - if let Some(report) = &market.report { - T::GlobalDisputes::push_voting_outcome( - &market_id, - report.outcome.clone(), - &report.by, - >::zero(), - )?; + let res_0 = match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => { + T::Authorized::has_failed(&market_id, &market)? + } + MarketDisputeMechanism::Court => T::Court::has_failed(&market_id, &market)?, + MarketDisputeMechanism::SimpleDisputes => { + T::SimpleDisputes::has_failed(&market_id, &market)? } + }; + let has_failed = res_0.result; + ensure!(has_failed, Error::::MarketDisputeMechanismNotFailed); - for (index, MarketDispute { at: _, by, outcome }) in disputes.iter().enumerate() { - let dispute_bond = default_dispute_bond::(index); - T::GlobalDisputes::push_voting_outcome( - &market_id, - outcome.clone(), - by, - dispute_bond, - )?; + let res_1 = match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => { + T::Authorized::on_global_dispute(&market_id, &market)? + } + MarketDisputeMechanism::Court => T::Court::on_global_dispute(&market_id, &market)?, + MarketDisputeMechanism::SimpleDisputes => { + T::SimpleDisputes::on_global_dispute(&market_id, &market)? } + }; - // TODO(#372): Allow court with global disputes. - // ensure, that global disputes controls the resolution now - // it does not end after the dispute period now, but after the global dispute end + let mut initial_items: Vec> = Vec::new(); - // ignore first of tuple because we always have max disputes - let (_, ids_len_2) = Self::clear_auto_resolve(&market_id)?; + initial_items.push(InitialItemOf:: { + outcome: report.outcome.clone(), + owner: report.by.clone(), + amount: >::zero(), + }); - let now = >::block_number(); - let global_dispute_end = now.saturating_add(T::GlobalDisputePeriod::get()); - let market_ids_len = >::try_mutate( - global_dispute_end, - |ids| -> Result { - ids.try_push(market_id).map_err(|_| >::StorageOverflow)?; - Ok(ids.len() as u32) - }, - )?; + let gd_items = res_1.result; + + // push vote outcomes other than the report outcome + for GlobalDisputeItem { outcome, owner, initial_vote_amount } in gd_items { + initial_items.push(InitialItemOf:: { + outcome, + owner, + amount: initial_vote_amount, + }); + } + + // ensure, that global disputes controls the resolution now + // it does not end after the dispute period now, but after the global dispute end - Self::deposit_event(Event::GlobalDisputeStarted(market_id)); + // ignore first of tuple because we always have max disputes + let (_, ids_len_2) = Self::clear_auto_resolve(&market_id)?; - Ok(Some(T::WeightInfo::start_global_dispute(market_ids_len, ids_len_2)).into()) + if market.status == MarketStatus::Reported { + // this is the case that a dispute can not be initiated, + // because court has not enough juror and delegator stake (dispute errors) + >::mutate_market(&market_id, |m| { + m.status = MarketStatus::Disputed; + Ok(()) + })?; } - #[cfg(not(feature = "with-global-disputes"))] - Err(Error::::GlobalDisputesDisabled.into()) + // global disputes uses DisputeResolution API to control its resolution + let ids_len_1 = + T::GlobalDisputes::start_global_dispute(&market_id, initial_items.as_slice())?; + + Self::deposit_event(Event::GlobalDisputeStarted(market_id)); + + Ok(Some(T::WeightInfo::start_global_dispute(ids_len_1, ids_len_2)).into()) } } @@ -1562,6 +1567,7 @@ mod pallet { type Authorized: zrml_authorized::AuthorizedPalletApi< AccountId = Self::AccountId, Balance = BalanceOf, + NegativeImbalance = NegativeImbalanceOf, BlockNumber = Self::BlockNumber, MarketId = MarketIdOf, Moment = MomentOf, @@ -1575,6 +1581,7 @@ mod pallet { type Court: zrml_court::CourtPalletApi< AccountId = Self::AccountId, Balance = BalanceOf, + NegativeImbalance = NegativeImbalanceOf, BlockNumber = Self::BlockNumber, MarketId = MarketIdOf, Moment = MomentOf, @@ -1588,21 +1595,16 @@ mod pallet { #[pallet::constant] type DisputeBond: Get>; - /// The additional amount of currency that must be bonded when creating a subsequent - /// dispute. - #[pallet::constant] - type DisputeFactor: Get>; - /// Event type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// See [`GlobalDisputesPalletApi`]. - #[cfg(feature = "with-global-disputes")] - type GlobalDisputes: GlobalDisputesPalletApi, Self::AccountId, BalanceOf>; - - /// The number of blocks the global dispute period remains open. - #[cfg(feature = "with-global-disputes")] - type GlobalDisputePeriod: Get; + type GlobalDisputes: GlobalDisputesPalletApi< + MarketIdOf, + Self::AccountId, + BalanceOf, + Self::BlockNumber, + >; type LiquidityMining: LiquidityMiningPalletApi< AccountId = Self::AccountId, @@ -1690,9 +1692,10 @@ mod pallet { type ResolveOrigin: EnsureOrigin; /// See [`DisputeApi`]. - type SimpleDisputes: DisputeApi< + type SimpleDisputes: zrml_simple_disputes::SimpleDisputesPalletApi< AccountId = Self::AccountId, Balance = BalanceOf, + NegativeImbalance = NegativeImbalanceOf, BlockNumber = Self::BlockNumber, MarketId = MarketIdOf, Moment = MomentOf, @@ -1723,8 +1726,6 @@ mod pallet { EditorNotCreator, /// EditReason's length greater than MaxEditReasonLen. EditReasonLengthExceedsMaxEditReasonLen, - /// The global dispute resolution system is disabled. - GlobalDisputesDisabled, /// Market account does not have enough funds to pay out. InsufficientFundsInMarketAccount, /// Sender does not have enough share balance. @@ -1763,10 +1764,8 @@ mod pallet { MarketStartTooSoon, /// The point in time when the market becomes active is too late. MarketStartTooLate, - /// The maximum number of disputes has been reached. - MaxDisputesReached, - /// The maximum number of disputes is needed for this operation. - MaxDisputesNeeded, + /// The market dispute mechanism has not failed. + MarketDisputeMechanismNotFailed, /// Tried to settle missing bond. MissingBond, /// The number of categories for a categorical market is too low. @@ -1809,12 +1808,12 @@ mod pallet { OracleDurationGreaterThanMaxOracleDuration, /// The weights length has to be equal to the assets length. WeightsLenMustEqualAssetsLen, - /// The start of the global dispute for this market happened already. - GlobalDisputeAlreadyStarted, /// Provided base_asset is not allowed to be used as base_asset. InvalidBaseAsset, /// A foreign asset in not registered in AssetRegistry. UnregisteredForeignAsset, + /// The start of the global dispute for this market happened already. + GlobalDisputeExistsAlready, } #[pallet::event] @@ -1840,8 +1839,8 @@ mod pallet { MarketInsufficientSubsidy(MarketIdOf, MarketStatus), /// A market has been closed. \[market_id\] MarketClosed(MarketIdOf), - /// A market has been disputed. \[market_id, new_market_status, new_outcome\] - MarketDisputed(MarketIdOf, MarketStatus, MarketDispute), + /// A market has been disputed \[market_id, new_market_status\] + MarketDisputed(MarketIdOf, MarketStatus), /// An advised market has ended before it was approved or rejected. \[market_id\] MarketExpired(MarketIdOf), /// A pending market has been rejected as invalid with a reason. @@ -1987,6 +1986,7 @@ mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(PhantomData); + // TODO(#986) after storage migration of release-dispute-system branch is complete, delete this Disputes storage item /// For each market, this holds the dispute information for each dispute that's /// been issued. #[pallet::storage] @@ -1994,7 +1994,7 @@ mod pallet { _, Blake2_128Concat, MarketIdOf, - BoundedVec, T::MaxDisputes>, + BoundedVec, T::MaxDisputes>, ValueQuery, >; @@ -2081,13 +2081,16 @@ mod pallet { impl_unreserve_bond!(unreserve_creation_bond, creation); impl_unreserve_bond!(unreserve_oracle_bond, oracle); impl_unreserve_bond!(unreserve_outsider_bond, outsider); + impl_unreserve_bond!(unreserve_dispute_bond, dispute); impl_slash_bond!(slash_creation_bond, creation); impl_slash_bond!(slash_oracle_bond, oracle); impl_slash_bond!(slash_outsider_bond, outsider); + impl_slash_bond!(slash_dispute_bond, dispute); impl_repatriate_bond!(repatriate_oracle_bond, oracle); impl_is_bond_pending!(is_creation_bond_pending, creation); impl_is_bond_pending!(is_oracle_bond_pending, oracle); impl_is_bond_pending!(is_outsider_bond_pending, outsider); + impl_is_bond_pending!(is_dispute_bond_pending, dispute); fn slash_pending_bonds(market_id: &MarketIdOf, market: &MarketOf) -> DispatchResult { if Self::is_creation_bond_pending(market_id, market, false) { @@ -2099,6 +2102,9 @@ mod pallet { if Self::is_outsider_bond_pending(market_id, market, false) { Self::slash_outsider_bond(market_id, None)?; } + if Self::is_dispute_bond_pending(market_id, market, false) { + Self::slash_dispute_bond(market_id, None)?; + } Ok(()) } @@ -2207,7 +2213,7 @@ mod pallet { /// Clears this market from being stored for automatic resolution. fn clear_auto_resolve(market_id: &MarketIdOf) -> Result<(u32, u32), DispatchError> { let market = >::market(market_id)?; - let (ids_len, disputes_len) = match market.status { + let (ids_len, mdm_len) = match market.status { MarketStatus::Reported => { let report = market.report.ok_or(Error::::MarketIsNotReported)?; let dispute_duration_ends_at_block = @@ -2222,30 +2228,45 @@ mod pallet { ) } MarketStatus::Disputed => { - let disputes = Disputes::::get(market_id); // TODO(#782): use multiple benchmarks paths for different dispute mechanisms - let auto_resolve_block_opt = match market.dispute_mechanism { - MarketDisputeMechanism::Authorized => { - T::Authorized::get_auto_resolve(&disputes, market_id, &market)? - } - MarketDisputeMechanism::Court => { - T::Court::get_auto_resolve(&disputes, market_id, &market)? - } - MarketDisputeMechanism::SimpleDisputes => { - T::SimpleDisputes::get_auto_resolve(&disputes, market_id, &market)? - } - }; + let ResultWithWeightInfo { result: auto_resolve_block_opt, weight: _ } = + match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => { + T::Authorized::get_auto_resolve(market_id, &market) + } + MarketDisputeMechanism::Court => { + T::Court::get_auto_resolve(market_id, &market) + } + MarketDisputeMechanism::SimpleDisputes => { + T::SimpleDisputes::get_auto_resolve(market_id, &market) + } + }; if let Some(auto_resolve_block) = auto_resolve_block_opt { let ids_len = remove_auto_resolve::(market_id, auto_resolve_block); - (ids_len, disputes.len() as u32) + (ids_len, 0u32) } else { - (0u32, disputes.len() as u32) + (0u32, 0u32) } } _ => (0u32, 0u32), }; - Ok((ids_len, disputes_len)) + Ok((ids_len, mdm_len)) + } + + /// The dispute mechanism is intended to clear its own storage here. + fn clear_dispute_mechanism(market_id: &MarketIdOf) -> DispatchResult { + let market = >::market(market_id)?; + + // TODO(#782): use multiple benchmarks paths for different dispute mechanisms + match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => T::Authorized::clear(market_id, &market)?, + MarketDisputeMechanism::Court => T::Court::clear(market_id, &market)?, + MarketDisputeMechanism::SimpleDisputes => { + T::SimpleDisputes::clear(market_id, &market)? + } + }; + Ok(()) } pub(crate) fn do_buy_complete_set( @@ -2324,26 +2345,6 @@ mod pallet { } } - fn ensure_can_not_dispute_the_same_outcome( - disputes: &[MarketDispute], - report: &Report, - outcome: &OutcomeReport, - ) -> DispatchResult { - if let Some(last_dispute) = disputes.last() { - ensure!(&last_dispute.outcome != outcome, Error::::CannotDisputeSameOutcome); - } else { - ensure!(&report.outcome != outcome, Error::::CannotDisputeSameOutcome); - } - - Ok(()) - } - - #[inline] - fn ensure_disputes_does_not_exceed_max_disputes(num_disputes: u32) -> DispatchResult { - ensure!(num_disputes < T::MaxDisputes::get(), Error::::MaxDisputesReached); - Ok(()) - } - fn ensure_market_is_active(market: &MarketOf) -> DispatchResult { ensure!(market.status == MarketStatus::Active, Error::::MarketIsNotActive); Ok(()) @@ -2520,6 +2521,8 @@ mod pallet { } } + /// Handle a market resolution, which is currently in the reported state. + /// Returns the resolved outcome of a market, which is the reported outcome. fn resolve_reported_market( market_id: &MarketIdOf, market: &MarketOf, @@ -2540,49 +2543,115 @@ mod pallet { Ok(report.outcome.clone()) } + /// Handle a market resolution, which is currently in the disputed state. + /// Returns the resolved outcome of a market. fn resolve_disputed_market( market_id: &MarketIdOf, market: &MarketOf, - ) -> Result { + ) -> Result, DispatchError> { let report = market.report.as_ref().ok_or(Error::::MarketIsNotReported)?; - let disputes = Disputes::::get(market_id); + let mut weight = Weight::zero(); + + let res: ResultWithWeightInfo = + Self::get_resolved_outcome(market_id, market, &report.outcome)?; + let resolved_outcome = res.result; + weight = weight.saturating_add(res.weight); + let imbalance_left = Self::settle_bonds(market_id, market, &resolved_outcome, report)?; + + let remainder = match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => { + let res = T::Authorized::exchange( + market_id, + market, + &resolved_outcome, + imbalance_left, + )?; + let remainder = res.result; + weight = weight.saturating_add(res.weight); + remainder + } + MarketDisputeMechanism::Court => { + let res = + T::Court::exchange(market_id, market, &resolved_outcome, imbalance_left)?; + let remainder = res.result; + weight = weight.saturating_add(res.weight); + remainder + } + MarketDisputeMechanism::SimpleDisputes => { + let res = T::SimpleDisputes::exchange( + market_id, + market, + &resolved_outcome, + imbalance_left, + )?; + let remainder = res.result; + weight = weight.saturating_add(res.weight); + remainder + } + }; + + T::Slash::on_unbalanced(remainder); + + let res = ResultWithWeightInfo { result: resolved_outcome, weight }; + + Ok(res) + } + + /// Get the outcome the market should resolve to. + pub(crate) fn get_resolved_outcome( + market_id: &MarketIdOf, + market: &MarketOf, + reported_outcome: &OutcomeReport, + ) -> Result, DispatchError> { let mut resolved_outcome_option = None; + let mut weight = Weight::zero(); - #[cfg(feature = "with-global-disputes")] if let Some(o) = T::GlobalDisputes::determine_voting_winner(market_id) { resolved_outcome_option = Some(o); } - // TODO(#782): use multiple benchmarks paths for different dispute mechanisms - // Try to get the outcome of the MDM. If the MDM failed to resolve, default to // the oracle's report. if resolved_outcome_option.is_none() { resolved_outcome_option = match market.dispute_mechanism { MarketDisputeMechanism::Authorized => { - T::Authorized::on_resolution(&disputes, market_id, market)? + let res = T::Authorized::on_resolution(market_id, market)?; + weight = weight.saturating_add(res.weight); + res.result } MarketDisputeMechanism::Court => { - T::Court::on_resolution(&disputes, market_id, market)? + let res = T::Court::on_resolution(market_id, market)?; + weight = weight.saturating_add(res.weight); + res.result } MarketDisputeMechanism::SimpleDisputes => { - T::SimpleDisputes::on_resolution(&disputes, market_id, market)? + let res = T::SimpleDisputes::on_resolution(market_id, market)?; + weight = weight.saturating_add(res.weight); + res.result } }; } let resolved_outcome = - resolved_outcome_option.unwrap_or_else(|| report.outcome.clone()); + resolved_outcome_option.unwrap_or_else(|| reported_outcome.clone()); - let mut correct_reporters: Vec = Vec::new(); + let res = ResultWithWeightInfo { result: resolved_outcome, weight }; + + Ok(res) + } - // If the oracle reported right, return the OracleBond, otherwise slash it to - // pay the correct reporters. + /// Manage the outstanding bonds (oracle, outsider, dispute) of the market. + fn settle_bonds( + market_id: &MarketIdOf, + market: &MarketOf, + resolved_outcome: &OutcomeReport, + report: &Report, + ) -> Result, DispatchError> { let mut overall_imbalance = NegativeImbalanceOf::::zero(); let report_by_oracle = report.by == market.oracle; - let is_correct = report.outcome == resolved_outcome; + let is_correct = &report.outcome == resolved_outcome; let unreserve_outsider = || -> DispatchResult { if Self::is_outsider_bond_pending(market_id, market, true) { @@ -2620,42 +2689,21 @@ mod pallet { } } - for (i, dispute) in disputes.iter().enumerate() { - let actual_bond = default_dispute_bond::(i); - if dispute.outcome == resolved_outcome { - T::AssetManager::unreserve_named( - &Self::reserve_id(), - Asset::Ztg, - &dispute.by, - actual_bond, - ); - - correct_reporters.push(dispute.by.clone()); - } else { - let (imbalance, _) = CurrencyOf::::slash_reserved_named( - &Self::reserve_id(), - &dispute.by, - actual_bond.saturated_into::().saturated_into(), - ); - overall_imbalance.subsume(imbalance); - } - } - - // Fold all the imbalances into one and reward the correct reporters. The - // number of correct reporters might be zero if the market defaults to the - // report after abandoned dispute. In that case, the rewards remain slashed. - if let Some(reward_per_each) = - overall_imbalance.peek().checked_div(&correct_reporters.len().saturated_into()) - { - for correct_reporter in &correct_reporters { - let (actual_reward, leftover) = overall_imbalance.split(reward_per_each); - overall_imbalance = leftover; - CurrencyOf::::resolve_creating(correct_reporter, actual_reward); + if let Some(bond) = &market.bonds.dispute { + if !bond.is_settled { + if is_correct { + let imb = Self::slash_dispute_bond(market_id, None)?; + overall_imbalance.subsume(imb); + } else { + // If the report outcome was wrong, the dispute was justified + Self::unreserve_dispute_bond(market_id)?; + CurrencyOf::::resolve_creating(&bond.who, overall_imbalance); + overall_imbalance = NegativeImbalanceOf::::zero(); + } } } - T::Slash::on_unbalanced(overall_imbalance); - Ok(resolved_outcome) + Ok(overall_imbalance) } pub fn on_resolution( @@ -2670,7 +2718,11 @@ mod pallet { let resolved_outcome = match market.status { MarketStatus::Reported => Self::resolve_reported_market(market_id, market)?, - MarketStatus::Disputed => Self::resolve_disputed_market(market_id, market)?, + MarketStatus::Disputed => { + let res = Self::resolve_disputed_market(market_id, market)?; + total_weight = total_weight.saturating_add(res.weight); + res.result + } _ => return Err(Error::::InvalidMarketStatus.into()), }; let clean_up_weight = Self::clean_up_pool(market, market_id, &resolved_outcome)?; @@ -2686,7 +2738,7 @@ mod pallet { m.resolved_outcome = Some(resolved_outcome.clone()); Ok(()) })?; - Disputes::::remove(market_id); + Self::deposit_event(Event::MarketResolved( *market_id, MarketStatus::Resolved, @@ -2936,20 +2988,6 @@ mod pallet { )) } - // If the market is already disputed, does nothing. - fn set_market_as_disputed( - market: &MarketOf, - market_id: &MarketIdOf, - ) -> DispatchResult { - if market.status != MarketStatus::Disputed { - >::mutate_market(market_id, |m| { - m.status = MarketStatus::Disputed; - Ok(()) - })?; - } - Ok(()) - } - // If a market has a pool that is `Active`, then changes from `Active` to `Clean`. If // the market does not exist or the market does not have a pool, does nothing. fn clean_up_pool( @@ -3009,19 +3047,6 @@ mod pallet { Ok(T::WeightInfo::start_subsidy(total_assets.saturated_into())) } - fn validate_dispute( - disputes: &[MarketDispute], - market: &MarketOf, - num_disputes: u32, - outcome_report: &OutcomeReport, - ) -> DispatchResult { - let report = market.report.as_ref().ok_or(Error::::MarketIsNotReported)?; - ensure!(market.matches_outcome_report(outcome_report), Error::::OutcomeMismatch); - Self::ensure_can_not_dispute_the_same_outcome(disputes, report, outcome_report)?; - Self::ensure_disputes_does_not_exceed_max_disputes(num_disputes)?; - Ok(()) - } - fn construct_market( base_asset: Asset>, creator: T::AccountId, @@ -3088,16 +3113,6 @@ mod pallet { } } - // No-one can bound more than BalanceOf, therefore, this functions saturates - pub(crate) fn default_dispute_bond(n: usize) -> BalanceOf - where - T: Config, - { - T::DisputeBond::get().saturating_add( - T::DisputeFactor::get().saturating_mul(n.saturated_into::().into()), - ) - } - fn remove_item(items: &mut BoundedVec, item: &I) { if let Some(pos) = items.iter().position(|i| i == item) { items.swap_remove(pos); @@ -3123,7 +3138,6 @@ mod pallet { type Balance = BalanceOf; type BlockNumber = T::BlockNumber; type MarketId = MarketIdOf; - type MaxDisputes = T::MaxDisputes; type Moment = MomentOf; fn resolve( @@ -3154,12 +3168,5 @@ mod pallet { fn remove_auto_resolve(market_id: &Self::MarketId, resolve_at: Self::BlockNumber) -> u32 { remove_auto_resolve::(market_id, resolve_at) } - - fn get_disputes( - market_id: &Self::MarketId, - ) -> BoundedVec, Self::MaxDisputes> - { - Disputes::::get(market_id) - } } } diff --git a/zrml/prediction-markets/src/migrations.rs b/zrml/prediction-markets/src/migrations.rs index 3bc5ddb7c..2134275b0 100644 --- a/zrml/prediction-markets/src/migrations.rs +++ b/zrml/prediction-markets/src/migrations.rs @@ -15,3 +15,846 @@ // // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . + +#[cfg(feature = "try-runtime")] +use crate::MarketIdOf; +use crate::{BalanceOf, Config, MomentOf}; +#[cfg(feature = "try-runtime")] +use alloc::collections::BTreeMap; +#[cfg(feature = "try-runtime")] +use alloc::format; +use alloc::vec::Vec; +#[cfg(feature = "try-runtime")] +use frame_support::migration::storage_key_iter; +use frame_support::{ + dispatch::Weight, + log, + pallet_prelude::PhantomData, + traits::{Get, OnRuntimeUpgrade, StorageVersion}, + RuntimeDebug, +}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_runtime::traits::Saturating; +use zeitgeist_primitives::types::{ + Asset, Bond, Deadlines, Market, MarketBonds, MarketCreation, MarketDisputeMechanism, + MarketPeriod, MarketStatus, MarketType, OutcomeReport, Report, ScoringRule, +}; +use zrml_market_commons::{MarketCommonsPalletApi, Pallet as MarketCommonsPallet}; + +#[cfg(any(feature = "try-runtime", test))] +const MARKET_COMMONS: &[u8] = b"MarketCommons"; +#[cfg(any(feature = "try-runtime", test))] +const MARKETS: &[u8] = b"Markets"; + +const MARKET_COMMONS_REQUIRED_STORAGE_VERSION: u16 = 6; +const MARKET_COMMONS_NEXT_STORAGE_VERSION: u16 = 7; + +#[derive(Clone, Decode, Encode, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct OldMarketBonds { + pub creation: Option>, + pub oracle: Option>, + pub outsider: Option>, +} + +#[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub struct OldMarket { + pub base_asset: A, + pub creator: AI, + pub creation: MarketCreation, + pub creator_fee: u8, + pub oracle: AI, + pub metadata: Vec, + pub market_type: MarketType, + pub period: MarketPeriod, + pub deadlines: Deadlines, + pub scoring_rule: ScoringRule, + pub status: MarketStatus, + pub report: Option>, + pub resolved_outcome: Option, + pub dispute_mechanism: MarketDisputeMechanism, + pub bonds: OldMarketBonds, +} + +type OldMarketOf = OldMarket< + ::AccountId, + BalanceOf, + ::BlockNumber, + MomentOf, + Asset<::MarketId>, +>; + +#[frame_support::storage_alias] +pub(crate) type Markets = StorageMap< + MarketCommonsPallet, + frame_support::Blake2_128Concat, + ::MarketId, + OldMarketOf, +>; + +pub struct AddDisputeBond(PhantomData); + +impl OnRuntimeUpgrade for AddDisputeBond { + fn on_runtime_upgrade() -> Weight { + let mut total_weight = T::DbWeight::get().reads(1); + let market_commons_version = StorageVersion::get::>(); + if market_commons_version != MARKET_COMMONS_REQUIRED_STORAGE_VERSION { + log::info!( + "AddDisputeBond: market-commons version is {:?}, but {:?} is required", + market_commons_version, + MARKET_COMMONS_REQUIRED_STORAGE_VERSION, + ); + return total_weight; + } + log::info!("AddDisputeBond: Starting..."); + + let mut translated = 0u64; + zrml_market_commons::Markets::::translate::, _>( + |market_id, old_market| { + translated.saturating_inc(); + + let mut dispute_bond = None; + // SimpleDisputes is regarded in the following migration `MoveDataToSimpleDisputes` + if let MarketDisputeMechanism::Authorized = old_market.dispute_mechanism { + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); + let old_disputes = crate::Disputes::::get(market_id); + if let Some(first_dispute) = old_disputes.first() { + let OldMarketDispute { at: _, by, outcome: _ } = first_dispute; + dispute_bond = Some(Bond::new(by.clone(), T::DisputeBond::get())); + } + } + + let new_market = Market { + base_asset: old_market.base_asset, + creator: old_market.creator, + creation: old_market.creation, + creator_fee: old_market.creator_fee, + oracle: old_market.oracle, + metadata: old_market.metadata, + market_type: old_market.market_type, + period: old_market.period, + scoring_rule: old_market.scoring_rule, + status: old_market.status, + report: old_market.report, + resolved_outcome: old_market.resolved_outcome, + dispute_mechanism: old_market.dispute_mechanism, + deadlines: old_market.deadlines, + bonds: MarketBonds { + creation: old_market.bonds.creation, + oracle: old_market.bonds.oracle, + outsider: old_market.bonds.outsider, + dispute: dispute_bond, + }, + }; + + Some(new_market) + }, + ); + log::info!("AddDisputeBond: Upgraded {} markets.", translated); + total_weight = + total_weight.saturating_add(T::DbWeight::get().reads_writes(translated, translated)); + + StorageVersion::new(MARKET_COMMONS_NEXT_STORAGE_VERSION).put::>(); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + log::info!("AddDisputeBond: Done!"); + total_weight + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + use frame_support::pallet_prelude::Blake2_128Concat; + + let old_markets = storage_key_iter::, OldMarketOf, Blake2_128Concat>( + MARKET_COMMONS, + MARKETS, + ) + .collect::>(); + + let markets = Markets::::iter_keys().count() as u32; + let decodable_markets = Markets::::iter_values().count() as u32; + if markets != decodable_markets { + log::error!( + "Can only decode {} of {} markets - others will be dropped", + decodable_markets, + markets + ); + } else { + log::info!("Markets: {}, Decodable Markets: {}", markets, decodable_markets); + } + + Ok(old_markets.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(previous_state: Vec) -> Result<(), &'static str> { + let old_markets: BTreeMap, OldMarketOf> = + Decode::decode(&mut &previous_state[..]) + .expect("Failed to decode state: Invalid state"); + let new_market_count = >::market_iter().count(); + assert_eq!(old_markets.len(), new_market_count); + for (market_id, new_market) in >::market_iter() { + let old_market = old_markets + .get(&market_id) + .expect(&format!("Market {:?} not found", market_id)[..]); + assert_eq!(new_market.base_asset, old_market.base_asset); + assert_eq!(new_market.creator, old_market.creator); + assert_eq!(new_market.creation, old_market.creation); + assert_eq!(new_market.creator_fee, old_market.creator_fee); + assert_eq!(new_market.oracle, old_market.oracle); + assert_eq!(new_market.metadata, old_market.metadata); + assert_eq!(new_market.market_type, old_market.market_type); + assert_eq!(new_market.period, old_market.period); + assert_eq!(new_market.deadlines, old_market.deadlines); + assert_eq!(new_market.scoring_rule, old_market.scoring_rule); + assert_eq!(new_market.status, old_market.status); + assert_eq!(new_market.report, old_market.report); + assert_eq!(new_market.resolved_outcome, old_market.resolved_outcome); + assert_eq!(new_market.dispute_mechanism, old_market.dispute_mechanism); + assert_eq!(new_market.bonds.oracle, old_market.bonds.oracle); + assert_eq!(new_market.bonds.creation, old_market.bonds.creation); + assert_eq!(new_market.bonds.outsider, old_market.bonds.outsider); + // new fields + // other dispute mechanisms are regarded in the migration after this migration + if let MarketDisputeMechanism::Authorized = new_market.dispute_mechanism { + let old_disputes = crate::Disputes::::get(market_id); + if let Some(first_dispute) = old_disputes.first() { + let OldMarketDispute { at: _, by, outcome: _ } = first_dispute; + assert_eq!( + new_market.bonds.dispute, + Some(Bond { + who: by.clone(), + value: T::DisputeBond::get(), + is_settled: false + }) + ); + } + } else { + assert_eq!(new_market.bonds.dispute, None); + } + } + + log::info!("AddDisputeBond: Market Counter post-upgrade is {}!", new_market_count); + assert!(new_market_count > 0); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + mock::{DisputeBond, ExtBuilder, Runtime}, + MarketIdOf, MarketOf, + }; + use frame_support::{ + dispatch::fmt::Debug, migration::put_storage_value, Blake2_128Concat, StorageHasher, + }; + use zrml_market_commons::MarketCommonsPalletApi; + + #[test] + fn on_runtime_upgrade_increments_the_storage_version() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + AddDisputeBond::::on_runtime_upgrade(); + assert_eq!( + StorageVersion::get::>(), + MARKET_COMMONS_NEXT_STORAGE_VERSION + ); + }); + } + + #[test] + fn on_runtime_upgrade_is_noop_if_versions_are_not_correct() { + ExtBuilder::default().build().execute_with(|| { + // Don't set up chain to signal that storage is already up to date. + let (_, new_markets) = construct_old_new_tuple(None); + populate_test_data::, MarketOf>( + MARKET_COMMONS, + MARKETS, + new_markets.clone(), + ); + AddDisputeBond::::on_runtime_upgrade(); + let actual = >::market(&0u128).unwrap(); + assert_eq!(actual, new_markets[0]); + }); + } + + #[test] + fn on_runtime_upgrade_correctly_updates_markets_with_none_disputor() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + let (old_markets, new_markets) = construct_old_new_tuple(None); + populate_test_data::, OldMarketOf>( + MARKET_COMMONS, + MARKETS, + old_markets, + ); + AddDisputeBond::::on_runtime_upgrade(); + let actual = >::market(&0u128).unwrap(); + assert_eq!(actual, new_markets[0]); + }); + } + + #[test] + fn on_runtime_upgrade_correctly_updates_markets_with_some_disputor() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + let mut disputes = crate::Disputes::::get(0); + let disputor = crate::mock::EVE; + let dispute = + OldMarketDispute { at: 0, by: disputor, outcome: OutcomeReport::Categorical(0u16) }; + disputes.try_push(dispute).unwrap(); + crate::Disputes::::insert(0, disputes); + let (old_markets, new_markets) = construct_old_new_tuple(Some(disputor)); + populate_test_data::, OldMarketOf>( + MARKET_COMMONS, + MARKETS, + old_markets, + ); + AddDisputeBond::::on_runtime_upgrade(); + let actual = >::market(&0u128).unwrap(); + assert_eq!(actual, new_markets[0]); + }); + } + + fn set_up_version() { + StorageVersion::new(MARKET_COMMONS_REQUIRED_STORAGE_VERSION) + .put::>(); + } + + fn construct_old_new_tuple( + disputor: Option, + ) -> (Vec>, Vec>) { + let base_asset = Asset::Ztg; + let creator = 999; + let creator_fee = 1; + let oracle = 2; + let metadata = vec![3, 4, 5]; + let market_type = MarketType::Categorical(6); + let period = MarketPeriod::Block(7..8); + let scoring_rule = ScoringRule::CPMM; + let status = MarketStatus::Disputed; + let creation = MarketCreation::Permissionless; + let report = None; + let resolved_outcome = None; + let dispute_mechanism = MarketDisputeMechanism::Authorized; + let deadlines = Deadlines::default(); + let old_bonds = OldMarketBonds { + creation: Some(Bond::new(creator, ::ValidityBond::get())), + oracle: Some(Bond::new(creator, ::OracleBond::get())), + outsider: Some(Bond::new(creator, ::OutsiderBond::get())), + }; + let dispute_bond = disputor.map(|disputor| Bond::new(disputor, DisputeBond::get())); + let new_bonds = MarketBonds { + creation: Some(Bond::new(creator, ::ValidityBond::get())), + oracle: Some(Bond::new(creator, ::OracleBond::get())), + outsider: Some(Bond::new(creator, ::OutsiderBond::get())), + dispute: dispute_bond, + }; + + let old_market = OldMarket { + base_asset, + creator, + creation: creation.clone(), + creator_fee, + oracle, + metadata: metadata.clone(), + market_type: market_type.clone(), + period: period.clone(), + scoring_rule, + status, + report: report.clone(), + resolved_outcome: resolved_outcome.clone(), + dispute_mechanism: dispute_mechanism.clone(), + deadlines, + bonds: old_bonds, + }; + let new_market = Market { + base_asset, + creator, + creation, + creator_fee, + oracle, + metadata, + market_type, + period, + scoring_rule, + status, + report, + resolved_outcome, + dispute_mechanism, + deadlines, + bonds: new_bonds, + }; + (vec![old_market], vec![new_market]) + } + + #[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()); + } + } +} + +use frame_support::dispatch::EncodeLike; +use sp_runtime::SaturatedConversion; +use zeitgeist_primitives::types::{MarketDispute, OldMarketDispute}; + +const PREDICTION_MARKETS_REQUIRED_STORAGE_VERSION: u16 = 6; +const PREDICTION_MARKETS_NEXT_STORAGE_VERSION: u16 = 7; + +#[cfg(feature = "try-runtime")] +type OldDisputesOf = frame_support::BoundedVec< + OldMarketDispute< + ::AccountId, + ::BlockNumber, + >, + ::MaxDisputes, +>; + +pub struct MoveDataToSimpleDisputes(PhantomData); + +impl OnRuntimeUpgrade + for MoveDataToSimpleDisputes +where + ::MarketId: EncodeLike< + <::MarketCommons as MarketCommonsPalletApi>::MarketId, + >, +{ + fn on_runtime_upgrade() -> Weight { + use orml_traits::NamedMultiReservableCurrency; + + let mut total_weight = T::DbWeight::get().reads(1); + let pm_version = StorageVersion::get::>(); + if pm_version != PREDICTION_MARKETS_REQUIRED_STORAGE_VERSION { + log::info!( + "MoveDataToSimpleDisputes: prediction-markets version is {:?}, but {:?} is \ + required", + pm_version, + PREDICTION_MARKETS_REQUIRED_STORAGE_VERSION, + ); + return total_weight; + } + log::info!("MoveDataToSimpleDisputes: Starting..."); + + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); + + // important drain disputes storage item from prediction markets pallet + for (market_id, old_disputes) in crate::Disputes::::drain() { + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + if let Ok(market) = >::market(&market_id) { + match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => continue, + // just transform SimpleDispute disputes + MarketDisputeMechanism::SimpleDisputes => (), + MarketDisputeMechanism::Court => continue, + } + } else { + log::warn!( + "MoveDataToSimpleDisputes: Could not find market with market id {:?}", + market_id + ); + } + + total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); + let mut new_disputes = zrml_simple_disputes::Disputes::::get(market_id); + for (i, old_dispute) in old_disputes.iter().enumerate() { + let bond = zrml_simple_disputes::default_outcome_bond::(i); + let new_dispute = MarketDispute { + at: old_dispute.at, + by: old_dispute.by.clone(), + outcome: old_dispute.outcome.clone(), + bond, + }; + let res = new_disputes.try_push(new_dispute); + if res.is_err() { + log::error!( + "MoveDataToSimpleDisputes: Could not push dispute for market id {:?}", + market_id + ); + } + + // switch to new reserve identifier for simple disputes + let sd_reserve_id = >::reserve_id(); + let pm_reserve_id = >::reserve_id(); + + // charge weight defensivly for unreserve_named + // https://github.com/open-web3-stack/open-runtime-module-library/blob/24f0a8b6e04e1078f70d0437fb816337cdf4f64c/tokens/src/lib.rs#L1516-L1547 + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(4, 3)); + let reserved_balance = ::AssetManager::reserved_balance_named( + &pm_reserve_id, + Asset::Ztg, + &old_dispute.by, + ); + if reserved_balance < bond.saturated_into::().saturated_into() { + // warns for battery station market id 386 + // https://discord.com/channels/737780518313000960/817041223201587230/958682619413934151 + log::warn!( + "MoveDataToSimpleDisputes: Could not unreserve {:?} for {:?} because \ + reserved balance is only {:?}. Market id: {:?}", + bond, + old_dispute.by, + reserved_balance, + market_id, + ); + } + ::AssetManager::unreserve_named( + &pm_reserve_id, + Asset::Ztg, + &old_dispute.by, + bond.saturated_into::().saturated_into(), + ); + + // charge weight defensivly for reserve_named + // https://github.com/open-web3-stack/open-runtime-module-library/blob/24f0a8b6e04e1078f70d0437fb816337cdf4f64c/tokens/src/lib.rs#L1486-L1499 + total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(3, 3)); + let res = ::AssetManager::reserve_named( + &sd_reserve_id, + Asset::Ztg, + &old_dispute.by, + bond.saturated_into::().saturated_into(), + ); + if res.is_err() { + log::error!( + "MoveDataToSimpleDisputes: Could not reserve bond for dispute caller {:?} \ + and market id {:?}", + old_dispute.by, + market_id + ); + } + } + + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + zrml_simple_disputes::Disputes::::insert(market_id, new_disputes); + } + + StorageVersion::new(PREDICTION_MARKETS_NEXT_STORAGE_VERSION).put::>(); + total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); + log::info!("MoveDataToSimpleDisputes: Done!"); + total_weight + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + log::info!("MoveDataToSimpleDisputes: Start pre_upgrade!"); + + let old_disputes = crate::Disputes::::iter().collect::>(); + Ok(old_disputes.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(previous_state: Vec) -> Result<(), &'static str> { + let old_disputes: BTreeMap, OldDisputesOf> = + Decode::decode(&mut &previous_state[..]) + .expect("Failed to decode state: Invalid state"); + + log::info!("MoveDataToSimpleDisputes: (post_upgrade) Start first try-runtime part!"); + + for (market_id, o) in old_disputes.iter() { + let market = >::market(market_id) + .expect(&format!("Market for market id {:?} not found", market_id)[..]); + + // market id is a reference, but we need the raw value to encode with the where clause + let disputes = zrml_simple_disputes::Disputes::::get(*market_id); + + match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => { + let simple_disputes_count = disputes.iter().count(); + assert_eq!(simple_disputes_count, 0); + continue; + } + MarketDisputeMechanism::SimpleDisputes => { + let new_count = disputes.iter().count(); + let old_count = o.iter().count(); + assert_eq!(new_count, old_count); + } + MarketDisputeMechanism::Court => { + panic!("Court should not be contained at all.") + } + } + } + + log::info!("MoveDataToSimpleDisputes: (post_upgrade) Start second try-runtime part!"); + + assert!(crate::Disputes::::iter().count() == 0); + + for (market_id, new_disputes) in zrml_simple_disputes::Disputes::::iter() { + let old_disputes = old_disputes + .get(&market_id.saturated_into::().saturated_into()) + .expect(&format!("Disputes for market {:?} not found", market_id)[..]); + + let market = ::MarketCommons::market(&market_id) + .expect(&format!("Market for market id {:?} not found", market_id)[..]); + match market.dispute_mechanism { + MarketDisputeMechanism::Authorized => { + panic!("Authorized should not be contained in simple disputes."); + } + MarketDisputeMechanism::SimpleDisputes => (), + MarketDisputeMechanism::Court => { + panic!("Court should not be contained in simple disputes."); + } + } + + for (i, new_dispute) in new_disputes.iter().enumerate() { + let old_dispute = + old_disputes.get(i).expect(&format!("Dispute at index {} not found", i)[..]); + assert_eq!(new_dispute.at, old_dispute.at); + assert_eq!(new_dispute.by, old_dispute.by); + assert_eq!(new_dispute.outcome, old_dispute.outcome); + assert_eq!(new_dispute.bond, zrml_simple_disputes::default_outcome_bond::(i)); + } + } + + log::info!("MoveDataToSimpleDisputes: Done! (post_upgrade)"); + Ok(()) + } +} + +#[cfg(test)] +mod tests_simple_disputes_migration { + use super::*; + use crate::{ + mock::{DisputeBond, ExtBuilder, Runtime}, + MarketOf, + }; + use orml_traits::NamedMultiReservableCurrency; + use zrml_market_commons::MarketCommonsPalletApi; + + #[test] + fn on_runtime_upgrade_increments_the_storage_version() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + MoveDataToSimpleDisputes::::on_runtime_upgrade(); + assert_eq!( + StorageVersion::get::>(), + PREDICTION_MARKETS_NEXT_STORAGE_VERSION + ); + }); + } + + #[test] + fn on_runtime_upgrade_is_noop_if_versions_are_not_correct() { + ExtBuilder::default().build().execute_with(|| { + // Don't set up chain to signal that storage is already up to date. + let market_id = 0u128; + let mut disputes = zrml_simple_disputes::Disputes::::get(market_id); + let dispute = MarketDispute { + at: 42u64, + by: 0u128, + outcome: OutcomeReport::Categorical(0u16), + bond: DisputeBond::get(), + }; + disputes.try_push(dispute.clone()).unwrap(); + zrml_simple_disputes::Disputes::::insert(market_id, disputes); + let market = get_market(MarketDisputeMechanism::SimpleDisputes); + >::push_market(market).unwrap(); + + MoveDataToSimpleDisputes::::on_runtime_upgrade(); + + let actual = zrml_simple_disputes::Disputes::::get(0); + assert_eq!(actual, vec![dispute]); + }); + } + + #[test] + fn on_runtime_upgrade_correctly_updates_simple_disputes() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + let market_id = 0u128; + + let mut disputes = crate::Disputes::::get(0); + for i in 0..::MaxDisputes::get() { + let dispute = OldMarketDispute { + at: i as u64 + 42u64, + by: i as u128, + outcome: OutcomeReport::Categorical(i), + }; + disputes.try_push(dispute).unwrap(); + } + crate::Disputes::::insert(market_id, disputes); + let market = get_market(MarketDisputeMechanism::SimpleDisputes); + >::push_market(market).unwrap(); + + MoveDataToSimpleDisputes::::on_runtime_upgrade(); + + let mut disputes = zrml_simple_disputes::Disputes::::get(market_id); + for i in 0..::MaxDisputes::get() { + let dispute = disputes.get_mut(i as usize).unwrap(); + + assert_eq!(dispute.at, i as u64 + 42u64); + assert_eq!(dispute.by, i as u128); + assert_eq!(dispute.outcome, OutcomeReport::Categorical(i)); + + let bond = zrml_simple_disputes::default_outcome_bond::(i as usize); + assert_eq!(dispute.bond, bond); + } + }); + } + + #[test] + fn on_runtime_upgrade_correctly_updates_reserve_ids() { + ExtBuilder::default().build().execute_with(|| { + set_up_version(); + let market_id = 0u128; + + let mut disputes = crate::Disputes::::get(0); + for i in 0..::MaxDisputes::get() { + let dispute = OldMarketDispute { + at: i as u64 + 42u64, + by: i as u128, + outcome: OutcomeReport::Categorical(i), + }; + let bond = zrml_simple_disputes::default_outcome_bond::(i.into()); + let pm_reserve_id = crate::Pallet::::reserve_id(); + let res = ::AssetManager::reserve_named( + &pm_reserve_id, + Asset::Ztg, + &dispute.by, + bond.saturated_into::().saturated_into(), + ); + assert!(res.is_ok()); + disputes.try_push(dispute).unwrap(); + } + crate::Disputes::::insert(market_id, disputes); + let market = get_market(MarketDisputeMechanism::SimpleDisputes); + >::push_market(market).unwrap(); + + MoveDataToSimpleDisputes::::on_runtime_upgrade(); + + let mut disputes = zrml_simple_disputes::Disputes::::get(market_id); + for i in 0..::MaxDisputes::get() { + let dispute = disputes.get_mut(i as usize).unwrap(); + + let sd_reserve_id = zrml_simple_disputes::Pallet::::reserve_id(); + let reserved_balance = + ::AssetManager::reserved_balance_named( + &sd_reserve_id, + Asset::Ztg, + &dispute.by, + ); + let bond = zrml_simple_disputes::default_outcome_bond::(i.into()); + assert_eq!(reserved_balance, bond); + assert!(reserved_balance > 0); + + let pm_reserve_id = crate::Pallet::::reserve_id(); + let reserved_balance = + ::AssetManager::reserved_balance_named( + &pm_reserve_id, + Asset::Ztg, + &dispute.by, + ); + assert_eq!(reserved_balance, 0); + } + }); + } + + fn set_up_version() { + StorageVersion::new(PREDICTION_MARKETS_REQUIRED_STORAGE_VERSION) + .put::>(); + } + + fn get_market(dispute_mechanism: MarketDisputeMechanism) -> MarketOf { + let base_asset = Asset::Ztg; + let creator = 999; + let creator_fee = 1; + let oracle = 2; + let metadata = vec![3, 4, 5]; + let market_type = MarketType::Categorical(6); + let period = MarketPeriod::Block(7..8); + let scoring_rule = ScoringRule::CPMM; + let status = MarketStatus::Disputed; + let creation = MarketCreation::Permissionless; + let report = None; + let resolved_outcome = None; + let deadlines = Deadlines::default(); + let bonds = MarketBonds { + creation: Some(Bond::new(creator, ::ValidityBond::get())), + oracle: Some(Bond::new(creator, ::OracleBond::get())), + outsider: None, + dispute: None, + }; + + Market { + base_asset, + creator, + creation, + creator_fee, + oracle, + metadata, + market_type, + period, + scoring_rule, + status, + report, + resolved_outcome, + dispute_mechanism, + deadlines, + bonds, + } + } +} + +// We use these utilities to prevent having to make the swaps pallet a dependency of +// prediciton-markets. The calls are based on the implementation of `StorageVersion`, found here: +// https://github.com/paritytech/substrate/blob/bc7a1e6c19aec92bfa247d8ca68ec63e07061032/frame/support/src/traits/metadata.rs#L168-L230 +// and previous migrations. + +mod utility { + use crate::{BalanceOf, Config, MarketIdOf}; + use alloc::vec::Vec; + use frame_support::{ + migration::{get_storage_value, put_storage_value}, + storage::{storage_prefix, unhashed}, + traits::StorageVersion, + Blake2_128Concat, StorageHasher, + }; + use parity_scale_codec::Encode; + use zeitgeist_primitives::types::{Pool, PoolId}; + + #[allow(unused)] + const SWAPS: &[u8] = b"Swaps"; + #[allow(unused)] + const POOLS: &[u8] = b"Pools"; + #[allow(unused)] + fn storage_prefix_of_swaps_pallet() -> [u8; 32] { + storage_prefix(b"Swaps", b":__STORAGE_VERSION__:") + } + #[allow(unused)] + pub fn key_to_hash(key: K) -> Vec + where + H: StorageHasher, + K: Encode, + { + key.using_encoded(H::hash).as_ref().to_vec() + } + #[allow(unused)] + pub fn get_on_chain_storage_version_of_swaps_pallet() -> StorageVersion { + let key = storage_prefix_of_swaps_pallet(); + unhashed::get_or_default(&key) + } + #[allow(unused)] + pub fn put_storage_version_of_swaps_pallet(value: u16) { + let key = storage_prefix_of_swaps_pallet(); + unhashed::put(&key, &StorageVersion::new(value)); + } + #[allow(unused)] + pub fn get_pool(pool_id: PoolId) -> Option, MarketIdOf>> { + let hash = key_to_hash::(pool_id); + let pool_maybe = + get_storage_value::, MarketIdOf>>>(SWAPS, POOLS, &hash); + pool_maybe.unwrap_or(None) + } + #[allow(unused)] + pub fn set_pool(pool_id: PoolId, pool: Pool, MarketIdOf>) { + let hash = key_to_hash::(pool_id); + put_storage_value(SWAPS, POOLS, &hash, Some(pool)); + } +} diff --git a/zrml/prediction-markets/src/mock.rs b/zrml/prediction-markets/src/mock.rs index f5d81641f..f7807b67a 100644 --- a/zrml/prediction-markets/src/mock.rs +++ b/zrml/prediction-markets/src/mock.rs @@ -27,7 +27,7 @@ use frame_support::{ construct_runtime, ord_parameter_types, parameter_types, traits::{Everything, NeverEnsureOrigin, OnFinalize, OnInitialize}, }; -use frame_system::EnsureSignedBy; +use frame_system::{EnsureRoot, EnsureSignedBy}; #[cfg(feature = "parachain")] use orml_asset_registry::AssetMetadata; use sp_arithmetic::per_things::Percent; @@ -38,15 +38,17 @@ use sp_runtime::{ use substrate_fixed::{types::extra::U33, FixedI128, FixedU128}; use zeitgeist_primitives::{ constants::mock::{ - AuthorizedPalletId, BalanceFractionalDecimals, BlockHashCount, CorrectionPeriod, - CourtCaseDuration, CourtPalletId, DisputeFactor, ExistentialDeposit, ExistentialDeposits, - ExitFee, GetNativeCurrencyId, LiquidityMiningPalletId, MaxApprovals, MaxAssets, - MaxCategories, MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, MaxGracePeriod, - MaxInRatio, MaxMarketLifetime, MaxOracleDuration, MaxOutRatio, MaxRejectReasonLen, - MaxReserves, MaxSubsidyPeriod, MaxSwapFee, MaxTotalWeight, MaxWeight, MinAssets, - MinCategories, MinDisputeDuration, MinOracleDuration, MinSubsidy, MinSubsidyPeriod, - MinWeight, MinimumPeriod, OutsiderBond, PmPalletId, SimpleDisputesPalletId, StakeWeight, - SwapsPalletId, TreasuryPalletId, BASE, CENT, MILLISECS_PER_BLOCK, + AggregationPeriod, AppealBond, AppealPeriod, AuthorizedPalletId, BalanceFractionalDecimals, + BlockHashCount, BlocksPerYear, CorrectionPeriod, CourtPalletId, ExistentialDeposit, + ExistentialDeposits, ExitFee, GetNativeCurrencyId, InflationPeriod, + LiquidityMiningPalletId, LockId, MaxAppeals, MaxApprovals, MaxAssets, MaxCategories, + MaxCourtParticipants, MaxDelegations, MaxDisputeDuration, MaxDisputes, MaxEditReasonLen, + MaxGracePeriod, MaxInRatio, MaxMarketLifetime, MaxOracleDuration, MaxOutRatio, + MaxRejectReasonLen, MaxReserves, MaxSelectedDraws, MaxSubsidyPeriod, MaxSwapFee, + MaxTotalWeight, MaxWeight, MinAssets, MinCategories, MinDisputeDuration, MinJurorStake, + MinOracleDuration, MinSubsidy, MinSubsidyPeriod, MinWeight, MinimumPeriod, OutcomeBond, + OutcomeFactor, OutsiderBond, PmPalletId, RequestInterval, SimpleDisputesPalletId, + SwapsPalletId, TreasuryPalletId, VotePeriod, BASE, CENT, MILLISECS_PER_BLOCK, }, types::{ AccountIdTest, Amount, Asset, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, @@ -54,10 +56,9 @@ use zeitgeist_primitives::{ }, }; -#[cfg(feature = "with-global-disputes")] use zeitgeist_primitives::constants::mock::{ - GlobalDisputeLockId, GlobalDisputePeriod, GlobalDisputesPalletId, MaxGlobalDisputeVotes, - MaxOwners, MinOutcomeVoteAmount, RemoveKeysLimit, VotingOutcomeFee, + AddOutcomePeriod, GdVotingPeriod, GlobalDisputeLockId, GlobalDisputesPalletId, + MaxGlobalDisputeVotes, MaxOwners, MinOutcomeVoteAmount, RemoveKeysLimit, VotingOutcomeFee, }; use zrml_rikiddo::types::{EmaMarketVolume, FeeSigmoid, RikiddoSigmoidMV}; @@ -84,7 +85,6 @@ parameter_types! { pub const DisputeBond: Balance = 109 * CENT; } -#[cfg(feature = "with-global-disputes")] construct_runtime!( pub enum Runtime where @@ -111,32 +111,6 @@ construct_runtime!( } ); -#[cfg(not(feature = "with-global-disputes"))] -construct_runtime!( - pub enum Runtime - where - Block = BlockTest, - NodeBlock = BlockTest, - UncheckedExtrinsic = UncheckedExtrinsicTest, - { - Authorized: zrml_authorized::{Event, Pallet, Storage}, - Balances: pallet_balances::{Call, Config, Event, Pallet, Storage}, - Court: zrml_court::{Event, Pallet, Storage}, - AssetManager: orml_currencies::{Call, Pallet, Storage}, - LiquidityMining: zrml_liquidity_mining::{Config, Event, Pallet}, - MarketCommons: zrml_market_commons::{Pallet, Storage}, - PredictionMarkets: prediction_markets::{Event, Pallet, Storage}, - RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Pallet, Storage}, - RikiddoSigmoidFeeMarketEma: zrml_rikiddo::{Pallet, Storage}, - SimpleDisputes: zrml_simple_disputes::{Event, Pallet, Storage}, - Swaps: zrml_swaps::{Call, Event, Pallet}, - System: frame_system::{Call, Config, Event, Pallet, Storage}, - Timestamp: pallet_timestamp::{Pallet}, - Tokens: orml_tokens::{Config, Event, Pallet, Storage}, - Treasury: pallet_treasury::{Call, Event, Pallet, Storage}, - } -); - impl crate::Config for Runtime { type AdvisoryBond = AdvisoryBond; type AdvisoryBondSlashPercentage = AdvisoryBondSlashPercentage; @@ -148,12 +122,8 @@ impl crate::Config for Runtime { type Court = Court; type DestroyOrigin = EnsureSignedBy; type DisputeBond = DisputeBond; - type DisputeFactor = DisputeFactor; type RuntimeEvent = RuntimeEvent; - #[cfg(feature = "with-global-disputes")] type GlobalDisputes = GlobalDisputes; - #[cfg(feature = "with-global-disputes")] - type GlobalDisputePeriod = GlobalDisputePeriod; type LiquidityMining = LiquidityMining; type MaxCategories = MaxCategories; type MaxDisputes = MaxDisputes; @@ -275,13 +245,27 @@ impl zrml_authorized::Config for Runtime { } impl zrml_court::Config for Runtime { - type CourtCaseDuration = CourtCaseDuration; + type AppealBond = AppealBond; + type BlocksPerYear = BlocksPerYear; type DisputeResolution = prediction_markets::Pallet; + type VotePeriod = VotePeriod; + type AggregationPeriod = AggregationPeriod; + type AppealPeriod = AppealPeriod; + type LockId = LockId; + type Currency = Balances; type RuntimeEvent = RuntimeEvent; + type InflationPeriod = InflationPeriod; type MarketCommons = MarketCommons; + type MaxAppeals = MaxAppeals; + type MaxDelegations = MaxDelegations; + type MaxSelectedDraws = MaxSelectedDraws; + type MaxCourtParticipants = MaxCourtParticipants; + type MinJurorStake = MinJurorStake; + type MonetaryGovernanceOrigin = EnsureRoot; type PalletId = CourtPalletId; type Random = RandomnessCollectiveFlip; - type StakeWeight = StakeWeight; + type RequestInterval = RequestInterval; + type Slash = Treasury; type TreasuryPalletId = TreasuryPalletId; type WeightInfo = zrml_court::weights::WeightInfo; } @@ -317,15 +301,21 @@ impl zrml_rikiddo::Config for Runtime { } impl zrml_simple_disputes::Config for Runtime { + type AssetManager = AssetManager; type RuntimeEvent = RuntimeEvent; + type OutcomeBond = OutcomeBond; + type OutcomeFactor = OutcomeFactor; type DisputeResolution = prediction_markets::Pallet; type MarketCommons = MarketCommons; + type MaxDisputes = MaxDisputes; type PalletId = SimpleDisputesPalletId; + type WeightInfo = zrml_simple_disputes::weights::WeightInfo; } -#[cfg(feature = "with-global-disputes")] impl zrml_global_disputes::Config for Runtime { + type AddOutcomePeriod = AddOutcomePeriod; type RuntimeEvent = RuntimeEvent; + type DisputeResolution = prediction_markets::Pallet; type MarketCommons = MarketCommons; type Currency = Balances; type GlobalDisputeLockId = GlobalDisputeLockId; @@ -334,6 +324,7 @@ impl zrml_global_disputes::Config for Runtime { type MaxOwners = MaxOwners; type MinOutcomeVoteAmount = MinOutcomeVoteAmount; type RemoveKeysLimit = RemoveKeysLimit; + type GdVotingPeriod = GdVotingPeriod; type VotingOutcomeFee = VotingOutcomeFee; type WeightInfo = zrml_global_disputes::weights::WeightInfo; } @@ -458,11 +449,13 @@ impl ExtBuilder { pub fn run_to_block(n: BlockNumber) { while System::block_number() < n { Balances::on_finalize(System::block_number()); + Court::on_finalize(System::block_number()); PredictionMarkets::on_finalize(System::block_number()); System::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); PredictionMarkets::on_initialize(System::block_number()); + Court::on_initialize(System::block_number()); Balances::on_initialize(System::block_number()); } } diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index 83e772fb7..2269a8455 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2023 Forecasting Technologies Ltd. +// Copyright 2022-2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -19,23 +19,30 @@ #![cfg(all(feature = "mock", test))] #![allow(clippy::reversed_empty_ranges)] +extern crate alloc; + use crate::{ - default_dispute_bond, mock::*, Config, Disputes, Error, Event, LastTimeFrame, MarketIdsForEdit, - MarketIdsPerCloseBlock, MarketIdsPerDisputeBlock, MarketIdsPerOpenBlock, - MarketIdsPerReportBlock, TimeFrame, + mock::*, Config, Error, Event, LastTimeFrame, MarketIdsForEdit, MarketIdsPerCloseBlock, + MarketIdsPerDisputeBlock, MarketIdsPerOpenBlock, MarketIdsPerReportBlock, TimeFrame, }; +use alloc::collections::BTreeMap; use core::ops::{Range, RangeInclusive}; use frame_support::{ assert_err, assert_noop, assert_ok, dispatch::{DispatchError, DispatchResultWithPostInfo}, traits::{NamedReservableCurrency, OnInitialize}, }; +use sp_runtime::{traits::BlakeTwo256, Perquintill}; use test_case::test_case; +use zrml_court::{types::*, Error as CError}; use orml_traits::{MultiCurrency, MultiReservableCurrency}; -use sp_runtime::traits::{AccountIdConversion, SaturatedConversion, Zero}; +use sp_runtime::traits::{AccountIdConversion, Hash, SaturatedConversion, Zero}; use zeitgeist_primitives::{ - constants::mock::{DisputeFactor, OutsiderBond, BASE, CENT, MILLISECS_PER_BLOCK}, + constants::mock::{ + MaxAppeals, MaxSelectedDraws, MinJurorStake, OutcomeBond, OutcomeFactor, OutsiderBond, + BASE, CENT, MILLISECS_PER_BLOCK, + }, traits::Swaps as SwapsPalletApi, types::{ AccountIdTest, Asset, Balance, BlockNumber, Bond, Deadlines, Market, MarketBonds, @@ -43,12 +50,14 @@ use zeitgeist_primitives::{ Moment, MultiHash, OutcomeReport, PoolStatus, ScalarPosition, ScoringRule, }, }; -use zrml_authorized::Error as AuthorizedError; +use zrml_global_disputes::{ + types::{OutcomeInfo, Possession}, + GlobalDisputesPalletApi, Outcomes, PossessionOf, +}; use zrml_market_commons::MarketCommonsPalletApi; use zrml_swaps::Pools; - -const SENTINEL_AMOUNT: u128 = BASE; const LIQUIDITY: u128 = 100 * BASE; +const SENTINEL_AMOUNT: u128 = BASE; fn get_deadlines() -> Deadlines<::BlockNumber> { Deadlines { @@ -602,11 +611,7 @@ fn admin_destroy_market_correctly_slashes_permissionless_market_disputed() { OutcomeReport::Categorical(1) )); run_to_block(grace_period + 2); - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - 0, - OutcomeReport::Categorical(0) - )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); assert_ok!(AssetManager::deposit(Asset::Ztg, &ALICE, 2 * SENTINEL_AMOUNT)); assert_ok!(Balances::reserve_named( &PredictionMarkets::reserve_id(), @@ -632,7 +637,7 @@ fn admin_destroy_market_correctly_slashes_permissionless_market_disputed() { } #[test] -fn admin_destroy_market_correctly_unreserves_dispute_bonds() { +fn admin_destroy_market_correctly_slashes_dispute_bonds() { ExtBuilder::default().build().execute_with(|| { let end = 2; simple_create_categorical_market( @@ -652,12 +657,13 @@ fn admin_destroy_market_correctly_unreserves_dispute_bonds() { OutcomeReport::Categorical(1) )); run_to_block(grace_period + 2); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(0) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(DAVE), 0, OutcomeReport::Categorical(1) @@ -686,13 +692,13 @@ fn admin_destroy_market_correctly_unreserves_dispute_bonds() { ); assert_eq!( Balances::free_balance(CHARLIE), - balance_free_before_charlie + default_dispute_bond::(0) + balance_free_before_charlie + zrml_simple_disputes::default_outcome_bond::(0) ); assert_eq!( Balances::free_balance(DAVE), - balance_free_before_dave + default_dispute_bond::(1), + balance_free_before_dave + zrml_simple_disputes::default_outcome_bond::(1), ); - assert!(Disputes::::get(market_id).is_empty()); + assert!(zrml_simple_disputes::Disputes::::get(market_id).is_empty()); }); } @@ -916,11 +922,7 @@ fn admin_destroy_market_correctly_slashes_advised_market_disputed() { 0, OutcomeReport::Categorical(1) )); - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - 0, - OutcomeReport::Categorical(0) - )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); assert_ok!(AssetManager::deposit(Asset::Ztg, &ALICE, 2 * SENTINEL_AMOUNT)); assert_ok!(Balances::reserve_named( &PredictionMarkets::reserve_id(), @@ -2663,7 +2665,8 @@ fn it_allows_to_dispute_the_outcome_of_a_market() { let dispute_at = grace_period + 2; run_to_block(dispute_at); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(0) @@ -2672,7 +2675,7 @@ fn it_allows_to_dispute_the_outcome_of_a_market() { let market = MarketCommons::market(&0).unwrap(); assert_eq!(market.status, MarketStatus::Disputed); - let disputes = crate::Disputes::::get(0); + let disputes = zrml_simple_disputes::Disputes::::get(0); assert_eq!(disputes.len(), 1); let dispute = &disputes[0]; assert_eq!(dispute.at, dispute_at); @@ -2687,7 +2690,7 @@ fn it_allows_to_dispute_the_outcome_of_a_market() { } #[test] -fn dispute_fails_authority_reported_already() { +fn dispute_fails_disputed_already() { ExtBuilder::default().build().execute_with(|| { let end = 2; assert_ok!(PredictionMarkets::create_market( @@ -2717,19 +2720,175 @@ fn dispute_fails_authority_reported_already() { let dispute_at = grace_period + 2; run_to_block(dispute_at); - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - 0, - OutcomeReport::Categorical(0) + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + + assert_noop!( + PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0), + Error::::InvalidMarketStatus, + ); + }); +} + +#[test] +fn dispute_fails_if_market_not_reported() { + ExtBuilder::default().build().execute_with(|| { + let end = 2; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(::MinCategories::get()), + MarketDisputeMechanism::Authorized, + ScoringRule::CPMM, )); + // Run to the end of the trading phase. + let market = MarketCommons::market(&0).unwrap(); + let grace_period = end + market.deadlines.grace_period; + run_to_block(grace_period + 1); + + // no report happening here... + + let dispute_at = grace_period + 2; + run_to_block(dispute_at); + assert_noop!( - PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - 0, - OutcomeReport::Categorical(1) - ), - AuthorizedError::::OnlyOneDisputeAllowed + PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0), + Error::::InvalidMarketStatus, + ); + }); +} + +#[test] +fn dispute_reserves_dispute_bond() { + ExtBuilder::default().build().execute_with(|| { + let end = 2; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(::MinCategories::get()), + MarketDisputeMechanism::Authorized, + ScoringRule::CPMM, + )); + + // Run to the end of the trading phase. + let market = MarketCommons::market(&0).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) + )); + + let dispute_at = grace_period + 2; + run_to_block(dispute_at); + + let free_charlie_before = Balances::free_balance(CHARLIE); + let reserved_charlie = Balances::reserved_balance(CHARLIE); + assert_eq!(reserved_charlie, 0); + + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + + let free_charlie_after = Balances::free_balance(CHARLIE); + assert_eq!(free_charlie_before - free_charlie_after, DisputeBond::get()); + + let reserved_charlie = Balances::reserved_balance(CHARLIE); + assert_eq!(reserved_charlie, DisputeBond::get()); + }); +} + +#[test] +fn dispute_updates_market() { + ExtBuilder::default().build().execute_with(|| { + let end = 2; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(::MinCategories::get()), + MarketDisputeMechanism::Authorized, + ScoringRule::CPMM, + )); + + // Run to the end of the trading phase. + let market = MarketCommons::market(&0).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) + )); + + let dispute_at = grace_period + 2; + run_to_block(dispute_at); + + let market = MarketCommons::market(&0).unwrap(); + assert_eq!(market.status, MarketStatus::Reported); + assert_eq!(market.bonds.dispute, None); + + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + + let market = MarketCommons::market(&0).unwrap(); + assert_eq!(market.status, MarketStatus::Disputed); + assert_eq!( + market.bonds.dispute, + Some(Bond { who: CHARLIE, value: DisputeBond::get(), is_settled: false }) + ); + }); +} + +#[test] +fn dispute_emits_event() { + ExtBuilder::default().build().execute_with(|| { + let end = 2; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(::MinCategories::get()), + MarketDisputeMechanism::Authorized, + ScoringRule::CPMM, + )); + + // Run to the end of the trading phase. + let market = MarketCommons::market(&0).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) + )); + + let dispute_at = grace_period + 2; + run_to_block(dispute_at); + + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + + System::assert_last_event( + Event::MarketDisputed(0u32.into(), MarketStatus::Disputed).into(), ); }); } @@ -2853,10 +3012,18 @@ fn it_resolves_a_disputed_market() { OutcomeReport::Categorical(0) )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + + let market = MarketCommons::market(&0).unwrap(); + assert_eq!(market.status, MarketStatus::Disputed); + + let charlie_reserved = Balances::reserved_balance(CHARLIE); + assert_eq!(charlie_reserved, DisputeBond::get()); + let dispute_at_0 = report_at + 1; run_to_block(dispute_at_0); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(1) @@ -2865,7 +3032,7 @@ fn it_resolves_a_disputed_market() { let dispute_at_1 = report_at + 2; run_to_block(dispute_at_1); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(DAVE), 0, OutcomeReport::Categorical(0) @@ -2874,7 +3041,7 @@ fn it_resolves_a_disputed_market() { let dispute_at_2 = report_at + 3; run_to_block(dispute_at_2); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(EVE), 0, OutcomeReport::Categorical(1) @@ -2885,16 +3052,16 @@ fn it_resolves_a_disputed_market() { // check everyone's deposits let charlie_reserved = Balances::reserved_balance(CHARLIE); - assert_eq!(charlie_reserved, DisputeBond::get()); + assert_eq!(charlie_reserved, DisputeBond::get() + OutcomeBond::get()); let dave_reserved = Balances::reserved_balance(DAVE); - assert_eq!(dave_reserved, DisputeBond::get() + DisputeFactor::get()); + assert_eq!(dave_reserved, OutcomeBond::get() + OutcomeFactor::get()); let eve_reserved = Balances::reserved_balance(EVE); - assert_eq!(eve_reserved, DisputeBond::get() + 2 * DisputeFactor::get()); + assert_eq!(eve_reserved, OutcomeBond::get() + 2 * OutcomeFactor::get()); // check disputes length - let disputes = crate::Disputes::::get(0); + let disputes = zrml_simple_disputes::Disputes::::get(0); assert_eq!(disputes.len(), 3); // make sure the old mappings of market id per dispute block are erased @@ -2917,7 +3084,7 @@ fn it_resolves_a_disputed_market() { let market_after = MarketCommons::market(&0).unwrap(); assert_eq!(market_after.status, MarketStatus::Resolved); - let disputes = crate::Disputes::::get(0); + let disputes = zrml_simple_disputes::Disputes::::get(0); assert_eq!(disputes.len(), 0); assert_ok!(PredictionMarkets::redeem_shares(RuntimeOrigin::signed(CHARLIE), 0)); @@ -2925,16 +3092,18 @@ fn it_resolves_a_disputed_market() { // Make sure rewards are right: // // Slashed amounts: - // - Dave's reserve: DisputeBond::get() + DisputeFactor::get() + // - Dave's reserve: OutcomeBond::get() + OutcomeFactor::get() // - Alice's oracle bond: OracleBond::get() - // Total: OracleBond::get() + DisputeBond::get() + DisputeFactor::get() + // simple-disputes reward: OutcomeBond::get() + OutcomeFactor::get() + // Charlie gets OracleBond, because the dispute was justified. + // A dispute is justified if the oracle's report is different to the final outcome. // - // Charlie and Eve each receive half of the total slashed amount as bounty. - let dave_reserved = DisputeBond::get() + DisputeFactor::get(); - let total_slashed = OracleBond::get() + dave_reserved; + // Charlie and Eve each receive half of the simple-disputes reward as bounty. + let dave_reserved = OutcomeBond::get() + OutcomeFactor::get(); + let total_slashed = dave_reserved; let charlie_balance = Balances::free_balance(CHARLIE); - assert_eq!(charlie_balance, 1_000 * BASE + total_slashed / 2); + assert_eq!(charlie_balance, 1_000 * BASE + OracleBond::get() + total_slashed / 2); let charlie_reserved_2 = Balances::reserved_balance(CHARLIE); assert_eq!(charlie_reserved_2, 0); let eve_balance = Balances::free_balance(EVE); @@ -2953,6 +3122,7 @@ fn it_resolves_a_disputed_market() { assert!(market_after.bonds.creation.unwrap().is_settled); assert!(market_after.bonds.oracle.unwrap().is_settled); + assert!(market_after.bonds.dispute.unwrap().is_settled); }; ExtBuilder::default().build().execute_with(|| { test(Asset::Ztg); @@ -2963,146 +3133,410 @@ fn it_resolves_a_disputed_market() { }); } -#[test_case(MarketStatus::Active; "active")] -#[test_case(MarketStatus::CollectingSubsidy; "collecting_subsidy")] -#[test_case(MarketStatus::InsufficientSubsidy; "insufficient_subsidy")] -#[test_case(MarketStatus::Closed; "closed")] -#[test_case(MarketStatus::Proposed; "proposed")] -#[test_case(MarketStatus::Resolved; "resolved")] -fn dispute_fails_unless_reported_or_disputed_market(status: MarketStatus) { - ExtBuilder::default().build().execute_with(|| { - // Creates a permissionless market. - simple_create_categorical_market( - Asset::Ztg, +#[test] +fn it_resolves_a_disputed_court_market() { + let test = |base_asset: Asset| { + let juror_0 = 1000; + let juror_1 = 1001; + let juror_2 = 1002; + let juror_3 = 1003; + let juror_4 = 1004; + let juror_5 = 1005; + + for j in &[juror_0, juror_1, juror_2, juror_3, juror_4, juror_5] { + let amount = MinJurorStake::get() + *j; + assert_ok!(AssetManager::deposit(Asset::Ztg, j, amount + SENTINEL_AMOUNT)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(*j), amount)); + } + + // just to have enough jurors for the dispute + for j in 1006..(1006 + Court::necessary_draws_weight(0usize) as u32) { + let juror = j as u128; + let amount = MinJurorStake::get() + juror; + assert_ok!(AssetManager::deposit(Asset::Ztg, &juror, amount + SENTINEL_AMOUNT)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(juror), amount)); + } + + let end = 2; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + base_asset, + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), MarketCreation::Permissionless, - 0..1, + MarketType::Categorical(::MinCategories::get()), + MarketDisputeMechanism::Court, ScoringRule::CPMM, + )); + + let market_id = 0; + let market = MarketCommons::market(&0).unwrap(); + + let report_at = end + market.deadlines.grace_period + 1; + run_to_block(report_at); + + assert_ok!(PredictionMarkets::report( + RuntimeOrigin::signed(BOB), + market_id, + OutcomeReport::Categorical(0) + )); + + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), market_id,)); + + let court = zrml_court::Courts::::get(market_id).unwrap(); + let vote_start = court.round_ends.pre_vote + 1; + + run_to_block(vote_start); + + // overwrite draws to disregard randomness + zrml_court::SelectedDraws::::remove(market_id); + let mut draws = zrml_court::SelectedDraws::::get(market_id); + for juror in &[juror_0, juror_1, juror_2, juror_3, juror_4, juror_5] { + let draw = Draw { + court_participant: *juror, + weight: 1, + vote: Vote::Drawn, + slashable: MinJurorStake::get(), + }; + let index = draws + .binary_search_by_key(juror, |draw| draw.court_participant) + .unwrap_or_else(|j| j); + draws.try_insert(index, draw).unwrap(); + } + let old_draws = draws.clone(); + zrml_court::SelectedDraws::::insert(market_id, draws); + + let salt = ::Hash::default(); + + // outcome_0 is the plurality decision => right outcome + let outcome_0 = OutcomeReport::Categorical(0); + let vote_item_0 = VoteItem::Outcome(outcome_0.clone()); + // outcome_1 is the wrong outcome + let outcome_1 = OutcomeReport::Categorical(1); + let vote_item_1 = VoteItem::Outcome(outcome_1); + + let commitment_0 = BlakeTwo256::hash_of(&(juror_0, vote_item_0.clone(), salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(juror_0), market_id, commitment_0)); + + // juror_1 votes for non-plurality outcome => slashed later + let commitment_1 = BlakeTwo256::hash_of(&(juror_1, vote_item_1.clone(), salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(juror_1), market_id, commitment_1)); + + let commitment_2 = BlakeTwo256::hash_of(&(juror_2, vote_item_0.clone(), salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(juror_2), market_id, commitment_2)); + + let commitment_3 = BlakeTwo256::hash_of(&(juror_3, vote_item_0.clone(), salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(juror_3), market_id, commitment_3)); + + // juror_4 fails to vote in time + + let commitment_5 = BlakeTwo256::hash_of(&(juror_5, vote_item_0.clone(), salt)); + assert_ok!(Court::vote(RuntimeOrigin::signed(juror_5), market_id, commitment_5)); + + // juror_3 is denounced by juror_0 => slashed later + assert_ok!(Court::denounce_vote( + RuntimeOrigin::signed(juror_0), + market_id, + juror_3, + vote_item_0.clone(), + salt + )); + + let aggregation_start = court.round_ends.vote + 1; + run_to_block(aggregation_start); + + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(juror_0), + market_id, + vote_item_0.clone(), + salt + )); + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(juror_1), + market_id, + vote_item_1, + salt + )); + + let wrong_salt = BlakeTwo256::hash_of(&69); + assert_noop!( + Court::reveal_vote( + RuntimeOrigin::signed(juror_2), + market_id, + vote_item_0.clone(), + wrong_salt + ), + CError::::CommitmentHashMismatch ); + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(juror_2), + market_id, + vote_item_0.clone(), + salt + )); - assert_ok!(MarketCommons::mutate_market(&0, |market_inner| { - market_inner.status = status; - Ok(()) - })); + assert_noop!( + Court::reveal_vote( + RuntimeOrigin::signed(juror_3), + market_id, + vote_item_0.clone(), + salt + ), + CError::::VoteAlreadyDenounced + ); assert_noop!( - PredictionMarkets::dispute( - RuntimeOrigin::signed(EVE), - 0, - OutcomeReport::Categorical(1) + Court::reveal_vote( + RuntimeOrigin::signed(juror_4), + market_id, + vote_item_0.clone(), + salt ), - Error::::InvalidMarketStatus + CError::::JurorDidNotVote ); + + // juror_5 fails to reveal in time + + let resolve_at = court.round_ends.appeal; + let market_ids = MarketIdsPerDisputeBlock::::get(resolve_at); + assert_eq!(market_ids.len(), 1); + + run_blocks(resolve_at); + + let market_after = MarketCommons::market(&0).unwrap(); + assert_eq!(market_after.status, MarketStatus::Resolved); + assert_eq!(market_after.resolved_outcome, Some(outcome_0)); + let court_after = zrml_court::Courts::::get(market_id).unwrap(); + assert_eq!(court_after.status, CourtStatus::Closed { winner: vote_item_0 }); + + let free_juror_0_before = Balances::free_balance(juror_0); + let free_juror_1_before = Balances::free_balance(juror_1); + let free_juror_2_before = Balances::free_balance(juror_2); + let free_juror_3_before = Balances::free_balance(juror_3); + let free_juror_4_before = Balances::free_balance(juror_4); + let free_juror_5_before = Balances::free_balance(juror_5); + + assert_ok!(Court::reassign_court_stakes(RuntimeOrigin::signed(juror_0), market_id)); + + let free_juror_0_after = Balances::free_balance(juror_0); + let slashable_juror_0 = + old_draws.iter().find(|draw| draw.court_participant == juror_0).unwrap().slashable; + let free_juror_1_after = Balances::free_balance(juror_1); + let slashable_juror_1 = + old_draws.iter().find(|draw| draw.court_participant == juror_1).unwrap().slashable; + let free_juror_2_after = Balances::free_balance(juror_2); + let slashable_juror_2 = + old_draws.iter().find(|draw| draw.court_participant == juror_2).unwrap().slashable; + let free_juror_3_after = Balances::free_balance(juror_3); + let slashable_juror_3 = + old_draws.iter().find(|draw| draw.court_participant == juror_3).unwrap().slashable; + let free_juror_4_after = Balances::free_balance(juror_4); + let slashable_juror_4 = + old_draws.iter().find(|draw| draw.court_participant == juror_4).unwrap().slashable; + let free_juror_5_after = Balances::free_balance(juror_5); + let slashable_juror_5 = + old_draws.iter().find(|draw| draw.court_participant == juror_5).unwrap().slashable; + + let mut total_slashed = 0; + // juror_1 voted for the wrong outcome => slashed + assert_eq!(free_juror_1_before - free_juror_1_after, slashable_juror_1); + total_slashed += slashable_juror_1; + // juror_3 was denounced by juror_0 => slashed + assert_eq!(free_juror_3_before - free_juror_3_after, slashable_juror_3); + total_slashed += slashable_juror_3; + // juror_4 failed to vote => slashed + assert_eq!(free_juror_4_before - free_juror_4_after, slashable_juror_4); + total_slashed += slashable_juror_4; + // juror_5 failed to reveal => slashed + assert_eq!(free_juror_5_before - free_juror_5_after, slashable_juror_5); + total_slashed += slashable_juror_5; + // juror_0 and juror_2 voted for the right outcome => rewarded + let total_winner_stake = slashable_juror_0 + slashable_juror_2; + let juror_0_share = Perquintill::from_rational(slashable_juror_0, total_winner_stake); + assert_eq!(free_juror_0_after, free_juror_0_before + juror_0_share * total_slashed); + let juror_2_share = Perquintill::from_rational(slashable_juror_2, total_winner_stake); + assert_eq!(free_juror_2_after, free_juror_2_before + juror_2_share * total_slashed); + }; + ExtBuilder::default().build().execute_with(|| { + test(Asset::Ztg); + }); + #[cfg(feature = "parachain")] + ExtBuilder::default().build().execute_with(|| { + test(Asset::ForeignAsset(100)); }); } +fn simulate_appeal_cycle(market_id: MarketId) { + let court = zrml_court::Courts::::get(market_id).unwrap(); + let vote_start = court.round_ends.pre_vote + 1; + + run_to_block(vote_start); + + let salt = ::Hash::default(); + + let wrong_outcome = OutcomeReport::Categorical(1); + let wrong_vote_item = VoteItem::Outcome(wrong_outcome); + + let draws = zrml_court::SelectedDraws::::get(market_id); + for draw in &draws { + let commitment = + BlakeTwo256::hash_of(&(draw.court_participant, wrong_vote_item.clone(), salt)); + assert_ok!(Court::vote( + RuntimeOrigin::signed(draw.court_participant), + market_id, + commitment + )); + } + + let aggregation_start = court.round_ends.vote + 1; + run_to_block(aggregation_start); + + for draw in draws { + assert_ok!(Court::reveal_vote( + RuntimeOrigin::signed(draw.court_participant), + market_id, + wrong_vote_item.clone(), + salt, + )); + } + + let resolve_at = court.round_ends.appeal; + let market_ids = MarketIdsPerDisputeBlock::::get(resolve_at); + assert_eq!(market_ids.len(), 1); + + run_to_block(resolve_at - 1); + + let market_after = MarketCommons::market(&0).unwrap(); + assert_eq!(market_after.status, MarketStatus::Disputed); +} + #[test] -fn start_global_dispute_works() { - ExtBuilder::default().build().execute_with(|| { +fn it_appeals_a_court_market_to_global_dispute() { + let test = |base_asset: Asset| { + let mut free_before = BTreeMap::new(); + let jurors = 1000..(1000 + MaxSelectedDraws::get() as u128); + for j in jurors { + let amount = MinJurorStake::get() + j; + assert_ok!(AssetManager::deposit(Asset::Ztg, &j, amount + SENTINEL_AMOUNT)); + assert_ok!(Court::join_court(RuntimeOrigin::signed(j), amount)); + free_before.insert(j, Balances::free_balance(j)); + } + let end = 2; assert_ok!(PredictionMarkets::create_market( RuntimeOrigin::signed(ALICE), - Asset::Ztg, + base_asset, BOB, - MarketPeriod::Block(0..2), + MarketPeriod::Block(0..end), get_deadlines(), gen_metadata(2), MarketCreation::Permissionless, - MarketType::Categorical(::MaxDisputes::get() + 1), - MarketDisputeMechanism::SimpleDisputes, + MarketType::Categorical(::MinCategories::get()), + MarketDisputeMechanism::Court, ScoringRule::CPMM, )); - let market_id = MarketCommons::latest_market_id().unwrap(); - let market = MarketCommons::market(&market_id).unwrap(); - let grace_period = market.deadlines.grace_period; - run_to_block(end + grace_period + 1); + let market_id = 0; + let market = MarketCommons::market(&0).unwrap(); + + let report_at = end + market.deadlines.grace_period + 1; + run_to_block(report_at); + assert_ok!(PredictionMarkets::report( RuntimeOrigin::signed(BOB), market_id, OutcomeReport::Categorical(0) )); - let dispute_at_0 = end + grace_period + 2; - run_to_block(dispute_at_0); - for i in 1..=::MaxDisputes::get() { - if i == 1 { - #[cfg(feature = "with-global-disputes")] - assert_noop!( - PredictionMarkets::start_global_dispute( - RuntimeOrigin::signed(CHARLIE), - market_id - ), - Error::::InvalidMarketStatus - ); - } else { - #[cfg(feature = "with-global-disputes")] - assert_noop!( - PredictionMarkets::start_global_dispute( - RuntimeOrigin::signed(CHARLIE), - market_id - ), - Error::::MaxDisputesNeeded - ); - } - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - market_id, - OutcomeReport::Categorical(i.saturated_into()) - )); - run_blocks(1); - let market = MarketCommons::market(&market_id).unwrap(); - assert_eq!(market.status, MarketStatus::Disputed); + + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), market_id,)); + + for _ in 0..(MaxAppeals::get() - 1) { + simulate_appeal_cycle(market_id); + assert_ok!(Court::appeal(RuntimeOrigin::signed(BOB), market_id)); } - let disputes = crate::Disputes::::get(market_id); - assert_eq!(disputes.len(), ::MaxDisputes::get() as usize); + let court = zrml_court::Courts::::get(market_id).unwrap(); + let appeals = court.appeals; + assert_eq!(appeals.len(), (MaxAppeals::get() - 1) as usize); - let last_dispute = disputes.last().unwrap(); - let dispute_block = last_dispute.at.saturating_add(market.deadlines.dispute_duration); - let removable_market_ids = MarketIdsPerDisputeBlock::::get(dispute_block); - assert_eq!(removable_market_ids.len(), 1); + assert_noop!( + PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(BOB), market_id), + Error::::MarketDisputeMechanismNotFailed + ); - #[cfg(feature = "with-global-disputes")] - { - use zrml_global_disputes::GlobalDisputesPalletApi; + simulate_appeal_cycle(market_id); + assert_ok!(Court::appeal(RuntimeOrigin::signed(BOB), market_id)); - let now = >::block_number(); - assert_ok!(PredictionMarkets::start_global_dispute( - RuntimeOrigin::signed(CHARLIE), - market_id - )); + assert_noop!( + Court::appeal(RuntimeOrigin::signed(BOB), market_id), + CError::::MaxAppealsReached + ); - // report check - assert_eq!( - GlobalDisputes::get_voting_outcome_info(&market_id, &OutcomeReport::Categorical(0)), - Some((Zero::zero(), vec![BOB])), - ); - for i in 1..=::MaxDisputes::get() { - let dispute_bond = crate::default_dispute_bond::((i - 1).into()); - assert_eq!( - GlobalDisputes::get_voting_outcome_info( - &market_id, - &OutcomeReport::Categorical(i.saturated_into()) - ), - Some((dispute_bond, vec![CHARLIE])), - ); - } + assert!(!GlobalDisputes::does_exist(&market_id)); - // remove_last_dispute_from_market_ids_per_dispute_block works - let removable_market_ids = MarketIdsPerDisputeBlock::::get(dispute_block); - assert_eq!(removable_market_ids.len(), 0); + assert_ok!(PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(BOB), market_id)); - let market_ids = MarketIdsPerDisputeBlock::::get( - now + ::GlobalDisputePeriod::get(), - ); - assert_eq!(market_ids, vec![market_id]); - assert!(GlobalDisputes::is_started(&market_id)); - System::assert_last_event(Event::GlobalDisputeStarted(market_id).into()); + let now = >::block_number(); - assert_noop!( - PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(CHARLIE), market_id), - Error::::GlobalDisputeAlreadyStarted - ); - } + assert!(GlobalDisputes::does_exist(&market_id)); + System::assert_last_event(Event::GlobalDisputeStarted(market_id).into()); + + // report check + let possession: PossessionOf = + Possession::Shared { owners: frame_support::BoundedVec::try_from(vec![BOB]).unwrap() }; + let outcome_info = OutcomeInfo { outcome_sum: Zero::zero(), possession }; + assert_eq!( + Outcomes::::get(market_id, &OutcomeReport::Categorical(0)).unwrap(), + outcome_info + ); + + let add_outcome_end = now + GlobalDisputes::get_add_outcome_period(); + let vote_end = add_outcome_end + GlobalDisputes::get_vote_period(); + let market_ids = MarketIdsPerDisputeBlock::::get(vote_end); + assert_eq!(market_ids, vec![market_id]); + assert!(GlobalDisputes::is_active(&market_id)); + + assert_noop!( + PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(CHARLIE), market_id), + Error::::GlobalDisputeExistsAlready + ); + }; + ExtBuilder::default().build().execute_with(|| { + test(Asset::Ztg); + }); + #[cfg(feature = "parachain")] + ExtBuilder::default().build().execute_with(|| { + test(Asset::ForeignAsset(100)); + }); +} + +#[test_case(MarketStatus::Active; "active")] +#[test_case(MarketStatus::CollectingSubsidy; "collecting_subsidy")] +#[test_case(MarketStatus::InsufficientSubsidy; "insufficient_subsidy")] +#[test_case(MarketStatus::Closed; "closed")] +#[test_case(MarketStatus::Proposed; "proposed")] +#[test_case(MarketStatus::Resolved; "resolved")] +fn dispute_fails_unless_reported_or_disputed_market(status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + // Creates a permissionless market. + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..1, + ScoringRule::CPMM, + ); + + assert_ok!(MarketCommons::mutate_market(&0, |market_inner| { + market_inner.status = status; + Ok(()) + })); + + assert_noop!( + PredictionMarkets::dispute(RuntimeOrigin::signed(EVE), 0), + Error::::InvalidMarketStatus + ); }); } @@ -3136,16 +3570,11 @@ fn start_global_dispute_fails_on_wrong_mdm() { run_to_block(dispute_at_0); // only one dispute allowed for authorized mdm - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - market_id, - OutcomeReport::Categorical(1u32.saturated_into()) - )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), market_id,)); run_blocks(1); let market = MarketCommons::market(&market_id).unwrap(); assert_eq!(market.status, MarketStatus::Disputed); - #[cfg(feature = "with-global-disputes")] assert_noop!( PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(CHARLIE), market_id), Error::::InvalidDisputeMechanism @@ -3153,25 +3582,6 @@ fn start_global_dispute_fails_on_wrong_mdm() { }); } -#[test] -fn start_global_dispute_works_without_feature() { - ExtBuilder::default().build().execute_with(|| { - let non_market_id = 0; - - #[cfg(not(feature = "with-global-disputes"))] - assert_noop!( - PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(CHARLIE), non_market_id), - Error::::GlobalDisputesDisabled - ); - - #[cfg(feature = "with-global-disputes")] - assert_noop!( - PredictionMarkets::start_global_dispute(RuntimeOrigin::signed(CHARLIE), non_market_id), - zrml_market_commons::Error::::MarketDoesNotExist - ); - }); -} - #[test] fn it_allows_to_redeem_shares() { let test = |base_asset: Asset| { @@ -3683,19 +4093,20 @@ fn full_scalar_market_lifecycle() { assert_eq!(report.outcome, OutcomeReport::Scalar(100)); // dispute - assert_ok!(PredictionMarkets::dispute( + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(DAVE), 0)); + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(DAVE), 0, OutcomeReport::Scalar(25) )); - let disputes = crate::Disputes::::get(0); + let disputes = zrml_simple_disputes::Disputes::::get(0); assert_eq!(disputes.len(), 1); run_blocks(market.deadlines.dispute_duration); let market_after_resolve = MarketCommons::market(&0).unwrap(); assert_eq!(market_after_resolve.status, MarketStatus::Resolved); - let disputes = crate::Disputes::::get(0); + let disputes = zrml_simple_disputes::Disputes::::get(0); assert_eq!(disputes.len(), 0); // give EVE some shares @@ -3910,11 +4321,7 @@ fn authorized_correctly_resolves_disputed_market() { let dispute_at = grace_period + 1 + 1; run_to_block(dispute_at); - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(CHARLIE), - 0, - OutcomeReport::Categorical(1) - )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); if base_asset == Asset::Ztg { let charlie_balance = AssetManager::free_balance(Asset::Ztg, &CHARLIE); @@ -3945,10 +4352,6 @@ fn authorized_correctly_resolves_disputed_market() { let charlie_reserved = Balances::reserved_balance(CHARLIE); assert_eq!(charlie_reserved, DisputeBond::get()); - // check disputes length - let disputes = crate::Disputes::::get(0); - assert_eq!(disputes.len(), 1); - let market_ids_1 = MarketIdsPerDisputeBlock::::get( dispute_at + ::CorrectionPeriod::get(), ); @@ -3993,7 +4396,7 @@ fn authorized_correctly_resolves_disputed_market() { let market_after = MarketCommons::market(&0).unwrap(); assert_eq!(market_after.status, MarketStatus::Resolved); - let disputes = crate::Disputes::::get(0); + let disputes = zrml_simple_disputes::Disputes::::get(0); assert_eq!(disputes.len(), 0); assert_ok!(PredictionMarkets::redeem_shares(RuntimeOrigin::signed(CHARLIE), 0)); @@ -4316,13 +4719,21 @@ fn outsider_reports_wrong_outcome() { let dispute_at_0 = report_at + 1; run_to_block(dispute_at_0); - assert_ok!(PredictionMarkets::dispute( - RuntimeOrigin::signed(EVE), + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(EVE), 0,)); + check_reserve(&EVE, DisputeBond::get()); + + assert_ok!(SimpleDisputes::suggest_outcome( + RuntimeOrigin::signed(DAVE), 0, OutcomeReport::Categorical(0) )); + let outcome_bond = zrml_simple_disputes::default_outcome_bond::(0); + + check_reserve(&DAVE, outcome_bond); + let eve_balance_before = Balances::free_balance(EVE); + let dave_balance_before = Balances::free_balance(DAVE); // on_resolution called run_blocks(market.deadlines.dispute_duration); @@ -4332,12 +4743,13 @@ fn outsider_reports_wrong_outcome() { check_reserve(&outsider, 0); assert_eq!(Balances::free_balance(outsider), outsider_balance_before); - let dispute_bond = crate::default_dispute_bond::(0usize); - // disputor EVE gets the OracleBond and OutsiderBond and dispute bond + // disputor EVE gets the OracleBond and OutsiderBond and DisputeBond assert_eq!( Balances::free_balance(EVE), - eve_balance_before + dispute_bond + OutsiderBond::get() + OracleBond::get() + eve_balance_before + DisputeBond::get() + OutsiderBond::get() + OracleBond::get() ); + // DAVE gets his outcome bond back + assert_eq!(Balances::free_balance(DAVE), dave_balance_before + outcome_bond); }; ExtBuilder::default().build().execute_with(|| { test(Asset::Ztg); @@ -4468,7 +4880,8 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark 0, OutcomeReport::Categorical(0) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(1) @@ -4518,7 +4931,8 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma 0, OutcomeReport::Categorical(0) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(CHARLIE), 0,)); + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(1) @@ -4567,13 +4981,14 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark 0, OutcomeReport::Categorical(0) )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(EVE), 0,)); // EVE disputes with wrong outcome - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(EVE), 0, OutcomeReport::Categorical(1) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(0) @@ -4626,13 +5041,14 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma 0, OutcomeReport::Categorical(0) )); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(EVE), 0,)); // EVE disputes with wrong outcome - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(EVE), 0, OutcomeReport::Categorical(1) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(CHARLIE), 0, OutcomeReport::Categorical(0) @@ -4687,17 +5103,17 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark 0, OutcomeReport::Categorical(0) )); - let outsider_balance_before = Balances::free_balance(outsider); check_reserve(&outsider, OutsiderBond::get()); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(EVE), 0,)); // EVE disputes with wrong outcome - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(EVE), 0, OutcomeReport::Categorical(1) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(FRED), 0, OutcomeReport::Categorical(0) @@ -4758,17 +5174,17 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma 0, OutcomeReport::Categorical(0) )); - let outsider_balance_before = Balances::free_balance(outsider); check_reserve(&outsider, OutsiderBond::get()); + assert_ok!(PredictionMarkets::dispute(RuntimeOrigin::signed(EVE), 0,)); // EVE disputes with wrong outcome - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(EVE), 0, OutcomeReport::Categorical(1) )); - assert_ok!(PredictionMarkets::dispute( + assert_ok!(SimpleDisputes::suggest_outcome( RuntimeOrigin::signed(FRED), 0, OutcomeReport::Categorical(0) @@ -5111,6 +5527,7 @@ fn create_market_fails_if_market_duration_is_too_long_in_moments() { creation: Some(Bond::new(ALICE, ::AdvisoryBond::get())), oracle: Some(Bond::new(ALICE, ::OracleBond::get())), outsider: None, + dispute: None, } )] #[test_case( @@ -5121,6 +5538,7 @@ fn create_market_fails_if_market_duration_is_too_long_in_moments() { creation: Some(Bond::new(ALICE, ::ValidityBond::get())), oracle: Some(Bond::new(ALICE, ::OracleBond::get())), outsider: None, + dispute: None, } )] fn create_market_sets_the_correct_market_parameters_and_reserves_the_correct_amount( diff --git a/zrml/prediction-markets/src/weights.rs b/zrml/prediction-markets/src/weights.rs index d74189b2e..43e0092af 100644 --- a/zrml/prediction-markets/src/weights.rs +++ b/zrml/prediction-markets/src/weights.rs @@ -46,7 +46,7 @@ use frame_support::{traits::Get, weights::Weight}; /// Trait containing the required functions for weight retrival within /// zrml_prediction_markets (automatically generated) pub trait WeightInfoZeitgeist { - fn admin_destroy_disputed_market(a: u32, d: u32, o: u32, c: u32, r: u32) -> Weight; + fn admin_destroy_disputed_market(a: u32, o: u32, c: u32, r: u32) -> Weight; fn admin_destroy_reported_market(a: u32, o: u32, c: u32, r: u32) -> Weight; fn admin_move_market_to_closed(o: u32, c: u32) -> Weight; fn admin_move_market_to_resolved_scalar_reported(r: u32) -> Weight; @@ -84,32 +84,29 @@ pub trait WeightInfoZeitgeist { pub struct WeightInfo(PhantomData); impl WeightInfoZeitgeist for WeightInfo { // Storage: MarketCommons Markets (r:1 w:1) - // Storage: Balances Reserves (r:7 w:7) - // Storage: System Account (r:8 w:8) + // Storage: Balances Reserves (r:2 w:2) + // Storage: System Account (r:3 w:3) // Storage: MarketCommons MarketPool (r:1 w:1) // Storage: Swaps Pools (r:1 w:1) // Storage: Tokens Accounts (r:2 w:2) // Storage: Tokens TotalIssuance (r:2 w:2) - // Storage: PredictionMarkets Disputes (r:1 w:1) + // Storage: Authorized AuthorizedOutcomeReports (r:1 w:1) // Storage: PredictionMarkets MarketIdsPerDisputeBlock (r:1 w:1) - fn admin_destroy_disputed_market(a: u32, d: u32, o: u32, c: u32, r: u32) -> Weight { - Weight::from_ref_time(97_831_295) - // Standard Error: 44_990 - .saturating_add(Weight::from_ref_time(32_382_881).saturating_mul(a.into())) - // Standard Error: 514_058 - .saturating_add(Weight::from_ref_time(36_068_793).saturating_mul(d.into())) - // Standard Error: 44_768 - .saturating_add(Weight::from_ref_time(546_560).saturating_mul(o.into())) - // Standard Error: 44_768 - .saturating_add(Weight::from_ref_time(611_992).saturating_mul(c.into())) - // Standard Error: 44_768 - .saturating_add(Weight::from_ref_time(66_516).saturating_mul(r.into())) - .saturating_add(T::DbWeight::get().reads(8)) + fn admin_destroy_disputed_market(a: u32, o: u32, c: u32, r: u32) -> Weight { + Weight::from_ref_time(138_848_000) + // Standard Error: 35_000 + .saturating_add(Weight::from_ref_time(20_922_000)) + .saturating_mul(a.into()) + // Standard Error: 34_000 + .saturating_add(Weight::from_ref_time(1_091_000).saturating_mul(o.into())) + // Standard Error: 34_000 + .saturating_add(Weight::from_ref_time(984_000).saturating_mul(c.into())) + // Standard Error: 34_000 + .saturating_add(Weight::from_ref_time(1_026_000).saturating_mul(r.into())) + .saturating_add(T::DbWeight::get().reads(10)) .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(a.into()))) - .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(d.into()))) - .saturating_add(T::DbWeight::get().writes(8)) + .saturating_add(T::DbWeight::get().writes(10)) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(a.into()))) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(d.into()))) } // Storage: MarketCommons Markets (r:1 w:1) // Storage: Balances Reserves (r:1 w:1) @@ -284,16 +281,23 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(a.into()))) } // Storage: MarketCommons Markets (r:1 w:0) - // Storage: PredictionMarkets Disputes (r:1 w:0) // Storage: GlobalDisputes Winners (r:1 w:1) - // Storage: GlobalDisputes Outcomes (r:7 w:7) - // Storage: PredictionMarkets MarketIdsPerDisputeBlock (r:2 w:2) - fn start_global_dispute(m: u32, _n: u32) -> Weight { - Weight::from_ref_time(154_547_825) - // Standard Error: 4_967 - .saturating_add(Weight::from_ref_time(54_777).saturating_mul(m.into())) - .saturating_add(T::DbWeight::get().reads(12)) - .saturating_add(T::DbWeight::get().writes(10)) + // Storage: Court MarketIdToCourtId (r:1 w:0) + // Storage: Court JurorPool (r:1 w:0) + // Storage: Court Courts (r:1 w:1) + // Storage: Court CourtIdToMarketId (r:1 w:0) + // Storage: Court SelectedDraws (r:1 w:1) + // Storage: Court Jurors (r:30 w:30) + // Storage: GlobalDisputes Outcomes (r:1 w:1) + // Storage: PredictionMarkets MarketIdsPerDisputeBlock (r:1 w:1) + fn start_global_dispute(m: u32, n: u32) -> Weight { + Weight::from_ref_time(203_655_000) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(67_000).saturating_mul(m.into())) + // Standard Error: 1_000 + .saturating_add(Weight::from_ref_time(15_000).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(39)) + .saturating_add(T::DbWeight::get().writes(35)) } // Storage: PredictionMarkets Disputes (r:1 w:1) // Storage: MarketCommons Markets (r:1 w:1) diff --git a/zrml/simple-disputes/Cargo.toml b/zrml/simple-disputes/Cargo.toml index 7d6b13f2a..d84a51431 100644 --- a/zrml/simple-disputes/Cargo.toml +++ b/zrml/simple-disputes/Cargo.toml @@ -2,6 +2,7 @@ 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 } @@ -9,6 +10,8 @@ zeitgeist-primitives = { workspace = true } zrml-market-commons = { workspace = true } [dev-dependencies] +orml-currencies = { workspace = true, features = ["default"] } +orml-tokens = { workspace = true, features = ["default"] } pallet-balances = { workspace = true, features = ["default"] } pallet-timestamp = { workspace = true, features = ["default"] } sp-io = { workspace = true, features = ["default"] } diff --git a/zrml/simple-disputes/src/benchmarks.rs b/zrml/simple-disputes/src/benchmarks.rs new file mode 100644 index 000000000..f5adf90e4 --- /dev/null +++ b/zrml/simple-disputes/src/benchmarks.rs @@ -0,0 +1,177 @@ +// Copyright 2023 Forecasting Technologies LTD. +// Copyright 2021-2022 Zeitgeist PM LLC. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +#![allow( + // Auto-generated code is a no man's land + clippy::arithmetic_side_effects +)] +#![allow(clippy::type_complexity)] +#![cfg(feature = "runtime-benchmarks")] + +use crate::Pallet as SimpleDisputes; + +use super::*; +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; +use frame_support::{ + dispatch::RawOrigin, + traits::{Get, Imbalance}, +}; +use orml_traits::MultiCurrency; +use sp_runtime::traits::{One, Saturating}; +use zrml_market_commons::MarketCommonsPalletApi; + +fn fill_disputes(market_id: MarketIdOf, d: u32) { + for i in 0..d { + let now = >::block_number(); + let disputor = account("disputor", i, 0); + let bond = default_outcome_bond::(i as usize); + T::AssetManager::deposit(Asset::Ztg, &disputor, bond).unwrap(); + let outcome = OutcomeReport::Scalar((2 + i).into()); + SimpleDisputes::::suggest_outcome( + RawOrigin::Signed(disputor).into(), + market_id, + outcome, + ) + .unwrap(); + >::set_block_number(now.saturating_add(T::BlockNumber::one())); + } +} + +benchmarks! { + suggest_outcome { + let d in 1..(T::MaxDisputes::get() - 1); + let r in 1..63; + let e in 1..63; + + let caller: T::AccountId = whitelisted_caller(); + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + let disputes = Disputes::::get(market_id); + let last_dispute = disputes.last().unwrap(); + let auto_resolve = last_dispute.at.saturating_add(market.deadlines.dispute_duration); + for i in 0..r { + let id = T::MarketCommons::push_market(market_mock::()).unwrap(); + T::DisputeResolution::add_auto_resolve(&id, auto_resolve).unwrap(); + } + + let now = >::block_number(); + + let dispute_duration_ends_at_block = + now.saturating_add(market.deadlines.dispute_duration); + for i in 0..e { + let id = T::MarketCommons::push_market(market_mock::()).unwrap(); + T::DisputeResolution::add_auto_resolve(&id, dispute_duration_ends_at_block).unwrap(); + } + + let outcome = OutcomeReport::Scalar(1); + let bond = default_outcome_bond::(T::MaxDisputes::get() as usize); + T::AssetManager::deposit(Asset::Ztg, &caller, bond).unwrap(); + }: _(RawOrigin::Signed(caller.clone()), market_id, outcome) + + on_dispute_weight { + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + }: { + SimpleDisputes::::on_dispute(&market_id, &market).unwrap(); + } + + on_resolution_weight { + let d in 1..T::MaxDisputes::get(); + + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + }: { + SimpleDisputes::::on_resolution(&market_id, &market).unwrap(); + } + + exchange_weight { + let d in 1..T::MaxDisputes::get(); + + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + + let outcome = OutcomeReport::Scalar(1); + let imb = NegativeImbalanceOf::::zero(); + }: { + SimpleDisputes::::exchange(&market_id, &market, &outcome, imb).unwrap(); + } + + get_auto_resolve_weight { + let d in 1..T::MaxDisputes::get(); + + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + }: { + SimpleDisputes::::get_auto_resolve(&market_id, &market); + } + + has_failed_weight { + let d in 1..T::MaxDisputes::get(); + + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + }: { + SimpleDisputes::::has_failed(&market_id, &market).unwrap(); + } + + on_global_dispute_weight { + let d in 1..T::MaxDisputes::get(); + + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + }: { + SimpleDisputes::::on_global_dispute(&market_id, &market).unwrap(); + } + + clear_weight { + let d in 1..T::MaxDisputes::get(); + + let market_id = 0u32.into(); + let market = market_mock::(); + T::MarketCommons::push_market(market.clone()).unwrap(); + + fill_disputes::(market_id, d); + }: { + SimpleDisputes::::clear(&market_id, &market).unwrap(); + } + + impl_benchmark_test_suite!( + SimpleDisputes, + crate::mock::ExtBuilder::default().build(), + crate::mock::Runtime, + ); +} diff --git a/zrml/simple-disputes/src/lib.rs b/zrml/simple-disputes/src/lib.rs index 1db2ff3f9..b4494127a 100644 --- a/zrml/simple-disputes/src/lib.rs +++ b/zrml/simple-disputes/src/lib.rs @@ -21,55 +21,64 @@ extern crate alloc; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarks; mod mock; mod simple_disputes_pallet_api; mod tests; +pub mod weights; pub use pallet::*; pub use simple_disputes_pallet_api::SimpleDisputesPalletApi; +use zeitgeist_primitives::{ + traits::{DisputeApi, DisputeMaxWeightApi, DisputeResolutionApi, ZeitgeistAssetManager}, + types::{ + Asset, GlobalDisputeItem, Market, MarketDispute, MarketDisputeMechanism, MarketStatus, + OutcomeReport, Report, ResultWithWeightInfo, + }, +}; #[frame_support::pallet] mod pallet { - use crate::SimpleDisputesPalletApi; + use super::*; + use crate::{weights::WeightInfoZeitgeist, SimpleDisputesPalletApi}; + use alloc::vec::Vec; use core::marker::PhantomData; use frame_support::{ dispatch::DispatchResult, ensure, - traits::{Currency, Get, Hooks, IsType}, - PalletId, - }; - use sp_runtime::{traits::Saturating, DispatchError}; - use zeitgeist_primitives::{ - traits::{DisputeApi, DisputeResolutionApi}, - types::{ - Asset, Market, MarketDispute, MarketDisputeMechanism, MarketStatus, OutcomeReport, + pallet_prelude::{ + Blake2_128Concat, ConstU32, DispatchResultWithPostInfo, StorageMap, ValueQuery, Weight, }, + traits::{Currency, Get, Hooks, Imbalance, IsType, NamedReservableCurrency}, + transactional, BoundedVec, PalletId, + }; + use frame_system::pallet_prelude::*; + use orml_traits::currency::NamedMultiReservableCurrency; + use sp_runtime::{ + traits::{CheckedDiv, Saturating}, + DispatchError, SaturatedConversion, }; - use zrml_market_commons::MarketCommonsPalletApi; - - type BalanceOf = - as Currency<::AccountId>>::Balance; - pub(crate) type CurrencyOf = - <::MarketCommons as MarketCommonsPalletApi>::Currency; - pub(crate) type MarketIdOf = - <::MarketCommons as MarketCommonsPalletApi>::MarketId; - pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; - pub(crate) type MarketOf = Market< - ::AccountId, - BalanceOf, - ::BlockNumber, - MomentOf, - Asset>, - >; - #[pallet::call] - impl Pallet {} + use zrml_market_commons::MarketCommonsPalletApi; #[pallet::config] pub trait Config: frame_system::Config { + /// Shares of outcome assets and native currency + type AssetManager: ZeitgeistAssetManager< + Self::AccountId, + Balance = as Currency>::Balance, + CurrencyId = Asset>, + ReserveIdentifier = [u8; 8], + >; + /// Event type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The base amount of currency that must be bonded in order to create a dispute. + #[pallet::constant] + type OutcomeBond: Get>; + type DisputeResolution: DisputeResolutionApi< AccountId = Self::AccountId, BlockNumber = Self::BlockNumber, @@ -77,12 +86,70 @@ mod pallet { Moment = MomentOf, >; + /// The additional amount of currency that must be bonded when creating a subsequent + /// dispute. + #[pallet::constant] + type OutcomeFactor: Get>; + /// The identifier of individual markets. type MarketCommons: MarketCommonsPalletApi; + /// The maximum number of disputes allowed on any single market. + #[pallet::constant] + type MaxDisputes: Get; + /// The pallet identifier. #[pallet::constant] type PalletId: Get; + + /// Weights generated by benchmarks + type WeightInfo: WeightInfoZeitgeist; + } + + pub(crate) type BalanceOf = + as Currency<::AccountId>>::Balance; + pub(crate) type CurrencyOf = + <::MarketCommons as MarketCommonsPalletApi>::Currency; + pub(crate) type NegativeImbalanceOf = + as Currency<::AccountId>>::NegativeImbalance; + pub type MarketIdOf = <::MarketCommons as MarketCommonsPalletApi>::MarketId; + pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; + pub(crate) type MarketOf = Market< + ::AccountId, + BalanceOf, + ::BlockNumber, + MomentOf, + Asset>, + >; + pub(crate) type DisputesOf = BoundedVec< + MarketDispute< + ::AccountId, + ::BlockNumber, + BalanceOf, + >, + ::MaxDisputes, + >; + pub type CacheSize = ConstU32<64>; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + /// For each market, this holds the dispute information for each dispute that's + /// been issued. + #[pallet::storage] + pub type Disputes = + StorageMap<_, Blake2_128Concat, MarketIdOf, DisputesOf, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event + where + T: Config, + { + OutcomeReserved { + market_id: MarketIdOf, + dispute: MarketDispute>, + }, } #[pallet::error] @@ -92,22 +159,98 @@ mod pallet { InvalidMarketStatus, /// On dispute or resolution, someone tried to pass a non-simple-disputes market type MarketDoesNotHaveSimpleDisputesMechanism, + StorageOverflow, + OutcomeMismatch, + CannotDisputeSameOutcome, + MarketIsNotReported, + /// The maximum number of disputes has been reached. + MaxDisputesReached, } - #[pallet::event] - pub enum Event - where - T: Config, {} - #[pallet::hooks] impl Hooks for Pallet {} - impl Pallet - where - T: Config, - { + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::suggest_outcome( + T::MaxDisputes::get(), + CacheSize::get(), + CacheSize::get(), + ))] + #[transactional] + pub fn suggest_outcome( + origin: OriginFor, + #[pallet::compact] market_id: MarketIdOf, + outcome: OutcomeReport, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let market = T::MarketCommons::market(&market_id)?; + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, + Error::::MarketDoesNotHaveSimpleDisputesMechanism + ); + ensure!(market.status == MarketStatus::Disputed, Error::::InvalidMarketStatus); + ensure!(market.matches_outcome_report(&outcome), Error::::OutcomeMismatch); + let report = market.report.as_ref().ok_or(Error::::MarketIsNotReported)?; + + let now = >::block_number(); + let disputes = Disputes::::get(market_id); + let num_disputes: u32 = disputes.len().saturated_into(); + + Self::ensure_can_not_dispute_the_same_outcome(&disputes, report, &outcome)?; + Self::ensure_disputes_does_not_exceed_max_disputes(num_disputes)?; + + let bond = default_outcome_bond::(disputes.len()); + + T::AssetManager::reserve_named(&Self::reserve_id(), Asset::Ztg, &who, bond)?; + + let market_dispute = MarketDispute { at: now, by: who, outcome, bond }; + >::try_mutate(market_id, |disputes| { + disputes.try_push(market_dispute.clone()).map_err(|_| >::StorageOverflow) + })?; + + // each dispute resets dispute_duration + let r = Self::remove_auto_resolve(disputes.as_slice(), &market_id, &market); + let dispute_duration_ends_at_block = + now.saturating_add(market.deadlines.dispute_duration); + let e = + T::DisputeResolution::add_auto_resolve(&market_id, dispute_duration_ends_at_block)?; + + Self::deposit_event(Event::OutcomeReserved { market_id, dispute: market_dispute }); + + Ok((Some(T::WeightInfo::suggest_outcome(num_disputes, r, e))).into()) + } + } + + impl Pallet { + #[inline] + pub fn reserve_id() -> [u8; 8] { + T::PalletId::get().0 + } + + fn ensure_can_not_dispute_the_same_outcome( + disputes: &[MarketDispute>], + report: &Report, + outcome: &OutcomeReport, + ) -> DispatchResult { + if let Some(last_dispute) = disputes.last() { + ensure!(&last_dispute.outcome != outcome, Error::::CannotDisputeSameOutcome); + } else { + ensure!(&report.outcome != outcome, Error::::CannotDisputeSameOutcome); + } + + Ok(()) + } + + #[inline] + fn ensure_disputes_does_not_exceed_max_disputes(num_disputes: u32) -> DispatchResult { + ensure!(num_disputes < T::MaxDisputes::get(), Error::::MaxDisputesReached); + Ok(()) + } + fn get_auto_resolve( - disputes: &[MarketDispute], + disputes: &[MarketDispute>], market: &MarketOf, ) -> Option { disputes.last().map(|last_dispute| { @@ -116,16 +259,50 @@ mod pallet { } fn remove_auto_resolve( - disputes: &[MarketDispute], + disputes: &[MarketDispute>], market_id: &MarketIdOf, market: &MarketOf, - ) { + ) -> u32 { if let Some(dispute_duration_ends_at_block) = Self::get_auto_resolve(disputes, market) { - T::DisputeResolution::remove_auto_resolve( + return T::DisputeResolution::remove_auto_resolve( market_id, dispute_duration_ends_at_block, ); } + 0u32 + } + } + + impl DisputeMaxWeightApi for Pallet + where + T: Config, + { + fn on_dispute_max_weight() -> Weight { + T::WeightInfo::on_dispute_weight() + } + + fn on_resolution_max_weight() -> Weight { + T::WeightInfo::on_resolution_weight(T::MaxDisputes::get()) + } + + fn exchange_max_weight() -> Weight { + T::WeightInfo::exchange_weight(T::MaxDisputes::get()) + } + + fn get_auto_resolve_max_weight() -> Weight { + T::WeightInfo::get_auto_resolve_weight(T::MaxDisputes::get()) + } + + fn has_failed_max_weight() -> Weight { + T::WeightInfo::has_failed_weight(T::MaxDisputes::get()) + } + + fn on_global_dispute_max_weight() -> Weight { + T::WeightInfo::on_global_dispute_weight(T::MaxDisputes::get()) + } + + fn clear_max_weight() -> Weight { + T::WeightInfo::clear_weight(T::MaxDisputes::get()) } } @@ -135,75 +312,264 @@ mod pallet { { type AccountId = T::AccountId; type Balance = BalanceOf; + type NegativeImbalance = NegativeImbalanceOf; type BlockNumber = T::BlockNumber; type MarketId = MarketIdOf; type Moment = MomentOf; type Origin = T::RuntimeOrigin; fn on_dispute( - disputes: &[MarketDispute], - market_id: &Self::MarketId, + _: &Self::MarketId, market: &MarketOf, - ) -> DispatchResult { + ) -> Result, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, Error::::MarketDoesNotHaveSimpleDisputesMechanism ); - Self::remove_auto_resolve(disputes, market_id, market); - let curr_block_num = >::block_number(); - // each dispute resets dispute_duration - let dispute_duration_ends_at_block = - curr_block_num.saturating_add(market.deadlines.dispute_duration); - T::DisputeResolution::add_auto_resolve(market_id, dispute_duration_ends_at_block)?; - Ok(()) + + let res = + ResultWithWeightInfo { result: (), weight: T::WeightInfo::on_dispute_weight() }; + + Ok(res) } fn on_resolution( - disputes: &[MarketDispute], - _: &Self::MarketId, + market_id: &Self::MarketId, market: &MarketOf, - ) -> Result, DispatchError> { + ) -> Result>, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, Error::::MarketDoesNotHaveSimpleDisputesMechanism ); ensure!(market.status == MarketStatus::Disputed, Error::::InvalidMarketStatus); - if let Some(last_dispute) = disputes.last() { - Ok(Some(last_dispute.outcome.clone())) - } else { - Err(Error::::InvalidMarketStatus.into()) + let disputes = Disputes::::get(market_id); + + let last_dispute = match disputes.last() { + Some(l) => l, + // if there are no disputes, then the market is resolved with the default report + None => { + return Ok(ResultWithWeightInfo { + result: None, + weight: T::WeightInfo::on_resolution_weight(disputes.len() as u32), + }); + } + }; + + let res = ResultWithWeightInfo { + result: Some(last_dispute.outcome.clone()), + weight: T::WeightInfo::on_resolution_weight(disputes.len() as u32), + }; + + Ok(res) + } + + fn exchange( + market_id: &Self::MarketId, + market: &MarketOf, + resolved_outcome: &OutcomeReport, + mut overall_imbalance: NegativeImbalanceOf, + ) -> Result>, DispatchError> { + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, + Error::::MarketDoesNotHaveSimpleDisputesMechanism + ); + ensure!(market.status == MarketStatus::Disputed, Error::::InvalidMarketStatus); + + let disputes = Disputes::::get(market_id); + + let mut correct_reporters: Vec = Vec::new(); + + for dispute in disputes.iter() { + if &dispute.outcome == resolved_outcome { + T::AssetManager::unreserve_named( + &Self::reserve_id(), + Asset::Ztg, + &dispute.by, + dispute.bond.saturated_into::().saturated_into(), + ); + + correct_reporters.push(dispute.by.clone()); + } else { + let (imbalance, _) = CurrencyOf::::slash_reserved_named( + &Self::reserve_id(), + &dispute.by, + dispute.bond.saturated_into::().saturated_into(), + ); + overall_imbalance.subsume(imbalance); + } + } + + // Fold all the imbalances into one and reward the correct reporters. The + // number of correct reporters might be zero if the market defaults to the + // report after abandoned dispute. In that case, the rewards remain slashed. + if let Some(reward_per_each) = + overall_imbalance.peek().checked_div(&correct_reporters.len().saturated_into()) + { + for correct_reporter in &correct_reporters { + let (actual_reward, leftover) = overall_imbalance.split(reward_per_each); + overall_imbalance = leftover; + CurrencyOf::::resolve_creating(correct_reporter, actual_reward); + } } + + Disputes::::remove(market_id); + + let res = ResultWithWeightInfo { + result: overall_imbalance, + weight: T::WeightInfo::exchange_weight(disputes.len() as u32), + }; + + Ok(res) } fn get_auto_resolve( - disputes: &[MarketDispute], - _: &Self::MarketId, + market_id: &Self::MarketId, market: &MarketOf, - ) -> Result, DispatchError> { + ) -> ResultWithWeightInfo> { + if market.dispute_mechanism != MarketDisputeMechanism::SimpleDisputes { + return ResultWithWeightInfo { + result: None, + weight: T::WeightInfo::get_auto_resolve_weight(T::MaxDisputes::get()), + }; + } + + let disputes = Disputes::::get(market_id); + + let res = ResultWithWeightInfo { + result: Self::get_auto_resolve(disputes.as_slice(), market), + weight: T::WeightInfo::get_auto_resolve_weight(disputes.len() as u32), + }; + + res + } + + fn has_failed( + market_id: &Self::MarketId, + market: &MarketOf, + ) -> Result, DispatchError> { ensure!( market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, Error::::MarketDoesNotHaveSimpleDisputesMechanism ); - Ok(Self::get_auto_resolve(disputes, market)) + let disputes = >::get(market_id); + let disputes_len = disputes.len() as u32; + + let res = ResultWithWeightInfo { + result: disputes_len == T::MaxDisputes::get(), + weight: T::WeightInfo::has_failed_weight(disputes_len), + }; + + Ok(res) } - fn has_failed( - _: &[MarketDispute], - _: &Self::MarketId, + fn on_global_dispute( + market_id: &Self::MarketId, market: &MarketOf, - ) -> Result { + ) -> Result< + ResultWithWeightInfo>>, + DispatchError, + > { ensure!( market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, Error::::MarketDoesNotHaveSimpleDisputesMechanism ); - // TODO when does simple disputes fail? - Ok(false) + + let disputes_len = >::decode_len(market_id).unwrap_or(0) as u32; + + let res = ResultWithWeightInfo { + result: >::get(market_id) + .iter() + .map(|dispute| GlobalDisputeItem { + outcome: dispute.outcome.clone(), + owner: dispute.by.clone(), + initial_vote_amount: dispute.bond, + }) + .collect(), + weight: T::WeightInfo::on_global_dispute_weight(disputes_len), + }; + + Ok(res) + } + + fn clear( + market_id: &Self::MarketId, + market: &MarketOf, + ) -> Result, DispatchError> { + ensure!( + market.dispute_mechanism == MarketDisputeMechanism::SimpleDisputes, + Error::::MarketDoesNotHaveSimpleDisputesMechanism + ); + + let mut disputes_len = 0u32; + // `Disputes` is emtpy unless the market is disputed, so this is just a defensive + // check. + if market.status == MarketStatus::Disputed { + disputes_len = Disputes::::decode_len(market_id).unwrap_or(0) as u32; + for dispute in Disputes::::take(market_id).iter() { + T::AssetManager::unreserve_named( + &Self::reserve_id(), + Asset::Ztg, + &dispute.by, + dispute.bond.saturated_into::().saturated_into(), + ); + } + } + + let res = ResultWithWeightInfo { + result: (), + weight: T::WeightInfo::clear_weight(disputes_len), + }; + + Ok(res) } } impl SimpleDisputesPalletApi for Pallet where T: Config {} - #[pallet::pallet] - pub struct Pallet(PhantomData); + // No-one can bound more than BalanceOf, therefore, this functions saturates + pub fn default_outcome_bond(n: usize) -> BalanceOf + where + T: Config, + { + T::OutcomeBond::get().saturating_add( + T::OutcomeFactor::get().saturating_mul(n.saturated_into::().into()), + ) + } +} + +#[cfg(any(feature = "runtime-benchmarks", test))] +pub(crate) fn market_mock() -> MarketOf +where + T: crate::Config, +{ + use frame_support::traits::Get; + use sp_runtime::{traits::AccountIdConversion, SaturatedConversion}; + use zeitgeist_primitives::types::{MarketBonds, ScoringRule}; + + zeitgeist_primitives::types::Market { + base_asset: Asset::Ztg, + creation: zeitgeist_primitives::types::MarketCreation::Permissionless, + creator_fee: 0, + creator: T::PalletId::get().into_account_truncating(), + market_type: zeitgeist_primitives::types::MarketType::Scalar(0..=100), + dispute_mechanism: zeitgeist_primitives::types::MarketDisputeMechanism::SimpleDisputes, + metadata: Default::default(), + oracle: T::PalletId::get().into_account_truncating(), + period: zeitgeist_primitives::types::MarketPeriod::Block(Default::default()), + deadlines: zeitgeist_primitives::types::Deadlines { + grace_period: 1_u32.into(), + oracle_duration: 1_u32.into(), + dispute_duration: 42_u32.into(), + }, + report: Some(zeitgeist_primitives::types::Report { + outcome: OutcomeReport::Scalar(0), + at: 0u64.saturated_into(), + by: T::PalletId::get().into_account_truncating(), + }), + resolved_outcome: None, + scoring_rule: ScoringRule::CPMM, + status: zeitgeist_primitives::types::MarketStatus::Disputed, + bonds: MarketBonds::default(), + } } diff --git a/zrml/simple-disputes/src/mock.rs b/zrml/simple-disputes/src/mock.rs index c0848647b..052a1fb6a 100644 --- a/zrml/simple-disputes/src/mock.rs +++ b/zrml/simple-disputes/src/mock.rs @@ -20,10 +20,9 @@ use crate::{self as zrml_simple_disputes}; use frame_support::{ - construct_runtime, + construct_runtime, ord_parameter_types, pallet_prelude::{DispatchError, Weight}, traits::Everything, - BoundedVec, }; use sp_runtime::{ testing::Header, @@ -31,15 +30,30 @@ use sp_runtime::{ }; use zeitgeist_primitives::{ constants::mock::{ - BlockHashCount, MaxReserves, MinimumPeriod, PmPalletId, SimpleDisputesPalletId, + BlockHashCount, ExistentialDeposits, GetNativeCurrencyId, MaxDisputes, MaxReserves, + MinimumPeriod, OutcomeBond, OutcomeFactor, PmPalletId, SimpleDisputesPalletId, BASE, }, traits::DisputeResolutionApi, types::{ - AccountIdTest, Asset, Balance, BlockNumber, BlockTest, Hash, Index, Market, MarketDispute, - MarketId, Moment, UncheckedExtrinsicTest, + AccountIdTest, Amount, Asset, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, + CurrencyId, Hash, Index, Market, MarketId, Moment, UncheckedExtrinsicTest, }, }; +pub const ALICE: AccountIdTest = 0; +pub const BOB: AccountIdTest = 1; +pub const CHARLIE: AccountIdTest = 2; +pub const DAVE: AccountIdTest = 3; +pub const EVE: AccountIdTest = 4; +pub const FRED: AccountIdTest = 5; +pub const SUDO: AccountIdTest = 69; + +pub const INITIAL_BALANCE: u128 = 1_000 * BASE; + +ord_parameter_types! { + pub const Sudo: AccountIdTest = SUDO; +} + construct_runtime!( pub enum Runtime where @@ -47,11 +61,13 @@ construct_runtime!( NodeBlock = BlockTest, UncheckedExtrinsic = UncheckedExtrinsicTest, { + AssetManager: orml_currencies::{Call, Pallet, Storage}, Balances: pallet_balances::{Call, Config, Event, Pallet, Storage}, MarketCommons: zrml_market_commons::{Pallet, Storage}, SimpleDisputes: zrml_simple_disputes::{Event, Pallet, Storage}, System: frame_system::{Call, Config, Event, Pallet, Storage}, Timestamp: pallet_timestamp::{Pallet}, + Tokens: orml_tokens::{Config, Event, Pallet, Storage}, } ); @@ -63,7 +79,6 @@ impl DisputeResolutionApi for NoopResolution { type Balance = Balance; type BlockNumber = BlockNumber; type MarketId = MarketId; - type MaxDisputes = u32; type Moment = Moment; fn resolve( @@ -93,19 +108,18 @@ impl DisputeResolutionApi for NoopResolution { fn remove_auto_resolve(_market_id: &Self::MarketId, _resolve_at: Self::BlockNumber) -> u32 { 0u32 } - - fn get_disputes( - _market_id: &Self::MarketId, - ) -> BoundedVec, Self::MaxDisputes> { - Default::default() - } } impl crate::Config for Runtime { + type AssetManager = AssetManager; type RuntimeEvent = (); type DisputeResolution = NoopResolution; type MarketCommons = MarketCommons; + type MaxDisputes = MaxDisputes; + type OutcomeBond = OutcomeBond; + type OutcomeFactor = OutcomeFactor; type PalletId = SimpleDisputesPalletId; + type WeightInfo = zrml_simple_disputes::weights::WeightInfo; } impl frame_system::Config for Runtime { @@ -147,6 +161,27 @@ impl pallet_balances::Config for Runtime { type WeightInfo = (); } +impl orml_currencies::Config for Runtime { + type GetNativeCurrencyId = GetNativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type WeightInfo = (); +} + +impl orml_tokens::Config for Runtime { + type Amount = Amount; + type Balance = Balance; + type CurrencyId = CurrencyId; + type DustRemovalWhitelist = Everything; + type RuntimeEvent = (); + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = (); + type MaxReserves = MaxReserves; + type CurrencyHooks = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + impl zrml_market_commons::Config for Runtime { type Currency = Balances; type MarketId = MarketId; @@ -161,10 +196,34 @@ impl pallet_timestamp::Config for Runtime { type WeightInfo = (); } -pub struct ExtBuilder; +pub struct ExtBuilder { + balances: Vec<(AccountIdTest, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: vec![ + (ALICE, INITIAL_BALANCE), + (BOB, INITIAL_BALANCE), + (CHARLIE, INITIAL_BALANCE), + (DAVE, INITIAL_BALANCE), + (EVE, INITIAL_BALANCE), + (FRED, INITIAL_BALANCE), + (SUDO, INITIAL_BALANCE), + ], + } + } +} impl ExtBuilder { pub fn build(self) -> sp_io::TestExternalities { - frame_system::GenesisConfig::default().build_storage::().unwrap().into() + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + pallet_balances::GenesisConfig:: { balances: self.balances } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() } } diff --git a/zrml/simple-disputes/src/simple_disputes_pallet_api.rs b/zrml/simple-disputes/src/simple_disputes_pallet_api.rs index 3c723b624..f01588aaa 100644 --- a/zrml/simple-disputes/src/simple_disputes_pallet_api.rs +++ b/zrml/simple-disputes/src/simple_disputes_pallet_api.rs @@ -1,3 +1,4 @@ +// Copyright 2023 Forecasting Technologies LTD. // Copyright 2021-2022 Zeitgeist PM LLC. // // This file is part of Zeitgeist. @@ -15,6 +16,6 @@ // You should have received a copy of the GNU General Public License // along with Zeitgeist. If not, see . -use zeitgeist_primitives::traits::DisputeApi; +use zeitgeist_primitives::traits::{DisputeApi, DisputeMaxWeightApi}; -pub trait SimpleDisputesPalletApi: DisputeApi {} +pub trait SimpleDisputesPalletApi: DisputeApi + DisputeMaxWeightApi {} diff --git a/zrml/simple-disputes/src/tests.rs b/zrml/simple-disputes/src/tests.rs index f9e9d87af..f5f35ba89 100644 --- a/zrml/simple-disputes/src/tests.rs +++ b/zrml/simple-disputes/src/tests.rs @@ -20,10 +20,11 @@ use crate::{ mock::{ExtBuilder, Runtime, SimpleDisputes}, - Error, MarketOf, + Disputes, Error, MarketOf, }; -use frame_support::assert_noop; +use frame_support::{assert_noop, BoundedVec}; use zeitgeist_primitives::{ + constants::mock::{OutcomeBond, OutcomeFactor}, traits::DisputeApi, types::{ Asset, Deadlines, Market, MarketBonds, MarketCreation, MarketDispute, @@ -46,16 +47,16 @@ const DEFAULT_MARKET: MarketOf = Market { resolved_outcome: None, scoring_rule: ScoringRule::CPMM, status: MarketStatus::Disputed, - bonds: MarketBonds { creation: None, oracle: None, outsider: None }, + bonds: MarketBonds { creation: None, oracle: None, outsider: None, dispute: None }, }; #[test] fn on_dispute_denies_non_simple_disputes_markets() { - ExtBuilder.build().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { let mut market = DEFAULT_MARKET; market.dispute_mechanism = MarketDisputeMechanism::Court; assert_noop!( - SimpleDisputes::on_dispute(&[], &0, &market), + SimpleDisputes::on_dispute(&0, &market), Error::::MarketDoesNotHaveSimpleDisputesMechanism ); }); @@ -63,11 +64,11 @@ fn on_dispute_denies_non_simple_disputes_markets() { #[test] fn on_resolution_denies_non_simple_disputes_markets() { - ExtBuilder.build().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { let mut market = DEFAULT_MARKET; market.dispute_mechanism = MarketDisputeMechanism::Court; assert_noop!( - SimpleDisputes::on_resolution(&[], &0, &market), + SimpleDisputes::on_resolution(&0, &market), Error::::MarketDoesNotHaveSimpleDisputesMechanism ); }); @@ -75,16 +76,33 @@ fn on_resolution_denies_non_simple_disputes_markets() { #[test] fn on_resolution_sets_the_last_dispute_of_disputed_markets_as_the_canonical_outcome() { - ExtBuilder.build().execute_with(|| { + ExtBuilder::default().build().execute_with(|| { let mut market = DEFAULT_MARKET; market.status = MarketStatus::Disputed; - let disputes = [ - MarketDispute { at: 0, by: 0, outcome: OutcomeReport::Scalar(0) }, - MarketDispute { at: 0, by: 0, outcome: OutcomeReport::Scalar(20) }, - ]; + let disputes = BoundedVec::try_from( + [ + MarketDispute { + at: 0, + by: 0, + outcome: OutcomeReport::Scalar(0), + bond: OutcomeBond::get(), + }, + MarketDispute { + at: 0, + by: 0, + outcome: OutcomeReport::Scalar(20), + bond: OutcomeFactor::get() * OutcomeBond::get(), + }, + ] + .to_vec(), + ) + .unwrap(); + Disputes::::insert(0, &disputes); assert_eq!( - &SimpleDisputes::on_resolution(&disputes, &0, &market).unwrap().unwrap(), + &SimpleDisputes::on_resolution(&0, &market).unwrap().result.unwrap(), &disputes.last().unwrap().outcome ) }); } + +// TODO test `suggest_outcome` functionality and API functionality diff --git a/zrml/simple-disputes/src/weights.rs b/zrml/simple-disputes/src/weights.rs new file mode 100644 index 000000000..bfbc6cc8c --- /dev/null +++ b/zrml/simple-disputes/src/weights.rs @@ -0,0 +1,133 @@ +// Copyright 2023 Forecasting Technologies LTD. +// Copyright 2021-2022 Zeitgeist PM LLC. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +//! Autogenerated weights for zrml_simple_disputes +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-01-19, STEPS: `10`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Native), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/debug/zeitgeist +// benchmark +// pallet +// --chain=dev +// --steps=10 +// --repeat=10 +// --pallet=zrml_simple_disputes +// --extrinsic=* +// --execution=Native +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./zrml/simple-disputes/src/weights2.rs +// --template=./misc/weight_template.hbs + +#![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_simple_disputes (automatically generated) +pub trait WeightInfoZeitgeist { + fn suggest_outcome(d: u32, r: u32, e: u32) -> Weight; + fn on_dispute_weight() -> Weight; + fn on_resolution_weight(d: u32) -> Weight; + fn exchange_weight(d: u32) -> Weight; + fn get_auto_resolve_weight(d: u32) -> Weight; + fn has_failed_weight(d: u32) -> Weight; + fn on_global_dispute_weight(d: u32) -> Weight; + fn clear_weight(d: u32) -> Weight; +} + +/// Weight functions for zrml_simple_disputes (automatically generated) +pub struct WeightInfo(PhantomData); +impl WeightInfoZeitgeist for WeightInfo { + // Storage: MarketCommons Markets (r:1 w:0) + // Storage: SimpleDisputes Disputes (r:1 w:1) + // Storage: Balances Reserves (r:1 w:1) + // Storage: PredictionMarkets MarketIdsPerDisputeBlock (r:2 w:2) + fn suggest_outcome(d: u32, r: u32, e: u32) -> Weight { + Weight::from_ref_time(400_160_000) + // Standard Error: 1_302_000 + .saturating_add(Weight::from_ref_time(3_511_000).saturating_mul(d.into())) + // Standard Error: 69_000 + .saturating_add(Weight::from_ref_time(324_000).saturating_mul(r.into())) + // Standard Error: 69_000 + .saturating_add(Weight::from_ref_time(311_000).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) + } + + fn on_dispute_weight() -> Weight { + Weight::from_ref_time(0) + } + // Storage: SimpleDisputes Disputes (r:1 w:0) + fn on_resolution_weight(d: u32) -> Weight { + Weight::from_ref_time(5_464_000) + // Standard Error: 3_000 + .saturating_add(Weight::from_ref_time(210_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(1)) + } + // Storage: SimpleDisputes Disputes (r:1 w:1) + // Storage: Balances Reserves (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn exchange_weight(d: u32) -> Weight { + Weight::from_ref_time(18_573_000) + // Standard Error: 14_000 + .saturating_add(Weight::from_ref_time(19_710_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(d.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(d.into()))) + } + // Storage: SimpleDisputes Disputes (r:1 w:0) + fn get_auto_resolve_weight(d: u32) -> Weight { + Weight::from_ref_time(5_535_000) + // Standard Error: 3_000 + .saturating_add(Weight::from_ref_time(145_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(1)) + } + // Storage: SimpleDisputes Disputes (r:1 w:0) + fn has_failed_weight(d: u32) -> Weight { + Weight::from_ref_time(5_685_000) + // Standard Error: 2_000 + .saturating_add(Weight::from_ref_time(117_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(1)) + } + // Storage: SimpleDisputes Disputes (r:1 w:0) + fn on_global_dispute_weight(d: u32) -> Weight { + Weight::from_ref_time(5_815_000) + // Standard Error: 2_000 + .saturating_add(Weight::from_ref_time(66_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(1)) + } + // Storage: SimpleDisputes Disputes (r:1 w:1) + // Storage: Balances Reserves (r:1 w:1) + // Storage: System Account (r:1 w:1) + fn clear_weight(d: u32) -> Weight { + Weight::from_ref_time(15_958_000) + // Standard Error: 17_000 + .saturating_add(Weight::from_ref_time(13_085_000).saturating_mul(d.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(d.into()))) + .saturating_add(T::DbWeight::get().writes(1)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(d.into()))) + } +}