diff --git a/blockchain/modules/ecdp-ussd-loans/Cargo.toml b/blockchain/modules/ecdp-ussd-loans/Cargo.toml index b3edf82e..ea8cb6a2 100644 --- a/blockchain/modules/ecdp-ussd-loans/Cargo.toml +++ b/blockchain/modules/ecdp-ussd-loans/Cargo.toml @@ -5,46 +5,41 @@ authors = ["Setheum Labs"] edition = "2021" [dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true, features = ["max-encoded-len"] } scale-info = { workspace = true } -serde = { workspace = true, optional = true } -parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } -sp-runtime = { workspace = true } -sp-io = { workspace = true } -sp-std = { workspace = true } -frame-support = { workspace = true } + frame-system = { workspace = true } +frame-support = { workspace = true } +sp-std = { workspace = true } +sp-runtime = { workspace = true } -primitives = { package = "setheum-primitives", path = "../primitives", default-features = false } -support = { package = "module-support", path = "../support", default-features = false } -orml-traits = { path = "../submodules/orml/traits", default-features = false } +orml-traits = { workspace = true } +primitives = { workspace = true } +module-support = { workspace = true } [dev-dependencies] sp-core = { workspace = true, features = ["std"] } -pallet-balances = { workspace = true } -orml-tokens = { workspace = true } +sp-io = { workspace = true, features = ["std"] } +pallet-balances = { workspace = true, features = ["std"] } +orml-currencies = { workspace = true, features = ["std"] } +orml-tokens = { workspace = true, features = ["std"] } +module-ecdp-ussd-treasury = { workspace = true, features = ["std"] } [features] default = ["std"] std = [ - "scale-info/std", - "serde", "parity-scale-codec/std", - "sp-runtime/std", - "sp-std/std", - "sp-io/std", "frame-support/std", "frame-system/std", - "primitives/std", - "support/std", "orml-traits/std", -] -runtime-benchmarks = [ - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", + "primitives/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "module-support/std", ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "sp-runtime/try-runtime", ] diff --git a/blockchain/modules/ecdp-ussd-loans/TODO.md b/blockchain/modules/ecdp-ussd-loans/TODO.md index 3e9cd565..35e42ffa 100644 --- a/blockchain/modules/ecdp-ussd-loans/TODO.md +++ b/blockchain/modules/ecdp-ussd-loans/TODO.md @@ -54,3 +54,4 @@ These tasks are just for this file specifically. - [x] [[TODO.md:0] - Add TODO.md File](TODO.md): Add a TODO.md file to organise TODOs in the repo. - [x] [[TODO.md:1] - Add a `task_title`](/TODO.md/#tasks): Adda `task_title`. +- [ ] [[src/lib.rs:0] - Remove this from this module and add it to `EcdpSetrLoans` module.](/blockchain/modules/ecdp-ussd-loans/src/lib.rs): Add `OnLoanUpdate` to [EcdpSetrLoans Module](/blockchain/modules/ecdp-setr-loans/). diff --git a/blockchain/modules/ecdp-ussd-loans/src/lib.rs b/blockchain/modules/ecdp-ussd-loans/src/lib.rs new file mode 100644 index 00000000..72eaf42a --- /dev/null +++ b/blockchain/modules/ecdp-ussd-loans/src/lib.rs @@ -0,0 +1,378 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! # ECDP USSD Loans Module +//! +//! ## Overview +//! +//! ECDP USSD Loans module manages ECDP's collateral assets and the debits backed by these +//! assets. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] +#![allow(clippy::collapsible_if)] + +use frame_support::{pallet_prelude::*, transactional, PalletId}; +use module_support::{SlickUsdEcdpTreasury, SlickUsdRiskManager}; +use orml_traits::{Happened, MultiCurrency, MultiCurrencyExtended}; +use primitives::{Amount, Balance, CurrencyId, Position}; +use sp_runtime::{ + traits::{AccountIdConversion, Zero}, + ArithmeticError, DispatchResult, +}; + +mod mock; +mod tests; + +pub use module::*; + +#[frame_support::pallet] +pub mod module { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Currency type for deposit/withdraw collateral assets to/from USSD Loans module + type Currency: MultiCurrencyExtended< + Self::AccountId, + CurrencyId = CurrencyId, + Balance = Balance, + Amount = Amount, + >; + + /// Risk manager is used to limit the debit size of CDP. + type SlickUsdRiskManager: SlickUsdRiskManager; + + /// CDP treasury for issuing/burning USSD and debit value adjustment. + type SlickUsdEcdpTreasury: SlickUsdEcdpTreasury; + + /// The loan's module id, keep all collaterals of CDPs. + #[pallet::constant] + type PalletId: Get; + + // Remove it based on `TODO:[src/lib.rs:0]`. + /// Event handler which calls when update loan. + // type OnUpdateLoan: Happened<(Self::AccountId, CurrencyId, Amount, Balance)>; + } + + #[pallet::error] + pub enum Error { + AmountConvertFailed, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Position updated. + PositionUpdated { + owner: T::AccountId, + collateral_type: CurrencyId, + collateral_adjustment: Amount, + debit_adjustment: Amount, + }, + /// Confiscate CDP's collateral assets and eliminate its debit. + ConfiscateCollateralAndDebit { + owner: T::AccountId, + collateral_type: CurrencyId, + confiscated_collateral_amount: Balance, + deduct_debit_amount: Balance, + }, + /// Transfer loan. + TransferLoan { + from: T::AccountId, + to: T::AccountId, + currency_id: CurrencyId, + }, + } + + /// The collateralized debit positions, map from + /// Owner -> CollateralType -> Position + /// + /// Positions: double_map CurrencyId, AccountId => Position + #[pallet::storage] + #[pallet::getter(fn positions)] + pub type Positions = + StorageDoubleMap<_, Twox64Concat, CurrencyId, Twox64Concat, T::AccountId, Position, ValueQuery>; + + /// The total collateralized debit positions, map from + /// CollateralType -> Position + /// + /// TotalPositions: CurrencyId => Position + #[pallet::storage] + #[pallet::getter(fn total_positions)] + pub type TotalPositions = StorageMap<_, Twox64Concat, CurrencyId, Position, ValueQuery>; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::call] + impl Pallet {} +} + +impl Pallet { + pub fn account_id() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// confiscate collateral and debit to cdp treasury. + /// + /// Ensured atomic. + #[transactional] + pub fn confiscate_collateral_and_debit( + who: &T::AccountId, + currency_id: CurrencyId, + collateral_confiscate: Balance, + debit_decrease: Balance, + ) -> DispatchResult { + // convert balance type to amount type + let collateral_adjustment = Self::amount_try_from_balance(collateral_confiscate)?; + let debit_adjustment = Self::amount_try_from_balance(debit_decrease)?; + + // transfer collateral to cdp treasury + T::SlickUsdEcdpTreasury::deposit_collateral(&Self::account_id(), currency_id, collateral_confiscate)?; + + // deposit debit to cdp treasury + let bad_debt_value = T::SlickUsdRiskManager::get_debit_value(currency_id, debit_decrease); + T::SlickUsdEcdpTreasury::on_system_debit(bad_debt_value)?; + + // update loan + Self::update_loan( + who, + currency_id, + collateral_adjustment.saturating_neg(), + debit_adjustment.saturating_neg(), + )?; + + Self::deposit_event(Event::ConfiscateCollateralAndDebit { + owner: who.clone(), + collateral_type: currency_id, + confiscated_collateral_amount: collateral_confiscate, + deduct_debit_amount: debit_decrease, + }); + Ok(()) + } + + /// adjust the position. + /// + /// Ensured atomic. + #[transactional] + pub fn adjust_position( + who: &T::AccountId, + currency_id: CurrencyId, + collateral_adjustment: Amount, + debit_adjustment: Amount, + ) -> DispatchResult { + // mutate collateral and debit + // Note: if a new position, will inc consumer + Self::update_loan(who, currency_id, collateral_adjustment, debit_adjustment)?; + + let collateral_balance_adjustment = Self::balance_try_from_amount_abs(collateral_adjustment)?; + let debit_balance_adjustment = Self::balance_try_from_amount_abs(debit_adjustment)?; + let module_account = Self::account_id(); + + if collateral_adjustment.is_positive() { + T::Currency::transfer(currency_id, who, &module_account, collateral_balance_adjustment)?; + } else if collateral_adjustment.is_negative() { + T::Currency::transfer(currency_id, &module_account, who, collateral_balance_adjustment)?; + } + + if debit_adjustment.is_positive() { + // check debit cap when increase debit + T::SlickUsdRiskManager::check_debit_cap(currency_id, Self::total_positions(currency_id).debit)?; + + // issue debit with collateral backed by cdp treasury + T::SlickUsdEcdpTreasury::issue_debit( + who, + T::SlickUsdRiskManager::get_debit_value(currency_id, debit_balance_adjustment), + true, + )?; + } else if debit_adjustment.is_negative() { + // repay debit + // burn debit by cdp treasury + T::SlickUsdEcdpTreasury::burn_debit( + who, + T::SlickUsdRiskManager::get_debit_value(currency_id, debit_balance_adjustment), + )?; + } + + // ensure pass risk check + let Position { collateral, debit } = Self::positions(currency_id, who); + T::SlickUsdRiskManager::check_position_valid( + currency_id, + collateral, + debit, + collateral_adjustment.is_negative() || debit_adjustment.is_positive(), + )?; + + Ok(()) + } + + /// transfer whole loan of `from` to `to` + pub fn transfer_loan(from: &T::AccountId, to: &T::AccountId, currency_id: CurrencyId) -> DispatchResult { + // get `from` position data + let Position { collateral, debit } = Self::positions(currency_id, from); + + let Position { + collateral: to_collateral, + debit: to_debit, + } = Self::positions(currency_id, to); + let new_to_collateral_balance = to_collateral + .checked_add(collateral) + .expect("existing collateral balance cannot overflow; qed"); + let new_to_debit_balance = to_debit + .checked_add(debit) + .expect("existing debit balance cannot overflow; qed"); + + // check new position + T::SlickUsdRiskManager::check_position_valid(currency_id, new_to_collateral_balance, new_to_debit_balance, true)?; + + // balance -> amount + let collateral_adjustment = Self::amount_try_from_balance(collateral)?; + let debit_adjustment = Self::amount_try_from_balance(debit)?; + + Self::update_loan( + from, + currency_id, + collateral_adjustment.saturating_neg(), + debit_adjustment.saturating_neg(), + )?; + Self::update_loan(to, currency_id, collateral_adjustment, debit_adjustment)?; + + Self::deposit_event(Event::TransferLoan { + from: from.clone(), + to: to.clone(), + currency_id, + }); + Ok(()) + } + + /// mutate records of collaterals and debits + pub fn update_loan( + who: &T::AccountId, + currency_id: CurrencyId, + collateral_adjustment: Amount, + debit_adjustment: Amount, + ) -> DispatchResult { + let collateral_balance = Self::balance_try_from_amount_abs(collateral_adjustment)?; + let debit_balance = Self::balance_try_from_amount_abs(debit_adjustment)?; + + >::try_mutate_exists(currency_id, who, |may_be_position| -> DispatchResult { + let mut p = may_be_position.take().unwrap_or_default(); + let new_collateral = if collateral_adjustment.is_positive() { + p.collateral + .checked_add(collateral_balance) + .ok_or(ArithmeticError::Overflow) + } else { + p.collateral + .checked_sub(collateral_balance) + .ok_or(ArithmeticError::Underflow) + }?; + let new_debit = if debit_adjustment.is_positive() { + p.debit.checked_add(debit_balance).ok_or(ArithmeticError::Overflow) + } else { + p.debit.checked_sub(debit_balance).ok_or(ArithmeticError::Underflow) + }?; + + // increase account ref if new position + if p.collateral.is_zero() && p.debit.is_zero() { + if frame_system::Pallet::::inc_consumers(who).is_err() { + // No providers for the locks. This is impossible under normal circumstances + // since the funds that are under the lock will themselves be stored in the + // account and therefore will need a reference. + log::warn!( + "Warning: Attempt to introduce lock consumer reference, yet no providers. \ + This is unexpected but should be safe." + ); + } + } + + // TODO:[src/lib.rs:0] - Remove this from this module and add it to `EcdpUssdLoans` module. + // Use the collateral amount (of a position that has fulfilled the `PositionCloudCreditRequirements` - + // this is only offered for SETR) as the shares for Cloud Credit. + // + // T::OnUpdateLoan::happened(&(who.clone(), currency_id, collateral_adjustment, p.collateral)); + + p.collateral = new_collateral; + p.debit = new_debit; + + if p.collateral.is_zero() && p.debit.is_zero() { + // decrease account ref if zero position + frame_system::Pallet::::dec_consumers(who); + + // remove position storage if zero position + *may_be_position = None; + } else { + *may_be_position = Some(p); + } + + Ok(()) + })?; + + TotalPositions::::try_mutate(currency_id, |total_positions| -> DispatchResult { + total_positions.collateral = if collateral_adjustment.is_positive() { + total_positions + .collateral + .checked_add(collateral_balance) + .ok_or(ArithmeticError::Overflow) + } else { + total_positions + .collateral + .checked_sub(collateral_balance) + .ok_or(ArithmeticError::Underflow) + }?; + + total_positions.debit = if debit_adjustment.is_positive() { + total_positions + .debit + .checked_add(debit_balance) + .ok_or(ArithmeticError::Overflow) + } else { + total_positions + .debit + .checked_sub(debit_balance) + .ok_or(ArithmeticError::Underflow) + }?; + + Ok(()) + })?; + + Self::deposit_event(Event::PositionUpdated { + owner: who.clone(), + collateral_type: currency_id, + collateral_adjustment, + debit_adjustment, + }); + Ok(()) + } +} + +impl Pallet { + /// Convert `Balance` to `Amount`. + pub fn amount_try_from_balance(b: Balance) -> Result> { + TryInto::::try_into(b).map_err(|_| Error::::AmountConvertFailed) + } + + /// Convert the absolute value of `Amount` to `Balance`. + pub fn balance_try_from_amount_abs(a: Amount) -> Result> { + TryInto::::try_into(a.saturating_abs()).map_err(|_| Error::::AmountConvertFailed) + } +} diff --git a/blockchain/modules/ecdp-ussd-loans/src/mock.rs b/blockchain/modules/ecdp-ussd-loans/src/mock.rs new file mode 100644 index 00000000..512bb0a9 --- /dev/null +++ b/blockchain/modules/ecdp-ussd-loans/src/mock.rs @@ -0,0 +1,285 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Mocks for the ecdp_ussd_loans module. + +#![cfg(test)] + +use super::*; +use frame_support::{ + construct_runtime, derive_impl, ord_parameter_types, parameter_types, + traits::{ConstU128, ConstU32, Nothing}, + PalletId, +}; +use frame_system::EnsureSignedBy; +use module_support::{mocks::MockStableAsset, AuctionManager, SlickUsdRiskManager, SpecificJointsSwap}; +use orml_traits::parameter_type_with_key; +use primitives::TokenSymbol; +use sp_runtime::{ + traits::{AccountIdConversion, IdentityLookup}, + BuildStorage, +}; +use sp_std::cell::RefCell; +use std::collections::HashMap; + +pub type AccountId = u128; +pub type AuctionId = u32; +pub type BlockNumber = u64; + +pub const ALICE: AccountId = 1; +pub const BOB: AccountId = 2; +pub const SEE: CurrencyId = CurrencyId::Token(TokenSymbol::SEE); +pub const USSD: CurrencyId = CurrencyId::Token(TokenSymbol::USSD); +pub const EDF: CurrencyId = CurrencyId::Token(TokenSymbol::EDF); +pub const BTC: CurrencyId = CurrencyId::ForeignAsset(255); + +mod ecdp_ussd_loans { + pub use super::super::*; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + 100 + }; +} + +impl orml_tokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = Nothing; +} + +impl pallet_balances::Config for Runtime { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = frame_system::Pallet; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); +} + +parameter_types! { + pub const GetNativeCurrencyId: CurrencyId = SEE; +} + +impl orml_currencies::Config for Runtime { + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; + type WeightInfo = (); +} +pub type AdaptedBasicCurrency = orml_currencies::BasicCurrencyAdapter; + +pub struct MockAuctionManager; +impl AuctionManager for MockAuctionManager { + type CurrencyId = CurrencyId; + type Balance = Balance; + type AuctionId = AuctionId; + + fn new_collateral_auction( + _refund_recipient: &AccountId, + _currency_id: Self::CurrencyId, + _amount: Self::Balance, + _target: Self::Balance, + ) -> DispatchResult { + Ok(()) + } + + fn cancel_auction(_id: Self::AuctionId) -> DispatchResult { + Ok(()) + } + + fn get_total_target_in_auction() -> Self::Balance { + Default::default() + } + + fn get_total_collateral_in_auction(_id: Self::CurrencyId) -> Self::Balance { + Default::default() + } +} + +ord_parameter_types! { + pub const One: AccountId = 1; +} + +parameter_types! { + pub const GetStableCurrencyId: CurrencyId = USSD; + pub const SlickUsdEcdpTreasuryPalletId: PalletId = PalletId(*b"aca/cdpt"); + pub TreasuryAccount: AccountId = PalletId(*b"aca/hztr").into_account_truncating(); + pub AlternativeSwapPathJointList: Vec> = vec![]; +} + +impl module_ussd_ecdp_treasury::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Currencies; + type GetStableCurrencyId = GetStableCurrencyId; + type AuctionManagerHandler = MockAuctionManager; + type UpdateOrigin = EnsureSignedBy; + type DEX = (); + type Swap = SpecificJointsSwap<(), AlternativeSwapPathJointList>; + type MaxAuctionsCount = ConstU32<10_000>; + type PalletId = SlickUsdEcdpTreasuryPalletId; + type TreasuryAccount = TreasuryAccount; + type WeightInfo = (); + type StableAsset = MockStableAsset; +} + +// mock risk manager +pub struct MockSlickUsdRiskManager; +impl SlickUsdRiskManager for MockSlickUsdRiskManager { + fn get_debit_value(_currency_id: CurrencyId, debit_balance: Balance) -> Balance { + debit_balance / Balance::from(2u64) + } + + fn check_position_valid( + currency_id: CurrencyId, + _collateral_balance: Balance, + _debit_balance: Balance, + check_required_ratio: bool, + ) -> DispatchResult { + match currency_id { + EDF => { + if check_required_ratio { + Err(sp_runtime::DispatchError::Other( + "mock below required collateral ratio error", + )) + } else { + Err(sp_runtime::DispatchError::Other("mock below liquidation ratio error")) + } + } + BTC => Ok(()), + _ => Err(sp_runtime::DispatchError::Other("mock below liquidation ratio error")), + } + } + + fn check_debit_cap(currency_id: CurrencyId, total_debit_balance: Balance) -> DispatchResult { + match (currency_id, total_debit_balance) { + (EDF, 1000) => Err(sp_runtime::DispatchError::Other("mock exceed debit value cap error")), + (BTC, 1000) => Err(sp_runtime::DispatchError::Other("mock exceed debit value cap error")), + (_, _) => Ok(()), + } + } +} + +thread_local! { + pub static EDF_SHARES: RefCell> = RefCell::new(HashMap::new()); +} + +// Remove it based on `TODO:[src/lib.rs:0]`. +// pub struct MockOnUpdateLoan; +// impl Happened<(AccountId, CurrencyId, Amount, Balance)> for MockOnUpdateLoan { +// fn happened(info: &(AccountId, CurrencyId, Amount, Balance)) { +// let (who, currency_id, adjustment, previous_amount) = info; +// let adjustment_abs = TryInto::::try_into(adjustment.saturating_abs()).unwrap_or_default(); +// let new_share_amount = if adjustment.is_positive() { +// previous_amount.saturating_add(adjustment_abs) +// } else { +// previous_amount.saturating_sub(adjustment_abs) +// }; + +// if *currency_id == EDF { +// EDF_SHARES.with(|v| { +// let mut old_map = v.borrow().clone(); +// old_map.insert(*who, new_share_amount); +// *v.borrow_mut() = old_map; +// }); +// } +// } +// } + +parameter_types! { + pub const EcdpUssdLoansPalletId: PalletId = PalletId(*b"aca/loan"); +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Currencies; + type SlickUsdRiskManager = MockSlickUsdRiskManager; + type SlickUsdEcdpTreasury = SlickUsdEcdpTreasuryModule; + type PalletId = EcdpUssdLoansPalletId; +} + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + EcdpUssdLoansModule: ecdp_ussd_loans, + Tokens: orml_tokens, + PalletBalances: pallet_balances, + Currencies: orml_currencies, + SlickUsdEcdpTreasuryModule: module_ussd_ecdp_treasury, + } +); + +pub struct ExtBuilder { + balances: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + balances: vec![ + (ALICE, EDF, 1000), + (ALICE, BTC, 1000), + (BOB, EDF, 1000), + (BOB, BTC, 1000), + ], + } + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + orml_tokens::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .unwrap(); + t.into() + } +} diff --git a/blockchain/modules/ecdp-ussd-loans/src/tests.rs b/blockchain/modules/ecdp-ussd-loans/src/tests.rs new file mode 100644 index 00000000..848cc256 --- /dev/null +++ b/blockchain/modules/ecdp-ussd-loans/src/tests.rs @@ -0,0 +1,238 @@ +// بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم + +// This file is part of Setheum. + +// Copyright (C) 2019-Present Setheum Labs. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Unit tests for the ecdp_ussd_loans module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::{RuntimeEvent, *}; + +#[test] +fn debits_key() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 0); + assert_ok!(EcdpUssdLoansModule::adjust_position(&ALICE, BTC, 200, 200)); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 200); + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 200); + assert_ok!(EcdpUssdLoansModule::adjust_position(&ALICE, BTC, -100, -100)); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 100); + }); +} + +#[test] +fn check_update_loan_underflow_work() { + ExtBuilder::default().build().execute_with(|| { + // collateral underflow + assert_noop!( + EcdpUssdLoansModule::update_loan(&ALICE, BTC, -100, 0), + ArithmeticError::Underflow, + ); + + // debit underflow + assert_noop!( + EcdpUssdLoansModule::update_loan(&ALICE, BTC, 0, -100), + ArithmeticError::Underflow, + ); + }); +} + +#[test] +fn adjust_position_should_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_eq!(Currencies::free_balance(BTC, &ALICE), 1000); + + // balance too low + assert_noop!( + EcdpUssdLoansModule::adjust_position(&ALICE, BTC, 2000, 0), + orml_tokens::Error::::BalanceTooLow + ); + + // mock can't pass liquidation ratio check + assert_noop!( + EcdpUssdLoansModule::adjust_position(&ALICE, EDF, 500, 0), + sp_runtime::DispatchError::Other("mock below liquidation ratio error") + ); + + // mock can't pass required ratio check + assert_noop!( + EcdpUssdLoansModule::adjust_position(&ALICE, EDF, 500, 1), + sp_runtime::DispatchError::Other("mock below required collateral ratio error") + ); + + // mock exceed debit value cap + assert_noop!( + EcdpUssdLoansModule::adjust_position(&ALICE, BTC, 1000, 1000), + sp_runtime::DispatchError::Other("mock exceed debit value cap error") + ); + + // failed because ED of collateral + assert_noop!( + EcdpUssdLoansModule::adjust_position(&ALICE, BTC, 99, 0), + orml_tokens::Error::::ExistentialDeposit, + ); + + assert_eq!(Currencies::free_balance(BTC, &ALICE), 1000); + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 0); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).debit, 0); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).collateral, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 0); + assert_eq!(Currencies::free_balance(USSD, &ALICE), 0); + + // success + assert_ok!(EcdpUssdLoansModule::adjust_position(&ALICE, BTC, 500, 300)); + assert_eq!(Currencies::free_balance(BTC, &ALICE), 500); + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 500); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).debit, 300); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).collateral, 500); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 300); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 500); + assert_eq!(Currencies::free_balance(USSD, &ALICE), 150); + System::assert_has_event(RuntimeEvent::EcdpUssdLoansModule(crate::Event::PositionUpdated { + owner: ALICE, + collateral_type: BTC, + collateral_adjustment: 500, + debit_adjustment: 300, + })); + + // collateral_adjustment is negatives + assert_eq!(Currencies::total_balance(BTC, &EcdpUssdLoansModule::account_id()), 500); + assert_ok!(EcdpUssdLoansModule::adjust_position(&ALICE, BTC, -500, 0)); + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 0); + }); +} + +#[test] +fn update_loan_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 0); + assert_eq!(Currencies::free_balance(BTC, &ALICE), 1000); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).debit, 0); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).collateral, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 0); + assert!(!>::contains_key(BTC, &ALICE)); + + let alice_ref_count_0 = System::consumers(&ALICE); + + assert_ok!(EcdpUssdLoansModule::update_loan(&ALICE, BTC, 3000, 2000)); + + // just update records + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).debit, 2000); + assert_eq!(EcdpUssdLoansModule::total_positions(BTC).collateral, 3000); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 2000); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 3000); + + // increase ref count when open new position + let alice_ref_count_1 = System::consumers(&ALICE); + assert_eq!(alice_ref_count_1, alice_ref_count_0 + 1); + + // dot not manipulate balance + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 0); + assert_eq!(Currencies::free_balance(BTC, &ALICE), 1000); + + // should remove position storage if zero + assert!(>::contains_key(BTC, &ALICE)); + assert_ok!(EcdpUssdLoansModule::update_loan(&ALICE, BTC, -3000, -2000)); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 0); + assert!(!>::contains_key(BTC, &ALICE)); + + // decrease ref count after remove position + let alice_ref_count_2 = System::consumers(&ALICE); + assert_eq!(alice_ref_count_2, alice_ref_count_1 - 1); + }); +} + +#[test] +fn transfer_loan_should_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_ok!(EcdpUssdLoansModule::update_loan(&ALICE, BTC, 400, 500)); + assert_ok!(EcdpUssdLoansModule::update_loan(&BOB, BTC, 100, 600)); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 500); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 400); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &BOB).debit, 600); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &BOB).collateral, 100); + + assert_ok!(EcdpUssdLoansModule::transfer_loan(&ALICE, &BOB, BTC)); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &BOB).debit, 1100); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &BOB).collateral, 500); + System::assert_last_event(RuntimeEvent::EcdpUssdLoansModule(crate::Event::TransferLoan { + from: ALICE, + to: BOB, + currency_id: BTC, + })); + }); +} + +#[test] +fn confiscate_collateral_and_debit_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_ok!(EcdpUssdLoansModule::update_loan(&BOB, BTC, 5000, 1000)); + assert_eq!(Currencies::free_balance(BTC, &EcdpUssdLoansModule::account_id()), 0); + + // have no sufficient balance + assert_noop!( + EcdpUssdLoansModule::confiscate_collateral_and_debit(&BOB, BTC, 5000, 1000), + orml_tokens::Error::::BalanceTooLow + ); + + assert_ok!(EcdpUssdLoansModule::adjust_position(&ALICE, BTC, 500, 300)); + assert_eq!(SlickUsdEcdpTreasuryModule::get_total_collaterals(BTC), 0); + assert_eq!(SlickUsdEcdpTreasuryModule::debit_pool(), 0); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 300); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 500); + + assert_ok!(EcdpUssdLoansModule::confiscate_collateral_and_debit(&ALICE, BTC, 300, 200)); + assert_eq!(SlickUsdEcdpTreasuryModule::get_total_collaterals(BTC), 300); + assert_eq!(SlickUsdEcdpTreasuryModule::debit_pool(), 100); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).debit, 100); + assert_eq!(EcdpUssdLoansModule::positions(BTC, &ALICE).collateral, 200); + System::assert_last_event(RuntimeEvent::EcdpUssdLoansModule(crate::Event::ConfiscateCollateralAndDebit { + owner: ALICE, + collateral_type: BTC, + confiscated_collateral_amount: 300, + deduct_debit_amount: 200, + })); + }); +} + +// #[test] +// fn loan_updated_updated_when_adjust_collateral() { +// ExtBuilder::default().build().execute_with(|| { +// assert_eq!(EDF_SHARES.with(|v| *v.borrow().get(&BOB).unwrap_or(&0)), 0); + +// assert_ok!(EcdpUssdLoansModule::update_loan(&BOB, EDF, 1000, 0)); +// assert_eq!(EDF_SHARES.with(|v| *v.borrow().get(&BOB).unwrap_or(&0)), 1000); + +// assert_ok!(EcdpUssdLoansModule::update_loan(&BOB, EDF, 0, 200)); +// assert_eq!(EDF_SHARES.with(|v| *v.borrow().get(&BOB).unwrap_or(&0)), 1000); + +// assert_ok!(EcdpUssdLoansModule::update_loan(&BOB, EDF, -800, 500)); +// assert_eq!(EDF_SHARES.with(|v| *v.borrow().get(&BOB).unwrap_or(&0)), 200); +// }); +// } diff --git a/blockchain/modules/evm/src/lib.rs b/blockchain/modules/evm/src/lib.rs index 056699cb..ce2b4f02 100644 --- a/blockchain/modules/evm/src/lib.rs +++ b/blockchain/modules/evm/src/lib.rs @@ -1319,7 +1319,7 @@ impl Pallet { // /// We extend the principle of this EIP to also prevent `tx.sender` to be the address /// of a precompile. While mainnet Ethereum currently only has stateless precompiles, - /// Setheum EVM+ can have stateful precompiles that can manage funds or + /// Setheum EVM can have stateful precompiles that can manage funds or /// which calls other contracts that expects this precompile address to be trustworthy. fn ensure_eoa(caller: &EvmAddress) -> DispatchResult { if is_system_contract(caller) || Self::is_contract(caller) { diff --git a/blockchain/modules/support/src/ecdp.rs b/blockchain/modules/support/src/ecdp.rs index a73c8bc6..cbef19cb 100644 --- a/blockchain/modules/support/src/ecdp.rs +++ b/blockchain/modules/support/src/ecdp.rs @@ -152,7 +152,7 @@ pub trait SlickUsdEcdpTreasuryExtended: SlickUsdTreasury { fn max_auction() -> u32; } -/// Functionality of SlickUSD ECDP Protocol to be exposed to EVM+. +/// Functionality of SlickUSD ECDP Protocol to be exposed to EVM. pub trait SlickUsdEcdpManager { /// Adjust ECDP loan fn adjust_loan( @@ -172,3 +172,23 @@ pub trait SlickUsdEcdpManager { /// Get exchange rate of debit units to debit value for a currency_id fn get_debit_exchange_rate(currency_id: CurrencyId) -> ExchangeRate; } + +/// Functionality of Setter ECDP Protocol to be exposed to EVM. +pub trait SetterEcdpManager { + /// Adjust ECDP loan + fn adjust_loan( + who: &AccountId, + collateral_adjustment: Amount, + debit_adjustment: Amount, + ) -> DispatchResult; + /// Close ECDP loan using DEX + fn close_loan_by_dex(who: AccountId, max_collateral_amount: Balance) -> DispatchResult; + /// Get open ECDP corresponding to an account and collateral + fn get_position(who: &AccountId) -> ECDPPosition; + /// Get liquidation ratio for collateral + fn get_collateral_parameters() -> Vec; + /// Get current ratio of collateral to debit of open ECDP + fn get_current_collateral_ratio(who: &AccountId) -> Option; + /// Get exchange rate of debit units to debit value for a currency_id + fn get_debit_exchange_rate() -> ExchangeRate; +}