zeitgeistpm / zeitgeist

An evolving blockchain for prediction markets and futarchy.
https://zeitgeist.pm
GNU General Public License v3.0
175 stars 41 forks source link

Allow external organisation to create prediction market with own authority #918

Open Chralt98 opened 1 year ago

Chralt98 commented 1 year ago

Because we want both options. The market creator can either set the Advisory Committee as the arbitrator (which is more decentralized), or some other user (which centralized organisations running markets on Zeitgeist might prefer).

But here's the kicker: There's been a bit of a misunderstanding, and we actually want to allow DisputeMechanism::Authorized to have an origin field which is either the AC or an account. This would allow other organizations to run their prediction markets on Zeitgeist without ever relinquishing control over the resolution of the markets, even in case of a dispute. For this type of authority, we would add a time limit, but make it a field of Authorized, thereby allowing users to specify the duration and storing the duration on a market-by-market base in the process, making the code "migration-safe".

enum Authorized {
    AdvisoryCommittee,
    Account(AccountId),
};
Chralt98 commented 1 year ago

Reminder:

Use the following in authorized for account id authority in authorize_market_outcome. And say, that the MDM authorized fails for account id authority in has_failed API.

Concerns about the following:

This should also check if the authority already submitted a report. Otherwise we can run into a situation where the following happens:

let disputes = T::DisputeResolution::get_disputes(&market_id);
if let Some(dispute) = disputes.last() {
    let now = frame_system::Pallet::<T>::block_number();
    let not_expired = now <= dispute.at.saturating_add(T::ReportPeriod::get());
    ensure!(not_expired, Error::<T>::ReportPeriodExpired);
}

Add:

/// In case the authority is a simple account:
/// The period in which the authority has to report. This value must not be zero.
/// This value should be fairly large, so that the authority has enough time to report.
#[pallet::constant]
type ReportPeriod: Get<Self::BlockNumber>;
Chralt98 commented 1 year ago

Add the following tests to authorized:

#[test]
fn has_failed_works_without_report() {
    ExtBuilder::default().build().execute_with(|| {
        frame_system::Pallet::<Runtime>::set_block_number(42);
        let market = market_mock::<Runtime>();
        Markets::<Runtime>::insert(0, &market);
        let now = frame_system::Pallet::<Runtime>::block_number();
        let last_dispute = MarketDispute { at: now, by: BOB, outcome: OutcomeReport::Scalar(1) };

        assert!(!Authorized::has_failed(&[last_dispute.clone()], &0, &market).unwrap());

        frame_system::Pallet::<Runtime>::set_block_number(
            now + <Runtime as crate::Config>::ReportPeriod::get() + 1,
        );

        assert!(Authorized::has_failed(&[last_dispute], &0, &market).unwrap());
    });
}

#[test]
fn has_failed_works_with_renewed_reports() {
    ExtBuilder::default().build().execute_with(|| {
        frame_system::Pallet::<Runtime>::set_block_number(42);
        let market = market_mock::<Runtime>();
        Markets::<Runtime>::insert(0, &market);
        let now = frame_system::Pallet::<Runtime>::block_number();
        let last_dispute = MarketDispute { at: now, by: BOB, outcome: OutcomeReport::Scalar(1) };

        // assume `authorize_market_outcome` is renewed indefintiely
        // by a fallible authority (one account id)
        assert_ok!(Authorized::authorize_market_outcome(
            Origin::signed(AuthorizedDisputeResolutionUser::get()),
            0,
            OutcomeReport::Scalar(1)
        ));

        frame_system::Pallet::<Runtime>::set_block_number(
            now + <Runtime as crate::Config>::ReportPeriod::get() - 1,
        );

        assert!(!Authorized::has_failed(&[last_dispute.clone()], &0, &market).unwrap());

        frame_system::Pallet::<Runtime>::set_block_number(
            now + <Runtime as crate::Config>::ReportPeriod::get() + 1,
        );

        assert!(Authorized::has_failed(&[last_dispute], &0, &market).unwrap());
    });
}

#[test]
fn authorize_market_outcome_fails_with_report_period_expired() {
    ExtBuilder::default().build().execute_with(|| {
        frame_system::Pallet::<Runtime>::set_block_number(42);
        let market = market_mock::<Runtime>();
        Markets::<Runtime>::insert(0, &market);

        let dispute_at = 42;
        let last_dispute =
            MarketDispute { at: dispute_at, by: BOB, outcome: OutcomeReport::Scalar(42) };
        // get_disputes returns a sample dispute in the mock
        assert_eq!(
            <Runtime as crate::Config>::DisputeResolution::get_disputes(&0).pop().unwrap(),
            last_dispute
        );

        frame_system::Pallet::<Runtime>::set_block_number(
            dispute_at + <Runtime as crate::Config>::ReportPeriod::get() + 1,
        );

        assert_noop!(
            Authorized::authorize_market_outcome(
                Origin::signed(AuthorizedDisputeResolutionUser::get()),
                0,
                OutcomeReport::Scalar(1)
            ),
            Error::<Runtime>::ReportPeriodExpired
        );
    });
}
Chralt98 commented 1 year ago

