Skip to main content

Sale

//! # Unit Sales Pallet
//!
//! ## Overview
//!
//! Pallet that implements token sales for Unit Network.
//!
//! This is used to sell tokens at a fixed price.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create sales and offer tokens by the asset´s owner.
//! * Set bonuses by the asset´s owner.
//! * Change price and quantity for a sale when possible by the asset´s owner.
//! * Close an open sale by the asset´s owner.
//! * Buy tokens from a sale.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `buy_tokens`: Buy tokens from a stage.
//!
//! ### Privileged Functions
//!
//! - `create_sale_stage`: Create a stage where users can buy tokens from the bank.
//! - `update_price`: Update the price for a stage.
//! - `set_bonuses`: Update the bonuses percentage that will be paid to the inviter of the buyer.
//! - `update_quantity`: Update the quantity of tokens offered by a stage.
//! - `close_sale`: Close a sale stage.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
sp_runtime::{
traits::{
CheckedDiv,
CheckedMul,
SaturatedConversion,
Saturating,
Zero,
AccountIdConversion,
},
FixedPointOperand,
},
traits::{
fungibles,
fungibles::{ metadata::Inspect as InspectMetadata, Inspect, Mutate },
tokens::{ Balance, Preservation::Expendable },
Time,
},
PalletId,
};
pub use pallet::*;
use sp_arithmetic::Perbill;
use sp_std::vec::Vec;
use sp_std::vec;
use scale_info::prelude::format;

use traits::{
asset::{ AssetInterface, TokenType },
bank::BankInterface,
oracle::OracleInspect,
pool::PoolInterface,
profile::ProfileInspect,
subaccounts::{ SubAccounts, AccountOrigin },
teamsadvisors::TeamsAdvisorsInspect,
newsfeed::{ NewsfeedInterface, StatusType },
};
pub mod weights;
pub use weights::WeightInfo;

#[cfg(test)]
mod mock;

#[cfg(test)]
mod tests;

#[cfg(test)]
mod testing_utils;

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

#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;

pub type OracleKey<T> = <T as pallet_oracle::Config>::OracleKey;
pub type OracleValue<T> = <T as pallet_oracle::Config>::OracleValue;
pub type MomentOf<T> = <<T as Config>::Time as Time>::Moment;
/// The balance type of this pallet.
pub type BalanceOf<T> = <T as Config>::AssetBalance;
pub type AssetIdOf<T> = <T as Config>::AssetId;
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;

pub type BonusOf<T> = Bonus<AssetIdOf<T>, BalanceOf<T>, AccountIdOf<T>, MomentOf<T>>;

pub type Decimals = u8;

#[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 + pallet_oracle::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::Inspect<
Self::AccountId,
AssetId = <Self as pallet::Config>::AssetId,
Balance = Self::AssetBalance
> + //, AssetId = u32> // Hash this
InspectMetadata<Self::AccountId> +
fungibles::Mutate<Self::AccountId> +
AssetInterface<
<Self as pallet::Config>::AssetId,
Self::AssetBalance,
Self::AccountId,
Decimals,
TokenType
>;

type AssetId: Member +
Parameter +
Copy +
MaybeSerializeDeserialize +
MaxEncodedLen +
Default +
Zero +
From<u32> +
Into<u32>;

type AssetBalance: Balance +
FixedPointOperand +
MaxEncodedLen +
MaybeSerializeDeserialize +
TypeInfo;

// Interface to interact with bank pallet
type Bank: BankInterface<AssetIdOf<Self>, BalanceOf<Self>, Self::AccountId>;

// Trait to read teams advisors pallet
type TeamsAdvisors: TeamsAdvisorsInspect<AssetIdOf<Self>, Self::AccountId>;

type Oracle: OracleInspect<AssetIdOf<Self>, OracleKey<Self>, OracleValue<Self>>;

// Type to access the Pool Pallet.
type Pool: PoolInterface<AssetIdOf<Self>, BalanceOf<Self>>;

#[pallet::constant]
type StringLimit: Get<u32>;

/// Type to access the profile pallet
type Profile: ProfileInspect<Self::AccountId, Self::StringLimit>;

/// Time provider
type Time: Time;

/// Maximun percentage that a bonus can have.
#[pallet::constant]
type MaxBonus: Get<u8>;

/// Unit token Id
#[pallet::constant]
type UnitId: Get<u32>;

/// Usdu token Id
#[pallet::constant]
type UsduId: Get<u32>;

/// Decimals of USDU
#[pallet::constant]
type UsduTokenDecimals: Get<u32>;

/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;

// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;

/// Sales pallet id.
#[pallet::constant]
type PalletId: Get<PalletId>;

/// Newsfeed Interface type.
type Newsfeed: NewsfeedInterface<
Self::AccountId,
StatusType,
<Self as Config>::AssetId,
Self::AssetBalance
>;

// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type ProfileBenchmark;

#[cfg(feature = "runtime-benchmarks")]
type BankBenchmark;

#[cfg(feature = "runtime-benchmarks")]
type PoolBenchmark;
}

