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;
+}