#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode};
use frame_support::traits::{Currency, ExistenceRequirement::AllowDeath};
use frame_support::{
decl_error, decl_event, decl_module, decl_storage,
dispatch::{DispatchError, DispatchResult},
ensure,
};
use frame_system::{self as system, ensure_signed};
use pallet_collection::{CollectionInterface, TokenType};
use pallet_nft::NFTInterface;
use sp_runtime::{
traits::{AccountIdConversion, CheckedAdd, CheckedMul, CheckedSub, SaturatedConversion},
ModuleId,
};
use substrate_fixed::{transcendental::pow, types::I64F64};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
const PALLET_ID: ModuleId = ModuleId(*b"Exchange");
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
#[derive(Encode, Decode, Default, Clone, PartialEq)]
pub struct NonFungibleOrderInfo<Hash, AccountId, Balance> {
pub collection_id: Hash,
pub start_idx: u128,
pub seller: AccountId,
pub price: Balance,
pub amount: u128,
}
#[derive(Encode, Decode, Default, Clone, PartialEq)]
pub struct SemiFungiblePoolInfo<AccountId, Balance, BlockNumber> {
pub seller: AccountId,
pub supply: u128,
pub m: u128,
pub sold: u128,
pub reverse_ratio: u128,
pub pool_balance: Balance,
pub end_time: BlockNumber,
}
pub trait Config: frame_system::Config {
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
type Currency: Currency<Self::AccountId>;
type Collection: CollectionInterface<Self::Hash, Self::AccountId>;
type NFT: NFTInterface<Self::Hash, Self::AccountId>;
}
decl_storage! {
trait Store for Module<T: Config> as ExchangeModule {
NextNonFungibleOrderId get(fn next_nft_order_id): u128 = 0;
NonFungibleOrders get(fn nft_order): map hasher(blake2_128_concat) u128 => NonFungibleOrderInfo<T::Hash, T::AccountId, BalanceOf<T>>;
SemiFungiblePools get (fn semi_fungible_pool): map hasher(blake2_128_concat) (T::Hash, T::AccountId) => SemiFungiblePoolInfo<T::AccountId, BalanceOf<T>, T::BlockNumber>;
}
}
decl_event!(
pub enum Event<T>
where
AccountId = <T as frame_system::Config>::AccountId,
Hash = <T as frame_system::Config>::Hash,
Balance =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance,
{
NonFungibleOrderCreated(AccountId, u128),
NonFungibleOrderCanceled(AccountId, u128),
NonFungibleSold(AccountId, u128),
SemiFungiblePoolCreated(Hash),
SemiFungiblePoolWithdrew(Hash, AccountId),
SemiFungibleBought(Hash, Balance),
SemiFungibleSold(Hash, Balance),
}
);
decl_error! {
pub enum Error for Module<T: Config> {
NumOverflow,
CollectionNotFound,
TokenNotFound,
OrderNotFound,
PoolNotFound,
PoolExisted,
AmountTooLarge,
AmountLessThanOne,
ReverseRatioLessThanOne,
MLessThanOne,
PermissionDenied,
WrongTokenType,
ExpiredSoldTime,
CanNotWithdraw,
}
}
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::Origin {
type Error = Error<T>;
fn deposit_event() = default;
#[weight = 10_000]
pub fn sell_nft(origin, collection_id: T::Hash, token_id: u128, amount: u128, price: BalanceOf<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(T::NFT::token_exist(collection_id, token_id), Error::<T>::TokenNotFound);
let token = T::NFT::get_nft_token(collection_id, token_id);
ensure!(token.owner == who, Error::<T>::PermissionDenied);
let nft_order_id = Self::next_nft_order_id();
let next_nft_order_id = nft_order_id.checked_add(1).ok_or(Error::<T>::NumOverflow)?;
T::NFT::_transfer_non_fungible(who.clone(), Self::account_id(), collection_id, token_id, amount)?;
let order_info = NonFungibleOrderInfo {
collection_id,
start_idx: token_id,
seller: who.clone(),
price,
amount
};
NonFungibleOrders::<T>::insert(nft_order_id, order_info);
NextNonFungibleOrderId::put(next_nft_order_id);
Self::deposit_event(RawEvent::NonFungibleOrderCreated(
who,
nft_order_id
));
Ok(())
}
#[weight = 10_000]
pub fn buy_nft(origin, order_id: u128, amount: u128) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(amount >= 1, Error::<T>::AmountLessThanOne);
ensure!(NonFungibleOrders::<T>::contains_key(order_id), Error::<T>::OrderNotFound);
let order = Self::nft_order(order_id);
ensure!(order.amount >= amount, Error::<T>::AmountTooLarge);
let price = &order.price;
let b_amout = amount.saturated_into::<BalanceOf<T>>();
let cost = price.checked_mul(&b_amout).ok_or(Error::<T>::NumOverflow)?;
let left_amount = &order.amount.checked_sub(amount).ok_or(Error::<T>::NumOverflow)?;
let collection_id = &order.collection_id;
let token_id = &order.start_idx;
T::Currency::transfer(&who, &order.seller, cost, AllowDeath)?;
T::NFT::_transfer_non_fungible(Self::account_id(), who.clone(), *collection_id, *token_id, amount)?;
if *left_amount == 0 {
NonFungibleOrders::<T>::remove(order_id);
} else {
let start_idx = token_id.checked_add(&amount).ok_or(Error::<T>::NumOverflow)?;
let order = NonFungibleOrderInfo {
amount: *left_amount,
start_idx,
..order
};
NonFungibleOrders::<T>::insert(order_id, order);
}
Self::deposit_event(RawEvent::NonFungibleSold(
who,
*left_amount
));
Ok(())
}
#[weight = 10_000]
pub fn cancel_nft_order(origin, order_id: u128) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(NonFungibleOrders::<T>::contains_key(order_id), Error::<T>::OrderNotFound);
let order = Self::nft_order(order_id);
ensure!(order.seller == who, Error::<T>::PermissionDenied);
let amount = &order.amount;
let collection_id = &order.collection_id;
let token_id = &order.start_idx;
T::NFT::_transfer_non_fungible(Self::account_id(), who.clone(), *collection_id, *token_id, *amount)?;
NonFungibleOrders::<T>::remove(order_id);
Self::deposit_event(RawEvent::NonFungibleOrderCanceled(who, order_id));
Ok(())
}
#[weight = 10_000]
pub fn create_semi_token_pool(origin, collection_id: T::Hash, amount: u128, reverse_ratio: u128, m: u128, duration: T::BlockNumber) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(reverse_ratio >=1, Error::<T>::ReverseRatioLessThanOne);
ensure!(m >=1, Error::<T>::MLessThanOne);
ensure!(amount >= 1, Error::<T>::AmountLessThanOne);
ensure!(!SemiFungiblePools::<T>::contains_key((&collection_id, &who)), Error::<T>::PoolExisted);
ensure!(T::Collection::collection_exist(collection_id), Error::<T>::CollectionNotFound);
ensure!(T::NFT::get_balance(&collection_id, &who) >= amount, Error::<T>::AmountTooLarge);
let collection = T::Collection::get_collection(collection_id);
if let Some(token_type) = collection.token_type {
ensure!(
token_type == TokenType::Fungible,
Error::<T>::WrongTokenType
);
}
let block_number = <system::Pallet<T>>::block_number();
let end_time = block_number.checked_add(&duration).ok_or(Error::<T>::NumOverflow)?;
let pool = SemiFungiblePoolInfo {
m,
reverse_ratio,
end_time,
sold: 0,
seller: who.clone(),
supply: amount,
pool_balance: 0_u128.saturated_into::<BalanceOf<T>>(),
};
T::NFT::_transfer_fungible(who.clone(), Self::account_id(), collection_id, amount)?;
SemiFungiblePools::<T>::insert((&collection_id, &who), pool);
Self::deposit_event(RawEvent::SemiFungiblePoolCreated(
collection_id
));
Ok(())
}
#[weight = 10_000]
pub fn buy_semi_token(origin, collection_id: T::Hash, seller: T::AccountId, amount: u128) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(SemiFungiblePools::<T>::contains_key((&collection_id, &seller)), Error::<T>::PoolNotFound);
let pool = Self::semi_fungible_pool((&collection_id, &seller));
ensure!(amount <= pool.supply, Error::<T>::AmountTooLarge);
let block_number = <system::Pallet<T>>::block_number();
ensure!(block_number <= pool.end_time, Error::<T>::ExpiredSoldTime);
let reverse_ratio = &pool.reverse_ratio;
let total_supply = &pool.sold;
let cost = if pool.sold == 0 {
let m = &pool.m;
Self::first_buy_cost(*reverse_ratio, *m, amount)?
} else {
let pool_balance = &pool.pool_balance;
Self::buy_cost(*pool_balance, amount, *total_supply, *reverse_ratio)?
};
let cost = cost.saturated_into::<BalanceOf<T>>();
let sold = &pool.sold.checked_add(amount).ok_or(Error::<T>::NumOverflow)?;
let supply = &pool.supply.checked_sub(amount).ok_or(Error::<T>::NumOverflow)?;
let pool_balance = pool.pool_balance.clone().checked_add(&cost).ok_or(Error::<T>::NumOverflow)?;
let pool = SemiFungiblePoolInfo {
sold: *sold,
supply: *supply,
pool_balance,
..pool
};
T::Currency::transfer(&who, &Self::account_id(), cost, AllowDeath)?;
T::NFT::_transfer_fungible(Self::account_id(), who, collection_id, amount)?;
SemiFungiblePools::<T>::insert((&collection_id, &seller), pool);
Self::deposit_event(RawEvent::SemiFungibleBought(
collection_id,
cost
));
Ok(())
}
#[weight = 10_000]
pub fn sell_semi_token(origin, collection_id: T::Hash, seller: T::AccountId, amount: u128) -> DispatchResult {
let who = ensure_signed(origin)?;
let pool_id = (&collection_id, &seller);
ensure!(SemiFungiblePools::<T>::contains_key(pool_id), Error::<T>::PoolNotFound);
let pool = Self::semi_fungible_pool((&collection_id, &seller));
ensure!(amount >= 1, Error::<T>::AmountLessThanOne);
ensure!(pool.sold >= amount, Error::<T>::AmountTooLarge);
let block_number = <system::Pallet<T>>::block_number();
ensure!(block_number <= pool.end_time, Error::<T>::ExpiredSoldTime);
let reverse_ratio = &pool.reverse_ratio;
let total_supply = &pool.sold;
let pool_balance = &pool.pool_balance;
let receive = Self::sell_receive(*pool_balance, amount, *total_supply, *reverse_ratio)?;
let receive = receive.saturated_into::<BalanceOf<T>>();
let new_pool_balance = pool.pool_balance.clone().checked_sub(&receive).ok_or(Error::<T>::NumOverflow)?;
let sold = &pool.sold.checked_sub(amount).ok_or(Error::<T>::NumOverflow)?;
let supply = &pool.supply.checked_add(amount).ok_or(Error::<T>::NumOverflow)?;
let pool = SemiFungiblePoolInfo {
sold: *sold,
supply: *supply,
pool_balance: new_pool_balance,
..pool
};
T::NFT::_transfer_fungible(who.clone(), Self::account_id(), collection_id, amount)?;
T::Currency::transfer(&Self::account_id(), &who, receive, AllowDeath)?;
SemiFungiblePools::<T>::insert(pool_id, pool);
Self::deposit_event(RawEvent::SemiFungibleSold(
collection_id,
receive
));
Ok(())
}
#[weight = 10_000]
pub fn withdraw_pool(origin, collection_id: T::Hash) -> DispatchResult {
let who = ensure_signed(origin)?;
let pool_id = (&collection_id, &who);
ensure!(SemiFungiblePools::<T>::contains_key(pool_id), Error::<T>::PoolNotFound);
let pool = Self::semi_fungible_pool((&collection_id, &who));
ensure!(pool.seller == who, Error::<T>::PermissionDenied);
let block_number = <system::Pallet<T>>::block_number();
ensure!(block_number > pool.end_time, Error::<T>::CanNotWithdraw);
let pool_balance = &pool.pool_balance;
let supply = &pool.supply;
SemiFungiblePools::<T>::remove(pool_id);
T::NFT::_transfer_fungible(Self::account_id(), who.clone(), collection_id, *supply)?;
T::Currency::transfer(&Self::account_id(), &who, *pool_balance, AllowDeath)?;
Self::deposit_event(RawEvent::SemiFungiblePoolWithdrew(
collection_id,
who,
));
Ok(())
}
}
}
impl<T: Config> Module<T> {
pub fn account_id() -> T::AccountId {
PALLET_ID.into_account()
}
fn pow(operand: I64F64, reverse_ratio: u128) -> Result<I64F64, DispatchError> {
let max_weight = 1000000;
if reverse_ratio == max_weight {
return Ok(operand);
}
let exponent: I64F64 = I64F64::from_num(max_weight) / I64F64::from_num(reverse_ratio);
let operand = I64F64::from_num(operand);
let result: I64F64 = pow(operand, exponent).map_err(|_| Error::<T>::NumOverflow)?;
Ok(result)
}
fn two_decimal_places(operand: I64F64) -> Result<I64F64, DispatchError> {
let hundred = I64F64::from_num(100);
let r = operand
.checked_mul(hundred)
.ok_or(Error::<T>::NumOverflow)?;
Ok(r.round() / 100)
}
fn first_buy_cost(reverse_ratio: u128, m: u128, amount: u128) -> Result<u128, DispatchError> {
let max_weight = I64F64::from_num(1000000);
let m = I64F64::from_num(m);
let amount = I64F64::from_num(amount);
let r: I64F64 = I64F64::from_num(reverse_ratio) / max_weight;
let exponent: I64F64 = I64F64::from_num(max_weight) / I64F64::from_num(reverse_ratio);
let operand: I64F64 = pow(amount, exponent).map_err(|_| Error::<T>::NumOverflow)?;
let operand = operand.checked_mul(m).ok_or(Error::<T>::NumOverflow)?;
let p = operand.checked_mul(r).ok_or(Error::<T>::NumOverflow)?;
let p = Self::two_decimal_places(p)?;
Ok(p.ceil().to_num::<u128>())
}
fn buy_cost(
pool_balance: BalanceOf<T>,
amount: u128,
total_supply: u128,
reverse_ratio: u128,
) -> Result<u128, DispatchError> {
let pool_balance = pool_balance.saturated_into::<u128>();
let one = I64F64::from_num(1);
let operand = I64F64::from_num(amount)
.checked_div(I64F64::from_num(total_supply))
.ok_or(Error::<T>::NumOverflow)?;
let operand = one.checked_add(operand).ok_or(Error::<T>::NumOverflow)?;
let p = Self::pow(operand, reverse_ratio)?;
let p = p.checked_sub(one).ok_or(Error::<T>::NumOverflow)?;
let p = I64F64::from_num(pool_balance)
.checked_mul(p)
.ok_or(Error::<T>::NumOverflow)?;
let p = Self::two_decimal_places(p)?;
Ok(p.ceil().to_num::<u128>())
}
fn sell_receive(
pool_balance: BalanceOf<T>,
amount: u128,
total_supply: u128,
reverse_ratio: u128,
) -> Result<u128, DispatchError> {
let pool_balance = pool_balance.saturated_into::<u128>();
let one = I64F64::from_num(1);
let operand = I64F64::from_num(amount)
.checked_div(I64F64::from_num(total_supply))
.ok_or(Error::<T>::NumOverflow)?;
let operand = one.checked_sub(operand).ok_or(Error::<T>::NumOverflow)?;
let p = Self::pow(operand, reverse_ratio)?;
let p = one.checked_sub(p).ok_or(Error::<T>::NumOverflow)?;
let p = I64F64::from_num(pool_balance)
.checked_mul(p)
.ok_or(Error::<T>::NumOverflow)?;
Ok(p.to_num::<u128>())
}
}