Skip to content

Commit

Permalink
Merge pull request #822 from galacticcouncil/feat/add-omnipool-liquid…
Browse files Browse the repository at this point in the history
…ty-limits

feat: add omnipool liquidity limits
  • Loading branch information
dmoka authored Jun 7, 2024
2 parents 0b662d0 + 2aa1302 commit d14172d
Show file tree
Hide file tree
Showing 6 changed files with 1,453 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pallets/omnipool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-omnipool"
version = "4.2.2"
version = "4.3.0"
authors = ['GalacticCouncil']
edition = "2021"
license = "Apache-2.0"
Expand Down
84 changes: 84 additions & 0 deletions pallets/omnipool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ pub mod pallet {
ZeroAmountOut,
/// Existential deposit of asset is not available.
ExistentialDepositNotAvailable,
/// Slippage protection
SlippageLimit,
}

#[pallet::call]
Expand Down Expand Up @@ -580,6 +582,44 @@ pub mod pallet {
)]
#[transactional]
pub fn add_liquidity(origin: OriginFor<T>, asset: T::AssetId, amount: Balance) -> DispatchResult {
Self::add_liquidity_with_limit(origin, asset, amount, Balance::MIN)
}

/// Add liquidity of asset `asset` in quantity `amount` to Omnipool.
///
/// Limit protection is applied.
///
/// `add_liquidity` adds specified asset amount to Omnipool and in exchange gives the origin
/// corresponding shares amount in form of NFT at current price.
///
/// Asset's tradable state must contain ADD_LIQUIDITY flag, otherwise `NotAllowed` error is returned.
///
/// NFT is minted using NTFHandler which implements non-fungibles traits from frame_support.
///
/// Asset weight cap must be respected, otherwise `AssetWeightExceeded` error is returned.
/// Asset weight is ratio between new HubAsset reserve and total reserve of Hub asset in Omnipool.
///
/// Add liquidity fails if price difference between spot price and oracle price is higher than allowed by `PriceBarrier`.
///
/// Parameters:
/// - `asset`: The identifier of the new asset added to the pool. Must be already in the pool
/// - `amount`: Amount of asset added to omnipool
/// - `min_shares_limit`: The min amount of delta share asset the user should receive in the position
///
/// Emits `LiquidityAdded` event when successful.
///
#[pallet::call_index(13)]
#[pallet::weight(<T as Config>::WeightInfo::add_liquidity()
.saturating_add(T::OmnipoolHooks::on_liquidity_changed_weight()
.saturating_add(T::ExternalPriceOracle::get_price_weight()))
)]
#[transactional]
pub fn add_liquidity_with_limit(
origin: OriginFor<T>,
asset: T::AssetId,
amount: Balance,
min_shares_limit: Balance,
) -> DispatchResult {
let who = ensure_signed(origin.clone())?;

ensure!(
Expand Down Expand Up @@ -625,6 +665,11 @@ pub mod pallet {
)
.ok_or(ArithmeticError::Overflow)?;

ensure!(
*state_changes.asset.delta_shares >= min_shares_limit,
Error::<T>::SlippageLimit
);

let new_asset_state = asset_state
.delta_update(&state_changes.asset)
.ok_or(ArithmeticError::Overflow)?;
Expand Down Expand Up @@ -724,6 +769,40 @@ pub mod pallet {
origin: OriginFor<T>,
position_id: T::PositionItemId,
amount: Balance,
) -> DispatchResult {
Self::remove_liquidity_with_limit(origin, position_id, amount, Balance::MIN)
}

/// Remove liquidity of asset `asset` in quantity `amount` from Omnipool
///
/// Limit protection is applied.
///
/// `remove_liquidity` removes specified shares amount from given PositionId (NFT instance).
///
/// Asset's tradable state must contain REMOVE_LIQUIDITY flag, otherwise `NotAllowed` error is returned.
///
/// if all shares from given position are removed, position is destroyed and NFT is burned.
///
/// Remove liquidity fails if price difference between spot price and oracle price is higher than allowed by `PriceBarrier`.
///
/// Dynamic withdrawal fee is applied if withdrawal is not safe. It is calculated using spot price and external price oracle.
/// Withdrawal is considered safe when trading is disabled.
///
/// Parameters:
/// - `position_id`: The identifier of position which liquidity is removed from.
/// - `amount`: Amount of shares removed from omnipool
/// - `min_limit`: The min amount of asset to be removed for the user
///
/// Emits `LiquidityRemoved` event when successful.
///
#[pallet::call_index(14)]
#[pallet::weight(<T as Config>::WeightInfo::remove_liquidity().saturating_add(T::OmnipoolHooks::on_liquidity_changed_weight()))]
#[transactional]
pub fn remove_liquidity_with_limit(
origin: OriginFor<T>,
position_id: T::PositionItemId,
amount: Balance,
min_limit: Balance,
) -> DispatchResult {
let who = ensure_signed(origin.clone())?;

Expand Down Expand Up @@ -790,6 +869,11 @@ pub mod pallet {
)
.ok_or(ArithmeticError::Overflow)?;

ensure!(
*state_changes.asset.delta_reserve >= min_limit,
Error::<T>::SlippageLimit
);

let new_asset_state = asset_state
.delta_update(&state_changes.asset)
.ok_or(ArithmeticError::Overflow)?;
Expand Down
248 changes: 248 additions & 0 deletions pallets/omnipool/src/tests/add_liquidity_with_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use super::*;
use frame_support::assert_noop;

#[test]
fn add_liquidity_should_work_when_asset_exists_in_pool() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.add_endowed_accounts((LP2, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP2, 2000 * ONE)
.build()
.execute_with(|| {
let token_amount = 2000 * ONE;
let liq_added = 400 * ONE;

// ACT
let position_id = last_position_id();
assert_ok!(Omnipool::add_liquidity_with_limit(
RuntimeOrigin::signed(LP1),
1_000,
liq_added,
liq_added
));

// ASSERT - asset state, pool state, position
assert_asset_state!(
1_000,
AssetReserveState {
reserve: token_amount + liq_added,
hub_reserve: 1560 * ONE,
shares: 2400 * ONE,
protocol_shares: Balance::zero(),
cap: DEFAULT_WEIGHT_CAP,
tradable: Tradability::default(),
}
);

let position = Positions::<Test>::get(position_id).unwrap();

let expected = Position::<Balance, AssetId> {
asset_id: 1_000,
amount: liq_added,
shares: liq_added,
price: (1560 * ONE, token_amount + liq_added),
};

assert_eq!(position, expected);

assert_pool_state!(12_060 * ONE, 24_120 * ONE, SimpleImbalance::default());

assert_balance!(LP1, 1_000, 4600 * ONE);

let minted_position = POSITIONS.with(|v| v.borrow().get(&position_id).copied());

assert_eq!(minted_position, Some(LP1));
});
}

#[test]
fn add_stable_asset_liquidity_works() {
ExtBuilder::default()
.add_endowed_accounts((LP1, DAI, 5000 * ONE))
.add_endowed_accounts((LP2, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.build()
.execute_with(|| {
let liq_added = 400 * ONE;
let position_id = <NextPositionId<Test>>::get();
assert_ok!(Omnipool::add_liquidity_with_limit(
RuntimeOrigin::signed(LP1),
DAI,
liq_added,
liq_added
));

assert_asset_state!(
DAI,
AssetReserveState {
reserve: 1000 * ONE + liq_added,
hub_reserve: 700000000000000,
shares: 1400000000000000,
protocol_shares: 0,
cap: DEFAULT_WEIGHT_CAP,
tradable: Tradability::default(),
}
);

let position = Positions::<Test>::get(position_id).unwrap();

let expected = Position::<Balance, AssetId> {
asset_id: DAI,
amount: liq_added,
shares: liq_added,
price: (700 * ONE, 1400 * ONE),
};

assert_eq!(position, expected);

assert_pool_state!(10_700 * ONE, 21_400 * ONE, SimpleImbalance::default());

assert_balance!(LP1, DAI, 4600 * ONE);

let minted_position = POSITIONS.with(|v| v.borrow().get(&position_id).copied());

assert_eq!(minted_position, Some(LP1));
});
}

#[test]
fn add_liquidity_for_non_pool_token_fails() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.build()
.execute_with(|| {
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP1), 1_000, 2000 * ONE, 2000 * ONE),
Error::<Test>::AssetNotFound
);
});
}

#[test]
fn add_liquidity_with_insufficient_balance_fails() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP1, 2000 * ONE)
.build()
.execute_with(|| {
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP3), 1_000, 2000 * ONE, 2000 * ONE),
Error::<Test>::InsufficientBalance
);
});
}

