Skip to content

Commit

Permalink
feat: Relative Slippage Limits (PRO-1207) (#4547)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlastairHolmes authored Feb 27, 2024
1 parent 681eaf8 commit 2759fd9
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 144 deletions.
2 changes: 1 addition & 1 deletion state-chain/amm/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub fn bounded_sqrt_price(quote: Amount, base: Amount) -> SqrtPriceQ64F96 {
if base.is_zero() {
MAX_SQRT_PRICE
} else {
let unbounded_sqrt_price = SqrtPriceQ64F96::try_from(
let unbounded_sqrt_price = U256::try_from(
((U512::from(quote) << 256) / U512::from(base)).integer_sqrt() >>
(128 - SQRT_PRICE_FRACTIONAL_BITS),
)
Expand Down
13 changes: 13 additions & 0 deletions state-chain/amm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ impl<LiquidityProvider: Clone + Ord> PoolState<LiquidityProvider> {
}
}

/// Returns the current sqrt price for a given direction of swap. The price is measured in units
/// of the specified Pairs argument
pub fn swap_sqrt_price(
order: Side,
input_amount: Amount,
output_amount: Amount,
) -> SqrtPriceQ64F96 {
match order.to_sold_pair() {
Pairs::Base => common::bounded_sqrt_price(output_amount, input_amount),
Pairs::Quote => common::bounded_sqrt_price(input_amount, output_amount),
}
}

fn inner_worst_price(order: Side) -> SqrtPriceQ64F96 {
match order.to_sold_pair() {
Pairs::Quote => QuoteToBase::WORST_SQRT_PRICE,
Expand Down
12 changes: 12 additions & 0 deletions state-chain/pallets/cf-pools/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,5 +251,17 @@ mod benchmarks {
assert!(!ScheduledLimitOrderUpdates::<T>::get(BlockNumberFor::<T>::from(5u32)).is_empty());
}

#[benchmark]
fn set_maximum_relative_slippage() {
let call = Call::<T>::set_maximum_relative_slippage { ticks: Some(1000) };

#[block]
{
assert_ok!(
call.dispatch_bypass_filter(T::EnsureGovernance::try_successful_origin().unwrap())
);
}
}

impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test,);
}
124 changes: 91 additions & 33 deletions state-chain/pallets/cf-pools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
use core::ops::Range;

use cf_amm::{
common::{Amount, PoolPairsMap, Price, Side, SqrtPriceQ64F96, Tick},
limit_orders,
limit_orders::{Collected, PositionInfo},
range_orders,
range_orders::Liquidity,
common::{self, Amount, PoolPairsMap, Price, Side, SqrtPriceQ64F96, Tick},
limit_orders::{self, Collected, PositionInfo},
range_orders::{self, Liquidity},
PoolState,
};
use cf_primitives::{chains::assets::any, Asset, AssetAmount, SwapOutput, STABLE_ASSET};
Expand Down Expand Up @@ -291,6 +289,10 @@ pub mod pallet {
pub(super) type ScheduledLimitOrderUpdates<T: Config> =
StorageMap<_, Twox64Concat, BlockNumberFor<T>, Vec<LimitOrderUpdate<T>>, ValueQuery>;

/// Maximum relative slippage for a single swap, measured in number of ticks.
#[pallet::storage]
pub(super) type MaximumRelativeSlippage<T: Config> = StorageValue<_, u32, OptionQuery>;

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub flip_buy_interval: BlockNumberFor<T>,
Expand Down Expand Up @@ -929,7 +931,7 @@ pub mod pallet {
/// - [BadOrigin](frame_system::BadOrigin)
/// - [UnsupportedCall](pallet_cf_pools::Error::UnsupportedCall)
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::schedule())]
#[pallet::weight(T::WeightInfo::schedule_limit_order_update())]
pub fn schedule_limit_order_update(
origin: OriginFor<T>,
call: Box<Call<T>>,
Expand Down Expand Up @@ -962,6 +964,21 @@ pub mod pallet {
_ => Err(Error::<T>::UnsupportedCall)?,
}
}

/// Sets the allowed percentage increase (in number of ticks) of the price of the brought
/// asset during a swap. Note this limit applies to the difference between the swap's mean
/// price, and both the pool price before and after the swap. If the limit is exceeded the
/// swap will fail and will be retried in the next block.
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::set_maximum_relative_slippage())]
pub fn set_maximum_relative_slippage(
origin: OriginFor<T>,
ticks: Option<u32>,
) -> DispatchResult {
T::EnsureGovernance::ensure_origin(origin)?;
MaximumRelativeSlippage::<T>::set(ticks);
Ok(())
}
}
}

