diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs index 5fc9ccf9d..7bccf8575 100644 --- a/zrml/prediction-markets/src/lib.rs +++ b/zrml/prediction-markets/src/lib.rs @@ -444,6 +444,12 @@ mod pallet { .into()) } + /// Allows the advisory committee or the market creator to schedule an early close. + /// TODO + /// + /// # Weight + /// + /// Complexity: `O(n)`, where `n` is ... #[pallet::call_index(17)] #[pallet::weight(( T::WeightInfo::admin_move_market_to_closed( @@ -577,6 +583,12 @@ mod pallet { Ok((Some(T::WeightInfo::admin_move_market_to_closed(0u32, 0u32)), Pays::Yes).into()) } + /// Allows anyone to dispute a scheduled early close. + /// TODO + /// + /// # Weight + /// + /// Complexity: `O(n)`, where `n` is ... #[pallet::call_index(18)] #[pallet::weight(( T::WeightInfo::admin_move_market_to_closed( @@ -642,6 +654,12 @@ mod pallet { Ok((Some(T::WeightInfo::admin_move_market_to_closed(0u32, 0u32)), Pays::Yes).into()) } + /// Allows the advisory committee to reject a scheduled early close. + /// TODO + /// + /// # Weight + /// + /// Complexity: `O(n)`, where `n` is ... #[pallet::call_index(19)] #[pallet::weight(( T::WeightInfo::admin_move_market_to_closed( diff --git a/zrml/prediction-markets/src/migrations.rs b/zrml/prediction-markets/src/migrations.rs index 820564749..e786c928f 100644 --- a/zrml/prediction-markets/src/migrations.rs +++ b/zrml/prediction-markets/src/migrations.rs @@ -37,8 +37,8 @@ use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_runtime::{traits::Saturating, Perbill}; use zeitgeist_primitives::types::{ - Asset, Deadlines, Market, MarketBonds, MarketCreation, MarketDisputeMechanism, MarketPeriod, - MarketStatus, MarketType, OutcomeReport, Report, ScoringRule, + Asset, Bond, Deadlines, Market, MarketBonds, MarketCreation, MarketDisputeMechanism, + MarketPeriod, MarketStatus, MarketType, OutcomeReport, Report, ScoringRule, }; #[cfg(feature = "try-runtime")] use zrml_market_commons::MarketCommonsPalletApi; @@ -90,7 +90,7 @@ pub struct OldMarket { /// The resolved outcome. pub resolved_outcome: Option, /// See [`MarketDisputeMechanism`]. - pub dispute_mechanism: MarketDisputeMechanism, + pub dispute_mechanism: Option, pub bonds: OldMarketBonds, } @@ -126,40 +126,38 @@ impl OnRuntimeUpgrade for AddEarlyClose } log::info!("AddEarlyCloseBonds: Starting..."); let mut translated = 0u64; - zrml_market_commons::Markets::::translate::, _>( - |market_id, old_market| { - translated.saturating_inc(); + zrml_market_commons::Markets::::translate::, _>(|_, old_market| { + translated.saturating_inc(); - let new_market = Market { - base_asset: old_market.base_asset, - creator: old_market.creator, - creation: old_market.creation, - // Zero can be safely assumed here as it was hardcoded before - creator_fee: Perbill::zero(), - oracle: old_market.oracle, - metadata: old_market.metadata, - market_type: old_market.market_type, - period: old_market.period, - scoring_rule: old_market.scoring_rule, - status: old_market.status, - report: old_market.report, - resolved_outcome: old_market.resolved_outcome, - dispute_mechanism: old_market.dispute_mechanism, - deadlines: old_market.deadlines, - bonds: MarketBonds { - creation: old_market.bonds.creation, - oracle: old_market.bonds.oracle, - outsider: old_market.bonds.outsider, - dispute: dispute_bond, - close_dispute: None, - close_request: None, - }, - premature_close: None, - }; + let new_market = Market { + base_asset: old_market.base_asset, + creator: old_market.creator, + creation: old_market.creation, + // Zero can be safely assumed here as it was hardcoded before + creator_fee: Perbill::zero(), + oracle: old_market.oracle, + metadata: old_market.metadata, + market_type: old_market.market_type, + period: old_market.period, + scoring_rule: old_market.scoring_rule, + status: old_market.status, + report: old_market.report, + resolved_outcome: old_market.resolved_outcome, + dispute_mechanism: old_market.dispute_mechanism, + deadlines: old_market.deadlines, + bonds: MarketBonds { + creation: old_market.bonds.creation, + oracle: old_market.bonds.oracle, + outsider: old_market.bonds.outsider, + dispute: old_market.bonds.dispute, + close_dispute: None, + close_request: None, + }, + premature_close: None, + }; - Some(new_market) - }, - ); + Some(new_market) + }); log::info!("AddEarlyCloseBonds: Upgraded {} markets.", translated); total_weight = total_weight.saturating_add(T::DbWeight::get().reads_writes(translated, translated)); @@ -226,10 +224,7 @@ impl OnRuntimeUpgrade for AddEarlyClose assert_eq!(new_market.bonds.close_dispute, None); } - log::info!( - "AddEarlyCloseBonds: Market Counter post-upgrade is {}!", - new_market_count - ); + log::info!("AddEarlyCloseBonds: Market Counter post-upgrade is {}!", new_market_count); assert!(new_market_count > 0); Ok(()) } @@ -246,7 +241,6 @@ mod tests { dispatch::fmt::Debug, migration::put_storage_value, storage_root, Blake2_128Concat, StateVersion, StorageHasher, }; - use test_case::test_case; use zrml_market_commons::MarketCommonsPalletApi; #[test] @@ -265,7 +259,7 @@ 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 (_, new_markets) = construct_old_new_tuple(MarketDisputeMechanism::Court); + let (_, new_markets) = construct_old_new_tuple(); populate_test_data::, MarketOf>( MARKET_COMMONS, MARKETS, @@ -277,12 +271,11 @@ mod tests { }); } - #[test_case(MarketDisputeMechanism::Authorized)] - #[test_case(MarketDisputeMechanism::Court)] - fn on_runtime_upgrade_correctly_updates_markets(dispute_mechanism: MarketDisputeMechanism) { + #[test] + fn on_runtime_upgrade_correctly_updates_markets() { ExtBuilder::default().build().execute_with(|| { set_up_version(); - let (old_markets, new_markets) = construct_old_new_tuple(dispute_mechanism); + let (old_markets, new_markets) = construct_old_new_tuple(); populate_test_data::, OldMarketOf>( MARKET_COMMONS, MARKETS, @@ -299,9 +292,7 @@ mod tests { .put::>(); } - fn construct_old_new_tuple( - dispute_mechanism: MarketDisputeMechanism, - ) -> (Vec>, Vec>) { + fn construct_old_new_tuple() -> (Vec>, Vec>) { let base_asset = Asset::Ztg; let creator = 999; let creator_fee = Perbill::from_parts(1); @@ -315,13 +306,13 @@ mod tests { let report = None; let resolved_outcome = None; let deadlines = Deadlines::default(); + let dispute_mechanism = Some(MarketDisputeMechanism::Court); let old_bonds = OldMarketBonds { creation: Some(Bond::new(creator, ::ValidityBond::get())), oracle: Some(Bond::new(creator, ::OracleBond::get())), outsider: Some(Bond::new(creator, ::OutsiderBond::get())), dispute: None, }; - let dispute_bond = disputor.map(|disputor| Bond::new(disputor, DisputeBond::get())); let new_bonds = MarketBonds { creation: Some(Bond::new(creator, ::ValidityBond::get())), oracle: Some(Bond::new(creator, ::OracleBond::get())), @@ -361,7 +352,7 @@ mod tests { status, report, resolved_outcome, - dispute_mechanism: Some(dispute_mechanism), + dispute_mechanism: dispute_mechanism, deadlines, bonds: new_bonds, premature_close: None, diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index e89e1fd9d..746291c2c 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -43,8 +43,9 @@ use sp_arithmetic::Perbill; use sp_runtime::traits::{AccountIdConversion, Hash, SaturatedConversion, Zero}; use zeitgeist_primitives::{ constants::mock::{ - CloseProtectionBlockPeriod, CloseProtectionTimeFramePeriod, MaxAppeals, MaxSelectedDraws, - MinJurorStake, OutcomeBond, OutcomeFactor, OutsiderBond, BASE, CENT, MILLISECS_PER_BLOCK, + CloseDisputeBond, CloseProtectionBlockPeriod, CloseProtectionTimeFramePeriod, + CloseRequestBond, MaxAppeals, MaxSelectedDraws, MinJurorStake, OutcomeBond, OutcomeFactor, + OutsiderBond, PrematureCloseBlockPeriod, BASE, CENT, MILLISECS_PER_BLOCK, }, traits::Swaps as SwapsPalletApi, types::{ @@ -2873,7 +2874,6 @@ fn schedule_early_close_emits_event() { let new_end = now + CloseProtectionBlockPeriod::get(); assert!(new_end < end); - let market = MarketCommons::market(&market_id).unwrap(); let new_period = MarketPeriod::Block(0..new_end); System::assert_last_event( Event::MarketEarlyCloseScheduled { @@ -2886,6 +2886,119 @@ fn schedule_early_close_emits_event() { }); } +#[test] +fn dispute_early_close_emits_event() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..end, + ScoringRule::CPMM, + ); + + // just to ensure events are emitted + run_blocks(2); + + let market_id = 0; + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + assert_ok!(PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,)); + + System::assert_last_event(Event::MarketEarlyCloseDisputed { market_id }.into()); + }); +} + +#[test] +fn reject_early_close_emits_event() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..end, + ScoringRule::CPMM, + ); + + // just to ensure events are emitted + run_blocks(2); + + let market_id = 0; + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + assert_ok!(PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,)); + + assert_ok!(PredictionMarkets::reject_early_close(RuntimeOrigin::signed(SUDO), market_id,)); + + System::assert_last_event(Event::MarketEarlyCloseRejected { market_id }.into()); + }); +} + +#[test] +fn reject_early_close_fails_if_state_is_scheduled_as_market_creator() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..end, + ScoringRule::CPMM, + ); + + // just to ensure events are emitted + run_blocks(2); + + let market_id = 0; + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + assert_noop!( + PredictionMarkets::reject_early_close(RuntimeOrigin::signed(SUDO), market_id,), + Error::::InvalidPrematureCloseState + ); + }); +} + +#[test] +fn reject_early_close_fails_if_state_is_rejected() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + simple_create_categorical_market( + Asset::Ztg, + MarketCreation::Permissionless, + 0..end, + ScoringRule::CPMM, + ); + + // just to ensure events are emitted + run_blocks(2); + + let market_id = 0; + + assert_ok!( + PredictionMarkets::schedule_early_close(RuntimeOrigin::signed(SUDO), market_id,) + ); + + assert_ok!(PredictionMarkets::reject_early_close(RuntimeOrigin::signed(SUDO), market_id,)); + + assert_noop!( + PredictionMarkets::reject_early_close(RuntimeOrigin::signed(SUDO), market_id,), + Error::::InvalidPrematureCloseState + ); + }); +} + #[test] fn sudo_schedule_early_close_at_block_works() { ExtBuilder::default().build().execute_with(|| { @@ -2900,7 +3013,7 @@ fn sudo_schedule_early_close_at_block_works() { gen_metadata(2), MarketCreation::Permissionless, MarketType::Categorical(::MinCategories::get()), - MarketDisputeMechanism::SimpleDisputes, + Some(MarketDisputeMechanism::Court), ScoringRule::CPMM )); @@ -2971,7 +3084,7 @@ fn sudo_schedule_early_close_at_timeframe_works() { gen_metadata(2), MarketCreation::Permissionless, MarketType::Categorical(::MinCategories::get()), - MarketDisputeMechanism::SimpleDisputes, + Some(MarketDisputeMechanism::Court), ScoringRule::CPMM )); @@ -3021,12 +3134,149 @@ fn sudo_schedule_early_close_at_timeframe_works() { set_timestamp_for_on_initialize((start_block + new_end) * MILLISECS_PER_BLOCK as u64); run_to_block(start_block + new_end); - let now = >::now(); let market = MarketCommons::market(&0).unwrap(); assert_eq!(market.status, MarketStatus::Closed); }); } +#[test] +fn schedule_early_close_as_market_creator_works() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + Perbill::zero(), + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(::MinCategories::get()), + Some(MarketDisputeMechanism::Court), + ScoringRule::CPMM + )); + + let market_id = 0; + let market = MarketCommons::market(&market_id).unwrap(); + let old_market_period = market.period; + assert_eq!(market.status, MarketStatus::Active); + let market_ids_to_close = >::iter().next().unwrap(); + assert_eq!(market_ids_to_close.0, end); + assert_eq!(market_ids_to_close.1.into_inner(), vec![market_id]); + assert!(market.premature_close.is_none()); + + let reserved_balance_alice = Balances::reserved_balance(&ALICE); + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + let reserved_balance_alice_after = Balances::reserved_balance(&ALICE); + assert_eq!(reserved_balance_alice_after - reserved_balance_alice, CloseRequestBond::get()); + + let now = >::block_number(); + let new_end = now + PrematureCloseBlockPeriod::get(); + assert!(new_end < end); + + let market = MarketCommons::market(&market_id).unwrap(); + let new_period = MarketPeriod::Block(0..new_end); + assert_eq!( + market.premature_close.unwrap(), + PrematureClose { + old: old_market_period, + new: new_period, + state: PrematureCloseState::ScheduledAsMarketCreator, + } + ); + + let market_ids_to_close = >::iter().collect::>(); + assert_eq!(market_ids_to_close.len(), 2); + + // The first entry is the old one without a market id inside. + let first = market_ids_to_close.first().unwrap(); + assert_eq!(first.0, end); + assert!(first.1.clone().into_inner().is_empty()); + + // The second entry is the new one with the market id inside. + let second = market_ids_to_close.last().unwrap(); + assert_eq!(second.0, new_end); + assert_eq!(second.1.clone().into_inner(), vec![market_id]); + + run_to_block(new_end + 1); + + let market = MarketCommons::market(&0).unwrap(); + assert_eq!(market.status, MarketStatus::Closed); + }); +} + +#[test] +fn dispute_early_close_from_market_creator_works() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + Perbill::zero(), + BOB, + MarketPeriod::Block(0..end), + get_deadlines(), + gen_metadata(2), + MarketCreation::Permissionless, + MarketType::Categorical(::MinCategories::get()), + Some(MarketDisputeMechanism::Court), + ScoringRule::CPMM + )); + + let market_id = 0; + let market = MarketCommons::market(&market_id).unwrap(); + let old_market_period = market.period; + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + let now = >::block_number(); + let new_end = now + PrematureCloseBlockPeriod::get(); + let market_ids_at_new_end = >::get(new_end); + assert_eq!(market_ids_at_new_end, vec![market_id]); + + run_blocks(1); + + let reserved_bob = Balances::reserved_balance(&BOB); + + assert_ok!(PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,)); + + let reserved_bob_after = Balances::reserved_balance(&BOB); + assert_eq!(reserved_bob_after - reserved_bob, CloseDisputeBond::get()); + + let market_ids_at_new_end = >::get(new_end); + assert!(market_ids_at_new_end.is_empty()); + + let market_ids_at_old_end = >::get(end); + assert_eq!(market_ids_at_old_end, vec![market_id]); + + let market = MarketCommons::market(&market_id).unwrap(); + let new_period = MarketPeriod::Block(0..new_end); + assert_eq!( + market.premature_close.unwrap(), + PrematureClose { + old: old_market_period, + new: new_period, + state: PrematureCloseState::Disputed, + } + ); + + run_to_block(new_end + 1); + + // verify the market doesn't close after proposed new market period end + let market = MarketCommons::market(&0).unwrap(); + assert_eq!(market.status, MarketStatus::Active); + }); +} + #[test] fn dispute_updates_market() { ExtBuilder::default().build().execute_with(|| {