For resolve_expired_mdm_authorized_categorical and resolve_expired_mdm_authorized_scalar add this to the benchmarks to ensure, has_failed returns true then.

let authority_report_period = <T as zrml_authorized::Config>::ReportPeriod::get();
let now = <frame_system::Pallet<T>>::block_number();
// authorized mdm fails after the ReportPeriod
<frame_system::Pallet<T>>::set_block_number(
    now + authority_report_period.saturated_into() + 1u64.saturated_into()
);
Chralt98 commented 1 year ago

https://github.com/zeitgeistpm/zeitgeist/pull/862/commits/eb7b5332245b882c8674a400bad8f909bfced219

maltekliemann commented 1 year ago

Another concern (not sure if this is addressed anywhere) is that the correction period can currently be used to postpone resolution indefinitely (keep calling the authorize_market_outcome extrinsic, which keeps resetting the correction period). I guess the solution (for this and other problems) is to make the reporting period (or reporting period plus correction period) a hard limit for the MDM to come to a conclusion.

sea212 commented 1 year ago

I am wondering why we need the Authorized enum as proposed above. Shouldn't it be sufficient to set the AuthorizedDisputeResolutionOrigin config value in the runtime to the allowed origins and to just specify a maximum report deadline? If a varying deadline is necessary we can supply that during invocation of the authorized mdm and cap it with a config value. I am surprised that we are trying to limit the code here to the two use-cases. There are also other origins, such as future fellowships or any part of the governance body or even (in the future) potentially external oracle services that invoke our code via XCM. I am strongly in favor of keeping the current design that allows any origin and for now just to configure the authorized origin in the runtime to be either the advisory committee or an account.

Chralt98 commented 1 year ago

Added new dispatchable call:

    resolve_expired_mdm_authorized_scalar {
        let report_outcome = OutcomeReport::Scalar(u128::MAX);
        let (caller, market_id) = create_close_and_report_market::<T>(
            MarketCreation::Permissionless,
            MarketType::Scalar(0u128..=u128::MAX),
            report_outcome,
        )?;

        <zrml_market_commons::Pallet::<T>>::mutate_market(&market_id, |market| {
            market.dispute_mechanism = MarketDisputeMechanism::Authorized;
            Ok(())
        })?;

        let market = <zrml_market_commons::Pallet::<T>>::market(&market_id)?;
        if let MarketType::Scalar(range) = market.market_type {
            assert!(1u128 < *range.end());
        } else {
            panic!("Must create scalar market");
        }

        // authorize mdm allows only one dispute
        let outcome = OutcomeReport::Scalar(1u128);
        let disputor = account("disputor", 0, 0);
        T::AssetManager::deposit(Asset::Ztg, &disputor, (u128::MAX).saturated_into())?;
        Pallet::<T>::dispute(RawOrigin::Signed(disputor).into(), market_id, outcome)?;

        let call = Call::<T>::resolve_failed_mdm { market_id };
    }: {
        call.dispatch_bypass_filter(RawOrigin::Signed(caller).into())?;
    } verify {
        assert_last_event::<T>(Event::FailedDisputeMechanismResolved::<T>(market_id).into());
    }

    resolve_expired_mdm_authorized_categorical {
        let categories = T::MaxCategories::get();
        let (caller, market_id) =
            setup_reported_categorical_market_with_pool::<T>(
                categories.into(),
                OutcomeReport::Categorical(0u16)
            )?;

        <zrml_market_commons::Pallet::<T>>::mutate_market(&market_id, |market| {
            market.dispute_mechanism = MarketDisputeMechanism::Authorized;
            Ok(())
        })?;

        // authorize mdm allows only one dispute
        let outcome = OutcomeReport::Categorical(1u16);
        let disputor = account("disputor", 0, 0);
        let dispute_bond = crate::pallet::default_dispute_bond::<T>(0_usize);
        T::AssetManager::deposit(
            Asset::Ztg,
            &disputor,
            dispute_bond,
        )?;
        Pallet::<T>::dispute(RawOrigin::Signed(disputor).into(), market_id, outcome)?;

        let call = Call::<T>::resolve_failed_mdm { market_id };
    }: {
        call.dispatch_bypass_filter(RawOrigin::Signed(caller).into())?;
    } verify {
        assert_last_event::<T>(Event::FailedDisputeMechanismResolved::<T>(market_id).into());
    }
        /// Resolve the market,
        /// if the dispute mechanism was unable to come to a conclusion in a specified time.
        ///
        /// # Weight
        ///
        /// Complexity: `O(n)`, where `n` is the number of outstanding disputes.
        #[pallet::weight(
            T::WeightInfo::resolve_expired_mdm_authorized_categorical()
                .max(T::WeightInfo::resolve_expired_mdm_authorized_scalar())
        )]
        #[transactional]
        pub fn resolve_failed_mdm(
            origin: OriginFor<T>,
            #[pallet::compact] market_id: MarketIdOf<T>,
        ) -> DispatchResultWithPostInfo {
            ensure_signed(origin)?;
            let market = <zrml_market_commons::Pallet<T>>::market(&market_id)?;
            ensure!(market.status == MarketStatus::Disputed, Error::<T>::InvalidMarketStatus);

            let disputes = Disputes::<T>::get(market_id);

            // TODO(#782): use multiple benchmarks paths for different dispute mechanisms
            let _has_failed = match market.dispute_mechanism {
                MarketDisputeMechanism::Authorized => {
                    T::Authorized::has_failed(&disputes, &market_id, &market)?
                }
                MarketDisputeMechanism::Court => {
                    T::Court::has_failed(&disputes, &market_id, &market)?
                }
                MarketDisputeMechanism::SimpleDisputes => {
                    T::SimpleDisputes::has_failed(&disputes, &market_id, &market)?
                }
            };

            // TODO (#918): benchmarks only reach the end when a dispute mechanism has failed
            #[cfg(feature = "runtime-benchmarks")]
            let _has_failed = true;

            ensure!(_has_failed, Error::<T>::DisputeMechanismHasNotFailed);

            Self::on_resolution(&market_id, &market)?;

            Self::deposit_event(Event::FailedDisputeMechanismResolved(market_id));

            let weight = match market.market_type {
                MarketType::Scalar(_) => T::WeightInfo::resolve_expired_mdm_authorized_scalar(),
                MarketType::Categorical(_) => {
                    T::WeightInfo::resolve_expired_mdm_authorized_categorical()
                }
            };

            Ok((Some(weight)).into())
        }
