-
Notifications
You must be signed in to change notification settings - Fork 6
/
lib.rs
2034 lines (1739 loc) · 64 KB
/
lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (C) 2020-2022 Intergalactic, Limited (GIB).
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Omnipool pallet
//!
//! Omnipool implementation
//!
//! ## Overview
//!
//! Omnipool is type of AMM where all assets are pooled together into one single pool.
//!
//! Each asset is internally paired with so called Hub Asset ( LRNA ). When a liquidity is provided, corresponding
//! amount of hub asset is minted. When a liquidity is removed, corresponding amount of hub asset is burned.
//!
//! Liquidity provider can provide any asset of their choice to the Omnipool and in return
//! they will receive pool shares for this single asset.
//!
//! The position is represented as a NFT token which stores the amount of shares distributed
//! and the price of the asset at the time of provision.
//!
//! For traders this means that they can benefit from non-fragmented liquidity.
//! They can send any token to the pool using the swap mechanism
//! and in return they will receive the token of their choice in the appropriate quantity.
//!
//! Omnipool is implemented with concrete Balance type: u128.
//!
//! ### Imbalance mechanism
//! The Imbalance mechanism is designed to stabilize the value of LRNA. By design it is a weak and passive mechanism,
//! and is specifically meant to deal with one cause of LRNA volatility: LRNA being sold back to the pool.
//!
//! Imbalance is always negative, internally represented by a special type `SimpleImbalance` which uses unsigned integer and boolean flag.
//! This was done initially because of the intention that in future imbalance can also become positive.
//!
//! ### Omnipool Hooks
//!
//! Omnipool pallet supports multiple hooks which are triggerred on certain operations:
//! - on_liquidity_changed - called when liquidity is added or removed from the pool
//! - on_trade - called when trade is executed
//! - on_trade_fee - called after successful trade with fee amount that can be taken out of the pool if needed.
//!
//! This is currently used to update on-chain oracle and in the circuit breaker.
//!
//! ## Terminology
//!
//! * **LP:** liquidity provider
//! * **Position:** a moment when LP added liquidity to the pool. It stores amount,shares and price at the time
//! of provision
//! * **Hub Asset:** dedicated 'hub' token for trade executions (LRNA)
//! * **Native Asset:** governance token
//! * **Imbalance:** Tracking of hub asset imbalance.
//!
//! ## Assumptions
//!
//! Below are assumptions that must be held when using this pallet.
//!
//! * Initial liquidity of new token being added to Omnipool must be transferred manually to pool account prior to calling add_token.
//! * All tokens added to the pool must be first registered in Asset Registry.
//!
//! ## Interface
//!
//! ### Dispatchable Functions
//!
//! * `add_token` - Adds token to the pool. Initial liquidity must be transffered to pool account prior to calling add_token.
//! * `add_liquidity` - Adds liquidity of selected asset to the pool. Mints corresponding position NFT.
//! * `remove_liquidity` - Removes liquidity of selected position from the pool. Partial withdrawals are allowed.
//! * `sell` - Trades an asset in for asset out by selling given amount of asset in.
//! * `buy` - Trades an asset in for asset out by buying given amount of asset out.
//! * `set_asset_tradable_state` - Updates asset's tradable state with new flags. This allows/forbids asset operation such SELL,BUY,ADD or REMOVE liquidtityy.
//! * `refund_refused_asset` - Refunds the initial liquidity amount sent to pool account prior to add_token if the token has been refused to be added.
//! * `sacrifice_position` - Destroys a position and position's shares become protocol's shares.
//! * `withdraw_protocol_liquidity` - Withdraws protocol's liquidity from the pool. Used to withdraw liquidity from sacrificed position.
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::pallet_prelude::{DispatchResult, Get};
use frame_support::require_transactional;
use frame_support::PalletId;
use frame_support::{ensure, transactional};
use sp_runtime::traits::{AccountIdConversion, AtLeast32BitUnsigned, One};
use sp_runtime::traits::{CheckedAdd, CheckedSub, Zero};
use sp_std::ops::{Add, Sub};
use sp_std::prelude::*;
use frame_support::traits::tokens::nonfungibles::{Create, Inspect, Mutate};
use hydra_dx_math::omnipool::types::{AssetStateChange, BalanceUpdate, I129};
use hydradx_traits::Registry;
use orml_traits::{GetByKey, MultiCurrency};
use scale_info::TypeInfo;
use sp_runtime::{ArithmeticError, DispatchError, FixedPointNumber, FixedU128, Permill};
#[cfg(test)]
mod tests;
pub mod provider;
pub mod router_execution;
pub mod traits;
pub mod types;
pub mod weights;
use crate::traits::{AssetInfo, OmnipoolHooks};
use crate::types::{AssetReserveState, AssetState, Balance, Position, SimpleImbalance, Tradability};
pub use pallet::*;
pub use weights::WeightInfo;
/// NFT class id type of provided nft implementation
pub type NFTCollectionIdOf<T> =
<<T as Config>::NFTHandler as Inspect<<T as frame_system::Config>::AccountId>>::CollectionId;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use crate::traits::{AssetInfo, ExternalPriceProvider, OmnipoolHooks, ShouldAllow};
use crate::types::{Position, Price, Tradability};
use codec::HasCompact;
use frame_support::pallet_prelude::*;
use frame_support::traits::DefensiveOption;
use frame_system::pallet_prelude::*;
use hydra_dx_math::ema::EmaPrice;
use hydra_dx_math::omnipool::types::{BalanceUpdate, I129};
use orml_traits::GetByKey;
use sp_runtime::ArithmeticError;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Asset type.
type AssetId: Member
+ Parameter
+ Default
+ Copy
+ HasCompact
+ MaybeSerializeDeserialize
+ MaxEncodedLen
+ TypeInfo;
/// Multi currency mechanism
type Currency: MultiCurrency<Self::AccountId, CurrencyId = Self::AssetId, Balance = Balance>;
/// Origin that can add token, refund refused asset and withdraw protocol liquidity.
type AuthorityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Origin that can change asset's tradability and weight.
type TechnicalOrigin: EnsureOrigin<Self::RuntimeOrigin>;
/// Asset Registry mechanism - used to check if asset is correctly registered in asset registry
type AssetRegistry: Registry<Self::AssetId, Vec<u8>, Balance, DispatchError>;
/// Native Asset ID
#[pallet::constant]
type HdxAssetId: Get<Self::AssetId>;
/// Hub Asset ID
#[pallet::constant]
type HubAssetId: Get<Self::AssetId>;
/// Dynamic fee support - returns (Asset Fee, Protocol Fee) for given asset
type Fee: GetByKey<Self::AssetId, (Permill, Permill)>;
/// Minimum withdrawal fee
#[pallet::constant]
type MinWithdrawalFee: Get<Permill>;
/// Minimum trading limit
#[pallet::constant]
type MinimumTradingLimit: Get<Balance>;
/// Minimum pool liquidity which can be added
#[pallet::constant]
type MinimumPoolLiquidity: Get<Balance>;
/// Max fraction of asset reserve to sell in single transaction
#[pallet::constant]
type MaxInRatio: Get<u128>;
/// Max fraction of asset reserve to buy in single transaction
#[pallet::constant]
type MaxOutRatio: Get<u128>;
/// Position identifier type
type PositionItemId: Member + Parameter + Default + Copy + HasCompact + AtLeast32BitUnsigned + MaxEncodedLen;
/// Collection id type
type CollectionId: TypeInfo + MaxEncodedLen;
/// Non fungible class id
#[pallet::constant]
type NFTCollectionId: Get<NFTCollectionIdOf<Self>>;
/// Non fungible handling - mint,burn, check owner
type NFTHandler: Mutate<Self::AccountId>
+ Create<Self::AccountId>
+ Inspect<Self::AccountId, ItemId = Self::PositionItemId, CollectionId = Self::CollectionId>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// Hooks are actions executed on add_liquidity, sell or buy.
type OmnipoolHooks: OmnipoolHooks<
Self::RuntimeOrigin,
Self::AccountId,
Self::AssetId,
Balance,
Error = DispatchError,
>;
/// Safety mechanism when adding and removing liquidity. Determines how much price can change between spot price and oracle price.
type PriceBarrier: ShouldAllow<Self::AccountId, Self::AssetId, EmaPrice>;
/// Oracle price provider. Provides price for given asset. Used in remove liquidity to support calculation of dynamic withdrawal fee.
type ExternalPriceOracle: ExternalPriceProvider<Self::AssetId, EmaPrice, Error = DispatchError>;
}
#[pallet::storage]
/// State of an asset in the omnipool
#[pallet::getter(fn assets)]
pub(super) type Assets<T: Config> = StorageMap<_, Blake2_128Concat, T::AssetId, AssetState<Balance>>;
#[pallet::storage]
/// Imbalance of hub asset
#[pallet::getter(fn current_imbalance)]
pub(super) type HubAssetImbalance<T: Config> = StorageValue<_, SimpleImbalance<Balance>, ValueQuery>;
// LRNA is only allowed to be sold
#[pallet::type_value]
pub fn DefaultHubAssetTradability() -> Tradability {
Tradability::SELL
}
#[pallet::storage]
/// Tradable state of hub asset.
pub(super) type HubAssetTradability<T: Config> =
StorageValue<_, Tradability, ValueQuery, DefaultHubAssetTradability>;
#[pallet::storage]
/// LP positions. Maps NFT instance id to corresponding position
#[pallet::getter(fn positions)]
pub(super) type Positions<T: Config> =
StorageMap<_, Blake2_128Concat, T::PositionItemId, Position<Balance, T::AssetId>>;
#[pallet::storage]
#[pallet::getter(fn next_position_id)]
/// Position ids sequencer
pub(super) type NextPositionId<T: Config> = StorageValue<_, T::PositionItemId, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
/// An asset was added to Omnipool
TokenAdded {
asset_id: T::AssetId,
initial_amount: Balance,
initial_price: Price,
},
/// An asset was removed from Omnipool
TokenRemoved {
asset_id: T::AssetId,
amount: Balance,
hub_withdrawn: Balance,
},
/// Liquidity of an asset was added to Omnipool.
LiquidityAdded {
who: T::AccountId,
asset_id: T::AssetId,
amount: Balance,
position_id: T::PositionItemId,
},
/// Liquidity of an asset was removed from Omnipool.
LiquidityRemoved {
who: T::AccountId,
position_id: T::PositionItemId,
asset_id: T::AssetId,
shares_removed: Balance,
fee: FixedU128,
},
/// PRotocol Liquidity was removed from Omnipool.
ProtocolLiquidityRemoved {
who: T::AccountId,
asset_id: T::AssetId,
amount: Balance,
hub_amount: Balance,
shares_removed: Balance,
},
/// Sell trade executed.
SellExecuted {
who: T::AccountId,
asset_in: T::AssetId,
asset_out: T::AssetId,
amount_in: Balance,
amount_out: Balance,
hub_amount_in: Balance,
hub_amount_out: Balance,
asset_fee_amount: Balance,
protocol_fee_amount: Balance,
},
/// Buy trade executed.
BuyExecuted {
who: T::AccountId,
asset_in: T::AssetId,
asset_out: T::AssetId,
amount_in: Balance,
amount_out: Balance,
hub_amount_in: Balance,
hub_amount_out: Balance,
asset_fee_amount: Balance,
protocol_fee_amount: Balance,
},
/// LP Position was created and NFT instance minted.
PositionCreated {
position_id: T::PositionItemId,
owner: T::AccountId,
asset: T::AssetId,
amount: Balance,
shares: Balance,
price: Price,
},
/// LP Position was destroyed and NFT instance burned.
PositionDestroyed {
position_id: T::PositionItemId,
owner: T::AccountId,
},
/// LP Position was updated.
PositionUpdated {
position_id: T::PositionItemId,
owner: T::AccountId,
asset: T::AssetId,
amount: Balance,
shares: Balance,
price: Price,
},
/// Asset's tradable state has been updated.
TradableStateUpdated { asset_id: T::AssetId, state: Tradability },
/// Amount has been refunded for asset which has not been accepted to add to omnipool.
AssetRefunded {
asset_id: T::AssetId,
amount: Balance,
recipient: T::AccountId,
},
/// Asset's weight cap has been updated.
AssetWeightCapUpdated { asset_id: T::AssetId, cap: Permill },
}
#[pallet::error]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum Error<T> {
/// Balance too low
InsufficientBalance,
/// Asset is already in omnipool
AssetAlreadyAdded,
/// Asset is not in omnipool
AssetNotFound,
/// Failed to add token to Omnipool due to insufficient initial liquidity.
MissingBalance,
/// Invalid initial asset price.
InvalidInitialAssetPrice,
/// Slippage protection - minimum limit has not been reached.
BuyLimitNotReached,
/// Slippage protection - maximum limit has been exceeded.
SellLimitExceeded,
/// Position has not been found.
PositionNotFound,
/// Insufficient shares in position
InsufficientShares,
/// Asset is not allowed to be traded.
NotAllowed,
/// Signed account is not owner of position instance.
Forbidden,
/// Asset weight cap has been exceeded.
AssetWeightCapExceeded,
/// Asset is not registered in asset registry
AssetNotRegistered,
/// Provided liquidity is below minimum allowed limit
InsufficientLiquidity,
/// Traded amount is below minimum allowed limit
InsufficientTradingAmount,
/// Sell or buy with same asset ids is not allowed.
SameAssetTradeNotAllowed,
/// LRNA update after trade results in positive value.
HubAssetUpdateError,
/// Imbalance results in positive value.
PositiveImbalance,
/// Amount of shares provided cannot be 0.
InvalidSharesAmount,
/// Hub asset is only allowed to be sold.
InvalidHubAssetTradableState,
/// Asset is not allowed to be refunded.
AssetRefundNotAllowed,
/// Max fraction of asset to buy has been exceeded.
MaxOutRatioExceeded,
/// Max fraction of asset to sell has been exceeded.
MaxInRatioExceeded,
/// Max allowed price difference has been exceeded.
PriceDifferenceTooHigh,
/// Invalid oracle price - division by zero.
InvalidOraclePrice,
/// Failed to calculate withdrawal fee.
InvalidWithdrawalFee,
/// More than allowed amount of fee has been transferred.
FeeOverdraft,
/// Token cannot be removed from Omnipool due to shares still owned by other users.
SharesRemaining,
/// Token cannot be removed from Omnipool because asset is not frozen.
AssetNotFrozen,
/// Calculated amount out from sell trade is zero.
ZeroAmountOut,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Add new token to omnipool in quantity `amount` at price `initial_price`
///
/// Initial liquidity must be transferred to pool's account for this new token manually prior to calling `add_token`.
///
/// Initial liquidity is pool's account balance of the token.
///
/// Position NFT token is minted for `position_owner`.
///
/// Parameters:
/// - `asset`: The identifier of the new asset added to the pool. Must be registered in Asset registry
/// - `initial_price`: Initial price
/// - `position_owner`: account id for which share are distributed in form on NFT
/// - `weight_cap`: asset weight cap
///
/// Emits `TokenAdded` event when successful.
///
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::add_token().saturating_add(T::OmnipoolHooks::on_liquidity_changed_weight()))]
#[transactional]
pub fn add_token(
origin: OriginFor<T>,
asset: T::AssetId,
initial_price: Price,
weight_cap: Permill,
position_owner: T::AccountId,
) -> DispatchResult {
T::AuthorityOrigin::ensure_origin(origin.clone())?;
ensure!(!Assets::<T>::contains_key(asset), Error::<T>::AssetAlreadyAdded);
ensure!(T::AssetRegistry::exists(asset), Error::<T>::AssetNotRegistered);
ensure!(initial_price > FixedU128::zero(), Error::<T>::InvalidInitialAssetPrice);
// ensure collection is created, we can simply ignore the error if it was already created.
let _ = T::NFTHandler::create_collection(
&T::NFTCollectionId::get(),
&Self::protocol_account(),
&Self::protocol_account(),
);
let amount = T::Currency::free_balance(asset, &Self::protocol_account());
ensure!(
amount >= T::MinimumPoolLiquidity::get() && amount > 0,
Error::<T>::MissingBalance
);
let hub_reserve = initial_price.checked_mul_int(amount).ok_or(ArithmeticError::Overflow)?;
// Initial state of asset
let state = AssetState::<Balance> {
hub_reserve,
shares: amount,
protocol_shares: Balance::zero(),
cap: FixedU128::from(weight_cap).into_inner(),
tradable: Tradability::default(),
};
let lp_position = Position::<Balance, T::AssetId> {
asset_id: asset,
amount,
shares: amount,
price: (initial_price.into_inner(), FixedU128::DIV),
};
let instance_id = Self::create_and_mint_position_instance(&position_owner)?;
<Positions<T>>::insert(instance_id, lp_position);
Self::deposit_event(Event::PositionCreated {
position_id: instance_id,
owner: position_owner,
asset,
amount,
shares: amount,
price: initial_price,
});
let current_imbalance = <HubAssetImbalance<T>>::get();
let current_hub_asset_liquidity =
T::Currency::free_balance(T::HubAssetId::get(), &Self::protocol_account());
let delta_imbalance = hydra_dx_math::omnipool::calculate_delta_imbalance(
hub_reserve,
I129 {
value: current_imbalance.value,
negative: current_imbalance.negative,
},
current_hub_asset_liquidity,
)
.ok_or(ArithmeticError::Overflow)?;
Self::update_imbalance(BalanceUpdate::Decrease(delta_imbalance))?;
let delta_hub_reserve = BalanceUpdate::Increase(hub_reserve);
Self::update_hub_asset_liquidity(&delta_hub_reserve)?;
let reserve = T::Currency::free_balance(asset, &Self::protocol_account());
let reserve_state: AssetReserveState<_> = (state.clone(), reserve).into();
let changes = AssetStateChange {
delta_hub_reserve,
delta_reserve: BalanceUpdate::Increase(reserve),
delta_shares: BalanceUpdate::Increase(amount),
delta_protocol_shares: BalanceUpdate::Increase(Balance::zero()),
};
T::OmnipoolHooks::on_liquidity_changed(
origin,
AssetInfo::new(asset, &AssetReserveState::default(), &reserve_state, &changes, false),
)?;
<Assets<T>>::insert(asset, state);
Self::deposit_event(Event::TokenAdded {
asset_id: asset,
initial_amount: amount,
initial_price,
});
Ok(())
}
/// Add liquidity of asset `asset` in quantity `amount` to Omnipool
///
/// `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
///
/// Emits `LiquidityAdded` event when successful.
///
#[pallet::call_index(2)]
#[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(origin: OriginFor<T>, asset: T::AssetId, amount: Balance) -> DispatchResult {
let who = ensure_signed(origin.clone())?;
ensure!(
amount >= T::MinimumPoolLiquidity::get(),
Error::<T>::InsufficientLiquidity
);
ensure!(
T::Currency::ensure_can_withdraw(asset, &who, amount).is_ok(),
Error::<T>::InsufficientBalance
);
let asset_state = Self::load_asset_state(asset)?;
ensure!(
asset_state.tradable.contains(Tradability::ADD_LIQUIDITY),
Error::<T>::NotAllowed
);
T::PriceBarrier::ensure_price(
&who,
T::HubAssetId::get(),
asset,
EmaPrice::new(asset_state.hub_reserve, asset_state.reserve),
)
.map_err(|_| Error::<T>::PriceDifferenceTooHigh)?;
let current_imbalance = <HubAssetImbalance<T>>::get();
let current_hub_asset_liquidity =
T::Currency::free_balance(T::HubAssetId::get(), &Self::protocol_account());
//
// Calculate add liquidity state changes
//
let state_changes = hydra_dx_math::omnipool::calculate_add_liquidity_state_changes(
&(&asset_state).into(),
amount,
I129 {
value: current_imbalance.value,
negative: current_imbalance.negative,
},
current_hub_asset_liquidity,
)
.ok_or(ArithmeticError::Overflow)?;
let new_asset_state = asset_state
.clone()
.delta_update(&state_changes.asset)
.ok_or(ArithmeticError::Overflow)?;
let hub_reserve_ratio = FixedU128::checked_from_rational(
new_asset_state.hub_reserve,
T::Currency::free_balance(T::HubAssetId::get(), &Self::protocol_account())
.checked_add(*state_changes.asset.delta_hub_reserve)
.ok_or(ArithmeticError::Overflow)?,
)
.ok_or(ArithmeticError::DivisionByZero)?;
ensure!(
hub_reserve_ratio <= new_asset_state.weight_cap(),
Error::<T>::AssetWeightCapExceeded
);
// Create LP position with given shares
let lp_position = Position::<Balance, T::AssetId> {
asset_id: asset,
amount,
shares: *state_changes.asset.delta_shares,
// Note: position needs price after asset state is updated.
price: (new_asset_state.hub_reserve, new_asset_state.reserve),
};
let instance_id = Self::create_and_mint_position_instance(&who)?;
<Positions<T>>::insert(instance_id, lp_position);
Self::deposit_event(Event::PositionCreated {
position_id: instance_id,
owner: who.clone(),
asset,
amount,
shares: *state_changes.asset.delta_shares,
price: new_asset_state.price().ok_or(ArithmeticError::DivisionByZero)?,
});
T::Currency::transfer(
asset,
&who,
&Self::protocol_account(),
*state_changes.asset.delta_reserve,
)?;
debug_assert_eq!(*state_changes.asset.delta_reserve, amount);
// Callback hook info
let info: AssetInfo<T::AssetId, Balance> =
AssetInfo::new(asset, &asset_state, &new_asset_state, &state_changes.asset, false);
Self::update_imbalance(state_changes.delta_imbalance)?;
Self::update_hub_asset_liquidity(&state_changes.asset.delta_hub_reserve)?;
Self::set_asset_state(asset, new_asset_state);
Self::deposit_event(Event::LiquidityAdded {
who,
asset_id: asset,
amount,
position_id: instance_id,
});
T::OmnipoolHooks::on_liquidity_changed(origin, info)?;
Ok(())
}
/// Remove liquidity of asset `asset` in quantity `amount` from Omnipool
///
/// `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
///
/// Emits `LiquidityRemoved` event when successful.
///
#[pallet::call_index(3)]
#[pallet::weight(<T as Config>::WeightInfo::remove_liquidity().saturating_add(T::OmnipoolHooks::on_liquidity_changed_weight()))]
#[transactional]
pub fn remove_liquidity(
origin: OriginFor<T>,
position_id: T::PositionItemId,
amount: Balance,
) -> DispatchResult {
let who = ensure_signed(origin.clone())?;
ensure!(amount > Balance::zero(), Error::<T>::InvalidSharesAmount);
ensure!(
T::NFTHandler::owner(&T::NFTCollectionId::get(), &position_id) == Some(who.clone()),
Error::<T>::Forbidden
);
let position = Positions::<T>::get(position_id).ok_or(Error::<T>::PositionNotFound)?;
ensure!(position.shares >= amount, Error::<T>::InsufficientShares);
let asset_id = position.asset_id;
let asset_state = Self::load_asset_state(asset_id)?;
ensure!(
asset_state.tradable.contains(Tradability::REMOVE_LIQUIDITY),
Error::<T>::NotAllowed
);
let safe_withdrawal = asset_state.tradable.is_safe_withdrawal();
// Skip price check if safe withdrawal - trading disabled.
if !safe_withdrawal {
T::PriceBarrier::ensure_price(
&who,
T::HubAssetId::get(),
asset_id,
EmaPrice::new(asset_state.hub_reserve, asset_state.reserve),
)
.map_err(|_| Error::<T>::PriceDifferenceTooHigh)?;
}
let ext_asset_price = T::ExternalPriceOracle::get_price(T::HubAssetId::get(), asset_id)?;
if ext_asset_price.is_zero() {
return Err(Error::<T>::InvalidOraclePrice.into());
}
let withdrawal_fee = hydra_dx_math::omnipool::calculate_withdrawal_fee(
asset_state.price().ok_or(ArithmeticError::DivisionByZero)?,
FixedU128::checked_from_rational(ext_asset_price.n, ext_asset_price.d)
.defensive_ok_or(Error::<T>::InvalidOraclePrice)?,
T::MinWithdrawalFee::get(),
);
let current_imbalance = <HubAssetImbalance<T>>::get();
let current_hub_asset_liquidity =
T::Currency::free_balance(T::HubAssetId::get(), &Self::protocol_account());
//
// calculate state changes of remove liquidity
//
let state_changes = hydra_dx_math::omnipool::calculate_remove_liquidity_state_changes(
&(&asset_state).into(),
amount,
&(&position).into(),
I129 {
value: current_imbalance.value,
negative: current_imbalance.negative,
},
current_hub_asset_liquidity,
withdrawal_fee,
)
.ok_or(ArithmeticError::Overflow)?;
let new_asset_state = asset_state
.clone()
.delta_update(&state_changes.asset)
.ok_or(ArithmeticError::Overflow)?;
// Update position state
let updated_position = position
.delta_update(
&state_changes.delta_position_reserve,
&state_changes.delta_position_shares,
)
.ok_or(ArithmeticError::Overflow)?;
T::Currency::transfer(
asset_id,
&Self::protocol_account(),
&who,
*state_changes.asset.delta_reserve,
)?;
Self::update_imbalance(state_changes.delta_imbalance)?;
// burn only difference between delta hub and lp hub amount.
Self::update_hub_asset_liquidity(
&state_changes
.asset
.delta_hub_reserve
.merge(BalanceUpdate::Increase(state_changes.lp_hub_amount))
.ok_or(ArithmeticError::Overflow)?,
)?;
// LP receives some hub asset
Self::process_hub_amount(state_changes.lp_hub_amount, &who)?;
if updated_position.shares == Balance::zero() {
// All liquidity removed, remove position and burn NFT instance
<Positions<T>>::remove(position_id);
T::NFTHandler::burn(&T::NFTCollectionId::get(), &position_id, Some(&who))?;
Self::deposit_event(Event::PositionDestroyed {
position_id,
owner: who.clone(),
});
} else {
Self::deposit_event(Event::PositionUpdated {
position_id,
owner: who.clone(),
asset: asset_id,
amount: updated_position.amount,
shares: updated_position.shares,
price: updated_position
.price_from_rational()
.ok_or(ArithmeticError::DivisionByZero)?,
});
<Positions<T>>::insert(position_id, updated_position);
}
// Callback hook info
let info: AssetInfo<T::AssetId, Balance> = AssetInfo::new(
asset_id,
&asset_state,
&new_asset_state,
&state_changes.asset,
safe_withdrawal,
);
Self::set_asset_state(asset_id, new_asset_state);
Self::deposit_event(Event::LiquidityRemoved {
who,
position_id,
asset_id,
shares_removed: amount,
fee: withdrawal_fee,
});
T::OmnipoolHooks::on_liquidity_changed(origin, info)?;
Ok(())
}
/// Sacrifice LP position in favor of pool.
///
/// A position is destroyed and liquidity owned by LP becomes pool owned liquidity.
///
/// Only owner of position can perform this action.
///
/// Emits `PositionDestroyed`.
#[pallet::call_index(4)]
#[pallet::weight(<T as Config>::WeightInfo::sacrifice_position())]
#[transactional]
pub fn sacrifice_position(origin: OriginFor<T>, position_id: T::PositionItemId) -> DispatchResult {
let who = ensure_signed(origin)?;
let position = Positions::<T>::get(position_id).ok_or(Error::<T>::PositionNotFound)?;
ensure!(
T::NFTHandler::owner(&T::NFTCollectionId::get(), &position_id) == Some(who.clone()),
Error::<T>::Forbidden
);
Assets::<T>::try_mutate(position.asset_id, |maybe_asset| -> DispatchResult {
let asset_state = maybe_asset.as_mut().ok_or(Error::<T>::AssetNotFound)?;
asset_state.protocol_shares = asset_state
.protocol_shares
.checked_add(position.shares)
.ok_or(ArithmeticError::Overflow)?;
Ok(())
})?;
// Destroy position and burn NFT
<Positions<T>>::remove(position_id);
T::NFTHandler::burn(&T::NFTCollectionId::get(), &position_id, Some(&who))?;
Self::deposit_event(Event::PositionDestroyed {
position_id,
owner: who,
});
Ok(())
}
/// Execute a swap of `asset_in` for `asset_out`.
///
/// Price is determined by the Omnipool.
///
/// Hub asset is traded separately.
///
/// Asset's tradable states must contain SELL flag for asset_in and BUY flag for asset_out, otherwise `NotAllowed` error is returned.
///
/// Parameters:
/// - `asset_in`: ID of asset sold to the pool
/// - `asset_out`: ID of asset bought from the pool
/// - `amount`: Amount of asset sold
/// - `min_buy_amount`: Minimum amount required to receive
///
/// Emits `SellExecuted` event when successful.
///
#[pallet::call_index(5)]
#[pallet::weight(<T as Config>::WeightInfo::sell()
.saturating_add(T::OmnipoolHooks::on_trade_weight())
.saturating_add(T::OmnipoolHooks::on_liquidity_changed_weight())
)]
#[transactional]
pub fn sell(
origin: OriginFor<T>,
asset_in: T::AssetId,
asset_out: T::AssetId,
amount: Balance,
min_buy_amount: Balance,
) -> DispatchResult {
let who = ensure_signed(origin.clone())?;
ensure!(asset_in != asset_out, Error::<T>::SameAssetTradeNotAllowed);
ensure!(
amount >= T::MinimumTradingLimit::get(),
Error::<T>::InsufficientTradingAmount
);
ensure!(
T::Currency::ensure_can_withdraw(asset_in, &who, amount).is_ok(),
Error::<T>::InsufficientBalance
);
// Special handling when one of the asset is Hub Asset
// Math is simplified and asset_in is actually part of asset_out state in this case
if asset_in == T::HubAssetId::get() {
return Self::sell_hub_asset(origin, &who, asset_out, amount, min_buy_amount);
}
if asset_out == T::HubAssetId::get() {
return Self::sell_asset_for_hub_asset(&who, asset_in, amount, min_buy_amount);
}
let asset_in_state = Self::load_asset_state(asset_in)?;
let asset_out_state = Self::load_asset_state(asset_out)?;
ensure!(
Self::allow_assets(&asset_in_state, &asset_out_state),
Error::<T>::NotAllowed
);
ensure!(
amount
<= asset_in_state
.reserve
.checked_div(T::MaxInRatio::get())
.ok_or(ArithmeticError::DivisionByZero)?, // Note: this can only fail if MaxInRatio is zero.
Error::<T>::MaxInRatioExceeded
);
let current_imbalance = <HubAssetImbalance<T>>::get();
let (asset_fee, _) = T::Fee::get(&asset_out);
let (_, protocol_fee) = T::Fee::get(&asset_in);
let state_changes = hydra_dx_math::omnipool::calculate_sell_state_changes(
&(&asset_in_state).into(),
&(&asset_out_state).into(),
amount,
asset_fee,
protocol_fee,
current_imbalance.value,
)
.ok_or(ArithmeticError::Overflow)?;
ensure!(
*state_changes.asset_out.delta_reserve > Balance::zero(),
Error::<T>::ZeroAmountOut
);