Skip to content

Commit

Permalink
Implement zrml-combo extrinsics (#1369)
Browse files Browse the repository at this point in the history
* Extend `Config`

* wip

* Some refactors

* Implement splitting tokens

* Implement merging tokens
  • Loading branch information
maltekliemann authored Oct 4, 2024
1 parent dcd4921 commit a04d02e
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 57 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions primitives/src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use crate::traits::ZeitgeistAssetEnumerator;
use crate::{
traits::PoolSharesId,
types::{CategoryIndex, PoolId},
types::{CategoryIndex, PoolId, CombinatorialId},
};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
Expand Down Expand Up @@ -48,13 +48,14 @@ use serde::{Deserialize, Serialize};
pub enum Asset<MarketId> {
CategoricalOutcome(MarketId, CategoryIndex),
ScalarOutcome(MarketId, ScalarPosition),
CombinatorialOutcome,
CombinatorialToken(CombinatorialId),
PoolShare(PoolId),
#[default]
Ztg,
ForeignAsset(u32),
ParimutuelShare(MarketId, CategoryIndex),
}
// TODO Needs storage migration

#[cfg(feature = "runtime-benchmarks")]
impl<MarketId: MaxEncodedLen> ZeitgeistAssetEnumerator<MarketId> for Asset<MarketId> {
Expand Down
3 changes: 3 additions & 0 deletions primitives/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub type BlockNumber = u64;
/// The index of the category for a `CategoricalOutcome` asset.
pub type CategoryIndex = u16;

/// The type used to identify combinatorial outcomes.
pub type CombinatorialId = [u8; 32];

/// Multihash for digest sizes up to 384 bit.
/// The multicodec encoding the hash algorithm uses only 1 byte,
/// effecitvely limiting the number of available hash types.
Expand Down
1 change: 1 addition & 0 deletions zrml/combo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ frame-benchmarking = { workspace = true, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
halo2curves = { workspace = true }
orml-traits = { workspace = true }
parity-scale-codec = { workspace = true, features = ["derive", "max-encoded-len"] }
scale-info = { workspace = true, features = ["derive"] }
sp-runtime = { workspace = true }
Expand Down
241 changes: 229 additions & 12 deletions zrml/combo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,58 @@ pub use pallet::*;

#[frame_support::pallet]
mod pallet {
use crate::traits::CombinatorialIdManager;
use core::marker::PhantomData;
use frame_support::{
ensure,
pallet_prelude::{IsType, StorageVersion},
require_transactional, transactional,
require_transactional, transactional, PalletId,
};
use frame_system::{
ensure_signed,
pallet_prelude::{BlockNumberFor, OriginFor},
};
use orml_traits::MultiCurrency;
use sp_runtime::{
traits::{AccountIdConversion, Get},
DispatchError, DispatchResult,
};
use zeitgeist_primitives::{
traits::MarketCommonsPalletApi,
types::{Asset, CombinatorialId},
};
use frame_system::{ensure_signed, pallet_prelude::OriginFor};
use sp_runtime::DispatchResult;

#[pallet::config]
pub trait Config: frame_system::Config {
type CombinatorialIdManager: CombinatorialIdManager<
Asset = AssetOf<Self>,
MarketId = MarketIdOf<Self>,
CombinatorialId = CombinatorialId,
>;

type MarketCommons: MarketCommonsPalletApi<AccountId = Self::AccountId, BlockNumber = BlockNumberFor<Self>>;

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

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

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

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(PhantomData<T>);

pub(crate) type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub(crate) type AssetOf<T> = Asset<MarketIdOf<T>>;
pub(crate) type BalanceOf<T> =
<<T as Config>::MultiCurrency as MultiCurrency<AccountIdOf<T>>>::Balance;
pub(crate) type CombinatorialIdOf<T> =
<<T as Config>::CombinatorialIdManager as CombinatorialIdManager>::CombinatorialId;
pub(crate) type MarketIdOf<T> =
<<T as Config>::MarketCommons as MarketCommonsPalletApi>::MarketId;

// TODO Types
pub(crate) const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);

Expand All @@ -58,36 +93,218 @@ mod pallet {
T: Config, {}

#[pallet::error]
pub enum Error<T> {}
pub enum Error<T> {
/// The specified partition is empty, contains overlaps or is too long.
InvalidPartition,

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

#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)] // TODO
#[transactional]
pub fn split_position(origin: OriginFor<T>) -> DispatchResult {
let _ = ensure_signed(origin)?;
Self::do_split_position()
pub fn split_position(
origin: OriginFor<T>,
// TODO Abstract this into a separate type.
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
partition: Vec<Vec<bool>>,
amount: BalanceOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_split_position(who, parent_collection_id, market_id, partition, amount)
}

#[pallet::call_index(1)]
#[pallet::weight(0)] // TODO
#[transactional]
pub fn merge_position(origin: OriginFor<T>) -> DispatchResult {
let _ = ensure_signed(origin)?;
Self::do_merge_position()
pub fn merge_position(
origin: OriginFor<T>,
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
partition: Vec<Vec<bool>>,
amount: BalanceOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_merge_position(who, parent_collection_id, market_id, partition, amount)
}
}

impl<T: Config> Pallet<T> {
#[require_transactional]
fn do_split_position() -> DispatchResult {
fn do_split_position(
who: AccountIdOf<T>,
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
partition: Vec<Vec<bool>>,
amount: BalanceOf<T>,
) -> DispatchResult {
let market = T::MarketCommons::market(&market_id)?;
let collateral_token = market.base_asset;

let free_index_set = Self::free_index_set(parent_collection_id, market_id, &partition)?;

// Destroy/store the tokens to be split.
if free_index_set.iter().any(|&i| i) {
// Vertical split.
if let Some(pci) = parent_collection_id {
// Split 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::withdraw(position, &who, amount)?;
} else {
// Split collateral into first level position. Store the collateral in the
// pallet account. This is the legacy `buy_complete_set`.
T::MultiCurrency::transfer(
collateral_token,
&who,
&Self::account_id(),
amount,
)?;
}
} else {
// Horizontal split.
let remaining_index_set = free_index_set.into_iter().map(|i| !i).collect();
let position = Self::position_from_collection(
parent_collection_id,
market_id,
remaining_index_set,
)?;
T::MultiCurrency::withdraw(position, &who, amount)?;
}

// Deposit the new tokens.
let position_ids = partition
.iter()
.cloned()
.map(|index_set| {
Self::position_from_collection(parent_collection_id, market_id, index_set)
})
.collect::<Result<Vec<_>, _>>()?;
for &position in position_ids.iter() {
T::MultiCurrency::deposit(position, &who, amount)?;
}

Ok(())
}

#[require_transactional]
fn do_merge_position() -> DispatchResult {
fn do_merge_position(
who: AccountIdOf<T>,
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
partition: Vec<Vec<bool>>,
amount: BalanceOf<T>,
) -> DispatchResult {
let market = T::MarketCommons::market(&market_id)?;
let collateral_token = market.base_asset;

let free_index_set = Self::free_index_set(parent_collection_id, market_id, &partition)?;

// Destory the old tokens.
let position_ids = partition
.iter()
.cloned()
.map(|index_set| {
Self::position_from_collection(parent_collection_id, market_id, index_set)
})
.collect::<Result<Vec<_>, _>>()?;
for &position in position_ids.iter() {
T::MultiCurrency::withdraw(position, &who, amount)?;
}

// Destroy/store the tokens to be split.
if free_index_set.iter().any(|&i| i) {
// Vertical merge.
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, amount)?;
} 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::transfer(
collateral_token,
&Self::account_id(),
&who,
amount,
)?;
}
} else {
// Horizontal merge.
let remaining_index_set = free_index_set.into_iter().map(|i| !i).collect();
let position = Self::position_from_collection(
parent_collection_id,
market_id,
remaining_index_set,
)?;
T::MultiCurrency::deposit(position, &who, amount)?;
}

Ok(())
}

fn free_index_set(
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
partition: &Vec<Vec<bool>>,
) -> Result<Vec<bool>, DispatchError> {
let market = T::MarketCommons::market(&market_id)?;
let asset_count = market.outcomes() as usize;
let mut free_index_set = vec![true; asset_count];

for index_set in partition.iter() {
// Ensure that the partition is not trivial.
let ones = index_set.iter().fold(0usize, |acc, &val| acc + (val as usize));
ensure!(ones > 0, Error::<T>::InvalidPartition);
ensure!(ones < asset_count, Error::<T>::InvalidPartition);

// Ensure that `index_set` is disjoint from the previously iterated elements of the
// partition.
ensure!(
free_index_set.iter().zip(index_set.iter()).all(|(i, j)| *i || !*j),
Error::<T>::InvalidPartition
);

// Remove indices of `index_set` from `free_index_set`.
free_index_set =
free_index_set.iter().zip(index_set.iter()).map(|(i, j)| *i && !*j).collect();
}

Ok(free_index_set)
}

fn position_from_collection(
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
index_set: Vec<bool>,
) -> Result<AssetOf<T>, DispatchError> {
let market = T::MarketCommons::market(&market_id)?;
let collateral_token = market.base_asset;

let collection_id = T::CombinatorialIdManager::get_collection_id(
parent_collection_id,
market_id,
index_set,
false, // TODO Expose this parameter!
)
.ok_or(Error::<T>::InvalidCollectionId)?;

let position_id =
T::CombinatorialIdManager::get_position_id(collateral_token, collection_id);
let asset = Asset::CombinatorialToken(position_id);

Ok(asset)
}

fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use sp_runtime::DispatchError;

pub(crate) trait IdManager {
pub(crate) trait CombinatorialIdManager {
type Asset;
type MarketId;
type Id;
type CombinatorialId;

// TODO Replace `Vec<bool>` with a more effective bit mask type.
fn get_collection_id(
parent_collection_id: Option<Self::Id>,
parent_collection_id: Option<Self::CombinatorialId>,
market_id: Self::MarketId,
index_set: Vec<bool>,
force_max_work: bool,
) -> Option<Self::Id>;
) -> Option<Self::CombinatorialId>;

fn get_position_id(
collateral: Self::Asset,
collection_id: Self::Id,
) -> Option<Self::Id>;
collection_id: Self::CombinatorialId,
) -> Self::CombinatorialId;
}
4 changes: 2 additions & 2 deletions zrml/combo/src/traits/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
mod id_manager;
mod combinatorial_id_manager;

pub(crate) use id_manager::IdManager;
pub(crate) use combinatorial_id_manager::CombinatorialIdManager;
Loading

0 comments on commit a04d02e

Please sign in to comment.