Skip to content

Commit

Permalink
Implement swap fees (#676)
Browse files Browse the repository at this point in the history
* Add tests for trading with fees

* Ensure swap fee values are valid

* Add `swap_fee` parameter to CPMM market creation

* Fix errors

* Add tests for single-asset liquidity functions with fees

* Extend pool parameter test

* Add new error for high swap fees

* Apply suggestions from code review

Co-authored-by: Chralt <chralt98@gmail.com>

* Reorganize tests

* Fix `get_spot_price` and extend tests

* Add notes to changelog

* Add sanity tests

* Update zrml/swaps/src/lib.rs

Co-authored-by: Harald Heckmann <mail@haraldheckmann.de>

* Constrain swap fees to legal values in fuzz tests

Co-authored-by: Chralt <chralt98@gmail.com>
Co-authored-by: Harald Heckmann <mail@haraldheckmann.de>
  • Loading branch information
3 people committed Jul 12, 2022
1 parent a98281e commit 852ebea
Show file tree
Hide file tree
Showing 11 changed files with 653 additions and 246 deletions.
18 changes: 14 additions & 4 deletions docs/changelog_for_devs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# v0.3.4

- Implemented swap fees for CPMM pools. This means that the following extrinsics
now have a (non-optional) `swap_fee` parameter:

- `create_cpmm_market_and_deploy_assets`
- `deploy_swap_pool_and_additional_liquidity`
- `deploy_swap_pool_for_market`

Furthermore, there's a maximum swap fee, specified by the `swaps` pallet's
on-chain constant `MaxSwapFee`.

- Changed the `weights` parameter of `deploy_swap_pool_and_additional_liquidity`
and `deploy_swap_pool_for_market` to be a vector whose length is equal to the
number of outcome tokens (one item shorter than before). The `weights` now
Expand All @@ -12,16 +22,16 @@
# v0.3.3

- Introduced `MarketStatus::Closed`. Markets are automatically transitioned into
this state when the market ends, and the `Event::MarketClosed` is
emitted. Trading is not allowed on markets that are closed.
this state when the market ends, and the `Event::MarketClosed` is emitted.
Trading is not allowed on markets that are closed.

- Introduced `PoolStatus::Closed`; the pool of a market is closed when the
market is closed. The `Event::PoolClosed` is emitted when this happens.

- Replace `PoolStatus::Stale` with `PoolStatus::Clean`. This state signals that
the corresponding market was resolved and the losing assets deleted from the
pool. The `Event::PoolCleanedUp` is emitted when the pool transitions into this
state.
pool. The `Event::PoolCleanedUp` is emitted when the pool transitions into
this state.

- Simplify `create_cpmm_market_and_deploy_assets`,
`deploy_swap_pool_and_additional_liquidity` and `deploy_swap_pool_for_market`
Expand Down
1 change: 1 addition & 0 deletions primitives/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ parameter_types! {
pub const MaxAssets: u16 = MaxCategories::get() + 1;
pub const MaxInRatio: Balance = (BASE / 3) + 1;
pub const MaxOutRatio: Balance = (BASE / 3) + 1;
pub const MaxSwapFee: Balance = BASE / 10; // 10%
pub const MaxTotalWeight: Balance = 50 * BASE;
pub const MaxWeight: Balance = 50 * BASE;
pub const MinLiquidity: Balance = 100 * BASE;
Expand Down
1 change: 1 addition & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ impl zrml_swaps::Config for Runtime {
type MaxAssets = MaxAssets;
type MaxInRatio = MaxInRatio;
type MaxOutRatio = MaxOutRatio;
type MaxSwapFee = MaxSwapFee;
type MaxTotalWeight = MaxTotalWeight;
type MaxWeight = MaxWeight;
type MinLiquidity = MinLiquidity;
Expand Down
5 changes: 3 additions & 2 deletions zrml/prediction-markets/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use frame_system::RawOrigin;
use orml_traits::MultiCurrency;
use sp_runtime::traits::{One, SaturatedConversion, Zero};
use zeitgeist_primitives::{
constants::{MinLiquidity, MinWeight, BASE},
constants::{MaxSwapFee, MinLiquidity, MinWeight, BASE},
traits::DisputeApi,
types::{
Asset, MarketCreation, MarketDisputeMechanism, MarketPeriod, MarketStatus, MarketType,
Expand Down Expand Up @@ -310,13 +310,14 @@ benchmarks! {
MarketType::Categorical(a.saturated_into()),
ScoringRule::CPMM
)?;
let max_swap_fee: BalanceOf::<T> = MaxSwapFee::get().saturated_into();
let min_liquidity: BalanceOf::<T> = MinLiquidity::get().saturated_into();
let _ = Call::<T>::buy_complete_set { market_id, amount: min_liquidity }
.dispatch_bypass_filter(RawOrigin::Signed(caller.clone()).into())?;

let weight_len: usize = MaxRuntimeUsize::from(a).into();
let weights = vec![MinWeight::get(); weight_len];
}: _(RawOrigin::Signed(caller), market_id, min_liquidity, weights)
}: _(RawOrigin::Signed(caller), market_id, max_swap_fee, min_liquidity, weights)

dispute {
let a in 0..(T::MaxDisputes::get() - 1) as u32;
Expand Down
11 changes: 9 additions & 2 deletions zrml/prediction-markets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ mod pallet {
/// * `metadata`: A hash pointer to the metadata of the market.
/// * `market_type`: The type of the market.
/// * `dispute_mechanism`: The market dispute mechanism.
/// * `swap_fee`: The swap fee, specified as fixed-point ratio (0.1 equals 10% fee)
/// * `amount`: The amount of each token to add to the pool.
/// * `weights`: The relative denormalized weight of each asset price.
#[pallet::weight(
Expand All @@ -420,6 +421,7 @@ mod pallet {
metadata: MultiHash,
market_type: MarketType,
dispute_mechanism: MarketDisputeMechanism<T::AccountId>,
#[pallet::compact] swap_fee: BalanceOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
weights: Vec<u128>,
) -> DispatchResultWithPostInfo {
Expand Down Expand Up @@ -447,6 +449,7 @@ mod pallet {
let deploy_and_populate_weight = Self::deploy_swap_pool_and_additional_liquidity(
origin,
market_id,
swap_fee,
amount,
weights.clone(),
)?
Expand Down Expand Up @@ -562,6 +565,7 @@ mod pallet {
/// # Arguments
///
/// * `market_id`: The id of the market.
/// * `swap_fee`: The swap fee, specified as fixed-point ratio (0.1 equals 10% fee)
/// * `amount`: The amount of each token to add to the pool.
/// * `weights`: The relative denormalized weight of each outcome asset. The sum of the
/// weights must be less or equal to _half_ of the `MaxTotalWeight` constant of the
Expand All @@ -581,6 +585,7 @@ mod pallet {
pub fn deploy_swap_pool_and_additional_liquidity(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
#[pallet::compact] swap_fee: BalanceOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
weights: Vec<u128>,
) -> DispatchResultWithPostInfo {
Expand All @@ -589,7 +594,7 @@ mod pallet {
.actual_weight
.unwrap_or_else(|| T::WeightInfo::buy_complete_set(T::MaxCategories::get().into()));
let weights_len = weights.len();
Self::deploy_swap_pool_for_market(origin, market_id, amount, weights)?;
Self::deploy_swap_pool_for_market(origin, market_id, swap_fee, amount, weights)?;
Ok(Some(weight_bcs.saturating_add(T::WeightInfo::deploy_swap_pool_for_market(
weights_len.saturated_into(),
)))
Expand All @@ -603,6 +608,7 @@ mod pallet {
/// # Arguments
///
/// * `market_id`: The id of the market.
/// * `swap_fee`: The swap fee, specified as fixed-point ratio (0.1 equals 10% fee)
/// * `amount`: The amount of each token to add to the pool.
/// * `weights`: The relative denormalized weight of each outcome asset. The sum of the
/// weights must be less or equal to _half_ of the `MaxTotalWeight` constant of the
Expand All @@ -614,6 +620,7 @@ mod pallet {
pub fn deploy_swap_pool_for_market(
origin: OriginFor<T>,
#[pallet::compact] market_id: MarketIdOf<T>,
#[pallet::compact] swap_fee: BalanceOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
mut weights: Vec<u128>,
) -> DispatchResult {
Expand All @@ -635,7 +642,7 @@ mod pallet {
base_asset,
market_id,
ScoringRule::CPMM,
Some(Zero::zero()),
Some(swap_fee),
Some(amount),
Some(weights),
)?;
Expand Down
9 changes: 5 additions & 4 deletions zrml/prediction-markets/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ use zeitgeist_primitives::{
AuthorizedPalletId, BalanceFractionalDecimals, BlockHashCount, CourtCaseDuration,
CourtPalletId, DisputeFactor, ExistentialDeposit, ExistentialDeposits, ExitFee,
GetNativeCurrencyId, LiquidityMiningPalletId, MaxAssets, MaxCategories, MaxDisputes,
MaxInRatio, MaxMarketPeriod, MaxOutRatio, MaxReserves, MaxSubsidyPeriod, MaxTotalWeight,
MaxWeight, MinAssets, MinCategories, MinLiquidity, MinSubsidy, MinSubsidyPeriod, MinWeight,
MinimumPeriod, PmPalletId, ReportingPeriod, SimpleDisputesPalletId, StakeWeight,
SwapsPalletId, BASE, CENT,
MaxInRatio, MaxMarketPeriod, MaxOutRatio, MaxReserves, MaxSubsidyPeriod, MaxSwapFee,
MaxTotalWeight, MaxWeight, MinAssets, MinCategories, MinLiquidity, MinSubsidy,
MinSubsidyPeriod, MinWeight, MinimumPeriod, PmPalletId, ReportingPeriod,
SimpleDisputesPalletId, StakeWeight, SwapsPalletId, BASE, CENT,
},
types::{
AccountIdTest, Amount, Asset, Balance, BasicCurrencyAdapter, BlockNumber, BlockTest,
Expand Down Expand Up @@ -242,6 +242,7 @@ impl zrml_swaps::Config for Runtime {
type MaxAssets = MaxAssets;
type MaxInRatio = MaxInRatio;
type MaxOutRatio = MaxOutRatio;
type MaxSwapFee = MaxSwapFee;
type MaxTotalWeight = MaxTotalWeight;
type MaxWeight = MaxWeight;
type MinAssets = MinAssets;
Expand Down
71 changes: 59 additions & 12 deletions zrml/prediction-markets/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ fn admin_destroy_market_correctly_cleans_up_accounts() {
gen_metadata(50),
MarketType::Categorical(3),
MarketDisputeMechanism::SimpleDisputes,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); 3],
));
Expand Down Expand Up @@ -783,6 +784,7 @@ fn on_market_close_successfully_auto_closes_market_with_blocks() {
gen_metadata(50),
MarketType::Categorical(category_count),
MarketDisputeMechanism::SimpleDisputes,
0,
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); category_count.into()],
));
Expand Down Expand Up @@ -817,6 +819,7 @@ fn on_market_close_successfully_auto_closes_market_with_timestamps() {
gen_metadata(50),
MarketType::Categorical(category_count),
MarketDisputeMechanism::SimpleDisputes,
0,
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); category_count.into()],
));
Expand Down Expand Up @@ -859,6 +862,7 @@ fn on_market_close_successfully_auto_closes_multiple_markets_after_stall() {
gen_metadata(50),
MarketType::Categorical(category_count),
MarketDisputeMechanism::SimpleDisputes,
0,
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); category_count.into()],
));
Expand All @@ -869,6 +873,7 @@ fn on_market_close_successfully_auto_closes_multiple_markets_after_stall() {
gen_metadata(50),
MarketType::Categorical(category_count),
MarketDisputeMechanism::SimpleDisputes,
0,
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); category_count.into()],
));
Expand Down Expand Up @@ -904,6 +909,7 @@ fn market_close_manager_skips_the_genesis_block_with_timestamp_zero() {
gen_metadata(50),
MarketType::Categorical(category_count),
MarketDisputeMechanism::SimpleDisputes,
123,
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); category_count.into()],
));
Expand Down Expand Up @@ -1134,6 +1140,7 @@ fn it_allows_to_deploy_a_pool() {
assert_ok!(PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(BOB),
0,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); 2],
));
Expand All @@ -1152,13 +1159,15 @@ fn deploy_swap_pool_for_market_fails_if_market_has_a_pool() {
assert_ok!(PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(BOB),
0,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); 2],
));
assert_noop!(
PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(BOB),
0,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); 2],
),
Expand All @@ -1181,6 +1190,7 @@ fn it_does_not_allow_to_deploy_a_pool_on_pending_advised_market() {
PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(BOB),
0,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); 2],
),
Expand Down Expand Up @@ -1725,15 +1735,19 @@ fn it_allows_to_redeem_shares() {
}

