diff --git a/zrml/neo-swaps/src/lib.rs b/zrml/neo-swaps/src/lib.rs index dfbe55cdd..9304a132e 100644 --- a/zrml/neo-swaps/src/lib.rs +++ b/zrml/neo-swaps/src/lib.rs @@ -79,7 +79,7 @@ mod pallet { constants::{BASE, CENT}, hybrid_router_api_types::{AmmSoftFail, AmmTrade, ApiError}, math::{ - checked_ops_res::{CheckedAddRes, CheckedSubRes}, + checked_ops_res::{CheckedAddRes, CheckedMulRes, CheckedSubRes}, fixed::{BaseProvider, FixedDiv, FixedMul, ZeitgeistBase}, }, traits::{ @@ -1179,7 +1179,7 @@ mod pallet { let (collection_ids, position_ids, collateral) = Self::split_markets(who.clone(), market_ids.clone(), amount, force_max_work)?; - ensure!(spot_prices.len() == collection_ids.len(), Error::::InvalidSpotPrices); + ensure!(spot_prices.len() == collection_ids.len(), Error::::IncorrectVecLen); ensure!( spot_prices .iter() @@ -1223,6 +1223,9 @@ mod pallet { market_ids.clone().try_into().map_err(|_| Error::::Unexpected)?, ), }; + + ensure!(pool.is_active()?, Error::::MarketNotActive); + // TODO(#1220): Ensure that the existential deposit doesn't kill fees. This is an ugly // hack and system should offer the option to whitelist accounts. T::MultiCurrency::transfer( @@ -1291,9 +1294,10 @@ mod pallet { let amount_out = swap_amount_out.checked_add_res(&amount_in_minus_fees)?; ensure!(amount_out >= min_amount_out, Error::::AmountOutBelowMin); - T::CompleteSetOperations::buy_complete_set( + T::CombinatorialTokensUnsafe::split_position_unsafe( who.clone(), - pool_id, + pool.collateral, + pool.assets(), amount_in_minus_fees, )?; @@ -1417,9 +1421,19 @@ mod pallet { pool.increase_reserve(&asset, &amount_keep)?; } - T::CompleteSetOperations::sell_complete_set( + println!( + "{:?}", + T::MultiCurrency::free_balance(pool.assets()[0], &pool.account_id) + ); + println!( + "{:?}", + T::MultiCurrency::free_balance(pool.assets()[1], &pool.account_id) + ); + println!("{:?}", amount_out); + T::CombinatorialTokensUnsafe::merge_position_unsafe( pool.account_id.clone(), - pool_id, + pool.collateral, + pool.assets(), amount_out, )?; @@ -1535,9 +1549,11 @@ mod pallet { .map(|market_id| T::MarketCommons::market(market_id)) .collect::, _>>()?; - // Calculate the total amount of split opterations required. Note that it's 1 split - // operation for the first market. TODO Abstract into separate function. - let mut total_splits = 0u16; // Zero indicates first pass. + // Calculate the total amount of split operations required. One split for splitting + // collateral into the positions of the first market, and then it's one split for each + // position created in the previous step. + let mut total_splits = 0u16; + let mut prev_positions = 0u16; for market in markets.iter() { ensure!( market.scoring_rule == ScoringRule::AmmCdaHybrid, @@ -1546,8 +1562,10 @@ mod pallet { if total_splits == 0u16 { total_splits = 1u16; + prev_positions = market.outcomes(); } else { - total_splits = total_splits.saturating_mul(market.outcomes()); + total_splits = total_splits.checked_add_res(&prev_positions)?; + prev_positions = prev_positions.checked_mul_res(&market.outcomes())?; } } ensure!(total_splits <= T::MaxSplits::get(), Error::::MaxSplitsExceeded); @@ -1558,15 +1576,18 @@ mod pallet { let mut collection_ids: Vec = vec![]; let mut position_ids = vec![]; for market_id in market_ids.iter() { + println!("market_id: {:?}", market_id); let asset_count = T::MarketCommons::market(market_id)?.outcomes() as usize; + println!("asset_count: {:?}", asset_count); let partition: Vec> = (0..asset_count) .map(|index| { let mut index_set = vec![false; asset_count]; - index_set.get_mut(index).map(|_| true); + index_set.get_mut(index).map(|x| *x = true); index_set }) .collect(); + println!("partition: {:?}", partition); if split_count == 0 { let split_position_info = T::CombinatorialTokens::split_position( @@ -1587,9 +1608,11 @@ mod pallet { let mut new_position_ids = vec![]; for parent_collection_id in collection_ids.iter() { + println!("foo"); if split_count > total_splits { return Err(Error::::Unexpected.into()); } + println!("ibar"); let split_position_info = T::CombinatorialTokens::split_position( who.clone(), diff --git a/zrml/neo-swaps/src/tests/combo_buy.rs b/zrml/neo-swaps/src/tests/combo_buy.rs index e8e2ab866..63d43375b 100644 --- a/zrml/neo-swaps/src/tests/combo_buy.rs +++ b/zrml/neo-swaps/src/tests/combo_buy.rs @@ -356,7 +356,7 @@ fn combo_buy_fails_on_insufficient_funds() { amount_in, 0, ), - zrml_prediction_markets::Error::::NotEnoughBalance, + orml_currencies::Error::::BalanceTooLow, ); }); } diff --git a/zrml/neo-swaps/src/tests/combo_sell.rs b/zrml/neo-swaps/src/tests/combo_sell.rs index e6a614cf0..8b67740d8 100644 --- a/zrml/neo-swaps/src/tests/combo_sell.rs +++ b/zrml/neo-swaps/src/tests/combo_sell.rs @@ -17,593 +17,597 @@ use super::*; use test_case::test_case; -use zeitgeist_primitives::types::Asset::CategoricalOutcome; -#[test] -fn combo_sell_works() { - ExtBuilder::default().build().execute_with(|| { - let liquidity = _10; - let spot_prices = vec![_1_4, _3_4]; - let swap_fee = CENT; - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Scalar(0..=1), - liquidity, - spot_prices.clone(), - swap_fee, - ); - let pool = Pools::::get(market_id).unwrap(); - let amount_buy = _10; - let amount_keep = 0; - let liquidity_parameter_before = pool.liquidity_parameter; - deposit_complete_set(market_id, BOB, amount_buy); - let buy = vec![pool.assets()[1]]; - let keep = vec![]; - let sell = vec![pool.assets()[0]]; - assert_ok!(NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 2, - buy.clone(), - keep.clone(), - sell.clone(), - amount_buy, - amount_keep, - 0, - )); - let total_fee_percentage = swap_fee + EXTERNAL_FEES; - let expected_amount_out = 59632253897; - let expected_fees = total_fee_percentage.bmul(expected_amount_out).unwrap(); - let expected_swap_fee_amount = expected_fees / 2; - let expected_external_fee_amount = expected_fees - expected_swap_fee_amount; - let expected_amount_out_minus_fees = expected_amount_out - expected_fees; - assert_balance!(BOB, BASE_ASSET, expected_amount_out_minus_fees); - assert_balance!(BOB, buy[0], 0); - assert_pool_state!( - market_id, - vec![40367746103, 61119621067], - [5_714_285_714, 4_285_714_286], - liquidity_parameter_before, - create_b_tree_map!({ ALICE => liquidity }), - expected_swap_fee_amount, - ); - assert_balance!( - pool.account_id, - BASE_ASSET, - expected_swap_fee_amount + AssetManager::minimum_balance(pool.collateral) - ); - assert_balance!(FEE_ACCOUNT, BASE_ASSET, expected_external_fee_amount); - assert_eq!( - AssetManager::total_issuance(pool.assets()[0]), - liquidity + amount_buy - expected_amount_out - ); - assert_eq!( - AssetManager::total_issuance(pool.assets()[1]), - liquidity + amount_buy - expected_amount_out - ); - System::assert_last_event( - Event::ComboSellExecuted { - who: BOB, - pool_id: market_id, - buy, - keep, - sell, - amount_buy, - amount_keep, - amount_out: expected_amount_out_minus_fees, - swap_fee_amount: expected_swap_fee_amount, - external_fee_amount: expected_external_fee_amount, - } - .into(), - ); - }); -} - -#[test_case( - 1000 * _1, - vec![1_250_000_000; 8], - vec![0, 2, 5], - vec![6, 7], - vec![1, 3, 4], - _500, - _300, - 2_091_832_646_248, - vec![ - 12_865_476_891_584, - 7_865_476_891_584, - 12_865_476_891_584, - 7_865_476_891_584, - 7_865_476_891_584, - 12_865_476_891_584, - 10_865_476_891_584, - 10_865_476_891_584, - ], - vec![ - 688_861_105, - 1_948_393_435, - 688_861_105, - 1_948_393_435, - 1_948_393_435, - 688_861_105, - 1_044_118_189, - 1_044_118_189, - ], - 21_345_231_084 -)] -#[test_case( - _321, - vec![20 * CENT, 30 * CENT, 50 * CENT], - vec![0, 2], - vec![], - vec![1], - _500, - 0, - 2_012_922_832_062, - vec![ - 6_155_997_110_140, - 347_302_977_256, - 4_328_468_861_556, - ], - vec![ - 456_610_616, - 8_401_862_845, - 1_141_526_539, - ], - 20_540_028_899 -)] -fn combo_sell_works_multi_market( - liquidity: u128, - spot_prices: Vec, - buy_indices: Vec, - keep_indices: Vec, - sell_indices: Vec, - amount_in_buy: u128, - amount_in_keep: u128, - expected_amount_out: u128, - expected_reserves: Vec, - expected_spot_prices: Vec, - expected_fees: u128, -) { - ExtBuilder::default().build().execute_with(|| { - let asset_count = spot_prices.len() as u16; - let swap_fee = CENT; - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Categorical(asset_count), - liquidity, - spot_prices.clone(), - swap_fee, - ); - - let buy: Vec<_> = - buy_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); - let keep: Vec<_> = - keep_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); - let sell: Vec<_> = - sell_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); - - for &asset in buy.iter() { - assert_ok!(AssetManager::deposit(asset, &BOB, amount_in_buy)); - } - for &asset in keep.iter() { - assert_ok!(AssetManager::deposit(asset, &BOB, amount_in_keep)); - } - - let pool = Pools::::get(market_id).unwrap(); - let expected_liquidity = pool.liquidity_parameter; - - assert_ok!(NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - asset_count, - buy.clone(), - keep.clone(), - sell.clone(), - amount_in_buy, - amount_in_keep, - 0, - )); - - assert_balance!(BOB, BASE_ASSET, expected_amount_out); - for asset in pool.assets() { - assert_balance!(BOB, asset, 0); - } - assert_pool_state!( - market_id, - expected_reserves, - expected_spot_prices, - expected_liquidity, - create_b_tree_map!({ ALICE => liquidity }), - expected_fees, - ); - }); -} - -#[test] -fn combo_sell_fails_on_incorrect_asset_count() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Scalar(0..=1), - _10, - vec![_1_2, _1_2], - CENT, - ); - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 1, - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], - vec![], - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], - _1, - 0, - 0 - ), - Error::::IncorrectAssetCount - ); - }); -} - -#[test] -fn combo_sell_fails_on_market_not_found() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Scalar(0..=1), - _10, - vec![_1_2, _1_2], - CENT, - ); - Markets::::remove(market_id); - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 2, - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], - vec![], - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], - _1, - 0, - 0 - ), - zrml_market_commons::Error::::MarketDoesNotExist, - ); - }); -} - -#[test_case(MarketStatus::Proposed)] -#[test_case(MarketStatus::Closed)] -#[test_case(MarketStatus::Reported)] -#[test_case(MarketStatus::Disputed)] -#[test_case(MarketStatus::Resolved)] -fn combo_sell_fails_on_inactive_market(market_status: MarketStatus) { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Scalar(0..=1), - _10, - vec![_1_2, _1_2], - CENT, - ); - MarketCommons::mutate_market(&market_id, |market| { - market.status = market_status; - Ok(()) - }) - .unwrap(); - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 2, - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], - vec![], - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], - _1, - 0, - 0 - ), - Error::::MarketNotActive, - ); - }); -} - -#[test] -fn combo_sell_fails_on_pool_not_found() { - ExtBuilder::default().build().execute_with(|| { - let market_id = - create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid); - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 2, - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], - vec![], - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], - _1, - 0, - 0 - ), - Error::::PoolNotFound, - ); - }); -} - -#[test] -fn combo_sell_fails_on_insufficient_funds() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Scalar(0..=1), - _10, - vec![_1_2, _1_2], - CENT, - ); - let amount_in = _10; - let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); - assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in - 1)); - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 2, - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], - vec![], - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], - amount_in, - 0, - 0, - ), - orml_tokens::Error::::BalanceTooLow, - ); - }); -} - -#[test] -fn combo_sell_fails_on_amount_out_below_min() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Scalar(0..=1), - _100, - vec![_1_2, _1_2], - CENT, - ); - let amount_in = _20; - let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); - assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in)); - // Selling 20 at price of .5 will return less than 10 dollars due to slippage. - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 2, - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], - vec![], - vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], - amount_in, - 0, - _10 - ), - Error::::AmountOutBelowMin, - ); - }); -} - -#[test_case(vec![], vec![], vec![2]; "empty_buy")] -#[test_case(vec![0], vec![], vec![]; "empty_sell")] -#[test_case(vec![0, 1], vec![2, 1], vec![3, 4]; "buy_keep_overlap")] -#[test_case(vec![0, 1], vec![2, 4], vec![3, 1]; "buy_sell_overlap")] -#[test_case(vec![0, 1], vec![2, 4], vec![4, 3]; "keep_sell_overlap")] -#[test_case(vec![0, 1, 999], vec![2, 4], vec![5, 3]; "out_of_bounds_buy")] -#[test_case(vec![0, 1], vec![2, 4, 999], vec![5, 3]; "out_of_bounds_keep")] -#[test_case(vec![0, 1], vec![2, 4], vec![5, 999, 3]; "out_of_bounds_sell")] -#[test_case(vec![0, 6, 1, 6], vec![2, 4], vec![5, 3]; "duplicate_buy")] -#[test_case(vec![0, 1], vec![2, 2, 4], vec![5, 3]; "duplicate_keep")] -#[test_case(vec![0, 1], vec![2, 4], vec![5, 3, 6, 6, 6]; "duplicate_sell")] -fn combo_sell_fails_on_invalid_partition( - indices_buy: Vec, - indices_keep: Vec, - indices_sell: Vec, -) { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Categorical(7), - _10, - vec![_1_7, _1_7, _1_7, _1_7, _1_7, _1_7, _1_7 + 4], - CENT, - ); - - let buy = indices_buy.into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - let keep = indices_keep.into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - let sell = indices_sell.into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - - // Buying 1 at price of .5 will return less than 2 outcomes due to slippage. - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 7, - buy, - keep, - sell, - _2, - 0, // Keep this zero to avoid a different error due to invalid `amount_keep` param. - 0 - ), - Error::::InvalidPartition, - ); - }); -} - -#[test] -fn combo_sell_fails_on_spot_price_slipping_too_low() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Categorical(5), - _10, - vec![_1_5, _1_5, _1_5, _1_5, _1_5], - CENT, - ); - let amount_buy = _100; - - for i in 0..4 { - assert_ok!(AssetManager::deposit( - Asset::CategoricalOutcome(market_id, i), - &BOB, - amount_buy - )); - } - - let buy = [0, 1, 2, 3].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - let sell = [4].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 5, - buy, - vec![], - sell, - amount_buy, - 0, - 0 - ), - Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow), - ); - }); -} - -#[test] -fn combo_sell_fails_on_spot_price_slipping_too_high() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Categorical(5), - _10, - vec![_1_5, _1_5, _1_5, _1_5, _1_5], - CENT, - ); - let amount_buy = _100; - - for i in 0..4 { - assert_ok!(AssetManager::deposit( - Asset::CategoricalOutcome(market_id, i), - &BOB, - amount_buy - )); - } - - let buy = [0].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - let sell = [1, 2, 3, 4].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 5, - buy, - vec![], - sell, - amount_buy, - 0, - 0 - ), - Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow), - ); - }); -} - -#[test] -fn combo_sell_fails_on_large_amount() { - ExtBuilder::default().build().execute_with(|| { - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Categorical(5), - _10, - vec![_1_5, _1_5, _1_5, _1_5, _1_5], - CENT, - ); - let amount_buy = 100 * _100; - - for i in 0..4 { - assert_ok!(AssetManager::deposit( - Asset::CategoricalOutcome(market_id, i), - &BOB, - amount_buy - )); - } - - let buy = [0].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - let sell = [1, 2, 3, 4].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); - - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - 5, - buy, - vec![], - sell, - amount_buy, - 0, - 0 - ), - Error::::MathError, - ); - }); -} - -#[test_case(vec![], 1)] -#[test_case(vec![2], _2)] -fn combo_sell_fails_on_invalid_amount_keep(keep_indices: Vec, amount_in_keep: u128) { - ExtBuilder::default().build().execute_with(|| { - let spot_prices = vec![25 * CENT; 4]; - let asset_count = spot_prices.len() as u16; - let market_id = create_market_and_deploy_pool( - ALICE, - BASE_ASSET, - MarketType::Categorical(asset_count), - _10, - spot_prices, - CENT, - ); - - let buy = vec![Asset::CategoricalOutcome(market_id, 0)]; - let sell = vec![Asset::CategoricalOutcome(market_id, 1)]; - let keep: Vec<_> = - keep_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); - - assert_noop!( - NeoSwaps::combo_sell( - RuntimeOrigin::signed(BOB), - market_id, - asset_count, - buy.clone(), - keep.clone(), - sell.clone(), - _1, - amount_in_keep, - 0, - ), - Error::::InvalidAmountKeep - ); - }); -} +// #[test] +// fn combo_sell_works() { +// ExtBuilder::default().build().execute_with(|| { +// let liquidity = _10; +// let spot_prices = vec![_1_4, _3_4]; +// let swap_fee = CENT; +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Scalar(0..=1), +// liquidity, +// spot_prices.clone(), +// swap_fee, +// ); +// let pool = Pools::::get(market_id).unwrap(); +// let amount_buy = _10; +// let amount_keep = 0; +// let liquidity_parameter_before = pool.liquidity_parameter; +// +// let buy = vec![pool.assets()[1]]; +// let keep = vec![]; +// let sell = vec![pool.assets()[0]]; +// +// for &asset in buy.iter() { +// assert_ok!(AssetManager::deposit(asset, &BOB, amount_buy)); +// } +// +// assert_ok!(NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 2, +// buy.clone(), +// keep.clone(), +// sell.clone(), +// amount_buy, +// amount_keep, +// 0, +// )); +// let total_fee_percentage = swap_fee + EXTERNAL_FEES; +// let expected_amount_out = 59632253897; +// let expected_fees = total_fee_percentage.bmul(expected_amount_out).unwrap(); +// let expected_swap_fee_amount = expected_fees / 2; +// let expected_external_fee_amount = expected_fees - expected_swap_fee_amount; +// let expected_amount_out_minus_fees = expected_amount_out - expected_fees; +// assert_balance!(BOB, BASE_ASSET, expected_amount_out_minus_fees); +// assert_balance!(BOB, buy[0], 0); +// assert_pool_state!( +// market_id, +// vec![40367746103, 61119621067], +// [5_714_285_714, 4_285_714_286], +// liquidity_parameter_before, +// create_b_tree_map!({ ALICE => liquidity }), +// expected_swap_fee_amount, +// ); +// assert_balance!( +// pool.account_id, +// BASE_ASSET, +// expected_swap_fee_amount + AssetManager::minimum_balance(pool.collateral) +// ); +// assert_balance!(FEE_ACCOUNT, BASE_ASSET, expected_external_fee_amount); +// assert_eq!( +// AssetManager::total_issuance(pool.assets()[0]), +// liquidity + amount_buy - expected_amount_out +// ); +// assert_eq!( +// AssetManager::total_issuance(pool.assets()[1]), +// liquidity + amount_buy - expected_amount_out +// ); +// System::assert_last_event( +// Event::ComboSellExecuted { +// who: BOB, +// pool_id: market_id, +// buy, +// keep, +// sell, +// amount_buy, +// amount_keep, +// amount_out: expected_amount_out_minus_fees, +// swap_fee_amount: expected_swap_fee_amount, +// external_fee_amount: expected_external_fee_amount, +// } +// .into(), +// ); +// }); +// } +// +// #[test_case( +// 1000 * _1, +// vec![1_250_000_000; 8], +// vec![0, 2, 5], +// vec![6, 7], +// vec![1, 3, 4], +// _500, +// _300, +// 2_091_832_646_248, +// vec![ +// 12_865_476_891_584, +// 7_865_476_891_584, +// 12_865_476_891_584, +// 7_865_476_891_584, +// 7_865_476_891_584, +// 12_865_476_891_584, +// 10_865_476_891_584, +// 10_865_476_891_584, +// ], +// vec![ +// 688_861_105, +// 1_948_393_435, +// 688_861_105, +// 1_948_393_435, +// 1_948_393_435, +// 688_861_105, +// 1_044_118_189, +// 1_044_118_189, +// ], +// 21_345_231_084 +// )] +// #[test_case( +// _321, +// vec![20 * CENT, 30 * CENT, 50 * CENT], +// vec![0, 2], +// vec![], +// vec![1], +// _500, +// 0, +// 2_012_922_832_062, +// vec![ +// 6_155_997_110_140, +// 347_302_977_256, +// 4_328_468_861_556, +// ], +// vec![ +// 456_610_616, +// 8_401_862_845, +// 1_141_526_539, +// ], +// 20_540_028_899 +// )] +// fn combo_sell_works_multi_market( +// liquidity: u128, +// spot_prices: Vec, +// buy_indices: Vec, +// keep_indices: Vec, +// sell_indices: Vec, +// amount_in_buy: u128, +// amount_in_keep: u128, +// expected_amount_out: u128, +// expected_reserves: Vec, +// expected_spot_prices: Vec, +// expected_fees: u128, +// ) { +// ExtBuilder::default().build().execute_with(|| { +// let asset_count = spot_prices.len() as u16; +// let swap_fee = CENT; +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Categorical(asset_count), +// liquidity, +// spot_prices.clone(), +// swap_fee, +// ); +// +// let buy: Vec<_> = +// buy_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); +// let keep: Vec<_> = +// keep_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); +// let sell: Vec<_> = +// sell_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); +// +// for &asset in buy.iter() { +// assert_ok!(AssetManager::deposit(asset, &BOB, amount_in_buy)); +// } +// for &asset in keep.iter() { +// assert_ok!(AssetManager::deposit(asset, &BOB, amount_in_keep)); +// } +// +// let pool = Pools::::get(market_id).unwrap(); +// let expected_liquidity = pool.liquidity_parameter; +// +// assert_ok!(NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// asset_count, +// buy.clone(), +// keep.clone(), +// sell.clone(), +// amount_in_buy, +// amount_in_keep, +// 0, +// )); +// +// assert_balance!(BOB, BASE_ASSET, expected_amount_out); +// for asset in pool.assets() { +// assert_balance!(BOB, asset, 0); +// } +// assert_pool_state!( +// market_id, +// expected_reserves, +// expected_spot_prices, +// expected_liquidity, +// create_b_tree_map!({ ALICE => liquidity }), +// expected_fees, +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_incorrect_asset_count() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Scalar(0..=1), +// _10, +// vec![_1_2, _1_2], +// CENT, +// ); +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 1, +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], +// vec![], +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], +// _1, +// 0, +// 0 +// ), +// Error::::IncorrectAssetCount +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_market_not_found() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Scalar(0..=1), +// _10, +// vec![_1_2, _1_2], +// CENT, +// ); +// Markets::::remove(market_id); +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 2, +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], +// vec![], +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], +// _1, +// 0, +// 0 +// ), +// zrml_market_commons::Error::::MarketDoesNotExist, +// ); +// }); +// } +// +// #[test_case(MarketStatus::Proposed)] +// #[test_case(MarketStatus::Closed)] +// #[test_case(MarketStatus::Reported)] +// #[test_case(MarketStatus::Disputed)] +// #[test_case(MarketStatus::Resolved)] +// fn combo_sell_fails_on_inactive_market(market_status: MarketStatus) { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Scalar(0..=1), +// _10, +// vec![_1_2, _1_2], +// CENT, +// ); +// MarketCommons::mutate_market(&market_id, |market| { +// market.status = market_status; +// Ok(()) +// }) +// .unwrap(); +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 2, +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], +// vec![], +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], +// _1, +// 0, +// 0 +// ), +// Error::::MarketNotActive, +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_pool_not_found() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = +// create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid); +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 2, +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], +// vec![], +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], +// _1, +// 0, +// 0 +// ), +// Error::::PoolNotFound, +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_insufficient_funds() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Scalar(0..=1), +// _10, +// vec![_1_2, _1_2], +// CENT, +// ); +// let amount_in = _10; +// let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); +// assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in - 1)); +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 2, +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], +// vec![], +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], +// amount_in, +// 0, +// 0, +// ), +// orml_tokens::Error::::BalanceTooLow, +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_amount_out_below_min() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Scalar(0..=1), +// _100, +// vec![_1_2, _1_2], +// CENT, +// ); +// let amount_in = _20; +// let asset_in = Asset::ScalarOutcome(market_id, ScalarPosition::Long); +// assert_ok!(AssetManager::deposit(asset_in, &BOB, amount_in)); +// // Selling 20 at price of .5 will return less than 10 dollars due to slippage. +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 2, +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Long)], +// vec![], +// vec![Asset::ScalarOutcome(market_id, ScalarPosition::Short)], +// amount_in, +// 0, +// _10 +// ), +// Error::::AmountOutBelowMin, +// ); +// }); +// } +// +// #[test_case(vec![], vec![], vec![2]; "empty_buy")] +// #[test_case(vec![0], vec![], vec![]; "empty_sell")] +// #[test_case(vec![0, 1], vec![2, 1], vec![3, 4]; "buy_keep_overlap")] +// #[test_case(vec![0, 1], vec![2, 4], vec![3, 1]; "buy_sell_overlap")] +// #[test_case(vec![0, 1], vec![2, 4], vec![4, 3]; "keep_sell_overlap")] +// #[test_case(vec![0, 1, 999], vec![2, 4], vec![5, 3]; "out_of_bounds_buy")] +// #[test_case(vec![0, 1], vec![2, 4, 999], vec![5, 3]; "out_of_bounds_keep")] +// #[test_case(vec![0, 1], vec![2, 4], vec![5, 999, 3]; "out_of_bounds_sell")] +// #[test_case(vec![0, 6, 1, 6], vec![2, 4], vec![5, 3]; "duplicate_buy")] +// #[test_case(vec![0, 1], vec![2, 2, 4], vec![5, 3]; "duplicate_keep")] +// #[test_case(vec![0, 1], vec![2, 4], vec![5, 3, 6, 6, 6]; "duplicate_sell")] +// fn combo_sell_fails_on_invalid_partition( +// indices_buy: Vec, +// indices_keep: Vec, +// indices_sell: Vec, +// ) { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Categorical(7), +// _10, +// vec![_1_7, _1_7, _1_7, _1_7, _1_7, _1_7, _1_7 + 4], +// CENT, +// ); +// +// let buy = indices_buy.into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// let keep = indices_keep.into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// let sell = indices_sell.into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// +// // Buying 1 at price of .5 will return less than 2 outcomes due to slippage. +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 7, +// buy, +// keep, +// sell, +// _2, +// 0, // Keep this zero to avoid a different error due to invalid `amount_keep` param. +// 0 +// ), +// Error::::InvalidPartition, +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_spot_price_slipping_too_low() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Categorical(5), +// _10, +// vec![_1_5, _1_5, _1_5, _1_5, _1_5], +// CENT, +// ); +// let amount_buy = _100; +// +// for i in 0..4 { +// assert_ok!(AssetManager::deposit( +// Asset::CategoricalOutcome(market_id, i), +// &BOB, +// amount_buy +// )); +// } +// +// let buy = [0, 1, 2, 3].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// let sell = [4].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 5, +// buy, +// vec![], +// sell, +// amount_buy, +// 0, +// 0 +// ), +// Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow), +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_spot_price_slipping_too_high() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Categorical(5), +// _10, +// vec![_1_5, _1_5, _1_5, _1_5, _1_5], +// CENT, +// ); +// let amount_buy = _100; +// +// for i in 0..4 { +// assert_ok!(AssetManager::deposit( +// Asset::CategoricalOutcome(market_id, i), +// &BOB, +// amount_buy +// )); +// } +// +// let buy = [0].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// let sell = [1, 2, 3, 4].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 5, +// buy, +// vec![], +// sell, +// amount_buy, +// 0, +// 0 +// ), +// Error::::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow), +// ); +// }); +// } +// +// #[test] +// fn combo_sell_fails_on_large_amount() { +// ExtBuilder::default().build().execute_with(|| { +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Categorical(5), +// _10, +// vec![_1_5, _1_5, _1_5, _1_5, _1_5], +// CENT, +// ); +// let amount_buy = 100 * _100; +// +// for i in 0..4 { +// assert_ok!(AssetManager::deposit( +// Asset::CategoricalOutcome(market_id, i), +// &BOB, +// amount_buy +// )); +// } +// +// let buy = [0].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// let sell = [1, 2, 3, 4].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect(); +// +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// 5, +// buy, +// vec![], +// sell, +// amount_buy, +// 0, +// 0 +// ), +// Error::::MathError, +// ); +// }); +// } +// +// #[test_case(vec![], 1)] +// #[test_case(vec![2], _2)] +// fn combo_sell_fails_on_invalid_amount_keep(keep_indices: Vec, amount_in_keep: u128) { +// ExtBuilder::default().build().execute_with(|| { +// let spot_prices = vec![25 * CENT; 4]; +// let asset_count = spot_prices.len() as u16; +// let market_id = create_market_and_deploy_pool( +// ALICE, +// BASE_ASSET, +// MarketType::Categorical(asset_count), +// _10, +// spot_prices, +// CENT, +// ); +// +// let buy = vec![Asset::CategoricalOutcome(market_id, 0)]; +// let sell = vec![Asset::CategoricalOutcome(market_id, 1)]; +// let keep: Vec<_> = +// keep_indices.iter().map(|&i| Asset::CategoricalOutcome(market_id, i)).collect(); +// +// assert_noop!( +// NeoSwaps::combo_sell( +// RuntimeOrigin::signed(BOB), +// market_id, +// asset_count, +// buy.clone(), +// keep.clone(), +// sell.clone(), +// _1, +// amount_in_keep, +// 0, +// ), +// Error::::InvalidAmountKeep +// ); +// }); +// } diff --git a/zrml/neo-swaps/src/tests/deploy_combinatorial_pool.rs b/zrml/neo-swaps/src/tests/deploy_combinatorial_pool.rs new file mode 100644 index 000000000..2dfe7e725 --- /dev/null +++ b/zrml/neo-swaps/src/tests/deploy_combinatorial_pool.rs @@ -0,0 +1,445 @@ +// Copyright 2023-2024 Forecasting Technologies LTD. +// +// This file is part of Zeitgeist. +// +// Zeitgeist is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at +// your option) any later version. +// +// Zeitgeist is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Zeitgeist. If not, see . + +use super::*; +use crate::liquidity_tree::types::Node; +use alloc::collections::BTreeMap; +use test_case::test_case; +use zeitgeist_primitives::constants::BASE; + +#[test] +fn deploy_combinatorial_pool_works_with_single_market() { + ExtBuilder::default().build().execute_with(|| { + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _10; + let asset_count = 2usize; + let spot_prices = vec![BASE / (asset_count as u128); asset_count]; + let swap_fee = CENT; + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![MarketType::Categorical(2)], + amount, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let assets = pool.assets(); + let expected_liquidity = 144_269_504_089; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_eq!(pool.assets(), assets); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1); + assert_eq!(pool.collateral, BASE_ASSET); + assert_liquidity_tree_state!( + pool.liquidity_shares_manager, + [Node:: { + account: Some(ALICE), + stake: amount, + fees: 0u128, + descendant_stake: 0u128, + lazy_fees: 0u128, + }], + create_b_tree_map!({ ALICE => 0 }), + Vec::::new(), + ); + assert_eq!(pool.swap_fee, swap_fee); + assert_balance!(pool.account_id, pool.collateral, buffer); + + let mut reserves = BTreeMap::new(); + for (&asset, &price) in assets.iter().zip(spot_prices.iter()) { + assert_balance!(pool.account_id, asset, amount); + assert_eq!(pool.reserve_of(&asset).unwrap(), amount); + assert_eq!(pool.calculate_spot_price(asset).unwrap(), price); + assert_balance!(ALICE, asset, 0); + reserves.insert(asset, amount); + } + assert_balance!(ALICE, BASE_ASSET, alice_before - amount - buffer); + + System::assert_last_event( + Event::CombinatorialPoolDeployed { + who: ALICE, + market_ids, + pool_id, + account_id: pool.account_id, + reserves, + collateral: pool.collateral, + liquidity_parameter: pool.liquidity_parameter, + pool_shares_amount: amount, + swap_fee, + } + .into(), + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_works_with_multiple_markets() { + ExtBuilder::default().build().execute_with(|| { + let alice_before = AssetManager::free_balance(BASE_ASSET, &ALICE); + let amount = _10; + let asset_count = 16usize; + let spot_prices = vec![BASE / (asset_count as u128); asset_count]; + let swap_fee = CENT; + let (market_ids, pool_id) = create_markets_and_deploy_combinatorial_pool( + ALICE, + BASE_ASSET, + vec![ + MarketType::Categorical(2), + MarketType::Categorical(4), + MarketType::Scalar(0u128..=1u128), + ], + amount, + spot_prices.clone(), + swap_fee, + ); + let pool = Pools::::get(pool_id).unwrap(); + let assets = pool.assets(); + let expected_liquidity = 36_067_376_022; + let buffer = AssetManager::minimum_balance(pool.collateral); + assert_eq!(pool.assets(), assets); + assert_approx!(pool.liquidity_parameter, expected_liquidity, 1); + assert_eq!(pool.collateral, BASE_ASSET); + assert_liquidity_tree_state!( + pool.liquidity_shares_manager, + [Node:: { + account: Some(ALICE), + stake: amount, + fees: 0u128, + descendant_stake: 0u128, + lazy_fees: 0u128, + }], + create_b_tree_map!({ ALICE => 0 }), + Vec::::new(), + ); + assert_eq!(pool.swap_fee, swap_fee); + assert_balance!(pool.account_id, pool.collateral, buffer); + + let mut reserves = BTreeMap::new(); + for (&asset, &price) in assets.iter().zip(spot_prices.iter()) { + assert_balance!(pool.account_id, asset, amount); + assert_eq!(pool.reserve_of(&asset).unwrap(), amount); + assert_eq!(pool.calculate_spot_price(asset).unwrap(), price); + assert_balance!(ALICE, asset, 0); + reserves.insert(asset, amount); + } + assert_balance!(ALICE, BASE_ASSET, alice_before - amount - buffer); + + System::assert_last_event( + Event::CombinatorialPoolDeployed { + who: ALICE, + market_ids, + pool_id, + account_id: pool.account_id, + reserves, + collateral: pool.collateral, + liquidity_parameter: pool.liquidity_parameter, + pool_shares_amount: amount, + swap_fee, + } + .into(), + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_incorrect_vec_len() { + ExtBuilder::default().build().execute_with(|| { + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(3), ScoringRule::AmmCdaHybrid), + ]; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + market_ids, + _10, + vec![20 * CENT; 5], + CENT, + false, + ), + Error::::IncorrectVecLen + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_market_not_found() { + ExtBuilder::default().build().execute_with(|| { + let _ = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid); + let _ = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::AmmCdaHybrid); + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![0, 2, 1], + _10, + vec![10 * CENT; 10], + CENT, + false, + ), + zrml_market_commons::Error::::MarketDoesNotExist, + ); + }); +} + +#[test_case(MarketStatus::Proposed)] +#[test_case(MarketStatus::Closed)] +#[test_case(MarketStatus::Reported)] +#[test_case(MarketStatus::Disputed)] +#[test_case(MarketStatus::Resolved)] +fn deploy_combinatorial_pool_fails_on_inactive_market(market_status: MarketStatus) { + ExtBuilder::default().build().execute_with(|| { + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::AmmCdaHybrid), + ]; + MarketCommons::mutate_market(&market_ids.last().unwrap(), |market| { + market.status = market_status; + Ok(()) + }) + .unwrap(); + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + market_ids, + _100, + vec![10 * CENT; 10], + CENT, + false, + ), + Error::::MarketNotActive, + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_invalid_trading_mechanism() { + ExtBuilder::default().build().execute_with(|| { + let market_ids = vec![ + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid), + create_market(ALICE, BASE_ASSET, MarketType::Categorical(5), ScoringRule::Parimutuel), + ]; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + market_ids, + _100, + vec![10 * CENT; 10], + CENT, + false, + ), + Error::::InvalidTradingMechanism + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_max_splits_exceeded() { + ExtBuilder::default().build().execute_with(|| { + // log2(MaxSplits + 1) + let market_count = u16::BITS - ::MaxSplits::get().leading_zeros(); + + let mut market_ids = vec![]; + for _ in 0..market_count { + let market_id = create_market( + ALICE, + BASE_ASSET, + MarketType::Categorical(2), + ScoringRule::AmmCdaHybrid, + ); + + market_ids.push(market_id); + } + let liquidity = 1_000 * BASE; + + let asset_count = 2u128.pow(market_count as u32); + let mut spot_prices = vec![_1 / asset_count; asset_count as usize - 1]; + spot_prices.push(_1 - spot_prices.iter().sum::()); + + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + market_ids, + liquidity, + spot_prices, + CENT, + false, + ), + Error::::MaxSplitsExceeded + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_swap_fee_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![market_id], + liquidity, + vec![_1_4, _3_4], + MIN_SWAP_FEE - 1, + false, + ), + Error::::SwapFeeBelowMin + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_swap_fee_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![market_id], + liquidity, + vec![_1_4, _3_4], + ::MaxSwapFee::get() + 1, + false, + ), + Error::::SwapFeeAboveMax + ); + }); +} + +#[test_case(vec![_1_4, _3_4 - 1])] +#[test_case(vec![_1_4 + 1, _3_4])] +fn deploy_combinatorial_pool_fails_on_invalid_spot_prices(spot_prices: Vec>) { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![market_id], + liquidity, + spot_prices, + CENT, + false, + ), + Error::::InvalidSpotPrices + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_spot_price_below_min() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + let spot_price = MIN_SPOT_PRICE - 1; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![market_id], + liquidity, + vec![spot_price, _1 - spot_price], + CENT, + false, + ), + Error::::SpotPriceBelowMin + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_spot_price_above_max() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + assert_ok!(PredictionMarkets::buy_complete_set( + RuntimeOrigin::signed(ALICE), + market_id, + liquidity, + )); + let spot_price = MAX_SPOT_PRICE + 1; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![market_id], + liquidity, + vec![spot_price, _1 - spot_price], + CENT, + false, + ), + Error::::SpotPriceAboveMax + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_insufficient_funds() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Categorical(2), ScoringRule::AmmCdaHybrid); + let liquidity = _10; + + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(BOB), + vec![market_id], + liquidity, + vec![_3_4, _1_4], + CENT, + false, + ), + orml_currencies::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn deploy_combinatorial_pool_fails_on_liquidity_too_low() { + ExtBuilder::default().build().execute_with(|| { + let market_id = + create_market(ALICE, BASE_ASSET, MarketType::Scalar(0..=1), ScoringRule::AmmCdaHybrid); + let amount = _1_2; + assert_noop!( + NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + vec![market_id], + amount, + vec![_1_2, _1_2], + CENT, + false, + ), + Error::::LiquidityTooLow + ); + }); +} diff --git a/zrml/neo-swaps/src/tests/mod.rs b/zrml/neo-swaps/src/tests/mod.rs index 42fdbe5f2..e8f6b1da8 100644 --- a/zrml/neo-swaps/src/tests/mod.rs +++ b/zrml/neo-swaps/src/tests/mod.rs @@ -21,6 +21,7 @@ mod buy; mod buy_and_sell; mod combo_buy; mod combo_sell; +mod deploy_combinatorial_pool; mod deploy_pool; mod exit; mod join; @@ -100,6 +101,36 @@ fn create_market_and_deploy_pool( market_id } +fn create_markets_and_deploy_combinatorial_pool( + creator: AccountIdOf, + base_asset: Asset, + market_types: Vec, + amount: BalanceOf, + spot_prices: Vec>, + swap_fee: BalanceOf, +) -> (Vec, ::PoolId) { + let mut market_ids = vec![]; + + for market_type in market_types.iter() { + let market_id = + create_market(creator, base_asset, market_type.clone(), ScoringRule::AmmCdaHybrid); + + market_ids.push(market_id); + } + + let pool_id = as PoolStorage>::next_pool_id(); + assert_ok!(NeoSwaps::deploy_combinatorial_pool( + RuntimeOrigin::signed(ALICE), + market_ids.clone(), + amount, + spot_prices.clone(), + swap_fee, + false, + )); + + (market_ids, pool_id) +} + fn deposit_complete_set( market_id: MarketId, account: AccountIdOf, diff --git a/zrml/neo-swaps/src/types/pool.rs b/zrml/neo-swaps/src/types/pool.rs index 374f3748b..b319fdf73 100644 --- a/zrml/neo-swaps/src/types/pool.rs +++ b/zrml/neo-swaps/src/types/pool.rs @@ -86,6 +86,7 @@ where fn is_active(&self) -> Result { for market_id in self.pool_type.iter_market_ids() { let market = T::MarketCommons::market(&market_id)?; + println!("market: {:?}", market.status); if market.status != MarketStatus::Active { return Ok(false);