#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use cfg_primitives::conversion::convert_balance_decimals;
use cfg_traits::{
swaps::{OrderInfo, OrderRatio, Swap, SwapInfo, TokenSwaps},
StatusNotificationHook, ValueProvider,
};
use frame_support::{
pallet_prelude::{DispatchResult, Member, StorageDoubleMap, StorageValue, *},
traits::{
fungibles::{Inspect as AssetInspect, InspectHold, Mutate, MutateHold},
tokens::{AssetId, Precision, Preservation},
},
Twox64Concat,
};
use frame_system::pallet_prelude::{OriginFor, *};
use orml_traits::asset_registry::{self, Inspect as _};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_arithmetic::traits::CheckedSub;
use sp_runtime::{
traits::{
AtLeast32BitUnsigned, EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureFixedPointNumber,
EnsureMul, EnsureSub, EnsureSubAssign, MaybeSerializeDeserialize, One, Zero,
},
FixedPointNumber, FixedPointOperand, TokenError,
};
use sp_std::cmp::{min, Ordering};
use super::*;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
pub type BalanceOf<T> =
<<T as Config>::Currency as AssetInspect<<T as frame_system::Config>::AccountId>>::Balance;
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AssetRegistry: asset_registry::Inspect<
AssetId = Self::CurrencyId,
Balance = BalanceOf<Self>,
>;
type CurrencyId: AssetId
+ Parameter
+ Default
+ Member
+ Copy
+ MaybeSerializeDeserialize
+ Ord;
type OrderIdNonce: Parameter
+ Member
+ AtLeast32BitUnsigned
+ Default
+ Copy
+ EnsureAdd
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
type BalanceIn: Member
+ Parameter
+ FixedPointOperand
+ AtLeast32BitUnsigned
+ EnsureMul
+ EnsureDiv
+ MaxEncodedLen
+ Into<BalanceOf<Self>>
+ From<BalanceOf<Self>>;
type BalanceOut: Member
+ Parameter
+ FixedPointOperand
+ AtLeast32BitUnsigned
+ EnsureMul
+ EnsureDiv
+ MaxEncodedLen
+ Into<BalanceOf<Self>>
+ From<BalanceOf<Self>>;
type Currency: AssetInspect<Self::AccountId, AssetId = Self::CurrencyId>
+ InspectHold<Self::AccountId, Reason = ()>
+ MutateHold<Self::AccountId>
+ Mutate<Self::AccountId>;
type Ratio: Parameter
+ Member
+ FixedPointNumber
+ EnsureMul
+ EnsureDiv
+ MaybeSerializeDeserialize
+ MaxEncodedLen;
#[pallet::constant]
type MinFulfillmentAmountNative: Get<Self::BalanceOut>;
#[pallet::constant]
type NativeDecimals: Get<u32>;
type FulfilledOrderHook: StatusNotificationHook<
Id = Self::OrderIdNonce,
Status = SwapInfo<Self::BalanceIn, Self::BalanceOut, Self::CurrencyId, Self::Ratio>,
Error = DispatchError,
>;
type FeederId: Parameter + Member + Ord + MaxEncodedLen;
type RatioProvider: ValueProvider<
Self::FeederId,
(Self::CurrencyId, Self::CurrencyId),
Value = Self::Ratio,
>;
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type Weights: WeightInfo;
}
#[derive(
Clone, RuntimeDebugNoBound, Encode, Decode, Eq, PartialEq, MaxEncodedLen, TypeInfo,
)]
#[scale_info(skip_type_params(T))]
pub struct Order<T: Config> {
pub order_id: T::OrderIdNonce,
pub placing_account: T::AccountId,
pub currency_in: T::CurrencyId,
pub currency_out: T::CurrencyId,
pub amount_in: T::BalanceIn,
pub amount_out: T::BalanceOut,
pub amount_out_initial: T::BalanceOut,
pub ratio: OrderRatio<T::Ratio>,
}
#[pallet::storage]
pub type Orders<T: Config> = StorageMap<
_,
Twox64Concat,
T::OrderIdNonce,
Order<T>,
ResultQuery<Error<T>::OrderNotFound>,
>;
#[pallet::storage]
pub type UserOrders<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Twox64Concat,
T::OrderIdNonce,
(),
ResultQuery<Error<T>::OrderNotFound>,
>;
#[pallet::storage]
pub type OrderIdNonceStore<T: Config> = StorageValue<_, T::OrderIdNonce, ValueQuery>;
#[pallet::storage]
pub type MarketFeederId<T: Config> =
StorageValue<_, T::FeederId, ResultQuery<Error<T>::MarketFeederNotFound>>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
OrderCreated {
order_id: T::OrderIdNonce,
creator_account: T::AccountId,
currency_in: T::CurrencyId,
currency_out: T::CurrencyId,
amount_out: T::BalanceOut,
min_fulfillment_amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
},
OrderCancelled {
account: T::AccountId,
order_id: T::OrderIdNonce,
},
OrderUpdated {
order_id: T::OrderIdNonce,
account: T::AccountId,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
min_fulfillment_amount_out: T::BalanceOut,
},
OrderFulfillment {
order_id: T::OrderIdNonce,
placing_account: T::AccountId,
fulfilling_account: T::AccountId,
partial_fulfillment: bool,
fulfillment_amount: T::BalanceOut,
currency_in: T::CurrencyId,
currency_out: T::CurrencyId,
ratio: T::Ratio,
},
FeederChanged { feeder_id: T::FeederId },
}
#[pallet::error]
#[derive(PartialEq)]
pub enum Error<T> {
SameCurrencyIds,
BelowMinFulfillmentAmount,
InvalidCurrencyId,
OrderNotFound,
Unauthorised,
FulfillAmountTooLarge,
MarketFeederNotFound,
MarketRatioNotFound,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::Weights::place_order())]
pub fn place_order(
origin: OriginFor<T>,
currency_in: T::CurrencyId,
currency_out: T::CurrencyId,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
Self::inner_place_order(
account_id,
currency_in,
currency_out,
amount_out,
ratio,
Self::min_fulfillment_amount(currency_out)?,
)?;
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(T::Weights::update_order())]
pub fn update_order(
origin: OriginFor<T>,
order_id: T::OrderIdNonce,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let order = Orders::<T>::get(order_id)?;
ensure!(
account_id == order.placing_account,
Error::<T>::Unauthorised
);
Self::inner_update_order(
order.clone(),
amount_out,
ratio,
Self::min_fulfillment_amount(order.currency_out)?,
)
}
#[pallet::call_index(2)]
#[pallet::weight(T::Weights::cancel_order())]
pub fn cancel_order(origin: OriginFor<T>, order_id: T::OrderIdNonce) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let order = <Orders<T>>::get(order_id)?;
ensure!(
account_id == order.placing_account,
Error::<T>::Unauthorised
);
<Self as TokenSwaps<T::AccountId>>::cancel_order(order_id)
}
#[pallet::call_index(3)]
#[pallet::weight(T::Weights::fill_order())]
pub fn fill_order(
origin: OriginFor<T>,
order_id: T::OrderIdNonce,
amount_out: T::BalanceOut,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let order = <Orders<T>>::get(order_id)?;
Self::fulfill_order_with_amount(order, amount_out, account_id)
}
#[pallet::call_index(4)]
#[pallet::weight(T::Weights::set_market_feeder())]
pub fn set_market_feeder(origin: OriginFor<T>, feeder_id: T::FeederId) -> DispatchResult {
T::AdminOrigin::ensure_origin(origin)?;
MarketFeederId::<T>::put(feeder_id.clone());
Self::deposit_event(Event::<T>::FeederChanged { feeder_id });
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn inner_place_order(
account: T::AccountId,
currency_in: T::CurrencyId,
currency_out: T::CurrencyId,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
min_fulfillment_amount_out: T::BalanceOut,
) -> Result<T::OrderIdNonce, DispatchError> {
let order_id = OrderIdNonceStore::<T>::try_mutate(|n| {
n.ensure_add_assign(One::one())?;
Ok::<_, DispatchError>(*n)
})?;
ensure!(
amount_out >= min_fulfillment_amount_out,
Error::<T>::BelowMinFulfillmentAmount
);
ensure!(currency_in != currency_out, Error::<T>::SameCurrencyIds);
T::Currency::hold(currency_out, &(), &account, amount_out.into())?;
let new_order = Order {
order_id,
placing_account: account.clone(),
currency_in,
currency_out,
amount_out,
ratio,
amount_out_initial: amount_out,
amount_in: Zero::zero(),
};
Orders::<T>::insert(order_id, new_order.clone());
UserOrders::<T>::insert(&account, order_id, ());
Self::deposit_event(Event::OrderCreated {
creator_account: account,
ratio,
order_id,
amount_out,
currency_in,
currency_out,
min_fulfillment_amount_out,
});
Ok(order_id)
}
fn inner_update_order(
mut order: Order<T>,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
min_fulfillment_amount_out: T::BalanceOut,
) -> DispatchResult {
ensure!(
amount_out >= min_fulfillment_amount_out,
Error::<T>::BelowMinFulfillmentAmount
);
match amount_out.cmp(&order.amount_out) {
Ordering::Greater => {
let amount_diff = amount_out.ensure_sub(order.amount_out)?;
order.amount_out_initial.ensure_add_assign(amount_diff)?;
T::Currency::hold(
order.currency_out,
&(),
&order.placing_account,
amount_diff.into(),
)?;
}
Ordering::Less => {
let amount_diff = order.amount_out.ensure_sub(amount_out)?;
order.amount_out_initial.ensure_sub_assign(amount_diff)?;
T::Currency::release(
order.currency_out,
&(),
&order.placing_account,
amount_diff.into(),
Precision::Exact,
)?;
}
Ordering::Equal => (),
}
order.amount_out = amount_out;
order.ratio = ratio;
Orders::<T>::insert(order.order_id, order.clone());
Self::deposit_event(Event::OrderUpdated {
account: order.placing_account,
order_id: order.order_id,
amount_out,
ratio,
min_fulfillment_amount_out,
});
Ok(())
}
pub fn remove_order(order_id: T::OrderIdNonce) -> DispatchResult {
let order = <Orders<T>>::get(order_id)?;
Orders::<T>::remove(order.order_id);
UserOrders::<T>::remove(&order.placing_account, order.order_id);
Ok(())
}
fn fulfill_order_with_amount(
order: Order<T>,
amount_out: T::BalanceOut,
fulfilling_account: T::AccountId,
) -> DispatchResult {
let min_fulfillment_amount_out = min(
order.amount_out,
Self::min_fulfillment_amount(order.currency_out)?,
);
ensure!(
amount_out >= min_fulfillment_amount_out,
Error::<T>::BelowMinFulfillmentAmount,
);
let ratio = match order.ratio {
OrderRatio::Market => Self::market_ratio(order.currency_out, order.currency_in)?,
OrderRatio::Custom(ratio) => ratio,
};
let amount_in =
Self::convert_with_ratio(order.currency_out, order.currency_in, ratio, amount_out)?;
let remaining_amount_out = order
.amount_out
.checked_sub(&amount_out)
.ok_or(Error::<T>::FulfillAmountTooLarge)?;
let partial_fulfillment = !remaining_amount_out.is_zero();
if partial_fulfillment {
let mut updated_order = order.clone();
updated_order.amount_out = remaining_amount_out;
updated_order.amount_in = order.amount_in.ensure_add(amount_in)?;
Orders::<T>::insert(updated_order.order_id, updated_order.clone());
} else {
Self::remove_order(order.order_id)?;
}
T::Currency::release(
order.currency_out,
&(),
&order.placing_account,
amount_out.into(),
Precision::Exact,
)?;
if T::Currency::balance(order.currency_out, &order.placing_account) < amount_out.into()
{
Err(DispatchError::Token(TokenError::FundsUnavailable))?
}
if T::Currency::balance(order.currency_in, &fulfilling_account) < amount_in.into() {
Err(DispatchError::Token(TokenError::FundsUnavailable))?
}
T::Currency::transfer(
order.currency_out,
&order.placing_account,
&fulfilling_account,
amount_out.into(),
Preservation::Expendable,
)?;
T::Currency::transfer(
order.currency_in,
&fulfilling_account,
&order.placing_account,
amount_in.into(),
Preservation::Expendable,
)?;
T::FulfilledOrderHook::notify_status_change(
order.order_id,
SwapInfo {
remaining: Swap {
amount_out: remaining_amount_out,
currency_in: order.currency_in,
currency_out: order.currency_out,
},
swapped_in: amount_in,
swapped_out: amount_out,
ratio,
},
)?;
Self::deposit_event(Event::OrderFulfillment {
order_id: order.order_id,
placing_account: order.placing_account,
fulfilling_account,
partial_fulfillment,
currency_in: order.currency_in,
currency_out: order.currency_out,
fulfillment_amount: amount_out,
ratio,
});
Ok(())
}
pub fn market_ratio(
currency_from: T::CurrencyId,
currency_to: T::CurrencyId,
) -> Result<T::Ratio, DispatchError> {
let feeder = MarketFeederId::<T>::get()?;
T::RatioProvider::get(&feeder, &(currency_from, currency_to))?
.ok_or(Error::<T>::MarketRatioNotFound.into())
}
pub fn convert_with_ratio(
currency_from: T::CurrencyId,
currency_to: T::CurrencyId,
ratio: T::Ratio,
amount_from: T::BalanceOut,
) -> Result<T::BalanceIn, DispatchError> {
let from_decimals = T::AssetRegistry::metadata(¤cy_from)
.ok_or(Error::<T>::InvalidCurrencyId)?
.decimals;
let to_decimals = T::AssetRegistry::metadata(¤cy_to)
.ok_or(Error::<T>::InvalidCurrencyId)?
.decimals;
let amount_in = ratio.ensure_mul_int(amount_from)?;
Ok(convert_balance_decimals(from_decimals, to_decimals, amount_in.into())?.into())
}
pub fn min_fulfillment_amount(
currency: T::CurrencyId,
) -> Result<T::BalanceOut, DispatchError> {
let to_decimals = T::AssetRegistry::metadata(¤cy)
.ok_or(Error::<T>::InvalidCurrencyId)?
.decimals;
Ok(convert_balance_decimals(
T::NativeDecimals::get(),
to_decimals,
T::MinFulfillmentAmountNative::get().into(),
)?
.into())
}
}
impl<T: Config> TokenSwaps<T::AccountId> for Pallet<T> {
type BalanceIn = T::BalanceIn;
type BalanceOut = T::BalanceOut;
type CurrencyId = T::CurrencyId;
type OrderId = T::OrderIdNonce;
type Ratio = T::Ratio;
fn place_order(
account: T::AccountId,
currency_in: T::CurrencyId,
currency_out: T::CurrencyId,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
) -> Result<Self::OrderId, DispatchError> {
Self::inner_place_order(
account,
currency_in,
currency_out,
amount_out,
ratio,
T::BalanceOut::zero(),
)
}
fn cancel_order(order: Self::OrderId) -> DispatchResult {
let order = <Orders<T>>::get(order)?;
let account_id = order.placing_account.clone();
T::Currency::release(
order.currency_out,
&(),
&order.placing_account,
order.amount_out.into(),
Precision::Exact,
)?;
Self::remove_order(order.order_id)?;
Self::deposit_event(Event::OrderCancelled {
account: account_id,
order_id: order.order_id,
});
Ok(())
}
fn update_order(
order_id: Self::OrderId,
amount_out: T::BalanceOut,
ratio: OrderRatio<T::Ratio>,
) -> DispatchResult {
let order = Orders::<T>::get(order_id)?;
Self::inner_update_order(order, amount_out, ratio, T::BalanceOut::zero())
}
fn get_order_details(
order: Self::OrderId,
) -> Option<OrderInfo<Self::BalanceOut, Self::CurrencyId, Self::Ratio>> {
Orders::<T>::get(order)
.map(|order| OrderInfo {
swap: Swap {
currency_in: order.currency_in,
currency_out: order.currency_out,
amount_out: order.amount_out,
},
ratio: order.ratio,
})
.ok()
}
fn fill_order(
account: T::AccountId,
order_id: Self::OrderId,
buy_amount: T::BalanceOut,
) -> DispatchResult {
let order = <Orders<T>>::get(order_id)?;
Self::fulfill_order_with_amount(order, buy_amount, account)
}
fn convert_by_market(
currency_in: Self::CurrencyId,
currency_out: Self::CurrencyId,
amount_out: T::BalanceOut,
) -> Result<T::BalanceIn, DispatchError> {
if currency_in == currency_out {
let amount: BalanceOf<T> = amount_out.into();
return Ok(amount.into());
}
let ratio = Self::market_ratio(currency_out, currency_in)?;
Self::convert_with_ratio(currency_out, currency_in, ratio, amount_out)
}
fn market_ratio(
currency_in: Self::CurrencyId,
currency_out: Self::CurrencyId,
) -> Result<Self::Ratio, DispatchError> {
Self::market_ratio(currency_out, currency_in)
}
}
#[cfg(feature = "runtime-benchmarks")]
impl<T: Config> ValueProvider<(), (T::CurrencyId, T::CurrencyId)> for Pallet<T> {
type Value = T::Ratio;
fn get(
_: &(),
(currency_out, currency_in): &(T::CurrencyId, T::CurrencyId),
) -> Result<Option<Self::Value>, DispatchError> {
Self::market_ratio(*currency_out, *currency_in).map(Some)
}
fn set(_: &(), pair: &(T::CurrencyId, T::CurrencyId), value: Self::Value) {
let feeder = MarketFeederId::<T>::get().unwrap();
T::RatioProvider::set(&feeder, &pair, value);
}
}
}