diff --git a/docs/changelog_for_devs.md b/docs/changelog_for_devs.md index a1aa9f888..8593db492 100644 --- a/docs/changelog_for_devs.md +++ b/docs/changelog_for_devs.md @@ -1,3 +1,10 @@ +# v0.3.7 + +- `reject_market` extrinsic now requires `reject_reason` parameter which is + `Vec`. The config constant `MaxRejectReasonLen` defines maximum length of + above parameter. `MarketRejected` event also contains `reject_reason` so that + it can be cached for market creator. + # v0.3.6 - Added new field `deadlines` in Market structure, which has `grace_period`, diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs index d40df9d76..7f124d64d 100644 --- a/primitives/src/constants/mock.rs +++ b/primitives/src/constants/mock.rs @@ -46,6 +46,7 @@ parameter_types! { pub const MaxDisputeDuration: BlockNumber = 5; pub const MaxGracePeriod: BlockNumber = 2; pub const MaxOracleDuration: BlockNumber = 3; + pub const MaxRejectReasonLen: u32 = 1024; } // Simple disputes parameters diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs index 939fd034f..0c29c4bbb 100644 --- a/runtime/battery-station/src/parameters.rs +++ b/runtime/battery-station/src/parameters.rs @@ -190,6 +190,8 @@ parameter_types! { /// (Slashable) A bond for creation markets that do not require approval. Slashed in case /// the market is forcefully destroyed. pub const ValidityBond: Balance = 50 * CENT; + /// Maximum string length allowed for reject reason. + pub const MaxRejectReasonLen: u32 = 1024; // Preimage pub const PreimageMaxSize: u32 = 4096 * 1024; diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index a1374bbd7..0073a1bb5 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -956,6 +956,7 @@ macro_rules! impl_config_traits { type MaxMarketPeriod = MaxMarketPeriod; type MinCategories = MinCategories; type MinSubsidyPeriod = MinSubsidyPeriod; + type MaxRejectReasonLen = MaxRejectReasonLen; type OracleBond = OracleBond; type PalletId = PmPalletId; type RejectOrigin = EnsureRootOrHalfAdvisoryCommittee; diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs index 4c81b1d27..470bc6a60 100644 --- a/runtime/zeitgeist/src/parameters.rs +++ b/runtime/zeitgeist/src/parameters.rs @@ -190,6 +190,8 @@ parameter_types! { /// (Slashable) A bond for creation markets that do not require approval. Slashed in case /// the market is forcefully destroyed. pub const ValidityBond: Balance = 1_000 * BASE; + /// Maximum string length allowed for reject reason. + pub const MaxRejectReasonLen: u32 = 1024; // Preimage pub const PreimageMaxSize: u32 = 4096 * 1024; diff --git a/zrml/prediction-markets/src/benchmarks.rs b/zrml/prediction-markets/src/benchmarks.rs index 9c7c5f5ff..f57f8de22 100644 --- a/zrml/prediction-markets/src/benchmarks.rs +++ b/zrml/prediction-markets/src/benchmarks.rs @@ -890,6 +890,7 @@ benchmarks! { reject_market { let c in 0..63; let o in 0..63; + let r in 0..::MaxRejectReasonLen::get(); let range_start: MomentOf = 100_000u64.saturated_into(); let range_end: MomentOf = 1_000_000u64.saturated_into(); @@ -915,7 +916,8 @@ benchmarks! { } let reject_origin = T::RejectOrigin::successful_origin(); - let call = Call::::reject_market { market_id }; + let reject_reason: Vec = vec![0; r as usize]; + let call = Call::::reject_market { market_id, reject_reason }; }: { call.dispatch_bypass_filter(reject_origin)? } report { diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index ad3c44986..8fb2f4868 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -81,6 +81,7 @@ mod pallet { <::MarketCommons as MarketCommonsPalletApi>::MarketId; pub(crate) type MomentOf = <::MarketCommons as MarketCommonsPalletApi>::Moment; pub type CacheSize = ConstU32<64>; + pub type RejectReason = BoundedVec::MaxRejectReasonLen>; #[pallet::call] impl Pallet { @@ -978,21 +979,34 @@ mod pallet { /// and `m` is the number of market ids, /// which close at the same time as the specified market. #[pallet::weight(( - T::WeightInfo::reject_market(CacheSize::get(), CacheSize::get()), + T::WeightInfo::reject_market( + CacheSize::get(), + CacheSize::get(), + reject_reason.len() as u32, + ), Pays::No, ))] #[transactional] pub fn reject_market( origin: OriginFor, #[pallet::compact] market_id: MarketIdOf, + reject_reason: Vec, ) -> DispatchResultWithPostInfo { T::RejectOrigin::ensure_origin(origin)?; let market = T::MarketCommons::market(&market_id)?; let open_ids_len = Self::clear_auto_open(&market_id)?; let close_ids_len = Self::clear_auto_close(&market_id)?; - Self::do_reject_market(&market_id, market)?; + let reject_reason: RejectReason = reject_reason + .try_into() + .map_err(|_| Error::::RejectReasonLengthExceedsMaxRejectReasonLen)?; + let reject_reason_len = reject_reason.len() as u32; + Self::do_reject_market(&market_id, market, reject_reason)?; // The RejectOrigin should not pay fees for providing this service - Ok((Some(T::WeightInfo::reject_market(close_ids_len, open_ids_len)), Pays::No).into()) + Ok(( + Some(T::WeightInfo::reject_market(close_ids_len, open_ids_len, reject_reason_len)), + Pays::No, + ) + .into()) } /// Reports the outcome of a market. @@ -1263,6 +1277,10 @@ mod pallet { #[pallet::constant] type MaxDisputeDuration: Get; + /// The maximum length of reject reason string. + #[pallet::constant] + type MaxRejectReasonLen: Get; + //NOTE: DisputePeriod will be removed once relevant migrations are executed. /// The number of blocks the dispute period remains open. #[pallet::constant] @@ -1361,6 +1379,8 @@ mod pallet { NoWinningBalance, /// Submitted outcome does not match market type. OutcomeMismatch, + /// RejectReason's length greater than MaxRejectReasonLen. + RejectReasonLengthExceedsMaxRejectReasonLen, /// The report is not coming from designated oracle. ReporterNotOracle, /// It was tried to append an item to storage beyond the boundaries. @@ -1424,8 +1444,8 @@ mod pallet { MarketDisputed(MarketIdOf, MarketStatus, MarketDispute), /// An advised market has ended before it was approved or rejected. \[market_id\] MarketExpired(MarketIdOf), - /// A pending market has been rejected as invalid. \[market_id\] - MarketRejected(MarketIdOf), + /// A pending market has been rejected as invalid with a reason. \[market_id, reject_reason\] + MarketRejected(MarketIdOf, RejectReason), /// A market has been reported on \[market_id, new_market_status, reported_outcome\] MarketReported(MarketIdOf, MarketStatus, Report), /// A market has been resolved \[market_id, new_market_status, real_outcome\] @@ -1796,6 +1816,7 @@ mod pallet { pub(crate) fn do_reject_market( market_id: &MarketIdOf, market: Market>, + reject_reason: RejectReason, ) -> DispatchResult { ensure!(market.status == MarketStatus::Proposed, Error::::InvalidMarketStatus); let creator = &market.creator; @@ -1816,7 +1837,7 @@ mod pallet { T::OracleBond::get().saturating_add(advisory_bond_unreserve_amount), ); T::MarketCommons::remove_market(market_id)?; - Self::deposit_event(Event::MarketRejected(*market_id)); + Self::deposit_event(Event::MarketRejected(*market_id, reject_reason)); Self::deposit_event(Event::MarketDestroyed(*market_id)); Ok(()) } diff --git a/zrml/prediction-markets/src/mock.rs b/zrml/prediction-markets/src/mock.rs index 4ee552503..49714c28f 100644 --- a/zrml/prediction-markets/src/mock.rs +++ b/zrml/prediction-markets/src/mock.rs @@ -39,10 +39,11 @@ use zeitgeist_primitives::{ CourtPalletId, DisputeFactor, ExistentialDeposit, ExistentialDeposits, ExitFee, GetNativeCurrencyId, LiquidityMiningPalletId, MaxApprovals, MaxAssets, MaxCategories, MaxDisputeDuration, MaxDisputes, MaxGracePeriod, MaxInRatio, MaxMarketPeriod, - MaxOracleDuration, MaxOutRatio, MaxReserves, MaxSubsidyPeriod, MaxSwapFee, MaxTotalWeight, - MaxWeight, MinAssets, MinCategories, MinDisputeDuration, MinLiquidity, MinOracleDuration, - MinSubsidy, MinSubsidyPeriod, MinWeight, MinimumPeriod, PmPalletId, SimpleDisputesPalletId, - StakeWeight, SwapsPalletId, TreasuryPalletId, BASE, CENT, MILLISECS_PER_BLOCK, + 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, }, types::{ AccountIdTest, Amount, Asset, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest, @@ -125,6 +126,7 @@ impl crate::Config for Runtime { type MaxMarketPeriod = MaxMarketPeriod; type MinCategories = MinCategories; type MinSubsidyPeriod = MinSubsidyPeriod; + type MaxRejectReasonLen = MaxRejectReasonLen; type OracleBond = OracleBond; type PalletId = PmPalletId; type RejectOrigin = EnsureSignedBy; diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index cd8e956a6..6eadc3e93 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -895,15 +895,24 @@ fn it_allows_advisory_origin_to_approve_markets() { #[test] fn it_allows_the_advisory_origin_to_reject_markets() { ExtBuilder::default().build().execute_with(|| { + run_to_block(2); // Creates an advised market. - simple_create_categorical_market(MarketCreation::Advised, 0..1, ScoringRule::CPMM); + simple_create_categorical_market(MarketCreation::Advised, 4..6, ScoringRule::CPMM); // make sure it's in status proposed let market = MarketCommons::market(&0); assert_eq!(market.unwrap().status, MarketStatus::Proposed); + let reject_reason: Vec = + vec![0; ::MaxRejectReasonLen::get() as usize]; // Now it should work from SUDO - assert_ok!(PredictionMarkets::reject_market(Origin::signed(SUDO), 0)); + assert_ok!(PredictionMarkets::reject_market( + Origin::signed(SUDO), + 0, + reject_reason.clone() + )); + let reject_reason = reject_reason.try_into().expect("BoundedVec conversion failed"); + System::assert_has_event(Event::MarketRejected(0, reject_reason).into()); assert_noop!( MarketCommons::market(&0), @@ -912,6 +921,25 @@ fn it_allows_the_advisory_origin_to_reject_markets() { }); } +#[test] +fn reject_errors_if_reject_reason_is_too_long() { + ExtBuilder::default().build().execute_with(|| { + // Creates an advised market. + simple_create_categorical_market(MarketCreation::Advised, 0..1, ScoringRule::CPMM); + + // make sure it's in status proposed + let market = MarketCommons::market(&0); + assert_eq!(market.unwrap().status, MarketStatus::Proposed); + + let reject_reason: Vec = + vec![0; ::MaxRejectReasonLen::get() as usize + 1]; + assert_noop!( + PredictionMarkets::reject_market(Origin::signed(SUDO), 0, reject_reason), + Error::::RejectReasonLengthExceedsMaxRejectReasonLen + ); + }); +} + #[test] fn reject_market_unreserves_oracle_bond_and_slashes_advisory_bond() { ExtBuilder::default().build().execute_with(|| { @@ -931,7 +959,9 @@ fn reject_market_unreserves_oracle_bond_and_slashes_advisory_bond() { let balance_reserved_before_alice = Balances::reserved_balance_named(&PredictionMarkets::reserve_id(), &ALICE); - assert_ok!(PredictionMarkets::reject_market(Origin::signed(SUDO), 0)); + let reject_reason: Vec = + vec![0; ::MaxRejectReasonLen::get() as usize]; + assert_ok!(PredictionMarkets::reject_market(Origin::signed(SUDO), 0, reject_reason)); // AdvisoryBond gets slashed after reject_market // OracleBond gets unreserved after reject_market @@ -969,7 +999,9 @@ fn reject_market_clears_auto_close_blocks() { simple_create_categorical_market(MarketCreation::Advised, 33..66, ScoringRule::CPMM); simple_create_categorical_market(MarketCreation::Advised, 22..66, ScoringRule::CPMM); simple_create_categorical_market(MarketCreation::Advised, 22..33, ScoringRule::CPMM); - assert_ok!(PredictionMarkets::reject_market(Origin::signed(SUDO), 0)); + let reject_reason: Vec = + vec![0; ::MaxRejectReasonLen::get() as usize]; + assert_ok!(PredictionMarkets::reject_market(Origin::signed(SUDO), 0, reject_reason)); let auto_close = MarketIdsPerCloseBlock::::get(66); assert_eq!(auto_close.len(), 1); @@ -2478,8 +2510,10 @@ fn reject_market_fails_on_permissionless_market() { ExtBuilder::default().build().execute_with(|| { // Creates an advised market. simple_create_categorical_market(MarketCreation::Permissionless, 0..1, ScoringRule::CPMM); + let reject_reason: Vec = + vec![0; ::MaxRejectReasonLen::get() as usize]; assert_noop!( - PredictionMarkets::reject_market(Origin::signed(SUDO), 0), + PredictionMarkets::reject_market(Origin::signed(SUDO), 0, reject_reason), Error::::InvalidMarketStatus ); }); @@ -2491,8 +2525,10 @@ fn reject_market_fails_on_approved_market() { // Creates an advised market. simple_create_categorical_market(MarketCreation::Advised, 0..1, ScoringRule::CPMM); assert_ok!(PredictionMarkets::approve_market(Origin::signed(SUDO), 0)); + let reject_reason: Vec = + vec![0; ::MaxRejectReasonLen::get() as usize]; assert_noop!( - PredictionMarkets::reject_market(Origin::signed(SUDO), 0), + PredictionMarkets::reject_market(Origin::signed(SUDO), 0, reject_reason), Error::::InvalidMarketStatus ); }); diff --git a/zrml/prediction-markets/src/weights.rs b/zrml/prediction-markets/src/weights.rs index 03fba428e..2571f8852 100644 --- a/zrml/prediction-markets/src/weights.rs +++ b/zrml/prediction-markets/src/weights.rs @@ -67,7 +67,7 @@ pub trait WeightInfoZeitgeist { fn process_subsidy_collecting_markets_raw(a: u32) -> Weight; fn redeem_shares_categorical() -> Weight; fn redeem_shares_scalar() -> Weight; - fn reject_market(c: u32, o: u32) -> Weight; + fn reject_market(c: u32, o: u32, r: u32) -> Weight; fn report(m: u32) -> Weight; fn sell_complete_set(a: u32) -> Weight; fn start_subsidy(a: u32) -> Weight; @@ -178,7 +178,7 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add((28_533_000 as Weight).saturating_mul(d as Weight)) .saturating_add(T::DbWeight::get().reads(7 as Weight)) .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(d as Weight))) - .saturating_add(T::DbWeight::get().writes(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(6 as Weight)) .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(d as Weight))) } // Storage: MarketCommons Markets (r:1 w:1) @@ -197,7 +197,7 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add((30_863_000 as Weight).saturating_mul(d as Weight)) .saturating_add(T::DbWeight::get().reads(8 as Weight)) .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(d as Weight))) - .saturating_add(T::DbWeight::get().writes(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(7 as Weight)) .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(d as Weight))) } // Storage: MarketCommons Markets (r:1 w:1) @@ -375,14 +375,16 @@ impl WeightInfoZeitgeist for WeightInfo { // Storage: PredictionMarkets MarketIdsPerOpenTimeFrame (r:1 w:1) // Storage: PredictionMarkets MarketIdsPerCloseTimeFrame (r:1 w:1) // Storage: Balances Reserves (r:1 w:1) - fn reject_market(c: u32, o: u32) -> Weight { - (84_109_000 as Weight) - // Standard Error: 3_000 - .saturating_add((6_000 as Weight).saturating_mul(c as Weight)) - // Standard Error: 3_000 - .saturating_add((32_000 as Weight).saturating_mul(o as Weight)) - .saturating_add(T::DbWeight::get().reads(4 as Weight)) - .saturating_add(T::DbWeight::get().writes(4 as Weight)) + fn reject_market(c: u32, o: u32, r: u32) -> Weight { + (70_584_000 as Weight) + // Standard Error: 275_000 + .saturating_add((93_000 as Weight).saturating_mul(c as Weight)) + // Standard Error: 275_000 + .saturating_add((118_000 as Weight).saturating_mul(o as Weight)) + // Standard Error: 16_000 + .saturating_add((12_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(5 as Weight)) } // Storage: MarketCommons Markets (r:1 w:1) // Storage: Timestamp Now (r:1 w:0)