//! # Unit Vault Pallet
//! <!-- Original author of paragraph: @gang
//!
//! ## Overview
//!
//! Pallet that allows a user to add and remove vault addresses.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Add vault address.
//! * Remove vault address.
//! * Add unit token.
//! * Remove unit token.
//! * Execute add vault detail.
//! * Execute remove vault detail.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `buy_token`: Buy token with USDU.
//! - `sell_token` : Sell token for USDU.
//! - `stake_token` : Stake token with USDU.
//! - `unstake_token` : Unstake token to receive token and USDU.
//! - `set_usdu_token_id` : Set USDU token id.
//! - `set_unit_token_id` : Set Unit token id.
//! - `get_liquidity` : Get pool liquidity's token.
//!
//!//! 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::membership::MembershipInterface;
#[cfg(feature = "runtime-benchmarks")]
use traits::profile::ProfileInterface;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use base64ct::{ Base64, Encoding };
use bitcoin::{ self, address::{ NetworkChecked, NetworkUnchecked }, Address, Network };
use frame_support::{
pallet_prelude::*,
sp_runtime::traits::{ CheckedDiv, CheckedMul, CheckedSub, Hash },
traits::{
fungibles,
tokens::{
fungibles::{ Inspect, Mutate },
Balance,
Fortitude::Polite,
Precision::Exact,
Preservation::Expendable,
},
SortedMembers,
},
PalletId,
};
use frame_system::pallet_prelude::*;
use sp_io::{ crypto::secp256k1_ecdsa_recover, hashing::keccak_256 };
use sp_runtime::{ traits::{ AccountIdConversion, Saturating }, FixedPointOperand };
use sp_std::vec::Vec;
use traits::{
asset::{ AssetInterface, TokenType },
oracle::OracleInterface,
pool::PoolInterface,
subaccounts::{ AccountOrigin, SubAccounts, SubAccountsAddress },
};
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;
/// 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, Balance = Self::BalanceOf> +
fungibles::Mutate<Self::AccountId> +
fungibles::metadata::Inspect<Self::AccountId> +
AssetInterface<
<Self::Fungibles as Inspect<Self::AccountId>>::AssetId,
BalanceOf<Self>,
Self::AccountId,
Decimals,
TokenType
>;
/// Membership of Executor.
type ExecutorMembers: SortedMembers<Self::AccountId>;
/// Vault pallet id, keep all assets in vault.
#[pallet::constant]
type PalletId: Get<PalletId>;
type BalanceOf: Balance +
FixedPointOperand +
MaxEncodedLen +
MaybeSerializeDeserialize +
TypeInfo +
From<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 to access the Pool Pallet.
type Pool: PoolInterface<AssetId<Self>, BalanceOf<Self>>;
/// Decimals of USDU
#[pallet::constant]
type UsduTokenDecimals: Get<u32>;
/// Unit base asset id.
#[pallet::constant]
type UnitAssetId: Get<AssetId<Self>>;
/// The maximum length of strings.
#[pallet::constant]
type StringLimit: Get<u32>;
/// Type to access the sub account pallet.
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin> +
SubAccountsAddress<AssetId<Self>, Self::AccountId, Self::Hash>;
#[pallet::constant]
type BitcoinAssetId: Get<AssetId<Self>>;
#[pallet::constant]
type EthereumAssetId: Get<AssetId<Self>>;
#[pallet::constant]
type NearAssetId: Get<AssetId<Self>>;
#[pallet::constant]
type PolygonAssetId: Get<AssetId<Self>>;
type ProfileStringLimit: Get<u32>;
/// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: ProfileInterface<Self::AccountId, Self::ProfileStringLimit>;
/// Helper trait for membership interface.
#[cfg(feature = "runtime-benchmarks")]
type MembershipHelper: MembershipInterface<Self::AccountId> +
SortedMembers<Self::AccountId>;
// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
/// Mapping User Address -> AssetId -> External Address
/// Vault Balance of each user are provide in this.
#[pallet::storage]
#[pallet::getter(fn user_asset_vault)]
pub type UserAssetVault<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Twox64Concat,
AssetId<T>,
VaultBalance<T::AccountId, AssetId<T>, BalanceOf<T>, T::StringLimit>
>;
/// Mapping AssetId -> Vault Details
/// Vault Details of each assetId.
/// When external on chain has deposit should also update on this.
#[pallet::storage]
#[pallet::getter(fn asset_vault_detail)]
pub type AssetVaultDetails<T: Config> = StorageMap<
_,
Twox64Concat,
AssetId<T>,
VaultDetails<AssetId<T>, BalanceOf<T>>
>;
/// Mapping AssetId -> Users
/// All users that register on each vaults
#[pallet::storage]
#[pallet::getter(fn vault_users)]
pub type VaultUsers<T: Config> = StorageMap<
_,
Twox64Concat,
AssetId<T>,
Vec<T::AccountId>,
ValueQuery
>;
/// Mapping Vec<u8> -> Users
/// External vault address to account id
#[pallet::storage]
#[pallet::getter(fn vault_address_user)]
pub type VaultAddressUser<T: Config> = StorageMap<
_,
Twox64Concat,
BoundedVec<u8, T::StringLimit>,
T::AccountId
>;
// 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> {
AddedVaultAddress {
who: T::AccountId,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
},
ChangedVaultAddress {
who: T::AccountId,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
},
AddedUnitToken {
who: T::AccountId,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
amount: BalanceOf<T>,
},
RemovedUnitToken {
who: T::AccountId,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
amount: BalanceOf<T>,
},
ExecutedAddVault {
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
amount: BalanceOf<T>,
},
ExecutedRemoveVault {
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
amount: BalanceOf<T>,
},
}
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
NoPermission,
AssetIdNotProvided,
AlreadyHasVaultAddress,
NoVaultAddress,
NoVaultDetails,
NotEnoughBalance,
UnderlyingBalanceNonZero,
InsufficientVaultBalance,
InsufficientVaultDetailsBalance,
AlreadyUsedVaultAddress,
VerificationFailed,
InvalidSignedMessage,
InvalidPublicKey,
UnprotectedAsset,
ProtectedAsset,
AssetIdDoesNotExist,
Overflow,
Underflow,
TooHighAmount,
UnitAssetIdNotStored,
NoDecimals,
NoOracleProvided,
}
// 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> {
#[pallet::call_index(0)]
/// The origin can add vault address.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `vault_address`: Vault address.
///
/// Emits `AddedVaultAddress` event when successful.
pub fn add_vault_address(
origin: OriginFor<T>,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
let address_hash = T::Hashing::hash(&vault_address);
T::SubAccounts::is_account_address_owner(who.clone(), asset_id.clone(), address_hash)?;
ensure!(
!UserAssetVault::<T>::contains_key(&who, &asset_id),
Error::<T>::AlreadyHasVaultAddress
);
ensure!(
!VaultAddressUser::<T>::contains_key(vault_address.clone()),
Error::<T>::AlreadyUsedVaultAddress
);
let vault_balance = VaultBalance {
account_id: who.clone(),
asset_id: asset_id.clone(),
balance: Default::default(),
underlying_balance: Default::default(),
vault_address: vault_address.clone(),
};
VaultAddressUser::<T>::insert(vault_address.clone(), who.clone());
UserAssetVault::<T>::insert(&who, &asset_id, vault_balance);
VaultUsers::<T>::mutate(&asset_id, |val| -> DispatchResult {
val.push(who.clone());
Ok(())
})?;
Self::deposit_event(Event::AddedVaultAddress { who, asset_id, vault_address });
Ok(())
}
#[pallet::call_index(1)]
/// The origin can change vault address.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `vault_address`: Vault address.
/// - `signed_message`: Signature
///
/// Emits `ChangedVaultAddress` event when successful.
pub fn change_vault_address(
origin: OriginFor<T>,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(UserAssetVault::<T>::contains_key(&who, &asset_id), Error::<T>::NoVaultAddress);
ensure!(
!VaultAddressUser::<T>::contains_key(vault_address.clone()),
Error::<T>::AlreadyUsedVaultAddress
);
let address_hash = T::Hashing::hash(&vault_address);
T::SubAccounts::is_account_address_owner(who.clone(), asset_id.clone(), address_hash)?;
UserAssetVault::<T>::try_mutate(
who.clone(),
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
// Delete old address
VaultAddressUser::<T>::remove(vault_balance.vault_address.clone());
vault_balance.vault_address = vault_address.clone();
// Add new address
VaultAddressUser::<T>::insert(vault_address.clone(), who.clone());
Self::deposit_event(Event::ChangedVaultAddress {
who,
asset_id,
vault_address,
});
Ok(())
}
)?;
Ok(())
}
#[pallet::call_index(2)]
/// The origin can add unit token to each vault.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `amount`: Amount of token.
///
/// Emits `AddedUnitToken` event when successful.
pub fn add_unit_token(
origin: OriginFor<T>,
asset_id: AssetId<T>,
amount: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(UserAssetVault::<T>::contains_key(&who, &asset_id), Error::<T>::NoVaultAddress);
// Get the UNIT token ID
let unit_asset_id = T::UnitAssetId::get();
<T as pallet::Config>::Fungibles::transfer(
unit_asset_id,
&who,
&Self::account_id(),
amount,
Expendable
)?;
UserAssetVault::<T>::try_mutate(
who.clone(),
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
vault_balance.balance = vault_balance.balance.saturating_add(amount);
Self::deposit_event(Event::AddedUnitToken {
who,
asset_id: asset_id.clone(),
vault_address: vault_balance.vault_address.clone(),
amount,
});
Ok(())
}
)?;
match AssetVaultDetails::<T>::get(asset_id.clone()) {
Some(_) => {
AssetVaultDetails::<T>::try_mutate(
asset_id.clone(),
|maybe_vault_details| -> DispatchResult {
let vault_details = maybe_vault_details
.as_mut()
.ok_or(Error::<T>::NoVaultDetails)?;
vault_details.balance = vault_details.balance.saturating_add(amount);
Ok(())
}
)?;
}
None => {
let vault_detail = VaultDetails {
asset_id: asset_id.clone(),
balance: amount,
underlying_balance: Default::default(),
};
AssetVaultDetails::<T>::insert(asset_id.clone(), vault_detail);
}
}
Ok(())
}
#[pallet::call_index(3)]
/// The origin can remove unit token to each vault.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `amount`: Amount of token.
///
/// Emits `RemovedUnitToken` event when successful.
pub fn remove_unit_token(
origin: OriginFor<T>,
asset_id: AssetId<T>,
amount: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(UserAssetVault::<T>::contains_key(&who, &asset_id), Error::<T>::NoVaultAddress);
// Check if underlying_balance is zero
let vault_balance_opt = UserAssetVault::<T>::get(&who, &asset_id);
let underlying_balance = if let Some(vault_balance) = vault_balance_opt {
vault_balance.underlying_balance
} else {
return Err(Error::<T>::NoVaultAddress.into());
};
ensure!(underlying_balance == Default::default(), Error::<T>::UnderlyingBalanceNonZero);
// Get the UNIT token ID
let unit_asset_id = T::UnitAssetId::get();
<T as pallet::Config>::Fungibles::transfer(
unit_asset_id,
&Self::account_id(),
&who,
amount,
Expendable
)?;
UserAssetVault::<T>::try_mutate(
who.clone(),
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
ensure!(vault_balance.balance >= amount, Error::<T>::NotEnoughBalance);
vault_balance.balance = vault_balance.balance.saturating_sub(amount);
Self::deposit_event(Event::RemovedUnitToken {
who,
asset_id: asset_id.clone(),
vault_address: vault_balance.vault_address.clone(),
amount,
});
Ok(())
}
)?;
AssetVaultDetails::<T>::try_mutate(asset_id, |maybe_vault_details| -> DispatchResult {
let vault_details = maybe_vault_details.as_mut().ok_or(Error::<T>::NoVaultDetails)?;
vault_details.balance = vault_details.balance.saturating_sub(amount);
Ok(())
})?;
Ok(())
}
#[pallet::call_index(4)]
/// The origin can execute add vault detail.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `vault_address`: Vault address.
/// - `amount`: Amount of token.
///
/// Emits `ExecutedAddVault` event when successful.
pub fn execute_add_vault_detail(
origin: OriginFor<T>,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
amount: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(T::ExecutorMembers::contains(&who), Error::<T>::NoPermission);
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(AssetVaultDetails::<T>::contains_key(&asset_id), Error::<T>::NoVaultDetails);
AssetVaultDetails::<T>::try_mutate(
asset_id.clone(),
|maybe_vault_details| -> DispatchResult {
let vault_details = maybe_vault_details
.as_mut()
.ok_or(Error::<T>::NoVaultDetails)?;
vault_details.underlying_balance =
vault_details.underlying_balance.saturating_add(amount);
let user_account = VaultAddressUser::<T>
::get(&vault_address)
.ok_or(Error::<T>::NoVaultAddress);
UserAssetVault::<T>::try_mutate(
&user_account.unwrap(),
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
vault_balance.underlying_balance =
vault_balance.underlying_balance.saturating_add(amount);
Ok(())
}
)?;
Self::deposit_event(Event::ExecutedAddVault {
asset_id,
vault_address,
amount,
});
Ok(())
}
)?;
Ok(())
}
#[pallet::call_index(5)]
/// The origin can execute remove vault detail.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `vault_address`: Vault address.
/// - `amount`: Amount of token.
///
/// Emits `ExecutedRemoveVault` event when successful.
pub fn execute_remove_vault_detail(
origin: OriginFor<T>,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
amount: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(T::ExecutorMembers::contains(&who), Error::<T>::NoPermission);
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(AssetVaultDetails::<T>::contains_key(&asset_id), Error::<T>::NoVaultDetails);
AssetVaultDetails::<T>::try_mutate(
asset_id.clone(),
|maybe_vault_details| -> DispatchResult {
let vault_details = maybe_vault_details
.as_mut()
.ok_or(Error::<T>::NoVaultDetails)?;
vault_details.underlying_balance =
vault_details.underlying_balance.saturating_sub(amount);
let user_account = VaultAddressUser::<T>
::get(&vault_address)
.ok_or(Error::<T>::NoVaultAddress);
UserAssetVault::<T>::try_mutate(
&user_account.unwrap(),
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
vault_balance.underlying_balance =
vault_balance.underlying_balance.saturating_sub(amount);
Ok(())
}
)?;
Self::deposit_event(Event::ExecutedRemoveVault {
asset_id,
vault_address,
amount,
});
Ok(())
}
)?;
Ok(())
}
#[pallet::call_index(6)]
pub fn burn_asset_based_on_underlying_balance(
origin: OriginFor<T>,
asset_id: AssetId<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
UserAssetVault::<T>::try_mutate(
&who,
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
let amount_to_burn = vault_balance.underlying_balance;
let user_balance = <T as pallet::Config>::Fungibles::balance(
asset_id.clone(),
&who
);
ensure!(user_balance >= amount_to_burn, Error::<T>::NotEnoughBalance);
//Burn.
<T as pallet::Config>::Fungibles::burn_from(
asset_id,
&who,
amount_to_burn,
Exact,
Polite
)?;
// Update underlying balance
vault_balance.underlying_balance = Default::default();
Ok(())
}
)?;
Ok(())
}
#[pallet::call_index(7)]
#[pallet::weight({ 0 })]
pub fn burn_amount_underlying_balance(
origin: OriginFor<T>,
asset_id: AssetId<T>,
amount: BalanceOf<T>
) -> DispatchResult {
let who = ensure_signed(origin)?;
// Mutate the origin to use the main account
let main_who = T::SubAccounts::get_main_account(who)?;
ensure!(
<<T as pallet::Config>::Fungibles as Inspect<T::AccountId>>::asset_exists(
asset_id.clone()
),
Error::<T>::AssetIdDoesNotExist
);
UserAssetVault::<T>::try_mutate(
&main_who,
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
let underlying_balance = vault_balance.underlying_balance;
let user_balance = <T as pallet::Config>::Fungibles::balance(
asset_id.clone(),
&main_who
);
ensure!(user_balance >= amount, Error::<T>::NotEnoughBalance);
if underlying_balance < amount {
// Burn.
<T as pallet::Config>::Fungibles::burn_from(
asset_id.clone(),
&main_who,
underlying_balance,
Exact,
Polite
)?;
// Update underlying balance
vault_balance.underlying_balance = Default::default();
} else {
// Burn.
<T as pallet::Config>::Fungibles::burn_from(
asset_id.clone(),
&main_who,
amount,
Exact,
Polite
)?;
// Update underlying balance
vault_balance.underlying_balance = vault_balance.underlying_balance
.checked_sub(&amount)
.ok_or(Error::<T>::Underflow)?;
}
// Update AssetVaultDetails
AssetVaultDetails::<T>::try_mutate(
asset_id.clone(),
|maybe_vault_details| -> DispatchResult {
let vault_details = maybe_vault_details
.as_mut()
.ok_or(Error::<T>::NoVaultDetails)?;
if vault_details.underlying_balance < amount {
vault_details.underlying_balance = Default::default();
} else {
vault_details.underlying_balance = vault_details.underlying_balance
.checked_sub(&amount)
.ok_or(Error::<T>::Underflow)?;
}
Ok(())
}
)?;
Ok(())
}
)?;
Ok(())
}
/// The origin can add vault address.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `vault_address`: Vault address.
///
/// Emits `AddedVaultAddress` event when successful.
#[pallet::call_index(8)]
#[pallet::weight({ 0 })]
pub fn add_vault_address_unprotected(
origin: OriginFor<T>,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(
asset_id != T::BitcoinAssetId::get() &&
asset_id != T::EthereumAssetId::get() &&
asset_id != T::PolygonAssetId::get(),
Error::<T>::ProtectedAsset
);
ensure!(
!UserAssetVault::<T>::contains_key(&who, &asset_id),
Error::<T>::AlreadyHasVaultAddress
);
ensure!(
!VaultAddressUser::<T>::contains_key(vault_address.clone()),
Error::<T>::AlreadyUsedVaultAddress
);
let vault_balance = VaultBalance {
account_id: who.clone(),
asset_id: asset_id.clone(),
balance: Default::default(),
underlying_balance: Default::default(),
vault_address: vault_address.clone(),
};
VaultAddressUser::<T>::insert(vault_address.clone(), who.clone());
UserAssetVault::<T>::insert(&who, &asset_id, vault_balance);
VaultUsers::<T>::mutate(&asset_id, |val| -> DispatchResult {
val.push(who.clone());
Ok(())
})?;
Self::deposit_event(Event::AddedVaultAddress { who, asset_id, vault_address });
Ok(())
}
/// The origin can change vault address.
///
/// Parameters:
/// - `asset_id`: Asset id.
/// - `vault_address`: Vault address.
///
/// Emits `ChangedVaultAddress` event when successful.
#[pallet::call_index(9)]
#[pallet::weight({ 0 })]
pub fn change_vault_address_unprotected(
origin: OriginFor<T>,
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
<<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::asset_exists(asset_id.clone())?;
ensure!(UserAssetVault::<T>::contains_key(&who, &asset_id), Error::<T>::NoVaultAddress);
ensure!(
!VaultAddressUser::<T>::contains_key(vault_address.clone()),
Error::<T>::AlreadyUsedVaultAddress
);
ensure!(
asset_id != T::BitcoinAssetId::get() &&
asset_id != T::EthereumAssetId::get() &&
asset_id != T::NearAssetId::get() &&
asset_id != T::PolygonAssetId::get(),
Error::<T>::ProtectedAsset
);
UserAssetVault::<T>::try_mutate(
who.clone(),
asset_id.clone(),
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
// Delete old address
VaultAddressUser::<T>::remove(vault_balance.vault_address.clone());
vault_balance.vault_address = vault_address.clone();
// Add new address
VaultAddressUser::<T>::insert(vault_address.clone(), who.clone());
Self::deposit_event(Event::ChangedVaultAddress {
who,
asset_id,
vault_address,
});
Ok(())
}
)?;
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
pub fn verify_signature(
asset_id: AssetId<T>,
vault_address: BoundedVec<u8, T::StringLimit>,
signed_message: BoundedVec<u8, T::StringLimit>
) -> Result<(), Error<T>> {
let btc = T::BitcoinAssetId::get();
let eth = T::EthereumAssetId::get();
let polygon = T::PolygonAssetId::get();
if asset_id == btc {
Self::verify_bitcoin(signed_message, vault_address)?;
Ok(())
} else if asset_id == eth || asset_id == polygon {
Self::verify_eth(signed_message, vault_address)?;
Ok(())
} else {
Err(Error::<T>::UnprotectedAsset)
}
}
// Constructs the message that Ethereum RPC's `personal_sign` and `eth_sign` would sign.
pub fn ethereum_signable_message() -> Vec<u8> {
let what = b"Hello Unit".to_vec();
let mut l = what.len();
let mut rev = Vec::new();
// convert decimal into bytes
while l > 0 {
rev.push(b'0' + ((l % 10) as u8));
l /= 10;
}
let mut v = b"\x19Ethereum Signed Message:\n".to_vec();
v.extend(rev.into_iter().rev());
v.extend_from_slice(&what);
v
}
// Attempts to recover the Ethereum address from a message signature signed by using
// the Ethereum RPC's `personal_sign` and `eth_sign`.
pub fn eth_recover(s: &[u8; 65]) -> Option<[u8; 20]> {
let msg = keccak_256(&Self::ethereum_signable_message());
let mut res = [0u8; 20];
res[0..20].copy_from_slice(
&keccak_256(&secp256k1_ecdsa_recover(s, &msg).ok()?[..])[12..]
);
Some(res)
}
pub fn verify_eth(
signed_message: BoundedVec<u8, T::StringLimit>,
vault_address: BoundedVec<u8, T::StringLimit>
) -> Result<(), Error<T>> {
let vault_address_array: [u8; 20] = vault_address
.as_slice()
.try_into()
.map_err(|_| Error::<T>::InvalidPublicKey)?;
let signed_message_array: [u8; 65] = signed_message
.as_slice()
.try_into()
.map_err(|_| Error::<T>::InvalidSignedMessage)?;
let address = Self::eth_recover(&signed_message_array);
ensure!(address == Some(vault_address_array), Error::<T>::VerificationFailed);
Ok(())
}
pub fn verify_bitcoin(
signed_message: BoundedVec<u8, T::StringLimit>,
vault_address: BoundedVec<u8, T::StringLimit>
) -> Result<(), Error<T>> {
// Construct signature
let mut dec_buf = [0u8; 65];
let signature_message = Base64::decode(
sp_std::str
::from_utf8(&signed_message[..])
.map_err(|_| Error::<T>::InvalidSignedMessage)?,
&mut dec_buf
).map_err(|_| Error::<T>::InvalidSignedMessage)?;
let signature = bitcoin::sign_message::MessageSignature
::from_slice(signature_message)
.map_err(|_| Error::<T>::InvalidSignedMessage)?;
// Construct message
let message = "Hello Unit";
let msg_hash = bitcoin::sign_message::signed_msg_hash(message);
// Construct address
let address_str = sp_std::str
::from_utf8(&vault_address[..])
.map_err(|_| Error::<T>::InvalidPublicKey)?;
let address: Address<NetworkUnchecked> = address_str
.parse()
.map_err(|_| Error::<T>::InvalidPublicKey)?;
let address: Address<NetworkChecked> = address
.require_network(Network::Bitcoin)
.map_err(|_| Error::<T>::InvalidPublicKey)?;
// Verify signature is signed by address
let secp = bitcoin::secp256k1::Secp256k1::new();
// Returns Ok(false) if the proccess was successful but the message was not signed by
// the address
match signature.is_signed_by_address(&secp, &address, msg_hash) {
Ok(true) => Ok(()),
_ => Err(Error::<T>::VerificationFailed),
}
}
pub fn u128_to_asset_balance(input: u128) -> BalanceOf<T> {
input.into()
}
// pub fn u128_to_asset_balance(input: u128) -> BalanceOf<T> {
// input.into()
// }
pub fn get_tranche(amount: u128) -> Option<BalanceOf<T>> {
// Retrieve unit decimals
let unit_decimals = <<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::get_asset_decimals(&T::UnitAssetId::get())?;
let maybe_amount = amount.checked_mul((10u128).checked_pow(unit_decimals.into())?)?;
Some(maybe_amount.into())
}
pub fn to_usdu(asset_amount: BalanceOf<T>, asset_id: AssetId<T>) -> Option<BalanceOf<T>> {
let asset_decimals = <<T as pallet::Config>::Fungibles as traits::asset::AssetInterface<
AssetId<T>,
BalanceOf<T>,
T::AccountId,
Decimals,
TokenType
>>::get_asset_decimals((&asset_id).into())?;
let asset_price = if asset_id == T::UnitAssetId::get() {
// Calculate unit price through the liquidity pool
let (unit_reserve, usdu_reserve) = T::Pool::get_liquidity_pool(asset_id);
let unit_price = usdu_reserve
.checked_mul(
&(1u128).checked_mul((10u128).checked_pow(asset_decimals.into())?)?.into()
)?
.checked_div(&unit_reserve)?;
unit_price
} else {
let (asset_price, _) = T::OracleInterface::get_oracle_key_value(asset_id.clone())?;
asset_price
};
asset_price
.checked_mul(&asset_amount)?
.checked_div(
&Self::u128_to_asset_balance((10_u128).checked_pow(asset_decimals.into())?)
)
}
}
impl<T: Config> VaultAddressManager<AssetId<T>, T::AccountId, BalanceOf<T>> for Pallet<T> {
fn calculate_deposit_vault_address(
asset_id: AssetId<T>,
amount: BalanceOf<T>
) -> Option<(Vec<u8>, T::AccountId)> {
let users_vault_addressess = VaultUsers::<T>::get(&asset_id);
if users_vault_addressess.is_empty() {
return None;
}
// Determine the tranche based on the amount
let tranche = match amount {
amount if amount >= Self::get_tranche(10)? && amount < Self::get_tranche(100)? =>
(Self::get_tranche(100)?, Self::get_tranche(1000)?),
amount if amount >= Self::get_tranche(100)? && amount < Self::get_tranche(1000)? =>
(Self::get_tranche(1000)?, Self::get_tranche(10000)?),
amount if amount >= Self::get_tranche(1000)? && amount < Self::get_tranche(10000)? =>
(Self::get_tranche(10000)?, Self::get_tranche(100000)?),
amount if
amount >= Self::get_tranche(10000)? &&
amount < Self::get_tranche(100000)?
=> (Self::get_tranche(100000)?, Self::get_tranche(1000000)?),
amount if
amount >= Self::get_tranche(100000)? &&
amount < Self::get_tranche(1000000)?
=> (Self::get_tranche(1000000)?, Self::get_tranche(10000000)?),
amount if
amount >= Self::get_tranche(1000000)? &&
amount < Self::get_tranche(10000000)?
=> (Self::get_tranche(10000000)?, Self::get_tranche(100000000)?),
_ => {
return None;
} // Amount not in any tranche
};
let mut best_account_address: Option<T::AccountId> = None;
let mut best_account_ratio: BalanceOf<T> = Default::default();
for cur_address in &users_vault_addressess {
let vault_detail = UserAssetVault::<T>::get(cur_address, &asset_id)?;
// Balance is in unit
let vault_balance = Self::to_usdu(vault_detail.balance, T::UnitAssetId::get())?;
// underlying balance in token
let vault_underlying_balance = Self::to_usdu(
vault_detail.underlying_balance,
asset_id.clone()
)?;
if
vault_balance == Default::default() ||
vault_balance < tranche.0 ||
vault_balance >= tranche.1 ||
vault_balance < vault_underlying_balance.saturating_add(amount)
{
continue;
}
let cur_ratio = if vault_underlying_balance == Default::default() {
vault_balance
} else {
// let (asset_price, _) =
// T::OracleInterface::get_oracle_key_value(asset_id.clone())?;
// let underlying_balance_usdu =
// asset_price.checked_mul(&vault_detail.underlying_balance)?;
// // Calculate unit price through the liquidity pool
// let (unit_reserve, usdu_reserve) =
// T::Pool::get_liquidity_pool(Self::get_unit_id());
// let unit_price = usdu_reserve
// .checked_mul(&(1u128)
// .checked_mul(
// (10u128)
// .checked_pow(T::UsduTokenDecimals::get())?,
// )?.into()
// )?
// .checked_div(&unit_reserve)?;
// let underlying_balance_unit = underlying_balance_usdu
// .checked_div(&unit_price)?;
vault_balance.checked_div(&vault_underlying_balance)?
};
if cur_ratio >= best_account_ratio {
best_account_address = Some(cur_address.clone());
best_account_ratio = cur_ratio;
}
}
let fetched_address = best_account_address.ok_or(Error::<T>::TooHighAmount).ok()?;
match UserAssetVault::<T>::get(&fetched_address, &asset_id) {
Some(user_account) => Some((user_account.vault_address.into(), fetched_address)),
None => None,
}
}
fn increase_underlying_balance(
asset_id: AssetId<T>,
who: T::AccountId,
amount: BalanceOf<T>
) -> DispatchResult {
UserAssetVault::<T>::try_mutate(
who.clone(),
&asset_id,
|maybe_vault_balance| -> DispatchResult {
let vault_balance = maybe_vault_balance
.as_mut()
.ok_or(Error::<T>::NoVaultAddress)?;
vault_balance.underlying_balance =
vault_balance.underlying_balance.saturating_add(amount);
Ok(())
}
)?;
AssetVaultDetails::<T>::try_mutate(asset_id, |maybe_vault_details| -> DispatchResult {
let vault_details = maybe_vault_details.as_mut().ok_or(Error::<T>::NoVaultDetails)?;
vault_details.underlying_balance =
vault_details.underlying_balance.saturating_add(amount);
Ok(())
})
}
fn get_unit_id() -> AssetId<T> {
T::UnitAssetId::get()
}
}
}