Files
serai/substrate/dex/src/lib.rs

377 lines
12 KiB
Rust
Raw Normal View History

2025-11-30 21:27:04 -05:00
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
2025-11-30 21:27:04 -05:00
#![cfg_attr(not(any(feature = "std", test)), no_std)]
2025-11-30 21:27:04 -05:00
extern crate alloc;
#[cfg(test)]
mod mock;
2025-11-30 21:27:04 -05:00
#[expect(clippy::cast_possible_truncation)]
#[frame_support::pallet]
2025-11-30 21:27:04 -05:00
mod pallet {
use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::*;
use serai_abi::{
primitives::{
prelude::*,
dex::{Error as PrimitivesError, Reserves, Premise},
},
Event,
};
2025-11-30 21:27:04 -05:00
use serai_core_pallet::Pallet as Core;
type Coins<T> = serai_coins_pallet::Pallet<T, serai_coins_pallet::CoinsInstance>;
type LiquidityTokens<T> =
serai_coins_pallet::Pallet<T, serai_coins_pallet::LiquidityTokensInstance>;
2025-11-30 21:27:04 -05:00
use super::*;
2025-11-30 21:27:04 -05:00
/// The configuration of this pallet.
#[pallet::config]
pub trait Config:
2025-11-30 21:27:04 -05:00
frame_system::Config
+ serai_core_pallet::Config
+ serai_coins_pallet::Config<serai_coins_pallet::CoinsInstance>
+ serai_coins_pallet::Config<serai_coins_pallet::LiquidityTokensInstance>
{
}
2025-11-30 21:27:04 -05:00
/// An error incurred.
#[pallet::error]
pub enum Error<T> {
2025-11-30 21:27:04 -05:00
/// The effected/would-be-used liquidity is invalid.
InvalidLiquidity,
/// An arithmetic overflow occurred.
Overflow,
2025-11-30 21:27:04 -05:00
/// An arithmetic underflow occured.
Underflow,
/// The requested swap wasn't satisfied.
Unsatisfied,
/// The swap was from the coin it's to.
FromToSelf,
}
2025-11-30 21:27:04 -05:00
impl<T> From<PrimitivesError> for Error<T> {
fn from(error: PrimitivesError) -> Error<T> {
match error {
PrimitivesError::Overflow => Error::Overflow,
PrimitivesError::Underflow => Error::Underflow,
PrimitivesError::KInvariant => Error::Unsatisfied,
}
}
}
2025-11-30 21:27:04 -05:00
#[pallet::storage]
type LpFeeInThousandths<T: Config> = StorageMap<_, Identity, ExternalCoin, u8, OptionQuery>;
2025-11-30 21:27:04 -05:00
/// The Pallet struct.
#[pallet::pallet]
pub struct Pallet<T>(_);
2025-11-30 21:27:04 -05:00
impl<T: Config> Pallet<T> {
fn emit_event(event: Event) {
Core::<T>::emit_event(event)
}
}
2025-11-30 21:27:04 -05:00
const MINIMUM_LIQUIDITY: u64 = 1 << 16;
#[pallet::call]
impl<T: Config> Pallet<T> {
2025-11-30 21:27:04 -05:00
/// Add liquidity.
#[pallet::call_index(0)]
2025-11-30 21:27:04 -05:00
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn add_liquidity(
origin: OriginFor<T>,
2025-11-30 21:27:04 -05:00
external_coin: ExternalCoin,
sri_intended: Amount,
external_coin_intended: Amount,
sri_minimum: Amount,
external_coin_minimum: Amount,
) -> DispatchResult {
2025-11-30 21:27:04 -05:00
let from = ensure_signed(origin)?;
2025-11-30 21:27:04 -05:00
let pool = serai_abi::dex::address(external_coin);
let supply = LiquidityTokens::<T>::supply(Coin::from(external_coin)).0;
2025-11-30 21:27:04 -05:00
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::<T>::Overflow)?,
);
if liquidity.0 < MINIMUM_LIQUIDITY {
Err(Error::<T>::InvalidLiquidity)?;
}
2025-11-30 21:27:04 -05:00
(sri_intended, external_coin_intended, liquidity)
} else {
let reserves = Reserves {
sri: Coins::<T>::balance(pool, Coin::Serai),
external_coin: Coins::<T>::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::<T>::from)?,
Premise::establish(Coin::Serai, Coin::from(external_coin))
.expect("sri, ext satisfies sri ^ sri")
.quote_for_in(reserves, sri_intended)
.map_err(Error::<T>::from)?,
);
if sri_optimal < sri_intended {
if sri_optimal < sri_minimum {
Err(Error::<T>::Unsatisfied)?;
}
(sri_optimal, external_coin_intended)
} else {
if external_coin_optimal < external_coin_minimum {
Err(Error::<T>::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::<T>::Overflow)?;
let external_coin_liquidity = u64::try_from(
(u128::from(external_coin_actual.0) * supply) / u128::from(reserves.external_coin.0),
)
.map_err(|_| Error::<T>::Overflow)?;
Amount(sri_liquidity.min(external_coin_liquidity))
};
(sri_actual, external_coin_actual, liquidity)
};
2025-11-30 21:27:04 -05:00
Coins::<T>::transfer_fn(
from,
pool.into(),
Balance { coin: Coin::Serai, amount: sri_actual },
)?;
2025-11-30 21:27:04 -05:00
Coins::<T>::transfer_fn(
from,
pool.into(),
Balance { coin: Coin::from(external_coin), amount: external_coin_actual },
)?;
LiquidityTokens::<T>::mint(
2025-11-30 21:27:04 -05:00
from,
Balance { coin: Coin::from(external_coin), amount: liquidity },
)?;
2025-11-30 21:27:04 -05:00
// TODO: Event
Ok(())
}
2025-11-30 21:27:04 -05:00
/// Transfer these liquidity tokens to the specified address.
#[pallet::call_index(1)]
2025-11-30 21:27:04 -05:00
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn transfer_liquidity(
origin: OriginFor<T>,
2025-11-30 21:27:04 -05:00
to: SeraiAddress,
liquidity_tokens: ExternalBalance,
) -> DispatchResult {
2025-11-30 21:27:04 -05:00
let from = ensure_signed(origin)?;
LiquidityTokens::<T>::transfer_fn(from, to.into(), liquidity_tokens.into())?;
2025-11-30 21:27:04 -05:00
// TODO: Event
Ok(())
}
2025-11-30 21:27:04 -05:00
/// Remove liquidity.
#[pallet::call_index(2)]
2025-11-30 21:27:04 -05:00
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn remove_liquidity(
origin: OriginFor<T>,
2025-11-30 21:27:04 -05:00
liquidity_tokens: ExternalBalance,
sri_minimum: Amount,
external_coin_minimum: Amount,
) -> DispatchResult {
2025-11-30 21:27:04 -05:00
let from = ensure_signed(origin)?;
2025-11-30 21:27:04 -05:00
let external_coin = liquidity_tokens.coin;
let pool = serai_abi::dex::address(external_coin);
let supply = LiquidityTokens::<T>::supply(Coin::from(external_coin)).0;
if supply.saturating_sub(liquidity_tokens.amount.0) < MINIMUM_LIQUIDITY {
Err(Error::<T>::InvalidLiquidity)?;
}
2025-11-30 21:27:04 -05:00
let supply = u128::from(supply);
2025-11-30 21:27:04 -05:00
let reserves = Reserves {
sri: Coins::<T>::balance(pool, Coin::Serai),
external_coin: Coins::<T>::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::<T>::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::<T>::Overflow)?);
if (sri_amount < sri_minimum) || (external_coin_amount < external_coin_minimum) {
Err(Error::<T>::Unsatisfied)?;
}
2025-11-30 21:27:04 -05:00
LiquidityTokens::<T>::burn_fn(from, liquidity_tokens.into())?;
Coins::<T>::transfer_fn(
from,
pool.into(),
Balance { coin: Coin::Serai, amount: sri_amount },
)?;
Coins::<T>::transfer_fn(
from,
pool.into(),
Balance { coin: Coin::from(external_coin), amount: external_coin_amount },
)?;
2025-11-30 21:27:04 -05:00
// TODO: Event
Ok(())
}
2025-11-30 21:27:04 -05:00
/// Swap an exact amount of coins.
#[pallet::call_index(3)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn swap(
origin: OriginFor<T>,
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::<T>::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::<T>::balance(pool, Coin::Serai),
external_coin: Coins::<T>::balance(pool, Coin::from(external_coin)),
};
if (reserves.sri == Amount(0)) || (reserves.external_coin == Amount(0)) {
Err(Error::<T>::InvalidLiquidity)?;
}
2025-11-30 21:27:04 -05:00
// 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::<T>::transfer_fn(
transfer_from.into(),
pool.into(),
Balance { coin: swap.r#in(), amount: next_amount },
)?;
2025-11-30 21:27:04 -05:00
// Update the current status
transfer_from = pool;
next_amount = swap.quote_for_in(reserves, next_amount).map_err(Error::<T>::from)?;
}
2025-11-30 21:27:04 -05:00
// Check the amount meets the expectation
if next_amount.0 < minimum_to_receive.amount.0 {
Err(Error::<T>::Unsatisfied)?;
}
2025-11-30 21:27:04 -05:00
// Transfer the resulting coins to the origin
Coins::<T>::transfer_fn(
transfer_from.into(),
origin.into(),
Balance { coin: minimum_to_receive.coin, amount: next_amount },
)?;
2025-11-30 21:27:04 -05:00
// TODO: Event
2025-11-30 21:27:04 -05:00
Ok(())
}
2025-11-30 21:27:04 -05:00
/// Swap for an exact amount of coins.
#[pallet::call_index(4)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn swap_for(
origin: OriginFor<T>,
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::<T>::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::<T>::balance(pool, Coin::Serai),
external_coin: Coins::<T>::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::<T>::transfer_fn(
pool.into(),
transfer_to.into(),
Balance { coin: swap.out(), amount: next_amount },
)?;
2025-11-30 21:27:04 -05:00
transfer_to = pool;
next_amount = swap.quote_for_out(reserves, next_amount).map_err(Error::<T>::from)?;
2025-11-30 21:27:04 -05:00
i != 0
} {}
2025-11-30 21:27:04 -05:00
// Check the amount meets the expectation
if next_amount.0 > maximum_to_swap.amount.0 {
Err(Error::<T>::Unsatisfied)?;
}
2025-11-30 21:27:04 -05:00
// Transfer the necessary coins from the origin
Coins::<T>::transfer_fn(
origin.into(),
transfer_to.into(),
Balance { coin: maximum_to_swap.coin, amount: next_amount },
)?;
2025-11-30 21:27:04 -05:00
// TODO: Event
Ok(())
}
}
}
2025-11-30 21:27:04 -05:00
pub use pallet::*;