#[test]
fn create_market_and_deploy_assets_results_in_expected_balances() {
fn create_market_and_deploy_assets_results_in_expected_balances_and_pool_params() {
let oracle = ALICE;
let period = MarketPeriod::Block(0..42);
let metadata = gen_metadata(42);
let category_count = 4;
let assets = MarketType::Categorical(category_count);
let market_type = MarketType::Categorical(category_count);
let swap_fee = <Runtime as zrml_swaps::Config>::MaxSwapFee::get();
let amount = 123 * BASE;
let pool_id = 0;
let weights = vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); category_count.into()];
let weight = <Runtime as zrml_swaps::Config>::MinWeight::get();
let weights = vec![weight; category_count.into()];
let base_asset_weight = (category_count as u128) * weight;
let total_weight = 2 * base_asset_weight;

// Execute the combined convenience function
ExtBuilder::default().build().execute_with(|| {
Expand All @@ -1742,11 +1756,13 @@ fn create_market_and_deploy_assets_results_in_expected_balances() {
oracle,
period,
metadata,
assets,
market_type,
MarketDisputeMechanism::SimpleDisputes,
swap_fee,
amount,
weights,
));
let market_id = 0;

let pool_account = Swaps::pool_account_id(pool_id);
assert_eq!(Tokens::free_balance(Asset::CategoricalOutcome(0, 0), &ALICE), 0);
Expand All @@ -1759,6 +1775,29 @@ fn create_market_and_deploy_assets_results_in_expected_balances() {
assert_eq!(Tokens::free_balance(Asset::CategoricalOutcome(0, 2), &pool_account), amount);
assert_eq!(Tokens::free_balance(Asset::CategoricalOutcome(0, 3), &pool_account), amount);
assert_eq!(System::account(&pool_account).data.free, amount);

let pool = Pools::<Runtime>::get(0).unwrap();
let assets_expected = vec![
Asset::CategoricalOutcome(market_id, 0),
Asset::CategoricalOutcome(market_id, 1),
Asset::CategoricalOutcome(market_id, 2),
Asset::CategoricalOutcome(market_id, 3),
Asset::Ztg,
];
assert_eq!(pool.assets, assets_expected);
assert_eq!(pool.base_asset, Asset::Ztg);
assert_eq!(pool.market_id, market_id);
assert_eq!(pool.scoring_rule, ScoringRule::CPMM);
assert_eq!(pool.swap_fee, Some(swap_fee));
assert_eq!(pool.total_subsidy, None);
assert_eq!(pool.total_subsidy, None);
assert_eq!(pool.total_weight, Some(total_weight));
let pool_weights = pool.weights.unwrap();
assert_eq!(pool_weights[&Asset::CategoricalOutcome(market_id, 0)], weight);
assert_eq!(pool_weights[&Asset::CategoricalOutcome(market_id, 1)], weight);
assert_eq!(pool_weights[&Asset::CategoricalOutcome(market_id, 2)], weight);
assert_eq!(pool_weights[&Asset::CategoricalOutcome(market_id, 3)], weight);
assert_eq!(pool_weights[&Asset::Ztg], base_asset_weight);
});
}

