diff --git a/zrml/prediction-markets/src/benchmarks.rs b/zrml/prediction-markets/src/benchmarks.rs index 3987abf00..3e8474bb1 100644 --- a/zrml/prediction-markets/src/benchmarks.rs +++ b/zrml/prediction-markets/src/benchmarks.rs @@ -64,15 +64,16 @@ const LIQUIDITY: u128 = 100 * BASE; // Get default values for market creation. Also spawns an account with maximum // amount of native currency -fn create_market_common_parameters() --> Result<(T::AccountId, T::AccountId, Deadlines, MultiHash), &'static str> { +fn create_market_common_parameters( + is_disputable: bool, +) -> Result<(T::AccountId, T::AccountId, Deadlines, MultiHash), &'static str> { let caller: T::AccountId = whitelisted_caller(); T::AssetManager::deposit(Asset::Ztg, &caller, (100u128 * LIQUIDITY).saturated_into()).unwrap(); let oracle = caller.clone(); let deadlines = Deadlines:: { grace_period: 1_u32.into(), oracle_duration: T::MinOracleDuration::get(), - dispute_duration: T::MinDisputeDuration::get(), + dispute_duration: if is_disputable { T::MinDisputeDuration::get() } else { Zero::zero() }, }; let mut metadata = [0u8; 50]; metadata[0] = 0x15; @@ -86,13 +87,15 @@ fn create_market_common( options: MarketType, scoring_rule: ScoringRule, period: Option>>, + dispute_mechanism: Option, ) -> Result<(T::AccountId, MarketIdOf), &'static str> { pallet_timestamp::Pallet::::set_timestamp(0u32.into()); let range_start: MomentOf = 100_000u64.saturated_into(); let range_end: MomentOf = 1_000_000u64.saturated_into(); let creator_fee: Perbill = Perbill::zero(); let period = period.unwrap_or(MarketPeriod::Timestamp(range_start..range_end)); - let (caller, oracle, deadlines, metadata) = create_market_common_parameters::()?; + let (caller, oracle, deadlines, metadata) = + create_market_common_parameters::(dispute_mechanism.is_some())?; Call::::create_market { base_asset: Asset::Ztg, creator_fee, @@ -102,7 +105,7 @@ fn create_market_common( metadata, creation, market_type: options, - dispute_mechanism: Some(MarketDisputeMechanism::SimpleDisputes), + dispute_mechanism, scoring_rule, } .dispatch_bypass_filter(RawOrigin::Signed(caller.clone()).into())?; @@ -118,8 +121,13 @@ fn create_close_and_report_market( let range_start: MomentOf = 100_000u64.saturated_into(); let range_end: MomentOf = 1_000_000u64.saturated_into(); let period = MarketPeriod::Timestamp(range_start..range_end); - let (caller, market_id) = - create_market_common::(permission, options, ScoringRule::CPMM, Some(period))?; + let (caller, market_id) = create_market_common::( + permission, + options, + ScoringRule::CPMM, + Some(period), + Some(MarketDisputeMechanism::Court), + )?; Call::::admin_move_market_to_closed { market_id } .dispatch_bypass_filter(T::CloseOrigin::try_successful_origin().unwrap())?; let market = >::market(&market_id)?; @@ -146,6 +154,7 @@ fn setup_redeem_shares_common( market_type.clone(), ScoringRule::CPMM, None, + Some(MarketDisputeMechanism::Court), )?; let outcome: OutcomeReport; @@ -188,6 +197,7 @@ fn setup_reported_categorical_market_with_pool = MaxSwapFee::get().saturated_into(); @@ -241,6 +251,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), )?; for i in 0..o { @@ -440,6 +451,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, None, + Some(MarketDisputeMechanism::Court), )?; let approve_origin = T::ApproveOrigin::try_successful_origin().unwrap(); @@ -453,6 +465,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, None, + Some(MarketDisputeMechanism::Court), )?; let approve_origin = T::ApproveOrigin::try_successful_origin().unwrap(); @@ -467,6 +480,7 @@ benchmarks! { MarketType::Categorical(a.saturated_into()), ScoringRule::CPMM, None, + Some(MarketDisputeMechanism::Court), )?; let amount = BASE * 1_000; }: _(RawOrigin::Signed(caller), market_id, amount.saturated_into()) @@ -476,7 +490,7 @@ benchmarks! { create_market { let m in 0..63; - let (caller, oracle, deadlines, metadata) = create_market_common_parameters::()?; + let (caller, oracle, deadlines, metadata) = create_market_common_parameters::(true)?; let range_end = T::MaxSubsidyPeriod::get(); let period = MarketPeriod::Timestamp(T::MinSubsidyPeriod::get()..range_end); @@ -497,7 +511,7 @@ benchmarks! { metadata, MarketCreation::Permissionless, MarketType::Categorical(T::MaxCategories::get()), - Some(MarketDisputeMechanism::SimpleDisputes), + Some(MarketDisputeMechanism::Court), ScoringRule::CPMM ) @@ -505,13 +519,13 @@ benchmarks! { let m in 0..63; let market_type = MarketType::Categorical(T::MaxCategories::get()); - let dispute_mechanism = Some(MarketDisputeMechanism::SimpleDisputes); + let dispute_mechanism = Some(MarketDisputeMechanism::Court); let scoring_rule = ScoringRule::CPMM; let range_start: MomentOf = 100_000u64.saturated_into(); let range_end: MomentOf = 1_000_000u64.saturated_into(); let period = MarketPeriod::Timestamp(range_start..range_end); let (caller, oracle, deadlines, metadata) = - create_market_common_parameters::()?; + create_market_common_parameters::(true)?; Call::::create_market { base_asset: Asset::Ztg, creator_fee: Perbill::zero(), @@ -567,6 +581,7 @@ benchmarks! { MarketType::Categorical(a.saturated_into()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), )?; assert!( @@ -623,6 +638,7 @@ benchmarks! { MarketType::Categorical(a.saturated_into()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), )?; let market = >::market(&market_id.saturated_into())?; @@ -757,6 +773,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(T::MinSubsidyPeriod::get()..T::MaxSubsidyPeriod::get())), + Some(MarketDisputeMechanism::Court), )?; let market = >::market(&market_id.saturated_into())?; }: { Pallet::::handle_expired_advised_market(&market_id, market)? } @@ -900,6 +917,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), )?; for i in 0..o { @@ -932,6 +950,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), )?; >::mutate_market(&market_id, |market| { @@ -973,7 +992,7 @@ benchmarks! { pallet_timestamp::Pallet::::set_timestamp(0u32.into()); let start: MomentOf = >::now(); let end: MomentOf = 1_000_000u64.saturated_into(); - let (caller, oracle, _, metadata) = create_market_common_parameters::()?; + let (caller, oracle, _, metadata) = create_market_common_parameters::(false)?; Call::::create_market { base_asset: Asset::Ztg, creator_fee: Perbill::zero(), @@ -1007,6 +1026,7 @@ benchmarks! { MarketType::Categorical(a.saturated_into()), ScoringRule::CPMM, None, + Some(MarketDisputeMechanism::Court), )?; let amount: BalanceOf = LIQUIDITY.saturated_into(); Pallet::::buy_complete_set( @@ -1026,6 +1046,7 @@ benchmarks! { MarketType::Categorical(a.saturated_into()), ScoringRule::RikiddoSigmoidFeeMarketEma, Some(MarketPeriod::Timestamp(T::MinSubsidyPeriod::get()..T::MaxSubsidyPeriod::get())), + Some(MarketDisputeMechanism::Court), )?; let mut market_clone = None; >::mutate_market(&market_id, |market| { @@ -1048,6 +1069,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Block(start_block..end_block)), + Some(MarketDisputeMechanism::Court), ).unwrap(); } @@ -1059,6 +1081,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), ).unwrap(); } @@ -1111,6 +1134,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..range_end)), + Some(MarketDisputeMechanism::Court), )?; // ensure market is reported >::mutate_market(&market_id, |market| { @@ -1164,6 +1188,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..old_range_end)), + Some(MarketDisputeMechanism::Court), )?; for i in 0..o { @@ -1198,6 +1223,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..old_range_end)), + Some(MarketDisputeMechanism::Court), )?; for i in 0..o { @@ -1242,6 +1268,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..old_range_end)), + Some(MarketDisputeMechanism::Court), )?; let market = >::market(&market_id)?; @@ -1279,6 +1306,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..old_range_end)), + Some(MarketDisputeMechanism::Court), )?; let market_creator = caller.clone(); @@ -1329,6 +1357,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..old_range_end)), + Some(MarketDisputeMechanism::Court), )?; let market_creator = caller.clone(); @@ -1376,6 +1405,7 @@ benchmarks! { MarketType::Categorical(T::MaxCategories::get()), ScoringRule::CPMM, Some(MarketPeriod::Timestamp(range_start..old_range_end)), + Some(MarketDisputeMechanism::Court), )?; let market_creator = caller.clone(); @@ -1395,6 +1425,37 @@ benchmarks! { let call = Call::::reject_early_close { market_id }; }: { call.dispatch_bypass_filter(close_origin)? } + close_trusted_market { + let o in 0..63; + let c in 0..63; + + let range_start: MomentOf = 100_000u64.saturated_into(); + let range_end: MomentOf = 1_000_000u64.saturated_into(); + let (caller, market_id) = create_market_common::( + MarketCreation::Permissionless, + MarketType::Categorical(T::MaxCategories::get()), + ScoringRule::CPMM, + Some(MarketPeriod::Timestamp(range_start..range_end)), + None, + )?; + + for i in 0..o { + MarketIdsPerOpenTimeFrame::::try_mutate( + Pallet::::calculate_time_frame_of_moment(range_start), + |ids| ids.try_push(i.into()), + ).unwrap(); + } + + for i in 0..c { + MarketIdsPerCloseTimeFrame::::try_mutate( + Pallet::::calculate_time_frame_of_moment(range_end), + |ids| ids.try_push(i.into()), + ).unwrap(); + } + + let call = Call::::close_trusted_market { market_id }; + }: { call.dispatch_bypass_filter(RawOrigin::Signed(caller).into())? } + create_market_and_deploy_pool { let m in 0..63; // Number of markets closing on the same block. @@ -1403,7 +1464,7 @@ benchmarks! { let range_end = (100 * MILLISECS_PER_BLOCK) as u64; let period = MarketPeriod::Timestamp(range_start..range_end); let market_type = MarketType::Categorical(2); - let (caller, oracle, deadlines, metadata) = create_market_common_parameters::()?; + let (caller, oracle, deadlines, metadata) = create_market_common_parameters::(true)?; let price = (BASE / 2).saturated_into(); let amount = (10u128 * BASE).saturated_into(); diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index 5f20099fa..2e896d1d1 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -1715,6 +1715,34 @@ mod pallet { Ok(Some(weight).into()) } + + /// Allows the market creator of a trusted market + /// to immediately move an open market to closed. + /// + /// # Weight + /// + /// Complexity: `O(n + m)`, where `n` is the number of market ids, + /// which open at the same time as the specified market, + /// and `m` is the number of market ids, + /// which close at the same time as the specified market. + #[pallet::call_index(21)] + #[pallet::weight(T::WeightInfo::close_trusted_market(CacheSize::get(), CacheSize::get()))] + #[transactional] + pub fn close_trusted_market( + origin: OriginFor, + #[pallet::compact] market_id: MarketIdOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + let market = >::market(&market_id)?; + ensure!(market.creator == who, Error::::CallerNotMarketCreator); + ensure!(market.dispute_mechanism.is_none(), Error::::MarketIsNotTrusted); + Self::ensure_market_is_active(&market)?; + let open_ids_len = Self::clear_auto_open(&market_id)?; + let close_ids_len = Self::clear_auto_close(&market_id)?; + Self::close_market(&market_id)?; + Self::set_market_end(&market_id)?; + Ok(Some(T::WeightInfo::close_trusted_market(open_ids_len, close_ids_len)).into()) + } } #[pallet::config] @@ -2066,6 +2094,10 @@ mod pallet { /// After there was an early close already scheduled, /// only the `CloseMarketsEarlyOrigin` can schedule another one. OnlyAuthorizedCanScheduleEarlyClose, + /// The caller is not the market creator. + CallerNotMarketCreator, + /// The market is not trusted. + MarketIsNotTrusted, } #[pallet::event] diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index 0fbe22b84..177c2eca6 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -6342,6 +6342,149 @@ fn trusted_market_complete_lifecycle() { }); } +#[test] +fn close_trusted_market_works() { + ExtBuilder::default().build().execute_with(|| { + let end = 10; + let market_creator = ALICE; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(market_creator), + Asset::Ztg, + Perbill::zero(), + BOB, + MarketPeriod::Block(0..end), + Deadlines { + grace_period: 0, + oracle_duration: ::MinOracleDuration::get(), + dispute_duration: Zero::zero(), + }, + gen_metadata(0x99), + MarketCreation::Permissionless, + MarketType::Categorical(3), + None, + ScoringRule::CPMM, + )); + + let market_id = 0; + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.dispute_mechanism, None); + + let new_end = end / 2; + assert_ne!(new_end, end); + run_to_block(new_end); + + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.status, MarketStatus::Active); + + let auto_closes = MarketIdsPerCloseBlock::::get(end); + assert_eq!(auto_closes.first().cloned().unwrap(), market_id); + + assert_noop!( + PredictionMarkets::close_trusted_market(RuntimeOrigin::signed(BOB), market_id), + Error::::CallerNotMarketCreator + ); + + assert_ok!(PredictionMarkets::close_trusted_market( + RuntimeOrigin::signed(market_creator), + market_id + )); + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.period, MarketPeriod::Block(0..new_end)); + assert_eq!(market.status, MarketStatus::Closed); + + let auto_closes = MarketIdsPerCloseBlock::::get(end); + assert_eq!(auto_closes.len(), 0); + }); +} + +#[test] +fn close_trusted_market_fails_if_not_trusted() { + ExtBuilder::default().build().execute_with(|| { + let end = 10; + let market_creator = ALICE; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(market_creator), + Asset::Ztg, + Perbill::zero(), + BOB, + MarketPeriod::Block(0..end), + Deadlines { + grace_period: 0, + oracle_duration: ::MinOracleDuration::get(), + dispute_duration: ::MinDisputeDuration::get(), + }, + gen_metadata(0x99), + MarketCreation::Permissionless, + MarketType::Categorical(3), + Some(MarketDisputeMechanism::Court), + ScoringRule::CPMM, + )); + + let market_id = 0; + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.dispute_mechanism, Some(MarketDisputeMechanism::Court)); + + run_to_block(end / 2); + + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.status, MarketStatus::Active); + + assert_noop!( + PredictionMarkets::close_trusted_market( + RuntimeOrigin::signed(market_creator), + market_id + ), + Error::::MarketIsNotTrusted + ); + }); +} + +#[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")] +#[test_case(MarketStatus::Disputed; "disputed")] +#[test_case(MarketStatus::Reported; "report")] +#[test_case(MarketStatus::Suspended; "suspended")] +fn close_trusted_market_fails_if_invalid_market_state(status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let end = 10; + let market_creator = ALICE; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(market_creator), + Asset::Ztg, + Perbill::zero(), + BOB, + MarketPeriod::Block(0..end), + Deadlines { + grace_period: 0, + oracle_duration: ::MinOracleDuration::get(), + dispute_duration: Zero::zero(), + }, + gen_metadata(0x99), + MarketCreation::Permissionless, + MarketType::Categorical(3), + None, + ScoringRule::CPMM, + )); + + let market_id = 0; + assert_ok!(MarketCommons::mutate_market(&market_id, |market| { + market.status = status; + Ok(()) + })); + + assert_noop!( + PredictionMarkets::close_trusted_market( + RuntimeOrigin::signed(market_creator), + market_id + ), + Error::::MarketIsNotActive + ); + }); +} + fn deploy_swap_pool( market: Market>, market_id: u128, diff --git a/zrml/prediction-markets/src/weights.rs b/zrml/prediction-markets/src/weights.rs index 1d095de18..9a18cfdf9 100644 --- a/zrml/prediction-markets/src/weights.rs +++ b/zrml/prediction-markets/src/weights.rs @@ -87,6 +87,7 @@ pub trait WeightInfoZeitgeist { fn reject_early_close_after_authority(o: u32, n: u32) -> Weight; fn reject_early_close_after_dispute() -> Weight; fn create_market_and_deploy_pool(m: u32) -> Weight; + fn close_trusted_market(o: u32, c: u32) -> Weight; } /// Weight functions for zrml_prediction_markets (automatically generated) @@ -897,4 +898,17 @@ impl WeightInfoZeitgeist for WeightInfo { .saturating_add(T::DbWeight::get().reads(13)) .saturating_add(T::DbWeight::get().writes(13)) } + fn close_trusted_market(o: u32, c: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `792 + o * (16 ±0) + c * (16 ±0)` + // Estimated: `13229` + // Minimum execution time: 54_250 nanoseconds. + Weight::from_parts(59_415_334, 13229) + // Standard Error: 2_729 + .saturating_add(Weight::from_parts(17_380, 0).saturating_mul(o.into())) + // Standard Error: 2_729 + .saturating_add(Weight::from_parts(62_317, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + } }