Skip to main content

Pool Staking

#![cfg_attr(not(feature = "std"), no_std)]

/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;
use sp_std::vec;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

pub mod pool_manager;
pub use pool_manager::PoolManager;

mod types;
pub use types::{ StakePoolActivity, UnstakePoolActivity };

use traits::pool::PoolInterface;

// #[cfg(feature = "runtime-benchmarks")]
// mod benchmarking;

/// Type for unstake pool activities
pub type UnstakePoolActivityOf<T> =
UnstakePoolActivity<<T as frame_system::Config>::AccountId, AssetId<T>, BalanceOf<T>, u64>;

/// Type for stake pool activities
pub type StakePoolActivityOf<T> =
StakePoolActivity<<T as frame_system::Config>::AccountId, AssetId<T>, BalanceOf<T>, u64>;

#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
pallet_prelude::*,
sp_runtime::traits::AccountIdConversion,
traits::{
fungibles,

fungibles::{ Inspect, Mutate },
UnixTime,
tokens::Preservation::Expendable,
},
PalletId,
};
use sp_runtime::{ traits::CheckedAdd, DispatchError, SaturatedConversion, Saturating };
use sp_std::vec::Vec;

#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);

/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

/// Type to access the Assets Pallet.
type Fungibles: fungibles::metadata::Inspect<Self::AccountId> +
fungibles::Inspect<Self::AccountId> + //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId> +
fungibles::Create<Self::AccountId>;

#[pallet::constant]
type PalletId: Get<PalletId>;
type Time: UnixTime;
}

pub type BalanceOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;

pub type AssetId<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;

/// LiquidityPool: map AssetId => (AssetIdBalance, UsduBalance)
#[pallet::storage]
#[pallet::getter(fn liquidity_pool)]
pub type LiquidityPool<T: Config> = StorageMap<
_,
Twox64Concat,
AssetId<T>,
(BalanceOf<T>, BalanceOf<T>),
ValueQuery
>;

/// UserPoolShares: map user => AssetId => ShareBalance
#[pallet::storage]
#[pallet::getter(fn user_pool_shares)]
pub type UserPoolShares<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Twox64Concat,
AssetId<T>,
BalanceOf<T>,
ValueQuery
>;

/// PoolShares: map AssetId => UsduBalance
#[pallet::storage]
#[pallet::getter(fn pool_shares)]
pub type PoolShares<T: Config> = StorageMap<
_,
Twox64Concat,
AssetId<T>,
BalanceOf<T>,
ValueQuery
>;

/// StakePoolActivities
#[pallet::storage]
#[pallet::getter(fn stake_pool_activities)]
pub type StakePoolActivities<T: Config> = StorageMap<
_,
Twox64Concat,
AssetId<T>,
Vec<StakePoolActivityOf<T>>,
ValueQuery
>;

/// UnstakePoolActivities
#[pallet::storage]
#[pallet::getter(fn unstake_pool_activities)]
pub type UnstakePoolActivities<T: Config> = StorageMap<
_,
Twox64Concat,
AssetId<T>,
Vec<UnstakePoolActivityOf<T>>,
ValueQuery
>;

// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Add liquidity success.
AddLiquidity {
who: T::AccountId,
asset_id: AssetId<T>,
amount: BalanceOf<T>,
usdu_id: AssetId<T>,
usdu_amount: BalanceOf<T>,
share_increment: BalanceOf<T>,
},
/// Remove liquidity from the trading pool success.
RemoveLiquidity {
who: T::AccountId,
asset_id: AssetId<T>,
amount: BalanceOf<T>,
usdu_id: AssetId<T>,
usdu_amount: BalanceOf<T>,
},
}

// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
//Asset does not exist
AssetDoesNotExist,
// The user has not deposed token in the past
NoTokensDeposed,
// Not enought tokens in the pool to transfer back
NotEnoughToken,
// Pair of token dose not exist
PoolDoesNotExist,
// Invalid token amount
InvalidLiquidityIncrement,
// If the staking ratio is not correct
InvalidStakingRatio,
// Maths problems
Overflow,
Underflow,
// User has not enought shares
InsufficientShares,
/// Too low min tokens
TooLowMinTokens,
/// Too low min liquidity
TooLowMinLiquidity,
}

