From e586f668a2ceabc5677e7a32df25b2e156711d28 Mon Sep 17 00:00:00 2001 From: Malte Kliemann Date: Mon, 14 Oct 2024 20:50:55 +0200 Subject: [PATCH] Implement multi-market combinatorial betting tests (#1377) * . * Add more tests * . * Detailed testing * . * Add tests for `InvalidAmountKeep` * Clippy fixes --- primitives/src/constants.rs | 1 + primitives/src/constants/base_multiples.rs | 3 + zrml/neo-swaps/src/lib.rs | 13 +- zrml/neo-swaps/src/macros.rs | 2 +- zrml/neo-swaps/src/tests/combo_buy.rs | 126 +++++++++++++++- zrml/neo-swaps/src/tests/combo_sell.rs | 161 ++++++++++++++++++++- 6 files changed, 301 insertions(+), 5 deletions(-) diff --git a/primitives/src/constants.rs b/primitives/src/constants.rs index 3d831286e..7b13887d8 100644 --- a/primitives/src/constants.rs +++ b/primitives/src/constants.rs @@ -41,6 +41,7 @@ pub const BLOCKS_PER_HOUR: BlockNumber = BLOCKS_PER_MINUTE * 60; // 300 // Definitions for currency pub const DECIMALS: u8 = 10; pub const BASE: u128 = 10u128.pow(DECIMALS as u32); +pub const DIME: Balance = BASE / 10; // 1_000_000_000 pub const CENT: Balance = BASE / 100; // 100_000_000 pub const MILLI: Balance = CENT / 10; // 10_000_000 pub const MICRO: Balance = MILLI / 1000; // 10_000 diff --git a/primitives/src/constants/base_multiples.rs b/primitives/src/constants/base_multiples.rs index bdbba4657..42ca28f4a 100644 --- a/primitives/src/constants/base_multiples.rs +++ b/primitives/src/constants/base_multiples.rs @@ -42,6 +42,9 @@ 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 _300: u128 = 300 * _1; +pub const _321: u128 = 321 * _1; +pub const _400: u128 = 400 * _1; pub const _444: u128 = 444 * _1; pub const _500: u128 = 500 * _1; pub const _777: u128 = 777 * _1; diff --git a/zrml/neo-swaps/src/lib.rs b/zrml/neo-swaps/src/lib.rs index 1e04c849f..4b548fa8d 100644 --- a/zrml/neo-swaps/src/lib.rs +++ b/zrml/neo-swaps/src/lib.rs @@ -309,6 +309,10 @@ mod pallet { /// The buy/sell/keep partition specified is empty, or contains overlaps or assets that don't /// belong to the market. InvalidPartition, + + /// The `amount_keep` parameter must be zero if `keep` is empty and less than `amount_buy` + /// if `keep` is not empty. + InvalidAmountKeep, } #[derive(Decode, Encode, Eq, PartialEq, PalletError, RuntimeDebug, TypeInfo)] @@ -1134,6 +1138,13 @@ mod pallet { ensure!(amount_buy != Zero::zero(), Error::::ZeroAmount); let market = T::MarketCommons::market(&market_id)?; ensure!(market.status == MarketStatus::Active, Error::::MarketNotActive); + + if keep.is_empty() { + ensure!(amount_keep.is_zero(), Error::::InvalidAmountKeep); + } else { + ensure!(amount_keep < amount_buy, Error::::InvalidAmountKeep); + } + Self::try_mutate_pool(&market_id, |pool| { // Ensure that `buy` and `sell` partition are disjoint and only contain assets from // the market. @@ -1185,7 +1196,7 @@ mod pallet { } for &asset in keep.iter() { - T::MultiCurrency::transfer(asset, &pool.account_id, &who, amount_keep)?; + T::MultiCurrency::transfer(asset, &who, &pool.account_id, amount_keep)?; pool.increase_reserve(&asset, &amount_keep)?; } diff --git a/zrml/neo-swaps/src/macros.rs b/zrml/neo-swaps/src/macros.rs index 0c7bbb986..a688fb368 100644 --- a/zrml/neo-swaps/src/macros.rs +++ b/zrml/neo-swaps/src/macros.rs @@ -100,7 +100,7 @@ macro_rules! assert_pool_state { .fold(0u128, |acc, node| acc + node.fees + node.lazy_fees); assert_eq!(actual_total_fees, $total_fees); let invariant = actual_spot_prices.iter().sum::(); - assert_approx!(invariant, _1, 1); + assert_approx!(invariant, _1, 2); }; } diff --git a/zrml/neo-swaps/src/tests/combo_buy.rs b/zrml/neo-swaps/src/tests/combo_buy.rs index 7fb5ae5c8..39b3e772a 100644 --- a/zrml/neo-swaps/src/tests/combo_buy.rs +++ b/zrml/neo-swaps/src/tests/combo_buy.rs @@ -50,7 +50,6 @@ fn combo_buy_works() { let buy = vec![pool.assets()[0]]; let sell = pool.assets_complement(&buy); assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in)); - println!("{}", AssetManager::free_balance(BASE_ASSET, &BOB)); // Deposit some stuff in the pool account to check that the pools `reserves` fields tracks // the reserve correctly. assert_ok!(AssetManager::deposit(sell[0], &pool.account_id, _100)); @@ -103,6 +102,131 @@ fn combo_buy_works() { }); } +#[test_case( + 333 * _1, + vec![10 * CENT, 30 * CENT, 25 * CENT, 13 * CENT, 22 * CENT], + vec![0, 2], + vec![3], + vec![1, 4], + 102_040_816_327, + 236_865_613_849, + 100_000_000_001, + vec![3193134386152, 1841186221785, 1867994157274, 2950568636818, 2289732472863], + vec![1_099_260_911, 2_799_569_315, 2_748_152_277, 1_300_000_000, 2_053_017_497], + 1_020_408_163 +)] +#[test_case( + _100, + vec![80 * CENT, 5 * CENT, 5 * CENT, 5 * CENT, 5 * CENT], + vec![4], + vec![1, 2, 3], + vec![0], + 336_734_693_877, + 1_131_842_030_026, + 329_999_999_999, + vec![404_487_147_360, _100, _100, _100, 198_157_969_973], + vec![2_976_802_957, 5 * CENT, 5 * CENT, 5 * CENT, 5_523_197_043], + 3_367_346_939 +)] +#[test_case( + 1000 * _1, + vec![1_250_000_000; 8], + vec![0, 2, 5, 6, 7], + vec![], + vec![1, 3, 4], + 5_102_040_816_326, + 6_576_234_413_776, + 5_000_000_000_000, + vec![ + 8_423_765_586_224, + 1500 * _1, + 8_423_765_586_224, + 1500 * _1, + 1500 * _1, + 8_423_765_586_224, + 8_423_765_586_224, + 8_423_765_586_224, + ], + vec![ + 1_734_834_957, + 441_941_738, + 1_734_834_957, + 441_941_738, + 441_941_738, + 1_734_834_957, + 1_734_834_957, + 1_734_834_957, + ], + 51_020_408_163 +)] +fn combo_buy_works_multi_market( + liquidity: u128, + spot_prices: Vec, + buy_indices: Vec, + keep_indices: Vec, + sell_indices: Vec, + amount_in: u128, + expected_amount_out_buy: u128, + expected_amount_out_keep: 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 sentinel = 123_456_789; + assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in + sentinel)); + + let pool = Pools::::get(market_id).unwrap(); + let expected_liquidity = pool.liquidity_parameter; + + 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(); + assert_ok!(NeoSwaps::combo_buy( + RuntimeOrigin::signed(BOB), + market_id, + asset_count, + buy.clone(), + sell.clone(), + amount_in, + 0, + )); + + assert_balance!(BOB, BASE_ASSET, sentinel); + for &asset in buy.iter() { + assert_balance!(BOB, asset, expected_amount_out_buy); + } + for &asset in keep.iter() { + assert_balance!(BOB, asset, expected_amount_out_keep); + } + for &asset in sell.iter() { + 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_buy_fails_on_incorrect_asset_count() { ExtBuilder::default().build().execute_with(|| { diff --git a/zrml/neo-swaps/src/tests/combo_sell.rs b/zrml/neo-swaps/src/tests/combo_sell.rs index f8f117201..52d0ea810 100644 --- a/zrml/neo-swaps/src/tests/combo_sell.rs +++ b/zrml/neo-swaps/src/tests/combo_sell.rs @@ -100,6 +100,127 @@ fn combo_sell_works() { }); } +#[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(|| { @@ -296,7 +417,6 @@ fn combo_sell_fails_on_invalid_partition( indices_sell: Vec, ) { ExtBuilder::default().build().execute_with(|| { - println!("{:?}", _1_7); let market_id = create_market_and_deploy_pool( ALICE, BASE_ASSET, @@ -320,7 +440,7 @@ fn combo_sell_fails_on_invalid_partition( keep, sell, _2, - _1, + 0, // Keep this zero to avoid a different error due to invalid `amount_keep` param. 0 ), Error::::InvalidPartition, @@ -450,3 +570,40 @@ fn combo_sell_fails_on_large_amount() { ); }); } + +#[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 + ); + }); +}