#[test]
fn add_liquidity_exceeding_weight_cap_fails() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.with_asset_weight_cap(Permill::from_float(0.1))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP1, 100 * ONE)
.build()
.execute_with(|| {
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP1), 1_000, 2000 * ONE, 2000 * ONE),
Error::<Test>::AssetWeightCapExceeded
);
});
}

#[test]
fn add_insufficient_liquidity_fails() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.with_min_added_liquidity(5 * ONE)
.with_asset_weight_cap(Permill::from_float(0.1))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP1, 2000 * ONE)
.build()
.execute_with(|| {
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP3), 1_000, ONE, ONE),
Error::<Test>::InsufficientLiquidity
);
});
}

#[test]
fn add_liquidity_should_fail_when_asset_state_does_not_include_add_liquidity() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.with_min_added_liquidity(ONE)
.with_asset_weight_cap(Permill::from_float(0.1))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP1, 2000 * ONE)
.build()
.execute_with(|| {
assert_ok!(Omnipool::set_asset_tradable_state(
RuntimeOrigin::root(),
1000,
Tradability::SELL | Tradability::BUY | Tradability::REMOVE_LIQUIDITY
));

assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP1), 1_000, 2 * ONE, 2 * ONE),
Error::<Test>::NotAllowed
);
});
}

