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); + }); +}