// Dispatchable functions allows users to interact with the pallet and invoke state changes.
#[pallet::call]
impl<T: Config> Pallet<T> {}

impl<T: Config> Pallet<T> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}

pub fn asset_exists(asset_id: AssetId<T>) -> bool {
T::Fungibles::asset_exists(asset_id)
}

pub fn u64_to_fungible_balance(input: u64) -> BalanceOf<T> {
input.try_into().ok().unwrap()
}
}

impl<T: Config> PoolManager<T::AccountId, AssetId<T>, BalanceOf<T>> for Pallet<T> {
fn stake(
who: &T::AccountId,
token_id: AssetId<T>,
token_amount: BalanceOf<T>,
usdu_id: AssetId<T>,
usdu_amount: BalanceOf<T>
) -> Result<(BalanceOf<T>, BalanceOf<T>, BalanceOf<T>), DispatchError> {

// Ensure that both `amount` and `usdu_amount` are non-zero.
ensure!(
token_amount != (0u128).saturated_into::<BalanceOf<T>>() &&
usdu_amount != (0u128).saturated_into::<BalanceOf<T>>(),
Error::<T>::InvalidLiquidityIncrement
);

// Get the current balances of the token and USDU tokens in the liquidity pool.
let (current_pool_token_balance, _current_pool_usdu_balance) = LiquidityPool::<T>::get(
token_id.clone()
);

// Calculate the share count to issue based on tokens deposited
// If pool is empty, issue shares equivalent to amount. Otherwise, issue proportionally
let receiving_share_count = if
current_pool_token_balance == (0u128).saturated_into::<BalanceOf<T>>()
{
token_amount
} else {
token_amount.saturating_mul(Self::pool_shares(token_id.clone())) /
current_pool_token_balance
};

let pool_id = Self::account_id();

// Transfer tokens to the pool
T::Fungibles::transfer(
token_id.clone(), // asset
who, // source
&pool_id, // dest
token_amount, // amount
Expendable // preservation
)?;

// Transfer USDU to the pool
T::Fungibles::transfer(
usdu_id.clone(), // asset
who, // source
&pool_id, // dest
usdu_amount, // amount
Expendable // preservation
)?;

// Update user shares
UserPoolShares::<T>::try_mutate(
who,
&token_id,
|shares| -> Result<(), DispatchError> {
*shares = shares.saturating_add(receiving_share_count);
Ok(())
}
)?;

// Update total pool shares
PoolShares::<T>::try_mutate(
&token_id,
|shares| -> Result<(), DispatchError> {
*shares += receiving_share_count;
Ok(())
}
)?;

// Update stake pool activities
StakePoolActivities::<T>::try_mutate(
&token_id,
|activities| -> Result<(), DispatchError> {
let activity = StakePoolActivity {
account: who.clone(),
asset_id: token_id.clone(),
amount: token_amount,
usdu_id: usdu_id.clone(),
usdu_amount,
shares: receiving_share_count,
time: T::Time::now().as_secs(),
};
activities.push(activity);
Ok(())
}
)?;
// LiquidityPool update
LiquidityPool::<T>::try_mutate(
&token_id,
|(token_balance, usdu_balance)| -> Result<(), DispatchError> {
*token_balance = token_balance
.checked_add(&token_amount)
.ok_or(Error::<T>::Overflow)?;
*usdu_balance = usdu_balance
.checked_add(&usdu_amount)
.ok_or(Error::<T>::Overflow)?;
Ok(())
}
)?;

Self::deposit_event(Event::AddLiquidity {
who: who.clone(),
asset_id: token_id,
amount: token_amount,
usdu_id,
usdu_amount,
share_increment: receiving_share_count,
});

Ok((token_amount, usdu_amount, receiving_share_count))
}

fn unstake(
who: &T::AccountId,
token_id: AssetId<T>,
usdu_id: AssetId<T>,
remove_share: BalanceOf<T>,
min_tokens: Option<BalanceOf<T>>,
min_usdu: Option<BalanceOf<T>>,
) -> Result<(BalanceOf<T>, BalanceOf<T>), DispatchError> {
ensure!(
remove_share != (0u128).saturated_into::<BalanceOf<T>>(),
Error::<T>::InvalidLiquidityIncrement
);

let (current_pool_token_balance, current_pool_usdu_balance) = LiquidityPool::<T>::get(
&token_id
);
let total_shares = Self::pool_shares(token_id.clone());

// Ensure the user has enough shares
let user_shares = UserPoolShares::<T>::get(who, &token_id);
ensure!(user_shares >= remove_share, Error::<T>::InsufficientShares);

// Calculate amount to transfer
let token_amount_to_withdraw =
remove_share.saturating_mul(current_pool_token_balance) / total_shares;
let usdu_amount_to_withdraw =
remove_share.saturating_mul(current_pool_usdu_balance) / total_shares;

// Ensure token_amount_to_withdraw is greater than min_tokens only if min_tokens is Some
if let Some(min_tokens) = min_tokens {
ensure!(
token_amount_to_withdraw >= min_tokens,
Error::<T>::TooLowMinTokens
);
}

// Ensure usdu_amount_to_withdraw is greater than min_liquidity only if min_liquidity is Some
if let Some(min_usdu) = min_usdu {
ensure!(
usdu_amount_to_withdraw >= min_usdu,
Error::<T>::TooLowMinLiquidity
);
}

// Update the pool
LiquidityPool::<T>::insert(&token_id, (
current_pool_token_balance.saturating_sub(token_amount_to_withdraw),
current_pool_usdu_balance.saturating_sub(usdu_amount_to_withdraw),
));

// Make the transfer
let pool_id = Self::account_id();
T::Fungibles::transfer(
token_id.clone(),
&pool_id,
who,
token_amount_to_withdraw,
Expendable
)?;
T::Fungibles::transfer(
usdu_id.clone(),
&pool_id,
who,
usdu_amount_to_withdraw,
Expendable
)?;

// Update user and total shares
UserPoolShares::<T>::insert(who, &token_id, user_shares.saturating_sub(remove_share));
PoolShares::<T>::mutate(&token_id, |shares| {
*shares = shares.saturating_sub(remove_share);
});

// Record unstake activity
UnstakePoolActivities::<T>::try_mutate(
&token_id,
|activities| -> Result<(), DispatchError> {
let activity = UnstakePoolActivity {
account: who.clone(),
asset_id: token_id.clone(),
amount: token_amount_to_withdraw,
usdu_id: usdu_id.clone(),
usdu_amount: usdu_amount_to_withdraw,
shares: remove_share,
time: T::Time::now().as_secs(),
};
activities.push(activity);
Ok(())
}
)?;
// Event
Self::deposit_event(Event::RemoveLiquidity {
who: who.clone(),
asset_id: token_id,
amount: token_amount_to_withdraw,
usdu_id,
usdu_amount: usdu_amount_to_withdraw,
});

Ok((token_amount_to_withdraw, usdu_amount_to_withdraw))
}

fn update_buy(
token_id: AssetId<T>,
token_amount: BalanceOf<T>,
usdu_amount: BalanceOf<T>
) -> Result<(), DispatchError> {

LiquidityPool::<T>::try_mutate(
token_id,
|(token_balance, usdu_balance)| -> Result<(), DispatchError> {

*token_balance -= token_amount;
*usdu_balance += usdu_amount;

Ok(())
}
)?;

Ok(())
}

fn update_sell(
token_id: AssetId<T>,
token_amount: BalanceOf<T>,
usdu_amount: BalanceOf<T>
) -> Result<(), DispatchError> {

LiquidityPool::<T>::try_mutate(
token_id,
|(token_balance, usdu_balance)| -> Result<(), DispatchError> {

*token_balance += token_amount;
*usdu_balance -= usdu_amount;

Ok(())
}
)?;

Ok(())
}

fn get_pool_liquidity(token_id: AssetId<T>) -> Option<(BalanceOf<T>, BalanceOf<T>)> {
let (token_balance, usdu_balance) = LiquidityPool::<T>::get(token_id);
Some((token_balance, usdu_balance))
}

fn get_pool_account_id() -> T::AccountId {
Self::account_id()
}
}

impl<T: Config> PoolInterface<AssetId<T>, BalanceOf<T>> for Pallet<T> {
fn get_liquidity_pool(token_id: AssetId<T>) -> (BalanceOf<T>, BalanceOf<T>) {
let (token_balance, usdu_balance) = LiquidityPool::<T>::get(token_id);
(token_balance, usdu_balance)
}
}
}