/// States that a stage can have.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub enum SaleStatus {
ACTIVE,
COMPLETED,
}

/// Role that the users can have.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub enum Role {
TEAMMEMBER,
ADVISOR,
COMMUNITY,
}

/// Information about a stage.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub struct SaleStage<AssetId, Balance> {
pub stage_id: u64,
pub asset_id: AssetId,
pub price: Balance,
pub quantity: Balance,
pub tokens_sold: Balance,
pub status: SaleStatus,
pub core_team_cash_bonus: u8,
pub core_team_token_bonus: u8,
pub advisor_cash_bonus: u8,
pub advisor_token_bonus: u8,
pub community_cash_bonus: u8,
pub community_token_bonus: u8,
}

/// Information about a completed sale.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Sale<AssetId, Balance, AccountId, BlockNumber> {
pub stage_id: u64,
pub sale_id: u64,
pub asset_id: AssetId,
pub user: AccountId,
pub price: Balance,
pub quantity: Balance,
pub tokens_sold: Balance,
pub total_price: Balance,
pub core_team_cash_bonus_paid: Balance,
pub core_team_token_bonus_paid: Balance,
pub advisor_cash_bonus_paid: Balance,
pub advisor_token_bonus_paid: Balance,
pub community_cash_bonus_paid: Balance,
pub community_token_bonus_paid: Balance,
pub time: BlockNumber,
pub asset_used: AssetId,
pub asset_used_price: Balance,
pub asset_used_amount: Balance,
}

/// Information about the bonus paid for a sale.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Bonus<AssetId, Balance, AccountId, Moment> {
pub asset_id: AssetId,
pub asset_used_id: AssetId,
pub origin: AccountId,
pub price: Balance,
pub purchased_quantity: Balance,
pub crypto_paid: Balance,
pub bonus_paid: Balance,
pub time: Moment,
pub is_asset: bool,
}

/// Sale counter.
#[pallet::storage]
#[pallet::getter(fn sale_counter)]
pub type SaleCounter<T: Config> = StorageValue<_, u64, ValueQuery>;

/// Stage identifier counter.
#[pallet::storage]
#[pallet::getter(fn stage_id)]
pub type StageId<T: Config> = StorageValue<_, u64, ValueQuery>;

/// Information about all sales made for a stage.
#[pallet::storage]
#[pallet::getter(fn sale)]
pub type SalesByStage<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
u64, //stage id
Vec<u64> // sales ids
>;

#[pallet::storage]
#[pallet::getter(fn all_sales)]
pub type AllSales<T: Config> = StorageMap<
_,
Blake2_128Concat,
u64, //sale id
Sale<AssetIdOf<T>, BalanceOf<T>, T::AccountId, BlockNumberFor<T>>
>;

/// Store stage information for an asset.
#[pallet::storage]
#[pallet::getter(fn asset_stages)]
pub type SaleStages<T: Config> = StorageMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Vec<u64> //stage id
>;

/// Store stage info for stage id.
#[pallet::storage]
#[pallet::getter(fn stages)]
pub type AllStages<T: Config> = StorageMap<
_,
Blake2_128Concat,
u64, //stage id
SaleStage<AssetIdOf<T>, BalanceOf<T>>
>;

/// Store bonuses that an account has received.
#[pallet::storage]
#[pallet::getter(fn bonuses)]
pub type Bonuses<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
Vec<BonusOf<T>>,
ValueQuery
>;

#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub stages: Vec<AssetIdOf<T>>,
}

#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
let mut stage_id = StageId::<T>::get();

for id in self.stages.iter() {
let new_stage = SaleStage {
stage_id,
asset_id: *id,
price: Zero::zero(),
quantity: Zero::zero(),
tokens_sold: Zero::zero(),
status: SaleStatus::ACTIVE,
core_team_cash_bonus: 0u8,
core_team_token_bonus: 0u8,
advisor_cash_bonus: 0u8,
advisor_token_bonus: 0u8,
community_cash_bonus: 0u8,
community_token_bonus: 0u8,
};

SaleStages::<T>::insert(id, [stage_id].to_vec());

AllStages::<T>::insert(stage_id, new_stage);
stage_id += 1;
}

StageId::<T>::set(stage_id);
}
}

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A sale stage is created
SaleCreated {
asset_id: AssetIdOf<T>,
},
/// The price of the stage was updated
SalePriceUpdated {
asset_id: AssetIdOf<T>,
stage_id: u64,
},
/// The amount of tokens to sell in the stage was updated
SaleQuantityUpdated {
asset_id: AssetIdOf<T>,
stage_id: u64,
},
/// The bonuses of the stage were been updated
SaleBonusesUpdated {
asset_id: AssetIdOf<T>,
stage_id: u64,
},
/// User bought tokens
SaleSuccess {
user: T::AccountId,
asset_id: AssetIdOf<T>,
sale_id: u64,
},
/// The sale has been finished
SaleFinished {
asset_id: AssetIdOf<T>,
stage_id: u64,
},
}

// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// Invalid sale id
InvalidSaleId,
/// Cannot update the sale because is in course
SaleAlreadyActive,
/// The sale is not active
SaleInactive,
/// The caller is not owner of the asset
NotOwner,
/// The value cannot be zero
ZeroValue,
/// Invalid quantity
InvalidQuantity,
/// The bank doesn´t have enough balance
InsufficientBankBalance,
/// Bonus above max bonus
InvalidBonus,
/// Overflow when computing price
Overflow,
/// Underflow when computing price
Underflow,
/// The asset used to pay is not valid
InvalidAsset,
}

#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// The origin can create a sale stage for a particular asset.
///
/// The origin must be Signed and the sender should be the creator of the asset.
///
/// The price should be parsed with the USDU decimals
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to create the sale stage.
/// - `price`: The price in USD that each token will cost.
/// - `quantity`: The amount of tokens that this stage will offer.
///
/// Emits `SaleCreated` event when successful.
#[pallet::call_index(0)]
pub fn create_sale_stage(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
price: BalanceOf<T>,
quantity: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// check if the sender is the owner of the asset
Self::check_permission(who.clone(), asset_id)?;

ensure!(!quantity.is_zero(), Error::<T>::ZeroValue);
ensure!(!price.is_zero(), Error::<T>::ZeroValue);

let current_stage_id = StageId::<T>::get();
StageId::<T>::mutate(|n| {
*n += 1;
});

// create a new stage
let new_sale_stage = SaleStage {
stage_id: current_stage_id,
asset_id,
price,
quantity,
tokens_sold: (0u32).into(),
status: SaleStatus::ACTIVE,
core_team_cash_bonus: 0,
core_team_token_bonus: 0,
advisor_cash_bonus: 0,
advisor_token_bonus: 0,
community_cash_bonus: 0,
community_token_bonus: 0,
};

// Store the new stage
AllStages::<T>::insert(current_stage_id, new_sale_stage);
SaleStages::<T>::try_mutate(asset_id, |maybe_vec_stages| -> DispatchResult {
if let Some(stages_vec) = maybe_vec_stages {
stages_vec.push(current_stage_id);
} else {
*maybe_vec_stages = Some([current_stage_id].into());
}
Ok(())
})?;

// Transfer tokens from the bank to the sales pallet to lock the tokens in sale
<T as Config>::Bank::transfer_from_bank_to_pallet(
Self::account_id(),
asset_id.clone(),
quantity.clone()
)?;

// Post a status.
T::Newsfeed::post_status(
who.clone(),
StatusType::SaleCreated,
format!("Created sale stage for {:?} at ${:?}", asset_id, price)
.as_bytes()
.to_vec(),
asset_id,
None, // optional account
Some(price), // optional amount
None // Second optional amount
);

// emit an event
Self::deposit_event(Event::SaleCreated { asset_id });

// Return a successful DispatchResultWithPostInfo
Ok(())
}

/// The origin can update the price of a sale stage for a particular asset.
///
/// The origin must be Signed and the sender should be the creator of the asset.
///
/// The price should be parsed with the USDU decimals.
///
/// The sale should have NO tokens sold to change the price.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to update the sale stage.
/// - `stage_id`: The identifier of the stage to update the price.
/// - `new_price`: The new price in USD that each token will cost.
///
/// Emits `SalePriceUpdated` event when successful.
#[pallet::call_index(1)]
pub fn update_price(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
stage_id: u64,
new_price: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// check if the sender is the owner of the asset
Self::check_permission(who.clone(), asset_id)?;

let mut sale_stage = AllStages::<T>::get(stage_id).ok_or(Error::<T>::InvalidSaleId)?;

// check if sale is active
ensure!(sale_stage.status == SaleStatus::ACTIVE, Error::<T>::SaleInactive);

// price should be greater than 0
ensure!(!new_price.is_zero(), Error::<T>::ZeroValue);

ensure!(sale_stage.tokens_sold.is_zero(), Error::<T>::SaleAlreadyActive);

sale_stage = SaleStage {
stage_id: sale_stage.stage_id,
asset_id: sale_stage.asset_id,
price: new_price,
quantity: sale_stage.quantity,
tokens_sold: sale_stage.tokens_sold,
status: SaleStatus::ACTIVE,
core_team_cash_bonus: sale_stage.core_team_cash_bonus,
core_team_token_bonus: sale_stage.core_team_token_bonus,
advisor_cash_bonus: sale_stage.advisor_cash_bonus,
advisor_token_bonus: sale_stage.advisor_token_bonus,
community_cash_bonus: sale_stage.community_cash_bonus,
community_token_bonus: sale_stage.community_token_bonus,
};

AllStages::<T>::insert(stage_id, sale_stage);

Self::deposit_event(Event::SalePriceUpdated { asset_id, stage_id });

Ok(())
}

