//! # Unit Deposit Pallet
//! <!-- Original author of paragraph: @gang
//!
//! ## Overview
//!
//! Pallet that allows a user to deposit tokens to the platform.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Deposit token.
//! * Cancel deposit.
//! * Accept deposit.
//! * Decline deposit.
//! * Register a worker.
//! * Unregister a worker.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `deposit_token`: Deposit an amount of tokens to the platform.
//! - `cancel_deposit`: Cancel deposit.
//! - `accept_deposit`: Allows a worker to accept a deposit, authorizing it to be processed.
//! - `decline_deposit`: Allows a worker to decline a deposit, preventing it from being processed.
//! - `register_worker`: Permits an account ID to be registered as a worker.
//! - `unregister_worker`: Permits an account ID to be unregistered as a worker.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![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::*;
mod types;
pub use types::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
pub mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
use traits::profile::ProfileInterface;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use sp_std::prelude::Vec;
use frame_support::{
pallet_prelude::{OptionQuery, *},
traits::{
fungibles,
tokens::{
fungibles::{metadata::Inspect as MetadataInspect, Inspect, Mutate},
Balance,
},
UnixTime,
},
Blake2_128Concat,
};
use frame_system::pallet_prelude::*;
use pallet_vault::VaultAddressManager;
use sp_runtime::{
traits::{CheckedDiv, CheckedMul, Hash, SaturatedConversion, Zero},
FixedPointOperand, Saturating,
};
use traits::{
asset::{AssetInterface, TokenType},
deposit::WorkersAcceptDeposit,
oracle::OracleInterface,
pool::PoolInterface,
stakingworkers::GetAmountStaked,
subaccounts::{AccountOrigin, SubAccounts},
};
/// Account id type alias.
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
/// Type used to represent the deposit id.
pub type DepositId = u64;
/// Type used to represent the token decimals.
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 {
/// 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::AssetId, Balance = Self::BalanceOf>
+ //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId>
+ fungibles::Create<Self::AccountId>
+ fungibles::roles::Inspect<Self::AccountId>
+ AssetInterface<Self::AssetId, Self::BalanceOf, Self::AccountId, Decimals, TokenType>
+ fungibles::metadata::Inspect<Self::AccountId>;
type BalanceOf: Balance
+ FixedPointOperand
+ MaxEncodedLen
+ MaybeSerializeDeserialize
+ TypeInfo
+ From<u128>
+ Into<u128>;
/// Type to access the Oracle Pallet interface.
type OracleInterface: OracleInterface<
<Self::Fungibles as Inspect<Self::AccountId>>::AssetId,
<Self::Fungibles as Inspect<Self::AccountId>>::Balance,
Decimals,
>;
type AssetId: Member
+ Parameter
+ Copy
+ MaybeSerializeDeserialize
+ MaxEncodedLen
+ Default
+ Zero
+ From<u32>;
/// Limit of Address Legnth
#[pallet::constant]
type AddressLengthLimit: Get<u32>;
/// Vault Handler trait
type VaultManager: VaultAddressManager<Self::AssetId, Self::AccountId, Self::BalanceOf>;
// Provides actual time
type TimeProvider: UnixTime;
/// Type to access the sub account pallet.
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
#[pallet::constant]
type ProfileStringLimit: Get<u32>;
/// Decimals of USDU
#[pallet::constant]
type TokenDecimals: Get<u32>;
/// Type to access the Pool Pallet.
type Pool: PoolInterface<Self::AssetId, Self::BalanceOf>;
// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
/// Interface to query the total shares
type GetAmountStaked: GetAmountStaked<Self::BalanceOf>;
/// UNIT asset id.
#[pallet::constant]
type UnitAssetId: Get<Self::AssetId>;
/// BTCU asset id
#[pallet::constant]
type BitcoinAssetId: Get<Self::AssetId>;
/// ETHU asset id
#[pallet::constant]
type EthereumAssetId: Get<Self::AssetId>;
#[pallet::constant]
type VotingPeriodAfterConsensus: Get<BlockNumberFor<Self>>;
/// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: ProfileInterface<Self::AccountId, Self::ProfileStringLimit>;
}
/// Minimum deposit amount for BTCU and ETHU assets
pub const MINIMUM_DEPOSIT_BTCU_ETHU: u128 = 990_000_000_000;
/// Minimum deposit amount for all assets except BTCU and ETHU
pub const MINIMUM_DEPOSIT_ASSET: u128 = 190_000_000_000;
/// User deposit id
#[pallet::storage]
#[pallet::getter(fn user_deposits)]
pub type UserDepositsId<T: Config> =
StorageMap<_, Twox64Concat, AccountIdOf<T>, Vec<DepositId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn nonce)]
pub type Nonce<T: Config> = StorageValue<_, u32, ValueQuery>;
/// Deposit id
#[pallet::storage]
#[pallet::getter(fn deposit_id_counter)]
pub type DepositIdCounter<T: Config> = StorageValue<_, DepositId, ValueQuery>;
/// Pool worker id
#[pallet::storage]
#[pallet::getter(fn pool_worker_id)]
pub type PoolWorkerId<T: Config> = StorageValue<_, AccountIdOf<T>, OptionQuery>;
/// Store pending transactions.
/// deposit_id -> option<bool>
#[pallet::storage]
#[pallet::getter(fn get_pending_transactions)]
pub type PendingTransactions<T: Config> =
StorageMap<_, Blake2_128Concat, DepositId, bool, OptionQuery>;
/// Store transactions hashs processed.
/// hash -> option<bool>
#[pallet::storage]
#[pallet::getter(fn get_processed_transaction_hash)]
pub type ProcessedTransactionsHash<T: Config> =
StorageMap<_, Blake2_128Concat, T::Hash, bool, OptionQuery>;
/// Mapping Deposit id to Deposit
#[pallet::storage]
#[pallet::getter(fn deposit_id_to_deposit)]
pub type DepositIdToDeposit<T: Config> = StorageMap<
_,
Twox64Concat,
DepositId,
Deposit<AccountIdOf<T>, T::AssetId, T::BalanceOf, DepositId>,
>;
// 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> {
/// A `deposit` was created.
CreatedDeposit {
who: AccountIdOf<T>,
deposit_id: DepositId,
asset_id: T::AssetId,
amount: T::BalanceOf,
},
/// A `deposit` was cancelled.
CancelledDeposit { who: AccountIdOf<T>, deposit_id: DepositId },
/// A `deposit` was accepted.
DepositAccepted { deposit_id: DepositId },
/// A `deposit` was declined.
DepositDeclined { deposit_id: DepositId },
/// A `worker` was registered.
WorkerRegistered { who: AccountIdOf<T> },
/// A `worker` was unregistered.
WorkerUnregistered { who: AccountIdOf<T> },
}
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
// No Token Deposit Provided
AssetIdNotProvided,
// Not Enough Token Balance To Deposit
NotEnoughTokenDeposit,
// No Oracle Provide In This Asset Id
NoOracleProvided,
// No permission to do a function
NoPermission,
// No deposit id in deposit
NoDepositId,
// Deposit id already executed
DepositIdAlreadyExecuted,
// Not the owner of deposit
NotDepositOwner,
/// The account is already a registered as a deposit worker.
WorkerAlreadyRegistered,
/// The account is not registered as a deposit worker.
WorkerIsNotRegistered,
/// The transaction is already processed.
TransactionAlreadyProcessed,
/// No vault available,
NoVaultAvailable,
/// No Unit price available
NoUnitPrice,
/// Asset metadata does not provide decimals
NoDecimals,
/// Overflow of value
Overflow,
/// Underflow of value
Underflow,
NoPoolId,
/// Deposit amount shouldn't exceed half of the amount staked in workers
DepositAmountExceedsStakingLimit,
/// Unit can not be used in deposits
InvalidDepositAssetId,
/// Deposit amount should exceed the minimum deposit limit
InsufficiendDepositAmount,
}
// Dispatchable functions allows users to interact with the pallet and invoke state changes.
// These functions materialize as "extrinsics", which are often compared to transactions.
// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// The origin can deposit token.
///
/// The user that creates this deposit has a fixed amount of blocks to
/// transfer the cryptos in the external chain. This value is stored off-chain.
/// If the user don't transfer the tokens before this period the workers won't accept the
/// deposit.
///
/// Parameters:
/// - `from_address`: The address from which the user will send the token with ID asset_id
/// and the specified
/// amount `amount`.
/// - `asset_id`: Asset id.
/// - `amount`: Amount of token in standard denomination
///
/// Emits `CreateDeposited` event when successful.
#[pallet::call_index(0)]
pub fn deposit_token(
origin: OriginFor<T>,
asset_id: T::AssetId,
amount: T::BalanceOf,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(
<<T as pallet::Config>::Fungibles as frame_support::traits::fungibles::Inspect<
AccountIdOf<T>,
>>::asset_exists(asset_id),
Error::<T>::AssetIdNotProvided
);
ensure!(asset_id != T::UnitAssetId::get(), Error::<T>::InvalidDepositAssetId);
// Generate a pseudo-random number to create a unique amount in order to distringuish
// the transaction in the assets original chain
let current_block_number = <frame_system::Pallet<T>>::block_number();
let block_hash = <frame_system::Pallet<T>>::block_hash(current_block_number);
let pseudo_random_value = T::Hashing::hash_of(&block_hash);
let bytes = pseudo_random_value.as_ref();
let decimal_random_number: u128;
let nonce = Nonce::<T>::get();
// Use a Nonce to diffenciate two same amount deposited in the same block
let random_number: u32 = (bytes[0] as u32)
.checked_add(bytes[1] as u32)
.ok_or(Error::<T>::Overflow)?
.checked_add(bytes[2] as u32)
.ok_or(Error::<T>::Overflow)?
.checked_add(nonce)
.ok_or(Error::<T>::Overflow)? %
1000;
// Start to 0 again whenever the u32 reach its limit
Nonce::<T>::put(nonce.wrapping_add(1));
// Get token decimals
let token_decimals = T::Fungibles::decimals(asset_id);
let precision_factor =
(10u128).checked_pow(token_decimals as u32).ok_or(Error::<T>::Overflow)?;
if asset_id == T::BitcoinAssetId::get() || asset_id == T::EthereumAssetId::get() {
// Target the 7, 8 and 9 decimals
decimal_random_number = (random_number as u128)
.checked_mul(
precision_factor
.checked_div(10000000000_u128)
.ok_or(Error::<T>::Underflow)?,
)
.ok_or(Error::<T>::Overflow)?;
} else {
// Target the 3, 4 and 5 decimals
decimal_random_number = (random_number as u128)
.checked_mul(
precision_factor.checked_div(1000000_u128).ok_or(Error::<T>::Underflow)?,
)
.ok_or(Error::<T>::Overflow)?;
}
let unique_amount = amount
.clone()
.saturating_sub(Self::u128_to_asset_balance(decimal_random_number));
if let Some((asset_price, _)) = T::OracleInterface::get_oracle_key_value(asset_id) {
// Type cast values to u128
let deposit_id = DepositIdCounter::<T>::get();
let minimum_deposit;
if asset_id == T::BitcoinAssetId::get() || asset_id == T::EthereumAssetId::get() {
minimum_deposit = MINIMUM_DEPOSIT_BTCU_ETHU;
} else {
minimum_deposit = MINIMUM_DEPOSIT_ASSET;
}
let unique_amount_in_usdu = asset_price
.into()
.checked_mul(unique_amount.into())
.ok_or(Error::<T>::Overflow)?
.checked_div(
(10u128)
.checked_pow(T::TokenDecimals::get().into())
.ok_or(Error::<T>::Overflow)?,
)
.ok_or(Error::<T>::Underflow)?;
ensure!(
unique_amount_in_usdu >= minimum_deposit,
Error::<T>::InsufficiendDepositAmount
);
// Check that the amount does not exceed half of the staked amount in the stakers
// pool
let staking_limit_unit = T::GetAmountStaked::get_amount_pool()
.checked_div(&(2u128).into())
.ok_or(Error::<T>::Underflow)?;
// Calculate unit price through the liquidity pool
let (unit_reserve, usdu_reserve) =
T::Pool::get_liquidity_pool(T::VaultManager::get_unit_id());
let unit_price = usdu_reserve
.checked_mul(
&(10u128)
.checked_pow(T::TokenDecimals::get())
.ok_or(Error::<T>::Overflow)?
.into(),
)
.ok_or(Error::<T>::Overflow)?
.checked_div(&unit_reserve)
.ok_or(Error::<T>::Underflow)?;
let staking_limit_usdu = staking_limit_unit
.checked_mul(&unit_price)
.ok_or(Error::<T>::Overflow)?
.checked_div(
&(10u128)
.checked_pow(T::TokenDecimals::get())
.ok_or(Error::<T>::Overflow)?
.into(),
)
.ok_or(Error::<T>::Underflow)?;
ensure!(
unique_amount_in_usdu < staking_limit_usdu.into(),
Error::<T>::DepositAmountExceedsStakingLimit
);
let (address_to, vault_account_id) =
T::VaultManager::calculate_deposit_vault_address(
asset_id,
Self::u128_to_asset_balance(unique_amount_in_usdu),
)
.ok_or(Error::<T>::NoVaultAvailable)?;
let deposit = Deposit {
deposit_id,
statcode: StatCode::Pending,
user_address: who.clone(),
amount: unique_amount,
token_id: asset_id,
deposit_to_account: address_to,
deposit_vault_id: vault_account_id,
usd_amount: unique_amount_in_usdu.saturated_into::<T::BalanceOf>(),
timestamp: T::TimeProvider::now().as_secs(),
created_at: current_block_number.saturated_into::<u64>(),
can_vote_until: None,
};
DepositIdToDeposit::<T>::insert(deposit_id, deposit.clone());
PendingTransactions::<T>::insert(deposit_id, true);
UserDepositsId::<T>::mutate(who.clone(), |val| -> DispatchResult {
val.push(deposit_id);
Ok(())
})?;
DepositIdCounter::<T>::set(deposit_id.saturating_add(1));
Self::deposit_event(Event::CreatedDeposit {
who,
deposit_id,
asset_id,
amount: unique_amount,
});
Ok(())
} else {
Err(Error::<T>::NoOracleProvided.into())
}
}
/// The origin can cancel deposit.
///
/// Parameters:
/// - `deposit_id`: Deposit id.
///
/// Emits `CancelledDeposit` event when successful.
#[pallet::call_index(1)]
pub fn cancel_deposit(origin: OriginFor<T>, deposit_id: DepositId) -> DispatchResult {
let who = ensure_signed(origin.clone())?;
DepositIdToDeposit::<T>::try_mutate(deposit_id, |maybe_deposit| -> DispatchResult {
let deposit = maybe_deposit.as_mut().ok_or(Error::<T>::NoDepositId)?;
ensure!(
deposit.statcode == StatCode::Pending,
Error::<T>::DepositIdAlreadyExecuted
);
ensure!(deposit.user_address == who, Error::<T>::NotDepositOwner);
deposit.statcode = StatCode::Cancelled;
PendingTransactions::<T>::mutate(deposit_id, |deposit| {
*deposit = None;
});
Self::deposit_event(Event::CancelledDeposit { who, deposit_id });
Ok(())
})?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn u32_to_asset_balance(input: u32) -> T::BalanceOf {
input.into()
}
pub fn u128_to_asset_balance(input: u128) -> T::BalanceOf {
input.into()
}
}
impl<T: Config>
WorkersAcceptDeposit<
T::AssetId,
T::AccountId,
T::BalanceOf,
DepositId,
T::AddressLengthLimit,
Deposit<T::AccountId, T::AssetId, T::BalanceOf, DepositId>,
> for Pallet<T>
{
/// The origin can accept a deposit request if they are currently registered.
///
/// Parameters:
/// - `deposit_id`: The deposit request with deposit ID `deposit_id` that is accepted.
///
/// Emits `DepositAccepted` event when successful.
fn accept_deposit(
deposit_id: DepositId,
tx_hash: BoundedVec<u8, T::AddressLengthLimit>,
) -> DispatchResult {
DepositIdToDeposit::<T>::try_mutate(deposit_id, |maybe_deposit| -> DispatchResult {
let deposit = maybe_deposit.as_mut().ok_or(Error::<T>::NoDepositId)?;
let transaction_hash = T::Hashing::hash(&tx_hash);
ensure!(
!ProcessedTransactionsHash::<T>::contains_key(transaction_hash),
Error::<T>::TransactionAlreadyProcessed
);
let current_block = <frame_system::Pallet<T>>::block_number();
deposit.statcode = StatCode::Completed;
deposit.can_vote_until = Some(
current_block
.saturating_add(T::VotingPeriodAfterConsensus::get())
.saturated_into::<u64>(),
);
ProcessedTransactionsHash::<T>::insert(transaction_hash, true);
PendingTransactions::<T>::remove(deposit_id);
T::Fungibles::mint_into(deposit.token_id, &deposit.user_address, deposit.amount)?;
T::VaultManager::increase_underlying_balance(
deposit.token_id,
deposit.deposit_vault_id.clone(),
deposit.amount,
)?;
Self::deposit_event(Event::DepositAccepted { deposit_id });
Ok(())
})?;
Ok(())
}
/// The origin can decline a deposit request if they are currently registered.
///
/// Parameters:
/// - `deposit_id`: The deposit request with deposit ID `deposit_id` that is declined.
///
/// Emits `DepositDeclined` event when successful.
fn decline_deposit(deposit_id: DepositId) -> DispatchResult {
ensure!(DepositIdToDeposit::<T>::contains_key(deposit_id), Error::<T>::NoDepositId);
DepositIdToDeposit::<T>::mutate(deposit_id, |maybe_deposit| -> DispatchResult {
let deposit = maybe_deposit.as_mut().ok_or(Error::<T>::NoDepositId)?;
ensure!(
deposit.statcode == StatCode::Pending,
Error::<T>::DepositIdAlreadyExecuted
);
let current_block = <frame_system::Pallet<T>>::block_number();
deposit.statcode = StatCode::Cancelled;
deposit.can_vote_until = Some(
current_block
.saturating_add(T::VotingPeriodAfterConsensus::get())
.saturated_into::<u64>(),
);
PendingTransactions::<T>::remove(deposit_id);
Ok(())
})?;
Self::deposit_event(Event::DepositDeclined { deposit_id });
Ok(())
}
fn get_pool_id_worker(pool_id: T::AccountId) -> DispatchResult {
PoolWorkerId::<T>::put(pool_id);
Ok(())
}
/// Getter for the deposit info
fn get_deposit_info(
deposit_id: DepositId,
) -> Result<Deposit<T::AccountId, T::AssetId, T::BalanceOf, DepositId>, DispatchError> {
let deposit =
DepositIdToDeposit::<T>::get(deposit_id).ok_or(Error::<T>::NoDepositId)?;
Ok(deposit)
}
fn set_deposit_info_after_voting(
deposit_id: DepositId,
deposit: Deposit<T::AccountId, T::AssetId, T::BalanceOf, DepositId>,
) {
DepositIdToDeposit::<T>::insert(deposit_id, deposit.clone());
}
}
}