Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement zrml-combo extrinsics #1369

Merged
merged 5 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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