diff --git a/primitives/src/market.rs b/primitives/src/market.rs index d234dad32..dcf47498b 100644 --- a/primitives/src/market.rs +++ b/primitives/src/market.rs @@ -423,6 +423,7 @@ mod tests { resolved_outcome: None, dispute_mechanism: Some(MarketDisputeMechanism::Authorized), bonds: MarketBonds::default(), + premature_close: None, }; assert_eq!(market.matches_outcome_report(&outcome_report), expected); } diff --git a/runtime/battery-station/src/parameters.rs b/runtime/battery-station/src/parameters.rs index adf67ddaa..be790b846 100644 --- a/runtime/battery-station/src/parameters.rs +++ b/runtime/battery-station/src/parameters.rs @@ -199,6 +199,16 @@ parameter_types! { pub const AdvisoryBond: Balance = 25 * CENT; /// The percentage of the advisory bond that gets slashed when a market is rejected. pub const AdvisoryBondSlashPercentage: Percent = Percent::from_percent(0); + /// (Slashable) Bond that is provided for disputing an early market close by the market creator. + pub const CloseDisputeBond: Balance = 50 * BASE; + // 43_200_000 = 12 hours. Fat-finger protection for the advisory committe to reject + // the early market schedule. + pub const CloseProtectionTimeFramePeriod: Moment = 43_200_000; + // Fat-finger protection for the advisory committe to reject + // the early market schedule. + pub const CloseProtectionBlockPeriod: BlockNumber = 12 * BLOCKS_PER_HOUR; + /// (Slashable) Bond that is provided for scheduling an early market close. + pub const CloseRequestBond: Balance = 25 * BASE; /// (Slashable) Bond that is provided for disputing the outcome. /// Unreserved in case the dispute was justified otherwise slashed. /// This is when the resolved outcome is different to the default (reported) outcome. @@ -244,6 +254,12 @@ parameter_types! { pub const OutsiderBond: Balance = 2 * OracleBond::get(); /// Pallet identifier, mainly used for named balance reserves. pub const PmPalletId: PalletId = PM_PALLET_ID; + // Waiting time for market creator to close + // the market after an early close schedule. + pub const PrematureCloseBlockPeriod: BlockNumber = 5 * BLOCKS_PER_DAY; + // 432_000_000 = 5 days. Waiting time for market creator to close + // the market after an early close schedule. + pub const PrematureCloseTimeFramePeriod: Moment = 432_000_000; /// (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; diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index bbace19cb..061afc8de 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -58,10 +58,10 @@ macro_rules! decl_common_types { type Address = sp_runtime::MultiAddress; #[cfg(feature = "parachain")] - type Migrations = (zrml_prediction_markets::migrations::MigrateMarkets,); + type Migrations = (zrml_prediction_markets::migrations::AddEarlyCloseBonds,); #[cfg(not(feature = "parachain"))] - type Migrations = (zrml_prediction_markets::migrations::MigrateMarkets,); + type Migrations = (zrml_prediction_markets::migrations::AddEarlyCloseBonds,); pub type Executive = frame_executive::Executive< Runtime, @@ -1113,7 +1113,12 @@ macro_rules! impl_config_traits { type ApproveOrigin = EnsureRootOrMoreThanOneThirdAdvisoryCommittee; type Authorized = Authorized; type Court = Court; + type CloseDisputeBond = CloseDisputeBond; + type CloseMarketEarlyOrigin = EnsureRootOrMoreThanOneThirdAdvisoryCommittee; type CloseOrigin = EnsureRoot; + type CloseProtectionTimeFramePeriod = CloseProtectionTimeFramePeriod; + type CloseProtectionBlockPeriod = CloseProtectionBlockPeriod; + type CloseRequestBond = CloseRequestBond; type DestroyOrigin = EnsureRootOrAllAdvisoryCommittee; type DisputeBond = DisputeBond; type RuntimeEvent = RuntimeEvent; @@ -1139,6 +1144,8 @@ macro_rules! impl_config_traits { type OracleBond = OracleBond; type OutsiderBond = OutsiderBond; type PalletId = PmPalletId; + type PrematureCloseBlockPeriod = PrematureCloseBlockPeriod; + type PrematureCloseTimeFramePeriod = PrematureCloseTimeFramePeriod; type RejectOrigin = EnsureRootOrMoreThanTwoThirdsAdvisoryCommittee; type RequestEditOrigin = EnsureRootOrMoreThanOneThirdAdvisoryCommittee; type ResolveOrigin = EnsureRoot; diff --git a/runtime/zeitgeist/src/parameters.rs b/runtime/zeitgeist/src/parameters.rs index 2406637e2..3b01cedd3 100644 --- a/runtime/zeitgeist/src/parameters.rs +++ b/runtime/zeitgeist/src/parameters.rs @@ -199,6 +199,16 @@ parameter_types! { pub const AdvisoryBond: Balance = 200 * BASE; /// The percentage of the advisory bond that gets slashed when a market is rejected. pub const AdvisoryBondSlashPercentage: Percent = Percent::from_percent(0); + /// (Slashable) Bond that is provided for disputing an early market close by the market creator. + pub const CloseDisputeBond: Balance = 2_000 * BASE; + // 43_200_000 = 12 hours. Fat-finger protection for the advisory committe to reject + // the early market schedule. + pub const CloseProtectionTimeFramePeriod: Moment = 43_200_000; + // Fat-finger protection for the advisory committe to reject + // the early market schedule. + pub const CloseProtectionBlockPeriod: BlockNumber = 12 * BLOCKS_PER_HOUR; + /// (Slashable) Bond that is provided for scheduling an early market close. + pub const CloseRequestBond: Balance = 200 * BASE; /// (Slashable) Bond that is provided for disputing the outcome. /// Unreserved in case the dispute was justified otherwise slashed. /// This is when the resolved outcome is different to the default (reported) outcome. @@ -244,6 +254,12 @@ parameter_types! { 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; + // Waiting time for market creator to close + // the market after an early close schedule. + pub const PrematureCloseBlockPeriod: BlockNumber = 5 * BLOCKS_PER_DAY; + // 432_000_000 = 5 days. Waiting time for market creator to close + // the market after an early close schedule. + pub const PrematureCloseTimeFramePeriod: Moment = 432_000_000; /// (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; diff --git a/zrml/authorized/src/lib.rs b/zrml/authorized/src/lib.rs index c26cb7a33..c93df27e9 100644 --- a/zrml/authorized/src/lib.rs +++ b/zrml/authorized/src/lib.rs @@ -365,7 +365,7 @@ where { use frame_support::traits::Get; use sp_runtime::traits::AccountIdConversion; - use zeitgeist_primitives::types::{Asset, MarketBonds, ScoringRule}; + use zeitgeist_primitives::types::{Asset, MarketBonds, MarketDisputeMechanism, ScoringRule}; zeitgeist_primitives::types::Market { base_asset: Asset::Ztg, @@ -387,5 +387,6 @@ where scoring_rule: ScoringRule::CPMM, status: zeitgeist_primitives::types::MarketStatus::Disputed, bonds: MarketBonds::default(), + premature_close: None, } } diff --git a/zrml/court/src/benchmarks.rs b/zrml/court/src/benchmarks.rs index 97729349a..0466f4649 100644 --- a/zrml/court/src/benchmarks.rs +++ b/zrml/court/src/benchmarks.rs @@ -78,7 +78,15 @@ where resolved_outcome: None, status: MarketStatus::Disputed, scoring_rule: ScoringRule::CPMM, - bonds: MarketBonds { creation: None, oracle: None, outsider: None, dispute: None }, + bonds: MarketBonds { + creation: None, + oracle: None, + outsider: None, + dispute: None, + close_dispute: None, + close_request: None, + }, + premature_close: None, } } diff --git a/zrml/court/src/tests.rs b/zrml/court/src/tests.rs index 880649471..bc52da1b0 100644 --- a/zrml/court/src/tests.rs +++ b/zrml/court/src/tests.rs @@ -76,7 +76,15 @@ const DEFAULT_MARKET: MarketOf = Market { resolved_outcome: None, status: MarketStatus::Disputed, scoring_rule: ScoringRule::CPMM, - bonds: MarketBonds { creation: None, oracle: None, outsider: None, dispute: None }, + bonds: MarketBonds { + creation: None, + oracle: None, + outsider: None, + dispute: None, + close_dispute: None, + close_request: None, + }, + premature_close: None, }; fn initialize_court() -> CourtId { diff --git a/zrml/global-disputes/src/utils.rs b/zrml/global-disputes/src/utils.rs index bd87378bf..a3170356b 100644 --- a/zrml/global-disputes/src/utils.rs +++ b/zrml/global-disputes/src/utils.rs @@ -61,5 +61,6 @@ where scoring_rule: ScoringRule::CPMM, status: zeitgeist_primitives::types::MarketStatus::Disputed, bonds: Default::default(), + premature_close: None, } } diff --git a/zrml/liquidity-mining/src/tests.rs b/zrml/liquidity-mining/src/tests.rs index 88c0f7c98..e72b5dbda 100644 --- a/zrml/liquidity-mining/src/tests.rs +++ b/zrml/liquidity-mining/src/tests.rs @@ -222,6 +222,7 @@ fn create_default_market(market_id: u128, period: Range) { status: MarketStatus::Closed, scoring_rule: ScoringRule::CPMM, bonds: MarketBonds::default(), + premature_close: None, }, ); } diff --git a/zrml/market-commons/src/tests.rs b/zrml/market-commons/src/tests.rs index 025091e19..59ac807c1 100644 --- a/zrml/market-commons/src/tests.rs +++ b/zrml/market-commons/src/tests.rs @@ -48,7 +48,15 @@ const MARKET_DUMMY: Market { + ensure!(is_authorized, Error::::OnlyAuthorizedCanScheduleEarlyClose); + match p.state { // in these case the market period got already reset to the old period PrematureCloseState::Disputed => { @@ -546,8 +548,6 @@ mod pallet { } } - ensure!(is_authorized, Error::::OnlyAuthorizedCanScheduleEarlyClose); - get_new_period( T::CloseProtectionBlockPeriod::get(), T::CloseProtectionTimeFramePeriod::get(), @@ -716,7 +716,7 @@ mod pallet { if let Some(disputor_bond) = market.bonds.close_dispute.as_ref() { let close_disputor = &disputor_bond.who; - Self::repatriate_close_request_bond(&market_id, &close_disputor)?; + Self::repatriate_close_request_bond(&market_id, close_disputor)?; Self::unreserve_close_dispute_bond(&market_id)?; } @@ -2787,19 +2787,19 @@ mod pallet { Some(p) => { match p.state { PrematureCloseState::ScheduledAsMarketCreator => { - if Self::is_close_request_bond_pending(&market_id, &market, false) { - Self::unreserve_close_request_bond(&market_id)?; + if Self::is_close_request_bond_pending(market_id, market, false) { + Self::unreserve_close_request_bond(market_id)?; } } PrematureCloseState::Disputed => { // this is the case that the original close happened, // although requested early close or disputed // there was no decision made via `reject` or `approve` - if Self::is_close_dispute_bond_pending(&market_id, &market, false) { - Self::unreserve_close_dispute_bond(&market_id)?; + if Self::is_close_dispute_bond_pending(market_id, market, false) { + Self::unreserve_close_dispute_bond(market_id)?; } - if Self::is_close_request_bond_pending(&market_id, &market, false) { - Self::unreserve_close_request_bond(&market_id)?; + if Self::is_close_request_bond_pending(market_id, market, false) { + Self::unreserve_close_request_bond(market_id)?; } } PrematureCloseState::ScheduledAsOther diff --git a/zrml/prediction-markets/src/migrations.rs b/zrml/prediction-markets/src/migrations.rs index e786c928f..3df1800b9 100644 --- a/zrml/prediction-markets/src/migrations.rs +++ b/zrml/prediction-markets/src/migrations.rs @@ -213,7 +213,7 @@ impl OnRuntimeUpgrade for AddEarlyClose assert_eq!(new_market.status, old_market.status); assert_eq!(new_market.report, old_market.report); assert_eq!(new_market.resolved_outcome, old_market.resolved_outcome); - assert_eq!(new_market.dispute_mechanism, Some(old_market.dispute_mechanism.clone())); + assert_eq!(new_market.dispute_mechanism, old_market.dispute_mechanism); assert_eq!(new_market.bonds.oracle, old_market.bonds.oracle); assert_eq!(new_market.bonds.creation, old_market.bonds.creation); assert_eq!(new_market.bonds.outsider, old_market.bonds.outsider); diff --git a/zrml/prediction-markets/src/tests.rs b/zrml/prediction-markets/src/tests.rs index 746291c2c..1e54f399a 100644 --- a/zrml/prediction-markets/src/tests.rs +++ b/zrml/prediction-markets/src/tests.rs @@ -3259,6 +3259,8 @@ fn dispute_early_close_from_market_creator_works() { assert_eq!(market_ids_at_old_end, vec![market_id]); let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.period, old_market_period); + assert_eq!(market.bonds.close_dispute, Some(Bond::new(BOB, CloseDisputeBond::get()))); let new_period = MarketPeriod::Block(0..new_end); assert_eq!( market.premature_close.unwrap(), @@ -3277,6 +3279,194 @@ fn dispute_early_close_from_market_creator_works() { }); } +#[test] +fn dispute_early_close_fails_if_scheduled_as_sudo() { + 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(); + + assert_ok!( + PredictionMarkets::schedule_early_close(RuntimeOrigin::signed(SUDO), market_id,) + ); + + run_blocks(1); + + assert_noop!( + PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,), + Error::::InvalidPrematureCloseState + ); + }); +} + +#[test] +fn dispute_early_close_fails_if_already_disputed() { + 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(); + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + run_blocks(1); + + assert_ok!(PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,)); + + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.premature_close.unwrap().state, PrematureCloseState::Disputed); + + assert_noop!( + PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,), + Error::::InvalidPrematureCloseState + ); + }); +} + +#[test] +fn dispute_early_close_fails_if_already_rejected() { + 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(); + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + run_blocks(1); + + assert_ok!(PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,)); + + assert_ok!(PredictionMarkets::reject_early_close(RuntimeOrigin::signed(SUDO), market_id,)); + + let market = MarketCommons::market(&market_id).unwrap(); + assert_eq!(market.premature_close.unwrap().state, PrematureCloseState::Rejected); + + assert_noop!( + PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,), + Error::::InvalidPrematureCloseState + ); + }); +} + +#[test] +fn schedule_early_close_disputed_sudo_schedule_and_settle_bonds() { + ExtBuilder::default().build().execute_with(|| { + let end = 100; + let old_period = MarketPeriod::Block(0..end); + assert_ok!(PredictionMarkets::create_market( + RuntimeOrigin::signed(ALICE), + Asset::Ztg, + Perbill::zero(), + BOB, + old_period.clone(), + 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(); + + assert_ok!(PredictionMarkets::schedule_early_close( + RuntimeOrigin::signed(ALICE), + market_id, + )); + + run_blocks(1); + + assert_ok!(PredictionMarkets::dispute_early_close(RuntimeOrigin::signed(BOB), market_id,)); + + let reserved_bob = Balances::reserved_balance(&BOB); + let reserved_alice = Balances::reserved_balance(&ALICE); + let free_bob = Balances::free_balance(&BOB); + let free_alice = Balances::free_balance(&ALICE); + + assert_ok!( + PredictionMarkets::schedule_early_close(RuntimeOrigin::signed(SUDO), market_id,) + ); + + let reserved_bob_after = Balances::reserved_balance(&BOB); + let reserved_alice_after = Balances::reserved_balance(&ALICE); + let free_bob_after = Balances::free_balance(&BOB); + let free_alice_after = Balances::free_balance(&ALICE); + + assert_eq!(reserved_alice - reserved_alice_after, CloseRequestBond::get()); + assert_eq!(reserved_bob - reserved_bob_after, CloseDisputeBond::get()); + // market creator Alice gets the bonds + assert_eq!( + free_alice_after - free_alice, + CloseRequestBond::get() + CloseDisputeBond::get() + ); + assert_eq!(free_bob_after - free_bob, 0); + + let now = >::block_number(); + let new_end = now + CloseProtectionBlockPeriod::get(); + let market_ids_at_new_end = >::get(new_end); + assert_eq!(market_ids_at_new_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_period, + new: new_period, + state: PrematureCloseState::ScheduledAsOther, + } + ); + }); +} + #[test] fn dispute_updates_market() { ExtBuilder::default().build().execute_with(|| { diff --git a/zrml/simple-disputes/src/lib.rs b/zrml/simple-disputes/src/lib.rs index b89ee5713..9a395d698 100644 --- a/zrml/simple-disputes/src/lib.rs +++ b/zrml/simple-disputes/src/lib.rs @@ -559,5 +559,6 @@ where scoring_rule: ScoringRule::CPMM, status: zeitgeist_primitives::types::MarketStatus::Disputed, bonds: MarketBonds::default(), + premature_close: None, } } diff --git a/zrml/simple-disputes/src/tests.rs b/zrml/simple-disputes/src/tests.rs index c0ad3deaf..5584c8e2f 100644 --- a/zrml/simple-disputes/src/tests.rs +++ b/zrml/simple-disputes/src/tests.rs @@ -47,7 +47,15 @@ const DEFAULT_MARKET: MarketOf = Market { resolved_outcome: None, scoring_rule: ScoringRule::CPMM, status: MarketStatus::Disputed, - bonds: MarketBonds { creation: None, oracle: None, outsider: None, dispute: None }, + bonds: MarketBonds { + creation: None, + oracle: None, + outsider: None, + dispute: None, + close_dispute: None, + close_request: None, + }, + premature_close: None, }; #[test] diff --git a/zrml/swaps/src/benchmarks.rs b/zrml/swaps/src/benchmarks.rs index cf3b942ea..b2525eec0 100644 --- a/zrml/swaps/src/benchmarks.rs +++ b/zrml/swaps/src/benchmarks.rs @@ -144,6 +144,7 @@ fn push_default_market(caller: T::AccountId, oracle: T::AccountId) -> scoring_rule: ScoringRule::CPMM, status: MarketStatus::Active, bonds: MarketBonds::default(), + premature_close: None, }; T::MarketCommons::push_market(market).unwrap() @@ -244,6 +245,7 @@ benchmarks! { scoring_rule: ScoringRule::CPMM, status: MarketStatus::Active, bonds: MarketBonds::default(), + premature_close: None, } )?; let pool_id: PoolId = 0; @@ -285,6 +287,7 @@ benchmarks! { scoring_rule: ScoringRule::CPMM, status: MarketStatus::Active, bonds: MarketBonds::default(), + premature_close: None, } )?; let pool_id: PoolId = 0; diff --git a/zrml/swaps/src/mock.rs b/zrml/swaps/src/mock.rs index 9d44b1b9f..fd722adaf 100644 --- a/zrml/swaps/src/mock.rs +++ b/zrml/swaps/src/mock.rs @@ -345,5 +345,6 @@ pub(super) fn mock_market( scoring_rule: ScoringRule::CPMM, status: MarketStatus::Active, bonds: MarketBonds::default(), + premature_close: None, } }