#![cfg_attr(not(feature = "std"), no_std)]
mod benchmarking;
mod mock;
pub mod rate;
mod rate_tests;
mod tests;
extern crate alloc;
pub use eq_oracle;
pub use eq_volatility::VolatilityGetter;
use codec::Codec;
use eq_balances::{BalanceChecker, BalanceGetter, EqCurrency, SignedBalance, SignedBalance::*};
use eq_oracle::PriceGetter;
use eq_primitives::{
currency, currency::Currency::*, currency::*, Aggregates, FeeManager, InterestRateError,
UserGroup,
};
use eq_utils::{eq_ensure, log::eq_log, ok_or_error, *};
use frame_support::{
debug, decl_error, decl_event, decl_module, decl_storage,
dispatch::DispatchResult,
storage::IterableStorageMap,
traits::{ExistenceRequirement, Get, UnixTime, WithdrawReason, WithdrawReasons},
Parameter,
};
use sp_arithmetic::{traits::Saturating, FixedI128, FixedI64, FixedPointNumber};
#[cfg(feature = "std")]
use sp_runtime::serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use sp_runtime::traits::Zero;
use sp_runtime::traits::{AtLeast32BitUnsigned, Bounded, MaybeSerializeDeserialize, Member};
use sp_runtime::Permill;
use sp_runtime::{traits::AccountIdConversion, ModuleId};
use sp_std::convert::From;
use sp_std::{collections::btree_map::BTreeMap, default::Default, fmt::Debug, prelude::*};
use system as frame_system;
use system::{self as system, ensure_root, ensure_signed};
pub trait Trait: system::Trait {
type ModuleId: Get<ModuleId>;
type PriceGetter: eq_oracle::PriceGetter;
type VolatilityGetter: VolatilityGetter;
type UnixTime: UnixTime;
type Balance: Member
+ AtLeast32BitUnsigned
+ MaybeSerializeDeserialize
+ Codec
+ Copy
+ Parameter
+ Default
+ From<u64>
+ Into<u64>;
type BalanceGetter: BalanceGetter<Self::AccountId, Self::Balance>;
type EqCurrency: eq_balances::EqCurrency<Self::AccountId, Self::Balance>;
type Aggregates: Aggregates<Self::AccountId, Self::Balance>;
type CriticalLtv: Get<Permill>;
type MinTempBalanceUsd: Get<Self::Balance>;
type MinimalCollaterall: Get<Self::Balance>;
type RiskLoverBound: Get<FixedI64>;
type RiskUpperBound: Get<FixedI64>;
type RiskTarget: Get<FixedI64>;
type RiskNSigma: Get<FixedI64>;
type RiskRho: Get<FixedI64>;
type Alpha: Get<FixedI64>;
}
decl_storage! {
trait Store for Module<T: Trait> as Bailsman {}
add_extra_genesis {
build(|_| {
T::Aggregates::set_usergroup(&Module::<T>::get_account_id(), &UserGroup::Bailsmen, &true);
T::Aggregates::set_usergroup(&Module::<T>::get_account_id(), &UserGroup::Balances, &true);
});
}
}
decl_error! {
pub enum Error for Module<T: Trait> {
CollaterallMustBeMoreThanMin,
AlreadyBailsman,
NotAnBailsman,
PricesAreOutdated,
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
type Error = Error<T>;
#[weight = 10_000]
fn reinit(origin) {
ensure_signed(origin)?;
Self::_reinit()?;
}
#[weight = 10_000]
pub fn register_bailsman(origin)
{
let who = ensure_signed(origin)?;
Self::_register_bailsman(&who)?;
}
#[weight = 10_000]
pub fn unregister_bailsman(origin)
{
let who = ensure_signed(origin)?;
Self::_unregister_bailsman(&who)?;
}
#[weight = 10_000]
pub fn force_unregister_bailsman(origin, who: T::AccountId)
{
ensure_root(origin)?;
Self::_unregister_bailsman(&who)?;
}
#[weight = 10_000]
fn transfer(origin, currency: Currency, to: T::AccountId, value: T::Balance) -> DispatchResult
{
let from = ensure_signed(origin)?;
Self::_reinit()?;
T::EqCurrency::currency_transfer(currency, &from, &to, value, ExistenceRequirement::AllowDeath, eq_primitives::TransferReason::Common, true)
}
}
}
impl<T: Trait> BalanceChecker<T::Balance, T::AccountId> for Module<T> {
fn can_change_balance(
who: &T::AccountId,
currency: ¤cy::Currency,
change: &SignedBalance<T::Balance>,
reason: Option<WithdrawReasons>,
) -> Result<bool, sp_runtime::DispatchError> {
let is_tx_fee = reason
.map(|rs| rs.contains(WithdrawReason::TransactionPayment))
.unwrap_or(false);
if T::Aggregates::in_usergroup(who, &UserGroup::Bailsmen) && !is_tx_fee {
let bails_balance =
T::BalanceGetter::get_debt_and_colaterall(&Module::<T>::get_account_id());
let min_temp_balance_usd = fixedi128_from_balance(T::MinTempBalanceUsd::get());
if (bails_balance.0 > min_temp_balance_usd) || (bails_balance.1 > min_temp_balance_usd)
{
return Ok(false);
}
}
if *who == Self::get_account_id() {
return Ok(false);
}
let new_balance = T::BalanceGetter::get_balance(who, currency) + change.clone();
if let Negative(_) = &new_balance {
if *currency != Usd {
return Ok(false);
}
}
Self::check_ltv(who, change, currency)
}
}
pub trait LtvChecker<AccountId, Balance>
where
Balance: Member + Debug,
{
fn calculate_ltv(
owner: &AccountId,
change: &SignedBalance<Balance>,
currency: ¤cy::Currency,
) -> Result<FixedI128, sp_runtime::DispatchError>;
fn check_ltv(
owner: &AccountId,
change: &SignedBalance<Balance>,
currency: ¤cy::Currency,
) -> Result<bool, sp_runtime::DispatchError>;
}
impl<T: Trait> LtvChecker<T::AccountId, T::Balance> for Module<T> {
fn calculate_ltv(
owner: &T::AccountId,
change: &SignedBalance<T::Balance>,
acurrency: ¤cy::Currency,
) -> Result<FixedI128, sp_runtime::DispatchError> {
let mut debt = FixedI128::zero();
let mut collateral = FixedI128::zero();
let mut prices_with_errors = false;
for currency in eq_primitives::currency::Currency::iterator_with_usd() {
let mut balance = T::BalanceGetter::get_balance(owner, currency);
if balance.is_zero() && currency != acurrency {
continue;
}
if currency == acurrency {
balance = balance + change.clone();
}
let raw_price = T::PriceGetter::get_price(currency);
prices_with_errors = prices_with_errors || raw_price.is_err();
let price = fixedi128_from_fixedi64(raw_price.unwrap_or(FixedI64::zero()));
match balance {
Positive(value) => {
collateral = collateral + fixedi128_from_balance(value) * price;
}
Negative(value) => {
eq_ensure!(!raw_price.is_err() && !price.is_zero(), Error::<T>::PricesAreOutdated,
"{}:{}. Price is zero or not retrieved. Currency: {:?}, is_error: {:?}, is_zero: {:?}.",
file!(), line!(), currency, raw_price.is_err(), price.is_zero());
debt = debt + fixedi128_from_balance(value) * price;
}
};
}
if debt.is_zero() {
return Ok(FixedI128::max_value());
}
eq_ensure!(
!prices_with_errors,
Error::<T>::PricesAreOutdated,
"{}:{}. Some prices are not retrieved.",
file!(),
line!()
);
if collateral.is_zero() {
return Ok(FixedI128::min_value());
}
let ltv = collateral / debt;
Ok(ltv)
}
fn check_ltv(
owner: &T::AccountId,
change: &SignedBalance<T::Balance>,
currency: ¤cy::Currency,
) -> Result<bool, sp_runtime::DispatchError> {
let ltv = Self::calculate_ltv(owner, change, currency)?;
Ok(ltv >= FixedI128::from(T::CriticalLtv::get()) + FixedI128::one())
}
}
impl<T: Trait> Module<T> {
pub fn get_account_id() -> T::AccountId {
T::ModuleId::get().into_account()
}
fn _reinit() -> Result<(), Error<T>> {
let mut total_collateral_usd = FixedI128::zero();
let mut total_debt_usd = FixedI128::zero();
let self_account = Self::get_account_id();
let self_balances = T::BalanceGetter::iterate_account_balances(&self_account);
let mut self_balances_usd = FixedI128::from_inner(0);
for balance in &self_balances {
let price =
fixedi128_from_fixedi64(T::PriceGetter::get_price(&balance.0).map_err(|_| {
debug::error!("{}:{}.", file!(), line!());
Error::<T>::PricesAreOutdated
})?);
match balance.1 {
Positive(value) => {
self_balances_usd = self_balances_usd + fixedi128_from_balance(value) * price;
}
Negative(value) => {
self_balances_usd = self_balances_usd - fixedi128_from_balance(value) * price;
}
};
}
let min_temp_balance_usd = fixedi128_from_balance(T::MinTempBalanceUsd::get());
if self_balances_usd.saturating_abs() <= min_temp_balance_usd {
eq_log!("no need to reinit {:?}", self_balances_usd);
return Ok(());
}
for balances_aggregate in T::Aggregates::iter_total(&UserGroup::Bailsmen) {
let price = fixedi128_from_fixedi64(
T::PriceGetter::get_price(&balances_aggregate.0).map_err(|_| {
debug::error!("{}:{}.", file!(), line!());
Error::<T>::PricesAreOutdated
})?,
);
total_collateral_usd = total_collateral_usd
+ fixedi128_from_balance(balances_aggregate.1.collateral) * price;
eq_log!(
"total_debt_usd {:?} = total_debt_usd {:?} + balaggr {:?}",
total_debt_usd + fixedi128_from_balance(balances_aggregate.1.debt) * price,
total_debt_usd,
fixedi128_from_balance(balances_aggregate.1.debt) * price
);
total_debt_usd =
total_debt_usd + fixedi128_from_balance(balances_aggregate.1.debt) * price;
}
eq_log!(
"total_collateral_usd {:?} , total_debt_usd {:?}",
total_collateral_usd,
total_debt_usd
);
let total_usd = total_collateral_usd - total_debt_usd - self_balances_usd;
eq_log!(
"total_usd {:?} = total_collateral_usd {:?} - total_debt_usd {:?}",
total_usd,
total_collateral_usd,
total_debt_usd
);
for bailsman_acc in T::Aggregates::iter_account(&UserGroup::Bailsmen) {
if bailsman_acc == self_account {
continue;
}
let bailsman_balances = T::BalanceGetter::iterate_account_balances(&bailsman_acc);
let mut bailsman_debt_usd = FixedI128::zero();
let mut bailsman_collat_usd = FixedI128::zero();
let mut bailsman_balances_cache: BTreeMap<Currency, SignedBalance<T::Balance>> =
BTreeMap::new();
for signed_balance in bailsman_balances {
let price = fixedi128_from_fixedi64(
T::PriceGetter::get_price(&signed_balance.0).map_err(|_| {
debug::error!("{}:{}.", file!(), line!());
Error::<T>::PricesAreOutdated
})?,
);
bailsman_balances_cache.insert(signed_balance.0, signed_balance.1.clone());
match signed_balance.1 {
SignedBalance::Positive(abalance) => {
bailsman_collat_usd =
bailsman_collat_usd + fixedi128_from_balance(abalance) * price;
}
SignedBalance::Negative(abalance) => {
bailsman_debt_usd =
bailsman_debt_usd + fixedi128_from_balance(abalance) * price;
}
};
}
let portion = (bailsman_collat_usd - bailsman_debt_usd) / total_usd;
eq_log!(
"portion {:?} = bailsman_collat_usd {:?} - bailsman_debt_usd {:?}) / total_usd {:?}",
portion,
bailsman_collat_usd,
bailsman_debt_usd,
total_usd
);
#[allow(unused_must_use)]
for self_balance in &self_balances {
match self_balance.1 {
SignedBalance::Positive(abalance) => {
let amount: T::Balance =
balance_from_fixedi128(fixedi128_from_balance(abalance) * portion)
.unwrap();
T::EqCurrency::currency_transfer(
self_balance.0,
&self_account,
&bailsman_acc,
amount,
ExistenceRequirement::KeepAlive,
eq_primitives::TransferReason::BailsmenRedistribution,
false,
);
}
SignedBalance::Negative(abalance) => {
let amount: T::Balance =
balance_from_fixedi128(fixedi128_from_balance(abalance) * portion)
.unwrap();
eq_log!("portion amount - is {:?} {:?}", self_balance.0, amount);
T::EqCurrency::currency_transfer(
self_balance.0,
&bailsman_acc,
&self_account,
amount,
ExistenceRequirement::KeepAlive,
eq_primitives::TransferReason::BailsmenRedistribution,
false,
);
}
}
}
}
Ok(())
}
fn _register_bailsman(who: &T::AccountId) -> Result<(), Error<T>> {
Self::_reinit()?;
let existing = T::Aggregates::in_usergroup(who, &UserGroup::Bailsmen);
eq_ensure!(
!existing,
Error::<T>::AlreadyBailsman,
"{}:{}. Account is already bailsman. Who: {:?}.",
file!(),
line!(),
who
);
let mut balances_vec: Vec<(Currency, (T::Balance, T::Balance))> = Vec::new();
let mut balance_usd: FixedI64 = FixedI64::zero();
for abalance in T::BalanceGetter::iterate_account_balances(who) {
let (currency, balance) = abalance;
match balance {
Positive(value) => {
balances_vec.push((currency, (value.clone(), T::Balance::zero())));
let price = T::PriceGetter::get_price(¤cy).unwrap_or(FixedI64::zero());
let collateral_usd = fixedi64_from_balance(value).saturating_mul(price);
balance_usd = balance_usd + collateral_usd;
}
Negative(value) => {
balances_vec.push((currency, (T::Balance::zero(), value.clone())));
let price = T::PriceGetter::get_price(¤cy).unwrap_or(FixedI64::zero());
let debt_usd = fixedi64_from_balance(value).saturating_mul(price);
balance_usd = balance_usd - debt_usd;
}
}
}
let min_collaterall = Into::<u64>::into(T::MinimalCollaterall::get());
let min_collaterall = FixedI64::saturating_from_integer(min_collaterall);
eq_ensure!(
balance_usd > min_collaterall,
Error::<T>::CollaterallMustBeMoreThanMin,
"{}:{}. Total usd balance less or equal to min value. Total usd balance: {:?}, min value: {:?}.",
file!(), line!(), balance_usd, min_collaterall
);
T::Aggregates::set_usergroup(who, &UserGroup::Bailsmen, &true);
Ok(())
}
fn _unregister_bailsman(who: &T::AccountId) -> Result<(), Error<T>> {
Self::_reinit()?;
let existing = T::Aggregates::in_usergroup(who, &UserGroup::Bailsmen);
eq_ensure!(
existing == true,
Error::<T>::NotAnBailsman,
"{}:{}. Account not a bailsman. Who: {:?}.",
file!(),
line!(),
who
);
T::Aggregates::set_usergroup(who, &UserGroup::Bailsmen, &false);
Ok(())
}
fn calc_fee_percent(owner: &T::AccountId) -> Result<FixedI128, InterestRateError> {
if cfg!(feature = "test") {
Ok(FixedI128::saturating_from_rational(2i128, 100i128))
} else {
let interest_rate = rate::interest_rate::<Self>(owner)?;
Ok(fixedi128_from_fixedi64(interest_rate))
}
}
}
impl<T: Trait> rate::InterestRateDataSource for Module<T> {
type AccountId = T::AccountId;
fn get_settings() -> rate::InterestRateSettings {
rate::InterestRateSettings::new(
T::RiskLoverBound::get(),
T::RiskUpperBound::get(),
T::RiskTarget::get(),
T::RiskNSigma::get(),
T::RiskRho::get(),
T::Alpha::get(),
)
}
fn get_price(currency: &Currency) -> Result<FixedI64, sp_runtime::DispatchError> {
T::PriceGetter::get_price(currency)
}
fn get_covariance(c1: &Currency, c2: &Currency) -> FixedI64 {
T::VolatilityGetter::get_covariance(c1, c2)
}
fn get_bailsman_total_balance(currency: &Currency) -> rate::TotalBalance {
let totals = T::Aggregates::get_total(&UserGroup::Bailsmen, currency);
rate::TotalBalance::new(
fixedi64_from_balance(totals.collateral),
fixedi64_from_balance(totals.debt),
)
}
fn get_total_balance(currency: &Currency) -> rate::TotalBalance {
let totals = T::Aggregates::get_total(&UserGroup::Balances, currency);
rate::TotalBalance::new(
fixedi64_from_balance(totals.collateral),
fixedi64_from_balance(totals.debt),
)
}
fn get_balance(account_id: &T::AccountId, currency: &Currency) -> rate::TotalBalance {
let balance = T::BalanceGetter::get_balance(account_id, currency);
match balance {
SignedBalance::Positive(value) => rate::TotalBalance::new(
fixedi64_from_balance(value),
FixedI64::saturating_from_integer(0),
),
SignedBalance::Negative(value) => rate::TotalBalance::new(
FixedI64::saturating_from_integer(0),
fixedi64_from_balance(value),
),
}
}
}
pub trait BailsmanManager<AccountId> {
fn get_temp_balances_usd() -> Result<FixedI128, sp_runtime::DispatchError>;
fn receive_position(who: &AccountId);
fn reinit();
}
impl<T: Trait> BailsmanManager<T::AccountId> for Module<T> {
fn get_temp_balances_usd() -> Result<FixedI128, sp_runtime::DispatchError> {
let self_account = Self::get_account_id();
let self_balances = T::BalanceGetter::iterate_account_balances(&self_account);
let mut balance_usd = FixedI128::from_inner(0);
for balance in self_balances {
let price = T::PriceGetter::get_price(&balance.0)?;
let price = fixedi128_from_fixedi64(price);
match balance.1 {
Positive(value) => {
balance_usd = balance_usd + fixedi128_from_balance(value) * price;
}
Negative(value) => {
balance_usd = balance_usd - fixedi128_from_balance(value) * price;
}
};
}
Ok(balance_usd)
}
#[allow(unused_must_use)]
fn reinit() {
Self::_reinit();
}
#[allow(unused_must_use)]
fn receive_position(who: &T::AccountId) {
let self_account = Module::<T>::get_account_id();
for balance in T::BalanceGetter::iterate_account_balances(who) {
match balance.1 {
SignedBalance::Positive(abalance) => {
T::EqCurrency::currency_transfer(
balance.0,
who,
&self_account,
abalance,
ExistenceRequirement::KeepAlive,
eq_primitives::TransferReason::MarginCall,
false,
);
}
SignedBalance::Negative(abalance) => {
T::EqCurrency::currency_transfer(
balance.0,
&self_account,
who,
abalance,
ExistenceRequirement::KeepAlive,
eq_primitives::TransferReason::MarginCall,
false,
);
}
}
}
}
}
impl<T: Trait> FeeManager<T::AccountId, T::Balance> for Module<T> {
fn calc_fee(owner: &T::AccountId, last_update: &u64) -> Result<T::Balance, InterestRateError> {
let fee = Self::calc_fee_percent(owner)?;
let (debt, _) = T::BalanceGetter::get_debt_and_colaterall(owner);
let current_time = T::UnixTime::now().as_secs();
let elapsed = FixedI128::saturating_from_integer(current_time - *last_update);
let debt_amount_usd =
fee * debt * elapsed / FixedI128::saturating_from_integer(365 as i128 * 24 * 60 * 60);
let price = T::PriceGetter::get_price(&Currency::Eq).map_err(|_| {
debug::error!("{}:{}.", file!(), line!());
InterestRateError::ExternalError
})?;
let debt_amount = debt_amount_usd / fixedi128_from_fixedi64(price);
let option_balance = balance_from_fixedi128(debt_amount);
ok_or_error!(
option_balance,
InterestRateError::ValueError,
"{}:{}. Couldn't convert debt amount from FixedI128 to balance. Amount: {:?}.",
file!(),
line!(),
debt_amount
)
}
#[allow(unused_must_use)]
fn charge_fee_inner(owner: &T::AccountId, fee_amount: &T::Balance) {
let self_account = Module::<T>::get_account_id();
T::EqCurrency::currency_transfer(
Currency::Eq,
owner,
&self_account,
*fee_amount,
ExistenceRequirement::KeepAlive,
eq_primitives::TransferReason::InterestFee,
false,
);
}
}