diff --git a/Cargo.lock b/Cargo.lock index 71f249cbe..0c068e0e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,11 +190,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.8.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.4", "event-listener", "futures-core", ] @@ -207,7 +207,7 @@ checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ "async-lock", "async-task", - "concurrent-queue", + "concurrent-queue 2.1.0", "fastrand", "futures-lite", "slab", @@ -230,13 +230,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.12.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" dependencies = [ "async-lock", "autocfg", - "concurrent-queue", + "concurrent-queue 1.2.4", "futures-lite", "libc", "log", @@ -245,7 +245,7 @@ dependencies = [ "slab", "socket2", "waker-fn", - "windows-sys", + "winapi", ] [[package]] @@ -260,20 +260,20 @@ dependencies = [ [[package]] name = "async-process" -version = "1.6.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" +checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" dependencies = [ "async-io", - "async-lock", "autocfg", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", + "once_cell", "signal-hook", - "windows-sys", + "winapi", ] [[package]] @@ -711,9 +711,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +checksum = "895adc16c8b3273fbbc32685a7d55227705eda08c01e77704020f3491924b44b" dependencies = [ "arrayref", "arrayvec 0.7.2", @@ -764,16 +764,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.3.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" dependencies = [ "async-channel", - "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", + "once_cell", ] [[package]] @@ -851,6 +851,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "camino" version = "1.1.2" @@ -1133,6 +1139,15 @@ dependencies = [ "pallet-vesting", ] +[[package]] +name = "concurrent-queue" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +dependencies = [ + "cache-padded", +] + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -2283,9 +2298,9 @@ dependencies = [ [[package]] name = "environmental" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b" +checksum = "68b91989ae21441195d7d9b9993a2f9295c7e1a8c96255d8b729accddc124797" [[package]] name = "errno" @@ -2477,13 +2492,13 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.5.4", ] [[package]] @@ -4235,9 +4250,9 @@ dependencies = [ [[package]] name = "libp2p-pnet" -version = "0.22.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de160c5631696cea22be326c19bd9d306e254c4964945263aea10f25f6e0864e" +checksum = "1a5a702574223aa55d8878bdc8bf55c84a6086f87ddaddc28ce730b4caa81538" dependencies = [ "futures 0.3.25", "log", @@ -4748,6 +4763,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.6.2" @@ -5033,9 +5057,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.3" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -5095,9 +5119,9 @@ dependencies = [ [[package]] name = "num-format" -version = "0.4.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +checksum = "54b862ff8df690cf089058c98b183676a7ed0f974cc08b426800093227cbff3b" dependencies = [ "arrayvec 0.7.2", "itoa", @@ -11361,9 +11385,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -11426,9 +11450,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", @@ -11752,9 +11776,9 @@ checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601" dependencies = [ "byteorder", "crunchy", @@ -12854,9 +12878,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs index 1436e58a4..0c8c078e2 100644 --- a/primitives/src/constants/mock.rs +++ b/primitives/src/constants/mock.rs @@ -59,6 +59,7 @@ parameter_types! { // 60_000 = 1 minute. Should be raised to something more reasonable in the future. pub const MinSubsidyPeriod: Moment = 60_000; pub const OracleBond: Balance = 50 * CENT; + pub const OutsiderBond: Balance = 2 * OracleBond::get(); pub const PmPalletId: PalletId = PalletId(*b"zge/pred"); pub const ValidityBond: Balance = 50 * CENT; } diff --git a/primitives/src/market.rs b/primitives/src/market.rs index 2591ef2ee..0718766cd 100644 --- a/primitives/src/market.rs +++ b/primitives/src/market.rs @@ -86,6 +86,7 @@ impl Bond { pub struct MarketBonds { pub creation: Option>, pub oracle: Option>, + pub outsider: Option>, } impl MarketBonds { @@ -95,14 +96,16 @@ impl MarketBonds { Some(bond) if bond.who == *who => bond.value, _ => BA::zero(), }; - value_or_default(&self.creation).saturating_add(value_or_default(&self.oracle)) + value_or_default(&self.creation) + .saturating_add(value_or_default(&self.oracle)) + .saturating_add(value_or_default(&self.outsider)) } } // Used primarily for testing purposes. impl Default for MarketBonds { fn default() -> Self { - MarketBonds { creation: None, oracle: None } + MarketBonds { creation: None, oracle: None, outsider: None } } } diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs index ed2ba1aa5..fee8dbc3e 100644 --- a/runtime/battery-station/src/parameters.rs +++ b/runtime/battery-station/src/parameters.rs @@ -191,6 +191,9 @@ parameter_types! { /// (Slashable) The orcale bond. Slashed in case the final outcome does not match the /// outcome the oracle reported. pub const OracleBond: Balance = 50 * CENT; + /// (Slashable) A bond for an outcome reporter, who is not the oracle. + /// Slashed in case the final outcome does not match the outcome by the outsider. + pub const OutsiderBond: Balance = 2 * OracleBond::get(); /// Pallet identifier, mainly used for named balance reserves. pub const PmPalletId: PalletId = PM_PALLET_ID; /// (Slashable) A bond for creation markets that do not require approval. Slashed in case diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index a54e4df60..58707f793 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -58,11 +58,7 @@ macro_rules! decl_common_types { frame_system::ChainContext, Runtime, AllPalletsWithSystem, - ( - pallet_parachain_staking::migrations::MigrateAtStakeAutoCompound, - zrml_prediction_markets::migrations::UpdateMarketsForBaseAssetAndRecordBonds, - zrml_prediction_markets::migrations::AddFieldToAuthorityReport, - ), + zrml_prediction_markets::migrations::AddOutsiderBond, >; #[cfg(not(feature = "parachain"))] @@ -72,10 +68,7 @@ macro_rules! decl_common_types { frame_system::ChainContext, Runtime, AllPalletsWithSystem, - ( - zrml_prediction_markets::migrations::UpdateMarketsForBaseAssetAndRecordBonds, - zrml_prediction_markets::migrations::AddFieldToAuthorityReport, - ), + zrml_prediction_markets::migrations::AddOutsiderBond, >; pub type Header = generic::Header; @@ -1019,6 +1012,7 @@ macro_rules! impl_config_traits { type MaxEditReasonLen = MaxEditReasonLen; type MaxRejectReasonLen = MaxRejectReasonLen; type OracleBond = OracleBond; + type OutsiderBond = OutsiderBond; type PalletId = PmPalletId; type RejectOrigin = EnsureRootOrHalfAdvisoryCommittee; type RequestEditOrigin = EitherOfDiverse< diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs index 32745542d..d6eef76ad 100644 --- a/runtime/zeitgeist/src/parameters.rs +++ b/runtime/zeitgeist/src/parameters.rs @@ -191,6 +191,9 @@ parameter_types! { /// (Slashable) The orcale bond. Slashed in case the final outcome does not match the /// outcome the oracle reported. pub const OracleBond: Balance = 200 * BASE; + /// (Slashable) A bond for an outcome reporter, who is not the oracle. + /// Slashed in case the final outcome does not match the outcome by the outsider. + pub const OutsiderBond: Balance = 2 * OracleBond::get(); /// Pallet identifier, mainly used for named balance reserves. DO NOT CHANGE. pub const PmPalletId: PalletId = PM_PALLET_ID; /// (Slashable) A bond for creation markets that do not require approval. Slashed in case diff --git a/zrml/court/src/tests.rs b/zrml/court/src/tests.rs index 79df3d108..a383ac109 100644 --- a/zrml/court/src/tests.rs +++ b/zrml/court/src/tests.rs @@ -52,7 +52,7 @@ const DEFAULT_MARKET: MarketOf = Market { resolved_outcome: None, status: MarketStatus::Closed, scoring_rule: ScoringRule::CPMM, - bonds: MarketBonds { creation: None, oracle: None }, + bonds: MarketBonds { creation: None, oracle: None, outsider: None }, }; const DEFAULT_SET_OF_JURORS: &[(u128, Juror)] = &[ (7, Juror { status: JurorStatus::Ok }), diff --git a/zrml/market-commons/src/lib.rs b/zrml/market-commons/src/lib.rs index 29851b11a..3ba87e9ee 100644 --- a/zrml/market-commons/src/lib.rs +++ b/zrml/market-commons/src/lib.rs @@ -50,7 +50,7 @@ mod pallet { use zeitgeist_primitives::types::{Asset, Market, PoolId}; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(5); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(6); type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; diff --git a/zrml/market-commons/src/tests.rs b/zrml/market-commons/src/tests.rs index 40a8cef8d..e0d9cbabf 100644 --- a/zrml/market-commons/src/tests.rs +++ b/zrml/market-commons/src/tests.rs @@ -47,7 +47,7 @@ const MARKET_DUMMY: Market { + /// Settle the $bond_type bond by repatriating it to free balance of beneficiary. + /// + /// This function **should** only be called if the bond is not yet settled, and calling + /// it if the bond is settled is most likely a logic error. If the bond is already + /// settled, storage is not changed, a warning is raised and `Ok(())` is returned. + fn $fn_name(market_id: &MarketIdOf, beneficiary: &T::AccountId) -> DispatchResult { + let market = >::market(market_id)?; + let bond = market.bonds.$bond_type.as_ref().ok_or(Error::::MissingBond)?; + if bond.is_settled { + let warning = format!( + "Attempting to settle the {} bond of market {:?} multiple times", + stringify!($bond_type), + market_id, + ); + log::warn!("{}", warning); + debug_assert!(false, "{}", warning); + return Ok(()); + } + let res = >::repatriate_reserved_named( + &Self::reserve_id(), + &bond.who, + beneficiary, + bond.value, + BalanceStatus::Free, + ); + // If there's an error or missing balance, + // there's nothing we can do, so we don't count this as error + // and log a warning instead. + match res { + Ok(missing) if missing != >::zero() => { + let warning = format!( + "Failed to repatriate all of the {} bond of market {:?} (missing \ + balance {:?}).", + stringify!($bond_type), + market_id, + missing, + ); + log::warn!("{}", warning); + debug_assert!(false, "{}", warning); + } + Ok(_) => (), + Err(_err) => { + let warning = format!( + "Failed to settle the {} bond of market {:?} (error: {}).", + stringify!($bond_type), + market_id, + stringify!(_err), + ); + log::warn!("{}", warning); + debug_assert!(false, "{}", warning); + } + } + >::mutate_market(market_id, |m| { + m.bonds.$bond_type = Some(Bond { is_settled: true, ..bond.clone() }); + Ok(()) + })?; + Ok(()) + } + }; + } + + macro_rules! impl_is_bond_pending { + ($fn_name:ident, $bond_type:ident) => { + /// Check whether the $bond_type is present (ready to get unreserved or slashed). + /// Set the flag `with_warning` to `true`, when warnings should be logged + /// in case the bond is not present or already settled. + /// + /// Return `true` if the bond is present and not settled, `false` otherwise. + fn $fn_name( + market_id: &MarketIdOf, + market: &MarketOf, + with_warning: bool, + ) -> bool { + if let Some(bond) = &market.bonds.$bond_type { + if !bond.is_settled { + return true; + } else if with_warning { + let warning = format!( + "[PredictionMarkets] The {} bond is already settled for market {:?}.", + stringify!($bond_type), + market_id, + ); + log::warn!("{}", warning); + debug_assert!(false, "{}", warning); + } + } else if with_warning { + let warning = format!( + "[PredictionMarkets] The {} bond is not present for market {:?}.", + stringify!($bond_type), + market_id, + ); + log::warn!("{}", warning); + debug_assert!(false, "{}", warning); + } + + false + } + }; + } + #[pallet::call] impl Pallet { /// Destroy a market, including its outcome assets, market account and pool account. @@ -234,16 +336,7 @@ mod pallet { // Slash outstanding bonds; see // https://github.com/zeitgeistpm/runtime-audit-1/issues/34#issuecomment-1120187097 for // details. - if let Some(bond) = market.bonds.creation { - if !bond.is_settled { - Self::slash_creation_bond(&market_id, None)?; - } - } - if let Some(bond) = market.bonds.oracle { - if !bond.is_settled { - Self::slash_oracle_bond(&market_id, None)?; - } - } + Self::slash_pending_bonds(&market_id, &market)?; if market_status == MarketStatus::Proposed { MarketIdsForEdit::::remove(market_id); @@ -691,10 +784,12 @@ mod pallet { MarketCreation::Advised => MarketBonds { creation: Some(Bond::new(sender.clone(), T::AdvisoryBond::get())), oracle: Some(Bond::new(sender.clone(), T::OracleBond::get())), + ..Default::default() }, MarketCreation::Permissionless => MarketBonds { creation: Some(Bond::new(sender.clone(), T::ValidityBond::get())), oracle: Some(Bond::new(sender.clone(), T::OracleBond::get())), + ..Default::default() }, }; @@ -1228,13 +1323,26 @@ mod pallet { } } + let sender_is_oracle = sender == market.oracle; + let origin_has_permission = T::ResolveOrigin::ensure_origin(origin).is_ok(); + let sender_is_outsider = !sender_is_oracle && !origin_has_permission; + if should_check_origin { - let sender_is_oracle = sender == market.oracle; - let origin_has_permission = T::ResolveOrigin::ensure_origin(origin).is_ok(); ensure!( sender_is_oracle || origin_has_permission, Error::::ReporterNotOracle ); + } else if sender_is_outsider { + let outsider_bond = T::OutsiderBond::get(); + + market.bonds.outsider = Some(Bond::new(sender.clone(), outsider_bond)); + + T::AssetManager::reserve_named( + &Self::reserve_id(), + Asset::Ztg, + &sender, + outsider_bond, + )?; } market.report = Some(market_report.clone()); @@ -1546,6 +1654,9 @@ mod pallet { #[pallet::constant] type MaxEditReasonLen: Get; + #[pallet::constant] + type OutsiderBond: Get>; + /// The module identifier. #[pallet::constant] type PalletId: Get; @@ -1954,8 +2065,27 @@ mod pallet { impl Pallet { impl_unreserve_bond!(unreserve_creation_bond, creation); impl_unreserve_bond!(unreserve_oracle_bond, oracle); + impl_unreserve_bond!(unreserve_outsider_bond, outsider); impl_slash_bond!(slash_creation_bond, creation); impl_slash_bond!(slash_oracle_bond, oracle); + impl_slash_bond!(slash_outsider_bond, outsider); + 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); + + fn slash_pending_bonds(market_id: &MarketIdOf, market: &MarketOf) -> DispatchResult { + if Self::is_creation_bond_pending(market_id, market, false) { + Self::slash_creation_bond(market_id, None)?; + } + if Self::is_oracle_bond_pending(market_id, market, false) { + Self::slash_oracle_bond(market_id, None)?; + } + if Self::is_outsider_bond_pending(market_id, market, false) { + Self::slash_outsider_bond(market_id, None)?; + } + Ok(()) + } pub fn outcome_assets( market_id: MarketIdOf, @@ -2367,16 +2497,11 @@ mod pallet { if report.by == market.oracle { Self::unreserve_oracle_bond(market_id)?; } else { - let negative_imbalance = Self::slash_oracle_bond(market_id, None)?; - - // deposit only to the real reporter what actually was slashed - if let Err(err) = - T::AssetManager::deposit(Asset::Ztg, &report.by, negative_imbalance.peek()) - { - log::warn!( - "[PredictionMarkets] Cannot deposit to the reporter. error: {:?}", - err - ); + // reward outsider reporter with oracle bond + Self::repatriate_oracle_bond(market_id, &report.by)?; + + if Self::is_outsider_bond_pending(market_id, market, true) { + Self::unreserve_outsider_bond(market_id)?; } } @@ -2423,11 +2548,44 @@ mod pallet { // If the oracle reported right, return the OracleBond, otherwise slash it to // pay the correct reporters. let mut overall_imbalance = NegativeImbalanceOf::::zero(); - if report.by == market.oracle && report.outcome == resolved_outcome { - Self::unreserve_oracle_bond(market_id)?; + + let report_by_oracle = report.by == market.oracle; + let is_correct = report.outcome == resolved_outcome; + + let unreserve_outsider = || -> DispatchResult { + if Self::is_outsider_bond_pending(market_id, market, true) { + Self::unreserve_outsider_bond(market_id)?; + } + Ok(()) + }; + + let slash_outsider = || -> Result, DispatchError> { + if Self::is_outsider_bond_pending(market_id, market, true) { + let imbalance = Self::slash_outsider_bond(market_id, None)?; + return Ok(imbalance); + } + Ok(NegativeImbalanceOf::::zero()) + }; + + if report_by_oracle { + if is_correct { + Self::unreserve_oracle_bond(market_id)?; + } else { + let negative_imbalance = Self::slash_oracle_bond(market_id, None)?; + overall_imbalance.subsume(negative_imbalance); + } } else { - let imbalance = Self::slash_oracle_bond(market_id, None)?; - overall_imbalance.subsume(imbalance); + // outsider report + if is_correct { + // reward outsider reporter with oracle bond + Self::repatriate_oracle_bond(market_id, &report.by)?; + unreserve_outsider()?; + } else { + let oracle_imbalance = Self::slash_oracle_bond(market_id, None)?; + let outsider_imbalance = slash_outsider()?; + overall_imbalance.subsume(oracle_imbalance); + overall_imbalance.subsume(outsider_imbalance); + } } for (i, dispute) in disputes.iter().enumerate() { diff --git a/zrml/prediction-markets/src/migrations.rs b/zrml/prediction-markets/src/migrations.rs index 387d6236f..1f6fab770 100644 --- a/zrml/prediction-markets/src/migrations.rs +++ b/zrml/prediction-markets/src/migrations.rs @@ -17,35 +17,51 @@ #[cfg(feature = "try-runtime")] use crate::MarketIdOf; -use crate::{Config, MarketOf, MomentOf}; +use crate::{BalanceOf, Config, MomentOf}; +#[cfg(feature = "try-runtime")] +use alloc::collections::BTreeMap; #[cfg(feature = "try-runtime")] use alloc::format; -use alloc::{collections::BTreeMap, vec::Vec}; +use alloc::vec::Vec; +#[cfg(feature = "try-runtime")] +use frame_support::migration::storage_key_iter; #[cfg(feature = "try-runtime")] use frame_support::traits::OnRuntimeUpgradeHelpersExt; use frame_support::{ dispatch::Weight, log, - migration::{put_storage_value, storage_iter}, 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(feature = "try-runtime")] +use zrml_market_commons::MarketCommonsPalletApi; +use zrml_market_commons::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 = 4; -const MARKET_COMMONS_NEXT_STORAGE_VERSION: u16 = 5; + +const MARKET_COMMONS_REQUIRED_STORAGE_VERSION: u16 = 5; +const MARKET_COMMONS_NEXT_STORAGE_VERSION: u16 = 6; + +#[derive(Clone, Decode, Encode, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct OldMarketBonds { + pub creation: Option>, + pub oracle: Option>, +} #[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug, TypeInfo)] -pub struct OldMarket { +pub struct OldMarket { + pub base_asset: A, pub creator: AI, pub creation: MarketCreation, pub creator_fee: u8, @@ -59,88 +75,76 @@ pub struct OldMarket { 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 UpdateMarketsForBaseAssetAndRecordBonds(PhantomData); +pub struct AddOutsiderBond(PhantomData); -impl OnRuntimeUpgrade - for UpdateMarketsForBaseAssetAndRecordBonds -{ +impl OnRuntimeUpgrade for AddOutsiderBond { 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!( - "UpdateMarketsForBaseAssetAndRecordBonds: market-commons version is {:?}, but \ - {:?} is required", + "AddOutsiderBond: market-commons version is {:?}, but {:?} is required", market_commons_version, MARKET_COMMONS_REQUIRED_STORAGE_VERSION, ); return total_weight; } - log::info!("UpdateMarketsForBaseAssetAndRecordBonds: Starting..."); + log::info!("AddOutsiderBond: Starting..."); - let new_markets = storage_iter::>(MARKET_COMMONS, MARKETS) - .map(|(key, old_market)| { - total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); - let creation = Some(match old_market.creation { - MarketCreation::Advised => Bond { - who: old_market.creator.clone(), - value: T::AdvisoryBond::get(), - is_settled: old_market.status != MarketStatus::Proposed, - }, - MarketCreation::Permissionless => Bond { - who: old_market.creator.clone(), - value: T::ValidityBond::get(), - is_settled: matches!( - old_market.status, - MarketStatus::Resolved | MarketStatus::InsufficientSubsidy, - ), - }, - }); - let oracle = Some(Bond { - who: old_market.creator.clone(), - value: T::OracleBond::get(), - is_settled: matches!( - old_market.status, - MarketStatus::Resolved | MarketStatus::InsufficientSubsidy, - ), - }); - let new_market = Market { - base_asset: Asset::Ztg, - 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, oracle }, - }; - (key, new_market) - }) - .collect::>(); + let mut translated = 0u64; + zrml_market_commons::Markets::::translate::, _>(|_key, old_market| { + translated.saturating_inc(); - for (key, new_market) in new_markets { - put_storage_value::>(MARKET_COMMONS, MARKETS, &key, new_market); - total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); - } + 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: None, + }, + }; + + Some(new_market) + }); + log::info!("AddOutsiderBond: 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!("UpdateMarketsForBaseAssetAndRecordBonds: Done!"); + log::info!("AddOutsiderBond: Done!"); total_weight } @@ -154,6 +158,19 @@ impl OnRuntimeUpgrade ) .collect::>(); Self::set_temp_storage(old_markets, "old_markets"); + + 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(()) } @@ -167,13 +184,7 @@ impl OnRuntimeUpgrade let old_market = old_markets .get(&market_id) .expect(&format!("Market {:?} not found", market_id)[..]); - assert_eq!( - new_market.base_asset, - Asset::Ztg, - "found unexpected base_asset in new_market. market_id: {:?}, base_asset: {:?}", - market_id, - new_market.base_asset - ); + 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); @@ -187,13 +198,13 @@ impl OnRuntimeUpgrade 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.unwrap().value, T::OracleBond::get()); - let expected_creation_bond = match new_market.creation { - MarketCreation::Advised => T::AdvisoryBond::get(), - MarketCreation::Permissionless => T::ValidityBond::get(), - }; - assert_eq!(new_market.bonds.creation.unwrap().value, expected_creation_bond); + assert_eq!(new_market.bonds.oracle, old_market.bonds.oracle); + assert_eq!(new_market.bonds.creation, old_market.bonds.creation); + // new field + assert_eq!(new_market.bonds.outsider, None); } + log::info!("AddOutsiderBond: Market Counter post-upgrade is {}!", new_market_count); + assert!(new_market_count > 0); Ok(()) } } @@ -203,16 +214,18 @@ mod tests { use super::*; use crate::{ mock::{ExtBuilder, Runtime}, - MarketIdOf, + MarketIdOf, MarketOf, + }; + use frame_support::{ + dispatch::fmt::Debug, migration::put_storage_value, Blake2_128Concat, StorageHasher, }; - use frame_support::{dispatch::fmt::Debug, 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(); - UpdateMarketsForBaseAssetAndRecordBonds::::on_runtime_upgrade(); + AddOutsiderBond::::on_runtime_upgrade(); assert_eq!( StorageVersion::get::>(), MARKET_COMMONS_NEXT_STORAGE_VERSION @@ -224,20 +237,15 @@ mod tests { 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 test_vector = construct_test_vector(); - let new_markets = - test_vector.into_iter().map(|(_, new_market)| new_market).collect::>(); + let (_, new_markets) = construct_old_new_tuple(); populate_test_data::, MarketOf>( MARKET_COMMONS, MARKETS, new_markets.clone(), ); - UpdateMarketsForBaseAssetAndRecordBonds::::on_runtime_upgrade(); - for (market_id, expected) in new_markets.iter().enumerate() { - let actual = - >::market(&(market_id as u128)).unwrap(); - assert_eq!(actual, *expected); - } + AddOutsiderBond::::on_runtime_upgrade(); + let actual = >::market(&0u128).unwrap(); + assert_eq!(actual, new_markets[0]); }); } @@ -245,24 +253,15 @@ mod tests { fn on_runtime_upgrade_correctly_updates_markets() { ExtBuilder::default().build().execute_with(|| { set_up_version(); - let test_vector = construct_test_vector(); - let (old_markets, new_markets): (_, Vec>) = - test_vector.into_iter().unzip(); + let (old_markets, new_markets) = construct_old_new_tuple(); populate_test_data::, OldMarketOf>( MARKET_COMMONS, MARKETS, old_markets, ); - UpdateMarketsForBaseAssetAndRecordBonds::::on_runtime_upgrade(); - for (market_id, expected) in new_markets.iter().enumerate() { - let actual = - >::market(&(market_id as u128)).unwrap(); - assert_eq!(actual, *expected); - } - assert_eq!( - StorageVersion::get::>(), - MARKET_COMMONS_NEXT_STORAGE_VERSION - ); + AddOutsiderBond::::on_runtime_upgrade(); + let actual = >::market(&0u128).unwrap(); + assert_eq!(actual, new_markets[0]); }); } @@ -271,155 +270,66 @@ mod tests { .put::>(); } - fn construct_test_vector() -> Vec<(OldMarketOf, MarketOf)> { + fn construct_old_new_tuple() -> (Vec>, Vec>) { + let base_asset = Asset::Ztg; let creator = 999; - let construct_markets = |creation: MarketCreation, status, bonds| { - let base_asset = Asset::Ztg; - 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 report = None; - let resolved_outcome = None; - let dispute_mechanism = MarketDisputeMechanism::Authorized; - let deadlines = Deadlines::default(); + 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())), + }; + let new_bonds = MarketBonds { + creation: Some(Bond::new(creator, ::ValidityBond::get())), + oracle: Some(Bond::new(creator, ::OracleBond::get())), + outsider: None, + }; - let old_market = OldMarket { - 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, - }; - 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, - }; - (old_market, new_market) + 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, }; - vec![ - construct_markets( - MarketCreation::Permissionless, - MarketStatus::Disputed, - MarketBonds { - creation: Some(Bond { - who: creator, - value: ::ValidityBond::get(), - is_settled: false, - }), - oracle: Some(Bond { - who: creator, - value: ::OracleBond::get(), - is_settled: false, - }), - }, - ), - construct_markets( - MarketCreation::Permissionless, - MarketStatus::Resolved, - MarketBonds { - creation: Some(Bond { - who: creator, - value: ::ValidityBond::get(), - is_settled: true, - }), - oracle: Some(Bond { - who: creator, - value: ::OracleBond::get(), - is_settled: true, - }), - }, - ), - construct_markets( - MarketCreation::Advised, - MarketStatus::Proposed, - MarketBonds { - creation: Some(Bond { - who: creator, - value: ::AdvisoryBond::get(), - is_settled: false, - }), - oracle: Some(Bond { - who: creator, - value: ::OracleBond::get(), - is_settled: false, - }), - }, - ), - construct_markets( - MarketCreation::Advised, - MarketStatus::Active, - MarketBonds { - creation: Some(Bond { - who: creator, - value: ::AdvisoryBond::get(), - is_settled: true, - }), - oracle: Some(Bond { - who: creator, - value: ::OracleBond::get(), - is_settled: false, - }), - }, - ), - construct_markets( - MarketCreation::Advised, - MarketStatus::Resolved, - MarketBonds { - creation: Some(Bond { - who: creator, - value: ::AdvisoryBond::get(), - is_settled: true, - }), - oracle: Some(Bond { - who: creator, - value: ::OracleBond::get(), - is_settled: true, - }), - }, - ), - // Technically, the market below has the wrong scoring rule, but that's irrelevant to - // the test. - construct_markets( - MarketCreation::Permissionless, - MarketStatus::InsufficientSubsidy, - MarketBonds { - creation: Some(Bond { - who: creator, - value: ::ValidityBond::get(), - is_settled: true, - }), - oracle: Some(Bond { - who: creator, - value: ::OracleBond::get(), - is_settled: true, - }), - }, - ), - ] + 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)] @@ -437,286 +347,6 @@ mod tests { } } -#[cfg(feature = "try-runtime")] -use alloc::string::ToString; -use frame_support::{migration::storage_key_iter, Twox64Concat}; -use frame_system::pallet_prelude::BlockNumberFor; -use sp_runtime::traits::Saturating; -use zeitgeist_primitives::types::AuthorityReport; -use zrml_authorized::Pallet as AuthorizedPallet; - -const AUTHORIZED: &[u8] = b"Authorized"; -const AUTHORIZED_OUTCOME_REPORTS: &[u8] = b"AuthorizedOutcomeReports"; - -const AUTHORIZED_REQUIRED_STORAGE_VERSION: u16 = 2; -const AUTHORIZED_NEXT_STORAGE_VERSION: u16 = 3; - -pub struct AddFieldToAuthorityReport(PhantomData); - -// Add resolve_at block number value field to `AuthorizedOutcomeReports` map. -impl OnRuntimeUpgrade - for AddFieldToAuthorityReport -{ - fn on_runtime_upgrade() -> Weight - where - T: Config, - { - let mut total_weight = T::DbWeight::get().reads(1); - let authorized_version = StorageVersion::get::>(); - if authorized_version != AUTHORIZED_REQUIRED_STORAGE_VERSION { - log::info!( - "AddFieldToAuthorityReport: authorized version is {:?}, require {:?};", - authorized_version, - AUTHORIZED_REQUIRED_STORAGE_VERSION, - ); - return total_weight; - } - log::info!("AddFieldToAuthorityReport: Starting..."); - - let mut authorized_resolutions = - BTreeMap::<::MarketId, BlockNumberFor>::new(); - for (resolve_at, bounded_vec) in crate::MarketIdsPerDisputeBlock::::iter() { - total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); - - for id in bounded_vec.into_inner().iter() { - if let Ok(market) = >::market(id) { - if market.dispute_mechanism == MarketDisputeMechanism::Authorized { - authorized_resolutions.insert(*id, resolve_at); - } - } else { - log::warn!("AddFieldToAuthorityReport: Could not find market with id {:?}", id); - } - } - } - - let mut new_storage_map: Vec<( - ::MarketId, - AuthorityReport>, - )> = Vec::new(); - - let now = frame_system::Pallet::::block_number(); - total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); - - for (market_id, old_value) in storage_key_iter::< - ::MarketId, - OutcomeReport, - Twox64Concat, - >(AUTHORIZED, AUTHORIZED_OUTCOME_REPORTS) - { - total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); - - let resolve_at: Option> = - authorized_resolutions.get(&market_id).cloned(); - - match resolve_at { - Some(block) if now <= block => { - new_storage_map.push(( - market_id, - AuthorityReport { resolve_at: block, outcome: old_value }, - )); - } - _ => { - log::warn!( - "AddFieldToAuthorityReport: Market was not found in \ - MarketIdsPerDisputeBlock; market id: {:?}", - market_id - ); - // example case market id 432 - // https://github.com/zeitgeistpm/zeitgeist/pull/701 market id 432 is invalid, because of zero-division error in the past - // we have to handle manually here, because MarketIdsPerDisputeBlock does not contain 432 - let mut resolve_at = now.saturating_add(T::CorrectionPeriod::get()); - total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); - - let mut bounded_vec = >::get(resolve_at); - while bounded_vec.is_full() { - // roll the dice until we find a block that is not full - total_weight = total_weight.saturating_add(T::DbWeight::get().reads(1)); - resolve_at = resolve_at.saturating_add(1u32.into()); - bounded_vec = >::get(resolve_at); - } - // is not full, so we can push - bounded_vec.force_push(market_id); - >::insert(resolve_at, bounded_vec); - total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); - - new_storage_map - .push((market_id, AuthorityReport { resolve_at, outcome: old_value })); - } - } - } - - for (market_id, new_value) in new_storage_map { - let hash = utility::key_to_hash::< - Twox64Concat, - ::MarketId, - >(market_id); - put_storage_value::>( - AUTHORIZED, - AUTHORIZED_OUTCOME_REPORTS, - &hash, - new_value, - ); - total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); - } - - StorageVersion::new(AUTHORIZED_NEXT_STORAGE_VERSION).put::>(); - total_weight = total_weight.saturating_add(T::DbWeight::get().writes(1)); - log::info!("AddFieldToAuthorityReport: Done!"); - total_weight - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result<(), &'static str> { - let mut counter = 0_u32; - for (key, value) in storage_iter::(AUTHORIZED, AUTHORIZED_OUTCOME_REPORTS) { - Self::set_temp_storage(value, &format!("{:?}", key.as_slice())); - - counter = counter.saturating_add(1_u32); - } - let counter_key = "counter_key".to_string(); - Self::set_temp_storage(counter, &counter_key); - Ok(()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade() -> Result<(), &'static str> { - let mut markets_count = 0_u32; - let old_counter_key = "counter_key".to_string(); - for (key, new_value) in - storage_iter::>(AUTHORIZED, AUTHORIZED_OUTCOME_REPORTS) - { - let key_str = format!("{:?}", key.as_slice()); - - let AuthorityReport { resolve_at: _, outcome } = new_value; - let old_value: OutcomeReport = Self::get_temp_storage(&key_str) - .unwrap_or_else(|| panic!("old value not found for market id {:?}", key_str)); - - assert_eq!(old_value, outcome); - - markets_count += 1_u32; - } - let old_markets_count: u32 = - Self::get_temp_storage(&old_counter_key).expect("old counter key storage not found"); - assert_eq!(markets_count, old_markets_count); - Ok(()) - } -} - -#[cfg(test)] -mod tests_authorized { - use super::*; - use crate::{ - mock::{ExtBuilder, MarketCommons, Runtime, ALICE, BOB}, - CacheSize, MarketIdOf, - }; - use frame_support::{BoundedVec, Twox64Concat}; - use zeitgeist_primitives::types::{MarketId, OutcomeReport}; - use zrml_market_commons::MarketCommonsPalletApi; - - #[test] - fn on_runtime_upgrade_increments_the_storage_versions() { - ExtBuilder::default().build().execute_with(|| { - set_up_chain(); - AddFieldToAuthorityReport::::on_runtime_upgrade(); - let authorized_version = StorageVersion::get::>(); - assert_eq!(authorized_version, AUTHORIZED_NEXT_STORAGE_VERSION); - }); - } - - #[test] - fn on_runtime_sets_new_struct_with_resolve_at() { - ExtBuilder::default().build().execute_with(|| { - set_up_chain(); - - >::set_block_number(10_000); - - let hash = crate::migrations::utility::key_to_hash::(0); - let outcome = OutcomeReport::Categorical(42u16); - put_storage_value::( - AUTHORIZED, - AUTHORIZED_OUTCOME_REPORTS, - &hash, - outcome.clone(), - ); - - let resolve_at = 42_000; - - let sample_market = get_sample_market(); - let market_id: MarketId = MarketCommons::push_market(sample_market).unwrap(); - let bounded_vec = - BoundedVec::, CacheSize>::try_from(vec![market_id]) - .expect("BoundedVec should be created"); - crate::MarketIdsPerDisputeBlock::::insert(resolve_at, bounded_vec); - - AddFieldToAuthorityReport::::on_runtime_upgrade(); - - let expected = AuthorityReport { resolve_at, outcome }; - - let actual = frame_support::migration::get_storage_value::< - AuthorityReport<::BlockNumber>, - >(AUTHORIZED, AUTHORIZED_OUTCOME_REPORTS, &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(AUTHORIZED_NEXT_STORAGE_VERSION).put::>(); - - let hash = crate::migrations::utility::key_to_hash::(0); - let outcome = OutcomeReport::Categorical(42u16); - - let report = AuthorityReport { resolve_at: 42, outcome }; - put_storage_value::::BlockNumber>>( - AUTHORIZED, - AUTHORIZED_OUTCOME_REPORTS, - &hash, - report.clone(), - ); - - AddFieldToAuthorityReport::::on_runtime_upgrade(); - - let actual = frame_support::migration::get_storage_value::< - AuthorityReport<::BlockNumber>, - >(AUTHORIZED, AUTHORIZED_OUTCOME_REPORTS, &hash) - .unwrap(); - assert_eq!(report, actual); - }); - } - - fn set_up_chain() { - StorageVersion::new(AUTHORIZED_REQUIRED_STORAGE_VERSION).put::>(); - } - - fn get_sample_market() -> zeitgeist_primitives::types::Market> - { - zeitgeist_primitives::types::Market { - base_asset: Asset::Ztg, - creation: zeitgeist_primitives::types::MarketCreation::Permissionless, - creator_fee: 0, - creator: ALICE, - market_type: zeitgeist_primitives::types::MarketType::Scalar(0..=100), - dispute_mechanism: zeitgeist_primitives::types::MarketDisputeMechanism::Authorized, - metadata: Default::default(), - oracle: BOB, - 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: zeitgeist_primitives::types::ScoringRule::CPMM, - status: zeitgeist_primitives::types::MarketStatus::Disputed, - bonds: Default::default(), - } - } -} - // 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 diff --git a/zrml/prediction-markets/src/mock.rs b/zrml/prediction-markets/src/mock.rs index 7c77a017a..b537658c6 100644 --- a/zrml/prediction-markets/src/mock.rs +++ b/zrml/prediction-markets/src/mock.rs @@ -44,8 +44,9 @@ use zeitgeist_primitives::{ MaxInRatio, MaxMarketLifetime, MaxOracleDuration, MaxOutRatio, MaxRejectReasonLen, MaxReserves, MaxSubsidyPeriod, MaxSwapFee, MaxTotalWeight, MaxWeight, MinAssets, MinCategories, MinDisputeDuration, MinLiquidity, MinOracleDuration, MinSubsidy, - MinSubsidyPeriod, MinWeight, MinimumPeriod, PmPalletId, SimpleDisputesPalletId, - StakeWeight, SwapsPalletId, TreasuryPalletId, BASE, CENT, MILLISECS_PER_BLOCK, + MinSubsidyPeriod, MinWeight, MinimumPeriod, OutsiderBond, PmPalletId, + SimpleDisputesPalletId, StakeWeight, SwapsPalletId, TreasuryPalletId, BASE, CENT, + MILLISECS_PER_BLOCK, }, types::{ AccountIdTest, Amount, Asset, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, @@ -168,6 +169,7 @@ impl crate::Config for Runtime { type MaxEditReasonLen = MaxEditReasonLen; type MaxRejectReasonLen = MaxRejectReasonLen; type OracleBond = OracleBond; + type OutsiderBond = OutsiderBond; type PalletId = PmPalletId; type RejectOrigin = EnsureSignedBy; type RequestEditOrigin = EnsureSignedBy; diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index fe5d8da83..a44be1265 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -34,7 +34,7 @@ use test_case::test_case; use orml_traits::{MultiCurrency, MultiReservableCurrency}; use sp_runtime::traits::{AccountIdConversion, SaturatedConversion, Zero}; use zeitgeist_primitives::{ - constants::mock::{DisputeFactor, BASE, CENT, MILLISECS_PER_BLOCK}, + constants::mock::{DisputeFactor, OutsiderBond, BASE, CENT, MILLISECS_PER_BLOCK}, traits::Swaps as SwapsPalletApi, types::{ AccountIdTest, Asset, Balance, BlockNumber, Bond, Deadlines, Market, MarketBonds, @@ -63,6 +63,30 @@ fn gen_metadata(byte: u8) -> MultiHash { MultiHash::Sha3_384(metadata) } +fn reserve_sentinel_amounts() { + // Reserve a sentinel amount to check that we don't unreserve too much. + assert_ok!(Balances::reserve_named(&PredictionMarkets::reserve_id(), &ALICE, SENTINEL_AMOUNT)); + assert_ok!(Balances::reserve_named(&PredictionMarkets::reserve_id(), &BOB, SENTINEL_AMOUNT)); + assert_ok!(Balances::reserve_named( + &PredictionMarkets::reserve_id(), + &CHARLIE, + SENTINEL_AMOUNT + )); + assert_ok!(Balances::reserve_named(&PredictionMarkets::reserve_id(), &DAVE, SENTINEL_AMOUNT)); + assert_ok!(Balances::reserve_named(&PredictionMarkets::reserve_id(), &EVE, SENTINEL_AMOUNT)); + assert_ok!(Balances::reserve_named(&PredictionMarkets::reserve_id(), &FRED, SENTINEL_AMOUNT)); + assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + assert_eq!(Balances::reserved_balance(&BOB), SENTINEL_AMOUNT); + assert_eq!(Balances::reserved_balance(&CHARLIE), SENTINEL_AMOUNT); + assert_eq!(Balances::reserved_balance(&DAVE), SENTINEL_AMOUNT); + assert_eq!(Balances::reserved_balance(&EVE), SENTINEL_AMOUNT); + assert_eq!(Balances::reserved_balance(&FRED), SENTINEL_AMOUNT); +} + +fn check_reserve(account: &AccountIdTest, expected: Balance) { + assert_eq!(Balances::reserved_balance(account), SENTINEL_AMOUNT + expected); +} + fn simple_create_categorical_market( base_asset: Asset, creation: MarketCreation, @@ -3908,6 +3932,7 @@ fn authorized_correctly_resolves_disputed_market() { fn approve_market_correctly_unreserves_advisory_bond() { // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), base_asset, @@ -3921,19 +3946,10 @@ fn approve_market_correctly_unreserves_advisory_bond() { ScoringRule::CPMM, )); let market_id = 0; - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!( - Balances::reserved_balance(&ALICE), - SENTINEL_AMOUNT + AdvisoryBond::get() + OracleBond::get() - ); + check_reserve(&ALICE, AdvisoryBond::get() + OracleBond::get()); assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), market_id)); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT + OracleBond::get()); + check_reserve(&ALICE, OracleBond::get()); assert_eq!(Balances::free_balance(&ALICE), alice_balance_before + AdvisoryBond::get()); let market = MarketCommons::market(&market_id).unwrap(); assert!(market.bonds.creation.unwrap().is_settled); @@ -4056,6 +4072,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark { // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4069,17 +4086,8 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!( - Balances::reserved_balance(&ALICE), - SENTINEL_AMOUNT + ValidityBond::get() + OracleBond::get() - ); + check_reserve(&ALICE, ValidityBond::get() + OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; run_to_block(grace_period + 1); @@ -4089,7 +4097,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark OutcomeReport::Categorical(0) )); run_to_block(grace_period + market.deadlines.dispute_duration + 1); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); assert_eq!( Balances::free_balance(&ALICE), alice_balance_before + ValidityBond::get() + OracleBond::get() @@ -4109,6 +4117,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark { // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4122,30 +4131,111 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!( - Balances::reserved_balance(&ALICE), - SENTINEL_AMOUNT + ValidityBond::get() + OracleBond::get() - ); + check_reserve(&ALICE, ValidityBond::get() + OracleBond::get()); + + let charlie_balance_before = Balances::free_balance(&CHARLIE); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; let report_at = grace_period + market.deadlines.oracle_duration + 1; run_to_block(report_at); + + assert!(market.bonds.outsider.is_none()); assert_ok!(PredictionMarkets::report( Origin::signed(CHARLIE), 0, OutcomeReport::Categorical(1) )); + + let market = MarketCommons::market(&0).unwrap(); + assert_eq!(market.bonds.outsider, Some(Bond::new(CHARLIE, OutsiderBond::get()))); + check_reserve(&CHARLIE, OutsiderBond::get()); + assert_eq!(Balances::free_balance(&CHARLIE), charlie_balance_before - OutsiderBond::get()); + let charlie_balance_before = Balances::free_balance(&CHARLIE); + run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // Check that validity bond didn't get slashed, but oracle bond did assert_eq!(Balances::free_balance(&ALICE), alice_balance_before + ValidityBond::get()); + + check_reserve(&CHARLIE, 0); + // Check that the outsider gets the OracleBond together with the OutsiderBond + assert_eq!( + Balances::free_balance(&CHARLIE), + charlie_balance_before + OracleBond::get() + OutsiderBond::get() + ); + let market = MarketCommons::market(&0).unwrap(); + assert!(market.bonds.outsider.unwrap().is_settled); + }; + ExtBuilder::default().build().execute_with(|| { + test(Asset::Ztg); + }); + #[cfg(feature = "parachain")] + ExtBuilder::default().build().execute_with(|| { + test(Asset::ForeignAsset(100)); + }); +} + +#[test] +fn outsider_reports_wrong_outcome() { + // NOTE: Bonds are always in ZTG, irrespective of base_asset. + let test = |base_asset: Asset| { + reserve_sentinel_amounts(); + + let end = 100; + let alice_balance_before = Balances::free_balance(&ALICE); + assert_ok!(PredictionMarkets::create_market( + Origin::signed(ALICE), + base_asset, + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(2), + MarketDisputeMechanism::SimpleDisputes, + ScoringRule::CPMM, + )); + + let outsider = CHARLIE; + + let market = MarketCommons::market(&0).unwrap(); + let grace_period = end + market.deadlines.grace_period; + let report_at = grace_period + market.deadlines.oracle_duration + 1; + run_to_block(report_at); + assert_ok!(PredictionMarkets::report( + Origin::signed(outsider), + 0, + OutcomeReport::Categorical(1) + )); + + let outsider_balance_before = Balances::free_balance(&outsider); + check_reserve(&outsider, OutsiderBond::get()); + + let dispute_at_0 = report_at + 1; + run_to_block(dispute_at_0); + assert_ok!(PredictionMarkets::dispute( + Origin::signed(EVE), + 0, + OutcomeReport::Categorical(0) + )); + + let eve_balance_before = Balances::free_balance(&EVE); + + // on_resolution called + run_blocks(market.deadlines.dispute_duration); + + assert_eq!(Balances::free_balance(&ALICE), alice_balance_before - OracleBond::get()); + + 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 + assert_eq!( + Balances::free_balance(&EVE), + eve_balance_before + dispute_bond + OutsiderBond::get() + OracleBond::get() + ); }; ExtBuilder::default().build().execute_with(|| { test(Asset::Ztg); @@ -4161,6 +4251,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma { // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4174,15 +4265,9 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), 0)); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT + OracleBond::get()); + check_reserve(&ALICE, OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; let report_at = grace_period + 1; @@ -4193,7 +4278,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma OutcomeReport::Categorical(1) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // Check that nothing got slashed assert_eq!(Balances::free_balance(&ALICE), alice_balance_before + OracleBond::get()); }; @@ -4211,6 +4296,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma { // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4224,15 +4310,9 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), 0)); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT + OracleBond::get()); + check_reserve(&ALICE, OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; let report_at = grace_period + market.deadlines.oracle_duration + 1; @@ -4244,7 +4324,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma )); run_blocks(market.deadlines.dispute_duration); // Check that oracle bond got slashed - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); assert_eq!(Balances::free_balance(&ALICE), alice_balance_before); }; ExtBuilder::default().build().execute_with(|| { @@ -4262,6 +4342,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark // Oracle reports in time but incorrect report, so OracleBond gets slashed on resolution // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4275,17 +4356,8 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!( - Balances::reserved_balance(&ALICE), - SENTINEL_AMOUNT + ValidityBond::get() + OracleBond::get() - ); + check_reserve(&ALICE, ValidityBond::get() + OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; run_to_block(grace_period + 1); @@ -4300,7 +4372,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark OutcomeReport::Categorical(1) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // ValidityBond bond is returned but OracleBond is slashed assert_eq!(Balances::free_balance(&ALICE), alice_balance_before + ValidityBond::get()); }; @@ -4319,6 +4391,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma // Oracle reports in time but incorrect report, so OracleBond gets slashed on resolution // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4332,15 +4405,9 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), 0)); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT + OracleBond::get()); + check_reserve(&ALICE, OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; run_to_block(grace_period + 1); @@ -4355,7 +4422,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_approved_advised_ma OutcomeReport::Categorical(1) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // ValidityBond bond is returned but OracleBond is slashed assert_eq!(Balances::free_balance(&ALICE), alice_balance_before); }; @@ -4374,6 +4441,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark // Oracle reports in time and correct report, so OracleBond does not get slashed on resolution // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4387,17 +4455,8 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!( - Balances::reserved_balance(&ALICE), - SENTINEL_AMOUNT + ValidityBond::get() + OracleBond::get() - ); + check_reserve(&ALICE, ValidityBond::get() + OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; run_to_block(grace_period + 1); @@ -4418,7 +4477,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark OutcomeReport::Categorical(0) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // ValidityBond bond is returned but OracleBond is not slashed assert_eq!( Balances::free_balance(&ALICE), @@ -4440,6 +4499,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma // Oracle reports in time and correct report, so OracleBond does not get slashed on resolution // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4453,15 +4513,9 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), 0)); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT + OracleBond::get()); + check_reserve(&ALICE, OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let grace_period = end + market.deadlines.grace_period; run_to_block(grace_period + 1); @@ -4482,7 +4536,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma OutcomeReport::Categorical(0) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // ValidityBond bond is returned but OracleBond is not slashed assert_eq!(Balances::free_balance(&ALICE), alice_balance_before + OracleBond::get()); }; @@ -4501,6 +4555,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark // Oracle does not report in time, so OracleBond gets slashed on resolution // NOTE: Bonds are always in ZTG, irrespective of base_asset. let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4514,27 +4569,26 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); + let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!( - Balances::reserved_balance(&ALICE), - SENTINEL_AMOUNT + ValidityBond::get() + OracleBond::get() - ); + check_reserve(&ALICE, ValidityBond::get() + OracleBond::get()); + + let outsider = CHARLIE; + let market = MarketCommons::market(&0).unwrap(); let after_oracle_duration = end + market.deadlines.grace_period + market.deadlines.oracle_duration + 1; run_to_block(after_oracle_duration); // CHARLIE is not an Oracle assert_ok!(PredictionMarkets::report( - Origin::signed(CHARLIE), + Origin::signed(outsider), 0, OutcomeReport::Categorical(0) )); + + let outsider_balance_before = Balances::free_balance(&outsider); + check_reserve(&outsider, OutsiderBond::get()); + // EVE disputes with wrong outcome assert_ok!(PredictionMarkets::dispute( Origin::signed(EVE), @@ -4547,9 +4601,15 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_permissionless_mark OutcomeReport::Categorical(0) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // ValidityBond bond is returned but OracleBond is slashed assert_eq!(Balances::free_balance(&ALICE), alice_balance_before + ValidityBond::get()); + + check_reserve(&outsider, 0); + assert_eq!( + Balances::free_balance(&outsider), + outsider_balance_before + OracleBond::get() + OutsiderBond::get() + ); }; ExtBuilder::default().build().execute_with(|| { test(Asset::Ztg); @@ -4566,6 +4626,7 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma // Oracle does not report in time, so OracleBond gets slashed on resolution // NOTE: Bonds are always in ZTG let test = |base_asset: Asset| { + reserve_sentinel_amounts(); let end = 100; assert_ok!(PredictionMarkets::create_market( Origin::signed(ALICE), @@ -4579,25 +4640,26 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma MarketDisputeMechanism::SimpleDisputes, ScoringRule::CPMM, )); - // Reserve a sentinel amount to check that we don't unreserve too much. - assert_ok!(Balances::reserve_named( - &PredictionMarkets::reserve_id(), - &ALICE, - SENTINEL_AMOUNT - )); + + let outsider = CHARLIE; + assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), 0)); let alice_balance_before = Balances::free_balance(&ALICE); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT + OracleBond::get()); + check_reserve(&ALICE, OracleBond::get()); let market = MarketCommons::market(&0).unwrap(); let after_oracle_duration = end + market.deadlines.grace_period + market.deadlines.oracle_duration + 1; run_to_block(after_oracle_duration); // CHARLIE is not an Oracle assert_ok!(PredictionMarkets::report( - Origin::signed(CHARLIE), + Origin::signed(outsider), 0, OutcomeReport::Categorical(0) )); + + let outsider_balance_before = Balances::free_balance(&outsider); + check_reserve(&outsider, OutsiderBond::get()); + // EVE disputes with wrong outcome assert_ok!(PredictionMarkets::dispute( Origin::signed(EVE), @@ -4610,9 +4672,15 @@ fn on_resolution_correctly_reserves_and_unreserves_bonds_for_advised_approved_ma OutcomeReport::Categorical(0) )); run_blocks(market.deadlines.dispute_duration); - assert_eq!(Balances::reserved_balance(&ALICE), SENTINEL_AMOUNT); + check_reserve(&ALICE, 0); // ValidityBond bond is returned but OracleBond is slashed assert_eq!(Balances::free_balance(&ALICE), alice_balance_before); + + check_reserve(&outsider, 0); + assert_eq!( + Balances::free_balance(&outsider), + outsider_balance_before + OracleBond::get() + OutsiderBond::get() + ); }; ExtBuilder::default().build().execute_with(|| { test(Asset::Ztg); @@ -4936,6 +5004,7 @@ fn create_market_fails_if_market_duration_is_too_long_in_moments() { MarketBonds { creation: Some(Bond::new(ALICE, ::AdvisoryBond::get())), oracle: Some(Bond::new(ALICE, ::OracleBond::get())), + outsider: None, } )] #[test_case( @@ -4945,6 +5014,7 @@ fn create_market_fails_if_market_duration_is_too_long_in_moments() { MarketBonds { creation: Some(Bond::new(ALICE, ::ValidityBond::get())), oracle: Some(Bond::new(ALICE, ::OracleBond::get())), + outsider: None, } )] fn create_market_sets_the_correct_market_parameters_and_reserves_the_correct_amount( diff --git a/zrml/simple-disputes/src/tests.rs b/zrml/simple-disputes/src/tests.rs index 85a01b8b1..05b93a356 100644 --- a/zrml/simple-disputes/src/tests.rs +++ b/zrml/simple-disputes/src/tests.rs @@ -45,7 +45,7 @@ const DEFAULT_MARKET: MarketOf = Market { resolved_outcome: None, scoring_rule: ScoringRule::CPMM, status: MarketStatus::Disputed, - bonds: MarketBonds { creation: None, oracle: None }, + bonds: MarketBonds { creation: None, oracle: None, outsider: None }, }; #[test]