Skip to content

Commit

Permalink
Implement Payout for PredictionMarkets (#1386)
Browse files Browse the repository at this point in the history
* Fix copyright and formatting

* Implement `payout_vector` and test

* Fix copyright
  • Loading branch information
maltekliemann authored Oct 23, 2024
1 parent f57bf6b commit 3bda745
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 105 deletions.
5 changes: 1 addition & 4 deletions zrml/combinatorial-tokens/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down
2 changes: 1 addition & 1 deletion zrml/combinatorial-tokens/src/mock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@

pub(crate) mod consts;
pub mod ext_builder;
pub(crate) mod types;
pub(crate) mod runtime;
pub(crate) mod types;
17 changes: 17 additions & 0 deletions zrml/combinatorial-tokens/src/mock/types/mod.rs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

mod payout;

pub use payout::MockPayout;
17 changes: 17 additions & 0 deletions zrml/combinatorial-tokens/src/mock/types/payout.rs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

use alloc::vec;
use core::cell::RefCell;
use zeitgeist_primitives::{
Expand Down
145 changes: 45 additions & 100 deletions zrml/prediction-markets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -3053,108 +3054,52 @@ mod pallet {
}
}

impl<T> PayoutApi for Pallet<T> where T: Config {
impl<T> PayoutApi for Pallet<T>
where
T: Config,
{
type Balance = BalanceOf<T>;
type MarketId = MarketIdOf<T>;

fn payout_vector(_market_id: Self::MarketId) -> Option<Vec<Self::Balance>> {
None
// // TODO Abstract into separate function so we don't have to litter this with ok() calls.
// let market = <zrml_market_commons::Pallet<T>>::market(&market_id).ok()?;
// let market_account = Self::market_account(market_id);

// ensure!(market.status == MarketStatus::Resolved, Error::<T>::MarketIsNotResolved);
// ensure!(market.is_redeemable(), Error::<T>::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::<T>::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::<T>::zero() {
// Self::deposit_event(Event::TokensRedeemed(
// market_id,
// currency_id,
// balance,
// actual_payout,
// sender.clone(),
// ));
// }
fn payout_vector(market_id: Self::MarketId) -> Option<Vec<Self::Balance>> {
// TODO Abstract into separate function so we don't have to litter this with ok() calls.
let market = <zrml_market_commons::Pallet<T>>::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<T> = low.saturated_into();
let high_bal: BalanceOf<T> = high.saturated_into();
let value_bal: BalanceOf<T> = 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::<BalanceOf<T>>::get().ok()?.checked_sub(&payout_long)?;

vec![payout_long, payout_short]
}
};

Some(result)
}
}
}
1 change: 1 addition & 0 deletions zrml/prediction-markets/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
131 changes: 131 additions & 0 deletions zrml/prediction-markets/src/tests/payout_vector.rs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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<BalanceOf<Runtime>>) {
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);
});
}

0 comments on commit 3bda745

Please sign in to comment.