/// The origin can update the bonuses to be paid of a sale stage for a particular asset.
///
/// The origin must be Signed and the sender should be the creator of the asset.
///
/// The values are percentages and should be lesser than the Max Bonus constant.
///
/// The bonuses are paid to the inviter of the buyer.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to update the stage.
/// - `stage_id`: The identifier of the stage to update the bonuses.
/// - `core_team_cash_bonus`: Percentage of the crypto paid to the inviter of the buyer if
/// inviter is a core team member.
/// - `core_team_token_bonus`: Percentage of the asset paid to the inviter of the buyer if
/// inviter is a core team member.
/// - `advisor_cash_bonus`: Percentage of the crypto paid to the inviter of the buyer if
/// inviter is an advisor.
/// - `advisor_token_bonus`: Percentage of the asset paid to the inviter of the buyer if
/// inviter is an advisor.
/// - `community_cash_bonus`: Percentage of the crypto paid to the inviter of the buyer if
/// inviter is a community member.
/// - `community_token_bonus`: Percentage of the asset paid to the inviter of the buyer if
/// inviter is a community member.
///
/// Emits `SaleBonusesUpdated` event when successful.
#[pallet::call_index(2)]
pub fn set_bonuses(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
stage_id: u64,
core_team_cash_bonus: u8,
core_team_token_bonus: u8,
advisor_cash_bonus: u8,
advisor_token_bonus: u8,
community_cash_bonus: u8,
community_token_bonus: u8
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// check if the sender is the owner of the asset
Self::check_permission(who.clone(), asset_id)?;

//check if the bonuses are below the treshold.
ensure!(
core_team_cash_bonus <= T::MaxBonus::get() &&
core_team_token_bonus <= T::MaxBonus::get() &&
advisor_cash_bonus <= T::MaxBonus::get() &&
advisor_token_bonus <= T::MaxBonus::get() &&
community_cash_bonus <= T::MaxBonus::get() &&
community_token_bonus <= T::MaxBonus::get(),
Error::<T>::InvalidBonus
);

let mut sale_stage = AllStages::<T>::get(stage_id).ok_or(Error::<T>::InvalidSaleId)?;

// check if sale is active
ensure!(sale_stage.status == SaleStatus::ACTIVE, Error::<T>::SaleInactive);

sale_stage = SaleStage {
stage_id: sale_stage.stage_id,
asset_id: sale_stage.asset_id,
price: sale_stage.price,
quantity: sale_stage.quantity,
tokens_sold: sale_stage.tokens_sold,
status: SaleStatus::ACTIVE,
core_team_cash_bonus,
core_team_token_bonus,
advisor_cash_bonus,
advisor_token_bonus,
community_cash_bonus,
community_token_bonus,
};

AllStages::<T>::insert(stage_id, sale_stage);

Self::deposit_event(Event::SaleBonusesUpdated { asset_id, stage_id });

Ok(())
}

/// The origin can update the amount of tokens sold in a sale stage for a particular asset.
///
/// The origin must be Signed and the sender should be the creator of the asset.
///
/// The value should be greater than the tokens sold.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to update the stage.
/// - `stage_id`: The identifier of the stage to update the quantity.
/// - `new_quantity`: The new amount of tokens offered by this stage.
///
/// Emits `SaleQuantityUpdated` event when successful.
#[pallet::call_index(3)]
pub fn update_quantity(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
stage_id: u64,
new_quantity: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// check if the sender is the owner of the asset
Self::check_permission(who.clone(), asset_id)?;

let mut sale_stage = AllStages::<T>::get(stage_id).ok_or(Error::<T>::InvalidSaleId)?;

// check if sale is active
ensure!(sale_stage.status == SaleStatus::ACTIVE, Error::<T>::SaleInactive);

ensure!(sale_stage.tokens_sold < new_quantity, Error::<T>::InvalidQuantity);

// Transfer tokens from or to the bank
if new_quantity > sale_stage.quantity {
// Transfer tokens from the bank to the sales pallet to lock the tokens in sale
<T as Config>::Bank::transfer_from_bank_to_pallet(
Self::account_id(),
asset_id.clone(),
new_quantity - sale_stage.quantity
)?;
} else if new_quantity < sale_stage.quantity {
// Transfer tokens from the sales pallet to the bank to release the tokens that won't be sold
<T as Config>::Bank::transfer_from_pallet_to_bank(
Self::account_id(),
asset_id.clone(),
asset_id.clone(),
new_quantity - sale_stage.quantity
)?;
}

if new_quantity == sale_stage.quantity {
return Ok(());
}

sale_stage = SaleStage {
stage_id: sale_stage.stage_id,
asset_id: sale_stage.asset_id,
price: sale_stage.price,
quantity: new_quantity,
tokens_sold: sale_stage.tokens_sold,
status: SaleStatus::ACTIVE,
core_team_cash_bonus: sale_stage.core_team_cash_bonus,
core_team_token_bonus: sale_stage.core_team_token_bonus,
advisor_cash_bonus: sale_stage.advisor_cash_bonus,
advisor_token_bonus: sale_stage.advisor_token_bonus,
community_cash_bonus: sale_stage.community_cash_bonus,
community_token_bonus: sale_stage.community_token_bonus,
};

AllStages::<T>::insert(stage_id, sale_stage);

Self::deposit_event(Event::SaleQuantityUpdated { asset_id, stage_id });

Ok(())
}

