Skip to content

Commit

Permalink
Implement redeem_position (#1385)
Browse files Browse the repository at this point in the history
* Add `PayoutApi` and the corresponding mock

* Implement redeeming tokens

* Add tests for `redeem_position`

* Test `redeem_position` and dummy implement `Payout`
  • Loading branch information
maltekliemann authored Oct 22, 2024
1 parent dd58dc6 commit f57bf6b
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 18 deletions.
2 changes: 2 additions & 0 deletions primitives/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod hybrid_router_orderbook_api;
mod market_builder;
mod market_commons_pallet_api;
mod market_id;
mod payout_api;
mod swaps;
mod zeitgeist_asset;

Expand All @@ -41,5 +42,6 @@ pub use hybrid_router_orderbook_api::*;
pub use market_builder::*;
pub use market_commons_pallet_api::*;
pub use market_id::*;
pub use payout_api::*;
pub use swaps::*;
pub use zeitgeist_asset::*;
25 changes: 25 additions & 0 deletions primitives/src/traits/payout_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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::Vec;

pub trait PayoutApi {
type Balance;
type MarketId;

fn payout_vector(market_id: Self::MarketId) -> Option<Vec<Self::Balance>>;
}
3 changes: 2 additions & 1 deletion runtime/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,8 +1157,9 @@ macro_rules! impl_config_traits {
type CombinatorialIdManager = CryptographicIdManager<MarketId, Blake2_256>;
type MarketCommons = MarketCommons;
type MultiCurrency = AssetManager;
type PalletId = CombinatorialTokensPalletId;
type Payout = PredictionMarkets;
type RuntimeEvent = RuntimeEvent;
type PalletId = CombinatorialTokensPalletId;
}

impl zrml_court::Config for Runtime {
Expand Down
107 changes: 93 additions & 14 deletions zrml/combinatorial-tokens/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
// <https://github.com/gnosis/conditional-tokens-contracts>,
// and has been relicensed under GPL-3.0-or-later in this repository.

// TODO Refactor so that collection IDs are their own type with an `Fq` field and an `odd` field?

#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]

Expand Down Expand Up @@ -52,11 +50,15 @@ mod pallet {
};
use orml_traits::MultiCurrency;
use sp_runtime::{
traits::{AccountIdConversion, Get},
traits::{AccountIdConversion, Get, Zero},
DispatchError, DispatchResult,
};
use zeitgeist_primitives::{
traits::MarketCommonsPalletApi,
math::{
checked_ops_res::{CheckedAddRes},
fixed::FixedMul,
},
traits::{MarketCommonsPalletApi, PayoutApi},
types::{Asset, CombinatorialId},
};

Expand All @@ -72,10 +74,12 @@ mod pallet {

type MultiCurrency: MultiCurrency<Self::AccountId, CurrencyId = AssetOf<Self>>;

#[pallet::constant]
type PalletId: Get<PalletId>;
type Payout: PayoutApi<Balance = BalanceOf<Self>, MarketId = MarketIdOf<Self>>;

type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

#[pallet::constant]
type PalletId: Get<PalletId>;
}

#[pallet::pallet]
Expand Down Expand Up @@ -127,12 +131,27 @@ mod pallet {

#[pallet::error]
pub enum Error<T> {
/// The specified partition is empty, contains overlaps, is too long or doesn't match the
/// Specified index set is trival, empty, or doesn't match the market's number of outcomes.
InvalidIndexSet,

/// Specified partition is empty, contains overlaps, is too long or doesn't match the
/// market's number of outcomes.
InvalidPartition,

/// The specified collection ID is invalid.
/// Specified collection ID is invalid.
InvalidCollectionId,

/// Specified market is not resolved.
PayoutVectorNotFound,

/// Account holds no tokens of this type.
NoTokensFound,

/// Specified token holds no redeemable value.
TokenHasNoValue,

/// Something unexpected happened. You shouldn't see this.
UnexpectedError,
}

#[pallet::call]
Expand Down Expand Up @@ -165,6 +184,19 @@ mod pallet {
let who = ensure_signed(origin)?;
Self::do_merge_position(who, parent_collection_id, market_id, partition, amount)
}

#[pallet::call_index(2)]
#[pallet::weight({0})] // TODO
#[transactional]
pub fn redeem_position(
origin: OriginFor<T>,
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
index_set: Vec<bool>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_redeem_position(who, parent_collection_id, market_id, index_set)
}
}

impl<T: Config> Pallet<T> {
Expand All @@ -191,7 +223,7 @@ mod pallet {
let position = Asset::CombinatorialToken(position_id);

// This will fail if the market has a different collateral than the previous
// markets. TODO A cleaner error message would be nice though...
// markets. FIXME A cleaner error message would be nice though...
T::MultiCurrency::ensure_can_withdraw(position, &who, amount)?;
T::MultiCurrency::withdraw(position, &who, amount)?;

Expand Down Expand Up @@ -301,11 +333,6 @@ mod pallet {
} else {
// Merge first-level tokens into collateral. Move collateral from the pallet
// account to the user's wallet. This is the legacy `sell_complete_set`.
T::MultiCurrency::ensure_can_withdraw(
collateral_token,
&Self::account_id(),
amount,
)?; // Required because `transfer` throws `Underflow` errors sometimes.
T::MultiCurrency::transfer(
collateral_token,
&Self::account_id(),
Expand Down Expand Up @@ -338,6 +365,58 @@ mod pallet {
Ok(())
}

fn do_redeem_position(
who: T::AccountId,
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
index_set: Vec<bool>,
) -> DispatchResult {
let payout_vector =
T::Payout::payout_vector(market_id).ok_or(Error::<T>::PayoutVectorNotFound)?;

let market = T::MarketCommons::market(&market_id)?;
let asset_count = market.outcomes() as usize;
let collateral_token = market.base_asset;

ensure!(index_set.len() == asset_count, Error::<T>::InvalidIndexSet);
ensure!(index_set.iter().any(|&b| b), Error::<T>::InvalidIndexSet);
ensure!(!index_set.iter().all(|&b| b), Error::<T>::InvalidIndexSet);

// Add up values of each outcome.
let mut total_stake: BalanceOf<T> = Zero::zero();
for (&index, value) in index_set.iter().zip(payout_vector.iter()) {
if index {
total_stake = total_stake.checked_add_res(value)?;
}
}

ensure!(!total_stake.is_zero(), Error::<T>::TokenHasNoValue);

let position =
Self::position_from_parent_collection(parent_collection_id, market_id, index_set)?;
let amount = T::MultiCurrency::free_balance(position, &who);
ensure!(!amount.is_zero(), Error::<T>::NoTokensFound);
T::MultiCurrency::withdraw(position, &who, amount)?;

let total_payout = total_stake.bmul(amount)?;

if let Some(pci) = parent_collection_id {
// Merge combinatorial token into higher level position. Destroy the tokens.
let position_id = T::CombinatorialIdManager::get_position_id(collateral_token, pci);
let position = Asset::CombinatorialToken(position_id);
T::MultiCurrency::deposit(position, &who, total_payout)?;
} else {
T::MultiCurrency::transfer(
collateral_token,
&Self::account_id(),
&who,
total_payout,
)?;
}

Ok(())
}

pub(crate) fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
Expand Down
1 change: 1 addition & 0 deletions zrml/combinatorial-tokens/src/mock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@

pub(crate) mod consts;
pub mod ext_builder;
pub(crate) mod types;
pub(crate) mod runtime;
5 changes: 3 additions & 2 deletions zrml/combinatorial-tokens/src/mock/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// along with Zeitgeist. If not, see <https://www.gnu.org/licenses/>.

use crate as zrml_combinatorial_tokens;
use crate::types::CryptographicIdManager;
use crate::{mock::types::MockPayout, types::CryptographicIdManager};
use frame_support::{construct_runtime, traits::Everything, Blake2_256};
use frame_system::mocking::MockBlock;
use sp_runtime::traits::{BlakeTwo256, ConstU32, IdentityLookup};
Expand Down Expand Up @@ -46,8 +46,9 @@ impl zrml_combinatorial_tokens::Config for Runtime {
type CombinatorialIdManager = CryptographicIdManager<MarketId, Blake2_256>;
type MarketCommons = MarketCommons;
type MultiCurrency = Currencies;
type PalletId = CombinatorialTokensPalletId;
type Payout = MockPayout;
type RuntimeEvent = RuntimeEvent;
type PalletId = CombinatorialTokensPalletId;
}

impl orml_currencies::Config for Runtime {
Expand Down
3 changes: 3 additions & 0 deletions zrml/combinatorial-tokens/src/mock/types/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod payout;

pub use payout::MockPayout;
47 changes: 47 additions & 0 deletions zrml/combinatorial-tokens/src/mock/types/payout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use alloc::vec;
use core::cell::RefCell;
use zeitgeist_primitives::{
traits::PayoutApi,
types::{Balance, MarketId},
};

pub struct MockPayout;

impl MockPayout {
pub fn set_return_value(value: Option<Vec<Balance>>) {
PAYOUT_VECTOR_RETURN_VALUE.with(|v| *v.borrow_mut() = Some(value));
}

pub fn not_called() -> bool {
PAYOUT_VECTOR_CALL_DATA.with(|values| values.borrow().is_empty())
}

pub fn called_once_with(expected: MarketId) -> bool {
if PAYOUT_VECTOR_CALL_DATA.with(|values| values.borrow().len()) != 1 {
return false;
}

let actual =
PAYOUT_VECTOR_CALL_DATA.with(|value| *value.borrow().first().expect("can't be empty"));

actual == expected
}
}

impl PayoutApi for MockPayout {
type Balance = Balance;
type MarketId = MarketId;

fn payout_vector(market_id: Self::MarketId) -> Option<Vec<Self::Balance>> {
PAYOUT_VECTOR_CALL_DATA.with(|values| values.borrow_mut().push(market_id));

PAYOUT_VECTOR_RETURN_VALUE
.with(|value| value.borrow().clone())
.expect("MockPayout: No return value configured")
}
}

thread_local! {
pub static PAYOUT_VECTOR_CALL_DATA: RefCell<Vec<MarketId>> = const { RefCell::new(vec![]) };
pub static PAYOUT_VECTOR_RETURN_VALUE: RefCell<Option<Option<Vec<Balance>>>> = const { RefCell::new(None) };
}
2 changes: 2 additions & 0 deletions zrml/combinatorial-tokens/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

mod integration;
mod merge_position;
mod redeem_position;
mod split_position;

use crate::{
mock::{
ext_builder::ExtBuilder,
runtime::{CombinatorialTokens, Currencies, MarketCommons, Runtime, RuntimeOrigin, System},
types::MockPayout,
},
Error, Event, Pallet,
};
Expand Down
Loading

0 comments on commit f57bf6b

Please sign in to comment.