diff --git a/zrml/combinatorial-tokens/src/lib.rs b/zrml/combinatorial-tokens/src/lib.rs
index 47d348e54..6f4a34c9c 100644
--- a/zrml/combinatorial-tokens/src/lib.rs
+++ b/zrml/combinatorial-tokens/src/lib.rs
@@ -54,10 +54,7 @@ mod pallet {
DispatchError, DispatchResult,
};
use zeitgeist_primitives::{
- math::{
- checked_ops_res::{CheckedAddRes},
- fixed::FixedMul,
- },
+ math::{checked_ops_res::CheckedAddRes, fixed::FixedMul},
traits::{MarketCommonsPalletApi, PayoutApi},
types::{Asset, CombinatorialId},
};
diff --git a/zrml/combinatorial-tokens/src/mock/mod.rs b/zrml/combinatorial-tokens/src/mock/mod.rs
index 8d9831fb3..f2933dea7 100644
--- a/zrml/combinatorial-tokens/src/mock/mod.rs
+++ b/zrml/combinatorial-tokens/src/mock/mod.rs
@@ -19,5 +19,5 @@
pub(crate) mod consts;
pub mod ext_builder;
-pub(crate) mod types;
pub(crate) mod runtime;
+pub(crate) mod types;
diff --git a/zrml/combinatorial-tokens/src/mock/types/mod.rs b/zrml/combinatorial-tokens/src/mock/types/mod.rs
index f1234e807..03136bcda 100644
--- a/zrml/combinatorial-tokens/src/mock/types/mod.rs
+++ b/zrml/combinatorial-tokens/src/mock/types/mod.rs
@@ -1,3 +1,20 @@
+// Copyright 2024 Forecasting Technologies LTD.
+//
+// This file is part of Zeitgeist.
+//
+// Zeitgeist is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at
+// your option) any later version.
+//
+// Zeitgeist is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Zeitgeist. If not, see .
+
mod payout;
pub use payout::MockPayout;
diff --git a/zrml/combinatorial-tokens/src/mock/types/payout.rs b/zrml/combinatorial-tokens/src/mock/types/payout.rs
index 407a0a6e1..f3fbd8d2a 100644
--- a/zrml/combinatorial-tokens/src/mock/types/payout.rs
+++ b/zrml/combinatorial-tokens/src/mock/types/payout.rs
@@ -1,3 +1,20 @@
+// Copyright 2024 Forecasting Technologies LTD.
+//
+// This file is part of Zeitgeist.
+//
+// Zeitgeist is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at
+// your option) any later version.
+//
+// Zeitgeist is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Zeitgeist. If not, see .
+
use alloc::vec;
use core::cell::RefCell;
use zeitgeist_primitives::{
diff --git a/zrml/prediction-markets/src/lib.rs b/zrml/prediction-markets/src/lib.rs
index 1e5e4ff2a..abaf617df 100644
--- a/zrml/prediction-markets/src/lib.rs
+++ b/zrml/prediction-markets/src/lib.rs
@@ -60,11 +60,12 @@ mod pallet {
use orml_traits::{MultiCurrency, NamedMultiReservableCurrency};
use sp_arithmetic::per_things::{Perbill, Percent};
use sp_runtime::{
- traits::{Saturating, Zero},
+ traits::{CheckedSub, Saturating, Zero},
DispatchError, DispatchResult, SaturatedConversion,
};
use zeitgeist_primitives::{
constants::MILLISECS_PER_BLOCK,
+ math::fixed::{BaseProvider, FixedDiv, ZeitgeistBase},
traits::{
CompleteSetOperationsApi, DeployPoolApi, DisputeApi, DisputeMaxWeightApi,
DisputeResolutionApi, MarketBuilderTrait, PayoutApi,
@@ -3053,108 +3054,52 @@ mod pallet {
}
}
- impl PayoutApi for Pallet where T: Config {
+ impl PayoutApi for Pallet
+ where
+ T: Config,
+ {
type Balance = BalanceOf;
type MarketId = MarketIdOf;
- fn payout_vector(_market_id: Self::MarketId) -> Option> {
- None
- // // TODO Abstract into separate function so we don't have to litter this with ok() calls.
- // let market = >::market(&market_id).ok()?;
- // let market_account = Self::market_account(market_id);
-
- // ensure!(market.status == MarketStatus::Resolved, Error::::MarketIsNotResolved);
- // ensure!(market.is_redeemable(), Error::::InvalidResolutionMechanism);
-
- // let winning_assets = match resolved_outcome {
- // OutcomeReport::Categorical(category_index) => {
- // vec![(winning_currency_id, ZeitgeistBase::get(), ZeitgeistBase::get())],
- // }
- // OutcomeReport::Scalar(value) => {
- // let long_currency_id = Asset::ScalarOutcome(market_id, ScalarPosition::Long);
- // let short_currency_id = Asset::ScalarOutcome(market_id, ScalarPosition::Short);
-
- // let bound = if let MarketType::Scalar(range) = market.market_type {
- // range
- // } else {
- // return None;
- // };
-
- // let calc_payouts = |final_value: u128,
- // low: u128,
- // high: u128|
- // -> (Perbill, Perbill) {
- // if final_value <= low {
- // return (Perbill::zero(), Perbill::one());
- // }
- // if final_value >= high {
- // return (Perbill::one(), Perbill::zero());
- // }
-
- // let payout_long: Perbill = Perbill::from_rational(
- // final_value.saturating_sub(low),
- // high.saturating_sub(low),
- // );
- // let payout_short: Perbill = Perbill::from_parts(
- // Perbill::one().deconstruct().saturating_sub(payout_long.deconstruct()),
- // );
- // (payout_long, payout_short)
- // };
-
- // let (long_percent, short_percent) =
- // calc_payouts(value, *bound.start(), *bound.end());
-
- // let long_payout = long_percent.mul_floor(long_balance);
- // let short_payout = short_percent.mul_floor(short_balance);
- // // Ensure the market account has enough to pay out - if this is
- // // ever not true then we have an accounting problem.
- // ensure!(
- // T::AssetManager::free_balance(market.base_asset, &market_account)
- // >= long_payout.saturating_add(short_payout),
- // Error::::InsufficientFundsInMarketAccount,
- // );
-
- // vec![
- // (long_currency_id, long_payout, long_balance),
- // (short_currency_id, short_payout, short_balance),
- // ]
- // }
- // };
-
- // for (currency_id, payout, balance) in winning_assets {
- // // Destroy the shares.
- // let missing = T::AssetManager::slash(currency_id, &sender, balance);
- // debug_assert!(
- // missing.is_zero(),
- // "Could not slash all of the amount. currency_id {:?}, sender: {:?}, balance: \
- // {:?}.",
- // currency_id,
- // &sender,
- // balance,
- // );
-
- // // Pay out the winner.
- // let remaining_bal =
- // T::AssetManager::free_balance(market.base_asset, &market_account);
- // let actual_payout = payout.min(remaining_bal);
-
- // T::AssetManager::transfer(
- // market.base_asset,
- // &market_account,
- // &sender,
- // actual_payout,
- // )?;
- // // The if-check prevents scalar markets to emit events even if sender only owns one
- // // of the outcome tokens.
- // if balance != BalanceOf::::zero() {
- // Self::deposit_event(Event::TokensRedeemed(
- // market_id,
- // currency_id,
- // balance,
- // actual_payout,
- // sender.clone(),
- // ));
- // }
+ fn payout_vector(market_id: Self::MarketId) -> Option> {
+ // TODO Abstract into separate function so we don't have to litter this with ok() calls.
+ let market = >::market(&market_id).ok()?;
+
+ if market.status != MarketStatus::Resolved || !market.is_redeemable() {
+ return None;
+ }
+ let resolved_outcome = market.resolved_outcome.clone()?;
+
+ let result = match resolved_outcome {
+ OutcomeReport::Categorical(category_index) => {
+ let mut result = vec![Zero::zero(); market.outcomes() as usize];
+ *result.get_mut(category_index as usize)? = ZeitgeistBase::get().ok()?;
+
+ result
+ }
+ OutcomeReport::Scalar(value) => {
+ let MarketType::Scalar(range) = market.market_type else {
+ return None;
+ };
+ let low = *range.start();
+ let high = *range.end();
+
+ let low_bal: BalanceOf = low.saturated_into();
+ let high_bal: BalanceOf = high.saturated_into();
+ let value_bal: BalanceOf = value.saturated_into();
+
+ let value_clamped = value_bal.max(low_bal).min(high_bal);
+ let nominator = value_clamped.checked_sub(&low_bal)?;
+ let denominator = high_bal.checked_sub(&low_bal)?;
+ let payout_long = nominator.bdiv(denominator).ok()?;
+ let payout_short =
+ ZeitgeistBase::>::get().ok()?.checked_sub(&payout_long)?;
+
+ vec![payout_long, payout_short]
+ }
+ };
+
+ Some(result)
}
}
}
diff --git a/zrml/prediction-markets/src/tests/mod.rs b/zrml/prediction-markets/src/tests/mod.rs
index 80a6962ee..ee0c93570 100644
--- a/zrml/prediction-markets/src/tests/mod.rs
+++ b/zrml/prediction-markets/src/tests/mod.rs
@@ -33,6 +33,7 @@ mod manually_close_market;
mod on_initialize;
mod on_market_close;
mod on_resolution;
+mod payout_vector;
mod redeem_shares;
mod reject_early_close;
mod reject_market;
diff --git a/zrml/prediction-markets/src/tests/payout_vector.rs b/zrml/prediction-markets/src/tests/payout_vector.rs
new file mode 100644
index 000000000..94a4d0ddb
--- /dev/null
+++ b/zrml/prediction-markets/src/tests/payout_vector.rs
@@ -0,0 +1,131 @@
+// Copyright 2024 Forecasting Technologies LTD.
+//
+// This file is part of Zeitgeist.
+//
+// Zeitgeist is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the
+// Free Software Foundation, either version 3 of the License, or (at
+// your option) any later version.
+//
+// Zeitgeist is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Zeitgeist. If not, see .
+
+use super::*;
+use test_case::test_case;
+use zeitgeist_primitives::traits::PayoutApi;
+
+#[test]
+fn payout_vector_works_categorical() {
+ ExtBuilder::default().build().execute_with(|| {
+ let end = 2;
+ simple_create_categorical_market(
+ Asset::Ztg,
+ MarketCreation::Permissionless,
+ 0..end,
+ ScoringRule::AmmCdaHybrid,
+ );
+
+ let market_id = 0;
+
+ let market = MarketCommons::market(&market_id).unwrap();
+ let grace_period = end + market.deadlines.grace_period;
+ run_to_block(grace_period + 1);
+
+ assert_ok!(PredictionMarkets::report(
+ RuntimeOrigin::signed(BOB),
+ 0,
+ OutcomeReport::Categorical(1)
+ ));
+
+ run_blocks(market.deadlines.dispute_duration);
+
+ assert_eq!(PredictionMarkets::payout_vector(market_id), Some(vec![0, BASE]));
+ });
+}
+
+#[test_case(50, vec![0, BASE])]
+#[test_case(100, vec![0, BASE])]
+#[test_case(130, vec![30 * CENT, 70 * CENT])]
+#[test_case(200, vec![BASE, 0])]
+#[test_case(250, vec![BASE, 0])]
+fn payout_vector_works_scalar(value: u128, expected: Vec>) {
+ ExtBuilder::default().build().execute_with(|| {
+ let end = 2;
+ simple_create_scalar_market(
+ Asset::Ztg,
+ MarketCreation::Permissionless,
+ 0..end,
+ ScoringRule::AmmCdaHybrid,
+ );
+
+ let market_id = 0;
+
+ let market = MarketCommons::market(&market_id).unwrap();
+ let grace_period = end + market.deadlines.grace_period;
+ run_to_block(grace_period + 1);
+
+ assert_ok!(PredictionMarkets::report(
+ RuntimeOrigin::signed(BOB),
+ 0,
+ OutcomeReport::Scalar(value)
+ ));
+
+ run_blocks(market.deadlines.dispute_duration);
+
+ assert_eq!(PredictionMarkets::payout_vector(market_id), Some(expected));
+ });
+}
+
+#[test]
+fn payout_vector_fails_on_market_not_found() {
+ ExtBuilder::default().build().execute_with(|| {
+ assert_eq!(PredictionMarkets::payout_vector(1), None);
+ });
+}
+
+#[test]
+fn payout_vector_fails_if_market_is_not_redeemable() {
+ ExtBuilder::default().build().execute_with(|| {
+ simple_create_categorical_market(
+ Asset::Ztg,
+ MarketCreation::Permissionless,
+ 0..2,
+ ScoringRule::Parimutuel,
+ );
+
+ assert_ok!(MarketCommons::mutate_market(&0, |market_inner| {
+ market_inner.status = MarketStatus::Resolved;
+ Ok(())
+ }));
+
+ assert_eq!(PredictionMarkets::payout_vector(0), None);
+ });
+}
+
+#[test_case(MarketStatus::Proposed)]
+#[test_case(MarketStatus::Active)]
+#[test_case(MarketStatus::Closed)]
+#[test_case(MarketStatus::Reported)]
+#[test_case(MarketStatus::Disputed)]
+fn payout_vector_fails_on_invalid_market_status(status: MarketStatus) {
+ ExtBuilder::default().build().execute_with(|| {
+ simple_create_categorical_market(
+ Asset::Ztg,
+ MarketCreation::Permissionless,
+ 0..2,
+ ScoringRule::AmmCdaHybrid,
+ );
+
+ assert_ok!(MarketCommons::mutate_market(&0, |market_inner| {
+ market_inner.status = status;
+ Ok(())
+ }));
+
+ assert_eq!(PredictionMarkets::payout_vector(0), None);
+ });
+}