/// The origin can buy tokens in a sale stage for a particular asset.
///
/// The origin must be Signed.
///
/// The sender can choose an asset to pay.
/// The inviter of the sender will receive a bonus in crypto and in the purchased asset
/// depending on the configuration of the stage and the role that he has in the asset.
///
/// The asset used should be registered in the oracle pallet.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to buy in a stage.
/// - `stage_id`: The identifier of the stage to buy tokens.
/// - `amount`: The amount of tokens to be purchased.
/// - `asset_used`: The asset used to pay for the tokens.
///
/// Emits `SaleSuccess` event when successful.
#[pallet::call_index(4)]
pub fn buy_tokens(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
stage_id: u64,
amount: BalanceOf<T>,
asset_used: AssetIdOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// check if the sale is active
let sale_stage = AllStages::<T>::get(stage_id).ok_or(Error::<T>::InvalidSaleId)?;

ensure!(sale_stage.asset_id == asset_id, Error::<T>::InvalidSaleId);

// ensure that the asset used is different from the asset being purchased
ensure!(asset_id != asset_used, Error::<T>::InvalidAsset);

// ensure that the asset used is a wrapped crypto
ensure!(
<<T as pallet::Config>::Fungibles as AssetInterface<
<T as pallet::Config>::AssetId,
T::AssetBalance,
T::AccountId,
Decimals,
TokenType
>>::get_token_type(asset_used) == TokenType::WrappedCrypto ||
asset_used.clone().is_zero(),
Error::<T>::InvalidAsset
);

// check if sale is active
ensure!(sale_stage.status == SaleStatus::ACTIVE, Error::<T>::SaleInactive);

let (amount_to_buy, new_status) = Self::get_amount_and_state(
amount,
sale_stage.clone()
);

let inviter_address = T::Profile::get_inviter(who.clone())?;

// role of the person that invites the buyer
let inviter_role = Self::get_user_role(inviter_address.clone(), asset_id)?;

let asset_amount: BalanceOf<T>;
let asset_used_price: BalanceOf<T>;
if asset_used == T::UnitId::get().into() {
let (asset_reserve, usdu_reserve) = T::Pool::get_liquidity_pool(asset_used);
asset_used_price = usdu_reserve
.checked_mul(
&(1u128)
.saturating_mul(
(10u128)
.checked_pow(T::UsduTokenDecimals::get())
.ok_or(Error::<T>::Overflow)?
)
.saturated_into::<BalanceOf<T>>()
)
.ok_or(Error::<T>::Overflow)?
.checked_div(&asset_reserve)
.ok_or(Error::<T>::Underflow)?;

asset_amount = sale_stage.price
.checked_mul(&amount_to_buy)
.ok_or(Error::<T>::Overflow)?
.checked_div(&asset_used_price.saturated_into::<BalanceOf<T>>())
.ok_or(Error::<T>::Underflow)?;
} else {
asset_used_price = T::Oracle::get_value_from_u32(asset_used.into())?
.saturated_into::<u128>()
.saturated_into::<BalanceOf<T>>();

// get the amount of asset to be paid
asset_amount = sale_stage.price
.checked_mul(&amount_to_buy)
.ok_or(Error::<T>::Overflow)?
.checked_div(&asset_used_price)
.ok_or(Error::<T>::Underflow)?;
}

let mut core_team_cash_bonus: BalanceOf<T> = Zero::zero();
let mut core_team_token_bonus: BalanceOf<T> = Zero::zero();
let mut advisor_cash_bonus: BalanceOf<T> = Zero::zero();
let mut advisor_token_bonus: BalanceOf<T> = Zero::zero();
let mut community_cash_bonus: BalanceOf<T> = Zero::zero();
let mut community_token_bonus: BalanceOf<T> = Zero::zero();
let mut token_bonus_paid: BalanceOf<T> = Zero::zero();
#[allow(unused_assignments)]
let mut cash_bonus_paid: BalanceOf<T> = Zero::zero();
match inviter_role {
Role::TEAMMEMBER => {
core_team_cash_bonus =
Perbill::from_percent(sale_stage.core_team_cash_bonus.into()) *
asset_amount;
core_team_token_bonus =
Perbill::from_percent(sale_stage.core_team_token_bonus.into()) *
amount_to_buy;

//check if the bank can pay the token bonus
if
<T as pallet::Config>::Fungibles::balance(
asset_id,
&<T as pallet::Config>::Bank::get_bank_account()
) >= core_team_token_bonus &&
!core_team_token_bonus.clone().is_zero()
{
<T as pallet::Config>::Bank::transfer_from_bank_to_user(
<T as pallet::Config>::Bank::get_bank_account(),
inviter_address.clone(),
asset_id,
asset_id,
core_team_token_bonus
)?;
token_bonus_paid = core_team_token_bonus;
}
if !core_team_cash_bonus.clone().is_zero() {
//transfer the asset bonus to the inviter
<T as pallet::Config>::Fungibles::transfer(
asset_used,
&who,
&inviter_address,
core_team_cash_bonus,
Expendable
)?;
cash_bonus_paid = core_team_cash_bonus;
}
}
Role::ADVISOR => {
advisor_cash_bonus =
Perbill::from_percent(sale_stage.advisor_cash_bonus.into()) * asset_amount;
advisor_token_bonus =
Perbill::from_percent(sale_stage.advisor_token_bonus.into()) *
amount_to_buy;
//check if the bank can pay the token bonus
if
<T as pallet::Config>::Fungibles::balance(
asset_id,
&<T as pallet::Config>::Bank::get_bank_account()
) >= advisor_token_bonus &&
!advisor_token_bonus.clone().is_zero()
{
<T as pallet::Config>::Fungibles::transfer(
asset_id,
&<T as pallet::Config>::Bank::get_bank_account(),
&inviter_address,
advisor_token_bonus,
Expendable
)?;
token_bonus_paid = advisor_token_bonus;
}
if !advisor_cash_bonus.clone().is_zero() {
//transfer the asset bonus to the inviter
<T as pallet::Config>::Fungibles::transfer(
asset_used,
&who,
&inviter_address,
advisor_cash_bonus,
Expendable
)?;
cash_bonus_paid = advisor_cash_bonus;
}
}
Role::COMMUNITY => {
community_cash_bonus =
Perbill::from_percent(sale_stage.community_cash_bonus.into()) *
asset_amount;
community_token_bonus =
Perbill::from_percent(sale_stage.community_token_bonus.into()) *
amount_to_buy;
//check if the bank can pay the token bonus
if
<T as pallet::Config>::Fungibles::balance(
asset_id,
&<T as pallet::Config>::Bank::get_bank_account()
) >= community_token_bonus &&
!community_token_bonus.clone().is_zero()
{
<T as pallet::Config>::Fungibles::transfer(
asset_id,
&<T as pallet::Config>::Bank::get_bank_account(),
&inviter_address,
community_token_bonus,
Expendable
)?;
token_bonus_paid = community_token_bonus;
}
if !community_cash_bonus.clone().is_zero() {
//transfer the asset bonus to the inviter
<T as pallet::Config>::Fungibles::transfer(
asset_used,
&who,
&inviter_address,
community_cash_bonus,
Expendable
)?;
cash_bonus_paid = community_cash_bonus;
}
}
}

// transfer crypto to the bank
<T as Config>::Bank::transfer_from_user_to_bank(
who.clone(),
asset_id,
asset_used,
asset_amount - core_team_cash_bonus - advisor_cash_bonus - community_cash_bonus
)?;

//transfer tokens to the user
<T as Config>::Fungibles::transfer(
asset_id,
&Self::account_id(),
&who,
amount_to_buy,
Expendable
)?;

// Increase circulating supply of the token
<T as Config>::Fungibles::increase_supply(asset_id.clone(), amount_to_buy.clone());

let sale_id = SaleCounter::<T>::get();
SaleCounter::<T>::mutate(|n| {
*n += 1;
});

let asset_decimals = <T as pallet::Config>::Fungibles::decimals(asset_id);

// create a sale and store it
let new_sale = Sale {
stage_id: sale_stage.stage_id,
sale_id,
asset_id,
user: who.clone(),
price: sale_stage.price,
quantity: amount_to_buy,
tokens_sold: amount_to_buy,
total_price: sale_stage.price
.saturating_mul(amount_to_buy)
.checked_div(
&(10u128)
.checked_pow(asset_decimals.into())
.ok_or(Error::<T>::Overflow)?
.saturated_into::<BalanceOf<T>>()
)
.ok_or(Error::<T>::Underflow)?,
core_team_cash_bonus_paid: core_team_cash_bonus,
core_team_token_bonus_paid: core_team_token_bonus,
advisor_cash_bonus_paid: advisor_cash_bonus,
advisor_token_bonus_paid: advisor_token_bonus,
community_cash_bonus_paid: community_cash_bonus,
community_token_bonus_paid: community_token_bonus,
time: <frame_system::Pallet<T>>::block_number(),
asset_used,
asset_used_price,
asset_used_amount: asset_amount,
};

AllSales::<T>::insert(sale_id, new_sale);

SalesByStage::<T>::mutate(asset_id, sale_stage.stage_id, |s| {
if let Some(x) = s {
x.push(sale_id);
} else {
let vec = vec![sale_id];
*s = Some(vec);
}
});

Self::deposit_event(Event::SaleSuccess {
user: who.clone(),
asset_id,
sale_id,
});

//update the current sale
let updated_sale_stage = SaleStage {
stage_id: sale_stage.stage_id,
asset_id,
price: sale_stage.price,
quantity: sale_stage.quantity,
core_team_cash_bonus: sale_stage.core_team_cash_bonus,
core_team_token_bonus: sale_stage.core_team_token_bonus,
advisor_cash_bonus: sale_stage.advisor_cash_bonus,
advisor_token_bonus: sale_stage.advisor_token_bonus,
community_cash_bonus: sale_stage.community_cash_bonus,
community_token_bonus: sale_stage.community_token_bonus,
tokens_sold: sale_stage.tokens_sold + amount_to_buy,
status: new_status,
};

// insert the new stage in the storage for the asset
AllStages::<T>::insert(sale_stage.stage_id, updated_sale_stage.clone());

if !token_bonus_paid.is_zero() {
let token_bonus = Bonus {
asset_id,
asset_used_id: asset_used,
origin: who.clone(),
price: sale_stage.price,
purchased_quantity: amount_to_buy,
crypto_paid: asset_amount,
bonus_paid: token_bonus_paid,
time: <T as Config>::Time::now(),
is_asset: true,
};

// Update bonuses storage
Bonuses::<T>::mutate(&inviter_address, |s| s.push(token_bonus));
}

if !cash_bonus_paid.is_zero() {
let cash_bonus = Bonus {
asset_id,
asset_used_id: asset_used,
origin: who.clone(),
price: sale_stage.price,
purchased_quantity: amount_to_buy,
crypto_paid: asset_amount,
bonus_paid: cash_bonus_paid,
time: <T as Config>::Time::now(),
is_asset: false,
};

// Update bonuses storage
Bonuses::<T>::mutate(&inviter_address, |s| s.push(cash_bonus));
}

// Post a status.
T::Newsfeed::post_status(
who.clone(),
StatusType::SalePurchase,
format!(
"{:?} {:?} purchased in a sale round at ${:?}",
amount_to_buy,
asset_id,
sale_stage.price
)
.as_bytes()
.to_vec(),
asset_id,
None, // optional account
Some(amount_to_buy), // optional amount
Some(sale_stage.price) // Second optional amount
);

// Return a successful DispatchResultWithPostInfo
Ok(())
}

