#![doc = include_str!("../README.md")] #![deny(missing_docs)] #![cfg_attr(not(any(feature = "std", test)), no_std)] extern crate alloc; #[cfg(test)] mod mock; #[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] mod pallet { use frame_system::pallet_prelude::*; use frame_support::pallet_prelude::*; use serai_abi::{ primitives::{ prelude::*, dex::{Error as PrimitivesError, Reserves, Premise}, }, Event, }; use serai_core_pallet::Pallet as Core; type Coins = serai_coins_pallet::Pallet; type LiquidityTokens = serai_coins_pallet::Pallet; use super::*; /// The configuration of this pallet. #[pallet::config] pub trait Config: frame_system::Config + serai_core_pallet::Config + serai_coins_pallet::Config + serai_coins_pallet::Config { } /// An error incurred. #[pallet::error] pub enum Error { /// The effected/would-be-used liquidity is invalid. InvalidLiquidity, /// An arithmetic overflow occurred. Overflow, /// An arithmetic underflow occured. Underflow, /// The requested swap wasn't satisfied. Unsatisfied, /// The swap was from the coin it's to. FromToSelf, } impl From for Error { fn from(error: PrimitivesError) -> Error { match error { PrimitivesError::Overflow => Error::Overflow, PrimitivesError::Underflow => Error::Underflow, PrimitivesError::KInvariant => Error::Unsatisfied, } } } #[pallet::storage] type LpFeeInThousandths = StorageMap<_, Identity, ExternalCoin, u8, OptionQuery>; /// The Pallet struct. #[pallet::pallet] pub struct Pallet(_); impl Pallet { fn emit_event(event: Event) { Core::::emit_event(event) } } const MINIMUM_LIQUIDITY: u64 = 1 << 16; #[pallet::call] impl Pallet { /// Add liquidity. #[pallet::call_index(0)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn add_liquidity( origin: OriginFor, external_coin: ExternalCoin, sri_intended: Amount, external_coin_intended: Amount, sri_minimum: Amount, external_coin_minimum: Amount, ) -> DispatchResult { let from = ensure_signed(origin)?; let pool = serai_abi::dex::address(external_coin); let supply = LiquidityTokens::::supply(Coin::from(external_coin)).0; let (sri_actual, external_coin_actual, liquidity) = if supply == 0 { let sri_actual = sri_intended; let external_coin_actual = external_coin_intended; let liquidity = Amount( u64::try_from((u128::from(sri_actual.0) * u128::from(external_coin_actual.0)).isqrt()) .map_err(|_| Error::::Overflow)?, ); if liquidity.0 < MINIMUM_LIQUIDITY { Err(Error::::InvalidLiquidity)?; } (sri_intended, external_coin_intended, liquidity) } else { let reserves = Reserves { sri: Coins::::balance(pool, Coin::Serai), external_coin: Coins::::balance(pool, Coin::from(external_coin)), }; let (sri_actual, external_coin_actual) = { let (sri_optimal, external_coin_optimal) = ( Premise::establish(Coin::from(external_coin), Coin::Serai) .expect("ext, sri satisfies sri ^ sri") .quote_for_in(reserves, external_coin_intended) .map_err(Error::::from)?, Premise::establish(Coin::Serai, Coin::from(external_coin)) .expect("sri, ext satisfies sri ^ sri") .quote_for_in(reserves, sri_intended) .map_err(Error::::from)?, ); if sri_optimal < sri_intended { if sri_optimal < sri_minimum { Err(Error::::Unsatisfied)?; } (sri_optimal, external_coin_intended) } else { if external_coin_optimal < external_coin_minimum { Err(Error::::Unsatisfied)?; } (sri_intended, external_coin_optimal) } }; let liquidity = { let supply = u128::from(supply); let sri_liquidity = u64::try_from((u128::from(sri_actual.0) * supply) / u128::from(reserves.sri.0)) .map_err(|_| Error::::Overflow)?; let external_coin_liquidity = u64::try_from( (u128::from(external_coin_actual.0) * supply) / u128::from(reserves.external_coin.0), ) .map_err(|_| Error::::Overflow)?; Amount(sri_liquidity.min(external_coin_liquidity)) }; (sri_actual, external_coin_actual, liquidity) }; Coins::::transfer_fn( from, pool.into(), Balance { coin: Coin::Serai, amount: sri_actual }, )?; Coins::::transfer_fn( from, pool.into(), Balance { coin: Coin::from(external_coin), amount: external_coin_actual }, )?; LiquidityTokens::::mint( from, Balance { coin: Coin::from(external_coin), amount: liquidity }, )?; // TODO: Event Ok(()) } /// Transfer these liquidity tokens to the specified address. #[pallet::call_index(1)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn transfer_liquidity( origin: OriginFor, to: SeraiAddress, liquidity_tokens: ExternalBalance, ) -> DispatchResult { let from = ensure_signed(origin)?; LiquidityTokens::::transfer_fn(from, to.into(), liquidity_tokens.into())?; // TODO: Event Ok(()) } /// Remove liquidity. #[pallet::call_index(2)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn remove_liquidity( origin: OriginFor, liquidity_tokens: ExternalBalance, sri_minimum: Amount, external_coin_minimum: Amount, ) -> DispatchResult { let from = ensure_signed(origin)?; let external_coin = liquidity_tokens.coin; let pool = serai_abi::dex::address(external_coin); let supply = LiquidityTokens::::supply(Coin::from(external_coin)).0; if supply.saturating_sub(liquidity_tokens.amount.0) < MINIMUM_LIQUIDITY { Err(Error::::InvalidLiquidity)?; } let supply = u128::from(supply); let reserves = Reserves { sri: Coins::::balance(pool, Coin::Serai), external_coin: Coins::::balance(pool, Coin::from(external_coin)), }; let sri_amount = (u128::from(liquidity_tokens.amount.0) * u128::from(reserves.sri.0)) / supply; let sri_amount = Amount(u64::try_from(sri_amount).map_err(|_| Error::::Overflow)?); let external_coin_amount = (u128::from(liquidity_tokens.amount.0) * u128::from(reserves.external_coin.0)) / supply; let external_coin_amount = Amount(u64::try_from(external_coin_amount).map_err(|_| Error::::Overflow)?); if (sri_amount < sri_minimum) || (external_coin_amount < external_coin_minimum) { Err(Error::::Unsatisfied)?; } LiquidityTokens::::burn_fn(from, liquidity_tokens.into())?; Coins::::transfer_fn( from, pool.into(), Balance { coin: Coin::Serai, amount: sri_amount }, )?; Coins::::transfer_fn( from, pool.into(), Balance { coin: Coin::from(external_coin), amount: external_coin_amount }, )?; // TODO: Event Ok(()) } /// Swap an exact amount of coins. #[pallet::call_index(3)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn swap( origin: OriginFor, coins_to_swap: Balance, minimum_to_receive: Balance, ) -> DispatchResult { let origin = SeraiAddress::from(ensure_signed(origin)?); let mut transfer_from = origin; let mut next_amount = coins_to_swap.amount; let swaps = Premise::route(coins_to_swap.coin, minimum_to_receive.coin) .ok_or(Error::::FromToSelf)?; for swap in &swaps { let external_coin = swap.external_coin(); let pool = serai_abi::dex::address(external_coin); // Fetch the pool's reserves let reserves = Reserves { sri: Coins::::balance(pool, Coin::Serai), external_coin: Coins::::balance(pool, Coin::from(external_coin)), }; if (reserves.sri == Amount(0)) || (reserves.external_coin == Amount(0)) { Err(Error::::InvalidLiquidity)?; } // Transfer from the prior (the originating account or pool) to the current pool /* This is impossible to reach, yet would cause the amount _yet to be transferred out_ to be credited both as part of the reserves _and_ the amount in if violated. */ assert!(transfer_from != pool, "swap routed from a coin to itself"); Coins::::transfer_fn( transfer_from.into(), pool.into(), Balance { coin: swap.r#in(), amount: next_amount }, )?; // Update the current status transfer_from = pool; next_amount = swap.quote_for_in(reserves, next_amount).map_err(Error::::from)?; } // Check the amount meets the expectation if next_amount.0 < minimum_to_receive.amount.0 { Err(Error::::Unsatisfied)?; } // Transfer the resulting coins to the origin Coins::::transfer_fn( transfer_from.into(), origin.into(), Balance { coin: minimum_to_receive.coin, amount: next_amount }, )?; // TODO: Event Ok(()) } /// Swap for an exact amount of coins. #[pallet::call_index(4)] #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn swap_for( origin: OriginFor, coins_to_receive: Balance, maximum_to_swap: Balance, ) -> DispatchResult { let origin = SeraiAddress::from(ensure_signed(origin)?); let mut transfer_to = origin; let mut next_amount = coins_to_receive.amount; let swaps = Premise::route(maximum_to_swap.coin, coins_to_receive.coin) .ok_or(Error::::FromToSelf)?; let mut i = swaps.len(); while { i -= 1; let swap = swaps[i]; let external_coin = swap.external_coin(); let pool = serai_abi::dex::address(external_coin); // Fetch the pool's reserves let reserves = Reserves { sri: Coins::::balance(pool, Coin::Serai), external_coin: Coins::::balance(pool, Coin::from(external_coin)), }; /* Transfer the output requested. While this violates the traditional Checks-Effects-Interactions pattern, in a way favoring the caller, this function is within a `transactional` layer (due to being a call) making all its DB operations only persisted upon success. */ /* This is impossible to reach, yet ensures the amount not yet transferred in isn't excluded when determining the reserves on the next iteration. */ assert!(transfer_to != pool, "swap routed to a coin from itself"); Coins::::transfer_fn( pool.into(), transfer_to.into(), Balance { coin: swap.out(), amount: next_amount }, )?; transfer_to = pool; next_amount = swap.quote_for_out(reserves, next_amount).map_err(Error::::from)?; i != 0 } {} // Check the amount meets the expectation if next_amount.0 > maximum_to_swap.amount.0 { Err(Error::::Unsatisfied)?; } // Transfer the necessary coins from the origin Coins::::transfer_fn( origin.into(), transfer_to.into(), Balance { coin: maximum_to_swap.coin, amount: next_amount }, )?; // TODO: Event Ok(()) } } } pub use pallet::*;