Expand All @@ -987,13 +1004,49 @@ impl<T: Config> SwappingApi for Pallet<T> {
let (asset_pair, order) =
AssetPair::from_swap(from, to).ok_or(Error::<T>::PoolDoesNotExist)?;
Self::try_mutate_pool(asset_pair, |_asset_pair, pool| {
let (output_amount, remaining_amount) =
pool.pool_state.swap(order, input_amount.into(), None);
remaining_amount
.is_zero()
.then_some(())
.ok_or(Error::<T>::InsufficientLiquidity)?;
let output_amount = output_amount.try_into().map_err(|_| Error::<T>::OutputOverflow)?;
let output_amount = if input_amount == 0 {
0
} else {
let input_amount: Amount = input_amount.into();

let tick_before = pool
.pool_state
.current_price(order)
.ok_or(Error::<T>::InsufficientLiquidity)?
.2;
let (output_amount, _remaining_amount) =
pool.pool_state.swap(order, input_amount, None);
let tick_after = pool
.pool_state
.current_price(order)
.ok_or(Error::<T>::InsufficientLiquidity)?
.2;

let swap_tick = common::tick_at_sqrt_price(
PoolState::<(T::AccountId, OrderId)>::swap_sqrt_price(
order,
input_amount,
output_amount,
),
);
let bounded_swap_tick = if tick_after < tick_before {
core::cmp::min(core::cmp::max(tick_after, swap_tick), tick_before)
} else {
core::cmp::min(core::cmp::max(tick_before, swap_tick), tick_after)
};

if let Some(maximum_relative_slippage) = MaximumRelativeSlippage::<T>::get() {
if core::cmp::min(
bounded_swap_tick.abs_diff(tick_after),
bounded_swap_tick.abs_diff(tick_before),
) > maximum_relative_slippage
{
return Err(Error::<T>::InsufficientLiquidity.into());
}
}

output_amount.try_into().map_err(|_| Error::<T>::OutputOverflow)?
};
Self::deposit_event(Event::<T>::AssetSwapped { from, to, input_amount, output_amount });
Ok(output_amount)
})
Expand Down Expand Up @@ -1958,27 +2011,32 @@ impl<T: Config> cf_traits::AssetConverter for Pallet<T> {
})?
.output;

let input_amount_to_convert = multiply_by_rational_with_rounding(
desired_output_amount.into(),
available_input_amount.into(),
available_output_amount,
sp_arithmetic::Rounding::Down,
)
.defensive_proof(
"Unexpected overflow occurred during asset conversion. Please report this to Chainflip Labs."
)?;

Some((
available_input_amount.saturating_sub(input_amount_to_convert.unique_saturated_into()),
Self::swap_with_network_fee(
input_asset,
output_asset,
sp_std::cmp::min(input_amount_to_convert, available_input_amount.into()),
if available_output_amount == 0 {
None
} else {
let input_amount_to_convert = multiply_by_rational_with_rounding(
desired_output_amount.into(),
available_input_amount.into(),
available_output_amount,
sp_arithmetic::Rounding::Down,
)
.ok()?
.output
.unique_saturated_into(),
))
.defensive_proof(
"Unexpected overflow occurred during asset conversion. Please report this to Chainflip Labs."
)?;

Some((
available_input_amount
.saturating_sub(input_amount_to_convert.unique_saturated_into()),
Self::swap_with_network_fee(
input_asset,
output_asset,
sp_std::cmp::min(input_amount_to_convert, available_input_amount.into()),
)
.ok()?
.output
.unique_saturated_into(),
))
}
}
}

Expand Down
84 changes: 83 additions & 1 deletion state-chain/pallets/cf-pools/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
use cf_amm::common::{price_at_tick, tick_at_price, Side, Tick, PRICE_FRACTIONAL_BITS};
use cf_primitives::{chains::assets::any::Asset, AssetAmount, SwapOutput};
use cf_test_utilities::{assert_events_match, assert_has_event, last_event};
use cf_traits::AssetConverter;
use cf_traits::{AssetConverter, SwappingApi};
use frame_support::{assert_noop, assert_ok, traits::Hooks};
use frame_system::pallet_prelude::BlockNumberFor;
use sp_core::U256;
Expand Down Expand Up @@ -1121,3 +1121,85 @@ fn fees_are_getting_recorded() {
MockBalance::assert_fees_recorded(&BOB);
});
}

#[test]
fn test_maximum_slippage_limits() {
use cf_utilities::{assert_err, assert_ok};

new_test_ext().execute_with(|| {
let test_swaps = |size_limit_when_slippage_limit_is_hit| {
for (size, expected_output) in [
(0, 0),
(1, 0),
(100, 99),
(200, 199),
(250, 249),
(300, 299),
(400, 398),
(500, 497),
(1500, 1477),
(2500, 2439),
(3500, 3381),
(4500, 4306),
(5500, 5213),
(6500, 6103),
(7500, 6976),
(8500, 7834),
(9500, 8675),
(10500, 9502),
(11500, 10313),
(12500, 11111),
(13500, 11894),
(14500, 12663),
(15500, 13419),
] {
pallet_cf_pools::Pools::<Test>::remove(
AssetPair::new(Asset::Eth, Asset::Usdc).unwrap(),
);
assert_ok!(LiquidityPools::new_pool(
RuntimeOrigin::root(),
Asset::Eth,
STABLE_ASSET,
Default::default(),
price_at_tick(0).unwrap(),
));
assert_ok!(LiquidityPools::set_range_order(
RuntimeOrigin::signed(ALICE),
Asset::Eth,
STABLE_ASSET,
0,
Some(-10000..10000),
RangeOrderSize::Liquidity { liquidity: 100_000 },
));
let result = LiquidityPools::swap_single_leg(STABLE_ASSET, Asset::Eth, size);
if size < size_limit_when_slippage_limit_is_hit {
assert_eq!(expected_output, assert_ok!(result));
} else {
assert_err!(result);
}
}
};

test_swaps(u128::MAX);

assert_ok!(
LiquidityPools::set_maximum_relative_slippage(RuntimeOrigin::root(), Some(954),)
);

test_swaps(10500);

assert_ok!(LiquidityPools::set_maximum_relative_slippage(RuntimeOrigin::root(), None,));

test_swaps(u128::MAX);

assert_ok!(LiquidityPools::set_maximum_relative_slippage(RuntimeOrigin::root(), Some(10),));

test_swaps(300);

assert_ok!(
LiquidityPools::set_maximum_relative_slippage(RuntimeOrigin::root(), Some(300),)
);

test_swaps(3500);
});
}
Loading

0 comments on commit 2759fd9

Please sign in to comment.