#[pallet::call_index(5)]
pub fn buy_stable(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
stage_id: u64,
amount: BalanceOf<T>,
asset_used: AssetIdOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// Get the current sale
let sale_stage = AllStages::<T>::get(stage_id).ok_or(Error::<T>::InvalidSaleId)?;

ensure!(sale_stage.asset_id == asset_id, Error::<T>::InvalidSaleId);

// ensure that the asset used is a wrapped crypto
ensure!(
<<T as pallet::Config>::Fungibles as AssetInterface<
<T as pallet::Config>::AssetId,
T::AssetBalance,
T::AccountId,
Decimals,
TokenType
>>::get_token_type(asset_used) == TokenType::WrappedCrypto,
Error::<T>::InvalidAsset
);

let stage_price = T::Oracle::get_value_from_u32(asset_id.into())?
.saturated_into::<u128>()
.saturated_into::<BalanceOf<T>>();

let asset_used_price: BalanceOf<T> = T::Oracle::get_value_from_u32(asset_used.into())?
.saturated_into::<u128>()
.saturated_into::<BalanceOf<T>>();

// get the amount of asset to be paid
let asset_amount: BalanceOf<T> = stage_price
.checked_mul(&amount)
.ok_or(Error::<T>::Overflow)?
.checked_div(&asset_used_price)
.ok_or(Error::<T>::Underflow)?;

// transfer crypto to the bank
<T as Config>::Bank::transfer_from_user_to_bank(
who.clone(),
asset_id,
asset_used,
asset_amount
)?;

// Mint stable to the buyer
<<T as pallet::Config>::Fungibles as AssetInterface<
<T as pallet::Config>::AssetId,
T::AssetBalance,
T::AccountId,
Decimals,
TokenType
>>::mint(asset_id, who.clone(), amount, true)?;

let sale_id = SaleCounter::<T>::get();
SaleCounter::<T>::mutate(|n| {
*n += 1;
});

// create a sale and store it
let new_sale = Sale {
stage_id: sale_stage.stage_id,
sale_id,
asset_id,
user: who.clone(),
price: stage_price,
quantity: amount,
tokens_sold: amount,
total_price: stage_price
.saturating_mul(amount)
.checked_div(
&(10u128)
.checked_pow(10u32)
.ok_or(Error::<T>::Overflow)?
.saturated_into::<BalanceOf<T>>()
)
.ok_or(Error::<T>::Underflow)?,
core_team_cash_bonus_paid: Zero::zero(),
core_team_token_bonus_paid: Zero::zero(),
advisor_cash_bonus_paid: Zero::zero(),
advisor_token_bonus_paid: Zero::zero(),
community_cash_bonus_paid: Zero::zero(),
community_token_bonus_paid: Zero::zero(),
time: <frame_system::Pallet<T>>::block_number(),
asset_used,
asset_used_price,
asset_used_amount: asset_amount,
};

AllSales::<T>::insert(sale_id, new_sale);

SalesByStage::<T>::mutate(asset_id, sale_stage.stage_id, |s| {
if let Some(x) = s {
x.push(sale_id);
} else {
// let mut vec = Vec::new();
// vec.push(sale_id.clone());
let vec = vec![sale_id];
*s = Some(vec);
}
});

Self::deposit_event(Event::SaleSuccess {
user: who.clone(),
asset_id,
sale_id,
});

//update the current sale
let updated_sale_stage: SaleStage<<T as Config>::AssetId, BalanceOf<T>> = SaleStage {
stage_id: sale_stage.stage_id,
asset_id,
price: (0u32).saturated_into::<BalanceOf<T>>(),
quantity: (0u32).saturated_into::<BalanceOf<T>>(),
tokens_sold: sale_stage.tokens_sold + amount,
status: SaleStatus::ACTIVE,
core_team_cash_bonus: sale_stage.core_team_cash_bonus,
core_team_token_bonus: sale_stage.core_team_token_bonus,
advisor_cash_bonus: sale_stage.advisor_cash_bonus,
advisor_token_bonus: sale_stage.advisor_token_bonus,
community_cash_bonus: sale_stage.community_cash_bonus,
community_token_bonus: sale_stage.community_token_bonus,
};

// insert the new stage in the storage for the asset
AllStages::<T>::insert(sale_stage.stage_id, updated_sale_stage.clone());

// Post a status.
T::Newsfeed::post_status(
who.clone(),
StatusType::SalePurchase,
format!(
"{:?} {:?} purchased in a sale round at ${:?}",
amount,
asset_id,
stage_price
)
.as_bytes()
.to_vec(),
asset_id,
None, // optional account
Some(amount), // optional amount
Some(stage_price) // Second optional amount
);

// Return a successful DispatchResultWithPostInfo
Ok(())
}

