Skip to content

Commit

Permalink
Add tests for combinatorial-tokens (#1371)
Browse files Browse the repository at this point in the history
* Add mock for zrml-combinatorial-tokens

* Add test framework

* Add negative tests for `split_token`

* Add more tests, fix some bugs, extend `Event` object

* Add more tests

* Add more integration tests

* Add more integration tests

* Add more integration tests

* More tests

* Add merge tests

* final tests

* fixes
  • Loading branch information
maltekliemann authored Oct 11, 2024
1 parent fba1a07 commit 964e651
Show file tree
Hide file tree
Showing 13 changed files with 1,492 additions and 33 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions primitives/src/constants/base_multiples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub const _36: u128 = 36 * _1;
pub const _40: u128 = 40 * _1;
pub const _70: u128 = 70 * _1;
pub const _80: u128 = 80 * _1;
pub const _99: u128 = 99 * _1;
pub const _100: u128 = 100 * _1;
pub const _101: u128 = 101 * _1;
pub const _444: u128 = 444 * _1;
Expand Down
5 changes: 5 additions & 0 deletions primitives/src/constants/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ parameter_types! {
pub const CorrectionPeriod: BlockNumber = 4;
}

// CombinatorialTokens
parameter_types! {
pub const CombinatorialTokensPalletId: PalletId = PalletId(*b"zge/coto");
}

// Court
parameter_types! {
pub const AppealBond: Balance = 5 * BASE;
Expand Down
21 changes: 21 additions & 0 deletions zrml/combinatorial-tokens/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,33 @@ scale-info = { workspace = true, features = ["derive"] }
sp-runtime = { workspace = true }
zeitgeist-primitives = { workspace = true }

# mock

env_logger = { workspace = true, optional = true }
orml-currencies = { workspace = true, optional = true }
orml-tokens = { workspace = true, optional = true }
pallet-balances = { workspace = true, optional = true }
pallet-timestamp = { workspace = true, optional = true }
sp-io = { workspace = true, optional = true }
zrml-market-commons = { workspace = true, optional = true }

[dev-dependencies]
test-case = { workspace = true }
rstest = { workspace = true }
zrml-combinatorial-tokens = { workspace = true, features = ["default", "mock"] }

[features]
default = ["std"]
mock = [
"env_logger/default",
"orml-currencies/default",
"orml-tokens/default",
"sp-io/default",
"pallet-balances/default",
"pallet-timestamp/default",
"zrml-market-commons/default",
"zeitgeist-primitives/mock",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
Expand Down
157 changes: 124 additions & 33 deletions zrml/combinatorial-tokens/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

extern crate alloc;

pub mod mock;
mod tests;
mod traits;
pub mod types;

Expand Down Expand Up @@ -82,24 +84,44 @@ mod pallet {
pub(crate) type MarketIdOf<T> =
<<T as Config>::MarketCommons as MarketCommonsPalletApi>::MarketId;

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

// TODO Storage Items

#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T>
where
T: Config,
{
TokenSplit,
TokenMerged,
/// User `who` has split `amount` units of token `asset_in` into the same amount of each
/// token in `assets_out` using `partition`. The ith element of `partition` matches the ith
/// element of `assets_out`, so `assets_out[i]` is the outcome represented by the specified
/// `parent_collection_id` together with `partition` in `market_id`.
/// TODO The second sentence is confusing.
TokenSplit {
who: AccountIdOf<T>,
parent_collection_id: Option<CombinatorialId>,
market_id: MarketIdOf<T>,
partition: Vec<Vec<bool>>,
asset_in: AssetOf<T>,
assets_out: Vec<AssetOf<T>>,
collection_ids: Vec<CombinatorialId>,
amount: BalanceOf<T>,
},

/// User `who` has merged `amount` units of each of the tokens in `assets_in` into the same
/// amount of `asset_out`.
TokenMerged {
who: AccountIdOf<T>,
asset_out: AssetOf<T>,
assets_in: Vec<AssetOf<T>>,
amount: BalanceOf<T>,
},
}

#[pallet::error]
pub enum Error<T> {
/// The specified partition is empty, contains overlaps or is too long.
/// The 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.
Expand Down Expand Up @@ -153,48 +175,78 @@ mod pallet {
let free_index_set = Self::free_index_set(market_id, &partition)?;

// Destroy/store the tokens to be split.
if free_index_set.iter().any(|&i| i) {
let split_asset = 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);

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

position
} else {
// Split collateral into first level position. Store the collateral in the
// pallet account. This is the legacy `buy_complete_set`.
T::MultiCurrency::ensure_can_withdraw(collateral_token, &who, amount)?;
T::MultiCurrency::transfer(
collateral_token,
&who,
&Self::account_id(),
amount,
)?;

collateral_token
}
} else {
// Horizontal split.
let remaining_index_set = free_index_set.into_iter().map(|i| !i).collect();
let position = Self::position_from_collection(
let position = Self::position_from_parent_collection(
parent_collection_id,
market_id,
remaining_index_set,
)?;
T::MultiCurrency::ensure_can_withdraw(position, &who, amount)?;
T::MultiCurrency::withdraw(position, &who, amount)?;
}

position
};

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

Self::deposit_event(Event::<T>::TokenSplit);
Self::deposit_event(Event::<T>::TokenSplit {
who,
parent_collection_id,
market_id,
partition,
asset_in: split_asset,
assets_out: positions,
collection_ids,
amount,
});

Ok(())
}
Expand All @@ -213,52 +265,76 @@ mod pallet {
let free_index_set = Self::free_index_set(market_id, &partition)?;

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

// Destroy/store the tokens to be split.
if free_index_set.iter().any(|&i| i) {
let merged_token = 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)?;

position
} 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(),
&who,
amount,
)?;

collateral_token
}
} else {
// Horizontal merge.
let remaining_index_set = free_index_set.into_iter().map(|i| !i).collect();
let position = Self::position_from_collection(
let position = Self::position_from_parent_collection(
parent_collection_id,
market_id,
remaining_index_set,
)?;
T::MultiCurrency::deposit(position, &who, amount)?;
}

Self::deposit_event(Event::<T>::TokenMerged);
position
};

