Skip to content

Commit

Permalink
Implement and test numerical limits for combinatorial betting (#1376)
Browse files Browse the repository at this point in the history
* Add numerical limits and tests

* Add missing license
  • Loading branch information
maltekliemann authored Oct 14, 2024
1 parent c4e50d4 commit 0b0e607
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 7 deletions.
15 changes: 15 additions & 0 deletions zrml/combinatorial-tokens/src/mock/consts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
// Copyright 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 <https://www.gnu.org/licenses/>.

#[cfg(feature = "parachain")]
use zeitgeist_primitives::types::{Asset, MarketId};
Expand Down
57 changes: 51 additions & 6 deletions zrml/neo-swaps/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ mod pallet {
pub(crate) const MAX_SPOT_PRICE: u128 = BASE - CENT / 2;
/// The minimum allowed spot price when creating a pool.
pub(crate) const MIN_SPOT_PRICE: u128 = CENT / 2;
/// The maximum value the spot price is allowed to take in a combinatorial market.
pub(crate) const COMBO_MAX_SPOT_PRICE: u128 = BASE - CENT / 10;
/// The minimum value the spot price is allowed to take in a combinatorial market.
pub(crate) const COMBO_MIN_SPOT_PRICE: u128 = CENT / 10;
/// The minimum vallowed value of a pool's liquidity parameter.
pub(crate) const MIN_LIQUIDITY: u128 = BASE;
/// The minimum percentage each new LP position must increase the liquidity by, represented as
Expand Down Expand Up @@ -311,12 +315,14 @@ mod pallet {
pub enum NumericalLimitsError {
/// Selling is not allowed at prices this low.
SpotPriceTooLow,
/// Sells which move the price below this threshold are not allowed.
/// Interactions which move the price below a particular threshold are not allowed.
SpotPriceSlippedTooLow,
/// The maximum buy or sell amount was exceeded.
MaxAmountExceeded,
/// The minimum buy or sell amount was exceeded.
MinAmountNotMet,
/// Interactions which move the price above a particular threshold are not allowed.
SpotPriceSlippedTooHigh,
}

#[pallet::call]
Expand Down Expand Up @@ -1041,10 +1047,10 @@ mod pallet {
ensure!(!sell.is_empty(), Error::<T>::InvalidPartition);
for asset in buy.iter() {
ensure!(!sell.contains(asset), Error::<T>::InvalidPartition);
ensure!(market.outcome_assets().contains(asset), Error::<T>::InvalidPartition);
ensure!(pool.assets().contains(asset), Error::<T>::InvalidPartition);
}
for asset in sell.iter() {
ensure!(market.outcome_assets().contains(asset), Error::<T>::InvalidPartition);
ensure!(pool.assets().contains(asset), Error::<T>::InvalidPartition);
}
let buy_set = buy.iter().collect::<BTreeSet<_>>();
let sell_set = sell.iter().collect::<BTreeSet<_>>();
Expand Down Expand Up @@ -1084,6 +1090,19 @@ mod pallet {
pool.increase_reserve(&asset, &amount_in_minus_fees)?;
}

// Ensure that numerical limits of all prices are respected.
for &asset in pool.assets().iter() {
let spot_price = pool.calculate_spot_price(asset)?;
ensure!(
spot_price >= COMBO_MIN_SPOT_PRICE.saturated_into(),
Error::<T>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow)
);
ensure!(
spot_price <= COMBO_MAX_SPOT_PRICE.saturated_into(),
Error::<T>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooHigh)
);
}

Self::deposit_event(Event::<T>::ComboBuyExecuted {
who: who.clone(),
market_id,
Expand Down Expand Up @@ -1123,14 +1142,14 @@ mod pallet {
for asset in buy.iter() {
ensure!(!keep.contains(asset), Error::<T>::InvalidPartition);
ensure!(!sell.contains(asset), Error::<T>::InvalidPartition);
ensure!(market.outcome_assets().contains(asset), Error::<T>::InvalidPartition);
ensure!(pool.assets().contains(asset), Error::<T>::InvalidPartition);
}
for asset in sell.iter() {
ensure!(!keep.contains(asset), Error::<T>::InvalidPartition);
ensure!(market.outcome_assets().contains(asset), Error::<T>::InvalidPartition);
ensure!(pool.assets().contains(asset), Error::<T>::InvalidPartition);
}
for asset in keep.iter() {
ensure!(market.outcome_assets().contains(asset), Error::<T>::InvalidPartition);
ensure!(pool.assets().contains(asset), Error::<T>::InvalidPartition);
}
let buy_set = buy.iter().collect::<BTreeSet<_>>();
let keep_set = keep.iter().collect::<BTreeSet<_>>();
Expand Down Expand Up @@ -1193,6 +1212,32 @@ mod pallet {
amount_out_minus_fees,
)?;

// Ensure that numerical limits of all prices are respected.
for &asset in pool.assets().iter() {
let spot_price = pool.calculate_spot_price(asset)?;
ensure!(
spot_price >= COMBO_MIN_SPOT_PRICE.saturated_into(),
Error::<T>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow)
);
ensure!(
spot_price <= COMBO_MAX_SPOT_PRICE.saturated_into(),
Error::<T>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooHigh)
);
}

// Ensure that numerical limits of all prices are respected.
for &asset in pool.assets().iter() {
let spot_price = pool.calculate_spot_price(asset)?;
ensure!(
spot_price >= COMBO_MIN_SPOT_PRICE.saturated_into(),
Error::<T>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow)
);
ensure!(
spot_price <= COMBO_MAX_SPOT_PRICE.saturated_into(),
Error::<T>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooHigh)
);
}

Self::deposit_event(Event::<T>::ComboSellExecuted {
who: who.clone(),
market_id,
Expand Down
75 changes: 75 additions & 0 deletions zrml/neo-swaps/src/tests/combo_buy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,78 @@ fn combo_buy_fails_on_invalid_partition(indices_buy: Vec<u16>, indices_sell: Vec
);
});
}