/// The origin can close a sale stage for a particular asset.
///
/// The origin must be Signed and the sender should be the creator of the asset.
///
/// Parameters:
/// - `asset_id`: The identifier of the asset to access the stage.
/// - `stage_id`: The identifier of the stage to close.
///
/// Emits `SaleFinished` event when successful.
#[pallet::call_index(6)]
pub fn close_sale(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
stage_id: u64
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;

// check if the sender is the owner of the asset
Self::check_permission(who.clone(), asset_id)?;

// check if the sale exists
let sale_stage = AllStages::<T>::get(stage_id).ok_or(Error::<T>::InvalidSaleId)?;

// check if sale is active
ensure!(sale_stage.status == SaleStatus::ACTIVE, Error::<T>::SaleInactive);

let finished_sale = SaleStage {
stage_id: sale_stage.stage_id,
asset_id,
price: sale_stage.price,
quantity: sale_stage.quantity,
core_team_cash_bonus: sale_stage.core_team_cash_bonus,
core_team_token_bonus: sale_stage.core_team_token_bonus,
advisor_cash_bonus: sale_stage.advisor_cash_bonus,
advisor_token_bonus: sale_stage.advisor_token_bonus,
community_cash_bonus: sale_stage.community_cash_bonus,
community_token_bonus: sale_stage.community_token_bonus,
tokens_sold: sale_stage.tokens_sold,
status: SaleStatus::COMPLETED,
};

AllStages::<T>::insert(stage_id, finished_sale);

// Transfer tokens from the sales pallet to the bank to release the tokens that won't be sold
<T as Config>::Bank::transfer_from_pallet_to_bank(
Self::account_id(),
asset_id.clone(),
asset_id.clone(),
sale_stage.quantity - sale_stage.tokens_sold
)?;

Self::deposit_event(Event::SaleFinished { asset_id, stage_id: sale_stage.stage_id });

Ok(())
}
}