#[test]
fn add_liquidity_should_fail_when_prices_differ_and_is_higher() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.add_endowed_accounts((LP2, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP2, 2000 * ONE)
.with_max_allowed_price_difference(Permill::from_percent(1))
.with_external_price_adjustment((3, 100, false))
.build()
.execute_with(|| {
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE, 400 * ONE),
Error::<Test>::PriceDifferenceTooHigh
);
});
}

#[test]
fn add_liquidity_should_fail_when_prices_differ_and_is_lower() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.add_endowed_accounts((LP2, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP2, 2000 * ONE)
.with_max_allowed_price_difference(Permill::from_percent(1))
.with_external_price_adjustment((3, 100, true))
.build()
.execute_with(|| {
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE, 400 * ONE),
Error::<Test>::PriceDifferenceTooHigh
);
});
}

#[test]
fn add_liquidity_should_fail_when_doesnt_reach_min_limit() {
ExtBuilder::default()
.add_endowed_accounts((LP1, 1_000, 5000 * ONE))
.add_endowed_accounts((LP2, 1_000, 5000 * ONE))
.with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1))
.with_token(1_000, FixedU128::from_float(0.65), LP2, 2000 * ONE)
.build()
.execute_with(|| {
//Do some trade not to have parity between liquidity and shares
assert_ok!(Omnipool::sell(RuntimeOrigin::signed(LP1), 1_000, DAI, 20 * ONE, 0));

// ACT
assert_noop!(
Omnipool::add_liquidity_with_limit(RuntimeOrigin::signed(LP1), 1_000, 500 * ONE, 496 * ONE), //user received 495, so below limit
Error::<Test>::SlippageLimit
);
});
}
2 changes: 2 additions & 0 deletions pallets/omnipool/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ mod invariants;
mod remove_liquidity;
mod sell;

mod add_liquidity_with_limit;
mod barrier;
mod imbalance;
pub(crate) mod mock;
mod positions;
mod refund;
mod remove_liquidity_with_limit;
mod remove_token;
mod spot_price;
mod tradability;
Expand Down
Loading

0 comments on commit d14172d

Please sign in to comment.