#[test]
fn combo_buy_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_in = _100;

assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in));

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_buy(RuntimeOrigin::signed(BOB), market_id, 5, buy, sell, amount_in, 0),
Error::<Runtime>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooLow),
);
});
}

#[test]
fn combo_buy_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_in = _100;

assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in));

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_buy(RuntimeOrigin::signed(BOB), market_id, 5, buy, sell, amount_in, 0),
Error::<Runtime>::NumericalLimits(NumericalLimitsError::SpotPriceSlippedTooHigh),
);
});
}

#[test]
fn combo_buy_fails_on_large_buy() {
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_in = 100 * _100;

assert_ok!(AssetManager::deposit(BASE_ASSET, &BOB, amount_in));

let buy = [0].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect();
let sell = [1, 2].into_iter().map(|i| CategoricalOutcome(market_id, i)).collect();

assert_noop!(
NeoSwaps::combo_buy(RuntimeOrigin::signed(BOB), market_id, 5, buy, sell, amount_in, 0),
Error::<Runtime>::MathError,
);
});
}
125 changes: 124 additions & 1 deletion zrml/neo-swaps/src/tests/combo_sell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ fn combo_sell_fails_on_amount_out_below_min() {
#[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_buy_fails_on_invalid_partition(
fn combo_sell_fails_on_invalid_partition(
indices_buy: Vec<u16>,
indices_keep: Vec<u16>,
indices_sell: Vec<u16>,
Expand Down Expand Up @@ -327,3 +327,126 @@ fn combo_buy_fails_on_invalid_partition(
);
});
}

#[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::<Runtime>::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::<Runtime>::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::<Runtime>::MathError,
);
});
}

0 comments on commit 0b0e607

Please sign in to comment.