impl<T: Config> Pallet<T> {
/// Get AccountId assigned to the pallet.
pub fn account_id() -> AccountIdOf<T> {
T::PalletId::get().into_account_truncating()
}

fn get_amount_and_state(
amount: BalanceOf<T>,
sale_stage: SaleStage<AssetIdOf<T>, BalanceOf<T>>
) -> (BalanceOf<T>, SaleStatus) {
if sale_stage.tokens_sold + amount > sale_stage.quantity {
(sale_stage.quantity - sale_stage.tokens_sold, SaleStatus::COMPLETED)
} else {
(amount, SaleStatus::ACTIVE)
}
}

// get the role of the user in the team advisor pallet
fn get_user_role(
user_address: T::AccountId,
asset_id: AssetIdOf<T>
) -> Result<Role, DispatchError> {
// check if the user is a core team member
if T::TeamsAdvisors::is_member(asset_id, user_address.clone()) {
Ok(Role::TEAMMEMBER)
} else if T::TeamsAdvisors::is_advisor(asset_id, user_address) {
Ok(Role::ADVISOR)
} else {
Ok(Role::COMMUNITY)
}
}

fn check_permission(who: T::AccountId, asset_id: AssetIdOf<T>) -> DispatchResult {
// check if the sender has permission to create a sale
let asset_owner = <<T as pallet::Config>::Fungibles as AssetInterface<
<T as pallet::Config>::AssetId,
T::AssetBalance,
T::AccountId,
Decimals,
TokenType
>>::get_owner(asset_id)?;
ensure!(who == asset_owner, Error::<T>::NotOwner);
Ok(())
}
}
}