Self::deposit_event(Event::<T>::TokenMerged {
who,
asset_out: merged_token,
assets_in: positions,
amount,
});

Ok(())
}

pub(crate) fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}

fn free_index_set(
market_id: MarketIdOf<T>,
partition: &[Vec<bool>],
Expand All @@ -268,10 +344,10 @@ mod pallet {
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 the partition is not trivial and matches the market's outcomes.
ensure!(index_set.iter().any(|&i| i), Error::<T>::InvalidPartition);
ensure!(index_set.len() == asset_count, Error::<T>::InvalidPartition);
ensure!(!index_set.iter().all(|&i| i), Error::<T>::InvalidPartition);

// Ensure that `index_set` is disjoint from the previously iterated elements of the
// partition.
Expand All @@ -288,21 +364,26 @@ mod pallet {
Ok(free_index_set)
}

fn position_from_collection(
fn collection_id_from_parent_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(
) -> Result<CombinatorialIdOf<T>, DispatchError> {
T::CombinatorialIdManager::get_collection_id(
parent_collection_id,
market_id,
index_set,
false, // TODO Expose this parameter!
)
.ok_or(Error::<T>::InvalidCollectionId)?;
.ok_or(Error::<T>::InvalidCollectionId.into())
}

fn position_from_collection_id(
market_id: MarketIdOf<T>,
collection_id: CombinatorialIdOf<T>,
) -> Result<AssetOf<T>, DispatchError> {
let market = T::MarketCommons::market(&market_id)?;
let collateral_token = market.base_asset;

let position_id =
T::CombinatorialIdManager::get_position_id(collateral_token, collection_id);
Expand All @@ -311,8 +392,18 @@ mod pallet {
Ok(asset)
}

fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
fn position_from_parent_collection(
parent_collection_id: Option<CombinatorialIdOf<T>>,
market_id: MarketIdOf<T>,
index_set: Vec<bool>,
) -> Result<AssetOf<T>, DispatchError> {
let collection_id = Self::collection_id_from_parent_collection(
parent_collection_id,
market_id,
index_set,
)?;

Self::position_from_collection_id(market_id, collection_id)
}
}
}
5 changes: 5 additions & 0 deletions zrml/combinatorial-tokens/src/mock/consts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(feature = "parachain")]
use zeitgeist_primitives::types::{Asset, MarketId};

#[cfg(feature = "parachain")]
pub(crate) const FOREIGN_ASSET: Asset<MarketId> = Asset::ForeignAsset(1);
Loading

0 comments on commit 964e651

Please sign in to comment.