diff --git a/Cargo.lock b/Cargo.lock index 9d65ff9aa..bd31e2029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8226,7 +8226,7 @@ dependencies = [ [[package]] name = "pallet-omnipool" -version = "4.2.2" +version = "4.3.0" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", diff --git a/pallets/omnipool/Cargo.toml b/pallets/omnipool/Cargo.toml index e108fd6aa..f19811b37 100644 --- a/pallets/omnipool/Cargo.toml +++ b/pallets/omnipool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-omnipool" -version = "4.2.2" +version = "4.3.0" authors = ['GalacticCouncil'] edition = "2021" license = "Apache-2.0" diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index 752ce806a..245e458dc 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -426,6 +426,8 @@ pub mod pallet { ZeroAmountOut, /// Existential deposit of asset is not available. ExistentialDepositNotAvailable, + /// Slippage protection + SlippageLimit, } #[pallet::call] @@ -580,6 +582,44 @@ pub mod pallet { )] #[transactional] pub fn add_liquidity(origin: OriginFor, 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(::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, + asset: T::AssetId, + amount: Balance, + min_shares_limit: Balance, + ) -> DispatchResult { let who = ensure_signed(origin.clone())?; ensure!( @@ -625,6 +665,11 @@ pub mod pallet { ) .ok_or(ArithmeticError::Overflow)?; + ensure!( + *state_changes.asset.delta_shares >= min_shares_limit, + Error::::SlippageLimit + ); + let new_asset_state = asset_state .delta_update(&state_changes.asset) .ok_or(ArithmeticError::Overflow)?; @@ -724,6 +769,40 @@ pub mod pallet { origin: OriginFor, 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(::WeightInfo::remove_liquidity().saturating_add(T::OmnipoolHooks::on_liquidity_changed_weight()))] + #[transactional] + pub fn remove_liquidity_with_limit( + origin: OriginFor, + position_id: T::PositionItemId, + amount: Balance, + min_limit: Balance, ) -> DispatchResult { let who = ensure_signed(origin.clone())?; @@ -790,6 +869,11 @@ pub mod pallet { ) .ok_or(ArithmeticError::Overflow)?; + ensure!( + *state_changes.asset.delta_reserve >= min_limit, + Error::::SlippageLimit + ); + let new_asset_state = asset_state .delta_update(&state_changes.asset) .ok_or(ArithmeticError::Overflow)?; diff --git a/pallets/omnipool/src/tests/add_liquidity_with_limit.rs b/pallets/omnipool/src/tests/add_liquidity_with_limit.rs new file mode 100644 index 000000000..743b91e0a --- /dev/null +++ b/pallets/omnipool/src/tests/add_liquidity_with_limit.rs @@ -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::::get(position_id).unwrap(); + + let expected = Position:: { + 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 = >::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::::get(position_id).unwrap(); + + let expected = Position:: { + 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::::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::::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::::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::::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::::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::::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::::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::::SlippageLimit + ); + }); +} diff --git a/pallets/omnipool/src/tests/mod.rs b/pallets/omnipool/src/tests/mod.rs index fbf6e52ae..ac220cf71 100644 --- a/pallets/omnipool/src/tests/mod.rs +++ b/pallets/omnipool/src/tests/mod.rs @@ -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; diff --git a/pallets/omnipool/src/tests/remove_liquidity_with_limit.rs b/pallets/omnipool/src/tests/remove_liquidity_with_limit.rs new file mode 100644 index 000000000..dfdf655a7 --- /dev/null +++ b/pallets/omnipool/src/tests/remove_liquidity_with_limit.rs @@ -0,0 +1,1117 @@ +use super::*; +use crate::types::Tradability; +use frame_support::assert_noop; +use orml_traits::MultiCurrencyExtended; +use sp_runtime::traits::One; +use sp_runtime::DispatchError::BadOrigin; + +#[test] +fn remove_liquidity_works() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let token_amount = 2000 * ONE; + + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_balance!(LP1, 1_000, 4600 * ONE); + + let liq_removed = 200 * ONE; + assert_ok!(Omnipool::remove_liquidity_with_limit( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_removed, + liq_removed + )); + + assert_pool_state!(11_930 * ONE, 23_860_000_000_000_000, SimpleImbalance::default()); + + assert_balance!(LP1, 1_000, 4600 * ONE + liq_removed); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: token_amount + liq_added - liq_removed, + hub_reserve: 1430000000000000, + shares: 2400 * ONE - liq_removed, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + let position = Positions::::get(current_position_id).unwrap(); + + let expected = Position:: { + asset_id: 1_000, + amount: liq_added - liq_removed, + shares: liq_added - liq_removed, + price: (1560 * ONE, 2400 * ONE), + }; + + assert_eq!(position, expected); + }); +} + +#[test] +fn full_liquidity_removal_works() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let token_amount = 2000 * ONE; + + let liq_added = 400 * ONE; + let lp1_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert!( + get_mock_minted_position(lp1_position_id).is_some(), + "Position instance was not minted" + ); + + let liq_removed = 400 * ONE; + + assert_ok!(Omnipool::remove_liquidity_with_limit( + RuntimeOrigin::signed(LP1), + lp1_position_id, + liq_removed, + liq_removed + )); + + assert!( + Positions::::get(lp1_position_id).is_none(), + "Position still found" + ); + + assert_pool_state!(11_800 * ONE, 23_600_000_000_000_000, SimpleImbalance::default()); + + assert_balance!(LP1, 1_000, 5000 * ONE); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: token_amount + liq_added - liq_removed, + hub_reserve: 1300000000000000, + shares: 2400 * ONE - liq_removed, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + assert!( + get_mock_minted_position(lp1_position_id).is_none(), + "Position instance was not burned" + ); + }); +} + +#[test] +fn partial_liquidity_removal_works() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let token_amount = 2000 * ONE; + let liq_added = 400 * ONE; + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert!( + get_mock_minted_position(current_position_id).is_some(), + "Position instance was not minted" + ); + + let liq_removed = 200 * ONE; + + assert_ok!(Omnipool::remove_liquidity_with_limit( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_removed, + liq_removed + )); + + assert!( + Positions::::get(current_position_id).is_some(), + "Position has been removed incorrectly" + ); + + assert_pool_state!(11_930 * ONE, 23_860_000_000_000_000, SimpleImbalance::default()); + + assert_balance!(LP1, 1_000, 4800 * ONE); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: token_amount + liq_added - liq_removed, + hub_reserve: 1430000000000000, + shares: 2400 * ONE - liq_removed, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + assert!( + get_mock_minted_position(current_position_id).is_some(), + "Position instance was burned" + ); + let position = Positions::::get(current_position_id).unwrap(); + + let expected = Position:: { + asset_id: 1_000, + amount: liq_added - liq_removed, + shares: liq_added - liq_removed, + price: (1560 * ONE, 2400 * ONE), + }; + + assert_eq!(position, expected); + }); +} + +#[test] +fn lp_receives_lrna_when_price_is_higher() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP3, 1_000, 100 * ONE), + (LP1, 1_000, 5000 * ONE), + (LP2, DAI, 50000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(1_000, FixedU128::from_float(0.65), LP3, 100 * ONE) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_ok!(Omnipool::buy( + RuntimeOrigin::signed(LP2), + 1_000, + DAI, + 200 * ONE, + 500000 * ONE + )); + + assert_balance!(Omnipool::protocol_account(), 1000, 300 * ONE); + let expected_state = AssetReserveState { + reserve: 300 * ONE, + hub_reserve: 541666666666667, + shares: 500000000000000, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + }; + assert_asset_state!(1_000, expected_state); + + assert_ok!(Omnipool::remove_liquidity_with_limit( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_added, + 240 * ONE + )); + assert_balance!(Omnipool::protocol_account(), 1000, 60 * ONE); + assert_balance!(LP1, 1000, 4_840_000_000_000_000); + assert_balance!(LP1, LRNA, 203_921_568_627_449); + + assert_pool_state!(10391666666666667, 64723183391003641, SimpleImbalance::default()); + }); +} + +#[test] +fn remove_liquiduity_should_burn_lrna_when_amount_is_below_ed() { + let asset_id = 1_000; + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP3, asset_id, 500 * ONE), + (LP1, asset_id, 2 * ONE), + (LP2, DAI, 50000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(asset_id, FixedU128::from_float(0.65), LP3, 500 * ONE) + .build() + .execute_with(|| { + let liq_added = 2 * ONE; + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), asset_id, liq_added)); + + assert_ok!(Omnipool::buy( + RuntimeOrigin::signed(LP2), + asset_id, + DAI, + 100_000_000_000, + 500000 * ONE + )); + + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_added + )); + assert_balance!(LP1, LRNA, 0); + let lrna_issuance = Tokens::total_issuance(LRNA); + assert!(lrna_issuance < 10826000000025799); // this value is when lrna is transferred + }); +} + +#[test] +fn remove_liquiduity_should_transfer_lrna_below_ed_when_lp_has_sufficient_lrna_amount() { + let asset_id = 1_000; + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP3, asset_id, 500 * ONE), + (LP1, asset_id, 2 * ONE), + (LP2, DAI, 50000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(asset_id, FixedU128::from_float(0.65), LP3, 500 * ONE) + .build() + .execute_with(|| { + let liq_added = 2 * ONE; + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), asset_id, liq_added)); + + assert_ok!(Omnipool::buy( + RuntimeOrigin::signed(LP2), + asset_id, + DAI, + 100_000_000_000, + 500000 * ONE + )); + + Tokens::update_balance(LRNA, &LP1, ONE as i128).unwrap(); + + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_added + )); + assert_balance!(LP1, LRNA, 1000259041538); + let lrna_issuance = Tokens::total_issuance(LRNA); + assert_eq!(lrna_issuance, 10826000000025799); + }); +} + +#[test] +fn protocol_shares_should_update_when_removing_asset_liquidity_after_price_change() { + let asset_a: AssetId = 1_000; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP3, asset_a, 100 * ONE), + (LP1, asset_a, 5000 * ONE), + (LP2, asset_a, 5000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(asset_a, FixedU128::from_float(0.65), LP3, 100 * ONE) + .build() + .execute_with(|| { + // Arrange + // - init pool + // - add asset_a with initial liquidity of 100 * ONE + // - add more liquidity of asset a - 400 * ONE + // - perform a sell so the price changes - adding 1000 * ONE of asset a + let liq_added = 400 * ONE; + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), asset_a, liq_added)); + + let expected_state = AssetReserveState:: { + reserve: 500000000000000, + hub_reserve: 325000000000000, + shares: 500000000000000, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + }; + assert_asset_state!(asset_a, expected_state); + + assert_ok!(Omnipool::sell( + RuntimeOrigin::signed(LP2), + asset_a, + HDX, + 100 * ONE, + 10 * ONE + )); + + // ACT + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + 400 * ONE + )); + + // Assert + // - check if balance of LP and protocol are correct + // - check new state of asset a in the pool ( should have updated protocol shares) + assert_balance!(Omnipool::protocol_account(), asset_a, 206557377049181); + assert_balance!(LP1, asset_a, 4993442622950819); + + assert_pool_state!(10647404371584700, 21294808743169400, SimpleImbalance::default()); + + let expected_state = AssetReserveState { + reserve: 206557377049181, + hub_reserve: 93237704918034, + shares: 172131147540984, + protocol_shares: 72131147540984, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + }; + assert_asset_state!(asset_a, expected_state); + }); +} + +#[test] +fn remove_liquidity_by_non_owner_fails() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP1, 1_000, 5000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(1_000, FixedU128::one(), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 500 * ONE)); + + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP3), current_position_id, 100 * ONE), + Error::::Forbidden + ); + }); +} + +#[test] +fn remove_liquidity_from_non_existing_position_fails() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP1, 1_000, 5000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(1_000, FixedU128::one(), LP2, 2000 * ONE) + .build() + .execute_with(|| { + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 500 * ONE)); + + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP1), 1_000_000, 100 * ONE), + Error::::Forbidden + ); + }); +} + +#[test] +fn remove_liquidity_cannot_exceed_position_shares() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP1, 1_000, 5000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(1_000, FixedU128::one(), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 500 * ONE)); + + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP1), current_position_id, 500 * ONE + 1), + Error::::InsufficientShares + ); + }); +} + +#[test] +fn remove_liquidity_should_fail_when_asset_is_not_allowed_to_remove() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE)); + + assert_ok!(Omnipool::set_asset_tradable_state( + RuntimeOrigin::root(), + 1000, + Tradability::BUY | Tradability::ADD_LIQUIDITY + )); + + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP1), current_position_id, 400 * ONE), + Error::::NotAllowed + ); + }); +} + +#[test] +fn remove_liquidity_should_fail_when_shares_amount_is_zero() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP1), current_position_id, 0u128), + Error::::InvalidSharesAmount + ); + }); +} + +#[test] +fn remove_liquidity_should_when_prices_differ_and_is_higher() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_max_allowed_price_difference(Permill::from_percent(1)) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE)); + + EXT_PRICE_ADJUSTMENT.with(|v| { + *v.borrow_mut() = (3, 100, false); + }); + + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP1), current_position_id, 200 * ONE,), + Error::::PriceDifferenceTooHigh + ); + }); +} +#[test] +fn remove_liquidity_should_when_prices_differ_and_is_lower() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_max_allowed_price_difference(Permill::from_percent(1)) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE)); + + EXT_PRICE_ADJUSTMENT.with(|v| { + *v.borrow_mut() = (3, 100, true); + }); + + assert_noop!( + Omnipool::remove_liquidity(RuntimeOrigin::signed(LP1), current_position_id, 200 * ONE,), + Error::::PriceDifferenceTooHigh + ); + }); +} + +#[test] +fn remove_liquidity_should_apply_min_fee_when_price_is_the_same() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_balance!(LP1, 1_000, 4600 * ONE); + + let liq_removed = 200 * ONE; + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_removed + )); + + assert_pool_state!(11931300000000000, 23862600000000000, SimpleImbalance::default()); + + assert_balance!(LP1, 1_000, 4798000000000000); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 2202000000000000, + hub_reserve: 1431300000000000, + shares: 2400 * ONE - liq_removed, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + let position = Positions::::get(current_position_id).unwrap(); + + let expected = Position:: { + asset_id: 1_000, + amount: liq_added - liq_removed, + shares: liq_added - liq_removed, + price: (1560 * ONE, 2400 * ONE), + }; + + assert_eq!(position, expected); + }); +} + +#[test] +fn remove_liquidity_should_apply_correct_fee_when_price_is_different() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .with_withdrawal_adjustment((5, 100, false)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_balance!(LP1, 1_000, 4600 * ONE); + + let liq_removed = 200 * ONE; + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_removed + )); + + assert_pool_state!(11936190476190477, 23872380952380954, SimpleImbalance::default()); + + assert_balance!(LP1, 1_000, 4790476190476190); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 2209523809523810, + hub_reserve: 1436190476190477, + shares: 2400 * ONE - liq_removed, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + let position = Positions::::get(current_position_id).unwrap(); + + let expected = Position:: { + asset_id: 1_000, + amount: liq_added - liq_removed, + shares: liq_added - liq_removed, + price: (1560 * ONE, 2400 * ONE), + }; + + assert_eq!(position, expected); + }); +} + +#[test] +fn safe_withdrawal_should_work_correctly_when_trading_is_disabled() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_ok!(Omnipool::set_asset_tradable_state( + RuntimeOrigin::root(), + 1_000, + Tradability::ADD_LIQUIDITY | Tradability::REMOVE_LIQUIDITY + )); + + let position = Positions::::get(current_position_id).unwrap(); + + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + position.shares, + )); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 2004000000000000, + hub_reserve: 1302600000000000, + shares: 2000000000000000, + protocol_shares: Balance::zero(), + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::ADD_LIQUIDITY | Tradability::REMOVE_LIQUIDITY + } + ); + + let position = Positions::::get(current_position_id); + assert!(position.is_none()); + + assert_balance!(LP1, 1_000, 4996000000000000); + assert_balance!(LP1, LRNA, 0); + }); +} + +#[test] +fn safe_withdrawal_should_transfer_lrna() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP2, DAI, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_ok!(Omnipool::buy( + RuntimeOrigin::signed(LP2), + 1_000, + DAI, + 200 * ONE, + 500000 * ONE + )); + assert_ok!(Omnipool::set_asset_tradable_state( + RuntimeOrigin::root(), + 1_000, + Tradability::ADD_LIQUIDITY | Tradability::REMOVE_LIQUIDITY + )); + + let position = Positions::::get(current_position_id).unwrap(); + + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + position.shares, + )); + + let position = Positions::::get(current_position_id); + assert!(position.is_none()); + + assert_balance!(LP1, 1_000, 4962999999999999); + assert_balance!(LP1, LRNA, 24371320754716); + }); +} + +#[test] +fn withdraw_protocol_liquidity_should_work_correctly() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP2, DAI, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + let position = Positions::::get(current_position_id).unwrap(); + assert_ok!(Omnipool::sacrifice_position( + RuntimeOrigin::signed(LP1), + current_position_id + )); + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 2400000000000000, + hub_reserve: 1560000000000000, + shares: 2400000000000000, + protocol_shares: 400000000000000, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + assert_ok!(Omnipool::withdraw_protocol_liquidity( + RuntimeOrigin::root(), + 1000, + position.shares, + position.price, + 1234, + )); + assert_balance!(1234, 1_000, 400 * ONE); + assert_balance!(1234, LRNA, 0); + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 2000000000000000, + hub_reserve: 1300000000000000, + shares: 2000000000000000, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + }); +} + +#[test] +fn withdraw_protocol_liquidity_should_transfer_lrna_when_price_is_different() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP2, DAI, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + let position = Positions::::get(current_position_id).unwrap(); + assert_ok!(Omnipool::sacrifice_position( + RuntimeOrigin::signed(LP1), + current_position_id + )); + + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 2400000000000000, + hub_reserve: 1560000000000000, + shares: 2400000000000000, + protocol_shares: 400000000000000, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + + assert_ok!(Omnipool::buy( + RuntimeOrigin::signed(LP2), + 1_000, + DAI, + 200 * ONE, + 500000 * ONE + )); + + assert_ok!(Omnipool::withdraw_protocol_liquidity( + RuntimeOrigin::root(), + 1000, + position.shares, + position.price, + 1234, + )); + + let position = Positions::::get(current_position_id); + assert!(position.is_none()); + + assert_balance!(1234, 1_000, 366666666666666); + assert_balance!(1234, LRNA, 24617495711835); + assert_asset_state!( + 1_000, + AssetReserveState { + reserve: 1_833_333_333_333_334, + hub_reserve: 1418181818181819, + shares: 2000000000000000, + protocol_shares: 0, + cap: DEFAULT_WEIGHT_CAP, + tradable: Tradability::default(), + } + ); + }); +} + +#[test] +fn withdraw_protocol_liquidity_fail_when_not_root() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP2, DAI, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + let position = Positions::::get(current_position_id).unwrap(); + assert_ok!(Omnipool::sacrifice_position( + RuntimeOrigin::signed(LP1), + current_position_id + )); + + assert_noop!( + Omnipool::withdraw_protocol_liquidity( + RuntimeOrigin::signed(LP1), + 1000, + position.shares, + position.price, + 1234, + ), + BadOrigin + ); + }); +} + +#[test] +fn withdraw_protocol_liquidity_fail_when_withdrawing_more_protocol_shares() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (LP2, DAI, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_min_withdrawal_fee(Permill::from_float(0.01)) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + let position = Positions::::get(current_position_id).unwrap(); + assert_ok!(Omnipool::sacrifice_position( + RuntimeOrigin::signed(LP1), + current_position_id + )); + + let state = Assets::::get(1_000).unwrap(); + + assert_noop!( + Omnipool::withdraw_protocol_liquidity( + RuntimeOrigin::root(), + 1000, + state.protocol_shares + 1, + position.price, + 1234, + ), + Error::::InsufficientShares + ); + }); +} + +#[test] +fn remove_liquidity_should_skip_price_check_when_price_is_higher_and_is_safe_to_withdraw() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_max_allowed_price_difference(Permill::from_percent(1)) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE)); + + EXT_PRICE_ADJUSTMENT.with(|v| { + *v.borrow_mut() = (3, 100, false); + }); + assert_ok!(Omnipool::set_asset_tradable_state( + RuntimeOrigin::root(), + 1_000, + Tradability::ADD_LIQUIDITY | Tradability::REMOVE_LIQUIDITY + )); + + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + 200 * ONE, + ),); + }); +} + +#[test] +fn remove_liquidity_should_skip_price_check_when_price_is_lower_and_is_safe_to_withdraw() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP2, 1_000, 2000 * ONE), + (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), LP2, 2000 * ONE) + .with_max_allowed_price_difference(Permill::from_percent(1)) + .build() + .execute_with(|| { + let current_position_id = >::get(); + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, 400 * ONE)); + + EXT_PRICE_ADJUSTMENT.with(|v| { + *v.borrow_mut() = (3, 100, true); + }); + + assert_ok!(Omnipool::set_asset_tradable_state( + RuntimeOrigin::root(), + 1_000, + Tradability::ADD_LIQUIDITY | Tradability::REMOVE_LIQUIDITY + )); + assert_ok!(Omnipool::remove_liquidity( + RuntimeOrigin::signed(LP1), + current_position_id, + 200 * ONE, + ),); + }); +} + +#[test] +fn remove_liquidity_should_fail_when_min_limit_not_reached() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (Omnipool::protocol_account(), DAI, 1000 * ONE), + (Omnipool::protocol_account(), HDX, NATIVE_AMOUNT), + (LP3, 1_000, 100 * ONE), + (LP1, 1_000, 5000 * ONE), + (LP2, DAI, 50000 * ONE), + ]) + .with_initial_pool(FixedU128::from_float(0.5), FixedU128::from(1)) + .with_token(1_000, FixedU128::from_float(0.65), LP3, 100 * ONE) + .build() + .execute_with(|| { + let liq_added = 400 * ONE; + + let current_position_id = >::get(); + + assert_ok!(Omnipool::add_liquidity(RuntimeOrigin::signed(LP1), 1_000, liq_added)); + + assert_ok!(Omnipool::buy( + RuntimeOrigin::signed(LP2), + 1_000, + DAI, + 200 * ONE, + 500000 * ONE + )); + + assert_noop!( + Omnipool::remove_liquidity_with_limit( + RuntimeOrigin::signed(LP1), + current_position_id, + liq_added, + 250 * ONE // user would receive 240, which is below limit + ), + Error::::SlippageLimit + ); + }); +}