#[test]
fn on_resolution_defaults_to_oracle_report_in_case_of_failed_authorized_mdm() {
    ExtBuilder::default().build().execute_with(|| {
        assert!(Balances::free_balance(Treasury::account_id()).is_zero());
        let end = 2;
        let market_id = 0;
        assert_ok!(PredictionMarkets::create_market(
            Origin::signed(ALICE),
            BOB,
            MarketPeriod::Block(0..end),
            get_deadlines(),
            gen_metadata(2),
            MarketCreation::Permissionless,
            MarketType::Categorical(<Runtime as Config>::MinCategories::get()),
            MarketDisputeMechanism::Authorized,
            ScoringRule::CPMM,
        ));
        assert_ok!(PredictionMarkets::buy_complete_set(Origin::signed(CHARLIE), market_id, CENT));

        let market = MarketCommons::market(&0).unwrap();
        let grace_period = end + market.deadlines.grace_period;
        run_to_block(grace_period + 1);
        assert_ok!(PredictionMarkets::report(
            Origin::signed(BOB),
            market_id,
            OutcomeReport::Categorical(1)
        ));
        let dispute_at = end + grace_period + 2;
        run_to_block(dispute_at);
        assert_ok!(PredictionMarkets::dispute(
            Origin::signed(CHARLIE),
            market_id,
            OutcomeReport::Categorical(0)
        ));
        let market = MarketCommons::market(&market_id).unwrap();
        assert_eq!(market.status, MarketStatus::Disputed);

        let disputes = crate::Disputes::<Runtime>::get(0);
        assert_eq!(disputes.len(), 1);

        let charlie_reserved = Balances::reserved_balance(&CHARLIE);
        assert_eq!(charlie_reserved, DisputeBond::get());

        run_blocks(<Runtime as zrml_authorized::Config>::ReportPeriod::get());
        assert_noop!(
            PredictionMarkets::resolve_failed_mdm(Origin::signed(FRED), market_id),
            Error::<Runtime>::DisputeMechanismHasNotFailed
        );

        run_blocks(1);
        // ReportPeriod is now over
        assert_ok!(PredictionMarkets::resolve_failed_mdm(Origin::signed(FRED), market_id));

        let market_after = MarketCommons::market(&market_id).unwrap();
        assert_eq!(market_after.status, MarketStatus::Resolved);
        let disputes = crate::Disputes::<Runtime>::get(0);
        assert_eq!(disputes.len(), 0);
        assert_ok!(PredictionMarkets::redeem_shares(Origin::signed(CHARLIE), market_id));

        // Make sure rewards are right:
        //
        // - Bob reported "correctly" and in time, so Alice and Bob don't get slashed
        // - Charlie started a dispute which was abandoned, hence he's slashed and his rewards are
        // moved to the treasury
        let alice_balance = Balances::free_balance(&ALICE);
        assert_eq!(alice_balance, 1_000 * BASE);
        let bob_balance = Balances::free_balance(&BOB);
        assert_eq!(bob_balance, 1_000 * BASE);
        let charlie_balance = Balances::free_balance(&CHARLIE);
        assert_eq!(charlie_balance, 1_000 * BASE - charlie_reserved);
        assert_eq!(Balances::free_balance(Treasury::account_id()), charlie_reserved);

        assert!(market_after.bonds.creation.unwrap().is_settled);
        assert!(market_after.bonds.oracle.unwrap().is_settled);
    });
}