diff --git a/Cargo.lock b/Cargo.lock index f39fb7ece..3b4ac9ba8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15193,16 +15193,24 @@ version = "0.5.5" dependencies = [ "ark-bn254", "ark-ff", + "env_logger 0.10.2", "frame-benchmarking", "frame-support", "frame-system", + "orml-currencies", + "orml-tokens", "orml-traits", + "pallet-balances", + "pallet-timestamp", "parity-scale-codec", "rstest", "scale-info", + "sp-io", "sp-runtime", "test-case", "zeitgeist-primitives", + "zrml-combinatorial-tokens", + "zrml-market-commons", ] [[package]] diff --git a/primitives/src/constants/base_multiples.rs b/primitives/src/constants/base_multiples.rs index 2f8c41d8e..5d2c4de2d 100644 --- a/primitives/src/constants/base_multiples.rs +++ b/primitives/src/constants/base_multiples.rs @@ -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; diff --git a/primitives/src/constants/mock.rs b/primitives/src/constants/mock.rs index 215bdeade..d33f5386e 100644 --- a/primitives/src/constants/mock.rs +++ b/primitives/src/constants/mock.rs @@ -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; diff --git a/zrml/combinatorial-tokens/Cargo.toml b/zrml/combinatorial-tokens/Cargo.toml index c89e6f8ac..d64d8f42a 100644 --- a/zrml/combinatorial-tokens/Cargo.toml +++ b/zrml/combinatorial-tokens/Cargo.toml @@ -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", diff --git a/zrml/combinatorial-tokens/src/lib.rs b/zrml/combinatorial-tokens/src/lib.rs index 0dfbd4999..4c8c08b97 100644 --- a/zrml/combinatorial-tokens/src/lib.rs +++ b/zrml/combinatorial-tokens/src/lib.rs @@ -22,6 +22,8 @@ extern crate alloc; +pub mod mock; +mod tests; mod traits; pub mod types; @@ -82,24 +84,44 @@ mod pallet { pub(crate) type MarketIdOf = <::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 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, + parent_collection_id: Option, + market_id: MarketIdOf, + partition: Vec>, + asset_in: AssetOf, + assets_out: Vec>, + collection_ids: Vec, + amount: BalanceOf, + }, + + /// User `who` has merged `amount` units of each of the tokens in `assets_in` into the same + /// amount of `asset_out`. + TokenMerged { + who: AccountIdOf, + asset_out: AssetOf, + assets_in: Vec>, + amount: BalanceOf, + }, } #[pallet::error] pub enum Error { - /// 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. @@ -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::, _>>()?; - 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::, _>>()?; + for &position in positions.iter() { T::MultiCurrency::deposit(position, &who, amount)?; } - Self::deposit_event(Event::::TokenSplit); + Self::deposit_event(Event::::TokenSplit { + who, + parent_collection_id, + market_id, + partition, + asset_in: split_asset, + assets_out: positions, + collection_ids, + amount, + }); Ok(()) } @@ -213,19 +265,23 @@ 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::, _>>()?; - 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. @@ -233,32 +289,52 @@ mod pallet { 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::::TokenMerged); + position + }; + + Self::deposit_event(Event::::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, partition: &[Vec], @@ -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::::InvalidPartition); - ensure!(ones < asset_count, Error::::InvalidPartition); + // Ensure that the partition is not trivial and matches the market's outcomes. + ensure!(index_set.iter().any(|&i| i), Error::::InvalidPartition); + ensure!(index_set.len() == asset_count, Error::::InvalidPartition); + ensure!(!index_set.iter().all(|&i| i), Error::::InvalidPartition); // Ensure that `index_set` is disjoint from the previously iterated elements of the // partition. @@ -288,21 +364,26 @@ mod pallet { Ok(free_index_set) } - fn position_from_collection( + fn collection_id_from_parent_collection( parent_collection_id: Option>, market_id: MarketIdOf, index_set: Vec, - ) -> Result, DispatchError> { - let market = T::MarketCommons::market(&market_id)?; - let collateral_token = market.base_asset; - - let collection_id = T::CombinatorialIdManager::get_collection_id( + ) -> Result, DispatchError> { + T::CombinatorialIdManager::get_collection_id( parent_collection_id, market_id, index_set, false, // TODO Expose this parameter! ) - .ok_or(Error::::InvalidCollectionId)?; + .ok_or(Error::::InvalidCollectionId.into()) + } + + fn position_from_collection_id( + market_id: MarketIdOf, + collection_id: CombinatorialIdOf, + ) -> Result, 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); @@ -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>, + market_id: MarketIdOf, + index_set: Vec, + ) -> Result, 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) } } } diff --git a/zrml/combinatorial-tokens/src/mock/consts.rs b/zrml/combinatorial-tokens/src/mock/consts.rs new file mode 100644 index 000000000..6aecaf6f8 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/consts.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "parachain")] +use zeitgeist_primitives::types::{Asset, MarketId}; + +#[cfg(feature = "parachain")] +pub(crate) const FOREIGN_ASSET: Asset = Asset::ForeignAsset(1); diff --git a/zrml/combinatorial-tokens/src/mock/ext_builder.rs b/zrml/combinatorial-tokens/src/mock/ext_builder.rs new file mode 100644 index 000000000..7a12e7d41 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/ext_builder.rs @@ -0,0 +1,55 @@ +use crate::mock::runtime::{Runtime, System}; +use sp_io::TestExternalities; +use sp_runtime::BuildStorage; + +#[cfg(feature = "parachain")] +use {crate::mock::consts::FOREIGN_ASSET, zeitgeist_primitives::types::CustomMetadata}; + +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build() -> TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + // See the logs in tests when using `RUST_LOG=debug cargo test -- --nocapture` + let _ = env_logger::builder().is_test(true).try_init(); + + pallet_balances::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + #[cfg(feature = "parachain")] + { + orml_tokens::GenesisConfig:: { balances: vec![] } + .assimilate_storage(&mut t) + .unwrap(); + + let custom_metadata = + CustomMetadata { allow_as_base_asset: true, ..Default::default() }; + + orml_asset_registry::GenesisConfig:: { + assets: vec![( + FOREIGN_ASSET, + AssetMetadata { + decimals: 18, + name: "MKL".as_bytes().to_vec().try_into().unwrap(), + symbol: "MKL".as_bytes().to_vec().try_into().unwrap(), + existential_deposit: 0, + location: None, + additional: custom_metadata, + } + .encode(), + )], + last_asset_id: FOREIGN_ASSET, + } + .assimilate_storage(&mut t) + .unwrap(); + } + + let mut test_ext: sp_io::TestExternalities = t.into(); + + test_ext.execute_with(|| System::set_block_number(1)); + + test_ext + } +} diff --git a/zrml/combinatorial-tokens/src/mock/mod.rs b/zrml/combinatorial-tokens/src/mock/mod.rs new file mode 100644 index 000000000..f0140b64e --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/mod.rs @@ -0,0 +1,5 @@ +#![cfg(feature = "mock")] + +pub(crate) mod consts; +pub mod ext_builder; +pub(crate) mod runtime; diff --git a/zrml/combinatorial-tokens/src/mock/runtime.rs b/zrml/combinatorial-tokens/src/mock/runtime.rs new file mode 100644 index 000000000..07c9c6a54 --- /dev/null +++ b/zrml/combinatorial-tokens/src/mock/runtime.rs @@ -0,0 +1,110 @@ +use crate as zrml_combinatorial_tokens; +use crate::types::CryptographicIdManager; +use frame_support::{construct_runtime, traits::Everything, Blake2_256}; +use frame_system::mocking::MockBlock; +use sp_runtime::traits::{BlakeTwo256, ConstU32, IdentityLookup}; +use zeitgeist_primitives::{ + constants::mock::{ + BlockHashCount, CombinatorialTokensPalletId, ExistentialDeposit, ExistentialDeposits, + GetNativeCurrencyId, MaxLocks, MaxReserves, MinimumPeriod, + }, + types::{ + AccountIdTest, Amount, Balance, BasicCurrencyAdapter, CurrencyId, Hash, MarketId, Moment, + }, +}; + +construct_runtime! { + pub enum Runtime { + CombinatorialTokens: zrml_combinatorial_tokens, + Balances: pallet_balances, + Currencies: orml_currencies, + MarketCommons: zrml_market_commons, + System: frame_system, + Timestamp: pallet_timestamp, + Tokens: orml_tokens, + } +} + +impl zrml_combinatorial_tokens::Config for Runtime { + type CombinatorialIdManager = CryptographicIdManager; + type MarketCommons = MarketCommons; + type MultiCurrency = Currencies; + type PalletId = CombinatorialTokensPalletId; + type RuntimeEvent = RuntimeEvent; +} + +impl orml_currencies::Config for Runtime { + type GetNativeCurrencyId = GetNativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BasicCurrencyAdapter; + type WeightInfo = (); +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type FreezeIdentifier = (); + type RuntimeHoldReason = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type MaxHolds = (); + type MaxFreezes = (); + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl zrml_market_commons::Config for Runtime { + type Balance = Balance; + type MarketId = MarketId; + type Timestamp = Timestamp; +} + +impl frame_system::Config for Runtime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountIdTest; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type RuntimeCall = RuntimeCall; + type DbWeight = (); + type RuntimeEvent = RuntimeEvent; + type Hash = Hash; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type Nonce = u64; + type MaxConsumers = ConstU32<16>; + type OnKilledAccount = (); + type OnNewAccount = (); + type RuntimeOrigin = RuntimeOrigin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); + type OnSetCode = (); +} + +impl pallet_timestamp::Config for Runtime { + type MinimumPeriod = MinimumPeriod; + type Moment = Moment; + type OnTimestampSet = (); + type WeightInfo = (); +} + +impl orml_tokens::Config for Runtime { + type Amount = Amount; + type Balance = Balance; + type CurrencyId = CurrencyId; + type DustRemovalWhitelist = Everything; + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type CurrencyHooks = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} diff --git a/zrml/combinatorial-tokens/src/tests/integration.rs b/zrml/combinatorial-tokens/src/tests/integration.rs new file mode 100644 index 000000000..db95a2b6b --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/integration.rs @@ -0,0 +1,495 @@ +use super::*; + +#[test] +fn split_followed_by_merge_vertical_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + let amount = _1; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + partition.clone(), + amount, + )); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + assert_eq!(alice.free_balance(ct_001), _1); + assert_eq!(alice.free_balance(ct_110), _1); + assert_eq!(pallet.free_balance(Asset::Ztg), _1); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + partition, + amount, + )); + assert_eq!(alice.free_balance(Asset::Ztg), _100); + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(pallet.free_balance(Asset::Ztg), 0); + }); +} + +#[test] +fn split_followed_by_merge_vertical_with_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + let ct_001_1010 = CombinatorialToken([ + 107, 142, 3, 38, 49, 137, 237, 239, 1, 131, 197, 221, 236, 46, 246, 93, 185, 197, 228, + 184, 75, 79, 107, 73, 89, 19, 22, 124, 15, 58, 110, 100, + ]); + + let parent_market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let parent_amount = _3; + let parent_partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + parent_market_id, + parent_partition.clone(), + parent_amount, + )); + + let child_market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let child_amount = _1; + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + let child_partition = vec![vec![B0, B1, B0, B1], vec![B1, B0, B1, B0]]; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + child_partition.clone(), + child_amount, + )); + assert_eq!(alice.free_balance(ct_001), parent_amount - child_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(Asset::Ztg), _100 - parent_amount); + assert_eq!(alice.free_balance(ct_001_0101), child_amount); + assert_eq!(alice.free_balance(ct_001_1010), child_amount); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + child_partition, + child_amount, + )); + assert_eq!(alice.free_balance(ct_001), parent_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(Asset::Ztg), _100 - parent_amount); + assert_eq!(alice.free_balance(ct_001_0101), 0); + assert_eq!(alice.free_balance(ct_001_1010), 0); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + parent_market_id, + parent_partition, + parent_amount, + )); + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(Asset::Ztg), _100); + assert_eq!(alice.free_balance(ct_001_0101), 0); + assert_eq!(alice.free_balance(ct_001_1010), 0); + assert_eq!(pallet.free_balance(Asset::Ztg), 0); + }); +} + +#[test] +fn split_followed_by_merge_vertical_with_parent_in_opposite_order() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_0 = create_market(Asset::Ztg, MarketType::Categorical(3)); + let market_1 = create_market(Asset::Ztg, MarketType::Categorical(4)); + + let partition_0 = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + let partition_1 = vec![vec![B0, B0, B1, B1], vec![B1, B1, B0, B0]]; + + let amount = _1; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let id_001 = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + let id_110 = [ + 1, 189, 94, 224, 153, 162, 145, 214, 33, 231, 230, 19, 122, 179, 122, 117, 193, 123, + 73, 220, 240, 131, 180, 180, 137, 14, 179, 148, 188, 13, 107, 65, + ]; + + let ct_0011 = CombinatorialToken([ + 32, 70, 65, 46, 183, 161, 122, 58, 80, 224, 102, 106, 63, 89, 191, 19, 235, 137, 64, + 182, 25, 222, 198, 172, 230, 42, 120, 101, 100, 150, 172, 125, + ]); + let ct_1100 = CombinatorialToken([ + 28, 158, 82, 180, 87, 230, 168, 233, 74, 123, 50, 76, 131, 203, 82, 194, 214, 165, 87, + 200, 58, 244, 23, 184, 79, 127, 201, 39, 82, 243, 186, 1, + ]); + let id_0011 = [ + 77, 83, 228, 134, 221, 156, 53, 34, 133, 83, 120, 8, 232, 53, 54, 200, 181, 110, 13, + 145, 238, 130, 69, 147, 108, 167, 41, 217, 105, 22, 126, 136, + ]; + let id_1100 = [ + 10, 211, 115, 219, 24, 177, 205, 243, 234, 68, 234, 119, 21, 211, 103, 229, 185, 23, + 63, 75, 206, 10, 196, 75, 10, 110, 147, 40, 90, 61, 145, 90, + ]; + + let ct_001_0011 = CombinatorialToken([ + 156, 47, 254, 154, 29, 5, 149, 94, 214, 135, 92, 36, 188, 120, 42, 144, 136, 151, 255, + 91, 232, 152, 91, 236, 177, 66, 36, 72, 134, 234, 212, 177, + ]); + let ct_001_1100 = CombinatorialToken([ + 224, 47, 73, 22, 156, 226, 199, 74, 28, 251, 44, 108, 73, 125, 192, 151, 193, 60, 156, + 240, 215, 23, 138, 168, 181, 175, 241, 70, 71, 126, 48, 45, + ]); + let ct_110_0011 = CombinatorialToken([ + 191, 106, 159, 227, 136, 131, 143, 101, 127, 7, 109, 82, 45, 169, 246, 45, 250, 217, + 33, 147, 166, 174, 232, 35, 58, 20, 111, 167, 6, 6, 73, 67, + ]); + let ct_110_1100 = CombinatorialToken([ + 184, 155, 104, 90, 231, 10, 30, 1, 213, 7, 1, 58, 117, 172, 118, 72, 118, 89, 219, 216, + 140, 27, 228, 2, 87, 26, 169, 150, 172, 154, 49, 219, + ]); + + // Split ZTG into A|B and C. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_0, + partition_0.clone(), + amount, + )); + + // Split C into C&(U|V) and C&(W|X). + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(id_001), + market_1, + partition_1.clone(), + amount, + )); + + // Split A|B into into (A|B)&(U|V) and (A|B)&(W|X). + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(id_110), + market_1, + partition_1.clone(), + amount, + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), _1); + assert_eq!(alice.free_balance(ct_001_1100), _1); + assert_eq!(alice.free_balance(ct_110_0011), _1); + assert_eq!(alice.free_balance(ct_110_1100), _1); + assert_eq!(alice.free_balance(ct_0011), 0); + assert_eq!(alice.free_balance(ct_1100), 0); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + + // Merge C&(U|V) and (A|B)&(U|V) into U|V. + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(id_1100), + market_0, + partition_0.clone(), + amount, + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), _1); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_110_0011), _1); + assert_eq!(alice.free_balance(ct_110_1100), 0); + assert_eq!(alice.free_balance(ct_0011), 0); + assert_eq!(alice.free_balance(ct_1100), _1); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + + // Merge C&(W|X) and (A|B)&(W|X) into W|X. + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(id_0011), + market_0, + partition_0, + amount, + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), 0); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_110_0011), 0); + assert_eq!(alice.free_balance(ct_110_1100), 0); + assert_eq!(alice.free_balance(ct_0011), _1); + assert_eq!(alice.free_balance(ct_1100), _1); + assert_eq!(alice.free_balance(Asset::Ztg), _99); + + // Merge U|V and W|X into ZTG. + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_1, + partition_1, + amount, + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(ct_001_0011), 0); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_110_0011), 0); + assert_eq!(alice.free_balance(ct_110_1100), 0); + assert_eq!(alice.free_balance(ct_0011), 0); + assert_eq!(alice.free_balance(ct_1100), 0); + assert_eq!(alice.free_balance(Asset::Ztg), _100); + }); +} + +// This test shows that splitting a token horizontally can be accomplished by splitting the parent +// token vertically with a finer partition. +#[test] +fn split_vertical_followed_by_horizontal_split_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let amount = _1; + + // Split vertically and then horizontally. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + amount, + )); + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + amount, + )); + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_010 = CombinatorialToken([ + 23, 108, 101, 109, 145, 51, 201, 192, 240, 28, 43, 57, 53, 4, 75, 101, 116, 20, 184, + 25, 227, 71, 149, 136, 59, 82, 81, 105, 41, 160, 39, 142, + ]); + let ct_100 = CombinatorialToken([ + 63, 95, 93, 48, 199, 160, 113, 178, 33, 24, 52, 193, 247, 121, 229, 30, 231, 100, 209, + 14, 57, 98, 193, 214, 34, 251, 53, 51, 136, 146, 93, 26, + ]); + + assert_eq!(alice.free_balance(ct_001), amount); + assert_eq!(alice.free_balance(ct_010), amount); + assert_eq!(alice.free_balance(ct_100), amount); + + // Split vertically. This should yield the same amount as the two splits above. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0], vec![B0, B0, B1]], + amount, + )); + + assert_eq!(alice.free_balance(ct_001), 2 * amount); + assert_eq!(alice.free_balance(ct_010), 2 * amount); + assert_eq!(alice.free_balance(ct_100), 2 * amount); + }); +} + +// This test shows that splitting a token horizontally can be accomplished by splitting a the parent +// token vertically with a finer partition. +#[test] +fn split_vertical_followed_by_horizontal_split_with_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + // Prepare level 1 token. + let parent_market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let parent_amount = _6; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + parent_market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + parent_amount, + )); + + let child_market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let child_amount_first_pass = _3; + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let ct_001_0011 = CombinatorialToken([ + 156, 47, 254, 154, 29, 5, 149, 94, 214, 135, 92, 36, 188, 120, 42, 144, 136, 151, 255, + 91, 232, 152, 91, 236, 177, 66, 36, 72, 134, 234, 212, 177, + ]); + let ct_001_1100 = CombinatorialToken([ + 224, 47, 73, 22, 156, 226, 199, 74, 28, 251, 44, 108, 73, 125, 192, 151, 193, 60, 156, + 240, 215, 23, 138, 168, 181, 175, 241, 70, 71, 126, 48, 45, + ]); + let ct_001_1000 = CombinatorialToken([ + 9, 208, 130, 141, 130, 87, 234, 29, 150, 109, 181, 68, 138, 137, 66, 8, 251, 157, 224, + 152, 176, 104, 231, 193, 178, 99, 184, 123, 78, 213, 63, 150, + ]); + let ct_001_0100 = CombinatorialToken([ + 220, 137, 106, 212, 207, 90, 155, 125, 22, 15, 184, 90, 227, 159, 173, 59, 33, 73, 50, + 245, 183, 245, 46, 56, 66, 199, 94, 129, 154, 18, 48, 73, + ]); + + // Split vertically and then horizontally. + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + vec![vec![B0, B0, B1, B1], vec![B1, B1, B0, B0]], + child_amount_first_pass, + )); + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + vec![vec![B1, B0, B0, B0], vec![B0, B1, B0, B0]], + child_amount_first_pass, + )); + + assert_eq!(alice.free_balance(ct_001), parent_amount - child_amount_first_pass); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(ct_001_0011), child_amount_first_pass); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_001_1000), child_amount_first_pass); + assert_eq!(alice.free_balance(ct_001_0100), child_amount_first_pass); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + assert_eq!(pallet.free_balance(ct_001_1100), 0); + + // Split vertically. This should yield the same amount as the two splits above. + let child_amount_second_pass = _2; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + vec![vec![B1, B0, B0, B0], vec![B0, B1, B0, B0], vec![B0, B0, B1, B1]], + child_amount_second_pass, + )); + + let total_child_amount = child_amount_first_pass + child_amount_second_pass; + assert_eq!(alice.free_balance(ct_001), parent_amount - total_child_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(ct_001_0011), total_child_amount); + assert_eq!(alice.free_balance(ct_001_1100), 0); + assert_eq!(alice.free_balance(ct_001_1000), total_child_amount); + assert_eq!(alice.free_balance(ct_001_0100), total_child_amount); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + assert_eq!(pallet.free_balance(ct_001_1100), 0); + }); +} + +#[test] +fn split_horizontal_followed_by_merge_horizontal() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let amount = _1; + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + amount, + )); + + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + amount, + )); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + amount, + )); + + assert_eq!(alice.free_balance(ct_001), _1); + assert_eq!(alice.free_balance(ct_110), _1); + }); +} diff --git a/zrml/combinatorial-tokens/src/tests/merge_position.rs b/zrml/combinatorial-tokens/src/tests/merge_position.rs new file mode 100644 index 000000000..5b9f8bea0 --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/merge_position.rs @@ -0,0 +1,231 @@ +use super::*; +use test_case::test_case; + +#[test_case( + Asset::Ztg, + CombinatorialToken([207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139]), + CombinatorialToken([101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131]) +)] +#[test_case( + Asset::ForeignAsset(1), + CombinatorialToken([97, 71, 129, 186, 219, 73, 163, 242, 183, 111, 224, 26, 45, 104, 11, 229, 241, 31, 154, 126, 118, 218, 142, 191, 3, 255, 156, 77, 32, 1, 66, 227]), + CombinatorialToken([156, 42, 42, 43, 18, 242, 8, 247, 100, 196, 173, 111, 167, 225, 207, 149, 166, 194, 255, 1, 238, 128, 72, 199, 188, 57, 236, 168, 26, 58, 104, 156]) +)] +fn merge_position_works_no_parent( + collateral: Asset, + ct_001: Asset, + ct_110: Asset, +) { + ExtBuilder::build().execute_with(|| { + let amount = _100; + let alice = + Account::new(0).deposit(ct_001, amount).unwrap().deposit(ct_110, amount).unwrap(); + // Mock a deposit into the pallet's account. + let pallet = + Account::new(Pallet::::account_id()).deposit(collateral, amount).unwrap(); + + let market_id = create_market(collateral, MarketType::Categorical(3)); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + amount, + )); + + assert_eq!(alice.free_balance(ct_001), 0); + assert_eq!(alice.free_balance(ct_110), 0); + assert_eq!(alice.free_balance(collateral), _100); + assert_eq!(pallet.free_balance(collateral), 0); + }); +} + +#[test] +fn merge_position_works_parent() { + ExtBuilder::build().execute_with(|| { + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + let ct_001_1010 = CombinatorialToken([ + 107, 142, 3, 38, 49, 137, 237, 239, 1, 131, 197, 221, 236, 46, 246, 93, 185, 197, 228, + 184, 75, 79, 107, 73, 89, 19, 22, 124, 15, 58, 110, 100, + ]); + + let amount = _100; + let alice = Account::new(0) + .deposit(ct_001_0101, amount) + .unwrap() + .deposit(ct_001_1010, amount) + .unwrap(); + + let _ = create_market(Asset::Ztg, MarketType::Categorical(3)); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + Some(parent_collection_id), + market_id, + vec![vec![B0, B1, B0, B1], vec![B1, B0, B1, B0]], + amount, + )); + + assert_eq!(alice.free_balance(ct_001), amount); + assert_eq!(alice.free_balance(ct_001_0101), 0); + assert_eq!(alice.free_balance(ct_001_1010), 0); + }); +} + +#[test] +fn merge_position_horizontal_works() { + ExtBuilder::build().execute_with(|| { + let ct_100 = CombinatorialToken([ + 63, 95, 93, 48, 199, 160, 113, 178, 33, 24, 52, 193, 247, 121, 229, 30, 231, 100, 209, + 14, 57, 98, 193, 214, 34, 251, 53, 51, 136, 146, 93, 26, + ]); + let ct_010 = CombinatorialToken([ + 23, 108, 101, 109, 145, 51, 201, 192, 240, 28, 43, 57, 53, 4, 75, 101, 116, 20, 184, + 25, 227, 71, 149, 136, 59, 82, 81, 105, 41, 160, 39, 142, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + let amount = _100; + let alice = Account::new(0).deposit(ct_100, _100).unwrap().deposit(ct_010, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_ok!(CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B0, B1, B0], vec![B1, B0, B0]], + amount, + )); + + assert_eq!(alice.free_balance(ct_110), amount); + assert_eq!(alice.free_balance(ct_100), 0); + assert_eq!(alice.free_balance(ct_010), 0); + }); +} + +#[test] +fn merge_position_fails_if_market_not_found() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + 0, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + 1, + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test] +fn merge_position_fails_on_invalid_partition_length() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B1]]; + + assert_noop!( + CombinatorialTokens::merge_position(alice.signed(), None, market_id, partition, _1,), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn merge_position_fails_on_trivial_partition_member() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B0]]; + + assert_noop!( + CombinatorialTokens::merge_position(alice.signed(), None, market_id, partition, _1,), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn merge_position_fails_on_overlapping_partition_members() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B1]]; + + assert_noop!( + CombinatorialTokens::merge_position(alice.signed(), None, market_id, partition, _1,), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn merge_position_fails_on_insufficient_funds() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _99).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + ), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn merge_position_fails_on_insufficient_funds_foreign_token() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::ForeignAsset(1), _99).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::merge_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + ), + orml_tokens::Error::::BalanceTooLow + ); + }); +} diff --git a/zrml/combinatorial-tokens/src/tests/mod.rs b/zrml/combinatorial-tokens/src/tests/mod.rs new file mode 100644 index 000000000..4f0bd785c --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/mod.rs @@ -0,0 +1,81 @@ +#![cfg(all(feature = "mock", test))] + +mod integration; +mod merge_position; +mod split_position; + +use crate::{ + mock::{ + ext_builder::ExtBuilder, + runtime::{CombinatorialTokens, Currencies, MarketCommons, Runtime, RuntimeOrigin, System}, + }, + Error, Event, Pallet, +}; +use frame_support::{assert_noop, assert_ok}; +use orml_traits::MultiCurrency; +use sp_runtime::{DispatchError, Perbill}; +use zeitgeist_primitives::{ + constants::base_multiples::*, + types::{ + AccountIdTest, Asset, Asset::CombinatorialToken, Balance, Market, MarketBonds, + MarketCreation, MarketId, MarketPeriod, MarketStatus, MarketType, ScoringRule, + }, +}; +use zrml_market_commons::MarketCommonsPalletApi; + +// For better readability of index sets. +pub(crate) const B0: bool = false; +pub(crate) const B1: bool = true; + +fn create_market(base_asset: Asset, market_type: MarketType) -> MarketId { + let market = Market { + base_asset, + market_id: Default::default(), + creation: MarketCreation::Permissionless, + creator_fee: Perbill::zero(), + creator: Default::default(), + market_type, + dispute_mechanism: None, + metadata: Default::default(), + oracle: Default::default(), + period: MarketPeriod::Block(Default::default()), + deadlines: Default::default(), + report: None, + resolved_outcome: None, + scoring_rule: ScoringRule::AmmCdaHybrid, + status: MarketStatus::Disputed, + bonds: MarketBonds::default(), + early_close: None, + }; + MarketCommons::push_market(market).unwrap(); + MarketCommons::latest_market_id().unwrap() +} + +/// Utility struct for managing test accounts. +pub(crate) struct Account { + id: AccountIdTest, +} + +impl Account { + // TODO Not a pressing issue, but double booking accounts should be illegal. + pub(crate) fn new(id: AccountIdTest) -> Account { + Account { id } + } + + /// Deposits `amount` of `asset` and returns the account to allow call chains. + pub(crate) fn deposit( + self, + asset: Asset, + amount: Balance, + ) -> Result { + Currencies::deposit(asset, &self.id, amount).map(|_| self) + } + + pub(crate) fn signed(&self) -> RuntimeOrigin { + RuntimeOrigin::signed(self.id) + } + + pub(crate) fn free_balance(&self, asset: Asset) -> Balance { + Currencies::free_balance(asset, &self.id) + } +} diff --git a/zrml/combinatorial-tokens/src/tests/split_position.rs b/zrml/combinatorial-tokens/src/tests/split_position.rs new file mode 100644 index 000000000..7109fc13d --- /dev/null +++ b/zrml/combinatorial-tokens/src/tests/split_position.rs @@ -0,0 +1,351 @@ +use super::*; + +#[test] +fn split_position_works_vertical_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let parent_collection_id = None; + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B0, B0, B1], vec![B1, B1, B0]]; + + let amount = _1; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + parent_collection_id, + market_id, + partition.clone(), + amount, + )); + + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + assert_eq!(alice.free_balance(ct_001), amount); + assert_eq!(alice.free_balance(ct_110), amount); + assert_eq!(alice.free_balance(Asset::Ztg), _100 - amount); + assert_eq!(pallet.free_balance(Asset::Ztg), amount); + + System::assert_last_event( + Event::::TokenSplit { + who: alice.id, + parent_collection_id, + market_id, + partition, + asset_in: Asset::Ztg, + assets_out: vec![ct_001, ct_110], + collection_ids: vec![ + [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, + 196, 112, 45, 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ], + [ + 1, 189, 94, 224, 153, 162, 145, 214, 33, 231, 230, 19, 122, 179, 122, 117, + 193, 123, 73, 220, 240, 131, 180, 180, 137, 14, 179, 148, 188, 13, 107, 65, + ], + ], + amount, + } + .into(), + ); + }); +} + +#[test] +fn split_position_works_vertical_with_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + let pallet = Account::new(Pallet::::account_id()); + + let parent_market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let parent_amount = _3; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + parent_market_id, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + parent_amount, + )); + + let child_market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + let child_amount = _1; + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + let partition = vec![vec![B0, B1, B0, B1], vec![B1, B0, B1, B0]]; + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + child_market_id, + partition.clone(), + child_amount, + )); + + // Alice is left with 1 unit of [0, 0, 1], 2 units of [1, 1, 0] and one unit of each of the + // two new tokens. + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + let ct_001_0101 = CombinatorialToken([ + 38, 14, 141, 152, 199, 40, 88, 165, 208, 236, 195, 198, 208, 75, 93, 85, 114, 4, 175, + 225, 211, 72, 142, 210, 98, 202, 168, 193, 245, 217, 239, 28, + ]); + let ct_001_1010 = CombinatorialToken([ + 107, 142, 3, 38, 49, 137, 237, 239, 1, 131, 197, 221, 236, 46, 246, 93, 185, 197, 228, + 184, 75, 79, 107, 73, 89, 19, 22, 124, 15, 58, 110, 100, + ]); + + assert_eq!(alice.free_balance(Asset::Ztg), _100 - parent_amount); + assert_eq!(alice.free_balance(ct_001), parent_amount - child_amount); + assert_eq!(alice.free_balance(ct_110), parent_amount); + assert_eq!(alice.free_balance(ct_001_0101), child_amount); + assert_eq!(alice.free_balance(ct_001_1010), child_amount); + assert_eq!(pallet.free_balance(Asset::Ztg), parent_amount); + assert_eq!(pallet.free_balance(ct_001), 0); // Combinatorial tokens are destroyed when split. + + System::assert_last_event( + Event::::TokenSplit { + who: alice.id, + parent_collection_id: Some(parent_collection_id), + market_id: child_market_id, + partition, + asset_in: ct_001, + assets_out: vec![ct_001_0101, ct_001_1010], + collection_ids: vec![ + [ + 93, 24, 254, 39, 137, 146, 204, 128, 95, 226, 32, 110, 212, 68, 65, 13, + 128, 86, 96, 119, 117, 240, 144, 57, 224, 160, 106, 176, 250, 172, 157, 47, + ], + [ + 98, 123, 162, 148, 54, 175, 126, 250, 173, 76, 229, 156, 108, 125, 245, 68, + 132, 230, 48, 72, 247, 45, 233, 27, 100, 225, 243, 113, 21, 69, 45, 113, + ], + ], + amount: child_amount, + } + .into(), + ); + }); +} + +// Intentionally left out as it is covered by +// `integration::vertical_split_followed_by_horizontal_split_no_parent`. +// #[test] +// fn split_position_works_horizontal_no_parent() {} + +// Intentionally left out as it is covered by +// `integration::vertical_split_followed_by_horizontal_split_with_parent`. +// #[test] +// fn split_position_works_horizontal_with_parent() {} + +#[test] +fn split_position_fails_if_market_not_found() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + 0, + vec![vec![B0, B0, B1], vec![B1, B1, B0]], + 1, + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test] +fn split_position_fails_on_invalid_partition_length() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B1]]; + + assert_noop!( + CombinatorialTokens::split_position(alice.signed(), None, market_id, partition, _1), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_empty_partition_member() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Second element is empty. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B0]]; + + assert_noop!( + CombinatorialTokens::split_position(alice.signed(), None, market_id, partition, _1,), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_overlapping_partition_members() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + // Last elements overlap. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B0, B1], vec![B0, B0, B1]]; + + assert_noop!( + CombinatorialTokens::split_position(alice.signed(), None, market_id, partition, _1), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_trivial_partition() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _100).unwrap(); + + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + let partition = vec![vec![B1, B1, B1]]; + + assert_noop!( + CombinatorialTokens::split_position(alice.signed(), None, market_id, partition, _1), + Error::::InvalidPartition + ); + }); +} + +#[test] +fn split_position_fails_on_insufficient_funds_native_token_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::Ztg, _99).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + ), + orml_currencies::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn split_position_fails_on_insufficient_funds_foreign_token_no_parent() { + ExtBuilder::build().execute_with(|| { + let alice = Account::new(0).deposit(Asset::ForeignAsset(1), _99).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B1], vec![B0, B1, B0]], + _100, + ), + orml_currencies::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn split_position_vertical_fails_on_insufficient_funds_combinatorial_token() { + ExtBuilder::build().execute_with(|| { + let ct_001 = CombinatorialToken([ + 207, 168, 160, 93, 238, 221, 197, 1, 171, 102, 28, 24, 18, 107, 205, 231, 227, 98, 220, + 105, 211, 29, 181, 30, 53, 7, 200, 154, 134, 246, 38, 139, + ]); + + let alice = Account::new(0).deposit(ct_001, _99).unwrap(); + + // Collection ID of [0, 0, 1]. + let parent_collection_id = [ + 6, 44, 173, 50, 122, 106, 144, 185, 253, 19, 252, 218, 215, 241, 218, 37, 196, 112, 45, + 133, 165, 48, 231, 189, 87, 123, 131, 18, 190, 5, 110, 93, + ]; + + let _ = create_market(Asset::Ztg, MarketType::Categorical(3)); + let market_id = create_market(Asset::Ztg, MarketType::Categorical(4)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + market_id, + vec![vec![B1, B0, B1, B0], vec![B0, B1, B0, B1]], + _100, + ), + orml_tokens::Error::::BalanceTooLow + ); + + // Make sure that we're testing for the right balance. This call should work! + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + Some(parent_collection_id), + market_id, + vec![vec![B1, B0, B1, B0], vec![B0, B1, B0, B1]], + _99, + )); + }); +} + +#[test] +fn split_position_horizontal_fails_on_insufficient_funds_combinatorial_token() { + ExtBuilder::build().execute_with(|| { + let ct_110 = CombinatorialToken([ + 101, 210, 61, 196, 5, 247, 150, 41, 186, 49, 11, 63, 139, 53, 25, 65, 161, 83, 24, 142, + 225, 102, 57, 241, 199, 18, 226, 137, 68, 3, 219, 131, + ]); + + let alice = Account::new(0).deposit(ct_110, _99).unwrap(); + + // Market has three outcomes, but there's an element in the partition of size two. + let market_id = create_market(Asset::Ztg, MarketType::Categorical(3)); + + assert_noop!( + CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + _100, + ), + orml_tokens::Error::::BalanceTooLow + ); + + // Make sure that we're testing for the right balance. This call should work! + assert_ok!(CombinatorialTokens::split_position( + alice.signed(), + None, + market_id, + vec![vec![B1, B0, B0], vec![B0, B1, B0]], + _99, + )); + }); +}