Skip to content

Commit

Permalink
Feat: Set pool fees (#4050)
Browse files Browse the repository at this point in the history
* Added function to set liquidity pool fees.
Changing the pool fees will collect all fees and bought amount for LPer
and credit them to Lpers' account.

* Slight improvement

* cargo fmt

* Fixed some typo
Added benchmarking for setting pool fees

* Corrected a bug where the funds are accredited to the wrong asset
Pool's limit orders are updated with the pool's limit order
Updated unit to test this logic

* Added a new unit test for Range order
Fixed some minor typo

* Fixed build

* naming and extra comments

* updating limit order cache also emit events.

* Refactored some code out to reduce duplication

* Address some PR comments

* Fixed benchmarking

* fmt

---------

Co-authored-by: Alastair Holmes <holmes.alastair@outlook.com>
  • Loading branch information
syan095 and AlastairHolmes authored Oct 12, 2023
1 parent 6320a79 commit 390e3d1
Show file tree
Hide file tree
Showing 13 changed files with 749 additions and 220 deletions.
12 changes: 4 additions & 8 deletions api/lib/src/lp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ fn collect_range_order_returns(
.filter_map(|event| match event {
state_chain_runtime::RuntimeEvent::LiquidityPools(
pallet_cf_pools::Event::RangeOrderUpdated {
increase_or_decrease,
liquidity_delta,
position_delta: Some((increase_or_decrease, liquidity_delta)),
liquidity_total,
assets_delta,
collected_fees,
Expand All @@ -62,8 +61,7 @@ pub struct LimitOrderReturn {
amount_total: AssetAmount,
collected_fees: AssetAmount,
bought_amount: AssetAmount,
increase_or_decrease: IncreaseOrDecrease,
amount_delta: AssetAmount,
position_delta: Option<(IncreaseOrDecrease, AssetAmount)>,
}

fn collect_limit_order_returns(
Expand All @@ -74,8 +72,7 @@ fn collect_limit_order_returns(
.filter_map(|event| match event {
state_chain_runtime::RuntimeEvent::LiquidityPools(
pallet_cf_pools::Event::LimitOrderUpdated {
increase_or_decrease,
amount_delta,
position_delta,
amount_total,
collected_fees,
bought_amount,
Expand All @@ -87,8 +84,7 @@ fn collect_limit_order_returns(
amount_total,
collected_fees,
bought_amount,
increase_or_decrease,
amount_delta,
position_delta,
}),
_ => None,
})
Expand Down
27 changes: 17 additions & 10 deletions state-chain/amm/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use scale_info::TypeInfo;
use serde::{Deserialize, Serialize};
use sp_core::{U256, U512};

pub const ONE_IN_HUNDREDTH_PIPS: u32 = 1000000;
pub const ONE_IN_HUNDREDTH_PIPS: u32 = 1_000_000;
pub const MAX_LP_FEE: u32 = ONE_IN_HUNDREDTH_PIPS / 2;

/// Represents an amount of an asset, in its smallest unit i.e. Ethereum has 10^-18 precision, and
/// therefore an `Amount` with the literal value of `1` would represent 10^-18 Ethereum.
Expand All @@ -18,6 +19,12 @@ pub type SqrtPriceQ64F96 = U256;
/// The number of fractional bits used by `SqrtPriceQ64F96`.
pub const SQRT_PRICE_FRACTIONAL_BITS: u32 = 96;

#[derive(Debug)]
pub enum SetFeesError {
/// Fee must be between 0 - 50%
InvalidFeeAmount,
}

#[derive(
Debug,
Clone,
Expand Down Expand Up @@ -381,22 +388,22 @@ pub(super) fn sqrt_price_at_tick(tick: Tick) -> SqrtPriceQ64F96 {
}

/// Calculates the greatest tick value such that `sqrt_price_at_tick(tick) <= sqrt_price`
pub(super) fn tick_at_sqrt_price(sqrt_price: SqrtPriceQ64F96) -> Tick {
pub fn tick_at_sqrt_price(sqrt_price: SqrtPriceQ64F96) -> Tick {
assert!(is_sqrt_price_valid(sqrt_price));

let sqrt_price_q64f128 = sqrt_price << 32u128;

let (integer_log_2, mantissa) = {
let mut _bits_remaining = sqrt_price_q64f128;
let mut most_signifcant_bit = 0u8;
let mut most_significant_bit = 0u8;

// rustfmt chokes when formatting this macro.
// See: https://github.com/rust-lang/rustfmt/issues/5404
#[rustfmt::skip]
macro_rules! add_integer_bit {
($bit:literal, $lower_bits_mask:literal) => {
if _bits_remaining > U256::from($lower_bits_mask) {
most_signifcant_bit |= $bit;
most_significant_bit |= $bit;
_bits_remaining >>= $bit;
}
};
Expand All @@ -412,17 +419,17 @@ pub(super) fn tick_at_sqrt_price(sqrt_price: SqrtPriceQ64F96) -> Tick {
add_integer_bit!(1u8, 0x1u128);

(
// most_signifcant_bit is the log2 of sqrt_price_q64f128 as an integer. This
// converts most_signifcant_bit to the integer log2 of sqrt_price_q64f128 as an
// most_significant_bit is the log2 of sqrt_price_q64f128 as an integer. This
// converts most_significant_bit to the integer log2 of sqrt_price_q64f128 as an
// q64f128
((most_signifcant_bit as i16) + (-128i16)) as i8,
((most_significant_bit as i16) + (-128i16)) as i8,
// Calculate mantissa of sqrt_price_q64f128.
if most_signifcant_bit >= 128u8 {
if most_significant_bit >= 128u8 {
// The bits we possibly drop when right shifting don't contribute to the log2
// above the 14th fractional bit.
sqrt_price_q64f128 >> (most_signifcant_bit - 127u8)
sqrt_price_q64f128 >> (most_significant_bit - 127u8)
} else {
sqrt_price_q64f128 << (127u8 - most_signifcant_bit)
sqrt_price_q64f128 << (127u8 - most_significant_bit)
}
.as_u128(), // Conversion to u128 is safe as top 128 bits are always zero
)
Expand Down
23 changes: 20 additions & 3 deletions state-chain/amm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ use core::convert::Infallible;

use codec::{Decode, Encode};
use common::{
price_to_sqrt_price, sqrt_price_to_price, Amount, OneToZero, Order, Price, Side, SideMap,
SqrtPriceQ64F96, Tick, ZeroToOne,
price_to_sqrt_price, sqrt_price_to_price, Amount, OneToZero, Order, Price, SetFeesError, Side,
SideMap, SqrtPriceQ64F96, Tick, ZeroToOne,
};
use limit_orders::{Collected, PositionInfo};
use range_orders::Liquidity;
use scale_info::TypeInfo;
use sp_std::vec::Vec;
use sp_std::{collections::btree_map::BTreeMap, vec::Vec};

pub mod common;
pub mod limit_orders;
Expand Down Expand Up @@ -343,4 +344,20 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
),
})
}

#[allow(clippy::type_complexity)]
pub fn set_fees(
&mut self,
fee_hundredth_pips: u32,
) -> Result<SideMap<BTreeMap<(Tick, LiquidityProvider), (Collected, PositionInfo)>>, SetFeesError>
{
self.range_orders.set_fees(fee_hundredth_pips)?;
self.limit_orders.set_fees(fee_hundredth_pips)
}

// Returns if the pool fee is valid.
pub fn validate_fees(fee_hundredth_pips: u32) -> bool {
limit_orders::PoolState::<LiquidityProvider>::validate_fees(fee_hundredth_pips) &&
range_orders::PoolState::<LiquidityProvider>::validate_fees(fee_hundredth_pips)
}
}
51 changes: 24 additions & 27 deletions state-chain/amm/src/limit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ use sp_std::vec::Vec;

use crate::common::{
is_tick_valid, mul_div_ceil, mul_div_floor, sqrt_price_at_tick, sqrt_price_to_price,
tick_at_sqrt_price, Amount, OneToZero, Price, SideMap, SqrtPriceQ64F96, Tick, ZeroToOne,
ONE_IN_HUNDREDTH_PIPS, PRICE_FRACTIONAL_BITS,
tick_at_sqrt_price, Amount, OneToZero, Price, SetFeesError, SideMap, SqrtPriceQ64F96, Tick,
ZeroToOne, MAX_LP_FEE, ONE_IN_HUNDREDTH_PIPS, PRICE_FRACTIONAL_BITS,
};

// This is the maximum liquidity/amount of an asset that can be sold at a single tick/price. If an
Expand Down Expand Up @@ -119,7 +119,7 @@ impl FloatBetweenZeroAndOne {
// As the denominator <= U256::MAX, this div will not right-shift the mantissa more than
// 256 bits, so we maintain at least 256 accurate bits in the result.
let (d, div_remainder) =
U512::div_mod(mul_normalised_mantissa, U512::from(denominator)); // Note that d can never be zero as mul_normalised_mantissa always has atleast one bit
U512::div_mod(mul_normalised_mantissa, U512::from(denominator)); // Note that d can never be zero as mul_normalised_mantissa always has at least one bit
// set above the 256th bit.
let d = if div_remainder.is_zero() { d } else { d + U512::one() };
let normalise_shift = d.leading_zeros();
Expand Down Expand Up @@ -170,14 +170,14 @@ impl FloatBetweenZeroAndOne {

let (y_floor, shift_remainder) = Self::right_shift_mod(y_shifted_floor, negative_exponent);

let y_floor = y_floor.try_into().unwrap(); // Unwrap safe as numerator <= demoninator and therefore y cannot be greater than x
let y_floor = y_floor.try_into().unwrap(); // Unwrap safe as numerator <= denominator and therefore y cannot be greater than x

(
y_floor,
if div_remainder.is_zero() && shift_remainder.is_zero() {
y_floor
} else {
y_floor + 1 // Safe as for there to be a remainder y_floor must be atleast 1 less than x
y_floor + 1 // Safe as for there to be a remainder y_floor must be at least 1 less than x
},
)
}
Expand Down Expand Up @@ -247,12 +247,6 @@ pub enum NewError {
InvalidFeeAmount,
}

#[derive(Debug)]
pub enum SetFeesError {
/// Fee must be between 0 - 50%
InvalidFeeAmount,
}

#[derive(Debug)]
pub enum DepthError {
/// Invalid Price
Expand Down Expand Up @@ -355,8 +349,8 @@ pub(super) struct FixedPool {
/// associated position but have some liquidity available, but this would likely be a very
/// small amount.
available: Amount,
/// This is the big product of all `1.0 - percent_used_by_swap` for all swaps that have occured
/// since this FixedPool instance was created and used liquidity from it.
/// This is the big product of all `1.0 - percent_used_by_swap` for all swaps that have
/// occurred since this FixedPool instance was created and used liquidity from it.
percent_remaining: FloatBetweenZeroAndOne,
}

Expand All @@ -368,11 +362,11 @@ pub(super) struct PoolState<LiquidityProvider> {
/// The ID the next FixedPool that is created will use.
next_pool_instance: u128,
/// All the FixedPools that have some liquidity. They are grouped into all those that are
/// selling asset `Zero` and all those that are selling asset `one` used the SideMap.
/// selling asset `Zero` and all those that are selling asset `One` used the SideMap.
fixed_pools: SideMap<BTreeMap<SqrtPriceQ64F96, FixedPool>>,
/// All the Positions that either are providing liquidity currently, or were providing
/// liquidity directly after the last time they where updated. They are grouped into all those
/// that are selling asset `Zero` and all those that are selling asset `one` used the SideMap.
/// that are selling asset `Zero` and all those that are selling asset `One` used the SideMap.
/// Therefore there can be positions stored here that don't provide any liquidity.
positions: SideMap<BTreeMap<(SqrtPriceQ64F96, LiquidityProvider), Position>>,
}
Expand All @@ -383,7 +377,7 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
///
/// This function never panics.
pub(super) fn new(fee_hundredth_pips: u32) -> Result<Self, NewError> {
(fee_hundredth_pips <= ONE_IN_HUNDREDTH_PIPS / 2)
Self::validate_fees(fee_hundredth_pips)
.then_some(())
.ok_or(NewError::InvalidFeeAmount)?;

Expand All @@ -396,19 +390,18 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
}

/// Sets the fee for the pool. This will apply to future swaps. The fee may not be set
/// higher than 50%. Also runs collect for all positions in the pool.
/// higher than 50%. Also runs collect for all positions in the pool. Returns a SideMap
/// containing the state and fees collected from every position as part of the set_fees
/// operation. The positions are grouped into a SideMap by the asset they sell.
///
/// This function never panics.
#[allow(clippy::type_complexity)]
#[allow(dead_code)]
pub(super) fn set_fees(
&mut self,
fee_hundredth_pips: u32,
) -> Result<
SideMap<BTreeMap<(SqrtPriceQ64F96, LiquidityProvider), (Collected, PositionInfo)>>,
SetFeesError,
> {
(fee_hundredth_pips <= ONE_IN_HUNDREDTH_PIPS / 2)
) -> Result<SideMap<BTreeMap<(Tick, LiquidityProvider), (Collected, PositionInfo)>>, SetFeesError>
{
Self::validate_fees(fee_hundredth_pips)
.then_some(())
.ok_or(SetFeesError::InvalidFeeAmount)?;

Expand All @@ -422,7 +415,7 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
.into_iter()
.map(|(sqrt_price, lp)| {
(
(sqrt_price, lp.clone()),
(tick_at_sqrt_price(sqrt_price), lp.clone()),
self.inner_collect::<OneToZero>(&lp, sqrt_price).unwrap(),
)
})
Expand All @@ -434,7 +427,7 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
.into_iter()
.map(|(sqrt_price, lp)| {
(
(sqrt_price, lp.clone()),
(tick_at_sqrt_price(sqrt_price), lp.clone()),
self.inner_collect::<ZeroToOne>(&lp, sqrt_price).unwrap(),
)
})
Expand Down Expand Up @@ -563,7 +556,7 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
// bought_amount and fees than may exist in the pool
position.amount - remaining_amount_ceil,
// We under-estimate remaining liquidity so that lp's cannot burn more liquidity
// than truely exists in the pool
// than truly exists in the pool
if remaining_amount_floor.is_zero() {
None
} else {
Expand Down Expand Up @@ -749,7 +742,7 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
}

/// Collects any earnings from the specified position. The SwapDirection determines which
/// direction of swaps the liquidity/position you're refering to is for.
/// direction of swaps the liquidity/position you're referring to is for.
///
/// This function never panics.
pub(super) fn collect<SD: SwapDirection>(
Expand Down Expand Up @@ -854,4 +847,8 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
Err(DepthError::InvalidTickRange)
}
}

pub fn validate_fees(fee_hundredth_pips: u32) -> bool {
fee_hundredth_pips <= MAX_LP_FEE
}
}
21 changes: 7 additions & 14 deletions state-chain/amm/src/range_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ use sp_core::{U256, U512};

use crate::common::{
is_sqrt_price_valid, is_tick_valid, mul_div_ceil, mul_div_floor, sqrt_price_at_tick,
tick_at_sqrt_price, Amount, OneToZero, Side, SideMap, SqrtPriceQ64F96, Tick, ZeroToOne,
MAX_TICK, MIN_TICK, ONE_IN_HUNDREDTH_PIPS, SQRT_PRICE_FRACTIONAL_BITS,
tick_at_sqrt_price, Amount, OneToZero, SetFeesError, Side, SideMap, SqrtPriceQ64F96, Tick,
ZeroToOne, MAX_LP_FEE, MAX_TICK, MIN_TICK, ONE_IN_HUNDREDTH_PIPS, SQRT_PRICE_FRACTIONAL_BITS,
};

/// This is the invariant wrt xy = k. It represents / is proportional to the depth of the
Expand Down Expand Up @@ -87,7 +87,7 @@ impl Position {

/*
Proof that `mul_div_floor` does not overflow:
Note position.liqiudity: u128
Note position.liquidity: u128
U512::one() << 128 > u128::MAX
*/
mul_div_floor(
Expand Down Expand Up @@ -148,7 +148,7 @@ pub struct PoolState<LiquidityProvider> {
/// This is the highest tick that represents a strictly lower price than the
/// current_sqrt_price. `current_tick` is the tick that when you swap ZeroToOne the
/// `current_sqrt_price` is moving towards (going down in literal value), and will cross when
/// `current_sqrt_price` reachs it. `current_tick + 1` is the tick the price is moving towards
/// `current_sqrt_price` reaches it. `current_tick + 1` is the tick the price is moving towards
/// (going up in literal value) when you swap OneToZero and will cross when
/// `current_sqrt_price` reaches it,
current_tick: Tick,
Expand Down Expand Up @@ -338,12 +338,6 @@ pub enum NewError {
InvalidInitialPrice,
}

#[derive(Debug)]
pub enum SetFeesError {
/// Fee must be between 0 - 50%
InvalidFeeAmount,
}

#[derive(Debug)]
pub enum MintError<E> {
/// One of the start/end ticks of the range reached its maximum gross liquidity
Expand Down Expand Up @@ -473,7 +467,6 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
/// fee is greater than 50%.
///
/// This function never panics
#[allow(dead_code)]
pub(super) fn set_fees(&mut self, fee_hundredth_pips: u32) -> Result<(), SetFeesError> {
Self::validate_fees(fee_hundredth_pips)
.then_some(())
Expand All @@ -482,8 +475,8 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
Ok(())
}

fn validate_fees(fee_hundredth_pips: u32) -> bool {
fee_hundredth_pips <= ONE_IN_HUNDREDTH_PIPS / 2
pub fn validate_fees(fee_hundredth_pips: u32) -> bool {
fee_hundredth_pips <= MAX_LP_FEE
}

/// Returns the current sqrt price of the pool. None if the pool has no more liquidity and the
Expand Down Expand Up @@ -653,7 +646,7 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
let (amounts_owed, current_liquidity_delta) =
self.inner_liquidity_to_amounts::<false>(burnt_liquidity, lower_tick, upper_tick);
// Will not underflow as current_liquidity_delta must have previously been added to
// current_liquidity for it to need to be substrated now
// current_liquidity for it to need to be subtracted now
self.current_liquidity -= current_liquidity_delta;

if lower_delta.liquidity_gross == 0 &&
Expand Down
Loading

0 comments on commit 390e3d1

Please sign in to comment.