Expand Down Expand Up @@ -2299,6 +2338,7 @@ fn deploy_swap_pool_correctly_sets_weight_of_base_asset() {
gen_metadata(50),
MarketType::Categorical(3),
MarketDisputeMechanism::SimpleDisputes,
1,
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
weights,
));
Expand All @@ -2325,13 +2365,17 @@ fn deploy_swap_pool_for_market_returns_error_if_weights_is_too_short() {
MarketDisputeMechanism::SimpleDisputes,
ScoringRule::CPMM
));
let _ = Balances::set_balance(Origin::root(), ALICE, 246 * BASE, 0);
assert_ok!(PredictionMarkets::buy_complete_set(Origin::signed(ALICE), 0, 123 * BASE));
let amount = 123 * BASE;
assert_ok!(Balances::set_balance(Origin::root(), ALICE, 2 * amount, 0));
assert_ok!(PredictionMarkets::buy_complete_set(Origin::signed(ALICE), 0, amount));
// Attempt to create a pool with four weights; but we need five instead (base asset not
// counted).
assert_noop!(
PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(ALICE),
0,
123 * BASE,
1,
amount,
vec![
<Runtime as zrml_swaps::Config>::MinWeight::get();
(category_count - 1).into()
Expand All @@ -2356,15 +2400,17 @@ fn deploy_swap_pool_for_market_returns_error_if_weights_is_too_long() {
MarketDisputeMechanism::SimpleDisputes,
ScoringRule::CPMM
));
let _ = Balances::set_balance(Origin::root(), ALICE, 246 * BASE, 0);
assert_ok!(PredictionMarkets::buy_complete_set(Origin::signed(ALICE), 0, 123 * BASE));
// Attempt to create a pool with seven weights; but we need six instead (five for the
// outcome tokens, one for the base asset).
let amount = 123 * BASE;
assert_ok!(Balances::set_balance(Origin::root(), ALICE, 2 * amount, 0));
assert_ok!(PredictionMarkets::buy_complete_set(Origin::signed(ALICE), 0, amount));
// Attempt to create a pool with six weights; but we need five instead (base asset not
// counted).
assert_noop!(
PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(ALICE),
0,
123 * BASE,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
amount,
vec![
<Runtime as zrml_swaps::Config>::MinWeight::get();
(category_count + 1).into()
Expand Down Expand Up @@ -2661,6 +2707,7 @@ fn deploy_swap_pool(market: Market<u128, u64, u64>, market_id: u128) -> Dispatch
PredictionMarkets::deploy_swap_pool_for_market(
Origin::signed(FRED),
0,
<Runtime as zrml_swaps::Config>::MaxSwapFee::get(),
<Runtime as zrml_swaps::Config>::MinLiquidity::get(),
vec![<Runtime as zrml_swaps::Config>::MinWeight::get(); outcome_assets_len],
)
Expand Down
Loading

0 comments on commit 852ebea

Please sign in to comment.