pallet_dex_v2/
lib.rs

1// We make sure this pallet uses `no_std` for compiling to Wasm.
2#![cfg_attr(not(feature = "std"), no_std)]
3
4// Re-export pallet items so that they can be accessed from the crate namespace.
5use frame_support::pallet_prelude::*;
6use frame_support::traits::fungible;
7use frame_support::traits::fungibles;
8use frame_support::PalletId;
9use pallet::*;
10use sp_runtime::traits::{AccountIdConversion, CheckedDiv, CheckedMul, IntegerSquareRoot, Zero};
11
12// FRAME pallets require their own "mock runtimes" to be able to run unit tests. This module
13// contains a mock runtime specific for testing this pallet's functionality.
14#[cfg(test)]
15mod mock;
16
17// This module contains the liqiudity pool logic
18mod liquidity_pool;
19
20
21// Re-export pallet items so that they can be accessed from the crate namespace.
22pub use pallet::*;
23
24
25#[cfg(test)]
26mod tests;
27
28// Define type aliases for easier access
29pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
30pub type AssetIdOf<T> = <<T as Config>::Fungibles as fungibles::Inspect<
31    <T as frame_system::Config>::AccountId,
32>>::AssetId;
33
34pub type BalanceOf<T> = <<T as Config>::NativeBalance as fungible::Inspect<
35    <T as frame_system::Config>::AccountId,
36>>::Balance;
37
38pub type AssetBalanceOf<T> = <<T as Config>::Fungibles as fungibles::Inspect<
39    <T as frame_system::Config>::AccountId,
40>>::Balance;
41
42
43
44// All pallet logic is defined in its own module and must be annotated by the `pallet` attribute.
45#[frame_support::pallet]
46pub mod pallet {
47    // Import various useful types required by all FRAME pallets.
48    use super::*;
49    use crate::liquidity_pool::LiquidityPool;
50    use frame_support::traits::fungibles::Mutate;
51    use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
52    use frame_system::pallet_prelude::*;
53
54    // The `Pallet` struct serves as a placeholder to implement traits, methods and dispatchables
55    // (`Call`s) in this pallet.
56    #[pallet::pallet]
57    pub struct Pallet<T>(_);
58
59    /// The pallet's configuration trait.
60    #[pallet::config]
61    pub trait Config: frame_system::Config {
62        /// The overarching runtime event type.
63        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
64
65        // Type to access the Balances Pallet
66        type NativeBalance: fungible::Inspect<Self::AccountId>
67            + fungible::Mutate<Self::AccountId>
68            + fungible::hold::Inspect<Self::AccountId>
69            + fungible::hold::Mutate<Self::AccountId>
70            + fungible::freeze::Inspect<Self::AccountId>
71            + fungible::freeze::Mutate<Self::AccountId>;
72
73        // Type to access the Assets Pallet
74        type Fungibles: fungibles::Inspect<Self::AccountId, AssetId = u32>
75            + fungibles::Mutate<Self::AccountId>
76            + fungibles::Create<Self::AccountId>;
77
78        #[pallet::constant]
79        type PalletId: Get<PalletId>;
80    }
81
82    /// A storage map for storing liquidity pools
83    #[pallet::storage]
84    pub type LiquidityPools<T: Config> =
85        StorageMap<_, Blake2_128Concat, (AssetIdOf<T>, AssetIdOf<T>), LiquidityPool<T>>;
86
87    /// Storage map for storing mapping of liquidity token to asset pair
88    #[pallet::storage]
89    pub type LiquidityTokens<T: Config> =
90        StorageMap<_, Blake2_128Concat, AssetIdOf<T>, (AssetIdOf<T>, AssetIdOf<T>), ValueQuery>;
91
92    /// Events that functions in this pallet can emit.
93    #[pallet::event]
94    #[pallet::generate_deposit(pub(super) fn deposit_event)]
95    pub enum Event<T: Config> {
96        /// Liquidity pool created.
97        /// Parameters:
98        /// - `T::AccountId`: The account ID of the liquidity provider who created the pool.
99        /// - `(T::AssetId, T::AssetId)`: The trading pair of the created liquidity pool.
100        LiquidityPoolCreated(AccountIdOf<T>, (AssetIdOf<T>, AssetIdOf<T>)),
101
102        /// Liquidity minted.
103        /// Parameters:
104        /// - `T::AccountId`: The account ID of the liquidity provider who minted the liquidity.
105        /// - `(T::AssetId, T::AssetId)`: The trading pair of the liquidity pool.
106        /// - `T::Balance`: The amount of liquidity tokens minted.
107        LiquidityMinted(
108            AccountIdOf<T>,
109            (AssetIdOf<T>, AssetIdOf<T>),
110            AssetBalanceOf<T>,
111        ),
112
113        /// Liquidity burned.
114        /// Parameters:
115        /// - `T::AccountId`: The account ID of the liquidity provider who burned the liquidity.
116        /// - `(T::AssetId, T::AssetId)`: The trading pair of the liquidity pool.
117        /// - `T::Balance`: The amount of liquidity tokens burned.
118        LiquidityBurned(
119            AccountIdOf<T>,
120            (AssetIdOf<T>, AssetIdOf<T>),
121            AssetBalanceOf<T>,
122        ),
123
124        /// Assets swapped.
125        /// Parameters:
126        /// - `T::AccountId`: The account ID of the user who performed the swap.
127        /// - `T::AssetId`: The ID of the asset that was swapped (sold).
128        /// - `T::Balance`: The amount of the asset that was swapped (sold).
129        /// - `T::AssetId`: The ID of the asset that was received (bought).
130        /// - `T::Balance`: The amount of the asset that was received (bought).
131        Swapped(
132            AccountIdOf<T>,
133            AssetIdOf<T>,
134            AssetBalanceOf<T>,
135            AssetIdOf<T>,
136            AssetBalanceOf<T>,
137        ),
138    }
139
140    /// Errors that can be returned by this pallet.
141    #[pallet::error]
142    pub enum Error<T> {
143        /// cannot have the same asset in a trading pair
144        CannotHaveSameAssetInPair,
145
146        /// Insufficient liquidity available in the pool.
147        InsufficientLiquidity,
148
149        /// Insufficient reserves available in the pool for the requested operation.
150        InsufficientReserves,
151
152        /// Overflow occurred when adding to the reserve balance.
153        ReserveOverflow,
154
155        /// Overflow occurred when adding to the total liquidity.
156        LiquidityOverflow,
157
158        /// The asset being swapped in is not part of the specified trading pair.
159        InvalidAssetIn,
160
161        /// The asset being swapped out is not part of the specified trading pair.
162        InvalidAssetOut,
163
164        /// The reserves for the asset being swapped out is not sufficient.
165        InsufficientAmountOut,
166
167        /// Attempted to perform an operation that resulted in an overflow
168        ArithmeticOverflow,
169
170        /// Attempted to divide by zero
171        DivisionByZero,
172
173        /// The liquidity pool for the specified trading pair already exists.
174        LiquidityPoolAlreadyExists,
175
176        /// The liquidity pool with the provided asset pair not found
177        LiquidityPoolNotFound,
178
179        /// Minted is not greater than or equal to the minimum liquidity specified
180        InsufficientLiquidityMinted,
181
182        /// The liquidity pool does not have enough reserves
183        InsufficientAmountsOut,
184
185        /// There is no liquidity to burn
186        ZeroLiquidityBurned,
187    }
188
189    /// The pallet's dispatchable functions ([`Call`]s).
190    #[pallet::call]
191    impl<T: Config> Pallet<T> {
192        // Dispatchable call to create a new liquidity pool
193        #[pallet::call_index(0)]
194        #[pallet::weight(Weight::default())]
195        pub fn create_liquidity_pool(
196            origin: OriginFor<T>,
197            asset_a: AssetIdOf<T>,
198            asset_b: AssetIdOf<T>,
199            liquidity_token: AssetIdOf<T>,
200        ) -> DispatchResult {
201            // ensure that the origin has been signed
202            let sender = ensure_signed(origin)?;
203
204            let trading_pair = return_vaild_pair_format::<T>(asset_a, asset_b)?;
205            ensure!(
206                !LiquidityPools::<T>::contains_key(trading_pair),
207                Error::<T>::LiquidityPoolAlreadyExists
208            );
209
210            // Create a new liquidity pool
211            let liquidity_pool = LiquidityPool {
212                assets: trading_pair,
213                reserves: (Zero::zero(), Zero::zero()),
214                total_liquidity: Zero::zero(),
215                liquidity_token,
216            };
217
218            // Insert the new liquidity pool into the storage
219            LiquidityPools::<T>::insert(trading_pair, liquidity_pool);
220
221            // Insert the liquidity token into the storage
222            LiquidityTokens::<T>::insert(liquidity_token, trading_pair);
223
224
225            // Log an event indicating that the pool was created
226            Self::deposit_event(Event::LiquidityPoolCreated(sender, trading_pair));
227
228            Ok(())
229        }
230
231        #[pallet::call_index(1)]
232        #[pallet::weight(Weight::default())]
233        pub fn mint_liquidity(
234            origin: OriginFor<T>,
235            asset_a: AssetIdOf<T>,
236            asset_b: AssetIdOf<T>,
237            amount_a: AssetBalanceOf<T>,
238            amount_b: AssetBalanceOf<T>,
239            min_liquidity: AssetBalanceOf<T>,
240        ) -> DispatchResult {
241            let sender = ensure_signed(origin)?;
242
243            let trading_pair = return_vaild_pair_format::<T>(asset_a, asset_b)?;
244
245            // Get the liquidity pool from storage
246            let mut liquidity_pool =
247                LiquidityPools::<T>::get(&trading_pair).ok_or(Error::<T>::LiquidityPoolNotFound)?;
248
249            // Calculate the liquidity minted based on the provided amounts and the current reserves
250            let liquidity_minted = Self::calculate_liquidity_minted(
251                (amount_a, amount_b),
252                (liquidity_pool.reserves.0, liquidity_pool.reserves.1),
253                liquidity_pool.total_liquidity,
254            )?;
255
256            // Ensure that the liquidity minted is greater than or equal to the minimum liquidity specified
257            ensure!(
258                liquidity_minted >= min_liquidity,
259                Error::<T>::InsufficientLiquidityMinted
260            );
261
262            // Transfer the assets from the sender to the liquidity pool
263            Self::transfer_asset_to_pool(&sender, trading_pair.0, amount_a)?;
264            Self::transfer_asset_to_pool(&sender, trading_pair.1, amount_b)?;
265
266            // Mint liquidity tokens to the sender
267            Self::mint_liquidity_tokens(&sender, liquidity_pool.liquidity_token, liquidity_minted)?;
268
269            // Update the liquidity pool reserves and total liquidity using the `mint` method
270            liquidity_pool.mint((amount_a, amount_b), liquidity_minted)?;
271
272            // Update the liquidity pool in storage
273            LiquidityPools::<T>::insert(&trading_pair, liquidity_pool);
274
275            // Emit the LiquidityMinted event
276            Self::deposit_event(Event::LiquidityMinted(
277                sender,
278                trading_pair,
279                liquidity_minted,
280            ));
281
282            Ok(())
283        }
284
285        // Dispatchable call to burn liquidity tokens
286        #[pallet::call_index(2)]
287        #[pallet::weight(Weight::default())]
288        pub fn burn_liquidity(
289            origin: OriginFor<T>,
290            asset_a: AssetIdOf<T>,
291            asset_b: AssetIdOf<T>,
292            liquidity_burned: AssetBalanceOf<T>,
293            min_amount_a: AssetBalanceOf<T>,
294            min_amount_b: AssetBalanceOf<T>,
295        ) -> DispatchResult {
296            let sender = ensure_signed(origin)?;
297
298            let trading_pair = return_vaild_pair_format::<T>(asset_a, asset_b)?;
299
300            let mut liquidity_pool =
301                LiquidityPools::<T>::get(trading_pair).ok_or(Error::<T>::LiquidityPoolNotFound)?;
302
303            // Calculate the amounts of tokens to withdraw based on the liquidity burned and
304            // the current reserves
305            let amounts_out = Self::calculate_amounts_out(
306                liquidity_burned,
307                (liquidity_pool.reserves.0, liquidity_pool.reserves.1),
308                liquidity_pool.total_liquidity,
309            )?;
310            ensure!(
311                amounts_out.0 >= min_amount_a && amounts_out.1 >= min_amount_b,
312                Error::<T>::InsufficientAmountsOut
313            );
314
315            // Burn the liquidity tokens from the sender
316            Self::burn_liquidity_tokens(&sender, liquidity_pool.liquidity_token, liquidity_burned)?;
317
318            // Update the liquidity pool reserves and total liquidity
319            liquidity_pool.burn(liquidity_burned, amounts_out)?;
320            LiquidityPools::<T>::insert(trading_pair, liquidity_pool);
321
322            // Transfer the assets to the sender
323            Self::transfer_asset_to_user(&sender, trading_pair.0, amounts_out.0)?;
324            Self::transfer_asset_to_user(&sender, trading_pair.1, amounts_out.1)?;
325
326            Self::deposit_event(Event::LiquidityBurned(
327                sender,
328                trading_pair,
329                liquidity_burned,
330            ));
331
332            Ok(())
333        }
334
335        #[pallet::call_index(3)]
336        #[pallet::weight(Weight::default())]
337        pub fn swap(
338            origin: OriginFor<T>,
339            asset_in: AssetIdOf<T>,
340            asset_out: AssetIdOf<T>,
341            amount_in: AssetBalanceOf<T>,
342            min_amount_out: AssetBalanceOf<T>,
343        ) -> DispatchResult {
344            let sender = ensure_signed(origin)?;
345
346            let (liquidity_pool, trading_pair) = {
347                let trading_pair = return_vaild_pair_format::<T>(asset_in, asset_out)?;
348                (LiquidityPools::<T>::get(&trading_pair), trading_pair)
349            };
350
351            let mut liquidity_pool = liquidity_pool.ok_or(Error::<T>::LiquidityPoolNotFound)?;
352
353            let amount_out = liquidity_pool.swap(asset_in, asset_out, amount_in, min_amount_out)?;
354
355            Self::transfer_asset_from_user(&sender, asset_in, amount_in)?;
356            Self::transfer_asset_to_user(&sender, asset_out, amount_out)?;
357
358            LiquidityPools::<T>::insert(&trading_pair, liquidity_pool);
359
360            Self::deposit_event(Event::Swapped(
361                sender, asset_in, amount_in, asset_out, amount_out,
362            ));
363
364            Ok(())
365        }
366    }
367
368    /// The pallet's internal functions.
369    impl<T: Config> Pallet<T> {
370        fn calculate_liquidity_minted(
371            amounts: (AssetBalanceOf<T>, AssetBalanceOf<T>),
372            reserves: (AssetBalanceOf<T>, AssetBalanceOf<T>),
373            total_liquidity: AssetBalanceOf<T>,
374        ) -> Result<AssetBalanceOf<T>, DispatchError> {
375            let (amount_a, amount_b) = amounts;
376            let (reserve_a, reserve_b) = reserves;
377
378            ensure!(
379                !amount_a.is_zero() && !amount_b.is_zero(),
380                Error::<T>::InsufficientLiquidityMinted
381            );
382
383            if total_liquidity.is_zero() {
384                // If the liquidity pool is empty, the minted liquidity is the geometric mean of the amounts
385                let liquidity_minted = Self::geometric_mean(amount_a, amount_b)?;
386                Ok(liquidity_minted)
387            } else {
388                // If the liquidity pool is not empty, calculate the minted liquidity proportionally
389                let liquidity_minted_a = amount_a
390                    .checked_mul(&total_liquidity)
391                    .ok_or(Error::<T>::ArithmeticOverflow)?
392                    .checked_div(&reserve_a)
393                    .ok_or(Error::<T>::DivisionByZero)?;
394
395                let liquidity_minted_b = amount_b
396                    .checked_mul(&total_liquidity)
397                    .ok_or(Error::<T>::ArithmeticOverflow)?
398                    .checked_div(&reserve_b)
399                    .ok_or(Error::<T>::DivisionByZero)?;
400
401                // Choose the smaller minted liquidity to maintain the desired asset ratio
402                let liquidity_minted = sp_std::cmp::min(liquidity_minted_a, liquidity_minted_b);
403                Ok(liquidity_minted)
404            }
405        }
406
407        /// This is used ti estimate the amount of liquidity to be minted to a provider is the pool is a new pool
408        fn geometric_mean(
409            amount_a: AssetBalanceOf<T>,
410            amount_b: AssetBalanceOf<T>,
411        ) -> Result<AssetBalanceOf<T>, DispatchError> {
412            let sqrt_product = (amount_a
413                .checked_mul(&amount_b)
414                .ok_or(Error::<T>::ArithmeticOverflow)?)
415            .integer_sqrt();
416            Ok(sqrt_product)
417        }
418
419        fn pallet_account_id() -> T::AccountId {
420            T::PalletId::get().into_account_truncating()
421        }
422
423        fn transfer_asset_to_pool(
424            sender: &AccountIdOf<T>,
425            asset_id: AssetIdOf<T>,
426            amount: AssetBalanceOf<T>,
427        ) -> DispatchResult {
428            // Transfer the asset from the sender to the pool account
429            T::Fungibles::transfer(
430                asset_id,
431                sender,
432                &Self::pallet_account_id(),
433                amount,
434                Preservation::Expendable,
435            )?;
436
437            Ok(())
438        }
439
440        fn mint_liquidity_tokens(
441            recipient: &AccountIdOf<T>,
442            liquidity_token_id: AssetIdOf<T>,
443            amount: AssetBalanceOf<T>,
444        ) -> DispatchResult {
445            // Mint the liquidity tokens to the recipient
446            T::Fungibles::mint_into(liquidity_token_id, recipient, amount)?;
447            Ok(())
448        }
449
450        fn burn_liquidity_tokens(
451            sender: &AccountIdOf<T>,
452            liquidity_token_id: AssetIdOf<T>,
453            amount: AssetBalanceOf<T>,
454        ) -> DispatchResult {
455            // Burn the liquidity tokens from the sender's account
456            T::Fungibles::burn_from(
457                liquidity_token_id,
458                sender,
459                amount,
460                Precision::Exact,
461                Fortitude::Polite,
462            )?;
463            Ok(())
464        }
465
466        fn calculate_amounts_out(
467            liquidity_burned: AssetBalanceOf<T>,
468            reserves: (AssetBalanceOf<T>, AssetBalanceOf<T>),
469            total_liquidity: AssetBalanceOf<T>,
470        ) -> Result<(AssetBalanceOf<T>, AssetBalanceOf<T>), DispatchError> {
471            ensure!(!liquidity_burned.is_zero(), Error::<T>::ZeroLiquidityBurned);
472            ensure!(
473                !total_liquidity.is_zero(),
474                Error::<T>::InsufficientLiquidity
475            );
476
477            let (reserve_a, reserve_b) = reserves;
478            ensure!(
479                !reserve_a.is_zero() && !reserve_b.is_zero(),
480                Error::<T>::InsufficientLiquidity
481            );
482
483            let amount_a = liquidity_burned
484                .checked_mul(&reserve_a)
485                .ok_or(Error::<T>::ArithmeticOverflow)?
486                .checked_div(&total_liquidity)
487                .ok_or(Error::<T>::DivisionByZero)?;
488
489            let amount_b = liquidity_burned
490                .checked_mul(&reserve_b)
491                .ok_or(Error::<T>::ArithmeticOverflow)?
492                .checked_div(&total_liquidity)
493                .ok_or(Error::<T>::DivisionByZero)?;
494
495            Ok((amount_a, amount_b))
496        }
497
498        fn transfer_asset_from_user(
499            user: &AccountIdOf<T>,
500            asset_id: AssetIdOf<T>,
501            amount: AssetBalanceOf<T>,
502        ) -> DispatchResult {
503            T::Fungibles::transfer(
504                asset_id,
505                user,
506                &Self::pallet_account_id(),
507                amount,
508                Preservation::Expendable,
509            )?;
510            Ok(())
511        }
512
513        fn transfer_asset_to_user(
514            user: &AccountIdOf<T>,
515            asset_id: AssetIdOf<T>,
516            amount: AssetBalanceOf<T>,
517        ) -> DispatchResult {
518            T::Fungibles::transfer(
519                asset_id,
520                &Self::pallet_account_id(),
521                user,
522                amount,
523                Preservation::Expendable,
524            )?;
525            Ok(())
526        }
527    }
528}
529
530
531
532fn return_vaild_pair_format<T: pallet::Config>(asset_a: AssetIdOf<T>, asset_b: AssetIdOf<T>) -> Result<(AssetIdOf<T>, AssetIdOf<T>), DispatchError> {
533    if asset_a == asset_b {
534        return Err(Error::<T>::LiquidityPoolAlreadyExists.into());
535    }
536
537    if asset_a < asset_b {
538        Ok((asset_a, asset_b))
539    } else {
540        Ok((asset_b, asset_a))
541    }
542}